stream-chat-react-native-core 6.0.2 → 6.1.0-beta.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 (216) hide show
  1. package/README.md +1 -1
  2. package/lib/commonjs/components/Channel/Channel.js +371 -279
  3. package/lib/commonjs/components/Channel/Channel.js.map +1 -1
  4. package/lib/commonjs/components/Channel/hooks/useChannelDataState.js +8 -0
  5. package/lib/commonjs/components/Channel/hooks/useChannelDataState.js.map +1 -1
  6. package/lib/commonjs/components/Channel/hooks/useCreateChannelContext.js +10 -1
  7. package/lib/commonjs/components/Channel/hooks/useCreateChannelContext.js.map +1 -1
  8. package/lib/commonjs/components/Channel/hooks/useCreateMessagesContext.js +4 -0
  9. package/lib/commonjs/components/Channel/hooks/useCreateMessagesContext.js.map +1 -1
  10. package/lib/commonjs/components/Channel/hooks/useMessageListPagination.js +161 -69
  11. package/lib/commonjs/components/Channel/hooks/useMessageListPagination.js.map +1 -1
  12. package/lib/commonjs/components/Channel/hooks/useTargetedMessage.js +10 -0
  13. package/lib/commonjs/components/Channel/hooks/useTargetedMessage.js.map +1 -1
  14. package/lib/commonjs/components/Chat/hooks/handleEventToSyncDB.js +81 -54
  15. package/lib/commonjs/components/Chat/hooks/handleEventToSyncDB.js.map +1 -1
  16. package/lib/commonjs/components/Message/Message.js +6 -0
  17. package/lib/commonjs/components/Message/Message.js.map +1 -1
  18. package/lib/commonjs/components/Message/hooks/useMessageActionHandlers.js +117 -79
  19. package/lib/commonjs/components/Message/hooks/useMessageActionHandlers.js.map +1 -1
  20. package/lib/commonjs/components/Message/hooks/useMessageActions.js +32 -14
  21. package/lib/commonjs/components/Message/hooks/useMessageActions.js.map +1 -1
  22. package/lib/commonjs/components/Message/utils/messageActions.js +4 -0
  23. package/lib/commonjs/components/Message/utils/messageActions.js.map +1 -1
  24. package/lib/commonjs/components/MessageList/InlineUnreadIndicator.js +19 -55
  25. package/lib/commonjs/components/MessageList/InlineUnreadIndicator.js.map +1 -1
  26. package/lib/commonjs/components/MessageList/MessageList.js +249 -211
  27. package/lib/commonjs/components/MessageList/MessageList.js.map +1 -1
  28. package/lib/commonjs/components/MessageList/UnreadMessagesNotification.js +148 -0
  29. package/lib/commonjs/components/MessageList/UnreadMessagesNotification.js.map +1 -0
  30. package/lib/commonjs/components/MessageMenu/MessageActionListItem.js.map +1 -1
  31. package/lib/commonjs/contexts/channelContext/ChannelContext.js.map +1 -1
  32. package/lib/commonjs/contexts/messagesContext/MessagesContext.js.map +1 -1
  33. package/lib/commonjs/contexts/themeContext/utils/theme.js +7 -1
  34. package/lib/commonjs/contexts/themeContext/utils/theme.js.map +1 -1
  35. package/lib/commonjs/i18n/en.json +2 -0
  36. package/lib/commonjs/i18n/es.json +2 -0
  37. package/lib/commonjs/i18n/fr.json +2 -0
  38. package/lib/commonjs/i18n/he.json +2 -0
  39. package/lib/commonjs/i18n/hi.json +2 -0
  40. package/lib/commonjs/i18n/it.json +2 -0
  41. package/lib/commonjs/i18n/ja.json +2 -0
  42. package/lib/commonjs/i18n/ko.json +2 -0
  43. package/lib/commonjs/i18n/nl.json +2 -0
  44. package/lib/commonjs/i18n/pt-br.json +2 -0
  45. package/lib/commonjs/i18n/ru.json +2 -0
  46. package/lib/commonjs/i18n/tr.json +2 -0
  47. package/lib/commonjs/icons/UnreadIndicator.js +30 -0
  48. package/lib/commonjs/icons/UnreadIndicator.js.map +1 -0
  49. package/lib/commonjs/icons/index.js +11 -0
  50. package/lib/commonjs/icons/index.js.map +1 -1
  51. package/lib/commonjs/store/SqliteClient.js +1 -1
  52. package/lib/commonjs/store/schema.js +1 -0
  53. package/lib/commonjs/store/schema.js.map +1 -1
  54. package/lib/commonjs/types/types.js.map +1 -1
  55. package/lib/commonjs/utils/utils.js +35 -1
  56. package/lib/commonjs/utils/utils.js.map +1 -1
  57. package/lib/commonjs/version.json +1 -1
  58. package/lib/module/components/Channel/Channel.js +371 -279
  59. package/lib/module/components/Channel/Channel.js.map +1 -1
  60. package/lib/module/components/Channel/hooks/useChannelDataState.js +8 -0
  61. package/lib/module/components/Channel/hooks/useChannelDataState.js.map +1 -1
  62. package/lib/module/components/Channel/hooks/useCreateChannelContext.js +10 -1
  63. package/lib/module/components/Channel/hooks/useCreateChannelContext.js.map +1 -1
  64. package/lib/module/components/Channel/hooks/useCreateMessagesContext.js +4 -0
  65. package/lib/module/components/Channel/hooks/useCreateMessagesContext.js.map +1 -1
  66. package/lib/module/components/Channel/hooks/useMessageListPagination.js +161 -69
  67. package/lib/module/components/Channel/hooks/useMessageListPagination.js.map +1 -1
  68. package/lib/module/components/Channel/hooks/useTargetedMessage.js +10 -0
  69. package/lib/module/components/Channel/hooks/useTargetedMessage.js.map +1 -1
  70. package/lib/module/components/Chat/hooks/handleEventToSyncDB.js +81 -54
  71. package/lib/module/components/Chat/hooks/handleEventToSyncDB.js.map +1 -1
  72. package/lib/module/components/Message/Message.js +6 -0
  73. package/lib/module/components/Message/Message.js.map +1 -1
  74. package/lib/module/components/Message/hooks/useMessageActionHandlers.js +117 -79
  75. package/lib/module/components/Message/hooks/useMessageActionHandlers.js.map +1 -1
  76. package/lib/module/components/Message/hooks/useMessageActions.js +32 -14
  77. package/lib/module/components/Message/hooks/useMessageActions.js.map +1 -1
  78. package/lib/module/components/Message/utils/messageActions.js +4 -0
  79. package/lib/module/components/Message/utils/messageActions.js.map +1 -1
  80. package/lib/module/components/MessageList/InlineUnreadIndicator.js +19 -55
  81. package/lib/module/components/MessageList/InlineUnreadIndicator.js.map +1 -1
  82. package/lib/module/components/MessageList/MessageList.js +249 -211
  83. package/lib/module/components/MessageList/MessageList.js.map +1 -1
  84. package/lib/module/components/MessageList/UnreadMessagesNotification.js +148 -0
  85. package/lib/module/components/MessageList/UnreadMessagesNotification.js.map +1 -0
  86. package/lib/module/components/MessageMenu/MessageActionListItem.js.map +1 -1
  87. package/lib/module/contexts/channelContext/ChannelContext.js.map +1 -1
  88. package/lib/module/contexts/messagesContext/MessagesContext.js.map +1 -1
  89. package/lib/module/contexts/themeContext/utils/theme.js +7 -1
  90. package/lib/module/contexts/themeContext/utils/theme.js.map +1 -1
  91. package/lib/module/i18n/en.json +2 -0
  92. package/lib/module/i18n/es.json +2 -0
  93. package/lib/module/i18n/fr.json +2 -0
  94. package/lib/module/i18n/he.json +2 -0
  95. package/lib/module/i18n/hi.json +2 -0
  96. package/lib/module/i18n/it.json +2 -0
  97. package/lib/module/i18n/ja.json +2 -0
  98. package/lib/module/i18n/ko.json +2 -0
  99. package/lib/module/i18n/nl.json +2 -0
  100. package/lib/module/i18n/pt-br.json +2 -0
  101. package/lib/module/i18n/ru.json +2 -0
  102. package/lib/module/i18n/tr.json +2 -0
  103. package/lib/module/icons/UnreadIndicator.js +30 -0
  104. package/lib/module/icons/UnreadIndicator.js.map +1 -0
  105. package/lib/module/icons/index.js +11 -0
  106. package/lib/module/icons/index.js.map +1 -1
  107. package/lib/module/store/SqliteClient.js +1 -1
  108. package/lib/module/store/schema.js +1 -0
  109. package/lib/module/store/schema.js.map +1 -1
  110. package/lib/module/types/types.js.map +1 -1
  111. package/lib/module/utils/utils.js +35 -1
  112. package/lib/module/utils/utils.js.map +1 -1
  113. package/lib/module/version.json +1 -1
  114. package/lib/typescript/components/Channel/Channel.d.ts +15 -3
  115. package/lib/typescript/components/Channel/Channel.d.ts.map +1 -1
  116. package/lib/typescript/components/Channel/hooks/useChannelDataState.d.ts +1 -0
  117. package/lib/typescript/components/Channel/hooks/useChannelDataState.d.ts.map +1 -1
  118. package/lib/typescript/components/Channel/hooks/useCreateChannelContext.d.ts +1 -1
  119. package/lib/typescript/components/Channel/hooks/useCreateChannelContext.d.ts.map +1 -1
  120. package/lib/typescript/components/Channel/hooks/useCreateMessagesContext.d.ts +4 -2
  121. package/lib/typescript/components/Channel/hooks/useCreateMessagesContext.d.ts.map +1 -1
  122. package/lib/typescript/components/Channel/hooks/useMessageListPagination.d.ts +4 -1
  123. package/lib/typescript/components/Channel/hooks/useMessageListPagination.d.ts.map +1 -1
  124. package/lib/typescript/components/Channel/hooks/useTargetedMessage.d.ts +2 -1
  125. package/lib/typescript/components/Channel/hooks/useTargetedMessage.d.ts.map +1 -1
  126. package/lib/typescript/components/Chat/hooks/handleEventToSyncDB.d.ts.map +1 -1
  127. package/lib/typescript/components/Message/Message.d.ts +2 -1
  128. package/lib/typescript/components/Message/Message.d.ts.map +1 -1
  129. package/lib/typescript/components/Message/hooks/useMessageActionHandlers.d.ts +2 -1
  130. package/lib/typescript/components/Message/hooks/useMessageActionHandlers.d.ts.map +1 -1
  131. package/lib/typescript/components/Message/hooks/useMessageActions.d.ts +3 -2
  132. package/lib/typescript/components/Message/hooks/useMessageActions.d.ts.map +1 -1
  133. package/lib/typescript/components/Message/hooks/useMessageData.d.ts +1 -1
  134. package/lib/typescript/components/Message/utils/messageActions.d.ts +2 -1
  135. package/lib/typescript/components/Message/utils/messageActions.d.ts.map +1 -1
  136. package/lib/typescript/components/MessageList/InlineUnreadIndicator.d.ts.map +1 -1
  137. package/lib/typescript/components/MessageList/MessageList.d.ts +1 -1
  138. package/lib/typescript/components/MessageList/MessageList.d.ts.map +1 -1
  139. package/lib/typescript/components/MessageList/UnreadMessagesNotification.d.ts +13 -0
  140. package/lib/typescript/components/MessageList/UnreadMessagesNotification.d.ts.map +1 -0
  141. package/lib/typescript/components/MessageMenu/MessageActionListItem.d.ts +2 -2
  142. package/lib/typescript/components/MessageMenu/MessageActionListItem.d.ts.map +1 -1
  143. package/lib/typescript/contexts/channelContext/ChannelContext.d.ts +25 -8
  144. package/lib/typescript/contexts/channelContext/ChannelContext.d.ts.map +1 -1
  145. package/lib/typescript/contexts/messagesContext/MessagesContext.d.ts +5 -0
  146. package/lib/typescript/contexts/messagesContext/MessagesContext.d.ts.map +1 -1
  147. package/lib/typescript/contexts/themeContext/utils/theme.d.ts +6 -0
  148. package/lib/typescript/contexts/themeContext/utils/theme.d.ts.map +1 -1
  149. package/lib/typescript/i18n/en.json +2 -0
  150. package/lib/typescript/i18n/es.json +2 -0
  151. package/lib/typescript/i18n/fr.json +2 -0
  152. package/lib/typescript/i18n/he.json +2 -0
  153. package/lib/typescript/i18n/hi.json +2 -0
  154. package/lib/typescript/i18n/it.json +2 -0
  155. package/lib/typescript/i18n/ja.json +2 -0
  156. package/lib/typescript/i18n/ko.json +2 -0
  157. package/lib/typescript/i18n/nl.json +2 -0
  158. package/lib/typescript/i18n/pt-br.json +2 -0
  159. package/lib/typescript/i18n/ru.json +2 -0
  160. package/lib/typescript/i18n/tr.json +2 -0
  161. package/lib/typescript/icons/UnreadIndicator.d.ts +8 -0
  162. package/lib/typescript/icons/UnreadIndicator.d.ts.map +1 -0
  163. package/lib/typescript/icons/index.d.ts +1 -0
  164. package/lib/typescript/icons/index.d.ts.map +1 -1
  165. package/lib/typescript/store/mappers/mapStorableToChannel.d.ts +1 -1
  166. package/lib/typescript/store/schema.d.ts +1 -0
  167. package/lib/typescript/store/schema.d.ts.map +1 -1
  168. package/lib/typescript/types/types.d.ts +2 -1
  169. package/lib/typescript/types/types.d.ts.map +1 -1
  170. package/lib/typescript/utils/i18n/Streami18n.d.ts +2 -0
  171. package/lib/typescript/utils/i18n/Streami18n.d.ts.map +1 -1
  172. package/lib/typescript/utils/utils.d.ts +21 -1
  173. package/lib/typescript/utils/utils.d.ts.map +1 -1
  174. package/package.json +1 -1
  175. package/src/components/Channel/Channel.tsx +102 -24
  176. package/src/components/Channel/__tests__/Channel.test.js +109 -58
  177. package/src/components/Channel/__tests__/ownCapabilities.test.js +26 -0
  178. package/src/components/Channel/__tests__/useMessageListPagination.test.js +234 -37
  179. package/src/components/Channel/hooks/useChannelDataState.ts +8 -0
  180. package/src/components/Channel/hooks/useCreateChannelContext.ts +11 -0
  181. package/src/components/Channel/hooks/useCreateMessagesContext.ts +4 -0
  182. package/src/components/Channel/hooks/useMessageListPagination.tsx +134 -64
  183. package/src/components/Channel/hooks/useTargetedMessage.ts +9 -2
  184. package/src/components/Chat/hooks/handleEventToSyncDB.ts +23 -1
  185. package/src/components/Message/Message.tsx +8 -0
  186. package/src/components/Message/hooks/useMessageActionHandlers.ts +54 -40
  187. package/src/components/Message/hooks/useMessageActions.tsx +31 -14
  188. package/src/components/Message/utils/messageActions.ts +6 -0
  189. package/src/components/MessageList/InlineUnreadIndicator.tsx +17 -26
  190. package/src/components/MessageList/MessageList.tsx +197 -231
  191. package/src/components/MessageList/UnreadMessagesNotification.tsx +107 -0
  192. package/src/components/MessageList/__tests__/MessageList.test.js +213 -0
  193. package/src/components/MessageMenu/MessageActionListItem.tsx +2 -1
  194. package/src/components/Thread/__tests__/__snapshots__/Thread.test.js.snap +669 -679
  195. package/src/contexts/channelContext/ChannelContext.tsx +35 -9
  196. package/src/contexts/messagesContext/MessagesContext.tsx +7 -2
  197. package/src/contexts/themeContext/utils/theme.ts +12 -0
  198. package/src/i18n/en.json +2 -0
  199. package/src/i18n/es.json +2 -0
  200. package/src/i18n/fr.json +2 -0
  201. package/src/i18n/he.json +2 -0
  202. package/src/i18n/hi.json +2 -0
  203. package/src/i18n/it.json +2 -0
  204. package/src/i18n/ja.json +2 -0
  205. package/src/i18n/ko.json +2 -0
  206. package/src/i18n/nl.json +2 -0
  207. package/src/i18n/pt-br.json +2 -0
  208. package/src/i18n/ru.json +2 -0
  209. package/src/i18n/tr.json +2 -0
  210. package/src/icons/UnreadIndicator.tsx +18 -0
  211. package/src/icons/index.ts +1 -0
  212. package/src/store/SqliteClient.ts +1 -1
  213. package/src/store/schema.ts +2 -0
  214. package/src/types/types.ts +5 -2
  215. package/src/utils/utils.ts +61 -1
  216. package/src/version.json +1 -1
