Skip to content

Commit b50ed73

Browse files
authored
feat: support automatically translated messages (#1526)
* chore: convert MessageTextContainer to tsx * test: convert channel generator to ts * test: add test for translated MessageTextContainer * feat: translate messages in MessageTextContainer * test: convert ChannelPreview test to ts * test: add test for tranlated messages in channel preview * feat: translate messages in channel preview * fix: use latestMessage if provided in useLatestMessagePreview It's supposed to work in a way that it will fall back to the channel state messages if no latestMessage is provided, but until now, the check for latestMessage has always been considered falsy. * fix: separate userLanguage from the i18n translator setter * refactor: move logic to translate messages to a hook * feat: translate messages passed to the overlay * style: apply lint fixes * chore: fix type errors * Update package/src/hooks/useTranslatedMessage.ts * style: extract translator functions to separate type * fix: use MessageOverlayContext userLanguage if it is set * style: fix object key ordering to alphabetical
1 parent 8e4b2a4 commit b50ed73

File tree

20 files changed

+457
-157
lines changed

20 files changed

+457
-157
lines changed

package/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@
143143
"react-native-web": "0.15.0",
144144
"react-test-renderer": "17.0.2",
145145
"typescript": "4.4.3",
146-
"uuid": "8.3.2",
146+
"uuid": "^8.3.2",
147147
"webpack": "4.44.2"
148148
}
149149
}

package/src/components/ChannelPreview/ChannelPreview.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
} from '../../contexts/channelsContext/ChannelsContext';
1111
import { ChatContextValue, useChatContext } from '../../contexts/chatContext/ChatContext';
1212

13+
import { useTranslatedMessage } from '../../hooks/useTranslatedMessage';
1314
import type { DefaultStreamChatGenerics } from '../../types/types';
1415

1516
export type ChannelPreviewPropsWithContext<
@@ -38,10 +39,15 @@ const ChannelPreviewWithContext = <
3839
| MessageResponse<StreamChatGenerics>
3940
| undefined
4041
>(channel.state.messages[channel.state.messages.length - 1]);
42+
43+
const translatedLastMessage = useTranslatedMessage<StreamChatGenerics>(
44+
lastMessage || ({} as MessageResponse<StreamChatGenerics>),
45+
);
46+
4147
const [forceUpdate, setForceUpdate] = useState(0);
4248
const [unread, setUnread] = useState(channel.countUnread());
4349

44-
const latestMessagePreview = useLatestMessagePreview(channel, forceUpdate, lastMessage);
50+
const latestMessagePreview = useLatestMessagePreview(channel, forceUpdate, translatedLastMessage);
4551

4652
const channelLastMessage = channel.lastMessage();
4753
const channelLastMessageString = `${channelLastMessage?.id}${channelLastMessage?.updated_at}`;
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
1-
import React from 'react';
1+
import React, { ComponentType } from 'react';
22
import { Text } from 'react-native';
33

44
import { act, render, waitFor } from '@testing-library/react-native';
55

6-
import { getOrCreateChannelApi } from '../../../mock-builders/api/getOrCreateChannel';
6+
import type { Channel, StreamChat } from 'stream-chat';
7+
8+
import {
9+
getOrCreateChannelApi,
10+
GetOrCreateChannelApiParams,
11+
} from '../../../mock-builders/api/getOrCreateChannel';
712
import { useMockedApis } from '../../../mock-builders/api/useMockedApis';
813
import dispatchMessageNewEvent from '../../../mock-builders/event/messageNew';
914
import dispatchMessageReadEvent from '../../../mock-builders/event/messageRead';
@@ -13,8 +18,23 @@ import { generateUser } from '../../../mock-builders/generator/user';
1318
import { getTestClientWithUser } from '../../../mock-builders/mock';
1419
import { Chat } from '../../Chat/Chat';
1520
import { ChannelPreview } from '../ChannelPreview';
21+
import type { ChannelPreviewMessengerProps } from '../ChannelPreviewMessenger';
22+
23+
import '@testing-library/jest-native/extend-expect';
24+
25+
type ChannelPreviewUIComponentProps = {
26+
channel: {
27+
id: string;
28+
};
29+
latestMessagePreview: {
30+
messageObject: {
31+
text: string;
32+
};
33+
};
34+
unread: number;
35+
};
1636

