Skip to content

Commit 6431954

Browse files
authored
feat: remove default timestamp formatting props from DateSeparator, EventComponent, MessageTimestamp (#2442)
BREAKING CHANGE: Removed default values for timestamp formatting props like calendar or format for DateSeparator, EventComponent, MessageTimestamp. The formatting configuration now entirely relies on i18n translations.
1 parent c1651cc commit 6431954

File tree

24 files changed

+547
-223
lines changed

24 files changed

+547
-223
lines changed

docusaurus/docs/React/guides/date-time-formatting.mdx

Lines changed: 24 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ The following components provided by the SDK display datetime:
1818

1919
The datetime format customization can be done on multiple levels:
2020

21-
1. Override the default component prop values
21+
1. Component prop values
2222
2. Supply custom formatting function
2323
3. Format date via i18n
2424

@@ -29,11 +29,11 @@ All the mentioned components accept timestamp formatter props:
2929
```ts
3030
export type TimestampFormatterOptions = {
3131
/* If true, call the `Day.js` calendar function to get the date string to display (e.g. "Yesterday at 3:58 PM"). */
32-
calendar?: boolean | null;
32+
calendar?: boolean;
3333
/* Object specifying date display formats for dates formatted with calendar extension. Active only if calendar prop enabled. */
34-
calendarFormats?: Record<string, string> | null;
34+
calendarFormats?: Record<string, string>;
3535
/* Overrides the default timestamp format if calendar is disabled. */
36-
format?: string | null;
36+
format?: string;
3737
};
3838
```
3939

@@ -54,7 +54,7 @@ If calendar formatting is enabled, the dates are formatted with time-relative wo
5454
If any of the `calendarFormats` keys are missing, then the underlying library will fall back to hard-coded english equivalents
5555
:::
5656

57-
If `calendar` formatting is enabled, the `format` prop would be ignored. So to apply the `format` string, the `calendar` has to be disabled (applies to `DateSeparator` and `MessageTimestamp`.
57+
If `calendar` formatting is enabled, the `format` prop would be ignored. So to apply the `format` string, the `calendar` has to be disabled (applies to `DateSeparator` and `MessageTimestamp`).
5858

5959
All the components can be overridden through `Channel` component context:
6060

@@ -117,44 +117,32 @@ Until now, the datetime values could be customized within the `Channel` componen
117117

118118
The default datetime formatting configuration is stored in the JSON translation files. The default translation keys are namespaced with prefix `timestamp/` followed by the component name. For example, the message date formatting can be targeted via `timestamp/MessageTimestamp`, because the underlying component is called `MessageTimestamp`.
119119

120-
##### Overriding the prop defaults
121-
122-
The default date and time rendering components in the SDK were created with default prop values that override the configuration parameters provided over JSON translations. Therefore, if we wanted to configure the formatting from JSON translation files, we need to nullify the prop defaults first. An example follows:
120+
You can change the default configuration by passing an object to `translationsForLanguage` `Streami18n` option with all or some of the relevant translation keys:
123121

124122
```tsx
125-
import {
126-
DateSeparatorProps,
127-
DateSeparator,
128-
EventComponentProps,
129-
EventComponent,
130-
MessageTimestampProps,
131-
MessageTimestamp,
132-
} from 'stream-chat-react';
133-
134-
const CustomDateSeparator = (props: DateSeparatorProps) => (
135-
<DateSeparator {...props} calendar={null} /> // calendarFormats, neither format have default value
136-
);
123+
import { Chat, Streami18n } from 'stream-chat-react';
137124

138-
const SystemMessage = (props: EventComponentProps) => (
139-
<EventComponent {...props} format={null} /> // calendar neither calendarFormats have default value
140-
);
141-
142-
const CustomMessageTimestamp = (props: MessageTimestampProps) => (
143-
<MessageTimestamp {...props} calendar={null} format={null} /> // calendarFormats do not have default value
144-
);
145-
```
146-
147-
Now we can apply custom configuration in all the translation JSON files. It could look similar to the following key-value pair example.
125+
const i18n = new Streami18n({
126+
language: 'de',
127+
translationsForLanguage: {
128+
'timestamp/DateSeparator': '{{ timestamp | timestampFormatter(calendar: false) }}',
129+
'timestamp/MessageTimestamp':
130+
'{{ timestamp | timestampFormatter(calendar: true; calendarFormats: {"lastDay": "[gestern um] LT", "lastWeek": "[letzten] dddd [um] LT", "nextDay": "[morgen um] LT", "nextWeek": "dddd [um] LT", "sameDay": "[heute um] LT", "sameElse": "L"}) }}',
131+
},
132+
});
148133

149-
```json
150-
{
151-
"timestamp/SystemMessage": "{{ timestamp | timestampFormatter(format: YYYY) }}"
152-
}
134+
const ChatApp = ({ chatClient, children }) => {
135+
return (
136+
<Chat client={chatClient} i18nInstance={i18n}>
137+
{children}
138+
</Chat>
139+
);
140+
};
153141
```
154142

155143
##### Understanding the formatting syntax
156144

157-
Once the default prop values are nullified, we override the default formatting rules in the JSON translation value. We can take a look at an example of German translation for SystemMessage:
145+
Once the default prop values are nullified, we override the default formatting rules. We can take a look at an example of German translation for SystemMessage (below a JSON example - note the escaped quotes):
158146

159147
```
160148
"timestamp/SystemMessage": "{{ timestamp | timestampFormatter(calendar: true; calendarFormats: {\"lastDay\": \"[gestern um] LT\", \"lastWeek\": \"[letzten] dddd [um] LT\", \"nextDay\": \"[morgen um] LT\", \"nextWeek\": \"dddd [um] LT\", \"sameDay\": \"[heute um] LT\", \"sameElse\": \"L\"}) }}",
@@ -166,7 +154,7 @@ Let's dissect the example:
166154
- variable `timestamp` is the name of variable which value will be inserted into the string
167155
- value separator `|` signals the separation between the interpolated value and the formatting function name
168156
- `timestampFormatter` is the name of the formatting function that is used to convert the `timestamp` value into desired format
169-
- the `timestampFormatter` can be passed the same parameters as the React components (`calendar`, `calendarFormats`, `format`) as if the function was called with these values. The values can be simple scalar values as well as objects (note `calendarFormats` should be an object)
157+
- the `timestampFormatter` can be passed the same parameters as the React components (`calendar`, `calendarFormats`, `format`) as if the function was called with these values. The values can be simple scalar values as well as objects (note `calendarFormats` should be an object). The params should be separated by semicolon `;`.
170158

171159
:::note
172160
The described rules follow the formatting rules required by the i18n library used under the hood - `i18next`. You can learn more about the rules in [the formatting section of the `i18next` documentation](https://www.i18next.com/translation-function/formatting#basic-usage).

docusaurus/docs/React/release-guides/upgrade-to-v12.mdx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,21 @@ title: Upgrade to v12
44
keywords: [migration guide, upgrade, v12, breaking changes]
55
---
66

7+
## Date & time formatting
8+
9+
The components that display date and time are:
10+
11+
- `DateSeparator` - separates message groups in message lists
12+
- `EventComponent` - displays system messages
13+
- `MessageTimestamp` - displays the creation timestamp for a message in a message list
14+
15+
These components had previously default values for props like `format` or `calendar`. This setup required for a custom formatting to be set up via i18n service, the default values had to be nullified. For a better developer experience we decided to remove the default prop values and rely on default configuration provided via i18n translations. The value `null` is not a valid value for `format`, `calendar` or `calendarFormats` props.
16+
17+
:::important
18+
**Action required**<br/>
19+
If you are not using the default translations provided with the SDK, make sure to follow the [date & time formatting guide](../guides/date-time-formatting) to verify that your dates are formatted according to your needs.
20+
:::
21+
722
## Avatar changes
823

924
The `Avatar` styles are applied through CSS from the version 12 upwards. Therefore, the following changes were applied:

src/components/DateSeparator/DateSeparator.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export type DateSeparatorProps = TimestampFormatterOptions & {
1616

1717
const UnMemoizedDateSeparator = (props: DateSeparatorProps) => {
1818
const {
19-
calendar = true,
19+
calendar,
2020
date: messageCreatedAt,
2121
formatDate,
2222
position = 'right',

src/components/DateSeparator/__tests__/DateSeparator.test.js

Lines changed: 149 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -2,76 +2,182 @@ import React from 'react';
22
import renderer from 'react-test-renderer';
33
import Dayjs from 'dayjs';
44
import calendar from 'dayjs/plugin/calendar';
5-
import { cleanup, render, screen } from '@testing-library/react';
5+
import { act, cleanup, render, screen } from '@testing-library/react';
66
import '@testing-library/jest-dom';
77

8+
import { Chat } from '../../Chat';
89
import { DateSeparator } from '../DateSeparator';
9-
import { TranslationContext } from '../../../context';
10+
import { getTestClient } from '../../../mock-builders';
11+
import { Streami18n } from '../../../i18n';
1012

1113
Dayjs.extend(calendar);
1214

1315
afterEach(cleanup); // eslint-disable-line
1416

15-
const now = new Date('2020-03-30T22:57:47.173Z');
16-
17-
const withContext = (props) => {
18-
const t = jest.fn((key) => key);
19-
const tDateTimeParser = jest.fn((input) => Dayjs(input));
20-
const Component = (
21-
<TranslationContext.Provider value={{ t, tDateTimeParser }}>
22-
<DateSeparator {...props} />
23-
</TranslationContext.Provider>
24-
);
25-
26-
return { Component, t, tDateTimeParser };
17+
const DATE_SEPARATOR_TEST_ID = 'date-separator';
18+
const dateMock = 'the date';
19+
const date = new Date('2020-03-30T22:57:47.173Z');
20+
const formatDate = () => dateMock;
21+
22+
const renderComponent = async ({ chatProps, props }) => {
23+
let result;
24+
await act(() => {
25+
result = render(
26+
<Chat client={getTestClient()} {...chatProps}>
27+
<DateSeparator {...props} />
28+
</Chat>,
29+
);
30+
});
31+
return result;
2732
};
2833

2934
describe('DateSeparator', () => {
30-
it('should use formatDate if it is provided', () => {
31-
const { queryByText } = render(<DateSeparator date={now} formatDate={() => 'the date'} />);
35+
it('should use the default formatting with calendar', async () => {
36+
await renderComponent({ props: { date } });
37+
expect(screen.queryByText(Dayjs(date.toISOString()).calendar())).toBeInTheDocument();
38+
});
3239

40+
it('should apply custom formatting options from i18n service', async () => {
41+
await renderComponent({
42+
chatProps: {
43+
i18nInstance: new Streami18n({
44+
translationsForLanguage: {
45+
'timestamp/DateSeparator':
46+
'{{ timestamp | timestampFormatter(calendar: false, format: "YYYY") }}',
47+
},
48+
}),
49+
},
50+
props: { date },
51+
});
52+
expect(screen.queryByTestId(DATE_SEPARATOR_TEST_ID)).toHaveTextContent(
53+
date.getFullYear().toString(),
54+
);
55+
});
56+
57+
it('should combine default formatting options from 18n service with those passed through props', async () => {
58+
await renderComponent({
59+
props: {
60+
calendarFormats: {
61+
lastDay: 'A YYYY',
62+
lastWeek: 'B YYYY',
63+
nextDay: 'C YYYY',
64+
nextWeek: 'D YYYY',
65+
sameDay: 'E YYYY',
66+
sameElse: 'F YYYY',
67+
},
68+
date,
69+
},
70+
});
71+
expect(screen.queryByTestId(DATE_SEPARATOR_TEST_ID)).toHaveTextContent(
72+
`F ${date.getFullYear().toString()}`,
73+
);
74+
});
75+
76+
it('ignores calendarFormats if calendar is not enabled', async () => {
77+
await renderComponent({
78+
chatProps: {
79+
i18nInstance: new Streami18n({
80+
translationsForLanguage: {
81+
'timestamp/DateSeparator':
82+
'{{ timestamp | timestampFormatter(calendar: false, format: "YYYY") }}',
83+
},
84+
}),
85+
},
86+
props: {
87+
calendarFormats: {
88+
lastDay: 'A YYYY',
89+
lastWeek: 'B YYYY',
90+
nextDay: 'C YYYY',
91+
nextWeek: 'D YYYY',
92+
sameDay: 'E YYYY',
93+
sameElse: 'F YYYY',
94+
},
95+
date,
96+
},
97+
});
98+
99+
expect(screen.queryByTestId(DATE_SEPARATOR_TEST_ID)).toHaveTextContent(
100+
date.getFullYear().toString(),
101+
);
102+
});
103+
104+
it('should combine custom formatting options from i18n service with those passed through props', async () => {
105+
await renderComponent({
106+
chatProps: {
107+
i18nInstance: new Streami18n({
108+
translationsForLanguage: {
109+
'timestamp/DateSeparator': '{{ timestamp | timestampFormatter(calendar: false) }}',
110+
},
111+
}),
112+
},
113+
props: { date, format: 'YYYY' },
114+
});
115+
expect(screen.queryByTestId(DATE_SEPARATOR_TEST_ID)).toHaveTextContent(
116+
date.getFullYear().toString(),
117+
);
118+
});
119+
120+
it('should format date with formatDate instead of defaults provided with i18n service', async () => {
121+
const { queryByText } = await renderComponent({
122+
props: { date, formatDate },
123+
});
33124
expect(queryByText('the date')).toBeInTheDocument();
34125
});
35126

36-
it('should render New text if unread prop is true', () => {
37-
const { Component, t } = withContext({ date: now, unread: true });
38-
render(Component);
127+
it('should format date with formatDate instead of customs provided with i18n service', async () => {
128+
await renderComponent({
129+
chatProps: {
130+
i18nInstance: new Streami18n({
131+
translationsForLanguage: {
132+
'timestamp/DateSeparator':
133+
'{{ timestamp | timestampFormatter(calendar: false, format: "YYYY") }}',
134+
},
135+
}),
136+
},
137+
props: { date, formatDate },
138+
});
139+
expect(screen.queryByTestId(DATE_SEPARATOR_TEST_ID)).toHaveTextContent(dateMock);
140+
});
39141

40-
expect(screen.getByText('New - 03/30/2020')).toBeInTheDocument();
41-
expect(t).toHaveBeenCalledWith('New');
142+
it('should format date with formatDate instead of customs provided via props', async () => {
143+
await renderComponent({
144+
chatProps: {
145+
i18nInstance: new Streami18n({
146+
translationsForLanguage: {
147+
'timestamp/DateSeparator': '{{ timestamp | timestampFormatter(calendar: false) }}',
148+
},
149+
}),
150+
},
151+
props: { date, format: 'YYYY', formatDate },
152+
});
153+
expect(screen.queryByTestId(DATE_SEPARATOR_TEST_ID)).toHaveTextContent(dateMock);
42154
});
43155

44-
it('should render properly for unread', () => {
45-
const { Component } = withContext({ date: now, unread: true });
46-
const tree = renderer.create(Component).toJSON();
47-
expect(tree).toMatchInlineSnapshot(`
48-
<div
49-
className="str-chat__date-separator"
50-
data-testid="date-separator"
51-
>
52-
<hr
53-
className="str-chat__date-separator-line"
54-
/>
156+
it('should render New text if unread prop is true', async () => {
157+
const { container } = await renderComponent({ props: { date, unread: true } });
158+
expect(container).toMatchInlineSnapshot(`
159+
<div>
55160
<div
56-
className="str-chat__date-separator-date"
161+
class="str-chat__date-separator"
162+
data-testid="date-separator"
57163
>
58-
New - 03/30/2020
164+
<hr
165+
class="str-chat__date-separator-line"
166+
/>
167+
<div
168+
class="str-chat__date-separator-date"
169+
>
170+
New - 03/30/2020
171+
</div>
59172
</div>
60173
</div>
61174
`);
62-
});
63-
64-
it("should use tDateTimeParser's calendar method by default", () => {
65-
const { Component, tDateTimeParser } = withContext({ date: now });
66-
const { queryByText } = render(Component);
67-
68-
expect(tDateTimeParser).toHaveBeenCalledWith(now);
69-
expect(queryByText(Dayjs(now.toISOString()).calendar())).toBeInTheDocument();
175+
expect(screen.getByText('New - 03/30/2020')).toBeInTheDocument();
70176
});
71177

72178
describe('Position prop', () => {
73179
const renderWithPosition = (position) => (
74-
<DateSeparator date={now} formatDate={() => 'the date'} position={position} />
180+
<DateSeparator date={date} formatDate={formatDate} position={position} />
75181
);
76182

77183
const defaultPosition = renderer.create(renderWithPosition()).toJSON();

src/components/EventComponent/EventComponent.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ const UnMemoizedEventComponent = <
2626
>(
2727
props: EventComponentProps<StreamChatGenerics>,
2828
) => {
29-
const { calendar, calendarFormats, format = 'dddd L', Avatar = DefaultAvatar, message } = props;
29+
const { calendar, calendarFormats, format, Avatar = DefaultAvatar, message } = props;
3030

3131
const { t, tDateTimeParser } = useTranslationContext('EventComponent');
3232
const { created_at = '', event, text, type } = message;

0 commit comments

Comments
 (0)