@@ -337,10 +337,11 @@ describe('useMessageListPagination', () => {
337
337
  });
338
338
 
339
339
  it('should not do anything when the unread count is 0', async () => {
340
+ const messages = Array.from({ length: 20 }, (_, i) =>
341
+ generateMessage({ text: `message-${i}` }),
342
+ );
340
343
  const loadMessageIntoState = jest.fn(() => {
341
- channel.state.messages = Array.from({ length: 20 }, (_, i) =>
342
- generateMessage({ text: `message-${i}` }),
343
- );
344
+ channel.state.messages = messages;
344
345
  channel.state.messagePagination.hasPrev = true;
345
346
  });
346
347
  channel.state = {
@@ -352,68 +353,264 @@ describe('useMessageListPagination', () => {
352
353
  },
353
354
  };
354
355
 
355
- channel.countUnread = jest.fn(() => 0);
356
+ const user = generateUser();
357
+ const channelUnreadState = {
358
+ unread_messages: 0,
359
+ user,
360
+ };
361
+
362
+ const jumpToMessageFinishedMock = jest.fn();
363
+ mockedHook(channelInitialState, { jumpToMessageFinished: jumpToMessageFinishedMock });
356
364
 
357
365
  const { result } = renderHook(() => useMessageListPagination({ channel }));
358
366
 
359
367
  await act(async () => {
360
- await result.current.loadChannelAtFirstUnreadMessage({});
368
+ await result.current.loadChannelAtFirstUnreadMessage({ channelUnreadState });
361
369
  });
362
370
 
363
371
  await waitFor(() => {
364
- expect(loadMessageIntoState).toHaveBeenCalledTimes(0);
372
+ expect(jumpToMessageFinishedMock).toHaveBeenCalledTimes(0);
365
373
  });
366
374
  });
367
375
 
368
- function getElementsAround(array, key, id, limit) {
369
- const index = array.findIndex((obj) => obj[key] === id);
370
-
371
- if (index === -1) {
372
- return [];
373
- }
374
-
375
- const start = Math.max(0, index - limit); // 12 before the index
376
- const end = Math.min(array.length, index + limit); // 12 after the index
377
- return array.slice(start, end);
378
- }
376
+ const generateMessageArray = (length = 20) =>
377
+ Array.from({ length }, (_, i) => generateMessage({ id: i, text: `message-${i}` }));
378
+
379
+ // Test cases with different scenarios
380
+ const testCases = [
381
+ {
382
+ channelUnreadState: (messages) => ({
383
+ first_unread_message_id: messages[2].id,
384
+ unread_messages: 2,
385
+ }),
386
+ expectedCalls: {
387
+ jumpToMessageFinishedCalls: 1,
388
+ loadMessageIntoStateCalls: 0,
389
+ setChannelUnreadStateCalls: 0,
390
+ setTargetedMessageIdCalls: 1,
391
+ targetedMessageId: (messages) => messages[2].id,
392
+ },
393
+ initialMessages: generateMessageArray(),
394
+ name: 'first_unread_message_id present in current message set',
395
+ setupLoadMessageIntoState: null,
396
+ },
397
+ {
398
+ channelUnreadState: () => ({
399
+ first_unread_message_id: 21,
400
+ unread_messages: 2,
401
+ }),
402
+ expectedCalls: {
403
+ jumpToMessageFinishedCalls: 1,
404
+ loadMessageIntoStateCalls: 1,
405
+ setChannelUnreadStateCalls: 0,
406
+ setTargetedMessageIdCalls: 1,
407
+ targetedMessageId: () => 21,
408
+ },
409
+ initialMessages: generateMessageArray(),
410
+ name: 'first_unread_message_id not present in current message set',
411
+ setupLoadMessageIntoState: (channel) => {
412
+ const loadMessageIntoState = jest.fn(() => {
413
+ const newMessages = Array.from({ length: 20 }, (_, i) =>
414
+ generateMessage({ id: i + 21, text: `message-${i + 21}` }),
415
+ );
416
+ channel.state.messages = newMessages;
417
+ channel.state.messagePagination.hasPrev = true;
418
+ });
419
+ channel.state.loadMessageIntoState = loadMessageIntoState;
420
+ return loadMessageIntoState;
421
+ },
422
+ },
423
+ {
424
+ channelUnreadState: (messages) => ({
425
+ last_read_message_id: messages[2].id,
426
+ unread_messages: 2,
427
+ }),
428
+ expectedCalls: {
429
+ jumpToMessageFinishedCalls: 1,
430
+ loadMessageIntoStateCalls: 0,
431
+ setChannelUnreadStateCalls: 1,
432
+ setTargetedMessageIdCalls: 1,
433
+ targetedMessageId: (messages) => messages[3].id,
434
+ },
435
+ initialMessages: generateMessageArray(),
436
+ name: 'last_read_message_id present in current message set',
437
+ setupLoadMessageIntoState: null,
438
+ },
439
+ {
440
+ channelUnreadState: () => ({
441
+ last_read_message_id: 21,
442
+ unread_messages: 2,
443
+ }),
444
+ expectedCalls: {
445
+ jumpToMessageFinishedCalls: 1,
446
+ loadMessageIntoStateCalls: 1,
447
+ setChannelUnreadStateCalls: 1,
448
+ setTargetedMessageIdCalls: 1,
449
+ targetedMessageId: () => 22,
450
+ },
451
+ initialMessages: generateMessageArray(),
452
+ name: 'last_read_message_id not present in current message set',
453
+ setupLoadMessageIntoState: (channel) => {
454
+ const loadMessageIntoState = jest.fn(() => {
455
+ const newMessages = Array.from({ length: 20 }, (_, i) =>
456
+ generateMessage({ id: i + 21, text: `message-${i + 21}` }),
457
+ );
458
+ channel.state.messages = newMessages;
459
+ channel.state.messagePagination.hasPrev = true;
460
+ });
461
+ channel.state.loadMessageIntoState = loadMessageIntoState;
462
+ return loadMessageIntoState;
463
+ },
464
+ },
465
+ ];
379
466
 
380
- it('should call the loadMessageIntoState function when the unread count is greater than 0 and set the state', async () => {
381
- const messages = Array.from({ length: 30 }, (_, i) =>
382
- generateMessage({ text: `message-${i}` }),
383
- );
384
- const loadMessageIntoState = jest.fn((messageId) => {
385
- channel.state.messages = getElementsAround(messages, 'id', messageId, 5);
386
- channel.state.messagePagination.hasPrev = true;
387
- });
467
+ it.each(testCases)('$name', async (testCase) => {
468
+ // Setup channel state
469
+ const messages = testCase.initialMessages;
388
470
  channel.state = {
389
471
  ...channelInitialState,
390
- loadMessageIntoState,
391
472
  messagePagination: {
392
- hasNext: false,
473
+ hasNext: true,
393
474
  hasPrev: true,
394
475
  },
395
476
  messages,
396
- messageSets: [{ isCurrent: true, isLatest: true }],
397
477
  };
398
478
 
399
- const unreadCount = 5;
400
- channel.countUnread = jest.fn(() => unreadCount);
479
+ // Setup additional mocks if needed
480
+ const loadMessageIntoStateMock = testCase.setupLoadMessageIntoState
481
+ ? testCase.setupLoadMessageIntoState(channel)
482
+ : null;
483
+
484
+ // Generate user and channel unread state
485
+ const user = generateUser();
486
+ const channelUnreadState = {
487
+ user,
488
+ ...testCase.channelUnreadState(messages),
489
+ };
490
+
491
+ // Setup mocks
492
+ const jumpToMessageFinishedMock = jest.fn();
493
+ mockedHook(channelInitialState, { jumpToMessageFinished: jumpToMessageFinishedMock });
401
494
 
402
495
  const { result } = renderHook(() => useMessageListPagination({ channel }));
403
496
 
497
+ const setChannelUnreadStateMock = jest.fn();
498
+ const setTargetedMessageIdMock = jest.fn((message) => message);
499
+
500
+ // Execute the method
404
501
  await act(async () => {
405
- await result.current.loadChannelAtFirstUnreadMessage({});
502
+ await result.current.loadChannelAtFirstUnreadMessage({
503
+ channelUnreadState,
504
+ setChannelUnreadState: setChannelUnreadStateMock,
505
+ setTargetedMessage: setTargetedMessageIdMock,
506
+ });
406
507
  });
407
508
 
509
+ // Verify expectations
408
510
  await waitFor(() => {
409
- expect(loadMessageIntoState).toHaveBeenCalledTimes(1);
410
- expect(result.current.state.hasMore).toBe(true);
411
- expect(result.current.state.hasMoreNewer).toBe(false);
412
- expect(result.current.state.messages.length).toBe(10);
413
- expect(result.current.state.targetedMessageId).toBe(
414
- messages[messages.length - unreadCount].id,
511
+ if (loadMessageIntoStateMock) {
512
+ expect(loadMessageIntoStateMock).toHaveBeenCalledTimes(
513
+ testCase.expectedCalls.loadMessageIntoStateCalls,
514
+ );
515
+ }
516
+
517
+ expect(jumpToMessageFinishedMock).toHaveBeenCalledTimes(
518
+ testCase.expectedCalls.jumpToMessageFinishedCalls,
519
+ );
520
+
521
+ expect(setChannelUnreadStateMock).toHaveBeenCalledTimes(
522
+ testCase.expectedCalls.setChannelUnreadStateCalls,
415
523
  );
524
+
525
+ expect(setTargetedMessageIdMock).toHaveBeenCalledTimes(
526
+ testCase.expectedCalls.setTargetedMessageIdCalls,
527
+ );
528
+
529
+ if (testCase.expectedCalls.targetedMessageId) {
530
+ const expectedMessageId = testCase.expectedCalls.targetedMessageId(messages);
531
+ expect(setTargetedMessageIdMock).toHaveBeenCalledWith(expectedMessageId);
532
+ }
416
533
  });
417
534
  });
535
+
536
+ const messages = Array.from({ length: 20 }, (_, i) =>
537
+ generateMessage({
538
+ created_at: new Date(`2021-09-01T00:00:00.000Z`),
539
+ id: i,
540
+ text: `message-${i}`,
541
+ }),
542
+ );
543
+
544
+ const user = generateUser();
545
+
546
+ it.each`
547
+ scenario | last_read | expectedQueryCalls | expectedJumpToMessageFinishedCalls | expectedSetChannelUnreadStateCalls | expectedSetTargetedMessageCalls | expectedTargetedMessageId
548
+ ${'when last_read matches a message'} | ${new Date(messages[10].created_at)} | ${0} | ${1} | ${1} | ${1} | ${10}
549
+ ${'when last_read does not match any message'} | ${new Date('2021-09-02T00:00:00.000Z')} | ${1} | ${0} | ${0} | ${0} | ${undefined}
550
+ `(
551
+ '$scenario',
552
+ async ({
553
+ expectedJumpToMessageFinishedCalls,
554
+ expectedQueryCalls,
555
+ expectedSetChannelUnreadStateCalls,
556
+ expectedSetTargetedMessageCalls,
557
+ expectedTargetedMessageId,
558
+ last_read,
559
+ }) => {
560
+ // Set up channel state
561
+ channel.state = {
562
+ ...channelInitialState,
563
+ messagePagination: {
564
+ hasNext: true,
565
+ hasPrev: true,
566
+ },
567
+ messages,
568
+ };
569
+
570
+ const channelUnreadState = {
571
+ last_read,
572
+ unread_messages: 2,
573
+ user,
574
+ };
575
+
576
+ // Mock query if needed
577
+ const queryMock = jest.fn();
578
+ channel.query = queryMock;
579
+
580
+ // Set up mocks
581
+ const jumpToMessageFinishedMock = jest.fn();
582
+ mockedHook(channelInitialState, { jumpToMessageFinished: jumpToMessageFinishedMock });
583
+ const setChannelUnreadStateMock = jest.fn();
584
+ const setTargetedMessageIdMock = jest.fn((message) => message);
585
+
586
+ // Render hook
587
+ const { result } = renderHook(() => useMessageListPagination({ channel }));
588
+
589
+ // Act
590
+ await act(async () => {
591
+ await result.current.loadChannelAtFirstUnreadMessage({
592
+ channelUnreadState,
593
+ setChannelUnreadState: setChannelUnreadStateMock,
594
+ setTargetedMessage: setTargetedMessageIdMock,
595
+ });
596
+ });
597
+
598
+ // Assert
599
+ await waitFor(() => {
600
+ expect(queryMock).toHaveBeenCalledTimes(expectedQueryCalls);
601
+ expect(jumpToMessageFinishedMock).toHaveBeenCalledTimes(
602
+ expectedJumpToMessageFinishedCalls,
603
+ );
604
+ expect(setChannelUnreadStateMock).toHaveBeenCalledTimes(
605
+ expectedSetChannelUnreadStateCalls,
606
+ );
607
+ expect(setTargetedMessageIdMock).toHaveBeenCalledTimes(expectedSetTargetedMessageCalls);
608
+
609
+ if (expectedTargetedMessageId !== undefined) {
610
+ expect(setTargetedMessageIdMock).toHaveBeenCalledWith(expectedTargetedMessageId);
611
+ }
612
+ });
613
+ },
614
+ );
418
615
  });
419
616
  });
