Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: global date time formatting through i18n #2552

Merged
merged 27 commits into from
Jun 17, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
bc1099b
feat: global date time formatting through i18n
khushal87 Jun 13, 2024
0a9bbb8
fix: build and lint issues
khushal87 Jun 13, 2024
9e3dc12
fix: StickyHeader code
khushal87 Jun 13, 2024
0aca012
fix: broken translations for date time
khushal87 Jun 14, 2024
942849f
fix: broken translations for date time
khushal87 Jun 14, 2024
5c9e47f
fix: build issues
khushal87 Jun 14, 2024
a48156a
fix: remove timestampTranslationKey prop
khushal87 Jun 14, 2024
eb90df3
tests: fix broken tests
khushal87 Jun 14, 2024
a8ba201
tests: fix broken tests
khushal87 Jun 14, 2024
85b2d8d
fix: memoize the getDatestring logic
khushal87 Jun 14, 2024
cafd039
fix: memoize the getDatestring logic
khushal87 Jun 14, 2024
559c993
fix: memoize the getDatestring logic
khushal87 Jun 14, 2024
33508c2
fix: translations test
khushal87 Jun 14, 2024
b628b6e
fix: add tests
khushal87 Jun 16, 2024
167489b
fix: use i18n-parser for build-translations and improve related scripts
khushal87 Jun 16, 2024
31e164a
fix: use i18n-parser for build-translations and improve related scripts
khushal87 Jun 16, 2024
f109840
fix: prettier config
khushal87 Jun 16, 2024
5562e89
debug commit
khushal87 Jun 16, 2024
4c2f4d4
debug commit
khushal87 Jun 16, 2024
43ffc79
Delete package/src/i18n/pt-BR.json
khushal87 Jun 16, 2024
dded19c
debug commit
khushal87 Jun 16, 2024
b82ce7a
debug commit
khushal87 Jun 16, 2024
be213b2
set keepRemoved true
khushal87 Jun 17, 2024
3a703fb
fix: build config
khushal87 Jun 17, 2024
61b3f26
fix: build config
khushal87 Jun 17, 2024
86a792b
fix: build config
khushal87 Jun 17, 2024
2f125c9
fix: merge conflicts from develop
khushal87 Jun 17, 2024
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
85 changes: 85 additions & 0 deletions docusaurus/docs/reactnative/guides/date-time-formatting.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
---
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; calendarFormats: {\"sameDay\": \"LT\"}) }}",
```

or

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

:::note
For `calendarFormats`, you don't have to provide all the formats. Provide only the formats you want to customize.
:::

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.

:::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).
:::
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 @@ -71,7 +71,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
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
import { useEffect, useState } from 'react';

import { TFunction } from 'i18next';
import type { Channel, ChannelState, MessageResponse, StreamChat, UserResponse } from 'stream-chat';

import { useChatContext } from '../../../contexts/chatContext/ChatContext';
import {
isDayOrMoment,
TDateTimeParser,
useTranslationContext,
} from '../../../contexts/translationContext/TranslationContext';
import { useTranslationContext } from '../../../contexts/translationContext/TranslationContext';

import { useTranslatedMessage } from '../../../hooks/useTranslatedMessage';
import type { DefaultStreamChatGenerics } from '../../../types/types';
Expand All @@ -21,13 +18,13 @@ type LatestMessage<
export type LatestMessagePreview<
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics,
> = {
created_at: string | number | Date;
messageObject: LatestMessage<StreamChatGenerics> | undefined;
previews: {
bold: boolean;
text: string;
}[];
status: number;
created_at?: string | Date;
};

const getMessageSenderName = <
Expand Down Expand Up @@ -131,22 +128,6 @@ const getLatestMessageDisplayText = <
];
};

const getLatestMessageDisplayDate = <
StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics,
>(
message: LatestMessage<StreamChatGenerics> | undefined,
tDateTimeParser: TDateTimeParser,
) => {
const parserOutput = tDateTimeParser(message?.created_at);
if (isDayOrMoment(parserOutput)) {
if (parserOutput.isSame(new Date(), 'day')) {
return parserOutput.format('LT');
}
return parserOutput.format('L');
}
return parserOutput;
};

export enum MessageReadStatus {
NOT_SENT_BY_CURRENT_USER = 0,
UNREAD = 1,
Expand Down Expand Up @@ -190,13 +171,12 @@ const getLatestMessagePreview = <
channel: Channel<StreamChatGenerics>;
client: StreamChat<StreamChatGenerics>;
readEvents: boolean;
t: (key: string) => string;
tDateTimeParser: TDateTimeParser;
t: TFunction;
lastMessage?:
| ReturnType<ChannelState<StreamChatGenerics>['formatMessage']>
| MessageResponse<StreamChatGenerics>;
}) => {
const { channel, client, lastMessage, readEvents, t, tDateTimeParser } = params;
const { channel, client, lastMessage, readEvents, t } = params;

const messages = channel.state.messages;

Expand All @@ -219,7 +199,7 @@ const getLatestMessagePreview = <
const message = lastMessage !== undefined ? lastMessage : channelStateLastMessage;

return {
created_at: getLatestMessageDisplayDate(message, tDateTimeParser),
created_at: message?.created_at,
messageObject: message,
previews: getLatestMessageDisplayText(channel, client, message, t),
status: getLatestMessageReadStatus(channel, client, message, readEvents),
Expand All @@ -240,7 +220,7 @@ export const useLatestMessagePreview = <
forceUpdate: number,
) => {
const { client } = useChatContext<StreamChatGenerics>();
const { t, tDateTimeParser } = useTranslationContext();
const { t } = useTranslationContext();

const channelConfigExists = typeof channel?.getConfig === 'function';

Expand Down Expand Up @@ -286,7 +266,6 @@ export const useLatestMessagePreview = <
lastMessage: translatedLastMessage,
readEvents,
t,
tDateTimeParser,
}),
),
[channelLastMessageString, forceUpdate, readEvents, readStatus],
Expand Down
2 changes: 1 addition & 1 deletion package/src/components/Chat/Chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ import { SDK } from '../../native';
import { QuickSqliteClient } from '../../store/QuickSqliteClient';
import type { DefaultStreamChatGenerics } from '../../types/types';
import { DBSyncManager } from '../../utils/DBSyncManager';
import type { Streami18n } from '../../utils/i18n/Streami18n';
import { StreamChatRN } from '../../utils/StreamChatRN';
import type { Streami18n } from '../../utils/Streami18n';
import { version } from '../../version.json';

init();
Expand Down
Loading
Loading