Skip to content

Commit 9511550

Browse files
committed
feat: allow to configure date and time format over i18n
1 parent ded8f05 commit 9511550

26 files changed

+659
-45
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
---
2+
id: date-time-formatting
3+
title: Date and time formatting
4+
keywords: [date, time, datetime, timestamp, format, formatting]
5+
---
6+
7+
In this guide we will learn how date a time formatting can be customized within SDK's components.
8+
9+
## SDK components displaying date & time
10+
11+
The following components provided by the SDK display datetime:
12+
13+
- `DateSeparator`- component separating groups of messages in message lists
14+
- `EventComponent` - component that renders system messages (`message.type === 'system'`)
15+
- `Timestamp` - component to display non-system message timestamp
16+
17+
## Format customization
18+
19+
The datetime format customization can be done on multiple levels:
20+
21+
1. Override the default component prop values
22+
2. Supply custom formatting function
23+
3. Format date via i18n
24+
25+
### Override the component props defaults
26+
27+
All the mentioned components accept timestamp formatter props:
28+
29+
```ts
30+
export type TimestampFormatterOptions = {
31+
/* 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;
33+
/* Object specifying date display formats for dates formatted with calendar extension. Active only if calendar prop enabled. */
34+
calendarFormats?: Record<string, string> | null;
35+
/* Overrides the default timestamp format if calendar is disabled. */
36+
format?: string | null;
37+
};
38+
```
39+
40+
If calendar formatting is enabled, the dates are formatted with time-relative words ("yesterday at ...", "last ..."). The calendar strings can be further customized with `calendarFormats` object. It also means that the `format` prop would be ignored. On the other hand, if calendar is disabled, then `calendarFormats` is ignored and `format` string is applied.
41+
42+
All the components can be overridden through `Channel` component context
43+
44+
```tsx
45+
import {
46+
Channel,
47+
DateSeparatorProps,
48+
DateSeparator,
49+
EventComponentProps,
50+
EventComponent,
51+
MessageTimestampProps,
52+
MessageTimestamp,
53+
} from 'stream-chat-react';
54+
55+
const CustomDateSeparator = (props: DateSeparatorProps) => (
56+
<DateSeparator {...props} calendar={false} format={'YYYY'} /> // calendar is enabled by default
57+
);
58+
59+
const SystemMessage = (props: EventComponentProps) => (
60+
<EventComponent {...props} calendar={false} format={'YYYY'} /> // calendar is enabled by default
61+
);
62+
63+
const CustomMessageTimestamp = (props: MessageTimestampProps) => (
64+
<MessageTimestamp {...props} calendar={false} format={'YYYY-MM-DDTHH:mm:ss'} /> // calendar is enabled by default
65+
);
66+
67+
const App = () => (
68+
<Channel
69+
DateSeparator={CustomDateSeparator}
70+
MessageSystem={SystemMessage}
71+
MessageTimestamp={CustomMessageTimestamp}
72+
></Channel>
73+
);
74+
```
75+
76+
### Custom formatting function
77+
78+
Custom formatting function can be passed to `MessageList` or `VirtualizedMessageList` via prop `formatDate` (`(date: Date) => string;`). The `Message` component passes down the function to be consumed by the children via `MessageComponentContext`:
79+
80+
```jsx
81+
import { useMessageContext } from 'stream-chat-react';
82+
const CustomComponent = () => {
83+
const { formatDate } = useMessageContext();
84+
};
85+
```
86+
87+
By default, the function is consumed by the `MessageTimestamp` component. This means the formatting via `formatDate` is reduced only to timestamp shown by a message in the message list. Components `DateSeparator`, `EventComponent` would ignore the custom formatting.
88+
89+
### Date & time formatting with i18n service
90+
91+
Until now, the datetime values could be customized within `Channel` at best. Formatting via i18n service allows for SDK wide configuration. The configuration is stored with other translations in JSON files. Formatting with i18n service has the following advantages:
92+
93+
- it is centralized
94+
- it takes into consideration the locale out of the box
95+
- allows for high granularity - formatting per string, not component (opposed to props approach)
96+
- allows for high re-usability - apply the same configuration in multiple places via the same translation key
97+
- allows for custom formatting logic
98+
99+
#### Change the default configuration
100+
101+
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`.
102+
103+
##### Overriding the prop defaults
104+
105+
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. An example follows:
106+
107+
```jsx
108+
import {
109+
DateSeparatorProps,
110+
DateSeparator,
111+
EventComponentProps,
112+
EventComponent,
113+
MessageTimestampProps,
114+
MessageTimestamp,
115+
} from 'stream-chat-react';
116+
117+
const CustomDateSeparator = (props: DateSeparatorProps) => (
118+
<DateSeparator {...props} calendar={null} calendarFormats={null} format={null} />
119+
);
120+
121+
const SystemMessage = (props: EventComponentProps) => (
122+
<EventComponent {...props} calendar={null} calendarFormats={null} format={null} />
123+
);
124+
125+
const CustomMessageTimestamp = (props: MessageTimestampProps) => (
126+
<MessageTimestamp {...props} calendar={null} calendarFormats={null} format={null} />
127+
);
128+
```
129+
130+
Besides overriding the formatting parameters above, we can customize the translation key via `timestampTranslationKey` prop all the above components (`DateSeparator`, `EventComponent`, `Timestamp`).
131+
132+
```tsx
133+
import { MessageTimestampProps, MessageTimestamp } from 'stream-chat-react';
134+
135+
const CustomMessageTimestamp = (props: MessageTimestampProps) => (
136+
<MessageTimestamp {...props} timestampTranslationKey='customTimestampTranslationKey' />
137+
);
138+
```
139+
140+
##### Understanding the formatting syntax
141+
142+
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:
143+
144+
```
145+
"timestamp/SystemMessage": "{{ timestamp, timestampFormatter(calendar: true; calendarFormats: {\"sameElse\": \"dddd L\"}) }}",
146+
```
147+
148+
Let's dissect the example:
149+
150+
- The curly brackets (`{{`, `}}`) indicate the place where a value will be interpolated (inserted) into the string.
151+
- variable `timestamp` is the name of variable which value will be inserted into the string
152+
- `timestampFormatter` is the name of the formatting function that is used to convert the `timestamp` value into desired format
153+
- the `timestampFormatter` is 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)
154+
155+
:::note
156+
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).
157+
:::
158+
159+
#### Custom datetime formatter functions
160+
161+
Besides overriding the configuration parameters, we can override the default `timestampFormatter` function by providing custom `Streami18n` instance:
162+
163+
```tsx
164+
import { Chat, Streami18n, useCreateChatClient } from 'stream-chat-react';
165+
166+
const i18n = new Streami18n({
167+
formatters: {
168+
timestampFormatter: () => (val: string | Date) => {
169+
return new Date(val).getTime() + '';
170+
},
171+
},
172+
});
173+
174+
export const ChatApp = ({ apiKey, userId, userToken }) => {
175+
const chatClient = useCreateChatClient({
176+
apiKey,
177+
tokenOrProvider: userToken,
178+
userData: { id: userId },
179+
});
180+
return <Chat client={chatClient} i18nInstance={i18n}></Chat>;
181+
};
182+
```

src/components/DateSeparator/DateSeparator.tsx

+17-4
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,42 @@
11
import React from 'react';
22

33
import { useTranslationContext } from '../../context/TranslationContext';
4-
import { getDateString } from '../../i18n/utils';
4+
import { getDateString, TimestampFormatterOptions } from '../../i18n/utils';
55

6-
export type DateSeparatorProps = {
6+
export type DateSeparatorProps = TimestampFormatterOptions & {
77
/** The date to format */
88
date: Date;
99
/** Override the default formatting of the date. This is a function that has access to the original date object. */
1010
formatDate?: (date: Date) => string;
1111
/** Set the position of the date in the separator, options are 'left', 'center', 'right', @default right */
1212
position?: 'left' | 'center' | 'right';
13+
/* Lookup key in the language corresponding translations sheet to perform date formatting */
14+
timestampTranslationKey?: string;
1315
/** If following messages are not new */
1416
unread?: boolean;
1517
};
1618

1719
const UnMemoizedDateSeparator = (props: DateSeparatorProps) => {
18-
const { date: messageCreatedAt, formatDate, position = 'right', unread } = props;
20+
const {
21+
calendar = true,
22+
date: messageCreatedAt,
23+
formatDate,
24+
position = 'right',
25+
timestampTranslationKey = 'timestamp/DateSeparator',
26+
unread,
27+
...restTimestampFormatterOptions
28+
} = props;
1929

2030
const { t, tDateTimeParser } = useTranslationContext('DateSeparator');
2131

2232
const formattedDate = getDateString({
23-
calendar: true,
33+
calendar,
34+
...restTimestampFormatterOptions,
2435
formatDate,
2536
messageCreatedAt,
37+
t,
2638
tDateTimeParser,
39+
timestampTranslationKey,
2740
});
2841

2942
return (

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

+3-3
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ 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 } from '@testing-library/react';
5+
import { cleanup, render, screen } from '@testing-library/react';
66
import '@testing-library/jest-dom';
77

88
import { DateSeparator } from '../DateSeparator';
@@ -35,9 +35,9 @@ describe('DateSeparator', () => {
3535

3636
it('should render New text if unread prop is true', () => {
3737
const { Component, t } = withContext({ date: now, unread: true });
38-
const { queryByText } = render(Component);
38+
render(Component);
3939

40-
expect(queryByText('New - 03/30/2020')).toBeInTheDocument();
40+
expect(screen.getByText('New - 03/30/2020')).toBeInTheDocument();
4141
expect(t).toHaveBeenCalledWith('New');
4242
});
4343

src/components/EventComponent/EventComponent.tsx

+23-6
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,17 @@ import { useTranslationContext } from '../../context/TranslationContext';
77
import type { StreamMessage } from '../../context/ChannelStateContext';
88

99
import type { DefaultStreamChatGenerics } from '../../types/types';
10-
import { getDateString } from '../../i18n/utils';
10+
import { getDateString, TimestampFormatterOptions } from '../../i18n/utils';
1111

1212
export type EventComponentProps<
1313
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics
14-
> = {
14+
> = TimestampFormatterOptions & {
1515
/** Message object */
1616
message: StreamMessage<StreamChatGenerics>;
1717
/** Custom UI component to display user avatar, defaults to and accepts same props as: [Avatar](https://github.com/GetStream/stream-chat-react/blob/master/src/components/Avatar/Avatar.tsx) */
1818
Avatar?: React.ComponentType<AvatarProps>;
19+
/* Lookup key in the language corresponding translations sheet to perform date formatting */
20+
timestampTranslationKey?: string;
1921
};
2022

2123
/**
@@ -26,9 +28,16 @@ const UnMemoizedEventComponent = <
2628
>(
2729
props: EventComponentProps<StreamChatGenerics>,
2830
) => {
29-
const { Avatar = DefaultAvatar, message } = props;
31+
const {
32+
calendar = true,
33+
calendarFormats = { sameElse: 'dddd L' },
34+
format,
35+
Avatar = DefaultAvatar,
36+
message,
37+
timestampTranslationKey = 'timestamp/SystemMessage',
38+
} = props;
3039

31-
const { tDateTimeParser } = useTranslationContext('EventComponent');
40+
const { t, tDateTimeParser } = useTranslationContext('EventComponent');
3241
const { created_at = '', event, text, type } = message;
3342
const getDateOptions = { messageCreatedAt: created_at.toString(), tDateTimeParser };
3443

@@ -41,8 +50,16 @@ const UnMemoizedEventComponent = <
4150
<div className='str-chat__message--system__line' />
4251
</div>
4352
<div className='str-chat__message--system__date'>
44-
<strong>{getDateString({ ...getDateOptions, format: 'dddd' })} </strong>
45-
at {getDateString({ ...getDateOptions, format: 'hh:mm A' })}
53+
<strong>
54+
{getDateString({
55+
...getDateOptions,
56+
calendar,
57+
calendarFormats,
58+
format,
59+
t,
60+
timestampTranslationKey,
61+
})}
62+
</strong>
4663
</div>
4764
</div>
4865
);

src/components/EventComponent/__tests__/EventComponent.test.js

+1-4
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,8 @@ describe('EventComponent', () => {
4949
className="str-chat__message--system__date"
5050
>
5151
<strong>
52-
Friday
53-
52+
Friday 03/13/2020
5453
</strong>
55-
at
56-
10:18 AM
5754
</div>
5855
</div>
5956
`);