@@ -219,6 +219,13 @@ export const useChannelDataState = <
219
219
  }));
220
220
  }, []);
221
221
 
222
+ const setRead = useCallback((channel: Channel<StreamChatGenerics>) => {
223
+ setState((prev) => ({
224
+ ...prev,
225
+ read: { ...channel.state.read }, // Synchronize the read state from the channel
226
+ }));
227
+ }, []);
228
+
222
229
  const setTyping = useCallback((channel: Channel<StreamChatGenerics>) => {
223
230
  setState((prev) => ({
224
231
  ...prev,
@@ -229,6 +236,7 @@ export const useChannelDataState = <
229
236
  return {
230
237
  copyStateFromChannel,
231
238
  initStateFromChannel,
239
+ setRead,
232
240
  setTyping,
233
241
  state,
234
242
  };
@@ -7,6 +7,7 @@ export const useCreateChannelContext = <
7
7
  StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics,
8
8
  >({
9
9
  channel,
10
+ channelUnreadState,
10
11
  disabled,
11
12
  EmptyStateIndicator,
12
13
  enableMessageGroupingByUser,
@@ -15,9 +16,11 @@ export const useCreateChannelContext = <
15
16
  giphyEnabled,
16
17
  hideDateSeparators,
17
18
  hideStickyDateHeader,
19
+ highlightedMessageId,
18
20
  isChannelActive,
19
21
  lastRead,
20
22
  loadChannelAroundMessage,
23
+ loadChannelAtFirstUnreadMessage,
21
24
  loading,
22
25
  LoadingIndicator,
23
26
  markRead,
@@ -27,6 +30,7 @@ export const useCreateChannelContext = <
27
30
  read,
28
31
  reloadChannel,
29
32
  scrollToFirstUnreadThreshold,
33
+ setChannelUnreadState,
30
34
  setLastRead,
31
35
  setTargetedMessage,
32
36
  StickyHeader,
@@ -43,10 +47,12 @@ export const useCreateChannelContext = <
43
47
  const readUsers = Object.values(read);
44
48
  const readUsersLength = readUsers.length;
45
49
  const readUsersLastReads = readUsers.map(({ last_read }) => last_read.toISOString()).join();
50
+ const stringifiedChannelUnreadState = JSON.stringify(channelUnreadState);
46
51
 
47
52
  const channelContext: ChannelContextValue<StreamChatGenerics> = useMemo(
48
53
  () => ({
49
54
  channel,
55
+ channelUnreadState,
50
56
  disabled,
51
57
  EmptyStateIndicator,
52
58
  enableMessageGroupingByUser,
@@ -55,9 +61,11 @@ export const useCreateChannelContext = <
55
61
  giphyEnabled,
56
62
  hideDateSeparators,
57
63
  hideStickyDateHeader,
64
+ highlightedMessageId,
58
65
  isChannelActive,
59
66
  lastRead,
60
67
  loadChannelAroundMessage,
68
+ loadChannelAtFirstUnreadMessage,
61
69
  loading,
62
70
  LoadingIndicator,
63
71
  markRead,
@@ -67,6 +75,7 @@ export const useCreateChannelContext = <
67
75
  read,
68
76
  reloadChannel,
69
77
  scrollToFirstUnreadThreshold,
78
+ setChannelUnreadState,
70
79
  setLastRead,
71
80
  setTargetedMessage,
72
81
  StickyHeader,
@@ -82,11 +91,13 @@ export const useCreateChannelContext = <
82
91
  disabled,
83
92
  error,
84
93
  isChannelActive,
94
+ highlightedMessageId,
85
95
  lastReadTime,
86
96
  loading,
87
97
  membersLength,
88
98
  readUsersLength,
89
99
  readUsersLastReads,
100
+ stringifiedChannelUnreadState,
90
101
  targetedMessage,
91
102
  threadList,
92
103
  watcherCount,
@@ -36,6 +36,7 @@ export const useCreateMessagesContext = <
36
36
  handleDelete,
37
37
  handleEdit,
38
38
  handleFlag,
39
+ handleMarkUnread,
39
40
  handleMute,
40
41
  handlePinMessage,
41
42
  handleQuotedReply,
@@ -102,6 +103,7 @@ export const useCreateMessagesContext = <
102
103
  targetedMessage,
103
104
  TypingIndicator,
104
105
  TypingIndicatorContainer,
106
+ UnreadMessagesNotification,
105
107
  updateMessage,
106
108
  UrlPreview,
107
109
  VideoThumbnail,
@@ -147,6 +149,7 @@ export const useCreateMessagesContext = <
147
149
  handleDelete,
148
150
  handleEdit,
149
151
  handleFlag,
152
+ handleMarkUnread,
150
153
  handleMute,
151
154
  handlePinMessage,
152
155
  handleQuotedReply,
@@ -213,6 +216,7 @@ export const useCreateMessagesContext = <
213
216
  targetedMessage,
214
217
  TypingIndicator,
215
218
  TypingIndicatorContainer,
219
+ UnreadMessagesNotification,
216
220
  updateMessage,
217
221
  UrlPreview,
218
222
  VideoThumbnail,
@@ -1,12 +1,13 @@
1
1
  import { useRef } from 'react';
2
2
 
3
3
  import debounce from 'lodash/debounce';
4
- import { Channel, ChannelState } from 'stream-chat';
4
+ import { Channel, ChannelState, MessageResponse } from 'stream-chat';
5
5
 
6
6
  import { useChannelMessageDataState } from './useChannelDataState';
7
7
 
8
8
  import { ChannelContextValue } from '../../../contexts/channelContext/ChannelContext';
9
9
  import { DefaultStreamChatGenerics } from '../../../types/types';
10
+ import { findInMessagesByDate, findInMessagesById } from '../../../utils/utils';
10
11
 
11
12
  const defaultDebounceInterval = 500;
12
13
  const debounceOptions = {
@@ -153,6 +154,8 @@ export const useMessageListPagination = <
153
154
  setTargetedMessage(messageIdToLoadAround);
154
155
  }
155
156
  } catch (error) {
157
+ setLoadingMore(false);
158
+ setLoading(false);
156
159
  console.warn(
157
160
  'Message pagination(fetching messages in the channel around a message id) request failed with error:',
158
161
  error,
@@ -162,76 +165,143 @@ export const useMessageListPagination = <
162
165
  };
163
166
 
164
167
  /**
165
- * Loads channel at first unread message.
168
+ * Fetch messages around a specific timestamp.
166
169
  */
167
- const loadChannelAtFirstUnreadMessage = async ({
168
- limit = 25,
169
- setTargetedMessage,
170
- }: {
171
- limit?: number;
172
- setTargetedMessage?: (messageId: string) => void;
173
- }) => {
174
- let unreadMessageIdToScrollTo: string | undefined;
175
- const unreadCount = channel.countUnread();
176
- if (unreadCount === 0) return;
177
- const isLatestMessageSetShown = !!channel.state.messageSets.find(
178
- (set) => set.isCurrent && set.isLatest,
179
- );
180
-
181
- if (isLatestMessageSetShown && unreadCount <= channel.state.messages.length) {
182
- unreadMessageIdToScrollTo =
183
- channel.state.messages[channel.state.messages.length - unreadCount].id;
184
- if (unreadMessageIdToScrollTo) {
185
- setLoadingMore(true);
186
- await channel.state.loadMessageIntoState(unreadMessageIdToScrollTo, undefined, limit);
187
- loadMoreFinished(channel.state.messagePagination.hasPrev, channel.state.messages);
188
- jumpToMessageFinished(channel.state.messagePagination.hasNext, unreadMessageIdToScrollTo);
189
- if (setTargetedMessage) {
190
- setTargetedMessage(unreadMessageIdToScrollTo);
191
- }
192
- }
193
- return;
170
+ const fetchMessagesAround = async <
171
+ StreamChatGenerics extends DefaultStreamChatGenerics = DefaultStreamChatGenerics,
172
+ >(
173
+ channel: Channel<StreamChatGenerics>,
174
+ timestamp: string,
175
+ limit: number,
176
+ ): Promise<MessageResponse<StreamChatGenerics>[]> => {
177
+ try {
178
+ const { messages } = await channel.query(
179
+ { messages: { created_at_around: timestamp, limit } },
180
+ 'new',
181
+ );
182
+ return messages;
183
+ } catch (error) {
184
+ console.error('Error fetching messages around timestamp:', error);
185
+ throw error;
194
186
  }
195
- const lastReadDate = channel.lastRead();
196
- let messages;
197
- if (lastReadDate) {
187
+ };
188
+
189
+ /**
190
+ * Loads channel at first unread message.
191
+ */
192
+ const loadChannelAtFirstUnreadMessage: ChannelContextValue<StreamChatGenerics>['loadChannelAtFirstUnreadMessage'] =
193
+ async ({ channelUnreadState, limit = 25, setChannelUnreadState, setTargetedMessage }) => {
198
194
  try {
199
- messages = (
200
- await channel.query(
201
- {
202
- messages: {
203
- created_at_around: lastReadDate,
204
- limit: 30,
205
- },
206
- watch: true,
207
- },
208
- 'new',
209
- )
210
- ).messages;
211
-
212
- unreadMessageIdToScrollTo = messages.find(
213
- (m) => lastReadDate < (m.created_at ? new Date(m.created_at) : new Date()),
214
- )?.id;
215
- if (unreadMessageIdToScrollTo) {
216
- setLoadingMore(true);
217
- await channel.state.loadMessageIntoState(unreadMessageIdToScrollTo, undefined, limit);
218
- loadMoreFinished(channel.state.messagePagination.hasPrev, channel.state.messages);
219
- jumpToMessageFinished(channel.state.messagePagination.hasNext, unreadMessageIdToScrollTo);
220
- if (setTargetedMessage) {
221
- setTargetedMessage(unreadMessageIdToScrollTo);
195
+ if (!channelUnreadState?.unread_messages) return;
196
+ const { first_unread_message_id, last_read, last_read_message_id } = channelUnreadState;
197
+ let firstUnreadMessageId = first_unread_message_id;
198
+ let lastReadMessageId = last_read_message_id;
199
+ let isInCurrentMessageSet = false;
200
+ const messagesState = channel.state.messages;
201
+
202
+ // If the first unread message is already in the current message set, we don't need to load more messages.
203
+ if (firstUnreadMessageId) {
204
+ const messageIdx = findInMessagesById(messagesState, firstUnreadMessageId);
205
+ isInCurrentMessageSet = messageIdx !== -1;
206
+ }
207
+ // If the last read message is already in the current message set, we don't need to load more messages, and we set the first unread message id as that is what we want to operate on.
208
+ else if (lastReadMessageId) {
209
+ const messageIdx = findInMessagesById(messagesState, lastReadMessageId);
210
+ isInCurrentMessageSet = messageIdx !== -1;
211
+ firstUnreadMessageId = messageIdx > -1 ? messagesState[messageIdx + 1]?.id : undefined;
212
+ } else {
213
+ const lastReadTimestamp = last_read.getTime();
214
+ const { index: lastReadIdx, message: lastReadMessage } = findInMessagesByDate(
215
+ messagesState,
216
+ last_read,
217
+ );
218
+ if (lastReadMessage) {
219
+ lastReadMessageId = lastReadMessage.id;
220
+ firstUnreadMessageId = messagesState[lastReadIdx + 1].id;
221
+ isInCurrentMessageSet = !!firstUnreadMessageId;
222
+ } else {
223
+ setLoadingMore(true);
224
+ setLoading(true);
225
+ let messages;
226
+ try {
227
+ messages = await fetchMessagesAround(channel, last_read.toISOString(), limit);
228
+ } catch (error) {
229
+ setLoading(false);
230
+ loadMoreFinished(channel.state.messagePagination.hasPrev, messagesState);
231
+ console.log('Loading channel at first unread message failed with error:', error);
232
+ return;
233
+ }
234
+
235
+ const firstMessageWithCreationDate = messages.find((msg) => msg.created_at);
236
+ if (!firstMessageWithCreationDate) {
237
+ loadMoreFinished(channel.state.messagePagination.hasPrev, channel.state.messages);
238
+ throw new Error('Failed to jump to first unread message id.');
239
+ }
240
+ const firstMessageTimestamp = new Date(
241
+ firstMessageWithCreationDate.created_at as string,
242
+ ).getTime();
243
+
244
+ if (lastReadTimestamp < firstMessageTimestamp) {
245
+ // whole channel is unread
246
+ firstUnreadMessageId = firstMessageWithCreationDate.id;
247
+ } else {
248
+ const result = findInMessagesByDate(messages, last_read);
249
+ lastReadMessageId = result.message?.id;
250
+ }
251
+ loadMoreFinished(channel.state.messagePagination.hasPrev, channel.state.messages);
222
252
  }
223
253
  }
254
+
255
+ // If we still don't have the first and last read message id, we can't proceed.
256
+ if (!firstUnreadMessageId && !lastReadMessageId) {
257
+ throw new Error('Failed to jump to first unread message id.');
258
+ }
259
+
260
+ // If the first unread message is not in the current message set, we need to load message around the id.
261
+ if (!isInCurrentMessageSet) {
262
+ try {
263
+ setLoadingMore(true);
264
+ setLoading(true);
265
+ const targetedMessage = (firstUnreadMessageId || lastReadMessageId) as string;
266
+ await channel.state.loadMessageIntoState(targetedMessage, undefined, limit);
267
+ /**
268
+ * if the index of the last read message on the page is beyond the half of the page,
269
+ * we have arrived to the oldest page of the channel
270
+ */
271
+ const indexOfTarget = channel.state.messages.findIndex(
272
+ (message) => message.id === targetedMessage,
273
+ );
274
+
275
+ loadMoreFinished(channel.state.messagePagination.hasPrev, channel.state.messages);
276
+ firstUnreadMessageId =
277
+ firstUnreadMessageId ?? channel.state.messages[indexOfTarget + 1].id;
278
+ } catch (error) {
279
+ setLoading(false);
280
+ loadMoreFinished(channel.state.messagePagination.hasPrev, channel.state.messages);
281
+ console.log('Loading channel at first unread message failed with error:', error);
282
+ return;
283
+ }
284
+ }
285
+
286
+ if (!firstUnreadMessageId) {
287
+ throw new Error('Failed to jump to first unread message id.');
288
+ }
289
+ if (!first_unread_message_id && setChannelUnreadState) {
290
+ setChannelUnreadState({
291
+ ...channelUnreadState,
292
+ first_unread_message_id: firstUnreadMessageId,
293
+ last_read_message_id: lastReadMessageId,
294
+ });
295
+ }
296
+
297
+ jumpToMessageFinished(channel.state.messagePagination.hasNext, firstUnreadMessageId);
298
+ if (setTargetedMessage) {
299
+ setTargetedMessage(firstUnreadMessageId);
300
+ }
224
301
  } catch (error) {
225
- console.warn(
226
- 'Message pagination(fetching messages in the channel around unread message) request failed with error:',
227
- error,
228
- );
229
- return;
302
+ console.log('Loading channel at first unread message failed with error:', error);
230
303
  }
231
- } else {
232
- await loadLatestMessages();
233
- }
234
- };
304
+ };
235
305
 
236
306
  return {
237
307
  copyMessagesStateFromChannel,