stream-chat-react-native-core 6.0.2 → 6.1.0-beta.1

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 +101 -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
@@ -12,7 +12,6 @@ import {
12
12
  Channel as ChannelType,
13
13
  EventHandler,
14
14
  FormatMessageResponse,
15
- logChatPromiseExecution,
16
15
  MessageResponse,
17
16
  Reaction,
18
17
  SendMessageAPIResponse,
@@ -92,7 +91,7 @@ import {
92
91
  isImagePickerAvailable,
93
92
  } from '../../native';
94
93
  import * as dbApi from '../../store/apis';
95
- import { DefaultStreamChatGenerics, FileTypes } from '../../types/types';
94
+ import { ChannelUnreadState, DefaultStreamChatGenerics, FileTypes } from '../../types/types';
96
95
  import { addReactionToLocalState } from '../../utils/addReactionToLocalState';
97
96
  import { compressedImageURI } from '../../utils/compressImage';
98
97
  import { DBSyncManager } from '../../utils/DBSyncManager';
@@ -179,6 +178,7 @@ import { ScrollToBottomButton as ScrollToBottomButtonDefault } from '../MessageL
179
178
  import { StickyHeader as StickyHeaderDefault } from '../MessageList/StickyHeader';
180
179
  import { TypingIndicator as TypingIndicatorDefault } from '../MessageList/TypingIndicator';
181
180
  import { TypingIndicatorContainer as TypingIndicatorContainerDefault } from '../MessageList/TypingIndicatorContainer';
181
+ import { UnreadMessagesNotification as UnreadMessagesNotificationDefault } from '../MessageList/UnreadMessagesNotification';
182
182
  import { MessageActionList as MessageActionListDefault } from '../MessageMenu/MessageActionList';
183
183
  import { MessageActionListItem as MessageActionListItemDefault } from '../MessageMenu/MessageActionListItem';
184
184
  import { MessageMenu as MessageMenuDefault } from '../MessageMenu/MessageMenu';
@@ -188,6 +188,15 @@ import { MessageUserReactionsAvatar as MessageUserReactionsAvatarDefault } from
188
188
  import { MessageUserReactionsItem as MessageUserReactionsItemDefault } from '../MessageMenu/MessageUserReactionsItem';
189
189
  import { Reply as ReplyDefault } from '../Reply/Reply';
190
190
 
191
+ export type MarkReadFunctionOptions = {
192
+ /**
193
+ * Signal, whether the `channelUnreadUiState` should be updated.
194
+ * By default, the local state update is prevented when the Channel component is mounted.
195
+ * This is in order to keep the UI indicating the original unread state, when the user opens a channel.
196
+ */
197
+ updateChannelUnreadState?: boolean;
198
+ };
199
+
191
200
  const styles = StyleSheet.create({
192
201
  selectChannel: { fontWeight: 'bold', padding: 16 },
193
202
  });
@@ -301,6 +310,7 @@ export type ChannelPropsWithContext<
301
310
  | 'handleDelete'
302
311
  | 'handleEdit'
303
312
  | 'handleFlag'
313
+ | 'handleMarkUnread'
304
314
  | 'handleMute'
305
315
  | 'handlePinMessage'
306
316
  | 'handleReaction'
@@ -360,6 +370,7 @@ export type ChannelPropsWithContext<
360
370
  | 'VideoThumbnail'
361
371
  | 'PollContent'
362
372
  | 'hasCreatePoll'
373
+ | 'UnreadMessagesNotification'
363
374
  | 'StreamingMessageView'
364
375
  >
365
376
  > &
@@ -384,7 +395,10 @@ export type ChannelPropsWithContext<
384
395
  * Overrides the Stream default mark channel read request (Advanced usage only)
385
396
  * @param channel Channel object
386
397
  */
387
- doMarkReadRequest?: (channel: ChannelType<StreamChatGenerics>) => void;
398
+ doMarkReadRequest?: (
399
+ channel: ChannelType<StreamChatGenerics>,
400
+ setChannelUnreadUiState?: (state: ChannelUnreadState) => void,
401
+ ) => void;
388
402
  /**
389
403
  * Overrides the Stream default send message request (Advanced usage only)
390
404
  * @param channelId
@@ -433,6 +447,10 @@ export type ChannelPropsWithContext<
433
447
  * Custom loading error indicator to override the Stream default
434
448
  */
435
449
  LoadingErrorIndicator?: React.ComponentType<LoadingErrorProps>;
450
+ /**
451
+ * Boolean flag to enable/disable marking the channel as read on mount
452
+ */
453
+ markReadOnMount?: boolean;
436
454
  maxMessageLength?: number;
437
455
  /**
438
456
  * Load the channel at a specified message instead of the most recent message.
@@ -529,6 +547,7 @@ const ChannelWithContext = <
529
547
  handleDelete,
530
548
  handleEdit,
531
549
  handleFlag,
550
+ handleMarkUnread,
532
551
  handleMute,
533
552
  handlePinMessage,
534
553
  handleQuotedReply,
@@ -566,6 +585,7 @@ const ChannelWithContext = <
566
585
  loadingMore: loadingMoreProp,
567
586
  loadingMoreRecent: loadingMoreRecentProp,
568
587
  markdownRules,
588
+ markReadOnMount = true,
569
589
  maxMessageLength: maxMessageLengthProp,
570
590
  maxNumberOfFiles = 10,
571
591
  maxTimeBetweenGroupedMessages,
@@ -647,6 +667,7 @@ const ChannelWithContext = <
647
667
  threadMessages,
648
668
  TypingIndicator = TypingIndicatorDefault,
649
669
  TypingIndicatorContainer = TypingIndicatorContainerDefault,
670
+ UnreadMessagesNotification = UnreadMessagesNotificationDefault,
650
671
  UploadProgressIndicator = UploadProgressIndicatorDefault,
651
672
  UrlPreview = CardDefault,
652
673
  VideoThumbnail = VideoThumbnailDefault,
@@ -674,10 +695,13 @@ const ChannelWithContext = <
674
695
  const [thread, setThread] = useState<MessageType<StreamChatGenerics> | null>(threadProps || null);
675
696
  const [threadHasMore, setThreadHasMore] = useState(true);
676
697
  const [threadLoadingMore, setThreadLoadingMore] = useState(false);
698
+ const [channelUnreadState, setChannelUnreadState] = useState<ChannelUnreadState | undefined>(
699
+ undefined,
700
+ );
677
701
 
678
702
  const syncingChannelRef = useRef(false);
679
703
 
680
- const { setTargetedMessage, targetedMessage } = useTargetedMessage();
704
+ const { highlightedMessageId, setTargetedMessage, targetedMessage } = useTargetedMessage();
681
705
 
682
706
  /**
683
707
  * This ref will hold the abort controllers for
@@ -692,6 +716,7 @@ const ChannelWithContext = <
692
716
  const {
693
717
  copyStateFromChannel,
694
718
  initStateFromChannel,
719
+ setRead,
695
720
  setTyping,
696
721
  state: channelState,
697
722
  } = useChannelDataState<StreamChatGenerics>(channel);
@@ -754,6 +779,22 @@ const ChannelWithContext = <
754
779
  }
755
780
  }
756
781
 
782
+ if (event.type === 'notification.mark_unread') {
783
+ setChannelUnreadState((prev) => {
784
+ if (!(event.last_read_at && event.user)) return prev;
785
+ return {
786
+ first_unread_message_id: event.first_unread_message_id,
787
+ last_read: new Date(event.last_read_at),
788
+ last_read_message_id: event.last_read_message_id,
789
+ unread_messages: event.unread_messages ?? 0,
790
+ };
791
+ });
792
+ }
793
+
794
+ if (event.type === 'channel.truncated' && event.cid === channel.cid) {
795
+ setChannelUnreadState(undefined);
796
+ }
797
+
757
798
  // only update channel state if the events are not the previously subscribed useEffect's subscription events
758
799
  if (channel && channel.initialized) {
759
800
  copyChannelState();
@@ -764,6 +805,8 @@ const ChannelWithContext = <
764
805
  useEffect(() => {
765
806
  let listener: ReturnType<typeof channel.on>;
766
807
  const initChannel = async () => {
808
+ setLastRead(new Date());
809
+ const unreadCount = channel.countUnread();
767
810
  if (!channel || !shouldSyncChannel || channel.offlineMode) return;
768
811
  let errored = false;
769
812
 
@@ -782,14 +825,32 @@ const ChannelWithContext = <
782
825
  loadInitialMessagesStateFromChannel(channel, channel.state.messagePagination.hasPrev);
783
826
  }
784
827
 
828
+ if (client.user?.id && channel.state.read[client.user.id]) {
829
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
830
+ const { user, ...ownReadState } = channel.state.read[client.user.id];
831
+ setChannelUnreadState(ownReadState);
832
+ }
833
+
785
834
  if (messageId) {
786
835
  await loadChannelAroundMessage({ messageId, setTargetedMessage });
787
836
  } else if (
788
837
  initialScrollToFirstUnreadMessage &&
789
- channel.countUnread() > scrollToFirstUnreadThreshold
838
+ client.user &&
839
+ unreadCount > scrollToFirstUnreadThreshold
790
840
  ) {
791
- await loadChannelAtFirstUnreadMessage({ setTargetedMessage });
841
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
842
+ const { user, ...ownReadState } = channel.state.read[client.user.id];
843
+ await loadChannelAtFirstUnreadMessage({
844
+ channelUnreadState: ownReadState,
845
+ setChannelUnreadState,
846
+ setTargetedMessage,
847
+ });
848
+ }
849
+
850
+ if (unreadCount > 0 && markReadOnMount) {
851
+ await markRead({ updateChannelUnreadState: false });
792
852
  }
853
+
793
854
  listener = channel.on(handleEvent);
794
855
  };
795
856
 
@@ -819,12 +880,12 @@ const ChannelWithContext = <
819
880
  */
820
881
  useEffect(() => {
821
882
  const handleEvent: EventHandler<StreamChatGenerics> = (event) => {
822
- if (channel.cid === event.cid) copyChannelState();
883
+ if (channel.cid === event.cid) setRead(channel);
823
884
  };
824
885
 
825
886
  const { unsubscribe } = client.on('notification.mark_read', handleEvent);
826
887
  return unsubscribe;
827
- }, [channel.cid, client, copyChannelState]);
888
+ }, [channel, client, setRead]);
828
889
 
