stream-chat-react-native-core 3.9.0 → 3.9.2-next.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/commonjs/components/AutoCompleteInput/AutoCompleteInput.js +5 -5
- package/lib/commonjs/components/AutoCompleteInput/AutoCompleteInput.js.map +1 -1
- package/lib/commonjs/components/Channel/Channel.js +103 -75
- package/lib/commonjs/components/Channel/Channel.js.map +1 -1
- package/lib/commonjs/components/ChannelList/hooks/usePaginatedChannels.js +32 -11
- package/lib/commonjs/components/ChannelList/hooks/usePaginatedChannels.js.map +1 -1
- package/lib/commonjs/components/Chat/hooks/useIsOnline.js +23 -24
- package/lib/commonjs/components/Chat/hooks/useIsOnline.js.map +1 -1
- package/lib/commonjs/components/Message/MessageSimple/utils/renderText.js +1 -1
- package/lib/commonjs/components/Message/MessageSimple/utils/renderText.js.map +1 -1
- package/lib/commonjs/hooks/useIsMountedRef.js +19 -0
- package/lib/commonjs/hooks/useIsMountedRef.js.map +1 -0
- package/lib/commonjs/hooks/useStreami18n.js +4 -1
- package/lib/commonjs/hooks/useStreami18n.js.map +1 -1
- package/lib/commonjs/i18n/fr.json +1 -1
- package/lib/commonjs/version.json +1 -1
- package/lib/module/components/AutoCompleteInput/AutoCompleteInput.js +5 -5
- package/lib/module/components/AutoCompleteInput/AutoCompleteInput.js.map +1 -1
- package/lib/module/components/Channel/Channel.js +103 -75
- package/lib/module/components/Channel/Channel.js.map +1 -1
- package/lib/module/components/ChannelList/hooks/usePaginatedChannels.js +32 -11
- package/lib/module/components/ChannelList/hooks/usePaginatedChannels.js.map +1 -1
- package/lib/module/components/Chat/hooks/useIsOnline.js +23 -24
- package/lib/module/components/Chat/hooks/useIsOnline.js.map +1 -1
- package/lib/module/components/Message/MessageSimple/utils/renderText.js +1 -1
- package/lib/module/components/Message/MessageSimple/utils/renderText.js.map +1 -1
- package/lib/module/hooks/useIsMountedRef.js +19 -0
- package/lib/module/hooks/useIsMountedRef.js.map +1 -0
- package/lib/module/hooks/useStreami18n.js +4 -1
- package/lib/module/hooks/useStreami18n.js.map +1 -1
- package/lib/module/i18n/fr.json +1 -1
- package/lib/module/version.json +1 -1
- package/lib/typescript/hooks/useIsMountedRef.d.ts +20 -0
- package/lib/typescript/hooks/useStreami18n.d.ts +1 -1
- package/lib/typescript/i18n/fr.json +1 -1
- package/package.json +1 -1
- package/src/components/AutoCompleteInput/AutoCompleteInput.tsx +3 -8
- package/src/components/Channel/Channel.tsx +43 -15
- package/src/components/Channel/__tests__/Channel.test.js +18 -2
- package/src/components/ChannelList/hooks/usePaginatedChannels.ts +7 -1
- package/src/components/Chat/hooks/useIsOnline.ts +16 -17
- package/src/components/Message/MessageSimple/utils/renderText.tsx +1 -1
- package/src/hooks/useIsMountedRef.ts +36 -0
- package/src/hooks/useStreami18n.ts +5 -3
- package/src/i18n/fr.json +1 -1
- package/src/version.json +1 -1
|
@@ -544,7 +544,7 @@ const ChannelWithContext = <
|
|
|
544
544
|
colors: { black },
|
|
545
545
|
},
|
|
546
546
|
} = useTheme();
|
|
547
|
-
|
|
547
|
+
const [deleted, setDeleted] = useState(false);
|
|
548
548
|
const [editing, setEditing] = useState<boolean | MessageType<At, Ch, Co, Ev, Me, Re, Us>>(false);
|
|
549
549
|
const [error, setError] = useState(false);
|
|
550
550
|
const [hasMore, setHasMore] = useState(true);
|
|
@@ -670,7 +670,7 @@ const ChannelWithContext = <
|
|
|
670
670
|
const markRead: ChannelContextValue<At, Ch, Co, Ev, Me, Re, Us>['markRead'] = useRef(
|
|
671
671
|
throttle(
|
|
672
672
|
() => {
|
|
673
|
-
if (channel?.disconnected || !
|
|
673
|
+
if (!channel || channel?.disconnected || !clientChannelConfig?.read_events) {
|
|
674
674
|
return;
|
|
675
675
|
}
|
|
676
676
|
|
|
@@ -778,18 +778,33 @@ const ChannelWithContext = <
|
|
|
778
778
|
};
|
|
779
779
|
|
|
780
780
|
useEffect(() => {
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
781
|
+
const channelSubscriptions: Array<ReturnType<ChannelType['on']>> = [];
|
|
782
|
+
const clientSubscriptions: Array<ReturnType<StreamChat['on']>> = [];
|
|
783
|
+
|
|
784
|
+
const subscribe = () => {
|
|
785
|
+
if (!channel) return;
|
|
786
|
+
|
|
787
|
+
/**
|
|
788
|
+
* The more complex sync logic around internet connectivity (NetInfo) is part of Chat.tsx
|
|
789
|
+
* listen to client.connection.recovered and all channel events
|
|
790
|
+
*/
|
|
791
|
+
clientSubscriptions.push(client.on('connection.recovered', connectionRecoveredHandler));
|
|
792
|
+
clientSubscriptions.push(client.on('connection.changed', connectionChangedHandler));
|
|
793
|
+
clientSubscriptions.push(
|
|
794
|
+
client.on('channel.deleted', (event) => {
|
|
795
|
+
if (event.cid === channel.cid) {
|
|
796
|
+
setDeleted(true);
|
|
797
|
+
}
|
|
798
|
+
}),
|
|
799
|
+
);
|
|
800
|
+
channelSubscriptions.push(channel.on(handleEvent));
|
|
801
|
+
};
|
|
802
|
+
|
|
803
|
+
subscribe();
|
|
788
804
|
|
|
789
805
|
return () => {
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
channel?.off(handleEvent);
|
|
806
|
+
clientSubscriptions.forEach((s) => s.unsubscribe());
|
|
807
|
+
channelSubscriptions.forEach((s) => s.unsubscribe());
|
|
793
808
|
};
|
|
794
809
|
}, [channelId, connectionRecoveredHandler, handleEvent]);
|
|
795
810
|
|
|
@@ -1152,13 +1167,23 @@ const ChannelWithContext = <
|
|
|
1152
1167
|
}
|
|
1153
1168
|
};
|
|
1154
1169
|
|
|
1170
|
+
// In case the channel is disconnected which may happen when channel is deleted,
|
|
1171
|
+
// underlying js client throws an error. Following function ensures that Channel component
|
|
1172
|
+
// won't result in error in such a case.
|
|
1173
|
+
const getChannelConfigSafely = () => {
|
|
1174
|
+
try {
|
|
1175
|
+
return channel?.getConfig();
|
|
1176
|
+
} catch (_) {
|
|
1177
|
+
return null;
|
|
1178
|
+
}
|
|
1179
|
+
};
|
|
1180
|
+
|
|
1155
1181
|
/**
|
|
1156
1182
|
* Channel configs for use in disabling local functionality.
|
|
1157
1183
|
* Nullish coalescing is used to give first priority to props to override
|
|
1158
1184
|
* the server settings. Then priority to server settings to override defaults.
|
|
1159
1185
|
*/
|
|
1160
|
-
const clientChannelConfig =
|
|
1161
|
-
typeof channel?.getConfig === 'function' ? channel.getConfig() : undefined;
|
|
1186
|
+
const clientChannelConfig = getChannelConfigSafely();
|
|
1162
1187
|
|
|
1163
1188
|
const messagesConfig: MessagesConfig = {
|
|
1164
1189
|
/**
|
|
@@ -1597,7 +1622,7 @@ const ChannelWithContext = <
|
|
|
1597
1622
|
error,
|
|
1598
1623
|
giphyEnabled:
|
|
1599
1624
|
giphyEnabled ??
|
|
1600
|
-
!!(
|
|
1625
|
+
!!(clientChannelConfig?.commands || [])?.some((command) => command.name === 'giphy'),
|
|
1601
1626
|
hideDateSeparators,
|
|
1602
1627
|
hideStickyDateHeader,
|
|
1603
1628
|
isAdmin,
|
|
@@ -1780,6 +1805,9 @@ const ChannelWithContext = <
|
|
|
1780
1805
|
typing,
|
|
1781
1806
|
});
|
|
1782
1807
|
|
|
1808
|
+
// TODO: replace the null view with appropriate message. Currently this is waiting a design decision.
|
|
1809
|
+
if (deleted) return null;
|
|
1810
|
+
|
|
1783
1811
|
if (!channel || (error && messages.length === 0)) {
|
|
1784
1812
|
return <LoadingErrorIndicator error={error} listType='message' retry={reloadChannel} />;
|
|
1785
1813
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React, { useContext, useEffect } from 'react';
|
|
2
2
|
import { View } from 'react-native';
|
|
3
|
-
import { cleanup, render, waitFor } from '@testing-library/react-native';
|
|
3
|
+
import { act, cleanup, render, waitFor } from '@testing-library/react-native';
|
|
4
4
|
import { StreamChat } from 'stream-chat';
|
|
5
5
|
|
|
6
6
|
import { Channel } from '../Channel';
|
|
@@ -14,8 +14,10 @@ import {
|
|
|
14
14
|
MessagesProvider,
|
|
15
15
|
} from '../../../contexts/messagesContext/MessagesContext';
|
|
16
16
|
import { ThreadContext, ThreadProvider } from '../../../contexts/threadContext/ThreadContext';
|
|
17
|
+
|
|
17
18
|
import { useMockedApis } from '../../../mock-builders/api/useMockedApis';
|
|
18
19
|
import { getOrCreateChannelApi } from '../../../mock-builders/api/getOrCreateChannel';
|
|
20
|
+
import dispatchChannelDeletedEvent from '../../../mock-builders/event/channelDeleted';
|
|
19
21
|
import { generateChannel } from '../../../mock-builders/generator/channel';
|
|
20
22
|
import { generateMember } from '../../../mock-builders/generator/member';
|
|
21
23
|
import { generateMessage } from '../../../mock-builders/generator/message';
|
|
@@ -65,6 +67,7 @@ describe('Channel', () => {
|
|
|
65
67
|
chatClient = await getTestClientWithUser(user);
|
|
66
68
|
useMockedApis(chatClient, [getOrCreateChannelApi(mockedChannel)]);
|
|
67
69
|
channel = chatClient.channel('messaging', mockedChannel.id);
|
|
70
|
+
channel.cid = mockedChannel.channel.cid;
|
|
68
71
|
});
|
|
69
72
|
|
|
70
73
|
afterEach(() => {
|
|
@@ -77,7 +80,9 @@ describe('Channel', () => {
|
|
|
77
80
|
...channel,
|
|
78
81
|
cid: null,
|
|
79
82
|
off: () => {},
|
|
80
|
-
on: () => {
|
|
83
|
+
on: () => ({
|
|
84
|
+
unsubscribe: () => null,
|
|
85
|
+
}),
|
|
81
86
|
watch: () => {},
|
|
82
87
|
};
|
|
83
88
|
const { getByTestId } = renderComponent({ channel: nullChannel });
|
|
@@ -193,6 +198,17 @@ describe('Channel', () => {
|
|
|
193
198
|
await waitFor(() => expect(channelQuerySpy).toHaveBeenCalled());
|
|
194
199
|
});
|
|
195
200
|
|
|
201
|
+
it('should render null if channel gets deleted', async () => {
|
|
202
|
+
const { getByTestId, queryByTestId } = renderComponent({
|
|
203
|
+
channel,
|
|
204
|
+
children: <View testID='children' />,
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
await waitFor(() => expect(getByTestId('children')).toBeTruthy());
|
|
208
|
+
act(() => dispatchChannelDeletedEvent(chatClient, channel));
|
|
209
|
+
expect(queryByTestId('children')).toBeNull();
|
|
210
|
+
});
|
|
211
|
+
|
|
196
212
|
describe('ChannelContext', () => {
|
|
197
213
|
it('renders children without crashing', async () => {
|
|
198
214
|
const { getByTestId } = render(
|
|
@@ -3,6 +3,7 @@ import { useEffect, useMemo, useRef, useState } from 'react';
|
|
|
3
3
|
import { MAX_QUERY_CHANNELS_LIMIT } from '../utils';
|
|
4
4
|
|
|
5
5
|
import { useChatContext } from '../../../contexts/chatContext/ChatContext';
|
|
6
|
+
import { useIsMountedRef } from '../../../hooks/useIsMountedRef';
|
|
6
7
|
|
|
7
8
|
import type { Channel, ChannelFilters, ChannelOptions, ChannelSort } from 'stream-chat';
|
|
8
9
|
|
|
@@ -58,9 +59,10 @@ export const usePaginatedChannels = <
|
|
|
58
59
|
const [loadingChannels, setLoadingChannels] = useState(false);
|
|
59
60
|
const [loadingNextPage, setLoadingNextPage] = useState(false);
|
|
60
61
|
const [refreshing, setRefreshing] = useState(false);
|
|
62
|
+
const isMounted = useIsMountedRef();
|
|
61
63
|
|
|
62
64
|
const queryChannels = async (queryType = '', retryCount = 0): Promise<void> => {
|
|
63
|
-
if (!client || loadingChannels || loadingNextPage || refreshing) return;
|
|
65
|
+
if (!client || loadingChannels || loadingNextPage || refreshing || !isMounted.current) return;
|
|
64
66
|
|
|
65
67
|
if (queryType === 'reload') {
|
|
66
68
|
setLoadingChannels(true);
|
|
@@ -79,6 +81,8 @@ export const usePaginatedChannels = <
|
|
|
79
81
|
try {
|
|
80
82
|
const channelQueryResponse = await client.queryChannels(filters, sort, newOptions);
|
|
81
83
|
|
|
84
|
+
if (!isMounted.current) return;
|
|
85
|
+
|
|
82
86
|
channelQueryResponse.forEach((channel) => channel.state.setIsUpToDate(true));
|
|
83
87
|
|
|
84
88
|
const newChannels =
|
|
@@ -92,6 +96,8 @@ export const usePaginatedChannels = <
|
|
|
92
96
|
} catch (err) {
|
|
93
97
|
await wait(2000);
|
|
94
98
|
|
|
99
|
+
if (!isMounted.current) return;
|
|
100
|
+
|
|
95
101
|
if (retryCount === 3) {
|
|
96
102
|
setLoadingChannels(false);
|
|
97
103
|
setLoadingNextPage(false);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useCallback, useEffect, useState } from 'react';
|
|
2
2
|
|
|
3
3
|
import { useAppStateListener } from '../../../hooks/useAppStateListener';
|
|
4
|
+
import { useIsMountedRef } from '../../../hooks/useIsMountedRef';
|
|
4
5
|
import { NetInfo } from '../../../native';
|
|
5
6
|
|
|
6
7
|
import type { NetInfoSubscription } from '@react-native-community/netinfo';
|
|
@@ -35,10 +36,9 @@ export const useIsOnline = <
|
|
|
35
36
|
client: StreamChat<At, Ch, Co, Ev, Me, Re, Us>,
|
|
36
37
|
closeConnectionOnBackground = true,
|
|
37
38
|
) => {
|
|
38
|
-
const [unsubscribeNetInfo, setUnsubscribeNetInfo] = useState<NetInfoSubscription>();
|
|
39
39
|
const [isOnline, setIsOnline] = useState(true);
|
|
40
40
|
const [connectionRecovering, setConnectionRecovering] = useState(false);
|
|
41
|
-
|
|
41
|
+
const isMounted = useIsMountedRef();
|
|
42
42
|
const clientExits = !!client;
|
|
43
43
|
|
|
44
44
|
const onBackground = useCallback(() => {
|
|
@@ -83,35 +83,34 @@ export const useIsOnline = <
|
|
|
83
83
|
}
|
|
84
84
|
};
|
|
85
85
|
|
|
86
|
-
|
|
86
|
+
let unsubscribeNetInfo: NetInfoSubscription;
|
|
87
|
+
const setNetInfoListener = () => {
|
|
87
88
|
NetInfo.fetch().then((netInfoState) => {
|
|
88
89
|
notifyChatClient(netInfoState);
|
|
89
90
|
});
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
}),
|
|
94
|
-
);
|
|
91
|
+
unsubscribeNetInfo = NetInfo.addEventListener((netInfoState) => {
|
|
92
|
+
notifyChatClient(netInfoState);
|
|
93
|
+
});
|
|
95
94
|
};
|
|
96
95
|
|
|
97
96
|
const setInitialOnlineState = async () => {
|
|
98
97
|
const status = await NetInfo.fetch();
|
|
99
|
-
|
|
98
|
+
|
|
99
|
+
if (isMounted.current) setIsOnline(status);
|
|
100
100
|
};
|
|
101
101
|
|
|
102
102
|
setInitialOnlineState();
|
|
103
|
+
const chatListeners: Array<ReturnType<StreamChat['on']>> = [];
|
|
104
|
+
|
|
103
105
|
if (client) {
|
|
104
|
-
client.on('connection.changed', handleChangedEvent);
|
|
105
|
-
client.on('connection.recovered', handleRecoveredEvent);
|
|
106
|
-
|
|
106
|
+
chatListeners.push(client.on('connection.changed', handleChangedEvent));
|
|
107
|
+
chatListeners.push(client.on('connection.recovered', handleRecoveredEvent));
|
|
108
|
+
setNetInfoListener();
|
|
107
109
|
}
|
|
108
110
|
|
|
109
111
|
return () => {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
if (unsubscribeNetInfo) {
|
|
113
|
-
unsubscribeNetInfo();
|
|
114
|
-
}
|
|
112
|
+
chatListeners.forEach((listener) => listener.unsubscribe?.());
|
|
113
|
+
unsubscribeNetInfo?.();
|
|
115
114
|
};
|
|
116
115
|
}, [clientExits]);
|
|
117
116
|
|
|
@@ -262,7 +262,7 @@ export const renderText = <
|
|
|
262
262
|
<Markdown
|
|
263
263
|
key={`${JSON.stringify(mentioned_users)}-${onlyEmojis}-${
|
|
264
264
|
messageOverlay ? JSON.stringify(markdownStyles) : undefined
|
|
265
|
-
}`}
|
|
265
|
+
}-${JSON.stringify(colors)}`}
|
|
266
266
|
onLink={onLink}
|
|
267
267
|
rules={{
|
|
268
268
|
...customRules,
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Returns mount status of component. This hook can be used for purpose of avoiding any
|
|
5
|
+
* setState calls (within async operation) after component gets unmounted.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```
|
|
9
|
+
* const isMounted = useIsMountedRef();
|
|
10
|
+
* const [dummyValue, setDummyValue] = useState(false);
|
|
11
|
+
*
|
|
12
|
+
* useEffect(() => {
|
|
13
|
+
* someAsyncOperation().then(() => {
|
|
14
|
+
* if (isMounted.current) setDummyValue(true);
|
|
15
|
+
* })
|
|
16
|
+
* })
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* @returns isMounted {Object} Mount status ref for the component.
|
|
20
|
+
*/
|
|
21
|
+
export const useIsMountedRef = () => {
|
|
22
|
+
// Initial value has been set to true, since this hook exist only for sole purpose of
|
|
23
|
+
// avoiding any setState calls (within async operation) after component gets unmounted.
|
|
24
|
+
// Basically we don't need to know about state change from component being initialized -> mounted.
|
|
25
|
+
// We only need a state change from initialized or mounted state -> unmounted state.
|
|
26
|
+
const isMounted = useRef(true);
|
|
27
|
+
|
|
28
|
+
useEffect(
|
|
29
|
+
() => () => {
|
|
30
|
+
isMounted.current = false;
|
|
31
|
+
},
|
|
32
|
+
[],
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
return isMounted;
|
|
36
|
+
};
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { useEffect, useState } from 'react';
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
import { useIsMountedRef } from './useIsMountedRef';
|
|
4
4
|
import { Streami18n } from '../utils/Streami18n';
|
|
5
5
|
|
|
6
|
+
import type { TranslationContextValue } from '../contexts/translationContext/TranslationContext';
|
|
7
|
+
|
|
6
8
|
export const useStreami18n = ({
|
|
7
9
|
i18nInstance,
|
|
8
10
|
setTranslators,
|
|
@@ -11,7 +13,7 @@ export const useStreami18n = ({
|
|
|
11
13
|
i18nInstance?: Streami18n;
|
|
12
14
|
}) => {
|
|
13
15
|
const [loadingTranslators, setLoadingTranslators] = useState(true);
|
|
14
|
-
|
|
16
|
+
const isMounted = useIsMountedRef();
|
|
15
17
|
const i18nInstanceExists = !!i18nInstance;
|
|
16
18
|
useEffect(() => {
|
|
17
19
|
let streami18n: Streami18n;
|
|
@@ -26,7 +28,7 @@ export const useStreami18n = ({
|
|
|
26
28
|
setTranslators((prevTranslator) => ({ ...prevTranslator, t })),
|
|
27
29
|
);
|
|
28
30
|
streami18n.getTranslators().then((translator) => {
|
|
29
|
-
if (translator) setTranslators(translator);
|
|
31
|
+
if (translator && isMounted.current) setTranslators(translator);
|
|
30
32
|
});
|
|
31
33
|
|
|
32
34
|
setLoadingTranslators(false);
|
package/src/i18n/fr.json
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"Copy Message": "Copier le message",
|
|
10
10
|
"Delete": "Supprimer",
|
|
11
11
|
"Delete Message": "Supprimer un message",
|
|
12
|
-
"Dismiss": "
|
|
12
|
+
"Dismiss": "Fermer",
|
|
13
13
|
"Do you want to send a copy of this message to a moderator for further investigation?": "Voulez-vous envoyer une copie de ce message à un modérateur pour une enquête plus approfondie?",
|
|
14
14
|
"Edit Message": "Éditer un message",
|
|
15
15
|
"Editing Message": "Édite un message",
|
package/src/version.json
CHANGED