Skip to content

Commit cc6250d

Browse files
committed
chore: add 'mark as unread' functionality and update related UI components
1 parent fb3256b commit cc6250d

File tree

8 files changed

+131
-46
lines changed

8 files changed

+131
-46
lines changed

packages/uikit-react-native/src/components/ChannelMessageList/index.tsx

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ export type ChannelMessageListProps<T extends SendbirdGroupChannel | SendbirdOpe
7070
onResendFailedMessage: (failedMessage: HandleableMessage) => Promise<HandleableMessage | void>;
7171
onPressParentMessage?: (parentMessage: SendbirdMessage, childMessage: HandleableMessage) => void;
7272
onPressMediaMessage?: (message: SendbirdFileMessage, deleteMessage: () => Promise<void>, uri: string) => void;
73+
onPressMarkAsUnreadMessage?: (message: HandleableMessage) => void;
7374

7475
renderMessage: (props: {
7576
focused: boolean;
@@ -86,15 +87,18 @@ export type ChannelMessageListProps<T extends SendbirdGroupChannel | SendbirdOpe
8687
enableMessageGrouping: ChannelMessageListProps<T>['enableMessageGrouping'];
8788
bottomSheetItem?: BottomSheetItem;
8889
isFirstItem: boolean;
89-
isFirstUnreadMessage: boolean;
90+
isFirstUnreadMessage?: boolean;
9091
hideParentMessage?: boolean;
9192
}) => React.ReactElement | null;
9293
renderNewMessagesButton:
9394
| null
9495
| ((props: { visible: boolean; onPress: () => void; newMessages: SendbirdMessage[] }) => React.ReactElement | null);
9596
renderScrollToBottomButton: null | ((props: { visible: boolean; onPress: () => void }) => React.ReactElement | null);
96-
renderUnreadMessagesFloating: null | ((props: UnreadMessagesFloatingProps) => React.ReactElement | null);
97+
renderUnreadMessagesFloating?: null | ((props: UnreadMessagesFloatingProps) => React.ReactElement | null);
9798
unreadMessagesFloatingProps?: UnreadMessagesFloatingProps;
99+
hasSeenNewLine?: boolean;
100+
isNewLineInViewport?: boolean;
101+
hasUserMarkedAsUnread?: boolean;
98102
flatListComponent?: React.ComponentType<FlatListProps<SendbirdMessage>>;
99103
flatListProps?: Omit<FlatListProps<SendbirdMessage>, 'data' | 'renderItem'>;
100104
onViewableItemsChanged?: FlatListProps<SendbirdMessage>['onViewableItemsChanged'];
@@ -114,6 +118,7 @@ const ChannelMessageList = <T extends SendbirdGroupChannel | SendbirdOpenChannel
114118
onResendFailedMessage,
115119
onPressMediaMessage,
116120
onPressParentMessage,
121+
onPressMarkAsUnreadMessage,
117122
currentUserId,
118123
renderUnreadMessagesFloating,
119124
renderNewMessagesButton,
@@ -149,6 +154,7 @@ const ChannelMessageList = <T extends SendbirdGroupChannel | SendbirdOpenChannel
149154
onDeleteMessage,
150155
onResendFailedMessage,
151156
onPressMediaMessage,
157+
onPressMarkAsUnreadMessage,
152158
});
153159

154160
const renderItem: ListRenderItem<SendbirdMessage> = useFreshCallback(({ item, index }) => {
@@ -235,6 +241,7 @@ const useCreateMessagePressActions = <T extends SendbirdGroupChannel | SendbirdO
235241
onReplyInThreadMessage,
236242
onDeleteMessage,
237243
onPressMediaMessage,
244+
onPressMarkAsUnreadMessage,
238245
}: Pick<
239246
ChannelMessageListProps<T>,
240247
| 'channel'
@@ -245,6 +252,7 @@ const useCreateMessagePressActions = <T extends SendbirdGroupChannel | SendbirdO
245252
| 'onDeleteMessage'
246253
| 'onResendFailedMessage'
247254
| 'onPressMediaMessage'
255+
| 'onPressMarkAsUnreadMessage'
248256
>): CreateMessagePressActions => {
249257
const handlers = useSBUHandlers();
250258
const { colors } = useUIKitTheme();
@@ -304,10 +312,8 @@ const useCreateMessagePressActions = <T extends SendbirdGroupChannel | SendbirdO
304312
}
305313
};
306314

