@sendbird/uikit-react-native 3.9.5 → 3.10.0

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 (59) hide show
  1. package/lib/commonjs/components/ChannelMessageList/index.js +57 -9
  2. package/lib/commonjs/components/ChannelMessageList/index.js.map +1 -1
  3. package/lib/commonjs/components/GroupChannelMessageRenderer/GroupChannelMessageNewLine.js +53 -0
  4. package/lib/commonjs/components/GroupChannelMessageRenderer/GroupChannelMessageNewLine.js.map +1 -0
  5. package/lib/commonjs/components/GroupChannelMessageRenderer/index.js +5 -0
  6. package/lib/commonjs/components/GroupChannelMessageRenderer/index.js.map +1 -1
  7. package/lib/commonjs/components/UnreadMessagesFloating.js +77 -0
  8. package/lib/commonjs/components/UnreadMessagesFloating.js.map +1 -0
  9. package/lib/commonjs/domain/groupChannel/component/GroupChannelMessageList.js +156 -7
  10. package/lib/commonjs/domain/groupChannel/component/GroupChannelMessageList.js.map +1 -1
  11. package/lib/commonjs/domain/groupChannel/types.js.map +1 -1
  12. package/lib/commonjs/fragments/createGroupChannelFragment.js +57 -3
  13. package/lib/commonjs/fragments/createGroupChannelFragment.js.map +1 -1
  14. package/lib/commonjs/localization/StringSet.type.js.map +1 -1
  15. package/lib/commonjs/localization/createBaseStringSet.js +16 -2
  16. package/lib/commonjs/localization/createBaseStringSet.js.map +1 -1
  17. package/lib/commonjs/version.js +1 -1
  18. package/lib/commonjs/version.js.map +1 -1
  19. package/lib/module/components/ChannelMessageList/index.js +57 -9
  20. package/lib/module/components/ChannelMessageList/index.js.map +1 -1
  21. package/lib/module/components/GroupChannelMessageRenderer/GroupChannelMessageNewLine.js +46 -0
  22. package/lib/module/components/GroupChannelMessageRenderer/GroupChannelMessageNewLine.js.map +1 -0
  23. package/lib/module/components/GroupChannelMessageRenderer/index.js +5 -0
  24. package/lib/module/components/GroupChannelMessageRenderer/index.js.map +1 -1
  25. package/lib/module/components/UnreadMessagesFloating.js +70 -0
  26. package/lib/module/components/UnreadMessagesFloating.js.map +1 -0
  27. package/lib/module/domain/groupChannel/component/GroupChannelMessageList.js +158 -9
  28. package/lib/module/domain/groupChannel/component/GroupChannelMessageList.js.map +1 -1
  29. package/lib/module/domain/groupChannel/types.js.map +1 -1
  30. package/lib/module/fragments/createGroupChannelFragment.js +59 -5
  31. package/lib/module/fragments/createGroupChannelFragment.js.map +1 -1
  32. package/lib/module/localization/StringSet.type.js.map +1 -1
  33. package/lib/module/localization/createBaseStringSet.js +16 -2
  34. package/lib/module/localization/createBaseStringSet.js.map +1 -1
  35. package/lib/module/version.js +1 -1
  36. package/lib/module/version.js.map +1 -1
  37. package/lib/typescript/src/components/ChannelMessageList/index.d.ts +8 -1
  38. package/lib/typescript/src/components/GroupChannelMessageRenderer/GroupChannelMessageNewLine.d.ts +6 -0
  39. package/lib/typescript/src/components/GroupChannelMessageRenderer/index.d.ts +1 -0
  40. package/lib/typescript/src/components/OpenChannelMessageRenderer/index.d.ts +1 -0
  41. package/lib/typescript/src/components/UnreadMessagesFloating.d.ts +8 -0
  42. package/lib/typescript/src/containers/SendbirdUIKitContainer.d.ts +1 -1
  43. package/lib/typescript/src/domain/groupChannel/component/GroupChannelMessageList.d.ts +6 -1
  44. package/lib/typescript/src/domain/groupChannel/types.d.ts +13 -1
  45. package/lib/typescript/src/domain/openChannel/component/OpenChannelHeader.d.ts +1 -1
  46. package/lib/typescript/src/hooks/useChannelInputItems.d.ts +1 -1
  47. package/lib/typescript/src/localization/StringSet.type.d.ts +3 -0
  48. package/lib/typescript/src/version.d.ts +1 -1
  49. package/package.json +8 -7
  50. package/src/components/ChannelMessageList/index.tsx +71 -5
  51. package/src/components/GroupChannelMessageRenderer/GroupChannelMessageNewLine.tsx +45 -0
  52. package/src/components/GroupChannelMessageRenderer/index.tsx +5 -0
  53. package/src/components/UnreadMessagesFloating.tsx +60 -0
  54. package/src/domain/groupChannel/component/GroupChannelMessageList.tsx +204 -5
  55. package/src/domain/groupChannel/types.ts +15 -0
  56. package/src/fragments/createGroupChannelFragment.tsx +67 -4
  57. package/src/localization/StringSet.type.ts +3 -0
  58. package/src/localization/createBaseStringSet.ts +16 -4
  59. package/src/version.ts +1 -1
