, 'value' | 'onChange
onChange?: (dateRange: DateRange) => void;
};
+const minDate = (a: Date, b: Date) => (a.getTime() < b.getTime() ? a : b);
+
const DateRangePicker = ({ value, onChange, ...props }: DateRangePickerProps): ReactElement => {
const dispatch = useEffectEvent((action: DateRangeAction): void => {
const newRange = dateRangeReducer(value ?? { start: undefined, end: undefined }, action);
@@ -134,7 +141,7 @@ const DateRangePicker = ({ value, onChange, ...props }: DateRangePickerProps): R
const startDate = useMemo(() => formatToDateInput(value?.start), [value?.start]);
const endDate = useMemo(() => formatToDateInput(value?.end), [value?.end]);
const maxStartDate = useMemo(() => {
- return formatToDateInput(value?.end ? moment.min(moment(value.end), moment()).toDate() : new Date());
+ return formatToDateInput(value?.end ? minDate(value.end, new Date()) : new Date());
}, [value?.end]);
const minEndDate = startDate;
const maxEndDate = useMemo(() => formatToDateInput(new Date()), []);
diff --git a/apps/meteor/client/views/audit/utils/dateRange.ts b/apps/meteor/client/views/audit/utils/dateRange.ts
index 776ceb796ccec..e02a5d1c907c4 100644
--- a/apps/meteor/client/views/audit/utils/dateRange.ts
+++ b/apps/meteor/client/views/audit/utils/dateRange.ts
@@ -1,9 +1,9 @@
-import moment from 'moment';
+import { startOfDay, endOfDay } from 'date-fns';
export type DateRange = {
start?: Date;
end?: Date;
};
-export const createStartOfToday = () => moment().startOf('day').toDate();
-export const createEndOfToday = () => moment().endOf('day').toDate();
+export const createStartOfToday = () => startOfDay(new Date());
+export const createEndOfToday = () => endOfDay(new Date());
diff --git a/apps/meteor/client/views/marketplace/AppDetailsPage/AppDetailsPageHeader.tsx b/apps/meteor/client/views/marketplace/AppDetailsPage/AppDetailsPageHeader.tsx
index f3b1811efb5cf..e725eb15d4c4c 100644
--- a/apps/meteor/client/views/marketplace/AppDetailsPage/AppDetailsPageHeader.tsx
+++ b/apps/meteor/client/views/marketplace/AppDetailsPage/AppDetailsPageHeader.tsx
@@ -1,7 +1,7 @@
import type { App } from '@rocket.chat/core-typings';
import { Box, Tag } from '@rocket.chat/fuselage';
import { AppAvatar } from '@rocket.chat/ui-avatar';
-import moment from 'moment';
+import { formatDistanceToNow } from 'date-fns';
import type { ReactElement } from 'react';
import { useTranslation } from 'react-i18next';
@@ -31,7 +31,7 @@ const AppDetailsPageHeader = ({ app }: { app: App }): ReactElement => {
shortDescription,
} = app;
- const lastUpdated = modifiedAt && moment(modifiedAt).fromNow();
+ const lastUpdated = modifiedAt && formatDistanceToNow(new Date(modifiedAt), { addSuffix: true });
const incompatibleStatus = versionIncompatible ? appIncompatibleStatusProps() : undefined;
return (
diff --git a/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/__snapshots__/AppLogsItem.spec.tsx.snap b/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/__snapshots__/AppLogsItem.spec.tsx.snap
index dad8bcc9c77b2..3f7d31ace3a8c 100644
--- a/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/__snapshots__/AppLogsItem.spec.tsx.snap
+++ b/apps/meteor/client/views/marketplace/AppDetailsPage/tabs/AppLogs/__snapshots__/AppLogsItem.spec.tsx.snap
@@ -81,7 +81,7 @@ exports[`renders AppLogsItem without crashing 1`] = `
>
Time
- June 2, 2025 1:06 PM
+ June 2nd, 2025 1:06 PM
{
},
);
- expect(screen.getByRole('status', { name: 'Invited January 1, 2025' })).toBeInTheDocument();
+ expect(screen.getByRole('status', { name: 'Invited January 1st, 2025' })).toBeInTheDocument();
});
it('should not render InvitationBadge when subscription does not have status INVITED', () => {
diff --git a/apps/meteor/client/views/navigation/sidepanel/SidepanelItem/RoomSidePanelItemBadges.spec.tsx b/apps/meteor/client/views/navigation/sidepanel/SidepanelItem/RoomSidePanelItemBadges.spec.tsx
index 991dd9fa97ab6..b3bf2d901a4ea 100644
--- a/apps/meteor/client/views/navigation/sidepanel/SidepanelItem/RoomSidePanelItemBadges.spec.tsx
+++ b/apps/meteor/client/views/navigation/sidepanel/SidepanelItem/RoomSidePanelItemBadges.spec.tsx
@@ -67,7 +67,7 @@ describe('RoomSidePanelItemBadges', () => {
},
);
- expect(screen.getByRole('status', { name: 'Invited January 1, 2025' })).toBeInTheDocument();
+ expect(screen.getByRole('status', { name: 'Invited January 1st, 2025' })).toBeInTheDocument();
});
it('should not render InvitationBadge when subscription does not have status INVITED', () => {
diff --git a/apps/meteor/client/views/omnichannel/analytics/DateRangePicker.tsx b/apps/meteor/client/views/omnichannel/analytics/DateRangePicker.tsx
index b74940a58f937..746225ee2f44a 100644
--- a/apps/meteor/client/views/omnichannel/analytics/DateRangePicker.tsx
+++ b/apps/meteor/client/views/omnichannel/analytics/DateRangePicker.tsx
@@ -1,35 +1,41 @@
import { Box, InputBox, Field, FieldLabel, FieldRow } from '@rocket.chat/fuselage';
import { useEffectEvent } from '@rocket.chat/fuselage-hooks';
import { GenericMenu } from '@rocket.chat/ui-client';
-import type { Moment } from 'moment';
-import moment from 'moment';
+import { subDays, subMonths, startOfMonth, endOfMonth, format } from 'date-fns';
import type { ComponentProps, FormEvent } from 'react';
import { useState, useMemo, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
-moment.locale('en');
-
type DateRangePickerProps = Omit, 'onChange'> & {
onChange(range: { start: string; end: string }): void;
};
-const formatToDateInput = (date: Moment) => date.format('YYYY-MM-DD');
+const formatToDateInput = (date: Date) => format(date, 'yyyy-MM-dd');
-const todayDate = formatToDateInput(moment());
+const getTodayDate = () => formatToDateInput(new Date());
-const getMonthRange = (monthsToSubtractFromToday: number) => ({
- start: formatToDateInput(moment().subtract(monthsToSubtractFromToday, 'month').date(1)),
- end: formatToDateInput(monthsToSubtractFromToday === 0 ? moment() : moment().subtract(monthsToSubtractFromToday).date(0)),
-});
+const getMonthRange = (monthsToSubtractFromToday: number) => {
+ const now = new Date();
+ const startDate = monthsToSubtractFromToday === 0 ? startOfMonth(now) : startOfMonth(subMonths(now, monthsToSubtractFromToday));
+ const endDate = monthsToSubtractFromToday === 0 ? now : endOfMonth(subMonths(now, monthsToSubtractFromToday));
+ return {
+ start: formatToDateInput(startDate),
+ end: formatToDateInput(endDate),
+ };
+};
-const getWeekRange = (daysToSubtractFromStart: number, daysToSubtractFromEnd: number) => ({
- start: formatToDateInput(moment().subtract(daysToSubtractFromStart, 'day')),
- end: formatToDateInput(moment().subtract(daysToSubtractFromEnd, 'day')),
-});
+const getWeekRange = (daysToSubtractFromStart: number, daysToSubtractFromEnd: number) => {
+ const now = new Date();
+ return {
+ start: formatToDateInput(subDays(now, daysToSubtractFromStart)),
+ end: formatToDateInput(subDays(now, daysToSubtractFromEnd)),
+ };
+};
const DateRangePicker = ({ onChange = () => undefined, ...props }: DateRangePickerProps) => {
const { t } = useTranslation();
- const [range, setRange] = useState({ start: '', end: '' });
+ const todayDate = useMemo(() => getTodayDate(), []);
+ const [range, setRange] = useState({ start: todayDate, end: todayDate });
const { start, end } = range;
@@ -61,7 +67,7 @@ const DateRangePicker = ({ onChange = () => undefined, ...props }: DateRangePick
start: todayDate,
end: todayDate,
});
- }, [handleRange]);
+ }, [handleRange, todayDate]);
const options = useMemo(
() => [
diff --git a/apps/meteor/client/views/omnichannel/components/outboundMessage/components/OutboundMessageWizard/forms/RecipientForm/RecipientForm.spec.tsx b/apps/meteor/client/views/omnichannel/components/outboundMessage/components/OutboundMessageWizard/forms/RecipientForm/RecipientForm.spec.tsx
index 8af2ff2ebd81a..24d49e00c07d5 100644
--- a/apps/meteor/client/views/omnichannel/components/outboundMessage/components/OutboundMessageWizard/forms/RecipientForm/RecipientForm.spec.tsx
+++ b/apps/meteor/client/views/omnichannel/components/outboundMessage/components/OutboundMessageWizard/forms/RecipientForm/RecipientForm.spec.tsx
@@ -253,7 +253,7 @@ describe('RecipientForm', () => {
wrapper: appRoot.build(),
});
- await waitFor(() => expect(screen.getByLabelText('Channel*')).toHaveAccessibleDescription('Last contact a few seconds ago'));
+ await waitFor(() => expect(screen.getByLabelText('Channel*')).toHaveAccessibleDescription('Last contact less than a minute ago'));
});
it('should call onSubmit with correct values when form is submitted', async () => {
diff --git a/apps/meteor/client/views/omnichannel/directory/utils/formatQueuedAt.ts b/apps/meteor/client/views/omnichannel/directory/utils/formatQueuedAt.ts
index a1df53b9ff179..6be14dc2647a9 100644
--- a/apps/meteor/client/views/omnichannel/directory/utils/formatQueuedAt.ts
+++ b/apps/meteor/client/views/omnichannel/directory/utils/formatQueuedAt.ts
@@ -1,23 +1,23 @@
import type { IOmnichannelRoom, Serialized } from '@rocket.chat/core-typings';
-import moment from 'moment';
+import { formatDistance } from 'date-fns';
export const formatQueuedAt = (room: Serialized | undefined) => {
const { servedBy, closedAt, open, queuedAt, ts } = room || {};
const queueStartedAt = queuedAt || ts;
- // Room served
- if (servedBy) {
- return moment(servedBy.ts).from(moment(queueStartedAt), true);
+ // Room served: time from queueStartedAt to servedBy.ts
+ if (servedBy?.ts != null && queueStartedAt != null) {
+ return formatDistance(new Date(servedBy.ts), new Date(queueStartedAt), { addSuffix: false });
}
- // Room open and not served
- if (open) {
- return moment(queueStartedAt).fromNow(true);
+ // Room open and not served: time from queueStartedAt to now
+ if (open && queueStartedAt != null) {
+ return formatDistance(new Date(), new Date(queueStartedAt), { addSuffix: false });
}
- // Room closed and not served
- if (closedAt && queueStartedAt) {
- return moment(closedAt).from(moment(queueStartedAt), true);
+ // Room closed and not served: time from queueStartedAt to closedAt
+ if (closedAt != null && queueStartedAt != null) {
+ return formatDistance(new Date(closedAt), new Date(queueStartedAt), { addSuffix: false });
}
return '';
diff --git a/apps/meteor/client/views/omnichannel/realTimeMonitoring/charts/getMomentChartLabelsAndData.spec.ts b/apps/meteor/client/views/omnichannel/realTimeMonitoring/charts/getMomentChartLabelsAndData.spec.ts
index 57abfb23a0771..baad5ed7c524a 100644
--- a/apps/meteor/client/views/omnichannel/realTimeMonitoring/charts/getMomentChartLabelsAndData.spec.ts
+++ b/apps/meteor/client/views/omnichannel/realTimeMonitoring/charts/getMomentChartLabelsAndData.spec.ts
@@ -1,51 +1,21 @@
-import moment from 'moment-timezone';
-
-import 'moment/locale/fa';
import { getMomentChartLabelsAndData } from './getMomentChartLabelsAndData';
-moment.tz.setDefault('UTC');
-
-describe.each([
- [
- 'en',
- [
- '12AM-1AM',
- '1AM-2AM',
- '2AM-3AM',
- '3AM-4AM',
- '4AM-5AM',
- '5AM-6AM',
- '6AM-7AM',
- '7AM-8AM',
- '8AM-9AM',
- '9AM-10AM',
- '10AM-11AM',
- '11AM-12PM',
- ],
- ],
- /** @see: https://github.com/RocketChat/Rocket.Chat/issues/30191 */
- [
- 'fa',
- [
- '۱۲قبل از ظهر-۱قبل از ظهر',
- '۱قبل از ظهر-۲قبل از ظهر',
- '۲قبل از ظهر-۳قبل از ظهر',
- '۳قبل از ظهر-۴قبل از ظهر',
- '۴قبل از ظهر-۵قبل از ظهر',
- '۵قبل از ظهر-۶قبل از ظهر',
- '۶قبل از ظهر-۷قبل از ظهر',
- '۷قبل از ظهر-۸قبل از ظهر',
- '۸قبل از ظهر-۹قبل از ظهر',
- '۹قبل از ظهر-۱۰قبل از ظهر',
- '۱۰قبل از ظهر-۱۱قبل از ظهر',
- '۱۱قبل از ظهر-۱۲بعد از ظهر',
- ],
- ],
-])(`%p language`, (language, expectedTimingLabels) => {
- beforeEach(() => {
- moment.locale(language);
- });
+const expectedTimingLabels = [
+ '12AM-1AM',
+ '1AM-2AM',
+ '2AM-3AM',
+ '3AM-4AM',
+ '4AM-5AM',
+ '5AM-6AM',
+ '6AM-7AM',
+ '7AM-8AM',
+ '8AM-9AM',
+ '9AM-10AM',
+ '10AM-11AM',
+ '11AM-12PM',
+];
+describe('getMomentChartLabelsAndData', () => {
it('should create timing labels from midnight to noon', () => {
const [timingLabels] = getMomentChartLabelsAndData(12 * 60 * 60 * 1000);
expect(timingLabels).toStrictEqual(expectedTimingLabels);
diff --git a/apps/meteor/client/views/omnichannel/realTimeMonitoring/charts/getMomentChartLabelsAndData.ts b/apps/meteor/client/views/omnichannel/realTimeMonitoring/charts/getMomentChartLabelsAndData.ts
index 6457e05b499d1..e6e1e2f073ac1 100644
--- a/apps/meteor/client/views/omnichannel/realTimeMonitoring/charts/getMomentChartLabelsAndData.ts
+++ b/apps/meteor/client/views/omnichannel/realTimeMonitoring/charts/getMomentChartLabelsAndData.ts
@@ -1,13 +1,16 @@
-import moment from 'moment-timezone';
+import { startOfDay, addHours, format } from 'date-fns';
export const getMomentChartLabelsAndData = (timestamp = Date.now()) => {
- const timingLabels = [];
- const initData = [];
- const today = moment(timestamp).startOf('day');
- for (let m = today; m.diff(moment(timestamp), 'hours') < 0; m.add(1, 'hours')) {
- const n = moment(m).add(1, 'hours');
- timingLabels.push(`${m.format('hA')}-${n.format('hA')}`);
+ const timingLabels: string[] = [];
+ const initData: number[] = [];
+ const now = new Date(timestamp);
+ let m = startOfDay(timestamp);
+
+ while (m < now) {
+ const n = addHours(m, 1);
+ timingLabels.push(`${format(m, 'ha')}-${format(n, 'ha')}`);
initData.push(0);
+ m = n;
}
return [timingLabels, initData] as const;
diff --git a/apps/meteor/client/views/omnichannel/realTimeMonitoring/charts/getMomentCurrentLabel.spec.ts b/apps/meteor/client/views/omnichannel/realTimeMonitoring/charts/getMomentCurrentLabel.spec.ts
index 46d24dfe38460..3a9e3b6cbf825 100644
--- a/apps/meteor/client/views/omnichannel/realTimeMonitoring/charts/getMomentCurrentLabel.spec.ts
+++ b/apps/meteor/client/views/omnichannel/realTimeMonitoring/charts/getMomentCurrentLabel.spec.ts
@@ -1,21 +1,8 @@
-import moment from 'moment-timezone';
-import 'moment/locale/fa';
-
import { getMomentCurrentLabel } from './getMomentCurrentLabel';
-moment.tz.setDefault('UTC');
-
-describe.each([
- ['en', '12PM-1PM'],
- /** @see: https://github.com/RocketChat/Rocket.Chat/issues/30191 */
- ['fa', '۱۲بعد از ظهر-۱بعد از ظهر'],
-])(`%p language`, (language, expectedLabel) => {
- beforeEach(() => {
- moment.locale(language);
- });
-
+describe('getMomentCurrentLabel', () => {
it('should create timing labels from midnight to noon', () => {
const label = getMomentCurrentLabel(12 * 60 * 60 * 1000);
- expect(label).toStrictEqual(expectedLabel);
+ expect(label).toStrictEqual('12PM-1PM');
});
});
diff --git a/apps/meteor/client/views/omnichannel/realTimeMonitoring/charts/getMomentCurrentLabel.ts b/apps/meteor/client/views/omnichannel/realTimeMonitoring/charts/getMomentCurrentLabel.ts
index 209169f553972..94027f1ba6faf 100644
--- a/apps/meteor/client/views/omnichannel/realTimeMonitoring/charts/getMomentCurrentLabel.ts
+++ b/apps/meteor/client/views/omnichannel/realTimeMonitoring/charts/getMomentCurrentLabel.ts
@@ -1,8 +1,8 @@
-import moment from 'moment-timezone';
+import { format, addHours } from 'date-fns';
export const getMomentCurrentLabel = (timestamp = Date.now()) => {
- const m = moment(timestamp);
- const n = moment(m).add(1, 'hours');
+ const m = new Date(timestamp);
+ const n = addHours(m, 1);
- return `${m.format('hA')}-${n.format('hA')}`;
+ return `${format(m, 'ha')}-${format(n, 'ha')}`;
};
diff --git a/apps/meteor/client/views/room/body/RetentionPolicyWarning.spec.tsx b/apps/meteor/client/views/room/body/RetentionPolicyWarning.spec.tsx
index 7f3cdc893f992..f347936958dae 100644
--- a/apps/meteor/client/views/room/body/RetentionPolicyWarning.spec.tsx
+++ b/apps/meteor/client/views/room/body/RetentionPolicyWarning.spec.tsx
@@ -16,7 +16,7 @@ describe('RetentionPolicyWarning', () => {
render(, {
wrapper: createMock({ appliesToChannels: true, TTLChannels: 60000 }),
});
- expect(screen.getByRole('alert')).toHaveTextContent('a minute June 1, 2024 at 12:30 AM');
+ expect(screen.getByRole('alert')).toHaveTextContent('1 minute June 1, 2024 at 12:30 AM');
});
it('Should not render callout if settings are invalid', () => {
diff --git a/apps/meteor/client/views/room/contextualBar/PruneMessages/PruneMessagesWithData.tsx b/apps/meteor/client/views/room/contextualBar/PruneMessages/PruneMessagesWithData.tsx
index a6461d97b2eb1..5235c5e0e9499 100644
--- a/apps/meteor/client/views/room/contextualBar/PruneMessages/PruneMessagesWithData.tsx
+++ b/apps/meteor/client/views/room/contextualBar/PruneMessages/PruneMessagesWithData.tsx
@@ -2,13 +2,13 @@ import { isDirectMessageRoom } from '@rocket.chat/core-typings';
import { useEffectEvent } from '@rocket.chat/fuselage-hooks';
import { GenericModal } from '@rocket.chat/ui-client';
import { useSetModal, useToastMessageDispatch, useEndpoint, useRoomToolbox } from '@rocket.chat/ui-contexts';
-import moment from 'moment';
import type { ReactElement } from 'react';
import { useCallback, useMemo, useState } from 'react';
import { useForm, FormProvider } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import PruneMessages from './PruneMessages';
+import { formatDate } from '../../../../lib/utils/dateFormat';
import { useRoom } from '../../contexts/RoomContext';
const getTimeZoneOffset = (): string => {
@@ -136,7 +136,7 @@ const PruneMessagesWithData = (): ReactElement => {
return (
t('Prune_Warning_between', {
postProcess: 'sprintf',
- sprintf: [filesOrMessages, name, moment(fromDate).format('L LT'), moment(toDate).format('L LT')],
+ sprintf: [filesOrMessages, name, formatDate(fromDate, 'L LT'), formatDate(toDate, 'L LT')],
}) +
exceptPinned +
ifFrom
@@ -147,7 +147,7 @@ const PruneMessagesWithData = (): ReactElement => {
return (
t('Prune_Warning_after', {
postProcess: 'sprintf',
- sprintf: [filesOrMessages, name, moment(fromDate).format('L LT')],
+ sprintf: [filesOrMessages, name, formatDate(fromDate, 'L LT')],
}) +
exceptPinned +
ifFrom
@@ -158,7 +158,7 @@ const PruneMessagesWithData = (): ReactElement => {
return (
t('Prune_Warning_before', {
postProcess: 'sprintf',
- sprintf: [filesOrMessages, name, moment(toDate).format('L LT')],
+ sprintf: [filesOrMessages, name, formatDate(toDate, 'L LT')],
}) +
exceptPinned +
ifFrom
diff --git a/apps/meteor/package.json b/apps/meteor/package.json
index 5c86091690f38..02a3c6499d1a9 100644
--- a/apps/meteor/package.json
+++ b/apps/meteor/package.json
@@ -188,6 +188,7 @@
"cron-validator": "^1.3.1",
"css-vars-ponyfill": "^2.4.9",
"csv-parse": "^5.5.6",
+ "@date-fns/tz": "^1.4.1",
"date-fns": "~4.1.0",
"date.js": "~0.3.3",
"debug": "~4.3.7",
diff --git a/apps/uikit-playground/package.json b/apps/uikit-playground/package.json
index 79090b857bfe4..c0ade9431e9ff 100644
--- a/apps/uikit-playground/package.json
+++ b/apps/uikit-playground/package.json
@@ -30,7 +30,6 @@
"@rocket.chat/ui-contexts": "workspace:~",
"codemirror": "^6.0.2",
"eslint4b-prebuilt": "^6.7.2",
- "moment": "^2.30.1",
"prettier": "~3.3.3",
"rc-scrollbars": "^1.1.6",
"react": "~18.3.1",
diff --git a/apps/uikit-playground/src/utils/formatDate.ts b/apps/uikit-playground/src/utils/formatDate.ts
index efa63607a194e..f772b1b0f9663 100644
--- a/apps/uikit-playground/src/utils/formatDate.ts
+++ b/apps/uikit-playground/src/utils/formatDate.ts
@@ -1,5 +1,20 @@
-import moment from 'moment';
+import { format } from 'date-fns';
+
+const momentToDateFns: Record = {
+ L: 'PP',
+ l: 'P',
+ LL: 'PPP',
+ ll: 'PP',
+ LLL: 'PPP p',
+ lll: 'PP p',
+ LLLL: 'EEEE, PPP p',
+ llll: 'EEE, PP p',
+ LT: 'p',
+ LTS: 'pp',
+};
export const formatDate = (date: string, type = 'll') => {
- return moment(date).format(type);
+ const d = new Date(date);
+ const fmt = momentToDateFns[type] || type;
+ return format(d, fmt);
};
diff --git a/packages/i18n/src/locales/en.i18n.json b/packages/i18n/src/locales/en.i18n.json
index 2881b1432e9b7..07190e692f137 100644
--- a/packages/i18n/src/locales/en.i18n.json
+++ b/packages/i18n/src/locales/en.i18n.json
@@ -5928,6 +5928,7 @@
"Yes_remove_user": "Yes, remove user!",
"Yes_unarchive_it": "Yes, unarchive it!",
"Yesterday": "Yesterday",
+ "Yesterday_at": "Yesterday at",
"You": "You",
"You_and_users_Reacted_with": "You and {{users}} reacted with {{emoji}}",
"You_are_converting_team_to_channel": "You are converting this Team to a Channel.",
diff --git a/packages/tools/package.json b/packages/tools/package.json
index 286be26730976..6519f4ca30e76 100644
--- a/packages/tools/package.json
+++ b/packages/tools/package.json
@@ -17,9 +17,6 @@
"testunit": "jest",
"typecheck": "tsc -p tsconfig.json --noEmit"
},
- "dependencies": {
- "moment-timezone": "^0.5.48"
- },
"devDependencies": {
"@rocket.chat/jest-presets": "workspace:~",
"@rocket.chat/tsconfig": "workspace:*",
diff --git a/packages/tools/src/timezone.ts b/packages/tools/src/timezone.ts
index 2201dc63914e0..d418457d955aa 100644
--- a/packages/tools/src/timezone.ts
+++ b/packages/tools/src/timezone.ts
@@ -1,14 +1,36 @@
-import moment from 'moment-timezone';
+export const guessTimezoneFromOffset = (offset: string | number): string => {
+ const hours = Number(offset);
+ const totalMinutes = Math.round(hours * 60);
-const padOffset = (offset: string | number): string => {
- const numberOffset = Number(offset);
- const absOffset = Math.abs(numberOffset);
- const isNegative = !(numberOffset === absOffset);
+ const supportedZones = Intl.supportedValuesOf('timeZone');
+ const now = new Date();
- return `${isNegative ? '-' : '+'}${absOffset < 10 ? `0${absOffset}` : absOffset}:00`;
-};
+ for (const tz of supportedZones) {
+ // Skip synthetic Etc/ zones — prefer geographic names (DST-aware)
+ if (tz.startsWith('Etc/')) {
+ continue;
+ }
+ const formatter = new Intl.DateTimeFormat('en-US', { timeZone: tz, timeZoneName: 'shortOffset' });
+ const parts = formatter.formatToParts(now);
+ const tzPart = parts.find((p) => p.type === 'timeZoneName')?.value ?? '';
+ // tzPart looks like "GMT", "GMT+5:30", "GMT-3"
+ const match = tzPart.match(/^GMT([+-]\d{1,2}(?::(\d{2}))?)?$/);
+ if (!match) {
+ continue;
+ }
+ const tzHours = match[1] ? parseInt(match[1], 10) : 0;
+ const tzMinutes = match[2] ? parseInt(match[2], 10) * (tzHours < 0 ? -1 : 1) : 0;
+ if (tzHours * 60 + tzMinutes === totalMinutes) {
+ return tz;
+ }
+ }
-export const guessTimezoneFromOffset = (offset: string | number): string =>
- moment.tz.names().find((tz) => padOffset(offset) === moment.tz(tz).format('Z').toString()) || moment.tz.guess();
+ // Fallback to Etc/GMT when no geographic zone matches
+ const intHours = Math.trunc(hours);
+ if (intHours === 0) {
+ return 'Etc/GMT';
+ }
+ return `Etc/GMT${intHours > 0 ? '-' : '+'}${Math.abs(intHours)}`;
+};
-export const guessTimezone = (): string => moment.tz.guess();
+export const guessTimezone = (): string => new Intl.DateTimeFormat().resolvedOptions().timeZone;
diff --git a/packages/tools/tsconfig.json b/packages/tools/tsconfig.json
index e00a45b253fa4..5954cf18b430f 100644
--- a/packages/tools/tsconfig.json
+++ b/packages/tools/tsconfig.json
@@ -3,7 +3,8 @@
"compilerOptions": {
"declaration": true,
"rootDir": "./src",
- "outDir": "./dist"
+ "outDir": "./dist",
+ "lib": ["es2022"]
},
"include": ["./src/**/*"]
}