Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
804f1b9
chore: migrate from moment to date-fns for date handling across vario…
ggazzo Feb 27, 2026
ba2ec64
fix: handle null timestamps in ChatTranscript formatting
ggazzo Feb 28, 2026
f8ad5c9
refactor: replace moment.js with Date for conversation analytics methods
ggazzo Feb 28, 2026
a4ef5b6
refactor: update date handling to use native Date and date-fns across…
ggazzo Mar 3, 2026
8d9fb2f
test: standardize date formatting across multiple components to use "…
ggazzo Mar 3, 2026
1404586
fix: reorder import statements in ChatTranscript to improve clarity
ggazzo Mar 3, 2026
b9846c9
refactor: clean up import statements and improve code organization in…
ggazzo Mar 3, 2026
3379e8d
chore: update cron dependency to version 4.4.0 and clean up import st…
ggazzo Mar 3, 2026
905c90f
refactor: enable cron functionality by restoring and organizing impor…
ggazzo Mar 3, 2026
d54d151
fix: resolve lint import order and update RetentionPolicyWarning test
ggazzo Apr 7, 2026
1137b76
fix: guard against invalid dates in engagementDashboard
ggazzo Apr 7, 2026
1e9d59a
fix: convert moment format tokens in pdf-worker formatInTimezone
ggazzo Apr 7, 2026
ce58af2
fix: fallback for undefined dateFormat in pdf transcript generation
ggazzo Apr 7, 2026
4f325ae
fix: correct differenceInDays argument order in engagementDashboard
ggazzo Apr 7, 2026
e9df7ea
fix: handle ISO date strings in parseRangeInTimezone
ggazzo Apr 7, 2026
098a9ff
fix: correct timezone conversion in formatDayOfTheWeekFromServerTimez…
ggazzo Apr 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { LivechatContacts, Statistics, Users } from '@rocket.chat/models';
import moment from 'moment';
import { format } from 'date-fns';

import { settings } from '../../../settings/server';
import { statistics } from '../../../statistics/server';
Expand Down Expand Up @@ -69,7 +69,7 @@ export async function buildWorkspaceRegistrationData<T extends string | undefine
const workspaceType = settings.get<string>('Server_Type');

const seats = await Users.getActiveLocalUserCount();
const MAC = await LivechatContacts.countContactsOnPeriod(moment.utc().format('YYYY-MM'));
const MAC = await LivechatContacts.countContactsOnPeriod(format(new Date(), 'yyyy-MM'));

const license = settings.get<string>('Enterprise_License');

Expand Down
3 changes: 1 addition & 2 deletions apps/meteor/app/irc/server/irc-bridge/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { Logger } from '@rocket.chat/logger';
import { Settings } from '@rocket.chat/models';
import moment from 'moment';
import Queue from 'queue-fifo';