829
890
  const threadPropsExists = !!threadProps;
830
891
 
@@ -858,23 +919,33 @@ const ChannelWithContext = <
858
919
  /**
859
920
  * CHANNEL METHODS
860
921
  */
861
- const markRead: ChannelContextValue<StreamChatGenerics>['markRead'] = useRef(
862
- throttle(
863
- () => {
864
- if (!channel || channel?.disconnected || !clientChannelConfig?.read_events) {
865
- return;
866
- }
922
+ const markRead: ChannelContextValue<StreamChatGenerics>['markRead'] = throttle(
923
+ async (options?: MarkReadFunctionOptions) => {
924
+ const { updateChannelUnreadState = true } = options ?? {};
925
+ if (!channel || channel?.disconnected || !clientChannelConfig?.read_events) {
926
+ return;
927
+ }
867
928
 
868
- if (doMarkReadRequest) {
869
- doMarkReadRequest(channel);
870
- } else {
871
- logChatPromiseExecution(channel.markRead(), 'mark read');
929
+ if (doMarkReadRequest) {
930
+ doMarkReadRequest(channel, updateChannelUnreadState ? setChannelUnreadState : undefined);
931
+ } else {
932
+ try {
933
+ const response = await channel.markRead();
934
+ if (updateChannelUnreadState && response && lastRead) {
935
+ setChannelUnreadState({
936
+ last_read: lastRead,
937
+ last_read_message_id: response?.event.last_read_message_id,
938
+ unread_messages: 0,
939
+ });
940
+ }
941
+ } catch (err) {
942
+ console.log('Error marking channel as read:', err);
872
943
  }
873
- },
874
- defaultThrottleInterval,
875
- throttleOptions,
876
- ),
877
- ).current;
944
+ }
945
+ },
946
+ defaultThrottleInterval,
947
+ throttleOptions,
948
+ );
878
949
 
879
950
  const reloadThread = async () => {
880
951
  if (!channel || !thread?.id) return;
@@ -1596,8 +1667,9 @@ const ChannelWithContext = <
1596
1667
  overrideCapabilities: overrideOwnCapabilities,
1597
1668
  });
