diff --git a/packages/uikit-chat-hooks/src/common/useAppFeatures.ts b/packages/uikit-chat-hooks/src/common/useAppFeatures.ts index 592b59612..7670de7be 100644 --- a/packages/uikit-chat-hooks/src/common/useAppFeatures.ts +++ b/packages/uikit-chat-hooks/src/common/useAppFeatures.ts @@ -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, }; }; diff --git a/packages/uikit-react-native-foundation/src/ui/GroupChannelMessage/Message.file.voice.tsx b/packages/uikit-react-native-foundation/src/ui/GroupChannelMessage/Message.file.voice.tsx index 2a9924c43..1aa51da71 100644 --- a/packages/uikit-react-native-foundation/src/ui/GroupChannelMessage/Message.file.voice.tsx +++ b/packages/uikit-react-native-foundation/src/ui/GroupChannelMessage/Message.file.voice.tsx @@ -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) => { @@ -35,6 +38,9 @@ const VoiceFileMessage = (props: Props) => { message, durationMetaArrayKey = 'KEY_VOICE_MESSAGE_DURATION', onUnmount, + initialCurrentTime, + onSubscribeStatus, + onUnsubscribeStatus, } = props; const { colors } = useUIKitTheme(); @@ -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, }; }); @@ -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; diff --git a/packages/uikit-react-native-foundation/src/ui/GroupChannelMessage/MessageContainer.tsx b/packages/uikit-react-native-foundation/src/ui/GroupChannelMessage/MessageContainer.tsx index 05e34ee2c..fad2d4296 100644 --- a/packages/uikit-react-native-foundation/src/ui/GroupChannelMessage/MessageContainer.tsx +++ b/packages/uikit-react-native-foundation/src/ui/GroupChannelMessage/MessageContainer.tsx @@ -46,7 +46,7 @@ MessageContainer.Incoming = function MessageContainerIncoming({ {parentMessage} - {!groupedWithPrev && !message.parentMessage && ( + {!groupedWithPrev && !parentMessage && ( {(message.isFileMessage() || message.isUserMessage()) && ( diff --git a/packages/uikit-react-native/package.json b/packages/uikit-react-native/package.json index 8642bf887..2cc4a4946 100644 --- a/packages/uikit-react-native/package.json +++ b/packages/uikit-react-native/package.json @@ -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": { diff --git a/packages/uikit-react-native/src/components/ChannelInput/SendInput.tsx b/packages/uikit-react-native/src/components/ChannelInput/SendInput.tsx index f7d2c7f59..ba070401d 100644 --- a/packages/uikit-react-native/src/components/ChannelInput/SendInput.tsx +++ b/packages/uikit-react-native/src/components/ChannelInput/SendInput.tsx @@ -164,7 +164,13 @@ const SendInput = forwardRef(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; }; diff --git a/packages/uikit-react-native/src/components/ChannelThreadMessageList/index.tsx b/packages/uikit-react-native/src/components/ChannelThreadMessageList/index.tsx index 40afd50b1..bcbcbec66 100644 --- a/packages/uikit-react-native/src/components/ChannelThreadMessageList/index.tsx +++ b/packages/uikit-react-native/src/components/ChannelThreadMessageList/index.tsx @@ -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; @@ -147,7 +147,7 @@ const ChannelThreadMessageList = )} - (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>((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; @@ -79,7 +68,8 @@ const ChatFlatList = forwardRef(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} diff --git a/packages/uikit-react-native/src/components/GroupChannelMessageRenderer/GroupChannelMessageReplyInfo.tsx b/packages/uikit-react-native/src/components/GroupChannelMessageRenderer/GroupChannelMessageReplyInfo.tsx index b2691c84a..1e7632f23 100644 --- a/packages/uikit-react-native/src/components/GroupChannelMessageRenderer/GroupChannelMessageReplyInfo.tsx +++ b/packages/uikit-react-native/src/components/GroupChannelMessageRenderer/GroupChannelMessageReplyInfo.tsx @@ -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); }; diff --git a/packages/uikit-react-native/src/components/GroupChannelMessageRenderer/index.tsx b/packages/uikit-react-native/src/components/GroupChannelMessageRenderer/index.tsx index 412a78f26..ad70dc93a 100644 --- a/packages/uikit-react-native/src/components/GroupChannelMessageRenderer/index.tsx +++ b/packages/uikit-react-native/src/components/GroupChannelMessageRenderer/index.tsx @@ -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( @@ -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(() => { @@ -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 })); } @@ -272,6 +274,9 @@ const GroupChannelMessageRenderer: GroupChannelProps['Fragment']['renderMessage' { if (isVoiceMessage(message) && playerService.uri === message.url) { resetPlayer(); diff --git a/packages/uikit-react-native/src/components/ThreadChatFlatList/index.tsx b/packages/uikit-react-native/src/components/ThreadChatFlatList/index.tsx new file mode 100644 index 000000000..a76ad1de4 --- /dev/null +++ b/packages/uikit-react-native/src/components/ThreadChatFlatList/index.tsx @@ -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, 'onEndReached'> & { + onBottomReached: () => void; + onTopReached: () => void; + onScrolledAwayFromBottom: (value: boolean) => void; +}; +const ThreadChatFlatList = forwardRef(function ThreadChatFlatList( + { onTopReached, onBottomReached, onScrolledAwayFromBottom, onScroll, ...props }, + ref, +) { + const { select } = useUIKitTheme(); + const contentOffsetY = useRef(0); + + const _onScroll = useFreshCallback>((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 ( + + ); +}); + +export default ThreadChatFlatList; diff --git a/packages/uikit-react-native/src/containers/SendbirdUIKitContainer.tsx b/packages/uikit-react-native/src/containers/SendbirdUIKitContainer.tsx index c435ccc2a..98d597be6 100644 --- a/packages/uikit-react-native/src/containers/SendbirdUIKitContainer.tsx +++ b/packages/uikit-react-native/src/containers/SendbirdUIKitContainer.tsx @@ -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'; @@ -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) { @@ -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 } diff --git a/packages/uikit-react-native/src/contexts/SendbirdChatCtx.tsx b/packages/uikit-react-native/src/contexts/SendbirdChatCtx.tsx index 90d223c36..352ae5aff 100644 --- a/packages/uikit-react-native/src/contexts/SendbirdChatCtx.tsx +++ b/packages/uikit-react-native/src/contexts/SendbirdChatCtx.tsx @@ -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 { @@ -27,6 +28,7 @@ interface Props extends ChatRelatedFeaturesInUIKit, React.PropsWithChildren { emojiManager: EmojiManager; mentionManager: MentionManager; + voiceMessageStatusManager: VoiceMessageStatusManager; imageCompressionConfig: ImageCompressionConfig; voiceMessageConfig: VoiceMessageConfig; } @@ -39,6 +41,7 @@ export type SendbirdChatContextType = { // feature related instances emojiManager: EmojiManager; mentionManager: MentionManager; + voiceMessageStatusManager: VoiceMessageStatusManager; imageCompressionConfig: ImageCompressionConfig; voiceMessageConfig: VoiceMessageConfig; @@ -80,6 +83,7 @@ export type SendbirdChatContextType = { broadcastChannelEnabled: boolean; superGroupChannelEnabled: boolean; reactionEnabled: boolean; + uploadSizeLimit: number | undefined; }; }; }; @@ -90,6 +94,7 @@ export const SendbirdChatProvider = ({ sdkInstance, emojiManager, mentionManager, + voiceMessageStatusManager, imageCompressionConfig, voiceMessageConfig, enableAutoPushTokenRegistration, @@ -164,6 +169,7 @@ export const SendbirdChatProvider = ({ mentionManager, imageCompressionConfig, voiceMessageConfig, + voiceMessageStatusManager, currentUser, setCurrentUser, diff --git a/packages/uikit-react-native/src/domain/groupChannel/component/GroupChannelMessageList.tsx b/packages/uikit-react-native/src/domain/groupChannel/component/GroupChannelMessageList.tsx index 66f9fd2f8..996e956fd 100644 --- a/packages/uikit-react-native/src/domain/groupChannel/component/GroupChannelMessageList.tsx +++ b/packages/uikit-react-native/src/domain/groupChannel/component/GroupChannelMessageList.tsx @@ -1,9 +1,9 @@ -import React, { useContext, useEffect } from 'react'; +import React, { useContext, useEffect, useMemo } from 'react'; import { useChannelHandler } from '@sendbird/uikit-chat-hooks'; import { useToast } from '@sendbird/uikit-react-native-foundation'; import { SendbirdMessage, SendbirdSendableMessage } from '@sendbird/uikit-utils'; -import { isDifferentChannel, useFreshCallback, useIsFirstMount, useUniqHandlerId } from '@sendbird/uikit-utils'; +import { isDifferentChannel, useFreshCallback, useUniqHandlerId } from '@sendbird/uikit-utils'; import ChannelMessageList from '../../../components/ChannelMessageList'; import { MESSAGE_FOCUS_ANIMATION_DELAY, MESSAGE_SEARCH_SAFE_SCROLL_DELAY } from '../../../constants'; @@ -22,7 +22,9 @@ const GroupChannelMessageList = (props: GroupChannelProps['MessageList']) => { ); const id = useUniqHandlerId('GroupChannelMessageList'); - const isFirstMount = useIsFirstMount(); + const isChangedSearchItem = useMemo(() => { + return !!props.searchItem; + }, [props.searchItem?.startingPoint]); const scrollToMessageWithCreatedAt = useFreshCallback( (createdAt: number, focusAnimated: boolean, timeout: number): boolean => { @@ -93,13 +95,10 @@ const GroupChannelMessageList = (props: GroupChannelProps['MessageList']) => { }, [props.scrolledAwayFromBottom]); useEffect(() => { - // Only trigger once when message list mount with initial props.searchItem - // - Search screen + searchItem > mount message list - // - Reset message list + searchItem > re-mount message list - if (isFirstMount && props.searchItem) { + if (isChangedSearchItem && props.searchItem) { scrollToMessageWithCreatedAt(props.searchItem.startingPoint, false, MESSAGE_SEARCH_SAFE_SCROLL_DELAY); } - }, [isFirstMount]); + }, [isChangedSearchItem]); const onPressParentMessage = useFreshCallback( (parentMessage: SendbirdMessage, childMessage: SendbirdSendableMessage) => { @@ -108,7 +107,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'); diff --git a/packages/uikit-react-native/src/domain/groupChannelThread/component/GroupChannelThreadHeader.tsx b/packages/uikit-react-native/src/domain/groupChannelThread/component/GroupChannelThreadHeader.tsx index b31adb5cc..cd78ef47a 100644 --- a/packages/uikit-react-native/src/domain/groupChannelThread/component/GroupChannelThreadHeader.tsx +++ b/packages/uikit-react-native/src/domain/groupChannelThread/component/GroupChannelThreadHeader.tsx @@ -7,7 +7,7 @@ import { useLocalization, useSendbirdChat } from '../../../hooks/useContext'; import { GroupChannelThreadContexts } from '../module/moduleContext'; import type { GroupChannelThreadProps } from '../types'; -const GroupChannelThreadHeader = ({ onPressHeaderLeft }: GroupChannelThreadProps['Header']) => { +const GroupChannelThreadHeader = ({ onPressLeft, onPressSubtitle }: GroupChannelThreadProps['Header']) => { const { headerTitle, channel } = useContext(GroupChannelThreadContexts.Fragment); const { HeaderComponent } = useHeaderStyle(); const { STRINGS } = useLocalization(); @@ -19,6 +19,7 @@ const GroupChannelThreadHeader = ({ onPressHeaderLeft }: GroupChannelThreadProps return ( } left={} - onPressLeft={onPressHeaderLeft} + onPressLeft={onPressLeft} /> ); }; diff --git a/packages/uikit-react-native/src/domain/groupChannelThread/component/GroupChannelThreadMessageList.tsx b/packages/uikit-react-native/src/domain/groupChannelThread/component/GroupChannelThreadMessageList.tsx index f548e0304..6f833774d 100644 --- a/packages/uikit-react-native/src/domain/groupChannelThread/component/GroupChannelThreadMessageList.tsx +++ b/packages/uikit-react-native/src/domain/groupChannelThread/component/GroupChannelThreadMessageList.tsx @@ -1,4 +1,4 @@ -import React, { useContext, useEffect, useLayoutEffect } from 'react'; +import React, { useContext, useEffect, useLayoutEffect, useRef } from 'react'; import { useChannelHandler } from '@sendbird/uikit-chat-hooks'; import { isDifferentChannel, useFreshCallback, useUniqHandlerId } from '@sendbird/uikit-utils'; @@ -15,6 +15,19 @@ const GroupChannelThreadMessageList = (props: GroupChannelThreadProps['MessageLi const { flatListRef, lazyScrollToBottom, lazyScrollToIndex } = useContext(GroupChannelThreadContexts.MessageList); const id = useUniqHandlerId('GroupChannelThreadMessageList'); + const ignorePropReached = useRef(false); + + const _onTopReached = () => { + if (!ignorePropReached.current) { + props.onTopReached(); + } + }; + + const _onBottomReached = () => { + if (!ignorePropReached.current) { + props.onBottomReached(); + } + }; const scrollToBottom = useFreshCallback(async (animated = false) => { if (props.hasNext()) { @@ -33,7 +46,12 @@ const GroupChannelThreadMessageList = (props: GroupChannelThreadProps['MessageLi const foundMessageIndex = props.messages.findIndex((it) => it.createdAt === props.startingPoint); const isIncludedInList = foundMessageIndex > -1; if (isIncludedInList) { - lazyScrollToIndex({ index: foundMessageIndex, animated: true, timeout: 250 }); + ignorePropReached.current = true; + const timeout = 300; + lazyScrollToIndex({ index: foundMessageIndex, animated: true, timeout: timeout }); + setTimeout(() => { + ignorePropReached.current = false; + }, timeout + 50); } } }, [props.startingPoint]); @@ -73,9 +91,13 @@ const GroupChannelThreadMessageList = (props: GroupChannelThreadProps['MessageLi ); }; diff --git a/packages/uikit-react-native/src/domain/groupChannelThread/component/GroupChannelThreadParentMessageInfo.tsx b/packages/uikit-react-native/src/domain/groupChannelThread/component/GroupChannelThreadParentMessageInfo.tsx index 8d49378a5..828ad9347 100644 --- a/packages/uikit-react-native/src/domain/groupChannelThread/component/GroupChannelThreadParentMessageInfo.tsx +++ b/packages/uikit-react-native/src/domain/groupChannelThread/component/GroupChannelThreadParentMessageInfo.tsx @@ -48,7 +48,7 @@ const GroupChannelThreadParentMessageInfo = (props: GroupChannelThreadProps['Par const nickName = parentMessage.sender?.nickname || STRINGS.LABELS.USER_NO_NAME; const messageTimestamp = STRINGS.GROUP_CHANNEL_THREAD.PARENT_MESSAGE_TIME(parentMessage); - const replyCountText = STRINGS.GROUP_CHANNEL_THREAD.REPLAY_COUNT(parentMessage.threadInfo?.replyCount || 0); + const replyCountText = STRINGS.GROUP_CHANNEL_THREAD.REPLY_COUNT(parentMessage.threadInfo?.replyCount || 0); const createMessagePressActions = useCreateMessagePressActions({ channel: props.channel, currentUserId: props.currentUserId, diff --git a/packages/uikit-react-native/src/domain/groupChannelThread/types.ts b/packages/uikit-react-native/src/domain/groupChannelThread/types.ts index d1bd043fa..61f247d4e 100644 --- a/packages/uikit-react-native/src/domain/groupChannelThread/types.ts +++ b/packages/uikit-react-native/src/domain/groupChannelThread/types.ts @@ -27,7 +27,8 @@ export interface GroupChannelThreadProps { startingPoint?: number; onParentMessageDeleted: () => void; onChannelDeleted: () => void; - onPressHeaderLeft: GroupChannelThreadProps['Header']['onPressHeaderLeft']; + onPressHeaderLeft: GroupChannelThreadProps['Header']['onPressLeft']; + onPressHeaderSubtitle?: GroupChannelThreadProps['Header']['onPressSubtitle']; onPressMediaMessage?: GroupChannelThreadProps['MessageList']['onPressMediaMessage']; onBeforeSendUserMessage?: OnBeforeHandler; @@ -36,8 +37,6 @@ export interface GroupChannelThreadProps { onBeforeUpdateFileMessage?: OnBeforeHandler; renderMessage?: GroupChannelThreadProps['MessageList']['renderMessage']; - renderNewMessagesButton?: GroupChannelThreadProps['MessageList']['renderNewMessagesButton']; - renderScrollToBottomButton?: GroupChannelThreadProps['MessageList']['renderScrollToBottomButton']; enableMessageGrouping?: GroupChannelThreadProps['MessageList']['enableMessageGrouping']; @@ -46,7 +45,8 @@ export interface GroupChannelThreadProps { sortComparator?: UseGroupChannelMessagesOptions['sortComparator']; }; Header: { - onPressHeaderLeft: () => void; + onPressLeft: () => void; + onPressSubtitle: () => void; }; ParentMessageInfo: { channel: SendbirdGroupChannel; @@ -69,8 +69,6 @@ export interface GroupChannelThreadProps { | 'onDeleteMessage' | 'onPressMediaMessage' | 'renderMessage' - | 'renderNewMessagesButton' - | 'renderScrollToBottomButton' | 'flatListProps' | 'hasNext' | 'searchItem' diff --git a/packages/uikit-react-native/src/fragments/createGroupChannelFragment.tsx b/packages/uikit-react-native/src/fragments/createGroupChannelFragment.tsx index 4e153326c..853a49eac 100644 --- a/packages/uikit-react-native/src/fragments/createGroupChannelFragment.tsx +++ b/packages/uikit-react-native/src/fragments/createGroupChannelFragment.tsx @@ -2,13 +2,14 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { MessageCollection, MessageFilter } from '@sendbird/chat/groupChannel'; import { ReplyType } from '@sendbird/chat/message'; -import { Box } from '@sendbird/uikit-react-native-foundation'; +import { Box, useToast } from '@sendbird/uikit-react-native-foundation'; import { useGroupChannelMessages } from '@sendbird/uikit-tools'; -import type { +import { SendbirdFileMessage, SendbirdGroupChannel, SendbirdSendableMessage, SendbirdUserMessage, + getReadableFileSize, } from '@sendbird/uikit-utils'; import { NOOP, @@ -33,7 +34,8 @@ import type { GroupChannelProps, GroupChannelPubSubContextPayload, } from '../domain/groupChannel/types'; -import { usePlatformService, useSendbirdChat } from '../hooks/useContext'; +import { useLocalization, usePlatformService, useSendbirdChat } from '../hooks/useContext'; +import { FileType } from '../platform/types'; import pubsub from '../utils/pubsub'; const createGroupChannelFragment = (initModule?: Partial): GroupChannelFragment => { @@ -63,7 +65,9 @@ const createGroupChannelFragment = (initModule?: Partial): G collectionCreator, }) => { const { playerService, recorderService } = usePlatformService(); - const { sdk, currentUser, sbOptions } = useSendbirdChat(); + const { sdk, currentUser, sbOptions, voiceMessageStatusManager } = useSendbirdChat(); + const toast = useToast(); + const { STRINGS } = useLocalization(); const [internalSearchItem, setInternalSearchItem] = useState(searchItem); const navigateFromMessageSearch = useCallback(() => Boolean(searchItem), []); @@ -73,10 +77,17 @@ const createGroupChannelFragment = (initModule?: Partial): G const scrolledAwayFromBottomRef = useRefTracker(scrolledAwayFromBottom); const replyType = useIIFE(() => { - if (sbOptions.uikit.groupChannel.channel.replyType === 'none') return ReplyType.NONE; - else return ReplyType.ONLY_REPLY_TO_CHANNEL; + if (sbOptions.uikit.groupChannel.channel.replyType === 'none') { + return ReplyType.NONE; + } else { + return ReplyType.ONLY_REPLY_TO_CHANNEL; + } }); + useEffect(() => { + setInternalSearchItem(searchItem); + }, [searchItem?.startingPoint]); + const { loading, messages, @@ -113,6 +124,7 @@ const createGroupChannelFragment = (initModule?: Partial): G return Promise.allSettled([playerService.reset(), recorderService.reset()]); }; const _onPressHeaderLeft = useFreshCallback(async () => { + voiceMessageStatusManager.clear(); await onBlurFragment(); onPressHeaderLeft(); }); @@ -191,8 +203,17 @@ const createGroupChannelFragment = (initModule?: Partial): G const onPressSendFileMessage: GroupChannelProps['Input']['onPressSendFileMessage'] = useFreshCallback( async (params) => { const processedParams = await onBeforeSendFileMessage(params); - const message = await sendFileMessage(processedParams, onPending); - onSent(message); + const fileSize = (processedParams.file as FileType)?.size ?? processedParams.fileSize; + const uploadSizeLimit = sbOptions.appInfo.uploadSizeLimit; + + if (fileSize && uploadSizeLimit && fileSize > uploadSizeLimit) { + const sizeLimitString = `${getReadableFileSize(uploadSizeLimit)} MB`; + toast.show(STRINGS.TOAST.FILE_UPLOAD_SIZE_LIMIT_EXCEEDED_ERROR(sizeLimitString), 'error'); + return; + } else { + const message = await sendFileMessage(processedParams, onPending); + onSent(message); + } }, ); const onPressUpdateUserMessage: GroupChannelProps['Input']['onPressUpdateUserMessage'] = useFreshCallback( diff --git a/packages/uikit-react-native/src/fragments/createGroupChannelThreadFragment.tsx b/packages/uikit-react-native/src/fragments/createGroupChannelThreadFragment.tsx index 0088be636..963c05a16 100644 --- a/packages/uikit-react-native/src/fragments/createGroupChannelThreadFragment.tsx +++ b/packages/uikit-react-native/src/fragments/createGroupChannelThreadFragment.tsx @@ -7,6 +7,7 @@ import { SendbirdGroupChannel, type SendbirdMessage, SendbirdUserMessage, + getReadableFileSize, } from '@sendbird/uikit-utils'; import { NOOP, @@ -18,8 +19,6 @@ import { } from '@sendbird/uikit-utils'; import GroupChannelMessageRenderer from '../components/GroupChannelMessageRenderer'; -import NewMessagesButton from '../components/NewMessagesButton'; -import ScrollToBottomButton from '../components/ScrollToBottomButton'; import StatusComposition from '../components/StatusComposition'; import createGroupChannelThreadModule from '../domain/groupChannelThread/module/createGroupChannelThreadModule'; import type { @@ -37,11 +36,10 @@ const createGroupChannelThreadFragment = ( const GroupChannelThreadModule = createGroupChannelThreadModule(initModule); return ({ - renderNewMessagesButton = (props) => , - renderScrollToBottomButton = (props) => , renderMessage, enableMessageGrouping = true, onPressHeaderLeft = NOOP, + onPressHeaderSubtitle = NOOP, onPressMediaMessage = NOOP, onParentMessageDeleted = NOOP, onChannelDeleted = NOOP, @@ -57,7 +55,7 @@ const createGroupChannelThreadFragment = ( flatListProps, }) => { const { playerService, recorderService } = usePlatformService(); - const { sdk, currentUser, sbOptions } = useSendbirdChat(); + const { sdk, currentUser, sbOptions, voiceMessageStatusManager } = useSendbirdChat(); const [groupChannelThreadPubSub] = useState(() => pubsub()); const [scrolledAwayFromBottom, setScrolledAwayFromBottom] = useState(false); @@ -110,8 +108,14 @@ const createGroupChannelThreadFragment = ( }; const _onPressHeaderLeft = useFreshCallback(async () => { await onBlurFragment(); + voiceMessageStatusManager.publishAll(); onPressHeaderLeft(); }); + const _onPressHeaderSubtitle = useFreshCallback(async () => { + await onBlurFragment(); + voiceMessageStatusManager.publishAll(); + onPressHeaderSubtitle(); + }); const _onPressMediaMessage: NonNullable = useFreshCallback(async (message, deleteMessage, uri) => { await onBlurFragment(); @@ -143,7 +147,6 @@ const createGroupChannelThreadFragment = ( onPressMediaMessage={_onPressMediaMessage} /> ), - inverted: false, contentContainerStyle: { flexGrow: 1 }, ...flatListProps, }), @@ -182,8 +185,17 @@ const createGroupChannelThreadFragment = ( const onPressSendFileMessage: GroupChannelThreadProps['Input']['onPressSendFileMessage'] = useFreshCallback( async (params) => { const processedParams = await onBeforeSendFileMessage(params); - const message = await sendFileMessage(processedParams, onPending); - onSent(message); + const fileSize = (processedParams.file as File)?.size ?? processedParams.fileSize; + const uploadSizeLimit = sbOptions.appInfo.uploadSizeLimit; + + if (fileSize && uploadSizeLimit && fileSize > uploadSizeLimit) { + const sizeLimitString = `${getReadableFileSize(uploadSizeLimit)} MB`; + toast.show(STRINGS.TOAST.FILE_UPLOAD_SIZE_LIMIT_EXCEEDED_ERROR(sizeLimitString), 'error'); + return; + } else { + const message = await sendFileMessage(processedParams, onPending); + onSent(message); + } }, ); const onPressUpdateUserMessage: GroupChannelThreadProps['Input']['onPressUpdateUserMessage'] = useFreshCallback( @@ -213,7 +225,7 @@ const createGroupChannelThreadFragment = ( keyboardAvoidOffset={keyboardAvoidOffset} threadedMessages={messages} > - + }> members.filter((member) => member.userId !== currentUser?.userId)) .then((members) => members.slice(0, mentionManager.config.suggestionLimit)); } else { - return freshChannel.members - .sort((a, b) => a.nickname?.localeCompare(b.nickname)) - .filter( - (member) => - member.nickname?.toLowerCase().startsWith(searchString.toLowerCase()) && - member.userId !== currentUser?.userId && - member.isActive, - ) - .slice(0, mentionManager.config.suggestionLimit); + return ( + freshChannel.members + // NOTE: When using 'org.webkit:android-jsc', there is a problem with sorting lists that include words starting with uppercase and lowercase letters. + // To ensure consistent sorting regardless of the JSC, we compare the words in lowercase. + .sort((a, b) => a.nickname?.toLowerCase().localeCompare(b.nickname.toLowerCase())) + .filter( + (member) => + member.nickname?.toLowerCase().startsWith(searchString.toLowerCase()) && + member.userId !== currentUser?.userId && + member.isActive, + ) + .slice(0, mentionManager.config.suggestionLimit) + ); } }; diff --git a/packages/uikit-react-native/src/libs/VoiceMessageStatusManager.ts b/packages/uikit-react-native/src/libs/VoiceMessageStatusManager.ts new file mode 100644 index 000000000..da26d1540 --- /dev/null +++ b/packages/uikit-react-native/src/libs/VoiceMessageStatusManager.ts @@ -0,0 +1,56 @@ +interface VoiceMessageStatus { + currentTime: number; + subscribers?: Set<(currentTime: number) => void>; +} + +class VoiceMessageStatusManager { + private statusMap: Map = new Map(); + + private generateKey = (channelUrl: string, messageId: number): string => { + return `${channelUrl}-${messageId}`; + }; + + subscribe = (channelUrl: string, messageId: number, subscriber: (currentTime: number) => void) => { + const key = this.generateKey(channelUrl, messageId); + if (!this.statusMap.has(key)) { + this.statusMap.set(key, { currentTime: 0, subscribers: new Set() }); + } + this.statusMap.get(key)!.subscribers?.add(subscriber); + }; + + unsubscribe = (channelUrl: string, messageId: number, subscriber: (currentTime: number) => void) => { + const key = this.generateKey(channelUrl, messageId); + this.statusMap.get(key)?.subscribers?.delete(subscriber); + }; + + publishAll = (): void => { + this.statusMap.forEach((status) => { + status.subscribers?.forEach((subscriber) => { + subscriber(status.currentTime); + }); + }); + }; + + getCurrentTime = (channelUrl: string, messageId: number): number => { + const key = this.generateKey(channelUrl, messageId); + return this.statusMap.get(key)?.currentTime || 0; + }; + + setCurrentTime = (channelUrl: string, messageId: number, currentTime: number): void => { + const key = this.generateKey(channelUrl, messageId); + if (!this.statusMap.has(key)) { + this.statusMap.set(key, { currentTime }); + } else { + this.statusMap.get(key)!.currentTime = currentTime; + } + }; + + clear = (): void => { + this.statusMap.forEach((status) => { + status.subscribers?.clear(); + }); + this.statusMap.clear(); + }; +} + +export default VoiceMessageStatusManager; diff --git a/packages/uikit-react-native/src/localization/StringSet.type.ts b/packages/uikit-react-native/src/localization/StringSet.type.ts index 22df60e1c..48526153d 100644 --- a/packages/uikit-react-native/src/localization/StringSet.type.ts +++ b/packages/uikit-react-native/src/localization/StringSet.type.ts @@ -144,7 +144,7 @@ export interface StringSet { MESSAGE_BUBBLE_UNKNOWN_DESC: (message: SendbirdMessage) => string; PARENT_MESSAGE_TIME: (message: SendbirdMessage, locale?: Locale) => string; - REPLAY_COUNT: (replyCount: number) => string; + REPLY_COUNT: (replyCount: number, maxReplyCount?: number) => string; /** GroupChannelThread > Suggested mention list */ MENTION_LIMITED: (mentionLimit: number) => string; @@ -292,7 +292,8 @@ export interface StringSet { CHANNEL_INPUT_PLACEHOLDER_DISABLED: string; CHANNEL_INPUT_PLACEHOLDER_MUTED: string; CHANNEL_INPUT_PLACEHOLDER_REPLY: string; - CHANNEL_INPUT_PLACEHOLDER_THREAD: string; + CHANNEL_INPUT_PLACEHOLDER_REPLY_IN_THREAD: string; + CHANNEL_INPUT_PLACEHOLDER_REPLY_TO_THREAD: string; CHANNEL_INPUT_EDIT_OK: string; CHANNEL_INPUT_EDIT_CANCEL: string; /** ChannelInput > Attachments **/ @@ -371,6 +372,7 @@ export interface StringSet { GET_CHANNEL_ERROR: string; FIND_PARENT_MSG_ERROR: string; THREAD_PARENT_MESSAGE_DELETED_ERROR: string; + FILE_UPLOAD_SIZE_LIMIT_EXCEEDED_ERROR: (uploadSizeLimit: string) => string; }; PROFILE_CARD: { BUTTON_MESSAGE: string; diff --git a/packages/uikit-react-native/src/localization/createBaseStringSet.ts b/packages/uikit-react-native/src/localization/createBaseStringSet.ts index c1f318d3f..fec27daa1 100644 --- a/packages/uikit-react-native/src/localization/createBaseStringSet.ts +++ b/packages/uikit-react-native/src/localization/createBaseStringSet.ts @@ -142,7 +142,7 @@ export const createBaseStringSet = ({ dateLocale, overrides }: StringSetCreateOp PARENT_MESSAGE_TIME: (message: SendbirdMessage, locale?: Locale) => getThreadParentMessageTimeFormat(new Date(message.createdAt), locale ?? dateLocale), - REPLAY_COUNT: (replyCount: number) => getReplyCountFormat(replyCount), + REPLY_COUNT: (replyCount: number, maxReplyCount?: number) => getReplyCountFormat(replyCount, maxReplyCount), MENTION_LIMITED: (mentionLimit) => `You can have up to ${mentionLimit} mentions per message.`, ...overrides?.GROUP_CHANNEL_THREAD, @@ -280,9 +280,11 @@ export const createBaseStringSet = ({ dateLocale, overrides }: StringSetCreateOp return 'Several people are typing...'; }, REPLY_FROM_SENDER_TO_RECEIVER: (reply, parent, currentUserId = UNKNOWN_USER_ID) => { - const senderNickname = reply.sender.nickname || USER_NO_NAME; - const receiverNickname = parent.sender.nickname || USER_NO_NAME; - return `${reply.sender.userId !== currentUserId ? senderNickname : 'You'} replied to ${receiverNickname}`; + const replySenderNickname = + reply.sender.userId === currentUserId ? 'You' : reply.sender.nickname || USER_NO_NAME; + const parentSenderNickname = + parent.sender.userId === currentUserId ? 'You' : parent.sender.nickname || USER_NO_NAME; + return `${replySenderNickname} replied to ${parentSenderNickname}`; }, MESSAGE_UNAVAILABLE: 'Message unavailable', @@ -314,7 +316,8 @@ export const createBaseStringSet = ({ dateLocale, overrides }: StringSetCreateOp CHANNEL_INPUT_PLACEHOLDER_DISABLED: 'Chat not available in this channel.', CHANNEL_INPUT_PLACEHOLDER_MUTED: "You're muted by the operator.", CHANNEL_INPUT_PLACEHOLDER_REPLY: 'Reply to message', - CHANNEL_INPUT_PLACEHOLDER_THREAD: 'Reply in thread', + CHANNEL_INPUT_PLACEHOLDER_REPLY_IN_THREAD: 'Reply in thread', + CHANNEL_INPUT_PLACEHOLDER_REPLY_TO_THREAD: 'Reply to thread', CHANNEL_INPUT_EDIT_OK: 'Save', CHANNEL_INPUT_EDIT_CANCEL: 'Cancel', CHANNEL_INPUT_REPLY_PREVIEW_TITLE: (user) => `Reply to ${user.nickname || USER_NO_NAME}`, @@ -394,6 +397,9 @@ export const createBaseStringSet = ({ dateLocale, overrides }: StringSetCreateOp GET_CHANNEL_ERROR: "Couldn't retrieve channel.", FIND_PARENT_MSG_ERROR: "Couldn't find the original message for this reply.", THREAD_PARENT_MESSAGE_DELETED_ERROR: "The thread doesn't exist because the parent message was deleted.", + FILE_UPLOAD_SIZE_LIMIT_EXCEEDED_ERROR: (uploadSizeLimit: string) => { + return `The maximum size per file is ${uploadSizeLimit}.`; + }, ...overrides?.TOAST, }, PROFILE_CARD: { diff --git a/packages/uikit-utils/src/ui-format/common.ts b/packages/uikit-utils/src/ui-format/common.ts index b516e3b00..e0c515de1 100644 --- a/packages/uikit-utils/src/ui-format/common.ts +++ b/packages/uikit-utils/src/ui-format/common.ts @@ -157,14 +157,16 @@ export const millsToMSS = (mills: number) => { * Message reply count format * If reply count is 1: 1 'reply' * If the reply count is greater than 1 : '{count} replies' + * If the reply count is greater than {maxReplyCount} : '{maxReplyCount}+ replies' * */ -export const getReplyCountFormat = (replyCount: number) => { - if (replyCount === 1) { +export const getReplyCountFormat = (replyCount: number, maxReplyCount?: number) => { + if (maxReplyCount && replyCount > maxReplyCount) { + return `${maxReplyCount}+ replies`; + } else if (replyCount === 1) { return `${replyCount} reply`; } else if (replyCount > 1) { return `${replyCount} replies`; } - return ''; }; @@ -178,3 +180,18 @@ export const getReplyCountFormat = (replyCount: number) => { export const getThreadParentMessageTimeFormat = (date: Date, locale?: Locale): string => { return format(date, "MMM dd 'at' h:mm a", { locale }); }; + +/** + * File size format + * + * @param {number} fileSize + * @returns {string} + * */ +export const getReadableFileSize = (fileSize: number): string => { + if (fileSize <= 0) { + return '0 B'; + } + const units = ['B', 'KB', 'MB', 'GB', 'TB']; + const digitGroups = Math.floor(Math.log10(fileSize) / Math.log10(1024)); + return `${(fileSize / Math.pow(1024, digitGroups)).toFixed(1)} ${units[digitGroups]}`; +}; diff --git a/sample/src/App.tsx b/sample/src/App.tsx index 6adc5f57f..84d578e0b 100644 --- a/sample/src/App.tsx +++ b/sample/src/App.tsx @@ -62,7 +62,7 @@ const App = () => { groupChannel: { enableMention: true, typingIndicatorTypes: new Set([TypingIndicatorType.Text, TypingIndicatorType.Bubble]), - replyType: 'quote_reply', + replyType: 'thread', threadReplySelectType: 'thread', }, groupChannelList: { diff --git a/sample/src/screens/uikit/groupChannel/GroupChannelThreadScreen.tsx b/sample/src/screens/uikit/groupChannel/GroupChannelThreadScreen.tsx index 9ec14ad53..fb8a81e0e 100644 --- a/sample/src/screens/uikit/groupChannel/GroupChannelThreadScreen.tsx +++ b/sample/src/screens/uikit/groupChannel/GroupChannelThreadScreen.tsx @@ -43,6 +43,13 @@ const GroupChannelThreadScreen = () => { // Navigate back navigation.goBack(); }} + onPressHeaderSubtitle={() => { + // Navigate to parent message + navigation.navigate(Routes.GroupChannel, { + channelUrl: channel.url, + searchItem: { startingPoint: parentMessage.createdAt }, + }); + }} /> ); }; diff --git a/yarn.lock b/yarn.lock index f935ff794..8210ddae4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3389,10 +3389,10 @@ resolved "https://registry.yarnpkg.com/@sendbird/react-native-scrollview-enhancer/-/react-native-scrollview-enhancer-0.2.1.tgz#25de4af78293978a4c0ef6fddee25d822a364c46" integrity sha512-LN+Tm+ZUkE2MBVreg/JI8SVr8SOKRteZN0YFpGzRtbKkP45+pKyPN4JQPf73eFx7qO8zDL+TUVyzz/1MOnIK7g== -"@sendbird/uikit-tools@0.0.1-alpha.76": - version "0.0.1-alpha.76" - resolved "https://registry.yarnpkg.com/@sendbird/uikit-tools/-/uikit-tools-0.0.1-alpha.76.tgz#7f293aaa61089b644a9f4ca5f7929ce4f0d8885f" - integrity sha512-YbKsbpacI1OIvTGviUwuAAu7HPb/BeCxlo2AW6kyvRbNglocVxxcqvHW8Oe31r2oq98spEt7TPLMDuNyNNM5AA== +"@sendbird/uikit-tools@0.0.1-alpha.77": + version "0.0.1-alpha.77" + resolved "https://registry.yarnpkg.com/@sendbird/uikit-tools/-/uikit-tools-0.0.1-alpha.77.tgz#f305849dc767488b76853be653c2149ec240e558" + integrity sha512-MZEgPmBYGf5QI+gqxVXJmQf880giYt8eJ9WNndvBFAMCXGm6Z12LaurU0IOR87wPyROse5kTy8smwXT8UaiuYQ== "@sideway/address@^4.1.3": version "4.1.4"