stream-chat-react-native-core 6.7.3-beta.2 → 6.7.3-beta.3

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 (63) hide show
  1. package/lib/commonjs/components/Channel/Channel.js +273 -281
  2. package/lib/commonjs/components/Channel/Channel.js.map +1 -1
  3. package/lib/commonjs/components/Channel/hooks/useMessageListPagination.js +133 -147
  4. package/lib/commonjs/components/Channel/hooks/useMessageListPagination.js.map +1 -1
  5. package/lib/commonjs/components/KeyboardCompatibleView/KeyboardCompatibleView.js +7 -12
  6. package/lib/commonjs/components/KeyboardCompatibleView/KeyboardCompatibleView.js.map +1 -1
  7. package/lib/commonjs/components/MessageList/MessageList.js +167 -179
  8. package/lib/commonjs/components/MessageList/MessageList.js.map +1 -1
  9. package/lib/commonjs/components/MessageList/hooks/useMessageList.js +60 -37
  10. package/lib/commonjs/components/MessageList/hooks/useMessageList.js.map +1 -1
  11. package/lib/commonjs/contexts/messageInputContext/MessageInputContext.js +450 -459
  12. package/lib/commonjs/contexts/messageInputContext/MessageInputContext.js.map +1 -1
  13. package/lib/commonjs/contexts/messagesContext/MessagesContext.js.map +1 -1
  14. package/lib/commonjs/hooks/index.js +11 -0
  15. package/lib/commonjs/hooks/index.js.map +1 -1
  16. package/lib/commonjs/hooks/useStableCallback.js +13 -0
  17. package/lib/commonjs/hooks/useStableCallback.js.map +1 -0
  18. package/lib/commonjs/version.json +1 -1
  19. package/lib/module/components/Channel/Channel.js +273 -281
  20. package/lib/module/components/Channel/Channel.js.map +1 -1
  21. package/lib/module/components/Channel/hooks/useMessageListPagination.js +133 -147
  22. package/lib/module/components/Channel/hooks/useMessageListPagination.js.map +1 -1
  23. package/lib/module/components/KeyboardCompatibleView/KeyboardCompatibleView.js +7 -12
  24. package/lib/module/components/KeyboardCompatibleView/KeyboardCompatibleView.js.map +1 -1
  25. package/lib/module/components/MessageList/MessageList.js +167 -179
  26. package/lib/module/components/MessageList/MessageList.js.map +1 -1
  27. package/lib/module/components/MessageList/hooks/useMessageList.js +60 -37
  28. package/lib/module/components/MessageList/hooks/useMessageList.js.map +1 -1
  29. package/lib/module/contexts/messageInputContext/MessageInputContext.js +450 -459
  30. package/lib/module/contexts/messageInputContext/MessageInputContext.js.map +1 -1
  31. package/lib/module/contexts/messagesContext/MessagesContext.js.map +1 -1
  32. package/lib/module/hooks/index.js +11 -0
  33. package/lib/module/hooks/index.js.map +1 -1
  34. package/lib/module/hooks/useStableCallback.js +13 -0
  35. package/lib/module/hooks/useStableCallback.js.map +1 -0
  36. package/lib/module/version.json +1 -1
  37. package/lib/typescript/components/Channel/Channel.d.ts.map +1 -1
  38. package/lib/typescript/components/Channel/hooks/useMessageListPagination.d.ts +3 -3
  39. package/lib/typescript/components/Channel/hooks/useMessageListPagination.d.ts.map +1 -1
  40. package/lib/typescript/components/KeyboardCompatibleView/KeyboardCompatibleView.d.ts +3 -0
  41. package/lib/typescript/components/KeyboardCompatibleView/KeyboardCompatibleView.d.ts.map +1 -1
  42. package/lib/typescript/components/MessageList/MessageList.d.ts.map +1 -1
  43. package/lib/typescript/components/MessageList/hooks/useMessageList.d.ts +4 -0
  44. package/lib/typescript/components/MessageList/hooks/useMessageList.d.ts.map +1 -1
  45. package/lib/typescript/contexts/messageInputContext/MessageInputContext.d.ts.map +1 -1
  46. package/lib/typescript/contexts/messagesContext/MessagesContext.d.ts +1 -1
  47. package/lib/typescript/contexts/messagesContext/MessagesContext.d.ts.map +1 -1
  48. package/lib/typescript/hooks/index.d.ts +1 -0
  49. package/lib/typescript/hooks/index.d.ts.map +1 -1
  50. package/lib/typescript/hooks/useStableCallback.d.ts +26 -0
  51. package/lib/typescript/hooks/useStableCallback.d.ts.map +1 -0
  52. package/package.json +1 -1
  53. package/src/components/Channel/Channel.tsx +424 -408
  54. package/src/components/Channel/hooks/useMessageListPagination.tsx +152 -147
  55. package/src/components/KeyboardCompatibleView/KeyboardCompatibleView.tsx +6 -4
  56. package/src/components/MessageList/MessageList.tsx +147 -112
  57. package/src/components/MessageList/hooks/useMessageList.ts +69 -38
  58. package/src/contexts/messageInputContext/MessageInputContext.tsx +293 -267
  59. package/src/contexts/messageInputContext/__tests__/pickFile.test.tsx +2 -1
  60. package/src/contexts/messagesContext/MessagesContext.tsx +1 -0
  61. package/src/hooks/index.ts +1 -0
  62. package/src/hooks/useStableCallback.ts +37 -0
  63. package/src/version.json +1 -1
@@ -74,6 +74,7 @@ import {
74
74
  useTranslationContext,
75
75
  } from '../../contexts/translationContext/TranslationContext';
76
76
  import { TypingProvider } from '../../contexts/typingContext/TypingContext';
77
+ import { useStableCallback } from '../../hooks';
77
78
  import { useAppStateListener } from '../../hooks/useAppStateListener';
78
79
 