1598
1669
 
1599
- const channelContext = useCreateChannelContext({
1670
+ const channelContext = useCreateChannelContext<StreamChatGenerics>({
1600
1671
  channel,
1672
+ channelUnreadState,
1601
1673
  disabled: !!channel?.data?.frozen,
1602
1674
  EmptyStateIndicator,
1603
1675
  enableMessageGroupingByUser,
@@ -1608,9 +1680,11 @@ const ChannelWithContext = <
1608
1680
  !!(clientChannelConfig?.commands || [])?.some((command) => command.name === 'giphy'),
1609
1681
  hideDateSeparators,
1610
1682
  hideStickyDateHeader,
1683
+ highlightedMessageId,
1611
1684
  isChannelActive: shouldSyncChannel,
1612
1685
  lastRead,
1613
1686
  loadChannelAroundMessage,
1687
+ loadChannelAtFirstUnreadMessage,
1614
1688
  loading: channelMessagesState.loading,
1615
1689
  LoadingIndicator,
1616
1690
  markRead,
@@ -1620,6 +1694,7 @@ const ChannelWithContext = <
1620
1694
  read: channelState.read ?? {},
1621
1695
  reloadChannel,
1622
1696
  scrollToFirstUnreadThreshold,
1697
+ setChannelUnreadState,
1623
1698
  setLastRead,
1624
1699
  setTargetedMessage,
1625
1700
  StickyHeader,
@@ -1748,6 +1823,7 @@ const ChannelWithContext = <
1748
1823
  handleDelete,
1749
1824
  handleEdit,
1750
1825
  handleFlag,
1826
+ handleMarkUnread,
1751
1827
  handleMute,
1752
1828
  handlePinMessage,
1753
1829
  handleQuotedReply,
@@ -1815,6 +1891,7 @@ const ChannelWithContext = <
1815
1891
  targetedMessage,
1816
1892
  TypingIndicator,
1817
1893
  TypingIndicatorContainer,
1894
+ UnreadMessagesNotification,
1818
1895
  updateMessage,
1819
1896
  UrlPreview,
1820
1897
  VideoThumbnail,
@@ -29,6 +29,7 @@ import {
29
29
  useChannelDataState,
30
30
  useChannelMessageDataState,
31
31
  } from '../hooks/useChannelDataState';
32
+ import * as MessageListPaginationHooks from '../hooks/useMessageListPagination';
32
33
 
33
34
  // This component is used for performing effects in a component that consumes ChannelContext,
34
35
  // i.e. making use of the callbacks & values provided by the Channel component.
@@ -87,6 +88,7 @@ describe('Channel', () => {
87
88
  const nullChannel = {
88
89
  ...channel,
89
90
  cid: null,
91
+ countUnread: () => 0,
90
92
  off: () => {},
91
93
  on: () => ({
92
94
  unsubscribe: () => null,
@@ -464,79 +466,128 @@ describe('Channel initial load useEffect', () => {
464
466
  );
465
467
  });
466
468
 
467
- it("should not call loadChannelAtFirstUnreadMessage if channel's unread count is 0", async () => {
468
- const mockedChannel = generateChannelResponse({
469
- messages: Array.from({ length: 10 }, (_, i) => generateMessage({ text: `message-${i}` })),
469
+ describe('initialScrollToFirstUnreadMessage', () => {
470
+ afterEach(() => {
471
+ // Clear all mocks after each test
472
+ jest.clearAllMocks();
473
+ // Restore all mocks to their original implementation
474
+ jest.restoreAllMocks();
475
+ cleanup();
470
476
  });
477
+ const mockedHook = (values) =>
478
+ jest.spyOn(MessageListPaginationHooks, 'useMessageListPagination').mockImplementation(() => ({
479
+ copyMessagesStateFromChannel: jest.fn(),
480
+ loadChannelAroundMessage: jest.fn(),
481
+ loadChannelAtFirstUnreadMessage: jest.fn(),
482
+ loadInitialMessagesStateFromChannel: jest.fn(),
483
+ loadLatestMessages: jest.fn(),
484
+ loadMore: jest.fn(),
485
+ loadMoreRecent: jest.fn(),
486
+ state: { ...channelInitialState },
487
+ ...values,
488
+ }));
489
+ it("should not call loadChannelAtFirstUnreadMessage if channel's unread count is 0", async () => {
490
+ const mockedChannel = generateChannelResponse({
491
+ messages: Array.from({ length: 10 }, (_, i) => generateMessage({ text: `message-${i}` })),
492
+ });
471
493
 
472
- useMockedApis(chatClient, [getOrCreateChannelApi(mockedChannel)]);
473
- const channel = chatClient.channel('messaging', mockedChannel.id);
474
- await channel.watch();
475
- const messages = Array.from({ length: 100 }, (_, i) => generateMessage({ id: i }));
494
+ useMockedApis(chatClient, [getOrCreateChannelApi(mockedChannel)]);
495
+ const channel = chatClient.channel('messaging', mockedChannel.id);
496
+ await channel.watch();
497
+ const user = generateUser();
498
+ const read_data = {};
476
499
 
477
- const loadMessageIntoState = jest.fn();
478
- channel.state = {
479
- ...channelInitialState,
480
- loadMessageIntoState,
481
- messagePagination: {
482
- hasNext: true,
483
- hasPrev: true,
484
- },
485
- messages,
486
- };
487
- channel.countUnread = jest.fn(() => 0);
500
+ read_data[chatClient.user.id] = {
501
+ last_read: new Date(),
502
+ user,
503
+ };
488
504
 
489
- renderComponent({ channel, initialScrollToFirstUnreadMessage: true });
505
+ channel.state = {
506
+ ...channelInitialState,
507
+ read: read_data,
508
+ };
509
+ channel.countUnread = jest.fn(() => 0);
490
510
 
491
- await waitFor(() => {
492
- expect(loadMessageIntoState).not.toHaveBeenCalled();
493
- });
494
- });
511
+ const loadChannelAtFirstUnreadMessageFn = jest.fn();
495
512
 
496
- it("should call loadChannelAtFirstUnreadMessage if channel's unread count is greater than 0", async () => {
497
- const mockedChannel = generateChannelResponse({
498
- messages: Array.from({ length: 10 }, (_, i) => generateMessage({ text: `message-${i}` })),
499
- });
513
+ mockedHook({ loadChannelAtFirstUnreadMessage: loadChannelAtFirstUnreadMessageFn });
500
514
 
501
- useMockedApis(chatClient, [getOrCreateChannelApi(mockedChannel)]);
502
- const channel = chatClient.channel('messaging', mockedChannel.id);
503
- await channel.watch();
504
- const messages = Array.from({ length: 100 }, (_, i) => generateMessage({ id: i }));
515
+ renderComponent({ channel, initialScrollToFirstUnreadMessage: true });
505
516
 
506
- let targetedMessageId = 0;
507
- const loadMessageIntoState = jest.fn((id) => {
508
- targetedMessageId = id;
509
- const newMessages = getElementsAround(messages, 'id', id);
510
- channel.state.messages = newMessages;
517
+ await waitFor(() => {
518
+ expect(loadChannelAtFirstUnreadMessageFn).not.toHaveBeenCalled();
519
+ });
511
520
  });
512
521
 
513
- channel.state = {
514
- ...channelInitialState,
515
- loadMessageIntoState,
516
- messagePagination: {
517
- hasNext: true,
518
- hasPrev: true,
519
- },
520
- messages,
521
- messageSets: [{ isCurrent: true, isLatest: true }],
522
- };
522
+ it("should call loadChannelAtFirstUnreadMessage if channel's unread count is greater than 0", async () => {
523
+ const mockedChannel = generateChannelResponse({
524
+ messages: Array.from({ length: 10 }, (_, i) => generateMessage({ text: `message-${i}` })),
525
+ });
523
526
 
524
- channel.countUnread = jest.fn(() => 15);
527
+ useMockedApis(chatClient, [getOrCreateChannelApi(mockedChannel)]);
528
+ const channel = chatClient.channel('messaging', mockedChannel.id);
529
+ await channel.watch();
525
530
 
526
- renderComponent({ channel, initialScrollToFirstUnreadMessage: true });
531
+ const user = generateUser();
532
+ const numberOfUnreadMessages = 15;
533
+ const read_data = {};
527
534
 
528
- await waitFor(() => {
529
- expect(loadMessageIntoState).toHaveBeenCalledTimes(1);
535
+ read_data[chatClient.user.id] = {
536
+ last_read: new Date(),
537
+ unread_messages: numberOfUnreadMessages,
538
+ user,
539
+ };
540
+ channel.state = {
541
+ ...channelInitialState,
542
+ read: read_data,
543
+ };
544
+
545
+ channel.countUnread = jest.fn(() => numberOfUnreadMessages);
546
+ const loadChannelAtFirstUnreadMessageFn = jest.fn();
547
+
548
+ mockedHook({ loadChannelAtFirstUnreadMessage: loadChannelAtFirstUnreadMessageFn });
549
+
550
+ renderComponent({ channel, initialScrollToFirstUnreadMessage: true });
551
+
552
+ await waitFor(() => {
553
+ expect(loadChannelAtFirstUnreadMessageFn).toHaveBeenCalled();
554
+ });
530
555
  });
531
556
 
532
- const { result: channelMessageState } = renderHook(() => useChannelMessageDataState(channel));
533
- await waitFor(() =>
534
- expect(
535
- channelMessageState.current.state.messages.find(
536
- (message) => message.id === targetedMessageId,
537
- ),
538
- ).toBeDefined(),
539
- );
557
+ it("should not call loadChannelAtFirstUnreadMessage if channel's unread count is greater than 0 lesser than scrollToFirstUnreadThreshold", async () => {
558
+ const mockedChannel = generateChannelResponse({
559
+ messages: Array.from({ length: 10 }, (_, i) => generateMessage({ text: `message-${i}` })),
560
+ });
561
+
562
+ useMockedApis(chatClient, [getOrCreateChannelApi(mockedChannel)]);
563
+ const channel = chatClient.channel('messaging', mockedChannel.id);
564
+ await channel.watch();
565
+
566
+ const user = generateUser();
567
+ const numberOfUnreadMessages = 2;
568
+ const read_data = {};
569
+
570
+ read_data[chatClient.user.id] = {
571
+ last_read: new Date(),
572
+ unread_messages: numberOfUnreadMessages,
573
+ user,
574
+ };
575
+ channel.state = {
576
+ ...channelInitialState,
577
+ read: read_data,
578
+ };
579
+
580
+ channel.countUnread = jest.fn(() => numberOfUnreadMessages);
581
+ const loadChannelAtFirstUnreadMessageFn = jest.fn();
582
+
583
+ mockedHook({ loadChannelAtFirstUnreadMessage: loadChannelAtFirstUnreadMessageFn });
584
+
585
+ renderComponent({ channel, initialScrollToFirstUnreadMessage: true });
586
+
587
+ await waitFor(() => {
588
+ expect(loadChannelAtFirstUnreadMessageFn).not.toHaveBeenCalled();
589
+ });
590
+ });
540
591
  });
541
592
 
542
593
  it('should call resyncChannel when connection changed event is triggered', async () => {
@@ -236,6 +236,32 @@ describe('Own capabilities', () => {
236
236
  });
237
237
  });
238
238
 
239
+ describe(`${allOwnCapabilities.readEvents} capability`, () => {
240
+ it(`should render "Mark as Unread" action for messages when "${allOwnCapabilities.readEvents}" capability is enabled`, async () => {
241
+ await generateChannelWithCapabilities([allOwnCapabilities.readEvents]);
242
+ const { queryByLabelText } = await renderChannelAndOpenMessageActionsList(receivedMessage);
243
+ expect(!!queryByLabelText('markUnread action list item')).toBeTruthy();
244
+ });
245
+
246
+ it(`should not render "Mark Read" action for received message when "${allOwnCapabilities.readEvents}" capability is disabled`, async () => {
247
+ await generateChannelWithCapabilities();
248
+
249
+ const { queryByLabelText } = await renderChannelAndOpenMessageActionsList(receivedMessage);
250
+ expect(!!queryByLabelText('markUnread action list item')).toBeFalsy();
251
+ });
252
+
253
+ it('should override capability from "overrideOwnCapability.readEvents" prop', async () => {
254
+ await generateChannelWithCapabilities([allOwnCapabilities.readEvents]);
255
+
256
+ const { queryByLabelText } = await renderChannelAndOpenMessageActionsList(receivedMessage, {
257
+ overrideOwnCapabilities: {
258
+ readEvents: false,
259
+ },
260
+ });
261
+ expect(!!queryByLabelText('markUnread action list item')).toBeFalsy();
262
+ });
263
+ });
264
+
239
265
  describe(`${allOwnCapabilities.pinMessage} capability`, () => {
240
266
  it(`should render "Pin Message" action for sent message when "${allOwnCapabilities.pinMessage}" capability is enabled`, async () => {
241
267
  await generateChannelWithCapabilities([allOwnCapabilities.pinMessage]);