src/components/Message/MessageTimestamp.tsx

+7-9
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,23 @@
11
import React from 'react';
2-
3-
import type { StreamMessage } from '../../context/ChannelStateContext';
4-
import type { DefaultStreamChatGenerics } from '../../types/types';
5-
62
import { useMessageContext } from '../../context/MessageContext';
73
import { Timestamp as DefaultTimestamp } from './Timestamp';
84
import { useComponentContext } from '../../context';
95

6+
import type { StreamMessage } from '../../context/ChannelStateContext';
7+
import type { DefaultStreamChatGenerics } from '../../types/types';
8+
import type { TimestampFormatterOptions } from '../../i18n/utils';
9+
1010
export const defaultTimestampFormat = 'h:mmA';
1111

1212
export type MessageTimestampProps<
1313
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics
14-
> = {
15-
/* If true, call the `Day.js` calendar function to get the date string to display. */
16-
calendar?: boolean;
14+
> = TimestampFormatterOptions & {
1715
/* Adds a CSS class name to the component's outer `time` container. */
1816
customClass?: string;
19-
/* Overrides the default timestamp format */
20-
format?: string;
2117
/* The `StreamChat` message object, which provides necessary data to the underlying UI components (overrides the value from `MessageContext`) */
2218
message?: StreamMessage<StreamChatGenerics>;
19+
/* Lookup key in the language corresponding translations sheet to perform date formatting */
20+
timestampTranslationKey?: string;
2321
};
2422

2523
const UnMemoizedMessageTimestamp = <

0 commit comments

Comments
 (0)