@@ -1,16 +1,19 @@
1
- import React, { useContext, useEffect } from 'react';
1
+ import type { ViewToken } from '@react-native/virtualized-lists';
2
+ import React, { useCallback, useContext, useEffect, useRef, useState } from 'react';
2
3
 
3
4
  import { useToast } from '@sendbird/uikit-react-native-foundation';
4
5
  import { useGroupChannelHandler } from '@sendbird/uikit-tools';
5
6
  import {
6
7
  SendbirdMessage,
7
8
  SendbirdSendableMessage,
9
+ confirmAndMarkAsRead,
8
10
  isDifferentChannel,
9
11
  useFreshCallback,
10
12
  useIsFirstMount,
11
13
  } from '@sendbird/uikit-utils';
12
14
 
13
15
  import ChannelMessageList from '../../../components/ChannelMessageList';
16
+ import { UnreadMessagesFloatingProps } from '../../../components/UnreadMessagesFloating';
14
17
  import { MESSAGE_FOCUS_ANIMATION_DELAY, MESSAGE_SEARCH_SAFE_SCROLL_DELAY } from '../../../constants';
15
18
  import { GroupChannelFragmentOptionsPubSubContextPayload } from '../../../contexts/SendbirdChatCtx';
16
19
  import { useLocalization, useSendbirdChat } from '../../../hooks/useContext';