79
80
  import {
@@ -716,6 +717,12 @@ const ChannelWithContext = <
716
717
  * Its a map of filename to AbortController
717
718
  */
718
719
  const uploadAbortControllerRef = useRef<Map<string, AbortController>>(new Map());
720
+ /**
721
+ * This ref keeps track of message IDs which have already been optimistically updated.
722
+ * We need it to make sure we don't react on message.new/notification.message_new events
723
+ * if this is indeed the case, as it's a full list update for nothing.
724
+ */
725
+ const optimisticallyUpdatedNewMessages = useMemo<Set<string>>(() => new Set(), []);
719
726
 
720
727
  const channelId = channel?.id || '';
721
728
  const pollCreationEnabled = !channel.disconnected && !!channel?.id && channel?.getConfig()?.polls;
@@ -784,7 +791,7 @@ const ChannelWithContext = <
784
791
  [stateUpdateThrottleInterval, channel, copyStateFromChannel, copyMessagesStateFromChannel],
785
792
  );
786
793
 
787
- const handleEvent: EventHandler<StreamChatGenerics> = (event) => {
794
+ const handleEvent: EventHandler<StreamChatGenerics> = useStableCallback((event) => {
788
795
  if (shouldSyncChannel) {
789
796
  /**
790
797
  * Ignore user.watching.start and user.watching.stop as we should not copy the entire state when
@@ -838,8 +845,16 @@ const ChannelWithContext = <
838
845
 
839
846
  // only update channel state if the events are not the previously subscribed useEffect's subscription events
840
847
  if (channel && channel.initialized) {
841
- if (event.type === 'message.new') {
842
- copyMessagesStateFromChannelThrottled();
848
+ // we skip the new message events if we've already done an optimistic update for the new message
849
+ if (event.type === 'message.new' || event.type === 'notification.message_new') {
850
+ const messageId = event.message?.id ?? '';
851
+ if (
852
+ event.user?.id !== client.userID ||
853
+ !optimisticallyUpdatedNewMessages.has(messageId)
854
+ ) {
855
+ copyMessagesStateFromChannelThrottled();
856
+ }
857
+ optimisticallyUpdatedNewMessages.delete(messageId);
843
858
  return;
844
859
  }
845
860
 
@@ -851,7 +866,7 @@ const ChannelWithContext = <
851
866
  copyChannelState();
852
867
  }
853
868
  }
854
- };
869
+ });
855
870
 
856
871
  useEffect(() => {
857
872
  let listener: ReturnType<typeof channel.on>;
@@ -961,7 +976,7 @@ const ChannelWithContext = <
961
976
  /**
962
977
  * CHANNEL METHODS
963
978
  */
964
- const markRead: ChannelContextValue<StreamChatGenerics>['markRead'] = throttle(
979
+ const markReadInternal: ChannelContextValue<StreamChatGenerics>['markRead'] = throttle(
965
980
  async (options?: MarkReadFunctionOptions) => {
966
981
  const { updateChannelUnreadState = true } = options ?? {};
967
982
  if (!channel || channel?.disconnected || !clientChannelConfig?.read_events) {
@@ -990,7 +1005,9 @@ const ChannelWithContext = <
990
1005
  throttleOptions,
991
1006
  );
992
1007
 
993
- const reloadThread = async () => {
1008
+ const markRead = useStableCallback(markReadInternal);
1009
+
1010
+ const reloadThread = useStableCallback(async () => {
994
1011
  if (!channel || !thread?.id) {
995
1012
  return;
996
1013
  }
@@ -1023,9 +1040,9 @@ const ChannelWithContext = <
1023
1040
  setThreadLoadingMore(false);
1024
1041
  throw err;
1025
1042
  }
1026
- };
1043
+ });
1027
1044
 
1028
- const resyncChannel = async () => {
1045
+ const resyncChannel = useStableCallback(async () => {
1029
1046
  if (!channel || syncingChannelRef.current) {
1030
1047
  return;
1031
1048
  }
@@ -1081,7 +1098,7 @@ const ChannelWithContext = <
1081
1098
  }
1082
1099
 
1083
1100
  syncingChannelRef.current = false;
1084
- };
1101
+ });
1085
1102
 
1086
1103
  // resync channel is added to ref so that it can be used in useEffect without adding it as a dependency
1087
1104
  const resyncChannelRef = useRef(resyncChannel);
@@ -1132,16 +1149,16 @@ const ChannelWithContext = <
1132
1149
  */
1133
1150
  const clientChannelConfig = getChannelConfigSafely();
1134
1151
 
1135
- const reloadChannel = async () => {
1152
+ const reloadChannel = useStableCallback(async () => {
1136
1153
  try {
1137
1154
  await loadLatestMessages();
1138
1155
  } catch (err) {
1139
1156
  console.warn('Reloading channel failed with error:', err);
1140
1157
  }
1141
- };
1158
+ });
1142
1159
 
1143
1160
  const loadChannelAroundMessage: ChannelContextValue<StreamChatGenerics>['loadChannelAroundMessage'] =
1144
- async ({ messageId: messageIdToLoadAround }): Promise<void> => {
1161
+ useStableCallback(async ({ messageId: messageIdToLoadAround }): Promise<void> => {
1145
1162
  if (!messageIdToLoadAround) {
1146
1163
  return;
1147
1164
  }
@@ -1172,350 +1189,354 @@ const ChannelWithContext = <
1172
1189
  } catch (err) {
1173
1190
  console.warn('Loading channel around message failed with error:', err);
1174
1191
  }
1175
- };
1192
+ });
1176
1193
 
1177
1194
  /**
1178
1195
  * MESSAGE METHODS
1179
1196
  */
1180
- const updateMessage: MessagesContextValue<StreamChatGenerics>['updateMessage'] = (
1181
- updatedMessage,
1182
- extraState = {},
1183
- ) => {
1184
- if (!channel) {
1185
- return;
1186
- }
1197
+ const updateMessage: MessagesContextValue<StreamChatGenerics>['updateMessage'] =
1198
+ useStableCallback((updatedMessage, extraState = {}, throttled = false) => {
1199
+ if (!channel) {
1200
+ return;
1201
+ }
1187
1202
 
1188
- channel.state.addMessageSorted(updatedMessage, true);
1189
- copyMessagesStateFromChannel(channel);
1203
+ channel.state.addMessageSorted(updatedMessage, true);
1204
+ if (throttled) {
1205
+ copyMessagesStateFromChannelThrottled();
1206
+ } else {
1207
+ copyMessagesStateFromChannel(channel);
1208
+ }
1190
1209
 
1191
- if (thread && updatedMessage.parent_id) {
1192
- extraState.threadMessages = channel.state.threads[updatedMessage.parent_id] || [];
1193
- setThreadMessages(extraState.threadMessages);
1194
- }
1195
- };
1210
+ if (thread && updatedMessage.parent_id) {
1211
+ extraState.threadMessages = channel.state.threads[updatedMessage.parent_id] || [];
1212
+ setThreadMessages(extraState.threadMessages);
1213
+ }
1214
+ });
1196
1215
 
1197
- const replaceMessage = (
1198
- oldMessage: MessageResponse<StreamChatGenerics>,
1199
- newMessage: MessageResponse<StreamChatGenerics>,
1200
- ) => {
1201
- if (channel) {
1202
- channel.state.removeMessage(oldMessage);
1203
- channel.state.addMessageSorted(newMessage, true);
1204
- copyMessagesStateFromChannel(channel);
1216
+ const replaceMessage = useStableCallback(
1217
+ (
1218
+ oldMessage: MessageResponse<StreamChatGenerics>,
1219
+ newMessage: MessageResponse<StreamChatGenerics>,
1220
+ ) => {
1221
+ if (channel) {
1222
+ channel.state.removeMessage(oldMessage);
1223
+ channel.state.addMessageSorted(newMessage, true);
1224
+ copyMessagesStateFromChannel(channel);
1205
1225
 
1206
- if (thread && newMessage.parent_id) {
1207
- const threadMessages = channel.state.threads[newMessage.parent_id] || [];
1208
- setThreadMessages(threadMessages);
1226
+ if (thread && newMessage.parent_id) {
1227
+ const threadMessages = channel.state.threads[newMessage.parent_id] || [];
1228
+ setThreadMessages(threadMessages);
1229
+ }
1209
1230
  }
1210
- }
1211
- };
1231
+ },
1232
+ );
1212
1233
 
1213
- const createMessagePreview = ({
1214
- attachments,
1215
- mentioned_users,
1216
- parent_id,
1217
- poll,
1218
- poll_id,
1219
- text,
1220
- ...extraFields
1221
- }: Partial<StreamMessage<StreamChatGenerics>>) => {
1222
- // Exclude following properties from message.user within message preview,
1223
- // since they could be long arrays and have no meaning as sender of message.
1224
- // Storing such large value within user's table may cause sqlite queries to crash.
1225
- // @ts-ignore
1226
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
1227
- const { channel_mutes, devices, mutes, ...messageUser } = client.user;
1228
-
1229
- const preview = {
1230
- __html: text,
1234
+ const createMessagePreview = useStableCallback(
1235
+ ({
1231
1236
  attachments,
1232
- created_at: new Date(),
1233
- html: text,
1234
- id: `${client.userID}-${generateRandomId()}`,
1235
- mentioned_users:
1236
- mentioned_users?.map((userId) => ({
1237
- id: userId,
1238
- })) || [],
1237
+ mentioned_users,
1239
1238
  parent_id,
1240
1239
  poll,
1241
1240
  poll_id,
1242
- reactions: [],
1243
- status: MessageStatusTypes.SENDING,
1244
1241
  text,
1245
- type: 'regular',
1246
- user: {
1247
- ...messageUser,
1248
- id: client.userID,
1249
- },
1250
- ...extraFields,
1251
- } as unknown as MessageResponse<StreamChatGenerics>;
1252
-
1253
- /**
1254
- * This is added to the message for local rendering prior to the message
1255
- * being returned from the backend, it is removed when the message is sent
1256
- * as quoted_message is a reserved field.
1257
- */
1258
- if (preview.quoted_message_id) {
1259
- const quotedMessage = channelMessagesState.messages?.find(
1260
- (message) => message.id === preview.quoted_message_id,
1261
- );
1242
+ ...extraFields
1243
+ }: Partial<StreamMessage<StreamChatGenerics>>) => {
1244
+ // Exclude following properties from message.user within message preview,
1245
+ // since they could be long arrays and have no meaning as sender of message.
1246
+ // Storing such large value within user's table may cause sqlite queries to crash.
1247
+ // @ts-ignore
1248
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
1249
+ const { channel_mutes, devices, mutes, ...messageUser } = client.user;
1250
+
1251
+ const preview = {
1252
+ __html: text,
1253
+ attachments,
1254
+ created_at: new Date(),
1255
+ html: text,
1256
+ id: `${client.userID}-${generateRandomId()}`,
1257
+ mentioned_users:
1258
+ mentioned_users?.map((userId) => ({
1259
+ id: userId,
1260
+ })) || [],
1261
+ parent_id,
1262
+ poll,
1263
+ poll_id,
1264
+ reactions: [],
1265
+ status: MessageStatusTypes.SENDING,
1266
+ text,
1267
+ type: 'regular',
1268
+ user: {
1269
+ ...messageUser,
1270
+ id: client.userID,
1271
+ },
1272
+ ...extraFields,
1273
+ } as unknown as MessageResponse<StreamChatGenerics>;
1262
1274
 
1263
- preview.quoted_message =
1264
- quotedMessage as MessageResponse<StreamChatGenerics>['quoted_message'];
1265
- }
1266
- return preview;
1267
- };
1275
+ /**
1276
+ * This is added to the message for local rendering prior to the message
1277
+ * being returned from the backend, it is removed when the message is sent
1278
+ * as quoted_message is a reserved field.
1279
+ */
1280
+ if (preview.quoted_message_id) {
1281
+ const quotedMessage = channelMessagesState.messages?.find(
1282
+ (message) => message.id === preview.quoted_message_id,
1283
+ );
1268
1284
 
1269
- const uploadPendingAttachments = async (message: MessageResponse<StreamChatGenerics>) => {
1270
- const updatedMessage = { ...message };
1271
- if (updatedMessage.attachments?.length) {
1272
- for (let i = 0; i < updatedMessage.attachments?.length; i++) {
1273
- const attachment = updatedMessage.attachments[i];
1274
- const image = attachment.originalImage;
1275
- const file = attachment.originalFile;
1276
- // check if image_url is not a remote url
1277
- if (
1278
- attachment.type === FileTypes.Image &&
1279
- image?.uri &&
1280
- attachment.image_url &&
1281
- isLocalUrl(attachment.image_url)
1282
- ) {
1283
- const filename = image.name ?? getFileNameFromPath(image.uri);
1284
- // if any upload is in progress, cancel it
1285
- const controller = uploadAbortControllerRef.current.get(filename);
1286
- if (controller) {
1287
- controller.abort();
1288
- uploadAbortControllerRef.current.delete(filename);
1289
- }
1290
- const compressedUri = await compressedImageURI(image, compressImageQuality);
1291
- const contentType = lookup(filename) || 'multipart/form-data';
1285
+ preview.quoted_message =
1286
+ quotedMessage as MessageResponse<StreamChatGenerics>['quoted_message'];
1287
+ }
1288
+ return preview;
1289
+ },
1290
+ );
1292
1291
 
1293
- const uploadResponse = doImageUploadRequest
1294
- ? await doImageUploadRequest(image, channel)
1295
- : await channel.sendImage(compressedUri, filename, contentType);
1292
+ const uploadPendingAttachments = useStableCallback(
1293
+ async (message: MessageResponse<StreamChatGenerics>) => {
1294
+ const updatedMessage = { ...message };
1295
+ if (updatedMessage.attachments?.length) {
1296
+ for (let i = 0; i < updatedMessage.attachments?.length; i++) {
1297
+ const attachment = updatedMessage.attachments[i];
1298
+ const image = attachment.originalImage;
1299
+ const file = attachment.originalFile;
1300
+ // check if image_url is not a remote url
1301
+ if (
1302
+ attachment.type === FileTypes.Image &&
1303
+ image?.uri &&
1304
+ attachment.image_url &&
1305
+ isLocalUrl(attachment.image_url)
1306
+ ) {
1307
+ const filename = image.name ?? getFileNameFromPath(image.uri);
1308
+ // if any upload is in progress, cancel it
1309
+ const controller = uploadAbortControllerRef.current.get(filename);
1310
+ if (controller) {
1311
+ controller.abort();
1312
+ uploadAbortControllerRef.current.delete(filename);
1313
+ }
1314
+ const compressedUri = await compressedImageURI(image, compressImageQuality);
1315
+ const contentType = lookup(filename) || 'multipart/form-data';
1296
1316
 
1297
- attachment.image_url = uploadResponse.file;
1298
- delete attachment.originalFile;
1317
+ const uploadResponse = doImageUploadRequest
1318
+ ? await doImageUploadRequest(image, channel)
1319
+ : await channel.sendImage(compressedUri, filename, contentType);
1299
1320
 
1300
- await dbApi.updateMessage({
1301
- message: { ...updatedMessage, cid: channel.cid },
1302
- });
1303
- }
1321
+ attachment.image_url = uploadResponse.file;
1322
+ delete attachment.originalFile;
1304
1323
 
1305
- if (
1306
- (attachment.type === FileTypes.File ||
1307
- attachment.type === FileTypes.Audio ||
1308
- attachment.type === FileTypes.VoiceRecording ||
1309
- attachment.type === FileTypes.Video) &&
1310
- attachment.asset_url &&
1311
- isLocalUrl(attachment.asset_url) &&
1312
- file?.uri
1313
- ) {
1314
- // if any upload is in progress, cancel it
1315
- const controller = uploadAbortControllerRef.current.get(file.name);
1316
- if (controller) {
1317
- controller.abort();
1318
- uploadAbortControllerRef.current.delete(file.name);
1319
- }
1320
- const response = doDocUploadRequest
1321
- ? await doDocUploadRequest(file, channel)
1322
- : await channel.sendFile(file.uri, file.name, file.mimeType);
1323
- attachment.asset_url = response.file;
1324
- if (response.thumb_url) {
1325
- attachment.thumb_url = response.thumb_url;
1324
+ await dbApi.updateMessage({
1325
+ message: { ...updatedMessage, cid: channel.cid },
1326
+ });
1326
1327
  }
1327
1328
 
1328
- delete attachment.originalFile;
1329
- await dbApi.updateMessage({
1330
- message: { ...updatedMessage, cid: channel.cid },
1331
- });
1329
+ if (
1330
+ (attachment.type === FileTypes.File ||
1331
+ attachment.type === FileTypes.Audio ||
1332
+ attachment.type === FileTypes.VoiceRecording ||
1333
+ attachment.type === FileTypes.Video) &&
1334
+ attachment.asset_url &&
1335
+ isLocalUrl(attachment.asset_url) &&
1336
+ file?.uri
1337
+ ) {
1338
+ // if any upload is in progress, cancel it
1339
+ const controller = uploadAbortControllerRef.current.get(file.name);
1340
+ if (controller) {
1341
+ controller.abort();
1342
+ uploadAbortControllerRef.current.delete(file.name);
1343
+ }
1344
+ const response = doDocUploadRequest
1345
+ ? await doDocUploadRequest(file, channel)
1346
+ : await channel.sendFile(file.uri, file.name, file.mimeType);
1347
+ attachment.asset_url = response.file;
1348
+ if (response.thumb_url) {
1349
+ attachment.thumb_url = response.thumb_url;
1350
+ }
1351
+
1352
+ delete attachment.originalFile;
1353
+ await dbApi.updateMessage({
1354
+ message: { ...updatedMessage, cid: channel.cid },
1355
+ });
1356
+ }
1332
1357
  }
1333
1358
  }
1334
- }
1335
-
1336
- return updatedMessage;
1337
- };
1338
1359
 
1339
- const sendMessageRequest = async (
1340
- message: MessageResponse<StreamChatGenerics>,
1341
- retrying?: boolean,
1342
- ) => {
1343
- try {
1344
- const updatedMessage = await uploadPendingAttachments(message);
1345
- const extraFields = omit(updatedMessage, [
1346
- '__html',
1347
- 'attachments',
1348
- 'created_at',
1349
- 'deleted_at',
1350
- 'html',
1351
- 'id',
1352
- 'latest_reactions',
1353
- 'mentioned_users',
1354
- 'own_reactions',
1355
- 'parent_id',
1356
- 'quoted_message',
1357
- 'reaction_counts',
1358
- 'reaction_groups',
1359
- 'reactions',
1360
- 'status',
1361
- 'text',
1362
- 'type',
1363
- 'updated_at',
1364
- 'user',
1365
- ]);
1366
- const { attachments, id, mentioned_users, parent_id, text } = updatedMessage;
1367
- if (!channel.id) {
1368
- return;
1369
- }
1360
+ return updatedMessage;
1361
+ },
1362
+ );
1370
1363
 
1371
- const mentionedUserIds = mentioned_users?.map((user) => user.id) || [];
1364
+ const sendMessageRequest = useStableCallback(
1365
+ async (message: MessageResponse<StreamChatGenerics>, retrying?: boolean) => {
1366
+ try {
1367
+ const updatedMessage = await uploadPendingAttachments(message);
1368
+ const extraFields = omit(updatedMessage, [
1369
+ '__html',
1370
+ 'attachments',
1371
+ 'created_at',
1372
+ 'deleted_at',
1373
+ 'html',
1374
+ 'id',
1375
+ 'latest_reactions',
1376
+ 'mentioned_users',
1377
+ 'own_reactions',
1378
+ 'parent_id',
1379
+ 'quoted_message',
1380
+ 'reaction_counts',
1381
+ 'reaction_groups',
1382
+ 'reactions',
1383
+ 'status',
1384
+ 'text',
1385
+ 'type',
1386
+ 'updated_at',
1387
+ 'user',
1388
+ ]);
1389
+ const { attachments, id, mentioned_users, parent_id, text } = updatedMessage;
1390
+ if (!channel.id) {
1391
+ return;
1392
+ }
1372
1393
 
1373
- const messageData = {
1374
- attachments,
1375
- id,
1376
- mentioned_users: mentionedUserIds,
1377
- parent_id,
1378
- text: patchMessageTextCommand(text ?? '', mentionedUserIds),
1379
- ...extraFields,
1380
- } as StreamMessage<StreamChatGenerics>;
1394
+ const mentionedUserIds = mentioned_users?.map((user) => user.id) || [];
1395
+
1396
+ const messageData = {
1397
+ attachments,
1398
+ id,
1399
+ mentioned_users: mentionedUserIds,
1400
+ parent_id,
1401
+ text: patchMessageTextCommand(text ?? '', mentionedUserIds),
1402
+ ...extraFields,
1403
+ } as StreamMessage<StreamChatGenerics>;
1404
+
1405
+ let messageResponse = {} as SendMessageAPIResponse<StreamChatGenerics>;
1406
+ if (doSendMessageRequest) {
1407
+ messageResponse = await doSendMessageRequest(channel?.cid || '', messageData);
1408
+ } else if (channel) {
1409
+ messageResponse = await channel.sendMessage(messageData);
1410
+ }
1381
1411
 
1382
- let messageResponse = {} as SendMessageAPIResponse<StreamChatGenerics>;
1383
- if (doSendMessageRequest) {
1384
- messageResponse = await doSendMessageRequest(channel?.cid || '', messageData);
1385
- } else if (channel) {
1386
- messageResponse = await channel.sendMessage(messageData);
1387
- }
1412
+ if (messageResponse.message) {
1413
+ messageResponse.message.status = MessageStatusTypes.RECEIVED;
1388
1414
 
1389
- if (messageResponse.message) {
1390
- messageResponse.message.status = MessageStatusTypes.RECEIVED;
1415
+ if (enableOfflineSupport) {
1416
+ await dbApi.updateMessage({
1417
+ message: { ...messageResponse.message, cid: channel.cid },
1418
+ });
1419
+ }
1420
+ if (retrying) {
1421
+ replaceMessage(message, messageResponse.message);
1422
+ } else {
1423
+ updateMessage(messageResponse.message, {}, true);
1424
+ }
1425
+ }
1426
+ } catch (err) {
1427
+ console.log(err);
1428
+ message.status = MessageStatusTypes.FAILED;
1429
+ const updatedMessage = { ...message, cid: channel.cid };
1430
+ updateMessage(updatedMessage);
1431
+ threadInstance?.upsertReplyLocally?.({ message: updatedMessage });
1432
+ optimisticallyUpdatedNewMessages.delete(message.id);
1391
1433
 
1392
1434
  if (enableOfflineSupport) {
1393
1435
  await dbApi.updateMessage({
1394
- message: { ...messageResponse.message, cid: channel.cid },
1436
+ message: { ...message, cid: channel.cid },
1395
1437
  });
1396
1438
  }
1397
- if (retrying) {
1398
- replaceMessage(message, messageResponse.message);
1399
- } else {
1400
- updateMessage(messageResponse.message);
1401
- }
1402
1439
  }
1403
- } catch (err) {
1404
- console.log(err);
1405
- message.status = MessageStatusTypes.FAILED;
1406
- const updatedMessage = { ...message, cid: channel.cid };
1407
- updateMessage(updatedMessage);
1408
- threadInstance?.upsertReplyLocally?.({ message: updatedMessage });
1440
+ },
1441
+ );
1409
1442
 
1410
- if (enableOfflineSupport) {
1411
- await dbApi.updateMessage({
1412
- message: { ...message, cid: channel.cid },
1413
- });
1443
+ const sendMessage: InputMessageInputContextValue<StreamChatGenerics>['sendMessage'] =
1444
+ useStableCallback(async (message) => {
1445
+ if (channel?.state?.filterErrorMessages) {
1446
+ channel.state.filterErrorMessages();
1414
1447
  }
1415
- }
1416
- };
1417
1448
 
1418
- const sendMessage: InputMessageInputContextValue<StreamChatGenerics>['sendMessage'] = async (
1419
- message,
1420
- ) => {
1421
- if (channel?.state?.filterErrorMessages) {
1422
- channel.state.filterErrorMessages();
1423
- }
1449
+ const messagePreview = createMessagePreview({
1450
+ ...message,
1451
+ attachments: message.attachments || [],
1452
+ });
1424
1453
 
1425
- const messagePreview = createMessagePreview({
1426
- ...message,
1427
- attachments: message.attachments || [],
1428
- });
1454
+ updateMessage(messagePreview, {
1455
+ commands: [],
1456
+ messageInput: '',
1457
+ });
1458
+ threadInstance?.upsertReplyLocally?.({ message: messagePreview });
1459
+ optimisticallyUpdatedNewMessages.add(messagePreview.id);
1429
1460
 
1430
- updateMessage(messagePreview, {
1431
- commands: [],
1432
- messageInput: '',
1433
- });
1434
- threadInstance?.upsertReplyLocally?.({ message: messagePreview });
1461
+ if (enableOfflineSupport) {
1462
+ // While sending a message, we add the message to local db with failed status, so that
1463
+ // if app gets closed before message gets sent and next time user opens the app
1464
+ // then user can see that message in failed state and can retry.
1465
+ // If succesfull, it will be updated with received status.
1466
+ await dbApi.upsertMessages({
1467
+ messages: [{ ...messagePreview, cid: channel.cid, status: MessageStatusTypes.FAILED }],
1468
+ });
1469
+ }
1435
1470
 
1436
- if (enableOfflineSupport) {
1437
- // While sending a message, we add the message to local db with failed status, so that
1438
- // if app gets closed before message gets sent and next time user opens the app
1439
- // then user can see that message in failed state and can retry.
1440
- // If succesfull, it will be updated with received status.
1441
- await dbApi.upsertMessages({
1442
- messages: [{ ...messagePreview, cid: channel.cid, status: MessageStatusTypes.FAILED }],
1443
- });
1444
- }
1471
+ await sendMessageRequest(messagePreview);
1472
+ });
1445
1473
 
1446
- await sendMessageRequest(messagePreview);
1447
- };
1474
+ const retrySendMessage: MessagesContextValue<StreamChatGenerics>['retrySendMessage'] =
1475
+ useStableCallback(async (message) => {
1476
+ const statusPendingMessage = {
1477
+ ...message,
1478
+ status: MessageStatusTypes.SENDING,
1479
+ };
1448
1480
 
1449
- const retrySendMessage: MessagesContextValue<StreamChatGenerics>['retrySendMessage'] = async (
1450
- message,
1451
- ) => {
1452
- const statusPendingMessage = {
1453
- ...message,
1454
- status: MessageStatusTypes.SENDING,
1455
- };
1481
+ const messageWithoutReservedFields = removeReservedFields(statusPendingMessage);
1456
1482
 
1457
- const messageWithoutReservedFields = removeReservedFields(statusPendingMessage);
1483
+ // For bounced messages, we don't need to update the message, instead always send a new message.
1484
+ if (!isBouncedMessage(message)) {
1485
+ updateMessage(messageWithoutReservedFields as MessageResponse<StreamChatGenerics>);
1486
+ }
1458
1487
 
1459
- // For bounced messages, we don't need to update the message, instead always send a new message.
1460
- if (!isBouncedMessage(message)) {
1461
- updateMessage(messageWithoutReservedFields as MessageResponse<StreamChatGenerics>);
1462
- }
1488
+ await sendMessageRequest(
1489
+ messageWithoutReservedFields as MessageResponse<StreamChatGenerics>,
1490
+ true,
1491
+ );
1492
+ });
1463
1493
 
1464
- await sendMessageRequest(
1465
- messageWithoutReservedFields as MessageResponse<StreamChatGenerics>,
1466
- true,
1494
+ const editMessage: InputMessageInputContextValue<StreamChatGenerics>['editMessage'] =
1495
+ useStableCallback((updatedMessage) =>
1496
+ doUpdateMessageRequest
1497
+ ? doUpdateMessageRequest(channel?.cid || '', updatedMessage)
1498
+ : client.updateMessage(updatedMessage),
1467
1499
  );
1468
- };
1469
1500
 
1470
- const editMessage: InputMessageInputContextValue<StreamChatGenerics>['editMessage'] = (
1471
- updatedMessage,
1472
- ) =>
1473
- doUpdateMessageRequest
1474
- ? doUpdateMessageRequest(channel?.cid || '', updatedMessage)
1475
- : client.updateMessage(updatedMessage);
1476
-
1477
- const setEditingState: MessagesContextValue<StreamChatGenerics>['setEditingState'] = (
1478
- message,
1479
- ) => {
1480
- clearQuotedMessageState();
1481
- setEditing(message);
1482
- };
1501
+ const setEditingState: MessagesContextValue<StreamChatGenerics>['setEditingState'] =
1502
+ useStableCallback((message) => {
1503
+ clearQuotedMessageState();
1504
+ setEditing(message);
1505
+ });
1483
1506
 
1484
- const setQuotedMessageState: MessagesContextValue<StreamChatGenerics>['setQuotedMessageState'] = (
1485
- messageOrBoolean,
1486
- ) => {
1487
- setQuotedMessage(messageOrBoolean);
1488
- };
1507
+ const setQuotedMessageState: MessagesContextValue<StreamChatGenerics>['setQuotedMessageState'] =
1508
+ useStableCallback((messageOrBoolean) => {
1509
+ setQuotedMessage(messageOrBoolean);
1510
+ });
1489
1511
 
1490
1512
  const clearEditingState: InputMessageInputContextValue<StreamChatGenerics>['clearEditingState'] =
1491
- () => setEditing(undefined);
1513
+ useStableCallback(() => setEditing(undefined));
1492
1514
 
1493
1515
  const clearQuotedMessageState: InputMessageInputContextValue<StreamChatGenerics>['clearQuotedMessageState'] =
1494
- () => setQuotedMessage(undefined);
1516
+ useStableCallback(() => setQuotedMessage(undefined));
1495
1517
 
1496
1518
  /**
1497
1519
  * Removes the message from local state
1498
1520
  */
1499
- const removeMessage: MessagesContextValue<StreamChatGenerics>['removeMessage'] = async (
1500
- message,
1501
- ) => {
1502
- if (channel) {
1503
- channel.state.removeMessage(message);
1504
- copyMessagesStateFromChannel(channel);
1521
+ const removeMessage: MessagesContextValue<StreamChatGenerics>['removeMessage'] =
1522
+ useStableCallback(async (message) => {
1523
+ if (channel) {
1524
+ channel.state.removeMessage(message);
1525
+ copyMessagesStateFromChannel(channel);
1505
1526
 
1506
- if (thread) {
1507
- setThreadMessages(channel.state.threads[thread.id] || []);
1527
+ if (thread) {
1528
+ setThreadMessages(channel.state.threads[thread.id] || []);
1529
+ }
1508
1530
  }
1509
- }
1510
1531
 
1511
- if (enableOfflineSupport) {
1512
- await dbApi.deleteMessage({
1513
- id: message.id,
1514
- });
1515
- }
1516
- };
1532
+ if (enableOfflineSupport) {
1533
+ await dbApi.deleteMessage({
1534
+ id: message.id,
1535
+ });
1536
+ }
1537
+ });
1517
1538
 
1518
- const sendReaction = async (type: string, messageId: string) => {
1539
+ const sendReaction = useStableCallback(async (type: string, messageId: string) => {
1519
1540
  if (!channel?.id || !client.user) {
1520
1541
  throw new Error('Channel has not been initialized');
1521
1542
  }
@@ -1556,91 +1577,88 @@ const ChannelWithContext = <
1556
1577
  if (sendReactionResponse?.message) {
1557
1578
  threadInstance?.upsertReplyLocally?.({ message: sendReactionResponse.message });
1558
1579
  }
1559
- };
1580
+ });
1560
1581
 
1561
- const deleteMessage: MessagesContextValue<StreamChatGenerics>['deleteMessage'] = async (
1562
- message,
1563
- ) => {
1564
- if (!channel.id) {
1565
- throw new Error('Channel has not been initialized yet');
1566
- }
1582
+ const deleteMessage: MessagesContextValue<StreamChatGenerics>['deleteMessage'] =
1583
+ useStableCallback(async (message) => {
1584
+ if (!channel.id) {
1585
+ throw new Error('Channel has not been initialized yet');
1586
+ }
1587
+
1588
+ if (!enableOfflineSupport) {
1589
+ if (message.status === MessageStatusTypes.FAILED) {
1590
+ await removeMessage(message);
1591
+ return;
1592
+ }
1593
+ await client.deleteMessage(message.id);
1594
+ return;
1595
+ }
1567
1596
 
1568
- if (!enableOfflineSupport) {
1569
1597
  if (message.status === MessageStatusTypes.FAILED) {
1598
+ await DBSyncManager.dropPendingTasks({ messageId: message.id });
1570
1599
  await removeMessage(message);
1600
+ } else {
1601
+ const updatedMessage = {
1602
+ ...message,
1603
+ cid: channel.cid,
1604
+ deleted_at: new Date().toISOString(),
1605
+ type: 'deleted',
1606
+ };
1607
+ updateMessage(updatedMessage);
1608
+
1609
+ threadInstance?.upsertReplyLocally({ message: updatedMessage });
1610
+
1611
+ const data = await DBSyncManager.queueTask<StreamChatGenerics>({
1612
+ client,
1613
+ task: {
1614
+ channelId: channel.id,
1615
+ channelType: channel.type,
1616
+ messageId: message.id,
1617
+ payload: [message.id],
1618
+ type: 'delete-message',
1619
+ },
1620
+ });
1621
+
1622
+ if (data?.message) {
1623
+ updateMessage({ ...data.message });
1624
+ }
1625
+ }
1626
+ });
1627
+
1628
+ const deleteReaction: MessagesContextValue<StreamChatGenerics>['deleteReaction'] =
1629
+ useStableCallback(async (type: string, messageId: string) => {
1630
+ if (!channel?.id || !client.user) {
1631
+ throw new Error('Channel has not been initialized');
1632
+ }
1633
+
1634
+ const payload: Parameters<ChannelClass['deleteReaction']> = [messageId, type];
1635
+
1636
+ if (!enableOfflineSupport) {
1637
+ await channel.deleteReaction(...payload);
1571
1638
  return;
1572
1639
  }
1573
- await client.deleteMessage(message.id);
1574
- return;
1575
- }
1576
1640
 
1577
- if (message.status === MessageStatusTypes.FAILED) {
1578
- await DBSyncManager.dropPendingTasks({ messageId: message.id });
1579
- await removeMessage(message);
1580
- } else {
1581
- const updatedMessage = {
1582
- ...message,
1583
- cid: channel.cid,
1584
- deleted_at: new Date().toISOString(),
1585
- type: 'deleted',
1586
- };
1587
- updateMessage(updatedMessage);
1641
+ removeReactionFromLocalState({
1642
+ channel,
1643
+ messageId,
1644
+ reactionType: type,
1645
+ user: client.user,
1646
+ });
1588
1647
 
1589
- threadInstance?.upsertReplyLocally({ message: updatedMessage });
1648
+ copyMessagesStateFromChannel(channel);
1590
1649
 
1591
- const data = await DBSyncManager.queueTask<StreamChatGenerics>({
1650
+ await DBSyncManager.queueTask<StreamChatGenerics>({
1592
1651
  client,
1593
1652
  task: {
1594
1653
  channelId: channel.id,
1595
1654
  channelType: channel.type,
1596
- messageId: message.id,
1597
- payload: [message.id],
1598
- type: 'delete-message',
1655
+ messageId,
1656
+ payload,
1657
+ type: 'delete-reaction',
1599
1658
  },
1600
1659
  });
1601
-
1602
- if (data?.message) {
1603
- updateMessage({ ...data.message });
1604
- }
1605
- }
1606
- };
1607
-
1608
- const deleteReaction: MessagesContextValue<StreamChatGenerics>['deleteReaction'] = async (
1609
- type: string,
1610
- messageId: string,
1611
- ) => {
1612
- if (!channel?.id || !client.user) {
1613
- throw new Error('Channel has not been initialized');
1614
- }
1615
-
1616
- const payload: Parameters<ChannelClass['deleteReaction']> = [messageId, type];
1617
-
1618
- if (!enableOfflineSupport) {
1619
- await channel.deleteReaction(...payload);
1620
- return;
1621
- }
1622
-
1623
- removeReactionFromLocalState({
1624
- channel,
1625
- messageId,
1626
- reactionType: type,
1627
- user: client.user,
1628
1660
  });
1629
1661
 
1630
- copyMessagesStateFromChannel(channel);
1631
-
1632
- await DBSyncManager.queueTask<StreamChatGenerics>({
1633
- client,
1634
- task: {
1635
- channelId: channel.id,
1636
- channelType: channel.type,
1637
- messageId,
1638
- payload,
1639
- type: 'delete-reaction',
1640
- },
1641
- });
1642
- };
1643
-
1644
1662
  /**
1645
1663
  * THREAD METHODS
1646
1664
  */
@@ -1681,46 +1699,47 @@ const ChannelWithContext = <
1681
1699
  ),
1682
1700
  ).current;
1683
1701
 
1684
- const loadMoreThread: ThreadContextValue<StreamChatGenerics>['loadMoreThread'] = async () => {
1685
- if (threadLoadingMore || !thread?.id) {
1686
- return;
1687
- }
1688
- setThreadLoadingMore(true);
1702
+ const loadMoreThread: ThreadContextValue<StreamChatGenerics>['loadMoreThread'] =
1703
+ useStableCallback(async () => {
1704
+ if (threadLoadingMore || !thread?.id) {
1705
+ return;
1706
+ }
1707
+ setThreadLoadingMore(true);
1689
1708
 
1690
- try {
1691
- if (channel) {
1692
- const parentID = thread.id;
1693
-
1694
- /**
1695
- * In the channel is re-initializing, then threads may get wiped out during the process
1696
- * (check `addMessagesSorted` method on channel.state). In those cases, we still want to
1697
- * preserve the messages on active thread, so lets simply copy messages from UI state to
1698
- * `channel.state`.
1699
- */
1700
- channel.state.threads[parentID] = threadMessages;
1701
- const oldestMessageID = threadMessages?.[0]?.id;
1702
-
1703
- const limit = 50;
1704
- const queryResponse = await channel.getReplies(parentID, {
1705
- id_lt: oldestMessageID,
1706
- limit,
1707
- });
1709
+ try {
1710
+ if (channel) {
1711
+ const parentID = thread.id;
1712
+
1713
+ /**
1714
+ * In the channel is re-initializing, then threads may get wiped out during the process
1715
+ * (check `addMessagesSorted` method on channel.state). In those cases, we still want to
1716
+ * preserve the messages on active thread, so lets simply copy messages from UI state to
1717
+ * `channel.state`.
1718
+ */
1719
+ channel.state.threads[parentID] = threadMessages;
1720
+ const oldestMessageID = threadMessages?.[0]?.id;
1721
+
1722
+ const limit = 50;
1723
+ const queryResponse = await channel.getReplies(parentID, {
1724
+ id_lt: oldestMessageID,
1725
+ limit,
1726
+ });
1708
1727
 
1709
- const updatedHasMore = queryResponse.messages.length === limit;
1710
- const updatedThreadMessages = channel.state.threads[parentID] || [];
1711
- loadMoreThreadFinished(updatedHasMore, updatedThreadMessages);
1712
- }
1713
- } catch (err) {
1714
- console.warn('Message pagination request failed with error', err);
1715
- if (err instanceof Error) {
1716
- setError(err);
1717
- } else {
1718
- setError(true);
1728
+ const updatedHasMore = queryResponse.messages.length === limit;
1729
+ const updatedThreadMessages = channel.state.threads[parentID] || [];
1730
+ loadMoreThreadFinished(updatedHasMore, updatedThreadMessages);
1731
+ }
1732
+ } catch (err) {
1733
+ console.warn('Message pagination request failed with error', err);
1734
+ if (err instanceof Error) {
1735
+ setError(err);
1736
+ } else {
1737
+ setError(true);
1738
+ }
1739
+ setThreadLoadingMore(false);
1740
+ throw err;
1719
1741
  }
1720
- setThreadLoadingMore(false);
1721
- throw err;
1722
- }
1723
- };
1742
+ });
1724
1743
 
1725
1744
  const ownCapabilitiesContext = useCreateOwnCapabilitiesContext({
1726
1745
  channel,
@@ -1772,14 +1791,11 @@ const ChannelWithContext = <
1772
1791
  // but it is definitely not trivial, especially considering it depends on other inline functions that
1773
1792
  // are not wrapped in a useCallback() themselves hence creating a huge cascading change. Can be removed
1774
1793
  // once our memoization issues are fixed in most places in the app or we move to a reactive state store.
1775
- const sendMessageRef =
1776
- useRef<InputMessageInputContextValue<StreamChatGenerics>['sendMessage']>(sendMessage);
1777
- sendMessageRef.current = sendMessage;
1778
- const sendMessageStable = useCallback<
1779
- InputMessageInputContextValue<StreamChatGenerics>['sendMessage']
1780
- >((...args) => {
1781
- return sendMessageRef.current(...args);
1782
- }, []);
1794
+ // const sendMessageRef = useRef<InputMessageInputContextValue['sendMessage']>(sendMessage);
1795
+ // sendMessageRef.current = sendMessage;
1796
+ // const sendMessageStable = useCallback<InputMessageInputContextValue['sendMessage']>((...args) => {
1797
+ // return sendMessageRef.current(...args);
1798
+ // }, []);
1783
1799
 
1784
1800
  const inputMessageInputContext = useCreateInputMessageInputContext<StreamChatGenerics>({
1785
1801
  additionalTextInputProps,
@@ -1833,7 +1849,7 @@ const ChannelWithContext = <
1833
1849
  quotedMessage,
1834
1850
  SendButton,
1835
1851
  sendImageAsync,
1836
- sendMessage: sendMessageStable,
1852
+ sendMessage,
1837
1853
  SendMessageDisallowedIndicator,
1838
1854
  setInputRef,
1839
1855
  setQuotedMessageState,