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.
Files changed (46) hide show
  1. package/lib/commonjs/components/AutoCompleteInput/AutoCompleteInput.js +5 -5
  2. package/lib/commonjs/components/AutoCompleteInput/AutoCompleteInput.js.map +1 -1
  3. package/lib/commonjs/components/Channel/Channel.js +103 -75
  4. package/lib/commonjs/components/Channel/Channel.js.map +1 -1
  5. package/lib/commonjs/components/ChannelList/hooks/usePaginatedChannels.js +32 -11
  6. package/lib/commonjs/components/ChannelList/hooks/usePaginatedChannels.js.map +1 -1
  7. package/lib/commonjs/components/Chat/hooks/useIsOnline.js +23 -24
  8. package/lib/commonjs/components/Chat/hooks/useIsOnline.js.map +1 -1
  9. package/lib/commonjs/components/Message/MessageSimple/utils/renderText.js +1 -1
  10. package/lib/commonjs/components/Message/MessageSimple/utils/renderText.js.map +1 -1
  11. package/lib/commonjs/hooks/useIsMountedRef.js +19 -0
  12. package/lib/commonjs/hooks/useIsMountedRef.js.map +1 -0
  13. package/lib/commonjs/hooks/useStreami18n.js +4 -1
  14. package/lib/commonjs/hooks/useStreami18n.js.map +1 -1
  15. package/lib/commonjs/i18n/fr.json +1 -1
  16. package/lib/commonjs/version.json +1 -1
  17. package/lib/module/components/AutoCompleteInput/AutoCompleteInput.js +5 -5
  18. package/lib/module/components/AutoCompleteInput/AutoCompleteInput.js.map +1 -1
  19. package/lib/module/components/Channel/Channel.js +103 -75
  20. package/lib/module/components/Channel/Channel.js.map +1 -1
  21. package/lib/module/components/ChannelList/hooks/usePaginatedChannels.js +32 -11
  22. package/lib/module/components/ChannelList/hooks/usePaginatedChannels.js.map +1 -1
  23. package/lib/module/components/Chat/hooks/useIsOnline.js +23 -24
  24. package/lib/module/components/Chat/hooks/useIsOnline.js.map +1 -1
  25. package/lib/module/components/Message/MessageSimple/utils/renderText.js +1 -1
  26. package/lib/module/components/Message/MessageSimple/utils/renderText.js.map +1 -1
  27. package/lib/module/hooks/useIsMountedRef.js +19 -0
  28. package/lib/module/hooks/useIsMountedRef.js.map +1 -0
  29. package/lib/module/hooks/useStreami18n.js +4 -1
  30. package/lib/module/hooks/useStreami18n.js.map +1 -1
  31. package/lib/module/i18n/fr.json +1 -1
  32. package/lib/module/version.json +1 -1
  33. package/lib/typescript/hooks/useIsMountedRef.d.ts +20 -0
  34. package/lib/typescript/hooks/useStreami18n.d.ts +1 -1
  35. package/lib/typescript/i18n/fr.json +1 -1
  36. package/package.json +1 -1
  37. package/src/components/AutoCompleteInput/AutoCompleteInput.tsx +3 -8
  38. package/src/components/Channel/Channel.tsx +43 -15
  39. package/src/components/Channel/__tests__/Channel.test.js +18 -2
  40. package/src/components/ChannelList/hooks/usePaginatedChannels.ts +7 -1
  41. package/src/components/Chat/hooks/useIsOnline.ts +16 -17
  42. package/src/components/Message/MessageSimple/utils/renderText.tsx +1 -1
  43. package/src/hooks/useIsMountedRef.ts +36 -0
  44. package/src/hooks/useStreami18n.ts +5 -3
  45. package/src/i18n/fr.json +1 -1
  46. 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 || !channel?.getConfig?.()?.read_events) {
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
- * The more complex sync logic around internet connectivity (NetInfo) is part of Chat.tsx
783
- * listen to client.connection.recovered and all channel events
784
- */
785
- client.on('connection.recovered', connectionRecoveredHandler);
786
- client.on('connection.changed', connectionChangedHandler);
787
- channel?.on(handleEvent);
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
- client.off('connection.recovered', connectionRecoveredHandler);
791
- client.off('connection.changed', connectionChangedHandler);
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
- !!(channel?.getConfig?.()?.commands || [])?.some((command) => command.name === 'giphy'),
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
- const setConnectionListener = () => {
86
+ let unsubscribeNetInfo: NetInfoSubscription;
87
+ const setNetInfoListener = () => {
87
88
  NetInfo.fetch().then((netInfoState) => {
88
89
  notifyChatClient(netInfoState);
89
90
  });
90
- setUnsubscribeNetInfo(
91
- NetInfo.addEventListener((netInfoState) => {
92
- notifyChatClient(netInfoState);
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
- setIsOnline(status);
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
- setConnectionListener();
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
- client.off('connection.changed', handleChangedEvent);
111
- client.off('connection.recovered', handleRecoveredEvent);
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 type { TranslationContextValue } from '../contexts/translationContext/TranslationContext';
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": "Rejeter",
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
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "3.9.0"
2
+ "version": "3.9.2-next.2"
3
3
  }