17-
const ChannelPreviewUIComponent = (props) => (
37+
const ChannelPreviewUIComponent = (props: ChannelPreviewUIComponentProps) => (
1838
<>
1939
<Text testID='channel-id'>{props.channel.id}</Text>
2040
<Text testID='unread-count'>{props.unread}</Text>
@@ -28,21 +48,27 @@ const ChannelPreviewUIComponent = (props) => (
2848

2949
describe('ChannelPreview', () => {
3050
const clientUser = generateUser();
31-
let chatClient;
32-
let channel;
33-
34-
const getComponent = (props = {}) => (
35-
<Chat client={chatClient}>
36-
<ChannelPreview
37-
{...props}
38-
channel={channel}
39-
client={chatClient}
40-
Preview={ChannelPreviewUIComponent}
41-
/>
42-
</Chat>
43-
);
44-
45-
const initializeChannel = async (c) => {
51+
let chatClient: StreamChat;
52+
let channel: Channel | null;
53+
54+
const TestComponent = (props = {}) => {
55+
if (channel === null) {
56+
return null;
57+
}
58+
59+
return (
60+
<Chat client={chatClient}>
61+
<ChannelPreview
62+
{...props}
63+
channel={channel}
64+
client={chatClient}
65+
Preview={ChannelPreviewUIComponent as ComponentType<ChannelPreviewMessengerProps>}
66+
/>
67+
</Chat>
68+
);
69+
};
70+
71+
const useInitializeChannel = async (c: GetOrCreateChannelApiParams) => {
4672
useMockedApis(chatClient, [getOrCreateChannelApi(c)]);
4773

4874
channel = chatClient.channel('messaging');
@@ -58,31 +84,22 @@ describe('ChannelPreview', () => {
5884
channel = null;
5985
});
6086

61-
it('should render with latest message on channel', async () => {
62-
const message = generateMessage({
63-
user: clientUser,
64-
});
65-
const c = generateChannelResponse({
66-
messages: [message],
67-
});
68-
await initializeChannel(c);
69-
const { queryByText } = render(getComponent());
70-
await waitFor(() => queryByText(message.text));
71-
});
72-
7387
it('should mark channel as read, when message.read event is received for current user', async () => {
7488
const c = generateChannelResponse();
75-
await initializeChannel(c);
76-
channel.countUnread = () => 20;
89+
await useInitializeChannel(c);
90+
91+
if (channel !== null) {
92+
channel.countUnread = () => 20;
93+
}
7794

78-
const { getByTestId } = render(getComponent());
95+
const { getByTestId } = render(<TestComponent />);
7996

8097
await waitFor(() => getByTestId('channel-id'));
8198

8299
expect(getByTestId('unread-count')).toHaveTextContent('20');
83100

84101
act(() => {
85-
dispatchMessageReadEvent(chatClient, clientUser, channel);
102+
dispatchMessageReadEvent(chatClient, clientUser, channel || {});
86103
});
87104

88105
await waitFor(() => {
@@ -92,9 +109,9 @@ describe('ChannelPreview', () => {
92109

93110
it('should update the lastest message on "message.new" event', async () => {
94111
const c = generateChannelResponse();
95-
await initializeChannel(c);
112+
await useInitializeChannel(c);
96113

97-
const { getByTestId } = render(getComponent());
114+
const { getByTestId } = render(<TestComponent />);
98115

99116
await waitFor(() => getByTestId('channel-id'));
100117

@@ -103,7 +120,7 @@ describe('ChannelPreview', () => {
103120
});
104121

105122
act(() => {
106-
dispatchMessageNewEvent(chatClient, message, channel);
123+
dispatchMessageNewEvent(chatClient, message, channel || {});
107124
});
108125

109126
await waitFor(() => {
@@ -113,24 +130,45 @@ describe('ChannelPreview', () => {
113130

114131
it('should update the unread count on "message.new" event', async () => {
115132
const c = generateChannelResponse();
116-
await initializeChannel(c);
133+
await useInitializeChannel(c);
117134

118-
const { getByTestId } = render(getComponent());
135+
const { getByTestId } = render(<TestComponent />);
119136

120137
await waitFor(() => getByTestId('channel-id'));
121138

122139
const message = generateMessage({
123140
user: clientUser,
124141
});
125142

126-
channel.countUnread = () => 10;
143+
if (channel !== null) {
144+
channel.countUnread = () => 10;
145+
}
127146

128147
act(() => {
129-
dispatchMessageNewEvent(chatClient, message, channel);
148+
dispatchMessageNewEvent(chatClient, message, channel || {});
130149
});
131150

132151
await waitFor(() => {
133152
expect(getByTestId('unread-count')).toHaveTextContent('10');
134153
});
135154
});
155+
156+
it('displays messages translated if applicable', async () => {
157+
chatClient = await getTestClientWithUser({ id: 'mads', language: 'no' });
158+
159+
const message = {
160+
i18n: {
161+
no_text: 'Hallo verden!',
162+
},
163+
text: 'Hello world!',
164+
};
165+
const channel = generateChannelResponse({ messages: [message] });
166+
await useInitializeChannel(channel);
167+
168+
const { getByText } = render(<TestComponent />);
169+
170+
await waitFor(() => {
171+
expect(getByText(message.i18n.no_text)).toBeTruthy();
172+
});
173+
});
136174
});

package/src/components/ChannelPreview/hooks/__tests__/useLatestMessagePreview.test.tsx

Lines changed: 56 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,10 @@ import { waitFor } from '@testing-library/react-native';
55

66
import type { DefaultStreamChatGenerics } from 'src/types/types';
77

8-
import type { DefaultGenerics, StreamChat } from 'stream-chat';
8+
import type { DefaultGenerics, MessageResponse, StreamChat } from 'stream-chat';
99

1010
import { ChatContext, ChatContextValue } from '../../../../contexts/chatContext/ChatContext';
1111
import {
12-
CHANNEL,
1312
CHANNEL_WITH_DELETED_MESSAGES,
1413
CHANNEL_WITH_EMPTY_MESSAGE,
1514
CHANNEL_WITH_MENTIONED_USERS,
@@ -47,41 +46,36 @@ describe('useLatestMessagePreview', () => {
4746
</ChatContext.Provider>
4847
);
4948

50-
it('should return a channel latest message', async () => {
51-
const { result } = renderHook(
52-
() => useLatestMessagePreview(CHANNEL, FORCE_UPDATE, LATEST_MESSAGE),
53-
{ wrapper: ChatProvider },
54-
);
55-
await waitFor(() => {
56-
expect(result.current.previews).toBeTruthy();
57-
});
58-
});
49+
it('should return a deleted message preview if the latest message is deleted', async () => {
50+
const latestMessage = { type: 'deleted' } as unknown as MessageResponse;
5951

60-
it('should return a deleted message preview', async () => {
6152
const { result } = renderHook(
62-
() => useLatestMessagePreview(CHANNEL_WITH_DELETED_MESSAGES, FORCE_UPDATE, LATEST_MESSAGE),
53+
() => useLatestMessagePreview(CHANNEL_WITH_DELETED_MESSAGES, FORCE_UPDATE, latestMessage),
6354
{ wrapper: ChatProvider },
6455
);
6556
await waitFor(() => {
6657
expect(result.current.previews).toEqual([{ bold: false, text: 'Message deleted' }]);
6758
});
6859
});
6960

70-
it('should return an "Nothing yet..." message preview', async () => {
61+
it('should return an "Nothing yet..." message preview if channel has no messages', async () => {
62+
const latestMessage = undefined;
63+
7164
const { result } = renderHook(
72-
() => useLatestMessagePreview(CHANNEL_WITH_NO_MESSAGES, FORCE_UPDATE, LATEST_MESSAGE),
65+
() => useLatestMessagePreview(CHANNEL_WITH_NO_MESSAGES, FORCE_UPDATE, latestMessage),
7366
{ wrapper: ChatProvider },
7467
);
7568
await waitFor(() => {
7669
expect(result.current.previews).toEqual([{ bold: false, text: 'Nothing yet...' }]);
7770
});
7871
});
7972

80-
it('should return latest message preview', async () => {
73+
it('should use latestMessage if provided', async () => {
8174
const { result } = renderHook(
8275
() => useLatestMessagePreview(CHANNEL_WITH_MESSAGES_TEXT, FORCE_UPDATE, LATEST_MESSAGE),
8376
{ wrapper: ChatProvider },
8477
);
78+
8579
await waitFor(() => {
8680
expect(result.current.previews).toEqual([
8781
{ bold: true, text: '@okechukwu: ' },
@@ -91,10 +85,13 @@ describe('useLatestMessagePreview', () => {
9185
});
9286

9387
it('should return a channel with an empty message preview', async () => {
88+
const latestMessage = {} as unknown as MessageResponse;
89+
9490
const { result } = renderHook(
95-
() => useLatestMessagePreview(CHANNEL_WITH_EMPTY_MESSAGE, FORCE_UPDATE, LATEST_MESSAGE),
91+
() => useLatestMessagePreview(CHANNEL_WITH_EMPTY_MESSAGE, FORCE_UPDATE, latestMessage),
9692
{ wrapper: ChatProvider },
9793
);
94+
9895
await waitFor(() => {
9996
expect(result.current.previews).toEqual([
10097
{ bold: false, text: '' },
@@ -104,8 +101,16 @@ describe('useLatestMessagePreview', () => {
104101
});
105102

106103
it('should return a mentioned user (@Max) message preview', async () => {
104+
const latestMessage = {
105+
mentioned_users: [{ id: 'Max', name: 'Max' }],
106+
text: 'Max',
107+
user: {
108+
id: 'okechukwu',
109+
},
110+
} as unknown as MessageResponse;
111+
107112
const { result } = renderHook(
108-
() => useLatestMessagePreview(CHANNEL_WITH_MENTIONED_USERS, FORCE_UPDATE, LATEST_MESSAGE),
113+
() => useLatestMessagePreview(CHANNEL_WITH_MENTIONED_USERS, FORCE_UPDATE, latestMessage),
109114
{ wrapper: ChatProvider },
110115
);
111116
await waitFor(() => {
@@ -117,8 +122,15 @@ describe('useLatestMessagePreview', () => {
117122
});
118123

119124
it('should return the latest command preview', async () => {
125+
const latestMessage = {
126+
command: 'giphy',
127+
user: {
128+
id: 'okechukwu',
129+
},
130+
} as unknown as MessageResponse;
131+
120132
const { result } = renderHook(
121-
() => useLatestMessagePreview(CHANNEL_WITH_MESSAGES_COMMAND, FORCE_UPDATE, LATEST_MESSAGE),
133+
() => useLatestMessagePreview(CHANNEL_WITH_MESSAGES_COMMAND, FORCE_UPDATE, latestMessage),
122134
{ wrapper: ChatProvider },
123135
);
124136
await waitFor(() => {
@@ -130,16 +142,39 @@ describe('useLatestMessagePreview', () => {
130142
});
131143

132144
it('should return an attachment preview', async () => {
145+
const latestMessage = {
146+
attachments: ['arbitrary value'],
147+
user: {
148+
id: 'okechukwu',
149+
},
150+
} as unknown as MessageResponse;
151+
133152
const { result } = renderHook(
134-
() =>
135-
useLatestMessagePreview(CHANNEL_WITH_MESSAGES_ATTACHMENTS, FORCE_UPDATE, LATEST_MESSAGE),
153+
() => useLatestMessagePreview(CHANNEL_WITH_MESSAGES_ATTACHMENTS, FORCE_UPDATE, latestMessage),
136154
{ wrapper: ChatProvider },
137155
);
156+
138157
await waitFor(() => {
139158
expect(result.current.previews).toEqual([
140159
{ bold: true, text: '@okechukwu: ' },
141160
{ bold: false, text: '🏙 Attachment...' },
142161
]);
143162
});
144163
});
164+
165+
it('should default to messages from the channel state if latestMessage is undefined', async () => {
166+
const latestMessage = undefined;
167+
168+
const { result } = renderHook(
169+
() => useLatestMessagePreview(CHANNEL_WITH_MESSAGES_TEXT, FORCE_UPDATE, latestMessage),
170+
{ wrapper: ChatProvider },
171+
);
172+
173+
await waitFor(() => {
174+
expect(result.current.previews).toEqual([
175+
{ bold: true, text: '@okechukwu: ' },
176+
{ bold: false, text: 'jkbkbiubicbi' },
177+
]);
178+
});
179+
});
145180
});

package/src/components/ChannelPreview/hooks/useLatestMessagePreview.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,10 @@ const getLatestMessagePreview = <
207207
status: MessageReadStatus.NOT_SENT_BY_CURRENT_USER,
208208
};
209209
}
210-
const message = lastMessage || messages.length ? messages[messages.length - 1] : undefined;
210+
211+
const channelStateLastMessage = messages.length ? messages[messages.length - 1] : undefined;
212+
213+
const message = lastMessage !== undefined ? lastMessage : channelStateLastMessage;
211214

212215
return {
213216
created_at: getLatestMessageDisplayDate(message, tDateTimeParser),

0 commit comments

Comments
 (0)