Skip to content

Commit 95c4848

Browse files
authored
Merge pull request #182 from sendbird/feat/message-threading-qa
chore(CLNP-2919): fixed issues from message threading QA
2 parents 7e80e69 + a7d75a7 commit 95c4848

File tree

27 files changed

+326
-89
lines changed

27 files changed

+326
-89
lines changed
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import { ApplicationAttributes, PremiumFeatures, SendbirdChatSDK } from '@sendbird/uikit-utils';
22

33
export const useAppFeatures = (sdk: SendbirdChatSDK) => {
4-
const { premiumFeatureList = [], applicationAttributes = [] } = sdk.appInfo ?? {};
4+
const { premiumFeatureList = [], applicationAttributes = [], uploadSizeLimit } = sdk.appInfo ?? {};
55
return {
66
deliveryReceiptEnabled: premiumFeatureList.includes(PremiumFeatures.delivery_receipt),
77
broadcastChannelEnabled: applicationAttributes.includes(ApplicationAttributes.allow_broadcast_channel),
88
superGroupChannelEnabled: applicationAttributes.includes(ApplicationAttributes.allow_super_group_channel),
99
reactionEnabled: applicationAttributes.includes(ApplicationAttributes.reactions),
10+
uploadSizeLimit: uploadSizeLimit,
1011
};
1112
};

packages/uikit-react-native-foundation/src/ui/GroupChannelMessage/Message.file.voice.tsx

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ type Props = GroupChannelMessageProps<
2525
{
2626
durationMetaArrayKey?: string;
2727
onUnmount: () => void;
28+
initialCurrentTime?: number;
29+
onSubscribeStatus?: (channelUrl: string, messageId: number, subscriber: (currentTime: number) => void) => void;
30+
onUnsubscribeStatus?: (channelUrl: string, messageId: number, subscriber: (currentTime: number) => void) => void;
2831
}
2932
>;
3033
const VoiceFileMessage = (props: Props) => {
@@ -35,6 +38,9 @@ const VoiceFileMessage = (props: Props) => {
3538
message,
3639
durationMetaArrayKey = 'KEY_VOICE_MESSAGE_DURATION',
3740
onUnmount,
41+
initialCurrentTime,
42+
onSubscribeStatus,
43+
onUnsubscribeStatus,
3844
} = props;
3945

4046
const { colors } = useUIKitTheme();
@@ -45,7 +51,7 @@ const VoiceFileMessage = (props: Props) => {
4551
const initialDuration = value ? parseInt(value, 10) : 0;
4652
return {
4753
status: 'paused',
48-
currentTime: 0,
54+
currentTime: initialCurrentTime || 0,
4955
duration: initialDuration,
5056
};
5157
});
@@ -56,6 +62,16 @@ const VoiceFileMessage = (props: Props) => {
5662
};
5763
}, []);
5864

65+
useEffect(() => {
66+
const updateCurrentTime = (currentTime: number) => {
67+
setState((prev) => ({ ...prev, currentTime }));
68+
};
69+
onSubscribeStatus?.(props.channel.url, props.message.messageId, updateCurrentTime);
70+
return () => {
71+
onUnsubscribeStatus?.(props.channel.url, props.message.messageId, updateCurrentTime);
72+
};
73+
}, []);
74+
5975
const uiColors = colors.ui.groupChannelMessage[variant];
6076
const remainingTime = state.duration - state.currentTime;
6177