@@ -22,13 +25,42 @@ const GroupChannelMessageList = (props: GroupChannelProps['MessageList']) => {
22
25
  const { STRINGS } = useLocalization();
23
26
  const { sdk, sbOptions, groupChannelFragmentOptions } = useSendbirdChat();
24
27
  const { setMessageToEdit, setMessageToReply } = useContext(GroupChannelContexts.Fragment);
25
- const { subscribe } = useContext(GroupChannelContexts.PubSub);
28
+ const groupChannelPubSub = useContext(GroupChannelContexts.PubSub);
26
29
  const { flatListRef, lazyScrollToBottom, lazyScrollToIndex, onPressReplyMessageInThread } = useContext(
27
30
  GroupChannelContexts.MessageList,
28
31
  );
29
32
 
30
33
  const isFirstMount = useIsFirstMount();
31
34
 
35
+ const hasSeenNewLineRef = useRef(false);
36
+ const isNewLineInViewportRef = useRef(false);
37
+ const isNewLineExistInChannelRef = useRef(false);
38
+ const scrolledAwayFromBottomRef = useRef(false);
39
+ const [isVisibleUnreadMessageFloating, setIsVisibleUnreadMessageFloating] = useState(false);
40
+ const viewableMessages = useRef<SendbirdMessage[]>();
41
+ const hasUserMarkedAsUnreadRef = useRef(false);
42
+ const [unreadFirstMessage, setUnreadFirstMessage] = useState<SendbirdMessage | undefined>(undefined);
43
+
44
+ const updateHasSeenNewLine = useCallback(
45
+ (hasSeenNewLine: boolean) => {
46
+ if (hasSeenNewLineRef.current !== hasSeenNewLine) {
47
+ hasSeenNewLineRef.current = hasSeenNewLine;
48
+ props.onNewLineSeenChange?.(hasSeenNewLine);
49
+ }
50
+ },
51
+ [props.onNewLineSeenChange],
52
+ );
53
+
54
+ const updateHasUserMarkedAsUnread = useCallback(
55
+ (hasUserMarkedAsUnread: boolean) => {
56
+ if (hasUserMarkedAsUnreadRef.current !== hasUserMarkedAsUnread) {
57
+ hasUserMarkedAsUnreadRef.current = hasUserMarkedAsUnread;
58
+ props.onUserMarkedAsUnreadChange?.(hasUserMarkedAsUnread);
59
+ }
60
+ },
61
+ [props.onUserMarkedAsUnreadChange],
62
+ );
63
+
32
64
  const scrollToMessageWithCreatedAt = useFreshCallback(
33
65
  (createdAt: number, focusAnimated: boolean, timeout: number): boolean => {
34
66
  const foundMessageIndex = props.messages.findIndex((it) => it.createdAt === createdAt);
@@ -53,19 +85,164 @@ const GroupChannelMessageList = (props: GroupChannelProps['MessageList']) => {
53
85
  },
54
86
  );
55
87
 
88
+ const onScrolledAwayFromBottom = useFreshCallback((value: boolean) => {
89
+ scrolledAwayFromBottomRef.current = value;
90
+ props.onScrolledAwayFromBottom(value);
91
+ });
92
+
56
93
  const scrollToBottom = useFreshCallback(async (animated = false) => {
57
94
  if (props.hasNext()) {
58
95
  props.onUpdateSearchItem(undefined);
59
- props.onScrolledAwayFromBottom(false);
96
+ onScrolledAwayFromBottom(false);
60
97
 
61
98
  await props.onResetMessageList().catch((_) => {});
62
- props.onScrolledAwayFromBottom(false);
99
+ onScrolledAwayFromBottom(false);
63
100
  lazyScrollToBottom({ animated });
64
101
  } else {
65
102
  lazyScrollToBottom({ animated });
66
103
  }
67
104
  });
68
105
 
106
+ const onPressUnreadMessagesFloatingCloseButton = useCallback(() => {
107
+ updateHasSeenNewLine(true);
108
+ updateHasUserMarkedAsUnread(false);
109
+ props.resetNewMessages?.();
110
+ confirmAndMarkAsRead([props.channel]);
111
+ }, [updateHasSeenNewLine, updateHasUserMarkedAsUnread, props.channel.url, props.resetNewMessages]);
112
+
113
+ const getPrevNonSilentMessage = useCallback(
114
+ (messages: SendbirdMessage[], prevMessageIndex: number): SendbirdMessage | null => {
115
+ if (messages.length <= prevMessageIndex) {
116
+ return null;
117
+ }
118
+
119
+ const prevMessage = messages[prevMessageIndex];
120
+ if (prevMessage) {
121
+ if (prevMessage.silent) {
122
+ return getPrevNonSilentMessage(messages, prevMessageIndex + 1);
123
+ } else {
124
+ return prevMessage;
125
+ }
126
+ }
127
+ return null;
128
+ },
129
+ [],
130
+ );
131
+
132
+ const findFirstUnreadMessage = useFreshCallback((isNewLineExistInChannel: boolean) => {
133
+ if (!sbOptions.uikit.groupChannel.channel.enableMarkAsUnread || !isNewLineExistInChannel) {
134
+ return;
135
+ }
136
+
137
+ return props.messages.find((msg, index) => {
138
+ if (msg.silent) {
139
+ return false;
140
+ }
141
+
142
+ const isMarkedAsUnreadMessage = props.channel.myLastRead === msg.createdAt - 1;
143
+ if (isMarkedAsUnreadMessage) {
144
+ return true;
145
+ }
146
+
147
+ const prevNonSilentMessage = getPrevNonSilentMessage(props.messages, index + 1);
148
+ const hasNoPreviousAndNoPrevMessage = !props.hasPrevious?.() && prevNonSilentMessage == null;
149
+ const prevMessageIsRead =
150
+ prevNonSilentMessage != null && prevNonSilentMessage.createdAt <= props.channel.myLastRead;
151
+ const isMessageUnread = props.channel.myLastRead < msg.createdAt;
152
+ return (hasNoPreviousAndNoPrevMessage || prevMessageIsRead) && isMessageUnread;
153
+ });
154
+ });
155
+
156
+ useEffect(() => {
157
+ if (!unreadFirstMessage) {
158
+ const foundUnreadFirstMessage = findFirstUnreadMessage(props.isNewLineExistInChannel ?? false);
159
+ if (foundUnreadFirstMessage) {
160
+ processNewLineVisibility(foundUnreadFirstMessage);
161
+ setUnreadFirstMessage(foundUnreadFirstMessage);
162
+ }
163
+ }
164
+ }, [props.messages, props.channel.myLastRead, sbOptions.uikit.groupChannel.channel.enableMarkAsUnread]);
165
+
166
+ const processNewLineVisibility = useFreshCallback((unreadFirstMsg: SendbirdMessage | undefined) => {
167
+ const isNewLineInViewport = !!viewableMessages.current?.some(
168
+ (message) => message.messageId === unreadFirstMsg?.messageId,
169
+ );
170
+
171
+ if (isNewLineInViewportRef.current !== isNewLineInViewport) {
172
+ isNewLineInViewportRef.current = isNewLineInViewport;
173
+ updateUnreadMessagesFloatingProps();
174
+ if (!isNewLineInViewport || hasSeenNewLineRef.current) {
175
+ return;
176
+ }
177
+
178
+ updateHasSeenNewLine(true);
179
+ if (hasUserMarkedAsUnreadRef.current) {
180
+ return;
181
+ }
182
+
183
+ if (0 < props.newMessages.length) {
184
+ props.channel.markAsUnread(props.newMessages[0]);
185
+ } else {
186
+ props.channel.markAsRead();
187
+ }
188
+ }
189
+ });
190
+
191
+ const onViewableItemsChanged = useFreshCallback(
192
+ async (info: { viewableItems: Array<ViewToken<SendbirdMessage>>; changed: Array<ViewToken<SendbirdMessage>> }) => {
193
+ if (!sbOptions.uikit.groupChannel.channel.enableMarkAsUnread) {
194
+ return;
195
+ }
196
+
197
+ viewableMessages.current = info.viewableItems.filter((token) => token.item).map((token) => token.item);
198
+ processNewLineVisibility(unreadFirstMessage);
199
+ },
200
+ );
201
+
202
+ const onPressMarkAsUnreadMessage = useCallback(
203
+ async (message: SendbirdMessage) => {
204
+ if (sbOptions.uikit.groupChannel.channel.enableMarkAsUnread && message) {
205
+ await props.channel.markAsUnread(message);
206
+ updateHasUserMarkedAsUnread(true);
207
+ }
208
+ },
209
+ [sbOptions.uikit.groupChannel.channel.enableMarkAsUnread, updateHasUserMarkedAsUnread],
210
+ );
211
+
212
+ useEffect(() => {
213
+ isNewLineExistInChannelRef.current = !!props.isNewLineExistInChannel && !!viewableMessages.current;
214
+ }, [props.isNewLineExistInChannel, viewableMessages.current]);
215
+
216
+ const unreadMessagesFloatingPropsRef = useRef<UnreadMessagesFloatingProps>();
217
+ const updateUnreadMessagesFloatingProps = useFreshCallback(() => {
218
+ const canAutoMarkAsRead =
219
+ !scrolledAwayFromBottomRef.current &&
220
+ !hasUserMarkedAsUnreadRef.current &&
221
+ (hasSeenNewLineRef.current || !isNewLineExistInChannelRef.current);
222
+
223
+ unreadMessagesFloatingPropsRef.current = {
224
+ visible:
225
+ sbOptions.uikit.groupChannel.channel.enableMarkAsUnread &&
226
+ !canAutoMarkAsRead &&
227
+ isNewLineExistInChannelRef.current &&
228
+ 0 < props.channel.unreadMessageCount &&
229
+ !isNewLineInViewportRef.current,
230
+ onPressClose: onPressUnreadMessagesFloatingCloseButton,
231
+ unreadMessageCount: props.channel.unreadMessageCount,
232
+ };
233
+ if (isVisibleUnreadMessageFloating !== unreadMessagesFloatingPropsRef.current.visible) {
234
+ setIsVisibleUnreadMessageFloating(unreadMessagesFloatingPropsRef.current.visible);
235
+ }
236
+ });
237
+
238
+ useEffect(() => {
239
+ updateUnreadMessagesFloatingProps();
240
+ }, [
241
+ isNewLineExistInChannelRef.current,
242
+ props.channel.unreadMessageCount,
243
+ sbOptions.uikit.groupChannel.channel.enableMarkAsUnread,
244
+ ]);
245
+
69
246
  useGroupChannelHandler(sdk, {
70
247
  onReactionUpdated(channel, event) {
71
248
  if (isDifferentChannel(channel, props.channel)) return;
@@ -76,10 +253,13 @@ const GroupChannelMessageList = (props: GroupChannelProps['MessageList']) => {
76
253
  lazyScrollToBottom({ animated: true, timeout: 250 });
77
254
  }
78
255
  },
256
+ onChannelChanged(channel) {
257
+ if (isDifferentChannel(channel, props.channel)) return;
258
+ },
79
259
  });
80
260
 
81
261
  useEffect(() => {
82
- return subscribe(({ type, data }) => {
262
+ return groupChannelPubSub.subscribe(({ type, data }) => {
83
263
  switch (type) {
84
264
  case 'TYPING_BUBBLE_RENDERED':
85
265
  case 'MESSAGES_RECEIVED': {
@@ -109,6 +289,20 @@ const GroupChannelMessageList = (props: GroupChannelProps['MessageList']) => {
109
289
  scrollToBottom(false);
110
290
  break;
111
291
  }
292
+ case 'ON_MARKED_AS_READ_BY_CURRENT_USER': {
293
+ updateUnreadMessagesFloatingProps();
294
+ break;
295
+ }
296
+ case 'ON_MARKED_AS_UNREAD_BY_CURRENT_USER': {
297
+ isNewLineExistInChannelRef.current = true;
298
+ const foundFirstUnreadMessage = findFirstUnreadMessage(true);
299
+ processNewLineVisibility(foundFirstUnreadMessage);
300
+ setUnreadFirstMessage(foundFirstUnreadMessage);
301
+ if (!props.scrolledAwayFromBottom) {
302
+ scrollToBottom(true);
303
+ }
304
+ break;
305
+ }
112
306
  }
113
307
  });
114
308
  }, [props.scrolledAwayFromBottom]);
@@ -156,12 +350,17 @@ const GroupChannelMessageList = (props: GroupChannelProps['MessageList']) => {
156
350
  <ChannelMessageList
157
351
  {...props}
158
352
  ref={flatListRef}
353
+ onScrolledAwayFromBottom={onScrolledAwayFromBottom}
159
354
  onReplyMessage={setMessageToReply}
160
355
  onReplyInThreadMessage={setMessageToReply}
161
356
  onEditMessage={setMessageToEdit}
357
+ onViewableItemsChanged={onViewableItemsChanged}
162
358
  onPressParentMessage={onPressParentMessage}
163
359
  onPressNewMessagesButton={scrollToBottom}
164
360
  onPressScrollToBottomButton={scrollToBottom}
361
+ onPressMarkAsUnreadMessage={onPressMarkAsUnreadMessage}
362
+ unreadFirstMessage={unreadFirstMessage}
363
+ unreadMessagesFloatingProps={unreadMessagesFloatingPropsRef.current}
165
364
  />
166
365
  );
167
366
  };
@@ -41,6 +41,7 @@ export interface GroupChannelProps {
41
41
  renderMessage?: GroupChannelProps['MessageList']['renderMessage'];
42
42
  renderNewMessagesButton?: GroupChannelProps['MessageList']['renderNewMessagesButton'];
43
43
  renderScrollToBottomButton?: GroupChannelProps['MessageList']['renderScrollToBottomButton'];
44
+ renderUnreadMessagesFloating?: GroupChannelProps['MessageList']['renderUnreadMessagesFloating'];
44
45
 
45
46
  enableTypingIndicator?: GroupChannelProps['Provider']['enableTypingIndicator'];
46
47
  enableMessageGrouping?: GroupChannelProps['MessageList']['enableMessageGrouping'];
@@ -85,6 +86,7 @@ export interface GroupChannelProps {
85
86
  | 'renderMessage'
86
87
  | 'renderNewMessagesButton'
87
88
  | 'renderScrollToBottomButton'
89
+ | 'renderUnreadMessagesFloating'
88
90
  | 'flatListComponent'
89
91
  | 'flatListProps'
90
92
  | 'hasNext'
@@ -95,6 +97,11 @@ export interface GroupChannelProps {
95
97
 
96
98
  // Changing the search item will trigger the focus animation on messages.
97
99
  onUpdateSearchItem: (searchItem?: GroupChannelProps['MessageList']['searchItem']) => void;
100
+ hasPrevious?: () => boolean;
101
+ resetNewMessages?: () => void;
102
+ isNewLineExistInChannel?: boolean;
103
+ onNewLineSeenChange?: (hasSeenNewLine: boolean) => void;
104
+ onUserMarkedAsUnreadChange?: (hasUserMarkedAsUnread: boolean) => void;
98
105
  };
99
106
  Input: PickPartial<
100
107
  ChannelInputProps,
@@ -208,4 +215,12 @@ export type GroupChannelPubSubContextPayload =
208
215
  | {
209
216
  type: 'TYPING_BUBBLE_RENDERED';
210
217
  data?: undefined;
218
+ }
219
+ | {
220
+ type: 'ON_MARKED_AS_READ_BY_CURRENT_USER';
221
+ data?: undefined;
222
+ }
223
+ | {
224
+ type: 'ON_MARKED_AS_UNREAD_BY_CURRENT_USER';
225
+ data?: undefined;
211
226
  };
@@ -1,6 +1,11 @@
1
- import React, { useCallback, useEffect, useMemo, useState } from 'react';
1
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
2
 
3
- import { MessageCollection, MessageFilter } from '@sendbird/chat/groupChannel';
3
+ import {
4
+ type GroupChannel,
5
+ GroupChannelEventSource,
6
+ MessageCollection,
7
+ MessageFilter,
8
+ } from '@sendbird/chat/groupChannel';
4
9
  import { ReplyType } from '@sendbird/chat/message';
5
10
  import { Box, useToast } from '@sendbird/uikit-react-native-foundation';
6
11
  import { useGroupChannelMessages } from '@sendbird/uikit-tools';
@@ -27,6 +32,7 @@ import GroupChannelMessageRenderer, {
27
32
  import NewMessagesButton from '../components/NewMessagesButton';
28
33
  import ScrollToBottomButton from '../components/ScrollToBottomButton';
29
34
  import StatusComposition from '../components/StatusComposition';
35
+ import UnreadMessagesFloating from '../components/UnreadMessagesFloating';
30
36
  import createGroupChannelModule from '../domain/groupChannel/module/createGroupChannelModule';
31
37
  import type {
32
38
  GroupChannelFragment,
@@ -43,6 +49,7 @@ const createGroupChannelFragment = (initModule?: Partial<GroupChannelModule>): G
43
49
 
44
50
  return ({
45
51
  searchItem,
52
+ renderUnreadMessagesFloating = (props) => <UnreadMessagesFloating {...props} />,
46
53
  renderNewMessagesButton = (props) => <NewMessagesButton {...props} />,
47
54
  renderScrollToBottomButton = (props) => <ScrollToBottomButton {...props} />,
48
55
  renderMessage,
@@ -85,6 +92,36 @@ const createGroupChannelFragment = (initModule?: Partial<GroupChannelModule>): G
85
92
  }
86
93
  });
87
94
 
95
+ const [isNewLineExistInChannel, setIsNewLineExistInChannel] = useState(false);
96
+ const hasSeenNewLineRef = useRef(false);
97
+ const hasUserMarkedAsUnreadRef = useRef(false);
98
+
99
+ useEffect(() => {
100
+ setIsNewLineExistInChannel(channel.myLastRead < (channel.lastMessage?.createdAt ?? Number.MIN_SAFE_INTEGER));
101
+ }, [channel.url]);
102
+
103
+ const onNewLineSeenChange = useFreshCallback((hasSeenNewLine: boolean) => {
104
+ hasSeenNewLineRef.current = hasSeenNewLine;
105
+ });
106
+
107
+ const onUserMarkedAsUnreadChange = useFreshCallback((hasUserMarkedAsUnread: boolean) => {
108
+ hasUserMarkedAsUnreadRef.current = hasUserMarkedAsUnread;
109
+ });
110
+
111
+ const markAsRead = useFreshCallback((channels: GroupChannel[]) => {
112
+ if (sbOptions.uikit.groupChannel.channel.enableMarkAsUnread) {
113
+ if (
114
+ !scrolledAwayFromBottom &&
115
+ !hasUserMarkedAsUnreadRef.current &&
116
+ (hasSeenNewLineRef.current || !isNewLineExistInChannel)
117
+ ) {
118
+ confirmAndMarkAsRead(channels, true);
119
+ }
120
+ } else {
121
+ confirmAndMarkAsRead(channels);
122
+ }
123
+ });
124
+
88
125
  const {
89
126
  loading,
90
127
  messages,
@@ -93,6 +130,7 @@ const createGroupChannelFragment = (initModule?: Partial<GroupChannelModule>): G
93
130
  loadNext,
94
131
  loadPrevious,
95
132
  hasNext,
133
+ hasPrevious,
96
134
  sendFileMessage,
97
135
  sendUserMessage,
98
136
  updateFileMessage,
@@ -108,11 +146,23 @@ const createGroupChannelFragment = (initModule?: Partial<GroupChannelModule>): G
108
146
  onMessagesUpdated(messages) {
109
147
  groupChannelPubSub.publish({ type: 'MESSAGES_UPDATED', data: { messages } });
110
148
  },
149
+ onChannelUpdated(_, ctx) {
150
+ if (ctx?.source === GroupChannelEventSource.EVENT_CHANNEL_READ) {
151
+ if (ctx.userIds.includes(currentUser?.userId ?? '')) {
152
+ groupChannelPubSub.publish({ type: 'ON_MARKED_AS_READ_BY_CURRENT_USER' });
153
+ }
154
+ } else if (ctx?.source === GroupChannelEventSource.EVENT_CHANNEL_UNREAD) {
155
+ if (ctx.userIds.includes(currentUser?.userId ?? '')) {
156
+ setIsNewLineExistInChannel(true);
157
+ groupChannelPubSub.publish({ type: 'ON_MARKED_AS_UNREAD_BY_CURRENT_USER' });
158
+ }
159
+ }
160
+ },
111
161
  onChannelDeleted,
112
162
  onCurrentUserBanned: onChannelDeleted,
113
163
  collectionCreator: getCollectionCreator(channel, messageListQueryParams, collectionCreator),
114
164
  sortComparator,
115
- markAsRead: confirmAndMarkAsRead,
165
+ markAsRead: markAsRead,
116
166
  replyType,
117
167
  startingPoint: internalSearchItem?.startingPoint,
118
168
  });
@@ -227,7 +277,14 @@ const createGroupChannelFragment = (initModule?: Partial<GroupChannelModule>): G
227
277
  },
228
278
  );
229
279
  const onScrolledAwayFromBottom = useFreshCallback((value: boolean) => {
230
- if (!value) resetNewMessages();
280
+ if (!value) {
281
+ resetNewMessages();
282
+ if (sbOptions.uikit.groupChannel.channel.enableMarkAsUnread) {
283
+ if (!hasUserMarkedAsUnreadRef.current && (hasSeenNewLineRef.current || !isNewLineExistInChannel)) {
284
+ confirmAndMarkAsRead([channel]);
285
+ }
286
+ }
287
+ }
231
288
  setScrolledAwayFromBottom(value);
232
289
  });
233
290
 
@@ -261,15 +318,21 @@ const createGroupChannelFragment = (initModule?: Partial<GroupChannelModule>): G
261
318
  onTopReached={loadPrevious}
262
319
  onBottomReached={loadNext}
263
320
  hasNext={hasNext}
321
+ hasPrevious={hasPrevious}
322
+ resetNewMessages={resetNewMessages}
264
323
  scrolledAwayFromBottom={scrolledAwayFromBottom}
265
324
  onScrolledAwayFromBottom={onScrolledAwayFromBottom}
266
325
  renderNewMessagesButton={renderNewMessagesButton}
267
326
  renderScrollToBottomButton={renderScrollToBottomButton}
327
+ renderUnreadMessagesFloating={renderUnreadMessagesFloating}
268
328
  onResendFailedMessage={resendMessage}
269
329
  onDeleteMessage={deleteMessage}
270
330
  onPressMediaMessage={_onPressMediaMessage}
271
331
  flatListComponent={flatListComponent}
272
332
  flatListProps={memoizedFlatListProps}
333
+ isNewLineExistInChannel={isNewLineExistInChannel}
334
+ onNewLineSeenChange={onNewLineSeenChange}
335
+ onUserMarkedAsUnreadChange={onUserMarkedAsUnreadChange}
273
336
  />
274
337
  <GroupChannelModule.Input
275
338
  SuggestedMentionList={GroupChannelModule.SuggestedMentionList}
@@ -117,6 +117,8 @@ export interface StringSet {
117
117
  /** GroupChannel > List */
118
118
  LIST_DATE_SEPARATOR: (date: Date, locale?: Locale) => string;
119
119
  LIST_BUTTON_NEW_MSG: (newMessages: SendbirdMessage[]) => string;
120
+ LIST_FLOATING_UNREAD_MSG: (unreadMessageCount: number) => string;
121
+ LIST_NEW_LINE: string;
120
122
 
121
123
  /** GroupChannel > Message bubble */
122
124
  MESSAGE_BUBBLE_TIME: (message: SendbirdMessage, locale?: Locale) => string;
@@ -312,6 +314,7 @@ export interface StringSet {
312
314
  CHANNEL_MESSAGE_DELETE: string;
313
315
  CHANNEL_MESSAGE_REPLY: string;
314
316
  CHANNEL_MESSAGE_THREAD: string;
317
+ CHANNEL_MESSAGE_MARK_AS_UNREAD: string;
315
318
  /** Channel > Message > Delete confirm **/
316
319
  CHANNEL_MESSAGE_DELETE_CONFIRM_TITLE: string;
317
320
  CHANNEL_MESSAGE_DELETE_CONFIRM_OK: string;
@@ -117,8 +117,16 @@ export const createBaseStringSet = ({ dateLocale, overrides }: StringSetCreateOp
117
117
  GROUP_CHANNEL: {
118
118
  HEADER_TITLE: (uid, channel) => getGroupChannelTitle(uid, channel, USER_NO_NAME, CHANNEL_NO_MEMBERS),
119
119
  LIST_DATE_SEPARATOR: (date, locale) => getDateSeparatorFormat(date, locale ?? dateLocale),
120
- LIST_BUTTON_NEW_MSG: (newMessages) => `${newMessages.length} new messages`,
121
-
120
+ LIST_BUTTON_NEW_MSG: (newMessages) => {
121
+ const count = newMessages.length;
122
+ const displayCount = count >= 100 ? '99+' : count;
123
+ return count === 1 ? `${displayCount} new message` : `${displayCount} new messages`;
124
+ },
125
+ LIST_FLOATING_UNREAD_MSG: (unreadMessageCount) => {
126
+ const displayCount = unreadMessageCount >= 100 ? '99+' : unreadMessageCount;
127
+ return unreadMessageCount === 1 ? `${displayCount} unread message` : `${displayCount} unread messages`;
128
+ },
129
+ LIST_NEW_LINE: 'New messages',
122
130
  MESSAGE_BUBBLE_TIME: (message, locale) => getMessageTimeFormat(new Date(message.createdAt), locale ?? dateLocale),
123
131
  MESSAGE_BUBBLE_FILE_TITLE: (message) => message.name,
124
132
  MESSAGE_BUBBLE_EDITED_POSTFIX: ' (edited)',
@@ -132,8 +140,11 @@ export const createBaseStringSet = ({ dateLocale, overrides }: StringSetCreateOp
132
140
  HEADER_TITLE: 'Thread',
133
141
  HEADER_SUBTITLE: (uid, channel) => getGroupChannelTitle(uid, channel, USER_NO_NAME, CHANNEL_NO_MEMBERS),
134
142
  LIST_DATE_SEPARATOR: (date, locale) => getDateSeparatorFormat(date, locale ?? dateLocale),
135
- LIST_BUTTON_NEW_MSG: (newMessages) => `${newMessages.length} new messages`,
136
-
143
+ LIST_BUTTON_NEW_MSG: (newMessages) => {
144
+ const count = newMessages.length;
145
+ const displayCount = count >= 100 ? '99+' : count;
146
+ return count === 1 ? `${displayCount} new message` : `${displayCount} new messages`;
147
+ },
137
148
  MESSAGE_BUBBLE_TIME: (message, locale) => getMessageTimeFormat(new Date(message.createdAt), locale ?? dateLocale),
138
149
  MESSAGE_BUBBLE_FILE_TITLE: (message) => message.name,
139
150
  MESSAGE_BUBBLE_EDITED_POSTFIX: ' (edited)',
@@ -303,6 +314,7 @@ export const createBaseStringSet = ({ dateLocale, overrides }: StringSetCreateOp
303
314
  CHANNEL_MESSAGE_DELETE: 'Delete',
304
315
  CHANNEL_MESSAGE_REPLY: 'Reply',
305
316
  CHANNEL_MESSAGE_THREAD: 'Reply in thread',
317
+ CHANNEL_MESSAGE_MARK_AS_UNREAD: 'Mark as unread',
306
318
  CHANNEL_MESSAGE_DELETE_CONFIRM_TITLE: 'Delete message?',
307
319
  CHANNEL_MESSAGE_DELETE_CONFIRM_OK: 'Delete',
308
320
  CHANNEL_MESSAGE_DELETE_CONFIRM_CANCEL: 'Cancel',
package/src/version.ts CHANGED
@@ -1,2 +1,2 @@
1
- const VERSION = '3.9.5';
1
+ const VERSION = '3.10.0';
2
2
  export default VERSION;