Skip to content
Merged
Show file tree
Hide file tree
Changes from 45 commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
1727b18
Implement GroupChannel thread.
OnestarLee May 9, 2024
0620e93
Clean up code
OnestarLee May 30, 2024
48c6727
Fixed lint
OnestarLee May 30, 2024
636a0b9
Update uikit-tools version
OnestarLee May 30, 2024
4f06819
Apply review
OnestarLee May 31, 2024
252b977
Fixed lint
OnestarLee May 31, 2024
a4861ac
Apply review
OnestarLee May 31, 2024
169b40e
Apply review
OnestarLee May 31, 2024
f2de01a
Apply review
OnestarLee May 31, 2024
528c209
Apply review
OnestarLee May 31, 2024
1af97bf
Changed threadReplySelectType to thread
OnestarLee May 31, 2024
c793d25
Apply review
OnestarLee May 31, 2024
a915920
chore: update parent message reactions UI
bang9 May 31, 2024
6a9b220
chore: add applyReactionEvent to reaction addon
bang9 May 31, 2024
0e40c3d
chore: resolve message gap issue in thread fragment
bang9 May 31, 2024
08d4a54
Apply review
OnestarLee May 31, 2024
514efc1
chore: disable message grouping if reply or thread message
bang9 May 31, 2024
9ae9a63
chore: update message grouping logic
bang9 May 31, 2024
431454b
chore: add color to more button.
OnestarLee May 31, 2024
d6732fd
chore: fixed lint.
OnestarLee May 31, 2024
e494858
chore: update parent message properly
bang9 May 31, 2024
c519bbb
chore: improve thread message list.
OnestarLee Jun 1, 2024
db2c418
Merge remote-tracking branch 'origin/feat/message-threading' into fea…
OnestarLee Jun 1, 2024
03ae8a1
chore: improve thread message list.
OnestarLee Jun 1, 2024
fba519d
chore: change version for sample test.
OnestarLee Jun 1, 2024
f4753cd
chore: fixed not going to channel when clicking title.
OnestarLee Jun 4, 2024
8bd34b7
chore: fixed to show 'Reply to thread' in the placeholder when there…
OnestarLee Jun 4, 2024
7f2b70a
chore: removed NewMessageButton, ScrollToBottomButton in thread.
OnestarLee Jun 4, 2024
b61954b
chore: fixed usernames are not displaying in thread.
OnestarLee Jun 4, 2024
1e8b68e
chore: fixed an issue where it is possible to enter an invalid thread.
OnestarLee Jun 4, 2024
54e37b6
chore: fixed an issue where incorrect sentences are displayed when re…
OnestarLee Jun 4, 2024
805c80f
chore: removed NewMessageButton, ScrollToBottomButton in thread.
OnestarLee Jun 4, 2024
9d55ef1
Merge remote-tracking branch 'refs/remotes/origin/feat/message-thread…
OnestarLee Jun 14, 2024
742180b
chore: fixed inverted no message.
OnestarLee Jun 14, 2024
c2bd6c9
chore: update reply count format to display 99+ replies for counts of…
OnestarLee Jun 14, 2024
5147852
chore: fixed issue with mention suggestion list sorting
OnestarLee Jun 17, 2024
f7a4334
chore: fixed issue with file upload size limit exceeded
OnestarLee Jun 18, 2024
9fe3a0f
chore: fixed issue with file upload size limit exceeded
OnestarLee Jun 18, 2024
5390017
chore: add voice message status manager
OnestarLee Jun 21, 2024
226fdb6
chore: add voice message status manager
OnestarLee Jun 23, 2024
ceae6b8
chore: change scroll timeout
OnestarLee Jun 23, 2024
4e797aa
chore: organize code
OnestarLee Jun 23, 2024
7b9072c
chore: fixed scroll issue
OnestarLee Jun 23, 2024
1b59c86
chore: apply review.
OnestarLee Jun 28, 2024
f906482
chore: update note
bang9 Jun 28, 2024
6a07692
chore: apply review.
OnestarLee Jun 28, 2024
59c1b2d
Merge remote-tracking branch 'origin/feat/message-threading-qa' into …
OnestarLee Jun 28, 2024
484199d
chore: apply review when clicking the subtitle
OnestarLee Jul 3, 2024
a7d75a7
chore: apply review when clicking the subtitle
OnestarLee Jul 3, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/uikit-chat-hooks/src/common/useAppFeatures.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { ApplicationAttributes, PremiumFeatures, SendbirdChatSDK } from '@sendbird/uikit-utils';

