Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 7 additions & 0 deletions .changeset/ninety-lilies-kick.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@rocket.chat/gazzodown': patch
'@rocket.chat/i18n': patch
'@rocket.chat/meteor': patch
---

Fixes Insert Timestamp relative time preview not updating on input changes and losing the user's locale after the first refresh tick.
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,11 @@ export const TimestampPickerModal = ({ onClose, composer }: TimestampPickerProps
confirmDisabled={!isValid}
>
<Box display='flex' flexDirection='column'>
<Preview date={currentDate} format={currentFormat} timezone={currentTimezone} />
<Controller name='date' control={control} render={({ field }) => <DatePicker {...field} />} />
<Controller name='date' control={control} render={({ field }) => <TimePicker {...field} />} />
<Controller name='format' control={control} render={({ field }) => <FormatSelector {...field} />} />
<Controller name='timezone' control={control} render={({ field }) => <TimezoneSelector {...field} />} />
<Preview date={currentDate} format={currentFormat} timezone={currentTimezone} />
</Box>
</GenericModal>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Box, Field, FieldLabel, FieldRow, Select } from '@rocket.chat/fuselage';
import { Box, Field, FieldHint, FieldDescription, FieldLabel, FieldRow, Select } from '@rocket.chat/fuselage';
import type { ReactElement, Key } from 'react';
import { useTranslation } from 'react-i18next';

