Skip to content

Commit

Permalink
feat: global date time formatting through i18n (#2552)
Browse files Browse the repository at this point in the history
* feat: global date time formatting through i18n

* fix: build and lint issues

* fix: StickyHeader code

* fix: broken translations for date time

* fix: broken translations for date time

* fix: build issues

* fix: remove timestampTranslationKey prop

* tests: fix broken tests

* tests: fix broken tests

* fix: memoize the getDatestring logic

* fix: memoize the getDatestring logic

* fix: memoize the getDatestring logic

* fix: translations test

* fix: add tests

* fix: use i18n-parser for build-translations and improve related scripts

* fix: use i18n-parser for build-translations and improve related scripts

* fix: prettier config

* debug commit

* debug commit

* Delete package/src/i18n/pt-BR.json

* debug commit

* debug commit

* set keepRemoved true

* fix: build config

* fix: build config
  • Loading branch information
khushal87 authored Jun 17, 2024
1 parent 6037c4d commit 0ad7016
Show file tree
Hide file tree
Showing 52 changed files with 787 additions and 329 deletions.
129 changes: 129 additions & 0 deletions docusaurus/docs/reactnative/guides/date-time-formatting.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
---
id: date-time-formatting
title: Date and time formatting
---

In this guide we will learn how date a time formatting can be customized within SDK's components.

## SDK components displaying date & time

The following components provided by the SDK display datetime:

- `ChannelPreviewStatus` - Component showing last message date and time in `ChannelList`.
- `ImageGalleryHeader` - Component showing the header in the `ImageGallery`.
- `InlineDateSeparator` - Component separating groups of messages in `MessageList`.
- `MessageEditedTimestamp` - Component showing edited message time when clicked on an edited message.
- `MessageSystem` - Component showing system message.
- `MessageTimestamp` - Component showing message timestamp.
- `StickyHeader` - Component showing sticky header on the top of the `MessageList`/`Channel`.

## Format Customization

The datetime format can be customized by providing date format through the `i18n` JSON.

### Date & time formatting with i18n service

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:

- It is centralized.
- It takes into consideration the locale out of the box.
- Allows for high granularity - formatting per string, not component (opposed to props approach).
- Allows for high re-usability - apply the same configuration in multiple places via the same translation key.
- Allows for custom formatting logic.

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`.

We can apply custom configuration in all the translation JSON files. It could look similar to the following example key-value pair.

```json
"timestamp/SystemMessage": "{{ timestamp | timestampFormatter(format: YYYY) }}",
```

Besides overriding the formatting parameters above, we can customize the translation key via `timestampTranslationKey` prop. All the above components (`ChannelPreviewStatus`, `ImageGalleryHeader`, `InlineDateSeparator`, `MessageEditedTimestamp`, `MessageSystem`, `MessageTimestamp`, `StickyHeader`) accept this prop.

```tsx
import { MessageTimestampProps, MessageTimestamp } from 'stream-chat-react-native';

const CustomMessageTimestamp = (props: MessageTimestampProps) => (
<MessageTimestamp {...props} timestampTranslationKey='customTimestampTranslationKey' />
);
```

### Understanding the formatting syntax

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:

```json
"timestamp/MessageSystem": "{{ timestamp | timestampFormatter(calendar: true) }}",
```

or with custom calendar formats:

```json
"timestamp/MessageSystem": "{{ 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\"}) }}",
```

or with custom format:

```json
"timestamp/MessageTimestamp": "{{ timestamp | timestampFormatter(format: LT) }}",
```

Let's dissect the example:

- The curly brackets (`{{`, `}}`) indicate the place where a value will be interpolated (inserted) into the string.
- Variable `timestamp` is the name of variable which value will be inserted into the string.
- The `|` character is a pipe that separates the variable from the formatting function.
- The `timestampFormatter` is the name of the formatting function that is used to convert the `timestamp` value into desired format
- The `timestampFormatter` can be passed the same parameters as the React components (`calendar`, `calendarFormats`, `format`) as if the function was called with these values.

**Params**:

- `calendar` - This is a boolean field to decide if the date format should be in calendar format or not. The default value is `false`.
- `calendarFormats` - This is an object that contains the formats for the calendar. The default value is `{ sameDay: 'LT', nextDay: 'LT', nextWeek: 'dddd', lastDay: 'dddd', lastWeek: 'dddd', sameElse: 'L' }`.
- `format` - This is a string that contains the format of the date.

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. The `calendarFormats` object has to cover all the formatting cases as shows the example below:

```js
{
lastDay: '[gestern um] LT',
lastWeek: '[letzten] dddd [um] LT',
nextDay: '[morgen um] LT',
nextWeek: 'dddd [um] LT',
sameDay: '[heute um] LT',
sameElse: 'L',
}
```

:::important
If any of the `calendarFormats` keys are missing, then the underlying library will fall back to hard-coded english equivalents
:::

If `calendar` formatting is enabled, the `format` prop would be ignored. So to apply the `format` string, the `calendar` has to be disabled.

:::note
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).
:::

### Custom datetime formatter functions

Besides overriding the configuration parameters, we can override the default `timestampFormatter` function by providing custom `Streami18n` instance:

```tsx
import { Chat, Streami18n } from 'stream-chat-react-native';

const chatClient = 'Your Chat client here';

const i18n = new Streami18n({
formatters: {
timestampFormatter: () => (val: string | Date) => {
return new Date(val).getTime() + '';
},
},
});

export const ChatApp = ({ apiKey, userId, userToken }) => {
return <Chat client={chatClient} i18nInstance={i18n}></Chat>;
};
```
Original file line number Diff line number Diff line change
Expand Up @@ -11,38 +11,16 @@ This is the default component provided to the prop [`MessageEditedTimestamp`](..

## Props

### <div class="label description">_overrides the value from [MessageContext](../../contexts/message-context#message)_</div> `message` {#message}
### <div class="label description">_overrides the value from [MessageContext](../../contexts/message-context#message)_</div> `message` `{#message}`

<MessageProp />

### `calendar`
## UI components

Whether to show the time in Calendar time format. Calendar time displays time relative to a today's date.
### `MessageTimestamp`

| Type | Default |
| ---------------------- | ----------- |
| `Boolean`\|`undefined` | `undefined` |

### `format`

Format of the date.

| Type | Default |
| --------------------- | ----------- |
| `String`\|`undefined` | `undefined` |

### `formatDate`

Function to format the date.

| Type | Default |
| ----------------------- | ----------- |
| `Function`\|`undefined` | `undefined` |

### `timestamp`

The date to be shown after formatting.
The Component that renders the message timestamp.

| Type | Default |
| ----------------------------- | ----------- |
| `String`\|`Date`\|`undefined` | `undefined` |
| `ComponentType` \|`undefined` | `undefined` |
10 changes: 10 additions & 0 deletions docusaurus/docs/reactnative/ui-components/message-footer.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,13 @@ Weather message is deleted or not. In case of deleted message, `'Only visible to
### <div class="label description">_overrides the value from [MessageContext](../../contexts/message-context#showmessagestatus)_</div> `showMessageStatus` {#showmessagestatus}

<ShowMessageStatus />

## UI Components

### `MessageTimestamp`

The Component that renders the message timestamp.

| Type | Default |
| ----------------------------- | ----------- |
| `ComponentType` \|`undefined` | `undefined` |
1 change: 1 addition & 0 deletions docusaurus/sidebars-react-native.json
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@
],
"Advanced Guides": [
"guides/audio-messages-support",
"guides/date-time-formatting",
"customization/typescript",
"basics/troubleshooting",
"basics/stream_chat_with_navigation",
Expand Down
3 changes: 2 additions & 1 deletion package/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@
"@gorhom/bottom-sheet": "4.4.8",
"dayjs": "1.10.5",
"emoji-regex": "^10.3.0",
"i18next": "20.2.4",
"i18next": "^21.6.14",
"intl-pluralrules": "^2.0.1",
"linkifyjs": "^4.1.1",
"lodash-es": "4.17.21",
"mime-types": "^2.1.34",
Expand Down
2 changes: 1 addition & 1 deletion package/src/components/Attachment/__tests__/Giphy.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import { generateMember } from '../../../mock-builders/generator/member';
import { generateMessage } from '../../../mock-builders/generator/message';
import { generateUser } from '../../../mock-builders/generator/user';
import { getTestClientWithUser } from '../../../mock-builders/mock';
import { Streami18n } from '../../../utils/Streami18n';
import { Streami18n } from '../../../utils/i18n/Streami18n';
import { ImageLoadingFailedIndicator } from '../../Attachment/ImageLoadingFailedIndicator';
import { ImageLoadingIndicator } from '../../Attachment/ImageLoadingIndicator';
import { Channel } from '../../Channel/Channel';
Expand Down
7 changes: 6 additions & 1 deletion package/src/components/Channel/Channel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ import { MessageReplies as MessageRepliesDefault } from '../Message/MessageSimpl
import { MessageRepliesAvatars as MessageRepliesAvatarsDefault } from '../Message/MessageSimple/MessageRepliesAvatars';
import { MessageSimple as MessageSimpleDefault } from '../Message/MessageSimple/MessageSimple';
import { MessageStatus as MessageStatusDefault } from '../Message/MessageSimple/MessageStatus';
import { MessageTimestamp as MessageTimestampDefault } from '../Message/MessageSimple/MessageTimestamp';
import { ReactionList as ReactionListDefault } from '../Message/MessageSimple/ReactionList';
import { AttachButton as AttachButtonDefault } from '../MessageInput/AttachButton';
import { CommandsButton as CommandsButtonDefault } from '../MessageInput/CommandsButton';
Expand Down Expand Up @@ -156,6 +157,7 @@ import { MessageList as MessageListDefault } from '../MessageList/MessageList';
import { MessageSystem as MessageSystemDefault } from '../MessageList/MessageSystem';
import { NetworkDownIndicator as NetworkDownIndicatorDefault } from '../MessageList/NetworkDownIndicator';
import { ScrollToBottomButton as ScrollToBottomButtonDefault } from '../MessageList/ScrollToBottomButton';
import { StickyHeader as StickyHeaderDefault } from '../MessageList/StickyHeader';
import { TypingIndicator as TypingIndicatorDefault } from '../MessageList/TypingIndicator';
import { TypingIndicatorContainer as TypingIndicatorContainerDefault } from '../MessageList/TypingIndicatorContainer';
import { OverlayReactionList as OverlayReactionListDefault } from '../MessageOverlay/OverlayReactionList';
Expand Down Expand Up @@ -305,6 +307,7 @@ export type ChannelPropsWithContext<
| 'MessageStatus'
| 'MessageSystem'
| 'MessageText'
| 'MessageTimestamp'
| 'myMessageTheme'
| 'onLongPressMessage'
| 'onPressInMessage'
Expand Down Expand Up @@ -542,6 +545,7 @@ const ChannelWithContext = <
MessageStatus = MessageStatusDefault,
MessageSystem = MessageSystemDefault,
MessageText,
MessageTimestamp = MessageTimestampDefault,
MoreOptionsButton = MoreOptionsButtonDefault,
myMessageTheme,
NetworkDownIndicator = NetworkDownIndicatorDefault,
Expand Down Expand Up @@ -573,7 +577,7 @@ const ChannelWithContext = <
ShowThreadMessageInChannelButton = ShowThreadMessageInChannelButtonDefault,
StartAudioRecordingButton = AudioRecordingButtonDefault,
stateUpdateThrottleInterval = defaultThrottleInterval,
StickyHeader,
StickyHeader = StickyHeaderDefault,
supportedReactions = reactionData,
t,
thread: threadProps,
Expand Down Expand Up @@ -2328,6 +2332,7 @@ const ChannelWithContext = <
MessageStatus,
MessageSystem,
MessageText,
MessageTimestamp,
myMessageTheme,
onLongPressMessage,
onPressInMessage,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export const useCreateMessagesContext = <
MessageStatus,
MessageSystem,
MessageText,
MessageTimestamp,
myMessageTheme,
onLongPressMessage,
onPressInMessage,
Expand Down Expand Up @@ -165,6 +166,7 @@ export const useCreateMessagesContext = <
MessageStatus,
MessageSystem,
MessageText,
MessageTimestamp,
myMessageTheme,
onLongPressMessage,
onPressInMessage,
Expand Down
18 changes: 16 additions & 2 deletions package/src/components/ChannelPreview/ChannelPreviewStatus.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import React from 'react';
import React, { useMemo } from 'react';
import { StyleSheet, Text, View } from 'react-native';

import { ChannelPreviewProps } from './ChannelPreview';
import type { ChannelPreviewMessengerPropsWithContext } from './ChannelPreviewMessenger';
import { MessageReadStatus } from './hooks/useLatestMessagePreview';

import { useTheme } from '../../contexts/themeContext/ThemeContext';
import { useTranslationContext } from '../../contexts/translationContext/TranslationContext';
import { Check, CheckAll } from '../../icons';

import type { DefaultStreamChatGenerics } from '../../types/types';
import { getDateString } from '../../utils/i18n/getDateString';

const styles = StyleSheet.create({
date: {
Expand All @@ -35,6 +37,7 @@ export const ChannelPreviewStatus = <
props: ChannelPreviewStatusProps<StreamChatGenerics>,
) => {
const { formatLatestMessageDate, latestMessagePreview } = props;
const { t, tDateTimeParser } = useTranslationContext();
const {
theme: {
channelPreview: { checkAllIcon, checkIcon, date },
Expand All @@ -44,6 +47,17 @@ export const ChannelPreviewStatus = <

const created_at = latestMessagePreview.messageObject?.created_at;
const latestMessageDate = created_at ? new Date(created_at) : new Date();

const formattedDate = useMemo(
() =>
getDateString({
date: created_at,
t,
tDateTimeParser,
timestampTranslationKey: 'timestamp/ChannelPreviewStatus',
}),
[created_at, t, tDateTimeParser],
);
const status = latestMessagePreview.status;

return (
Expand All @@ -56,7 +70,7 @@ export const ChannelPreviewStatus = <
<Text style={[styles.date, { color: grey }, date]}>
{formatLatestMessageDate && latestMessageDate
? formatLatestMessageDate(latestMessageDate).toString()
: latestMessagePreview.created_at.toString()}
: formattedDate}
</Text>
</View>
);
Expand Down
Loading

0 comments on commit 0ad7016

Please sign in to comment.