export const useAppFeatures = (sdk: SendbirdChatSDK) => {
const { premiumFeatureList = [], applicationAttributes = [] } = sdk.appInfo ?? {};
const { premiumFeatureList = [], applicationAttributes = [], uploadSizeLimit } = sdk.appInfo ?? {};
return {
deliveryReceiptEnabled: premiumFeatureList.includes(PremiumFeatures.delivery_receipt),
broadcastChannelEnabled: applicationAttributes.includes(ApplicationAttributes.allow_broadcast_channel),
superGroupChannelEnabled: applicationAttributes.includes(ApplicationAttributes.allow_super_group_channel),
reactionEnabled: applicationAttributes.includes(ApplicationAttributes.reactions),
uploadSizeLimit: uploadSizeLimit,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ type Props = GroupChannelMessageProps<
{
durationMetaArrayKey?: string;
onUnmount: () => void;
initialCurrentTime?: number;
onSubscribeStatus?: (channelUrl: string, messageId: number, subscriber: (currentTime: number) => void) => void;
onUnsubscribeStatus?: (channelUrl: string, messageId: number, subscriber: (currentTime: number) => void) => void;
}
>;
const VoiceFileMessage = (props: Props) => {
Expand All @@ -35,6 +38,9 @@ const VoiceFileMessage = (props: Props) => {
message,
durationMetaArrayKey = 'KEY_VOICE_MESSAGE_DURATION',
onUnmount,
initialCurrentTime,
onSubscribeStatus,
onUnsubscribeStatus,
} = props;

const { colors } = useUIKitTheme();
Expand All @@ -45,7 +51,7 @@ const VoiceFileMessage = (props: Props) => {
const initialDuration = value ? parseInt(value, 10) : 0;
return {
status: 'paused',
currentTime: 0,
currentTime: initialCurrentTime || 0,
duration: initialDuration,
};
});
Expand All @@ -56,6 +62,16 @@ const VoiceFileMessage = (props: Props) => {
};
}, []);

useEffect(() => {
const updateCurrentTime = (currentTime: number) => {
setState((prev) => ({ ...prev, currentTime }));
};
onSubscribeStatus?.(props.channel.url, props.message.messageId, updateCurrentTime);
return () => {
onUnsubscribeStatus?.(props.channel.url, props.message.messageId, updateCurrentTime);
};
}, []);