import { withThrottling } from '../../../../lib/utils/highOrderFunctions';
Expand Down Expand Up @@ -60,7 +59,7 @@ class Bridge {

const lastPing = await Settings.findOneById('IRC_Bridge_Last_Ping');
if (lastPing) {
if (Math.abs(moment(lastPing.value).diff()) < 1000 * 30) {
if (Math.abs(new Date(lastPing.value).getTime() - Date.now()) < 1000 * 30) {
this.log('Not trying to connect.');
this.remove();
return;
Expand Down
5 changes: 2 additions & 3 deletions apps/meteor/app/lib/server/lib/notifyUsersOnMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import type { IMessage, IRoom, IUser, RoomType } from '@rocket.chat/core-typings
import { isEditedMessage } from '@rocket.chat/core-typings';
import type { Updater } from '@rocket.chat/models';
import { Subscriptions, Rooms } from '@rocket.chat/models';
import moment from 'moment';

import {
notifyOnSubscriptionChanged,
Expand Down Expand Up @@ -165,7 +164,7 @@ export async function updateThreadUsersSubscriptions(message: IMessage, replies:
export async function notifyUsersOnMessage(message: IMessage, room: IRoom, roomUpdater: Updater<IRoom>): Promise<IMessage> {
// Skips this callback if the message was edited and increments it if the edit was way in the past (aka imported)
if (isEditedMessage(message)) {
if (Math.abs(moment(message.editedAt).diff(Date.now())) > 60000) {
if (Math.abs(new Date(message.editedAt).getTime() - Date.now()) > 60000) {
// TODO: Review as I am not sure how else to get around this as the incrementing of the msgs count shouldn't be in this callback
Rooms.getIncMsgCountUpdateQuery(1, roomUpdater);
return message;
Expand All @@ -183,7 +182,7 @@ export async function notifyUsersOnMessage(message: IMessage, room: IRoom, roomU
return message;
}

if (message.ts && Math.abs(moment(message.ts).diff(Date.now())) > 60000) {
if (message.ts && Math.abs(new Date(message.ts).getTime() - Date.now()) > 60000) {
Rooms.getIncMsgCountUpdateQuery(1, roomUpdater);
return message;
}
Expand Down
3 changes: 1 addition & 2 deletions apps/meteor/app/lib/server/lib/processDirectEmail.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import type { IMessage } from '@rocket.chat/core-typings';
import { Messages, Subscriptions, Users, Rooms } from '@rocket.chat/models';
import type { ParsedMail } from 'mailparser';
import moment from 'moment';

import { canAccessRoomAsync } from '../../../authorization/server';
import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission';
Expand All @@ -25,7 +24,7 @@ export const processDirectEmail = async function (email: ParsedMail): Promise<vo

const ts = new Date(email.date);

const tsDiff = Math.abs(moment(ts).diff(new Date()));
const tsDiff = Math.abs(ts.getTime() - Date.now());

let msg = email.text.split('\n\n').join('\n');

Expand Down
3 changes: 1 addition & 2 deletions apps/meteor/app/lib/server/lib/sendNotificationsOnMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
} from '@rocket.chat/core-typings';
import { Subscriptions, Users } from '@rocket.chat/models';
import emojione from 'emojione';
import moment from 'moment';
import type { RootFilterOperators } from 'mongodb';

import { getMentions } from './notifyUsersOnMessage';
Expand Down Expand Up @@ -397,7 +396,7 @@ export async function sendAllNotifications(message: IMessage, room: IRoom) {
return message;
}

if (message.ts && Math.abs(moment(message.ts).diff(new Date())) > 60000) {
if (message.ts && Math.abs(new Date(message.ts).getTime() - Date.now()) > 60000) {
return message;
}

Expand Down
3 changes: 1 addition & 2 deletions apps/meteor/app/lib/server/methods/sendMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { Messages, Users } from '@rocket.chat/models';
import type { TOptions } from 'i18next';
import { check, Match } from 'meteor/check';
import { Meteor } from 'meteor/meteor';
import moment from 'moment';

import { i18n } from '../../../../server/lib/i18n';
import { SystemLogger } from '../../../../server/lib/logger/system';
Expand Down Expand Up @@ -50,7 +49,7 @@ export async function executeSendMessage(
const now = new Date();
message.ts = extraInfo?.ts ?? message.ts ?? now;
if (isTimestampFromClient) {
const tsDiff = Math.abs(moment(message.ts).diff(Date.now()));
const tsDiff = Math.abs(new Date(message.ts).getTime() - Date.now());
if (tsDiff > 60000) {
throw new Meteor.Error('error-message-ts-out-of-sync', 'Message timestamp is out of sync', {
method: 'sendMessage',
Expand Down
8 changes: 2 additions & 6 deletions apps/meteor/app/lib/server/methods/updateMessage.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import type { IEditedMessage, IMessage, IUser, AtLeast } from '@rocket.chat/core-typings';
import type { ServerMethods } from '@rocket.chat/ddp-client';
import { Messages, Users } from '@rocket.chat/models';
import { differenceInMinutes } from 'date-fns';
import { Match, check } from 'meteor/check';
import { Meteor } from 'meteor/meteor';
import moment from 'moment';

import { canSendMessageAsync } from '../../../authorization/server/functions/canSendMessage';
import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission';
Expand Down Expand Up @@ -65,13 +65,9 @@ export async function executeUpdateMessage(

if (!bypassBlockTimeLimit && Match.test(blockEditInMinutes, Number) && blockEditInMinutes !== 0) {
let currentTsDiff = 0;
let msgTs;

if (originalMessage.ts instanceof Date || Match.test(originalMessage.ts, Number)) {
msgTs = moment(originalMessage.ts);
}
if (msgTs) {
currentTsDiff = moment().diff(msgTs, 'minutes');
currentTsDiff = differenceInMinutes(new Date(), originalMessage.ts);
}
if (currentTsDiff >= blockEditInMinutes) {
throw new Meteor.Error('error-message-editing-blocked', 'Message editing is blocked', {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { isDirectMessageRoom, isEditedMessage, isOmnichannelRoom, isRoomFederate
import { Subscriptions, Users } from '@rocket.chat/models';
import { isTruthy } from '@rocket.chat/tools';
import type { ActionsBlock } from '@rocket.chat/ui-kit';
import moment from 'moment';

import { callbacks } from '../../../../server/lib/callbacks';
import { i18n } from '../../../../server/lib/i18n';
Expand Down Expand Up @@ -57,7 +56,7 @@ callbacks.add(
// TODO: check if I need to test this 60 second rule.
// If the message was edited, or is older than 60 seconds (imported)
// the notifications will be skipped, so we can also skip this validation
if (isEditedMessage(message) || (message.ts && Math.abs(moment(message.ts).diff(moment())) > 60000) || !message.mentions) {
if (isEditedMessage(message) || (message.ts && Math.abs(new Date(message.ts).getTime() - Date.now()) > 60000) || !message.mentions) {
return message;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,30 @@
import { TZDate, tzOffset } from '@date-fns/tz';
import type { AtLeast, ILivechatAgentStatus, ILivechatBusinessHour, ILivechatDepartment } from '@rocket.chat/core-typings';
import { UserStatus } from '@rocket.chat/core-typings';
import type { ILivechatBusinessHoursModel, IUsersModel } from '@rocket.chat/model-typings';
import { LivechatBusinessHours, Users } from '@rocket.chat/models';
import type { IWorkHoursCronJobsWrapper } from '@rocket.chat/models';
import moment from 'moment-timezone';
import { isBefore, isSameSecond } from 'date-fns';
import type { UpdateFilter } from 'mongodb';

import { notifyOnUserChange } from '../../../lib/server/lib/notifyListener';

const DAY_NAMES = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];

export function parseDayTimeInZone(day: string, timeStr: string, timezone: string): Date {
const [h = 0, m = 0] = timeStr.split(':').map(Number);
const dayIndex = DAY_NAMES.indexOf(day);
const refDate = 5 + dayIndex; // 2025-01-05 is Sunday
const tzDate = new TZDate(2025, 0, refDate, h, m, 0, timezone);
return new Date(tzDate.getTime());
}

export function formatUtcDayTime(d: Date): { ddd: string; time: string } {
const day = DAY_NAMES[d.getUTCDay()];
const time = `${String(d.getUTCHours()).padStart(2, '0')}:${String(d.getUTCMinutes()).padStart(2, '0')}`;
return { ddd: day, time };
}

export interface IBusinessHourBehavior {
findHoursToCreateJobs(): Promise<IWorkHoursCronJobsWrapper[]>;
openBusinessHoursByDayAndHour(day: string, hour: string): Promise<void>;
Expand Down Expand Up @@ -91,35 +108,32 @@ export abstract class AbstractBusinessHourType {
}

private convertWorkHours(businessHourData: ILivechatBusinessHour): ILivechatBusinessHour {
const tzName = businessHourData.timezone.name;
businessHourData.workHours.forEach((hour: any) => {
const startUtc = moment.tz(`${hour.day}:${hour.start}`, 'dddd:HH:mm', businessHourData.timezone.name).utc();
const finishUtc = moment.tz(`${hour.day}:${hour.finish}`, 'dddd:HH:mm', businessHourData.timezone.name).utc();
const startUtc = parseDayTimeInZone(hour.day, hour.start, tzName);
const finishUtc = parseDayTimeInZone(hour.day, hour.finish, tzName);

if (hour.open && finishUtc.isBefore(startUtc)) {
if (hour.open && isBefore(finishUtc, startUtc)) {
throw new Error('error-business-hour-finish-time-before-start-time');
}

if (hour.open && startUtc.isSame(finishUtc)) {
if (hour.open && isSameSecond(startUtc, finishUtc)) {
throw new Error('error-business-hour-finish-time-equals-start-time');
}

const startFmt = formatUtcDayTime(startUtc);
const finishFmt = formatUtcDayTime(finishUtc);
hour.start = {
time: hour.start,
utc: {
dayOfWeek: startUtc.clone().format('dddd'),
time: startUtc.clone().format('HH:mm'),
},
utc: { dayOfWeek: startFmt.ddd, time: startFmt.time },
cron: {
dayOfWeek: this.formatDayOfTheWeekFromServerTimezoneAndUtcHour(startUtc, 'dddd'),
time: this.formatDayOfTheWeekFromServerTimezoneAndUtcHour(startUtc, 'HH:mm'),
},
};
hour.finish = {
time: hour.finish,
utc: {
dayOfWeek: finishUtc.clone().format('dddd'),
time: finishUtc.clone().format('HH:mm'),
},
utc: { dayOfWeek: finishFmt.ddd, time: finishFmt.time },
cron: {
dayOfWeek: this.formatDayOfTheWeekFromServerTimezoneAndUtcHour(finishUtc, 'dddd'),
time: this.formatDayOfTheWeekFromServerTimezoneAndUtcHour(finishUtc, 'HH:mm'),
Expand All @@ -130,15 +144,21 @@ export abstract class AbstractBusinessHourType {
}

protected getUTCFromTimezone(timezone?: string): string {
if (!timezone) {
return String(moment().utcOffset() / 60);
}
return moment.tz(timezone).format('Z');
const d = new Date();
const offsetMinutes = timezone ? tzOffset(timezone, d) : -d.getTimezoneOffset();
const sign = offsetMinutes >= 0 ? '+' : '-';
const h = Math.floor(Math.abs(offsetMinutes) / 60);
const m = Math.abs(offsetMinutes) % 60;
return `${sign}${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`;
}

private formatDayOfTheWeekFromServerTimezoneAndUtcHour(utc: any, format: string): string {
return moment(utc.format('dddd:HH:mm'), 'dddd:HH:mm')
.add(moment().utcOffset() / 60, 'hours')
.format(format);
private formatDayOfTheWeekFromServerTimezoneAndUtcHour(utc: Date, fmt: 'dddd' | 'HH:mm'): string {
// Convert UTC date to server local timezone by adding the server's UTC offset
const serverOffsetMs = -new Date().getTimezoneOffset() * 60_000;
const local = new Date(utc.getTime() + serverOffsetMs);
if (fmt === 'dddd') {
return DAY_NAMES[local.getUTCDay()];
}
return `${String(local.getUTCHours()).padStart(2, '0')}:${String(local.getUTCMinutes()).padStart(2, '0')}`;
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { tzOffset } from '@date-fns/tz';
import type { ILivechatBusinessHour, IBusinessHourTimezone } from '@rocket.chat/core-typings';
import { LivechatBusinessHourTypes } from '@rocket.chat/core-typings';
import type { AgendaCronJobs } from '@rocket.chat/cron';
import { LivechatBusinessHours, LivechatDepartment, Users } from '@rocket.chat/models';
import moment from 'moment-timezone';

import type { IBusinessHourBehavior, IBusinessHourType } from './AbstractBusinessHour';
import { closeBusinessHour } from './closeBusinessHour';
Expand Down Expand Up @@ -211,11 +211,13 @@ export class BusinessHourManager {
type: 'open' | 'close',
job: (day: string, hour: string) => void,
): Promise<void> {
const dayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
const dayNum = dayNames.indexOf(day);
await Promise.all(
items.map((hour) => {
const time = moment(hour, 'HH:mm').day(day);
const jobName = `${time.format('dddd')}/${time.format('HH:mm')}/${type}`;
const scheduleAt = `${time.minutes()} ${time.hours()} * * ${time.day()}`;
const [h, m] = hour.split(':').map(Number);
const jobName = `${day}/${hour}/${type}`;
const scheduleAt = `${m ?? 0} ${h ?? 0} * * ${dayNum}`;
this.addToCache(jobName);
return this.cronJobs.add(jobName, scheduleAt, () => job(day, hour));
}),
Expand Down Expand Up @@ -257,12 +259,11 @@ export class BusinessHourManager {
}

hasDaylightSavingTimeChanged(timezone: IBusinessHourTimezone): boolean {
const now = moment().utc().tz(timezone.name);
const currentUTC = now.format('Z');
const existingTimezoneUTC = moment(timezone.utc, 'Z').utc().tz(timezone.name);
const DSTHasChanged = !moment(currentUTC, 'Z').utc().tz(timezone.name).isSame(existingTimezoneUTC);

return currentUTC !== timezone.utc && DSTHasChanged;
const now = new Date();
const currentOffsetMin = tzOffset(timezone.name, now);
const currentSign = currentOffsetMin >= 0 ? '+' : '-';
const currentStr = `${currentSign}${String(Math.floor(Math.abs(currentOffsetMin) / 60)).padStart(2, '0')}:${String(Math.abs(currentOffsetMin) % 60).padStart(2, '0')}`;
return currentStr !== timezone.utc;
}

async registerDaylightSavingTimeCronJob(): Promise<void> {
Expand Down
4 changes: 2 additions & 2 deletions apps/meteor/app/livechat/server/business-hour/Default.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { ILivechatBusinessHour } from '@rocket.chat/core-typings';
import { LivechatBusinessHourTypes } from '@rocket.chat/core-typings';
import moment from 'moment-timezone';
import { guessTimezone } from '@rocket.chat/tools';

import type { IBusinessHourType } from './AbstractBusinessHour';
import { AbstractBusinessHourType } from './AbstractBusinessHour';
Expand All @@ -23,7 +23,7 @@ export class DefaultBusinessHour extends AbstractBusinessHourType implements IBu
return businessHourData;
}
businessHourData.timezone = {
name: timezoneName || moment.tz.guess(),
name: timezoneName || guessTimezone(),
utc: this.getUTCFromTimezone(timezoneName),
};
await this.baseSaveBusinessHour(businessHourData);
Expand Down
4 changes: 1 addition & 3 deletions apps/meteor/app/livechat/server/business-hour/Helper.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import type { ILivechatBusinessHour } from '@rocket.chat/core-typings';
import { ILivechatAgentStatus, LivechatBusinessHourTypes } from '@rocket.chat/core-typings';
import { LivechatBusinessHours, Users } from '@rocket.chat/models';
import moment from 'moment';

import { createDefaultBusinessHourRow } from './LivechatBusinessHours';
import { filterBusinessHoursThatMustBeOpened } from './filterBusinessHoursThatMustBeOpened';
Expand All @@ -24,8 +23,7 @@ export const filterBusinessHoursThatMustBeOpenedByDay = async (
export const openBusinessHourDefault = async (): Promise<void> => {
try {
await Users.removeBusinessHoursFromAllUsers();
const currentTime = moment(moment().format('dddd:HH:mm'), 'dddd:HH:mm');
const day = currentTime.format('dddd');
const day = new Date().toLocaleString('en-US', { weekday: 'long' });
const activeBusinessHours = await LivechatBusinessHours.findDefaultActiveAndOpenBusinessHoursByDay(day, {
projection: {
workHours: 1,
Expand Down
Loading
Loading