Expand Down Expand Up @@ -26,9 +26,11 @@ const TimezoneSelector = ({ value, onChange }: TimezoneSelectorProps): ReactElem
<Box mb='x16'>
<Field>
<FieldLabel>{t('Timezone')}</FieldLabel>
<FieldDescription>{t('Timezone_picker_description')}</FieldDescription>
<FieldRow>
<Select value={value} onChange={handleTimezoneChange} options={options} width='full' />
</FieldRow>
<FieldHint>{t('Timezone_picker_hint')}</FieldHint>
</Field>
</Box>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,28 @@ exports[`Story Tests renders Default without crashing 1`] = `
<div
class="rcx-box rcx-box--full rcx-css-1tbw8nv"
>
<div
class="rcx-box rcx-box--full rcx-css-1bjg207"
>
<div
class="rcx-box rcx-box--full rcx-field"
>
<label
class="rcx-box rcx-box--full rcx-field__label rcx-label"
>
Preview
</label>
<span
class="rcx-box rcx-box--full rcx-field__row"
>
<div
data-testid="mock-gazzodown-text"
>
Mocked GazzodownText
</div>
</span>
</div>
</div>
<div
class="rcx-box rcx-box--full rcx-css-1bjg207"
>
Expand Down Expand Up @@ -274,6 +296,11 @@ exports[`Story Tests renders Default without crashing 1`] = `
>
Timezone
</label>
<span
class="rcx-box rcx-box--full rcx-field__description"
>
Timezone_picker_description
</span>
<span
class="rcx-box rcx-box--full rcx-field__row"
>
Expand Down Expand Up @@ -443,27 +470,10 @@ exports[`Story Tests renders Default without crashing 1`] = `
</i>
</button>
</span>
</div>
</div>
<div
class="rcx-box rcx-box--full rcx-css-1bjg207"
>
<div
class="rcx-box rcx-box--full rcx-field"
>
<label
class="rcx-box rcx-box--full rcx-field__label rcx-label"
>
Preview
</label>
<span
class="rcx-box rcx-box--full rcx-field__row"
class="rcx-box rcx-box--full rcx-field__hint"
>
<div
data-testid="mock-gazzodown-text"
>
Mocked GazzodownText
</div>
Timezone_picker_hint
</span>
</div>
</div>
Expand Down
4 changes: 2 additions & 2 deletions apps/meteor/client/lib/utils/timestamp/formats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,12 @@ export const TIMESTAMP_FORMATS: Record<TimestampFormat, ITimestampFormatConfig>
},
f: {
label: 'timestamps.fullDateTime',
format: 'PPPppp',
format: 'PPPP p',
description: 'timestamps.fullDateTimeDescription',
},
Comment thread
coderabbitai[bot] marked this conversation as resolved.
F: {
label: 'timestamps.fullDateTimeLong',
format: 'PPPPpppp',
format: 'PPPP pp',
description: 'timestamps.fullDateTimeLongDescription',
},
R: {
Expand Down
89 changes: 89 additions & 0 deletions packages/gazzodown/src/elements/Timestamp/RelativeTime.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { render, screen, act } from '@testing-library/react';

import TimestampWrapper from './index';
import { MarkupInteractionContext } from '../../MarkupInteractionContext';

beforeEach(() => {
jest.useFakeTimers();
});

afterEach(() => {
jest.useRealTimers();
});

const makeTimestampNode = (epochSeconds: number) =>
({
type: 'TIMESTAMP' as const,
value: {
timestamp: String(epochSeconds),
format: 'R' as const,
},
}) satisfies Parameters<typeof TimestampWrapper>[0]['children'];

it('should update displayed text immediately when the value prop changes', () => {
const now = new Date('2026-05-12T12:00:00.000Z');
jest.setSystemTime(now);

const futureA = Math.floor(now.getTime() / 1000) + 7200;
const futureB = Math.floor(now.getTime() / 1000) + 86400;

const { rerender } = render(<TimestampWrapper>{makeTimestampNode(futureA)}</TimestampWrapper>);

const tagA = screen.getByText(/2 hours/i);
expect(tagA).toBeInTheDocument();

rerender(<TimestampWrapper>{makeTimestampNode(futureB)}</TimestampWrapper>);

const tagB = screen.getByText(/tomorrow/i);
expect(tagB).toBeInTheDocument();
});

it('should preserve the locale from context after interval refresh', () => {
const now = new Date('2026-05-12T12:00:00.000Z');
jest.setSystemTime(now);

const futureEpoch = Math.floor(now.getTime() / 1000) + 30;

render(
<MarkupInteractionContext.Provider value={{ language: 'de' }}>
<TimestampWrapper>{makeTimestampNode(futureEpoch)}</TimestampWrapper>
</MarkupInteractionContext.Provider>,
);

const initial = screen.getByRole('time');
const initialText = initial.textContent;
expect(initialText).toBeTruthy();

act(() => {
jest.advanceTimersByTime(1500);
});

const afterTick = screen.getByRole('time');
const afterTickText = afterTick.textContent;
expect(afterTickText).toBeTruthy();

expect(afterTickText).not.toMatch(/seconds/i);
});

it('should update displayed text immediately when locale changes via context', () => {
const now = new Date('2026-05-12T12:00:00.000Z');
jest.setSystemTime(now);

const futureEpoch = Math.floor(now.getTime() / 1000) + 30;

const { rerender } = render(
<MarkupInteractionContext.Provider value={{ language: 'en' }}>
<TimestampWrapper>{makeTimestampNode(futureEpoch)}</TimestampWrapper>
</MarkupInteractionContext.Provider>,
);

expect(screen.getByRole('time').textContent).toMatch(/seconds/i);

rerender(
<MarkupInteractionContext.Provider value={{ language: 'de' }}>
<TimestampWrapper>{makeTimestampNode(futureEpoch)}</TimestampWrapper>
</MarkupInteractionContext.Provider>,
);

expect(screen.getByRole('time').textContent).not.toMatch(/seconds/i);
});
26 changes: 16 additions & 10 deletions packages/gazzodown/src/elements/Timestamp/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,10 @@ const ShortDate = ({ value }: { value: Date }) => <Time value={format(value, 'P'
const LongDate = ({ value }: { value: Date }) => <Time value={format(value, 'Pp')} dateTime={value.toISOString()} />;

// eslint-disable-next-line react/no-multi-comp
const FullDate = ({ value }: { value: Date }) => <Time value={format(value, 'PPPppp')} dateTime={value.toISOString()} />;
const FullDate = ({ value }: { value: Date }) => <Time value={format(value, 'PPPP p')} dateTime={value.toISOString()} />;

// eslint-disable-next-line react/no-multi-comp
const FullDateLong = ({ value }: { value: Date }) => <Time value={format(value, 'PPPPpppp')} dateTime={value.toISOString()} />;
const FullDateLong = ({ value }: { value: Date }) => <Time value={format(value, 'PPPP pp')} dateTime={value.toISOString()} />;

// eslint-disable-next-line react/no-multi-comp
const Time = ({ value, dateTime }: { value: string; dateTime: string }) => (
Expand All @@ -77,17 +77,23 @@ const RelativeTime = ({ value }: { value: Date }) => {
const time = value.getTime();

const { language } = useContext(MarkupInteractionContext);
const [text, setTime] = useState(() => intlFormatDistance(time, Date.now(), { locale: language ?? 'en' }));
const [timeToRefresh, setTimeToRefresh] = useState(() => getTimeToRefresh(time));
const locale = language ?? 'en';
const [text, setText] = useState(() => intlFormatDistance(time, Date.now(), { locale }));

useEffect(() => {
const interval = setInterval(() => {
setTime(intlFormatDistance(value.getTime(), Date.now(), { locale: 'en' }));
setTimeToRefresh(getTimeToRefresh(time));
}, timeToRefresh);
setText(intlFormatDistance(time, Date.now(), { locale }));

Comment thread
alfredodelfabro marked this conversation as resolved.
return () => clearInterval(interval);
}, [time, timeToRefresh, value]);
let timeoutId: ReturnType<typeof setTimeout>;

const refresh = () => {
setText(intlFormatDistance(time, Date.now(), { locale }));
timeoutId = setTimeout(refresh, getTimeToRefresh(time));
};

timeoutId = setTimeout(refresh, getTimeToRefresh(time));

return () => clearTimeout(timeoutId);
}, [time, locale]);

return <Time value={text} dateTime={value.toISOString()} />;
};
Expand Down
14 changes: 8 additions & 6 deletions packages/i18n/src/locales/en.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -5380,7 +5380,9 @@
"Timeout_in_miliseconds_hint": "The time in milliseconds to wait for an external service to respond before canceling the request.",
"Timeouts": "Timeouts",
"Timestamp": "Timestamp",
"Timezone": "Timezone",
"Timezone": "Time zone",
"Timezone_picker_description": "Select the time zone for the entered time.",
"Timezone_picker_hint": "Others see timestamps converted to their own time zone.",
"Title": "Title",
"Title_bar_color": "Title bar color",
"Title_bar_color_offline": "Title bar color offline",
Expand Down Expand Up @@ -7209,15 +7211,15 @@
"timestamps.shortTime": "Short time",
"timestamps.longTime": "Long time",
"timestamps.shortDate": "Short date",
"timestamps.longDate": "Long date",
"timestamps.fullDateTime": "Full date and time",
"timestamps.fullDateTimeLong": "Full date and time (long)",
"timestamps.relativeTime": "Relative time",
"timestamps.longDate": "Short date and time",
"timestamps.fullDateTime": "Full date and short time",
"timestamps.fullDateTimeLong": "Full date and long time",
"timestamps.relativeTime": "Relative",
"timestamps.shortTimeDescription": "12:00 AM",
"timestamps.longTimeDescription": "12:00:00 AM",
"timestamps.shortDateDescription": "12/31/2020",
"timestamps.longDateDescription": "12/31/2020, 12:00 AM",
"timestamps.fullDateTimeDescription": "December 31, 2020 12:00 AM",
"timestamps.fullDateTimeDescription": "Thursday, December 31, 2020 12:00 AM",
"timestamps.fullDateTimeLongDescription": "Thursday, December 31, 2020 12:00:00 AM",
"__username__profile_picture": "{{username}}'s profile picture",
"User_card": "User card",
Expand Down
Loading