const uiColors = colors.ui.groupChannelMessage[variant];
const remainingTime = state.duration - state.currentTime;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ MessageContainer.Incoming = function MessageContainerIncoming({
</Box>
<Box flexShrink={1}>
{parentMessage}
{!groupedWithPrev && !message.parentMessage && (
{!groupedWithPrev && !parentMessage && (
<Box marginLeft={12} marginBottom={4}>
{(message.isFileMessage() || message.isUserMessage()) && (
<Text caption1 color={color.enabled.textSenderName} numberOfLines={1}>
Expand Down
2 changes: 1 addition & 1 deletion packages/uikit-react-native/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@
"@openspacelabs/react-native-zoomable-view": "^2.1.5",
"@sendbird/uikit-chat-hooks": "3.5.4",
"@sendbird/uikit-react-native-foundation": "3.5.4",
"@sendbird/uikit-tools": "0.0.1-alpha.76",
"@sendbird/uikit-tools": "0.0.1-alpha.77",
"@sendbird/uikit-utils": "3.5.4"
},
"devDependencies": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,13 @@ const SendInput = forwardRef<RNTextInput, SendInputProps>(function SendInput(
if (inputFrozen) return STRINGS.LABELS.CHANNEL_INPUT_PLACEHOLDER_DISABLED;
if (inputDisabled) return STRINGS.LABELS.CHANNEL_INPUT_PLACEHOLDER_DISABLED;
if (messageToReply) return STRINGS.LABELS.CHANNEL_INPUT_PLACEHOLDER_REPLY;
if (messageForThread) return STRINGS.LABELS.CHANNEL_INPUT_PLACEHOLDER_THREAD;
if (messageForThread) {
if (messageForThread.threadInfo && messageForThread.threadInfo.replyCount > 0) {
return STRINGS.LABELS.CHANNEL_INPUT_PLACEHOLDER_REPLY_TO_THREAD;
} else {
return STRINGS.LABELS.CHANNEL_INPUT_PLACEHOLDER_REPLY_IN_THREAD;
}
}

return STRINGS.LABELS.CHANNEL_INPUT_PLACEHOLDER_ACTIVE;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ import {
import type { UserProfileContextType } from '../../contexts/UserProfileCtx';
import { useLocalization, usePlatformService, useSendbirdChat, useUserProfile } from '../../hooks/useContext';
import SBUUtils from '../../libs/SBUUtils';
import ChatFlatList from '../ChatFlatList';
import { ReactionAddons } from '../ReactionAddons';
import ThreadChatFlatList from '../ThreadChatFlatList';

type PressActions = { onPress?: () => void; onLongPress?: () => void; bottomSheetItem?: BottomSheetItem };
type HandleableMessage = SendbirdUserMessage | SendbirdFileMessage;
Expand Down Expand Up @@ -147,7 +147,7 @@ const ChannelThreadMessageList = <T extends SendbirdGroupChannel | SendbirdOpenC
{channel.isFrozen && (
<ChannelFrozenBanner style={styles.frozenBanner} text={STRINGS.LABELS.CHANNEL_MESSAGE_LIST_FROZEN} />
)}
<ChatFlatList
<ThreadChatFlatList
{...flatListProps}
onTopReached={onTopReached}
onBottomReached={onBottomReached}
Expand Down
24 changes: 7 additions & 17 deletions packages/uikit-react-native/src/components/ChatFlatList/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,30 +32,19 @@ const ChatFlatList = forwardRef<RNFlatList, Props>(function ChatFlatList(
) {
const { select } = useUIKitTheme();
const contentOffsetY = useRef(0);
// FIXME: inverted list of ListEmptyComponent is reversed {@link https://github.com/facebook/react-native/issues/21196#issuecomment-836937743}
const inverted = useRef(props.inverted ?? Boolean(props.data?.length));

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

const { contentOffset, contentSize, layoutMeasurement } = event.nativeEvent;
const { contentOffset } = event.nativeEvent;

const prevOffsetY = contentOffsetY.current;
const currOffsetY = contentOffset.y;

if (inverted.current) {
if (BOTTOM_DETECT_THRESHOLD < prevOffsetY && currOffsetY <= BOTTOM_DETECT_THRESHOLD) {
onScrolledAwayFromBottom(false);
} else if (BOTTOM_DETECT_THRESHOLD < currOffsetY && prevOffsetY <= BOTTOM_DETECT_THRESHOLD) {
onScrolledAwayFromBottom(true);
}
} else {
const bottomDetectThreshold = contentSize.height - layoutMeasurement.height - BOTTOM_DETECT_THRESHOLD;
if (bottomDetectThreshold < prevOffsetY && currOffsetY <= bottomDetectThreshold) {
onScrolledAwayFromBottom(true);
} else if (bottomDetectThreshold < currOffsetY && prevOffsetY <= bottomDetectThreshold) {
onScrolledAwayFromBottom(false);
}
if (BOTTOM_DETECT_THRESHOLD < prevOffsetY && currOffsetY <= BOTTOM_DETECT_THRESHOLD) {
onScrolledAwayFromBottom(false);
} else if (BOTTOM_DETECT_THRESHOLD < currOffsetY && prevOffsetY <= BOTTOM_DETECT_THRESHOLD) {
onScrolledAwayFromBottom(true);
}

contentOffsetY.current = contentOffset.y;
Expand All @@ -79,7 +68,8 @@ const ChatFlatList = forwardRef<RNFlatList, Props>(function ChatFlatList(
keyboardShouldPersistTaps={'handled'}
indicatorStyle={select({ light: 'black', dark: 'white' })}
{...props}
inverted={inverted.current}
// FIXME: inverted list of ListEmptyComponent is reversed {@link https://github.com/facebook/react-native/issues/21196#issuecomment-836937743}
inverted={Boolean(props.data?.length)}
ref={ref}
onEndReached={onTopReached}
onScrollToIndexFailed={NOOP}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ const GroupChannelMessageReplyInfo = ({ channel, message, onPress }: Props) => {

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

const replyCountText = STRINGS.GROUP_CHANNEL_THREAD.REPLAY_COUNT(message.threadInfo.replyCount || 0);
const replyCountText = STRINGS.GROUP_CHANNEL_THREAD.REPLY_COUNT(message.threadInfo.replyCount || 0, 99);
const onPressReply = () => {
onPress?.(message as SendbirdUserMessage | SendbirdFileMessage);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ const GroupChannelMessageRenderer: GroupChannelProps['Fragment']['renderMessage'
}) => {
const playerUnsubscribes = useRef<(() => void)[]>([]);
const { palette } = useUIKitTheme();
const { sbOptions, currentUser, mentionManager } = useSendbirdChat();
const { sbOptions, currentUser, mentionManager, voiceMessageStatusManager } = useSendbirdChat();
const { STRINGS } = useLocalization();
const { mediaService, playerService } = usePlatformService();
const { groupWithPrev, groupWithNext } = calcMessageGrouping(
Expand All @@ -62,6 +62,7 @@ const GroupChannelMessageRenderer: GroupChannelProps['Fragment']['renderMessage'
sbOptions.uikit.groupChannel.channel.replyType === 'thread',
shouldRenderParentMessage(message, hideParentMessage),
);

const variant = isMyMessage(message, currentUser?.userId) ? 'outgoing' : 'incoming';

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

const forPlayback = playerService.addPlaybackListener(({ stopped, currentTime, duration }) => {
voiceMessageStatusManager.setCurrentTime(message.channelUrl, message.messageId, currentTime);
if (seekFinished) {
setState((prevState) => ({ ...prevState, currentTime: stopped ? 0 : currentTime, duration }));
}
Expand Down Expand Up @@ -272,6 +274,9 @@ const GroupChannelMessageRenderer: GroupChannelProps['Fragment']['renderMessage'
<GroupChannelMessage.VoiceFile
message={message as SendbirdFileMessage}
durationMetaArrayKey={VOICE_MESSAGE_META_ARRAY_DURATION_KEY}
initialCurrentTime={voiceMessageStatusManager.getCurrentTime(message.channelUrl, message.messageId)}
onSubscribeStatus={voiceMessageStatusManager.subscribe}
onUnsubscribeStatus={voiceMessageStatusManager.unsubscribe}
onUnmount={() => {
if (isVoiceMessage(message) && playerService.uri === message.url) {
resetPlayer();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import React, { forwardRef, useRef } from 'react';
import { FlatListProps, FlatList as RNFlatList, StyleSheet } from 'react-native';

import { useUIKitTheme } from '@sendbird/uikit-react-native-foundation';
import { NOOP, SendbirdMessage, getMessageUniqId, useFreshCallback } from '@sendbird/uikit-utils';

import FlatListInternal from '../ChatFlatList/FlatListInternal';

const BOTTOM_DETECT_THRESHOLD = 50;
const UNREACHABLE_THRESHOLD = Number.MIN_SAFE_INTEGER;

type Props = Omit<FlatListProps<SendbirdMessage>, 'onEndReached'> & {
onBottomReached: () => void;
onTopReached: () => void;
onScrolledAwayFromBottom: (value: boolean) => void;
};
const ThreadChatFlatList = forwardRef<RNFlatList, Props>(function ThreadChatFlatList(
{ onTopReached, onBottomReached, onScrolledAwayFromBottom, onScroll, ...props },
ref,
) {
const { select } = useUIKitTheme();
const contentOffsetY = useRef(0);

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

const { contentOffset, contentSize, layoutMeasurement } = event.nativeEvent;

const prevOffsetY = contentOffsetY.current;
const currOffsetY = contentOffset.y;

const bottomDetectThreshold = contentSize.height - layoutMeasurement.height - BOTTOM_DETECT_THRESHOLD;
if (bottomDetectThreshold < prevOffsetY && currOffsetY <= bottomDetectThreshold) {
onScrolledAwayFromBottom(true);
} else if (bottomDetectThreshold < currOffsetY && prevOffsetY <= bottomDetectThreshold) {
onScrolledAwayFromBottom(false);
}

contentOffsetY.current = contentOffset.y;
});

return (
<FlatListInternal
bounces={false}
removeClippedSubviews
keyboardDismissMode={'on-drag'}
keyboardShouldPersistTaps={'handled'}
indicatorStyle={select({ light: 'black', dark: 'white' })}
{...props}
ref={ref}
onEndReached={onBottomReached}
onScrollToIndexFailed={NOOP}
onStartReached={onTopReached}
scrollEventThrottle={16}
onScroll={_onScroll}
keyExtractor={getMessageUniqId}
style={{ flex: 1, ...StyleSheet.flatten(props.style) }}
maintainVisibleContentPosition={{ minIndexForVisible: 0, autoscrollToTopThreshold: UNREACHABLE_THRESHOLD }}
/>
);
});

export default ThreadChatFlatList;
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import InternalLocalCacheStorage from '../libs/InternalLocalCacheStorage';
import MentionConfig, { MentionConfigInterface } from '../libs/MentionConfig';
import MentionManager from '../libs/MentionManager';
import VoiceMessageConfig, { VoiceMessageConfigInterface } from '../libs/VoiceMessageConfig';
import VoiceMessageStatusManager from '../libs/VoiceMessageStatusManager';
import StringSetEn from '../localization/StringSet.en';
import type { StringSet } from '../localization/StringSet.type';
import SBUDynamicModule from '../platform/dynamicModule';
Expand Down Expand Up @@ -179,6 +180,7 @@ const SendbirdUIKitContainer = (props: SendbirdUIKitContainerProps) => {
const { imageCompressionConfig, voiceMessageConfig, mentionConfig } = useConfigInstance(props);
const emojiManager = useMemo(() => new EmojiManager(internalStorage), [internalStorage]);
const mentionManager = useMemo(() => new MentionManager(mentionConfig), [mentionConfig]);
const voiceMessageStatusManager = useMemo(() => new VoiceMessageStatusManager(), []);

useLayoutEffect(() => {
if (!isFirstMount) {
Expand Down Expand Up @@ -226,6 +228,7 @@ const SendbirdUIKitContainer = (props: SendbirdUIKitContainerProps) => {
mentionManager={mentionManager}
imageCompressionConfig={imageCompressionConfig}
voiceMessageConfig={voiceMessageConfig}
voiceMessageStatusManager={voiceMessageStatusManager}
enableAutoPushTokenRegistration={
chatOptions.enableAutoPushTokenRegistration ?? SendbirdUIKit.DEFAULT.AUTO_PUSH_TOKEN_REGISTRATION
}
Expand Down
6 changes: 6 additions & 0 deletions packages/uikit-react-native/src/contexts/SendbirdChatCtx.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import type EmojiManager from '../libs/EmojiManager';
import type ImageCompressionConfig from '../libs/ImageCompressionConfig';
import type MentionManager from '../libs/MentionManager';
import type VoiceMessageConfig from '../libs/VoiceMessageConfig';
import VoiceMessageStatusManager from '../libs/VoiceMessageStatusManager';
import type { FileType } from '../platform/types';

export interface ChatRelatedFeaturesInUIKit {
Expand All @@ -27,6 +28,7 @@ interface Props extends ChatRelatedFeaturesInUIKit, React.PropsWithChildren {

emojiManager: EmojiManager;
mentionManager: MentionManager;
voiceMessageStatusManager: VoiceMessageStatusManager;
imageCompressionConfig: ImageCompressionConfig;
voiceMessageConfig: VoiceMessageConfig;
}
Expand All @@ -39,6 +41,7 @@ export type SendbirdChatContextType = {
// feature related instances
emojiManager: EmojiManager;
mentionManager: MentionManager;
voiceMessageStatusManager: VoiceMessageStatusManager;
imageCompressionConfig: ImageCompressionConfig;
voiceMessageConfig: VoiceMessageConfig;

Expand Down Expand Up @@ -80,6 +83,7 @@ export type SendbirdChatContextType = {
broadcastChannelEnabled: boolean;
superGroupChannelEnabled: boolean;
reactionEnabled: boolean;
uploadSizeLimit: number | undefined;
};
};
};
Expand All @@ -90,6 +94,7 @@ export const SendbirdChatProvider = ({
sdkInstance,
emojiManager,
mentionManager,
voiceMessageStatusManager,
imageCompressionConfig,
voiceMessageConfig,
enableAutoPushTokenRegistration,
Expand Down Expand Up @@ -164,6 +169,7 @@ export const SendbirdChatProvider = ({
mentionManager,
imageCompressionConfig,
voiceMessageConfig,
voiceMessageStatusManager,
currentUser,
setCurrentUser,

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,11 @@ const GroupChannelMessageList = (props: GroupChannelProps['MessageList']) => {
sbOptions.uikit.groupChannel.channel.replyType === 'thread' &&
sbOptions.uikit.groupChannel.channel.threadReplySelectType === 'thread'
) {
onPressReplyMessageInThread(parentMessage as SendbirdSendableMessage, childMessage.createdAt);
if (parentMessage.createdAt >= props.channel.messageOffsetTimestamp) {
onPressReplyMessageInThread(parentMessage as SendbirdSendableMessage, childMessage.createdAt);
} else {
toast.show(STRINGS.TOAST.FIND_PARENT_MSG_ERROR, 'error');
}
} else {
const canScrollToParent = scrollToMessageWithCreatedAt(parentMessage.createdAt, true, 0);
if (!canScrollToParent) toast.show(STRINGS.TOAST.FIND_PARENT_MSG_ERROR, 'error');
Expand Down
Loading