packages/uikit-react-native-foundation/src/ui/GroupChannelMessage/MessageContainer.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ MessageContainer.Incoming = function MessageContainerIncoming({
4646
</Box>
4747
<Box flexShrink={1}>
4848
{parentMessage}
49-
{!groupedWithPrev && !message.parentMessage && (
49+
{!groupedWithPrev && !parentMessage && (
5050
<Box marginLeft={12} marginBottom={4}>
5151
{(message.isFileMessage() || message.isUserMessage()) && (
5252
<Text caption1 color={color.enabled.textSenderName} numberOfLines={1}>

packages/uikit-react-native/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262
"@openspacelabs/react-native-zoomable-view": "^2.1.5",
6363
"@sendbird/uikit-chat-hooks": "3.5.4",
6464
"@sendbird/uikit-react-native-foundation": "3.5.4",
65-
"@sendbird/uikit-tools": "0.0.1-alpha.76",
65+
"@sendbird/uikit-tools": "0.0.1-alpha.77",
6666
"@sendbird/uikit-utils": "3.5.4"
6767
},
6868
"devDependencies": {

packages/uikit-react-native/src/components/ChannelInput/SendInput.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,13 @@ const SendInput = forwardRef<RNTextInput, SendInputProps>(function SendInput(
164164
if (inputFrozen) return STRINGS.LABELS.CHANNEL_INPUT_PLACEHOLDER_DISABLED;
165165
if (inputDisabled) return STRINGS.LABELS.CHANNEL_INPUT_PLACEHOLDER_DISABLED;
166166
if (messageToReply) return STRINGS.LABELS.CHANNEL_INPUT_PLACEHOLDER_REPLY;
167-
if (messageForThread) return STRINGS.LABELS.CHANNEL_INPUT_PLACEHOLDER_THREAD;
167+
if (messageForThread) {
168+
if (messageForThread.threadInfo && messageForThread.threadInfo.replyCount > 0) {
169+
return STRINGS.LABELS.CHANNEL_INPUT_PLACEHOLDER_REPLY_TO_THREAD;
170+
} else {
171+
return STRINGS.LABELS.CHANNEL_INPUT_PLACEHOLDER_REPLY_IN_THREAD;
172+
}
173+
}
168174

169175
return STRINGS.LABELS.CHANNEL_INPUT_PLACEHOLDER_ACTIVE;
170176
};

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ import {
3232
import type { UserProfileContextType } from '../../contexts/UserProfileCtx';
3333
import { useLocalization, usePlatformService, useSendbirdChat, useUserProfile } from '../../hooks/useContext';
3434
import SBUUtils from '../../libs/SBUUtils';
35-
import ChatFlatList from '../ChatFlatList';
3635
import { ReactionAddons } from '../ReactionAddons';
36+
import ThreadChatFlatList from '../ThreadChatFlatList';
3737

3838
type PressActions = { onPress?: () => void; onLongPress?: () => void; bottomSheetItem?: BottomSheetItem };
3939
type HandleableMessage = SendbirdUserMessage | SendbirdFileMessage;
@@ -147,7 +147,7 @@ const ChannelThreadMessageList = <T extends SendbirdGroupChannel | SendbirdOpenC
147147
{channel.isFrozen && (
148148
<ChannelFrozenBanner style={styles.frozenBanner} text={STRINGS.LABELS.CHANNEL_MESSAGE_LIST_FROZEN} />
149149
)}
150-
<ChatFlatList
150+
<ThreadChatFlatList
151151
{...flatListProps}
152152
onTopReached={onTopReached}
153153
onBottomReached={onBottomReached}

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

Lines changed: 7 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -32,30 +32,19 @@ const ChatFlatList = forwardRef<RNFlatList, Props>(function ChatFlatList(
3232
) {
3333
const { select } = useUIKitTheme();
3434
const contentOffsetY = useRef(0);
35-
// FIXME: inverted list of ListEmptyComponent is reversed {@link https://github.com/facebook/react-native/issues/21196#issuecomment-836937743}
36-
const inverted = useRef(props.inverted ?? Boolean(props.data?.length));
3735

3836
const _onScroll = useFreshCallback<NonNullable<Props['onScroll']>>((event) => {
3937
onScroll?.(event);
4038

41-
const { contentOffset, contentSize, layoutMeasurement } = event.nativeEvent;
39+
const { contentOffset } = event.nativeEvent;
4240

4341
const prevOffsetY = contentOffsetY.current;
4442
const currOffsetY = contentOffset.y;
4543

46-
if (inverted.current) {
47-
if (BOTTOM_DETECT_THRESHOLD < prevOffsetY && currOffsetY <= BOTTOM_DETECT_THRESHOLD) {
48-
onScrolledAwayFromBottom(false);
49-
} else if (BOTTOM_DETECT_THRESHOLD < currOffsetY && prevOffsetY <= BOTTOM_DETECT_THRESHOLD) {
50-
onScrolledAwayFromBottom(true);
51-
}
52-
} else {
53-
const bottomDetectThreshold = contentSize.height - layoutMeasurement.height - BOTTOM_DETECT_THRESHOLD;
54-
if (bottomDetectThreshold < prevOffsetY && currOffsetY <= bottomDetectThreshold) {
55-
onScrolledAwayFromBottom(true);
56-
} else if (bottomDetectThreshold < currOffsetY && prevOffsetY <= bottomDetectThreshold) {
57-
onScrolledAwayFromBottom(false);
58-
}
44+
if (BOTTOM_DETECT_THRESHOLD < prevOffsetY && currOffsetY <= BOTTOM_DETECT_THRESHOLD) {
45+
onScrolledAwayFromBottom(false);
46+
} else if (BOTTOM_DETECT_THRESHOLD < currOffsetY && prevOffsetY <= BOTTOM_DETECT_THRESHOLD) {
47+
onScrolledAwayFromBottom(true);
5948
}
6049

6150
contentOffsetY.current = contentOffset.y;
@@ -79,7 +68,8 @@ const ChatFlatList = forwardRef<RNFlatList, Props>(function ChatFlatList(
7968
keyboardShouldPersistTaps={'handled'}
8069
indicatorStyle={select({ light: 'black', dark: 'white' })}
8170
{...props}
82-
inverted={inverted.current}
71+
// FIXME: inverted list of ListEmptyComponent is reversed {@link https://github.com/facebook/react-native/issues/21196#issuecomment-836937743}
72+
inverted={Boolean(props.data?.length)}
8373
ref={ref}
8474
onEndReached={onTopReached}
8575
onScrollToIndexFailed={NOOP}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ const GroupChannelMessageReplyInfo = ({ channel, message, onPress }: Props) => {
5353

5454
if (!channel || !message.threadInfo || !message.threadInfo.replyCount) return null;
5555

56-
const replyCountText = STRINGS.GROUP_CHANNEL_THREAD.REPLAY_COUNT(message.threadInfo.replyCount || 0);
56+
const replyCountText = STRINGS.GROUP_CHANNEL_THREAD.REPLY_COUNT(message.threadInfo.replyCount || 0, 99);
5757
const onPressReply = () => {
5858
onPress?.(message as SendbirdUserMessage | SendbirdFileMessage);
5959
};

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ const GroupChannelMessageRenderer: GroupChannelProps['Fragment']['renderMessage'
5151
}) => {
5252
const playerUnsubscribes = useRef<(() => void)[]>([]);
5353
const { palette } = useUIKitTheme();
54-
const { sbOptions, currentUser, mentionManager } = useSendbirdChat();
54+
const { sbOptions, currentUser, mentionManager, voiceMessageStatusManager } = useSendbirdChat();
5555
const { STRINGS } = useLocalization();
5656
const { mediaService, playerService } = usePlatformService();
5757
const { groupWithPrev, groupWithNext } = calcMessageGrouping(
@@ -62,6 +62,7 @@ const GroupChannelMessageRenderer: GroupChannelProps['Fragment']['renderMessage'
6262
sbOptions.uikit.groupChannel.channel.replyType === 'thread',
6363
shouldRenderParentMessage(message, hideParentMessage),
6464
);
65+
6566
const variant = isMyMessage(message, currentUser?.userId) ? 'outgoing' : 'incoming';
6667

6768
const reactionChildren = useIIFE(() => {
@@ -121,6 +122,7 @@ const GroupChannelMessageRenderer: GroupChannelProps['Fragment']['renderMessage'
121122
let seekFinished = !shouldSeekToTime;
122123

123124
const forPlayback = playerService.addPlaybackListener(({ stopped, currentTime, duration }) => {
125+
voiceMessageStatusManager.setCurrentTime(message.channelUrl, message.messageId, currentTime);
124126
if (seekFinished) {
125127
setState((prevState) => ({ ...prevState, currentTime: stopped ? 0 : currentTime, duration }));
126128
}
@@ -272,6 +274,9 @@ const GroupChannelMessageRenderer: GroupChannelProps['Fragment']['renderMessage'
272274
<GroupChannelMessage.VoiceFile
273275
message={message as SendbirdFileMessage}
274276
durationMetaArrayKey={VOICE_MESSAGE_META_ARRAY_DURATION_KEY}
277+
initialCurrentTime={voiceMessageStatusManager.getCurrentTime(message.channelUrl, message.messageId)}
278+
onSubscribeStatus={voiceMessageStatusManager.subscribe}
279+
onUnsubscribeStatus={voiceMessageStatusManager.unsubscribe}
275280
onUnmount={() => {
276281
if (isVoiceMessage(message) && playerService.uri === message.url) {
277282
resetPlayer();
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import React, { forwardRef, useRef } from 'react';
2+
import { FlatListProps, FlatList as RNFlatList, StyleSheet } from 'react-native';
3+
4+
import { useUIKitTheme } from '@sendbird/uikit-react-native-foundation';
5+
import { NOOP, SendbirdMessage, getMessageUniqId, useFreshCallback } from '@sendbird/uikit-utils';
6+
7+
import FlatListInternal from '../ChatFlatList/FlatListInternal';
8+
9+
const BOTTOM_DETECT_THRESHOLD = 50;
10+
const UNREACHABLE_THRESHOLD = Number.MIN_SAFE_INTEGER;
11+
12+
type Props = Omit<FlatListProps<SendbirdMessage>, 'onEndReached'> & {
13+
onBottomReached: () => void;
14+
onTopReached: () => void;
15+
onScrolledAwayFromBottom: (value: boolean) => void;
16+
};
17+
const ThreadChatFlatList = forwardRef<RNFlatList, Props>(function ThreadChatFlatList(
18+
{ onTopReached, onBottomReached, onScrolledAwayFromBottom, onScroll, ...props },
19+
ref,
20+
) {
21+
const { select } = useUIKitTheme();
22+
const contentOffsetY = useRef(0);
23+
24+
const _onScroll = useFreshCallback<NonNullable<Props['onScroll']>>((event) => {
25+
onScroll?.(event);
26+
27+
const { contentOffset, contentSize, layoutMeasurement } = event.nativeEvent;
28+
29+
const prevOffsetY = contentOffsetY.current;
30+
const currOffsetY = contentOffset.y;
31+
32+
const bottomDetectThreshold = contentSize.height - layoutMeasurement.height - BOTTOM_DETECT_THRESHOLD;
33+
if (bottomDetectThreshold < prevOffsetY && currOffsetY <= bottomDetectThreshold) {
34+
onScrolledAwayFromBottom(true);
35+
} else if (bottomDetectThreshold < currOffsetY && prevOffsetY <= bottomDetectThreshold) {
36+
onScrolledAwayFromBottom(false);
37+
}
38+
39+
contentOffsetY.current = contentOffset.y;
40+
});
41+
42+
return (
43+
<FlatListInternal
44+
bounces={false}
45+
removeClippedSubviews
46+
keyboardDismissMode={'on-drag'}
47+
keyboardShouldPersistTaps={'handled'}
48+
indicatorStyle={select({ light: 'black', dark: 'white' })}
49+
{...props}
50+
ref={ref}
51+
onEndReached={onBottomReached}
52+
onScrollToIndexFailed={NOOP}
53+
onStartReached={onTopReached}
54+
scrollEventThrottle={16}
55+
onScroll={_onScroll}
56+
keyExtractor={getMessageUniqId}
57+
style={{ flex: 1, ...StyleSheet.flatten(props.style) }}
58+
maintainVisibleContentPosition={{ minIndexForVisible: 0, autoscrollToTopThreshold: UNREACHABLE_THRESHOLD }}
59+
/>
60+
);
61+
});
62+
63+
export default ThreadChatFlatList;

0 commit comments

Comments
 (0)