307-
const onMarkAsUnread = async (message: HandleableMessage) => {
308-
if (sbOptions.uikitWithAppInfo.groupChannel.channel.enableMarkAsUnread && channel.isGroupChannel()) {
309-
await (channel as SendbirdGroupChannel).markAsUnread(message);
310-
}
315+
const onMarkAsUnread = (message: HandleableMessage) => {
316+
onPressMarkAsUnreadMessage?.(message);
311317
};
312318

313319
const openSheetForFailedMessage = (message: HandleableMessage) => {

packages/uikit-react-native/src/components/GroupChannelMessageRenderer/GroupChannelMessageNewLine.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ const GroupChannelMessageNewLine = ({ shouldRenderNewLine }: Props) => {
1919
<View style={styles.container}>
2020
<Box backgroundColor={select({ light: palette.primary300, dark: palette.primary200 })} style={styles.line} />
2121
<Text caption3 color={select({ light: palette.primary300, dark: palette.primary200 })} style={styles.label}>
22-
{STRINGS.GROUP_CHANNEL.LIST_NEW_MESSAGE_SEPARATOR}
22+
{STRINGS.GROUP_CHANNEL.LIST_NEW_LINE}
2323
</Text>
2424
<Box backgroundColor={select({ light: palette.primary300, dark: palette.primary200 })} style={styles.line} />
2525
</View>

packages/uikit-react-native/src/domain/groupChannel/component/GroupChannelMessageList.tsx

Lines changed: 57 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@ import React, { useCallback, useContext, useEffect, useMemo, useRef } from 'reac
44
import { useToast } from '@sendbird/uikit-react-native-foundation';
55
import { useGroupChannelHandler } from '@sendbird/uikit-tools';
66
import {
7+
type SendbirdFileMessage,
78
SendbirdMessage,
89
SendbirdSendableMessage,
10+
type SendbirdUserMessage,
11+
confirmAndMarkAsRead,
912
isDifferentChannel,
1013
useFreshCallback,
1114
useIsFirstMount,
@@ -24,15 +27,18 @@ const GroupChannelMessageList = (props: GroupChannelProps['MessageList']) => {
2427
const { STRINGS } = useLocalization();
2528
const { sdk, sbOptions, groupChannelFragmentOptions } = useSendbirdChat();
2629
const { setMessageToEdit, setMessageToReply } = useContext(GroupChannelContexts.Fragment);
27-
const { subscribe } = useContext(GroupChannelContexts.PubSub);
30+
const groupChannelPubSub = useContext(GroupChannelContexts.PubSub);
2831
const {
2932
flatListRef,
3033
lazyScrollToBottom,
3134
lazyScrollToIndex,
3235
onPressReplyMessageInThread,
33-
setHasSeenNewLine,
34-
isNewLineVisible,
35-
setIsNewLineVisible,
36+
isNewLineInViewport,
37+
hasSeenNewLine,
38+
hasUserMarkedAsUnread,
39+
updateIsNewLineInViewport,
40+
updateHasSeenNewLine,
41+
updateHasUserMarkedAsUnread,
3642
} = useContext(GroupChannelContexts.MessageList);
3743

3844
const isFirstMount = useIsFirstMount();
@@ -90,7 +96,7 @@ const GroupChannelMessageList = (props: GroupChannelProps['MessageList']) => {
9096
});
9197

9298
useEffect(() => {
93-
return subscribe(({ type, data }) => {
99+
return groupChannelPubSub.subscribe(({ type, data }) => {
94100
switch (type) {
95101
case 'TYPING_BUBBLE_RENDERED':
96102
case 'MESSAGES_RECEIVED': {
@@ -164,26 +170,32 @@ const GroupChannelMessageList = (props: GroupChannelProps['MessageList']) => {
164170
);
165171

166172
const onPressUnreadMessagesFloatingCloseButton = useCallback(() => {
167-
setHasSeenNewLine?.(true);
168-
}, [setHasSeenNewLine]);
173+
updateHasSeenNewLine?.(true);
174+
updateHasUserMarkedAsUnread?.(false);
175+
confirmAndMarkAsRead([props.channel]);
176+
}, [updateHasSeenNewLine, updateHasUserMarkedAsUnread, props.channel.url]);
169177

170178
const unreadMessagesFloatingProps: UnreadMessagesFloatingProps = useMemo(() => {
171179
return {
172180
visible:
173181
sbOptions.uikitWithAppInfo.groupChannel.channel.enableMarkAsUnread &&
174182
0 < props.channel.unreadMessageCount &&
175-
!!isNewLineVisible,
183+
!!isNewLineInViewport,
176184
onPressClose: onPressUnreadMessagesFloatingCloseButton,
177185
unreadMessageCount: props.channel.unreadMessageCount,
178186
};
179187
}, [
180-
isNewLineVisible,
188+
isNewLineInViewport,
181189
props.channel.unreadMessageCount,
182190
sbOptions.uikitWithAppInfo.groupChannel.channel.enableMarkAsUnread,
183191
]);
184192

185193
const unreadFirstMessageRef = useRef<SendbirdMessage | undefined>(undefined);
186194
useEffect(() => {
195+
if (!sbOptions.uikitWithAppInfo.groupChannel.channel.enableMarkAsUnread) {
196+
return;
197+
}
198+
187199
if (!unreadFirstMessageRef.current) {
188200
unreadFirstMessageRef.current = props.messages.find((msg, index) => {
189201
const isMarkedAsUnreadMessage = props.channel.myLastRead === msg.createdAt - 1;
@@ -197,21 +209,47 @@ const GroupChannelMessageList = (props: GroupChannelProps['MessageList']) => {
197209
return isMarkedAsUnreadMessage || isFirstUnreadAfterReadMessages;
198210
});
199211
}
200-
}, [props.messages, props.channel.myLastRead]);
212+
}, [props.messages, props.channel.myLastRead, sbOptions.uikitWithAppInfo.groupChannel.channel.enableMarkAsUnread]);
213+
214+
const onViewableItemsChanged = useFreshCallback(
215+
async (info: { viewableItems: Array<ViewToken<SendbirdMessage>>; changed: Array<ViewToken<SendbirdMessage>> }) => {
216+
if (!sbOptions.uikitWithAppInfo.groupChannel.channel.enableMarkAsUnread) {
217+
return;
218+
}
201219

202-
const onViewableItemsChanged = useCallback(
203-
(info: { viewableItems: Array<ViewToken<SendbirdMessage>>; changed: Array<ViewToken<SendbirdMessage>> }) => {
204220
const foundViewableUnreadFirstMessage = info.viewableItems.find(
205221
(token) => token.item.createdAt === unreadFirstMessageRef.current?.createdAt,
206222
);
207-
if (foundViewableUnreadFirstMessage) {
208-
setIsNewLineVisible?.(true);
209-
setHasSeenNewLine?.(true);
210-
} else {
211-
setIsNewLineVisible?.(false);
223+
224+
const isUnreadFirstMessageInView = foundViewableUnreadFirstMessage !== undefined;
225+
if (isNewLineInViewport !== isUnreadFirstMessageInView) {
226+
updateIsNewLineInViewport?.(isUnreadFirstMessageInView);
227+
if (!isUnreadFirstMessageInView || hasSeenNewLine) {
228+
return;
229+
}
230+
231+
updateHasSeenNewLine?.(true);
232+
if (hasUserMarkedAsUnread) {
233+
return;
234+
}
235+
236+
if (0 < props.newMessages.length) {
237+
await props.channel.markAsUnread(props.newMessages[0]);
238+
} else {
239+
await props.channel.markAsRead();
240+
}
241+
}
242+
},
243+
);
244+
245+
const onPressMarkAsUnreadMessage = useCallback(
246+
async (message: SendbirdUserMessage | SendbirdFileMessage) => {
247+
if (sbOptions.uikitWithAppInfo.groupChannel.channel.enableMarkAsUnread && message) {
248+
await props.channel.markAsUnread(message);
249+
updateHasUserMarkedAsUnread?.(true);
212250
}
213251
},
214-
[],
252+
[sbOptions.uikitWithAppInfo.groupChannel.channel.enableMarkAsUnread, updateHasUserMarkedAsUnread],
215253
);
216254

217255
return (
@@ -225,6 +263,7 @@ const GroupChannelMessageList = (props: GroupChannelProps['MessageList']) => {
225263
onPressParentMessage={onPressParentMessage}
226264
onPressNewMessagesButton={scrollToBottom}
227265
onPressScrollToBottomButton={scrollToBottom}
266+
onPressMarkAsUnreadMessage={onPressMarkAsUnreadMessage}
228267
unreadFirstMessage={unreadFirstMessageRef.current}
229268
unreadMessagesFloatingProps={unreadMessagesFloatingProps}
230269
/>

packages/uikit-react-native/src/domain/groupChannel/module/moduleContext.tsx

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,11 @@ export const GroupChannelContexts: GroupChannelContextsType = {
4747
// noop
4848
},
4949
hasSeenNewLine: false,
50-
setHasSeenNewLine: () => NOOP,
51-
isNewLineVisible: false,
52-
setIsNewLineVisible: () => NOOP,
50+
isNewLineInViewport: false,
51+
hasUserMarkedAsUnread: false,
52+
updateHasSeenNewLine: () => NOOP,
53+
updateIsNewLineInViewport: () => NOOP,
54+
updateHasUserMarkedAsUnread: () => NOOP,
5355
} as MessageListContextValue),
5456
};
5557

@@ -71,14 +73,28 @@ export const GroupChannelContextsProvider: GroupChannelModule['Provider'] = ({
7173
const [typingUsers, setTypingUsers] = useState<SendbirdUser[]>([]);
7274
const [messageToEdit, setMessageToEdit] = useState<SendbirdUserMessage | SendbirdFileMessage>();
7375
const [messageToReply, setMessageToReply] = useState<SendbirdUserMessage | SendbirdFileMessage>();
74-
const [hasSeenNewLine, setHasSeenNewLine] = useState<boolean>();
75-
const [isNewLineVisible, setIsNewLineVisible] = useState<boolean>();
76+
77+
const hasSeenNewLineRef = useRef(false);
78+
const isNewLineInViewportRef = useRef(false);
79+
const hasUserMarkedAsUnreadRef = useRef(false);
7680

7781
const { flatListRef, lazyScrollToIndex, lazyScrollToBottom, scrollToMessage } = useScrollActions({
7882
messages,
7983
onUpdateSearchItem,
8084
});
8185

86+
const updateHasSeenNewLine = useCallback((hasSeenNewLine: boolean) => {
87+
hasSeenNewLineRef.current = hasSeenNewLine;
88+
}, []);
89+
90+
const updateIsNewLineInViewport = useCallback((isNewLineInViewport: boolean) => {
91+
isNewLineInViewportRef.current = isNewLineInViewport;
92+
}, []);
93+
94+
const updateHasUserMarkedAsUnread = useCallback((hasUserMarkedAsUnread: boolean) => {
95+
hasUserMarkedAsUnreadRef.current = hasUserMarkedAsUnread;
96+
}, []);
97+
8298
const updateInputMode = (mode: 'send' | 'edit' | 'reply', message?: SendbirdUserMessage | SendbirdFileMessage) => {
8399
if (mode === 'send' || !message) {
84100
setMessageToEdit(undefined);
@@ -150,10 +166,12 @@ export const GroupChannelContextsProvider: GroupChannelModule['Provider'] = ({
150166
lazyScrollToIndex,
151167
lazyScrollToBottom,
152168
onPressReplyMessageInThread,
153-
hasSeenNewLine,
154-
setHasSeenNewLine,
155-
isNewLineVisible,
156-
setIsNewLineVisible,
169+
hasSeenNewLine: hasSeenNewLineRef.current,
170+
updateHasSeenNewLine: updateHasSeenNewLine,
171+
isNewLineInViewport: isNewLineInViewportRef.current,
172+
updateIsNewLineInViewport: updateIsNewLineInViewport,
173+
hasUserMarkedAsUnread: hasUserMarkedAsUnreadRef.current,
174+
updateHasUserMarkedAsUnread: updateHasUserMarkedAsUnread,
157175
}}
158176
>
159177
{children}

packages/uikit-react-native/src/domain/groupChannel/types.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { FlatList } from 'react-native';
33

44
import type { MessageCollectionParams, MessageFilterParams } from '@sendbird/chat/groupChannel';
55
import type { UseGroupChannelMessagesOptions } from '@sendbird/uikit-chat-hooks';
6-
import type {
6+
import {
77
OnBeforeHandler,
88
PickPartial,
99
SendbirdFileMessage,
@@ -181,9 +181,11 @@ export interface GroupChannelContextsType {
181181

182182
onPressReplyMessageInThread?: (parentMessage: SendbirdSendableMessage, startingPoint?: number) => void;
183183
hasSeenNewLine?: boolean;
184-
setHasSeenNewLine?: (hasSeenNewLine: boolean) => void;
185-
isNewLineVisible?: boolean;
186-
setIsNewLineVisible?: (isNewLineVisible: boolean) => void;
184+
isNewLineInViewport?: boolean;
185+
hasUserMarkedAsUnread?: boolean;
186+
updateHasSeenNewLine?: (hasSeenNewLine: boolean) => void;
187+
updateIsNewLineInViewport?: (isNewLineInViewport: boolean) => void;
188+
updateHasUserMarkedAsUnread?: (hasUserMarkedAsUnread: boolean) => void;
187189
}>;
188190
}
189191
export interface GroupChannelModule {

packages/uikit-react-native/src/fragments/createGroupChannelFragment.tsx

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
import React, { useCallback, useEffect, useMemo, useState } from 'react';
1+
import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react';
22

3-
import { LogLevel } from '@sendbird/chat';
4-
import { MessageCollection, MessageFilter } from '@sendbird/chat/groupChannel';
3+
import { type GroupChannel, MessageCollection, MessageFilter } from '@sendbird/chat/groupChannel';
54
import { ReplyType } from '@sendbird/chat/message';
65
import { Box, useToast } from '@sendbird/uikit-react-native-foundation';
76
import { useGroupChannelMessages } from '@sendbird/uikit-tools';
@@ -30,6 +29,7 @@ import ScrollToBottomButton from '../components/ScrollToBottomButton';
3029
import StatusComposition from '../components/StatusComposition';
3130
import UnreadMessagesFloating from '../components/UnreadMessagesFloating';
3231
import createGroupChannelModule from '../domain/groupChannel/module/createGroupChannelModule';
32+
import { GroupChannelContexts } from '../domain/groupChannel/module/moduleContext';
3333
import type {
3434
GroupChannelFragment,
3535
GroupChannelModule,
@@ -80,7 +80,8 @@ const createGroupChannelFragment = (initModule?: Partial<GroupChannelModule>): G
8080
const [scrolledAwayFromBottom, setScrolledAwayFromBottom] = useState(false);
8181
const scrolledAwayFromBottomRef = useRefTracker(scrolledAwayFromBottom);
8282

83-
sdk.logLevel = LogLevel.VERBOSE;
83+
const { hasSeenNewLine, hasUserMarkedAsUnread } = useContext(GroupChannelContexts.MessageList);
84+
8485
const replyType = useIIFE(() => {
8586
if (sbOptions.uikit.groupChannel.channel.replyType === 'none') {
8687
return ReplyType.NONE;
@@ -89,6 +90,16 @@ const createGroupChannelFragment = (initModule?: Partial<GroupChannelModule>): G
8990
}
9091
});
9192

93+
const markAsRead = useFreshCallback((channels: GroupChannel[]) => {
94+
if (sbOptions.uikitWithAppInfo.groupChannel.channel.enableMarkAsUnread) {
95+
if (hasSeenNewLine && !scrolledAwayFromBottom && !hasUserMarkedAsUnread) {
96+
confirmAndMarkAsRead(channels);
97+
}
98+
} else {
99+
confirmAndMarkAsRead(channels);
100+
}
101+
});
102+
92103
const {
93104
loading,
94105
messages,
@@ -116,7 +127,7 @@ const createGroupChannelFragment = (initModule?: Partial<GroupChannelModule>): G
116127
onCurrentUserBanned: onChannelDeleted,
117128
collectionCreator: getCollectionCreator(channel, messageListQueryParams, collectionCreator),
118129
sortComparator,
119-
markAsRead: confirmAndMarkAsRead,
130+
markAsRead: markAsRead,
120131
replyType,
121132
startingPoint: internalSearchItem?.startingPoint,
122133
});
@@ -231,7 +242,16 @@ const createGroupChannelFragment = (initModule?: Partial<GroupChannelModule>): G
231242
},
232243
);
233244
const onScrolledAwayFromBottom = useFreshCallback((value: boolean) => {
234-
if (!value) resetNewMessages();
245+
if (!value) {
246+
if (sbOptions.uikitWithAppInfo.groupChannel.channel.enableMarkAsUnread) {
247+
if (!hasUserMarkedAsUnread && hasSeenNewLine) {
248+
resetNewMessages();
249+
confirmAndMarkAsRead([channel]);
250+
}
251+
} else {
252+
resetNewMessages();
253+
}
254+
}
235255
setScrolledAwayFromBottom(value);
236256
});
237257

packages/uikit-react-native/src/localization/StringSet.type.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ export interface StringSet {
118118
LIST_DATE_SEPARATOR: (date: Date, locale?: Locale) => string;
119119
LIST_BUTTON_NEW_MSG: (newMessages: SendbirdMessage[]) => string;
120120
LIST_FLOATING_UNREAD_MSG: (unreadMessageCount: number) => string;
121-
LIST_NEW_MESSAGE_SEPARATOR: string;
121+
LIST_NEW_LINE: string;
122122

123123
/** GroupChannel > Message bubble */
124124
MESSAGE_BUBBLE_TIME: (message: SendbirdMessage, locale?: Locale) => string;

0 commit comments

Comments
 (0)