diff --git a/packages/uikit-chat-hooks/src/channel/useOpenChannelList/useOpenChannelListWithQuery.ts b/packages/uikit-chat-hooks/src/channel/useOpenChannelList/useOpenChannelListWithQuery.ts index db173a8a5..f49f05d73 100644 --- a/packages/uikit-chat-hooks/src/channel/useOpenChannelList/useOpenChannelListWithQuery.ts +++ b/packages/uikit-chat-hooks/src/channel/useOpenChannelList/useOpenChannelListWithQuery.ts @@ -55,27 +55,26 @@ export const useOpenChannelListWithQuery: UseOpenChannelList = (sdk, userId, opt updateLoading(false); }, [init, userId]); - useChannelHandler(sdk, HOOK_NAME, { - onChannelChanged: (channel) => updateChannels([channel]), - onChannelFrozen: (channel) => updateChannels([channel]), - onChannelUnfrozen: (channel) => updateChannels([channel]), - onChannelMemberCountChanged: (channels) => updateChannels(channels), - onChannelDeleted: (url) => deleteChannels([url]), - onUserJoined: (channel) => updateChannels([channel]), - onUserLeft: (channel, user) => { - const isMe = user.userId === userId; - if (isMe) deleteChannels([channel.url]); - else updateChannels([channel]); + useChannelHandler( + sdk, + HOOK_NAME, + { + onChannelChanged: (channel) => updateChannels([channel]), + onChannelFrozen: (channel) => updateChannels([channel]), + onChannelUnfrozen: (channel) => updateChannels([channel]), + onChannelParticipantCountChanged: (channel) => updateChannels([channel]), + onChannelDeleted: (url) => deleteChannels([url]), + onUserBanned(channel, user) { + const isMe = user.userId === userId; + if (isMe) deleteChannels([channel.url]); + else updateChannels([channel]); + }, + onMessageReceived(channel) { + updateChannelsAndMarkAsDelivered([channel]); + }, }, - onUserBanned(channel, user) { - const isMe = user.userId === userId; - if (isMe) deleteChannels([channel.url]); - else updateChannels([channel]); - }, - onMessageReceived(channel) { - updateChannelsAndMarkAsDelivered([channel]); - }, - }); + 'open', + ); const refresh = useFreshCallback(async () => { updateRefreshing(true); diff --git a/packages/uikit-chat-hooks/src/channel/useOpenChannelMessages/useOpenChannelMessagesWithQuery.ts b/packages/uikit-chat-hooks/src/channel/useOpenChannelMessages/useOpenChannelMessagesWithQuery.ts index 189fbdf41..c57dcd3ad 100644 --- a/packages/uikit-chat-hooks/src/channel/useOpenChannelMessages/useOpenChannelMessagesWithQuery.ts +++ b/packages/uikit-chat-hooks/src/channel/useOpenChannelMessages/useOpenChannelMessagesWithQuery.ts @@ -68,55 +68,57 @@ export const useOpenChannelMessagesWithQuery: UseOpenChannelMessages = (sdk, cha } }; - useChannelHandler(sdk, HOOK_NAME, { - // Messages - onMessageReceived(eventChannel, message) { - if (isDifferentChannel(channel, eventChannel)) return; - channelMarkAsRead(); - updateNextMessages([message], false, sdk.currentUser.userId); - }, - onMessageUpdated(eventChannel, message) { - if (isDifferentChannel(channel, eventChannel)) return; - updateMessages([message], false, sdk.currentUser.userId); - }, - onMessageDeleted(eventChannel, messageId) { - if (isDifferentChannel(channel, eventChannel)) return; - deleteMessages([messageId], []); - deleteNextMessages([messageId], []); - }, - // Channels - onChannelChanged: channelUpdater, - onChannelFrozen: channelUpdater, - onChannelUnfrozen: channelUpdater, - onChannelHidden: channelUpdater, - onChannelMemberCountChanged(channels) { - const foundChannel = channels.find((c) => !isDifferentChannel(c, channel)); - if (foundChannel) channelUpdater(foundChannel); - }, - onChannelDeleted(channelUrl, type) { - if (channel.url === channelUrl && type === 'open') { - options?.onChannelDeleted?.(); - } - }, - // Users - onOperatorUpdated: channelUpdater, - onUserLeft: channelUpdater, - // onUserEntered: channelUpdater, - // onUserExited: channelUpdater, - onUserJoined: channelUpdater, - onUserUnbanned: channelUpdater, - onUserMuted: channelUpdater, - onUserUnmuted: channelUpdater, - onUserBanned(eventChannel, bannedUser) { - if (isDifferentChannel(channel, eventChannel)) return; - - if (bannedUser.userId === sdk.currentUser.userId) { - options?.onChannelDeleted?.(); - } else { + useChannelHandler( + sdk, + HOOK_NAME, + { + // Messages + onMessageReceived(eventChannel, message) { + if (isDifferentChannel(channel, eventChannel)) return; + channelMarkAsRead(); + updateNextMessages([message], false, sdk.currentUser.userId); + }, + onMessageUpdated(eventChannel, message) { + if (isDifferentChannel(channel, eventChannel)) return; + updateMessages([message], false, sdk.currentUser.userId); + }, + onMessageDeleted(eventChannel, messageId) { + if (isDifferentChannel(channel, eventChannel)) return; + deleteMessages([messageId], []); + deleteNextMessages([messageId], []); + }, + // Channels + onChannelChanged: channelUpdater, + onChannelFrozen: channelUpdater, + onChannelUnfrozen: channelUpdater, + onChannelParticipantCountChanged(eventChannel) { + if (isDifferentChannel(channel, eventChannel)) return; channelUpdater(eventChannel); - } + }, + onChannelDeleted(channelUrl, type) { + if (channel.url === channelUrl && type === 'open') { + options?.onChannelDeleted?.(); + } + }, + // Users + onOperatorUpdated: channelUpdater, + onUserEntered: channelUpdater, + onUserExited: channelUpdater, + onUserUnbanned: channelUpdater, + onUserMuted: channelUpdater, + onUserUnmuted: channelUpdater, + onUserBanned(eventChannel, bannedUser) { + if (isDifferentChannel(channel, eventChannel)) return; + + if (bannedUser.userId === sdk.currentUser.userId) { + options?.onChannelDeleted?.(); + } else { + channelUpdater(eventChannel); + } + }, }, - }); + 'open', + ); useAsyncEffect(async () => { updateLoading(true); diff --git a/packages/uikit-react-native/src/components/ChannelInput/EditInput.tsx b/packages/uikit-react-native/src/components/ChannelInput/EditInput.tsx index 17c5bec9d..b9fab9dc0 100644 --- a/packages/uikit-react-native/src/components/ChannelInput/EditInput.tsx +++ b/packages/uikit-react-native/src/components/ChannelInput/EditInput.tsx @@ -75,7 +75,7 @@ const EditInput = forwardRef(function EditInput( autoFocus={autoFocus} onChangeText={onChangeText} style={styles.input} - placeholder={STRINGS.GROUP_CHANNEL.INPUT_PLACEHOLDER_ACTIVE} + placeholder={STRINGS.LABELS.CHANNEL_INPUT_PLACEHOLDER_ACTIVE} onSelectionChange={onSelectionChange} > {mentionManager.textToMentionedComponents(text, mentionedUsers)} @@ -83,11 +83,11 @@ const EditInput = forwardRef(function EditInput( diff --git a/packages/uikit-react-native/src/components/ChannelInput/SendInput.tsx b/packages/uikit-react-native/src/components/ChannelInput/SendInput.tsx index eda4811ae..cfe1b6f20 100644 --- a/packages/uikit-react-native/src/components/ChannelInput/SendInput.tsx +++ b/packages/uikit-react-native/src/components/ChannelInput/SendInput.tsx @@ -70,7 +70,7 @@ const SendInput = forwardRef(function SendInput( openSheet({ sheetItems: [ { - title: STRINGS.GROUP_CHANNEL.DIALOG_ATTACHMENT_CAMERA, + title: STRINGS.LABELS.CHANNEL_INPUT_ATTACHMENT_CAMERA, icon: 'camera', onPress: async () => { const mediaFile = await fileService.openCamera({ @@ -117,7 +117,7 @@ const SendInput = forwardRef(function SendInput( }, }, { - title: STRINGS.GROUP_CHANNEL.DIALOG_ATTACHMENT_PHOTO_LIBRARY, + title: STRINGS.LABELS.CHANNEL_INPUT_ATTACHMENT_PHOTO_LIBRARY, icon: 'photo', onPress: async () => { const mediaFiles = await fileService.openMediaLibrary({ @@ -167,7 +167,7 @@ const SendInput = forwardRef(function SendInput( }, }, { - title: STRINGS.GROUP_CHANNEL.DIALOG_ATTACHMENT_FILES, + title: STRINGS.LABELS.CHANNEL_INPUT_ATTACHMENT_FILES, icon: 'document', onPress: async () => { const documentFile = await fileService.openDocument({ @@ -224,9 +224,9 @@ const SendInput = forwardRef(function SendInput( placeholder={conditionChaining( [inputFrozen, inputMuted], [ - STRINGS.GROUP_CHANNEL.INPUT_PLACEHOLDER_DISABLED, - STRINGS.GROUP_CHANNEL.INPUT_PLACEHOLDER_MUTED, - STRINGS.GROUP_CHANNEL.INPUT_PLACEHOLDER_ACTIVE, + STRINGS.LABELS.CHANNEL_INPUT_PLACEHOLDER_DISABLED, + STRINGS.LABELS.CHANNEL_INPUT_PLACEHOLDER_MUTED, + STRINGS.LABELS.CHANNEL_INPUT_PLACEHOLDER_ACTIVE, ], )} > diff --git a/packages/uikit-react-native/src/components/FileViewer.tsx b/packages/uikit-react-native/src/components/FileViewer.tsx index ed9d726b0..70486b02e 100644 --- a/packages/uikit-react-native/src/components/FileViewer.tsx +++ b/packages/uikit-react-native/src/components/FileViewer.tsx @@ -104,13 +104,13 @@ const FileViewer = ({ onPressDelete(fileMessage); } else { alert({ - title: STRINGS.GROUP_CHANNEL.DIALOG_MESSAGE_DELETE_CONFIRM_TITLE, + title: STRINGS.LABELS.CHANNEL_MESSAGE_DELETE_CONFIRM_TITLE, buttons: [ { - text: STRINGS.GROUP_CHANNEL.DIALOG_MESSAGE_DELETE_CONFIRM_CANCEL, + text: STRINGS.LABELS.CHANNEL_MESSAGE_DELETE_CONFIRM_CANCEL, }, { - text: STRINGS.GROUP_CHANNEL.DIALOG_MESSAGE_DELETE_CONFIRM_OK, + text: STRINGS.LABELS.CHANNEL_MESSAGE_DELETE_CONFIRM_OK, style: 'destructive', onPress: () => { deleteMessage() diff --git a/packages/uikit-react-native/src/components/OpenChannelMessageRenderer/index.tsx b/packages/uikit-react-native/src/components/OpenChannelMessageRenderer/index.tsx new file mode 100644 index 000000000..33c15e2d1 --- /dev/null +++ b/packages/uikit-react-native/src/components/OpenChannelMessageRenderer/index.tsx @@ -0,0 +1,121 @@ +import React from 'react'; +import { Pressable, PressableProps, View } from 'react-native'; + +import { Text, createStyleSheet } from '@sendbird/uikit-react-native-foundation'; +import { calcMessageGrouping, conditionChaining, useIIFE } from '@sendbird/uikit-utils'; + +import { DEFAULT_LONG_PRESS_DELAY } from '../../constants'; +import type { OpenChannelProps } from '../../domain/openChannel/types'; +import MessageContainer from '../MessageRenderer/MessageContainer'; +import MessageDateSeparator from '../MessageRenderer/MessageDateSeparator'; +import MessageIncomingAvatar from '../MessageRenderer/MessageIncomingAvatar'; +import MessageIncomingSenderName from '../MessageRenderer/MessageIncomingSenderName'; +import MessageTime from '../MessageRenderer/MessageTime'; + +const OpenChannelMessageRenderer: OpenChannelProps['Fragment']['renderMessage'] = ({ + message, + onPress, + onLongPress, + ...rest +}) => { + const { groupWithPrev, groupWithNext } = calcMessageGrouping( + Boolean(rest.enableMessageGrouping), + message, + rest.prevMessage, + rest.nextMessage, + ); + + const messageComponent = useIIFE(() => { + const pressableProps: PressableProps = { + style: styles.msgContainer, + disabled: !onPress && !onLongPress, + onPress, + onLongPress, + delayLongPress: DEFAULT_LONG_PRESS_DELAY, + }; + + // const messageProps = { ...rest, groupWithNext, groupWithPrev }; + + if (message.isUserMessage()) { + return {() => {'User message_' + message.message}}; + } + + if (message.isFileMessage()) { + return {() => {'File message'}}; + } + + if (message.isAdminMessage()) { + return {'Admin message'}; + } + + return {() => {'Unknown message'}}; + }); + + return ( + + + + + + + + {messageComponent} + + + + + + ); +}; + +const styles = createStyleSheet({ + chatIncoming: { + flexDirection: 'row', + justifyContent: 'flex-start', + alignItems: 'flex-end', + }, + chatOutgoing: { + flexDirection: 'row', + justifyContent: 'flex-end', + alignItems: 'flex-end', + }, + timeIncoming: { + marginLeft: 4, + }, + timeOutgoing: { + marginRight: 4, + }, + chatGroup: { + marginBottom: 2, + }, + chatNonGroup: { + marginBottom: 16, + }, + chatLastMessage: { + marginBottom: 16, + }, + msgContainer: { + maxWidth: 240, + }, + bubbleContainer: { + flexShrink: 1, + }, + bubbleWrapper: { + flexDirection: 'row', + alignItems: 'flex-end', + }, + outgoingContainer: { + flexDirection: 'row', + alignItems: 'flex-end', + justifyContent: 'center', + }, +}); + +export default React.memo(OpenChannelMessageRenderer); 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 3c165e7f7..e72c3fdfe 100644 --- a/packages/uikit-react-native/src/domain/groupChannel/component/GroupChannelMessageList.tsx +++ b/packages/uikit-react-native/src/domain/groupChannel/component/GroupChannelMessageList.tsx @@ -172,12 +172,12 @@ const useGetMessagePressActions = ({ openSheet({ sheetItems: [ { - title: STRINGS.GROUP_CHANNEL.DIALOG_MESSAGE_FAILED_RETRY, + title: STRINGS.LABELS.CHANNEL_MESSAGE_FAILED_RETRY, onPress: () => onResendFailedMessage(message).catch(() => toast.show(STRINGS.TOAST.RESEND_MSG_ERROR, 'error')), }, { - title: STRINGS.GROUP_CHANNEL.DIALOG_MESSAGE_FAILED_REMOVE, + title: STRINGS.LABELS.CHANNEL_MESSAGE_FAILED_REMOVE, titleColor: colors.ui.dialog.default.none.destructive, onPress: () => confirmDelete(message), }, @@ -186,13 +186,13 @@ const useGetMessagePressActions = ({ }; const confirmDelete = (message: HandleableMessage) => { alert({ - title: STRINGS.GROUP_CHANNEL.DIALOG_MESSAGE_DELETE_CONFIRM_TITLE, + title: STRINGS.LABELS.CHANNEL_MESSAGE_DELETE_CONFIRM_TITLE, buttons: [ { - text: STRINGS.GROUP_CHANNEL.DIALOG_MESSAGE_DELETE_CONFIRM_CANCEL, + text: STRINGS.LABELS.CHANNEL_MESSAGE_DELETE_CONFIRM_CANCEL, }, { - text: STRINGS.GROUP_CHANNEL.DIALOG_MESSAGE_DELETE_CONFIRM_OK, + text: STRINGS.LABELS.CHANNEL_MESSAGE_DELETE_CONFIRM_OK, style: 'destructive', onPress: () => onDeleteMessage(message).catch(() => toast.show(STRINGS.TOAST.DELETE_MSG_ERROR, 'error')), }, @@ -214,7 +214,7 @@ const useGetMessagePressActions = ({ if (msg.isUserMessage()) { sheetItems.push({ icon: 'copy', - title: STRINGS.GROUP_CHANNEL.DIALOG_MESSAGE_COPY, + title: STRINGS.LABELS.CHANNEL_MESSAGE_COPY, onPress: () => { clipboardService.setString(msg.message || ''); toast.show(STRINGS.TOAST.COPY_OK, 'success'); @@ -225,12 +225,12 @@ const useGetMessagePressActions = ({ sheetItems.push( { icon: 'edit', - title: STRINGS.GROUP_CHANNEL.DIALOG_MESSAGE_EDIT, + title: STRINGS.LABELS.CHANNEL_MESSAGE_EDIT, onPress: () => setMessageToEdit(msg), }, { icon: 'delete', - title: STRINGS.GROUP_CHANNEL.DIALOG_MESSAGE_DELETE, + title: STRINGS.LABELS.CHANNEL_MESSAGE_DELETE, onPress: () => confirmDelete(msg), }, ); @@ -240,7 +240,7 @@ const useGetMessagePressActions = ({ if (msg.isFileMessage()) { sheetItems.push({ icon: 'download', - title: STRINGS.GROUP_CHANNEL.DIALOG_MESSAGE_SAVE, + title: STRINGS.LABELS.CHANNEL_MESSAGE_SAVE, onPress: async () => { if (toMegabyte(msg.size) > 4) { toast.show(STRINGS.TOAST.DOWNLOAD_START, 'success'); @@ -262,7 +262,7 @@ const useGetMessagePressActions = ({ if (isMyMessage(msg, currentUserId) && msg.sendingStatus === 'succeeded') { sheetItems.push({ icon: 'delete', - title: STRINGS.GROUP_CHANNEL.DIALOG_MESSAGE_DELETE, + title: STRINGS.LABELS.CHANNEL_MESSAGE_DELETE, onPress: () => confirmDelete(msg), }); } diff --git a/packages/uikit-react-native/src/domain/openChannel/component/OpenChannelHeader.tsx b/packages/uikit-react-native/src/domain/openChannel/component/OpenChannelHeader.tsx index d8920f760..e64eae87e 100644 --- a/packages/uikit-react-native/src/domain/openChannel/component/OpenChannelHeader.tsx +++ b/packages/uikit-react-native/src/domain/openChannel/component/OpenChannelHeader.tsx @@ -1,16 +1,23 @@ import React, { useContext } from 'react'; -import { useHeaderStyle } from '@sendbird/uikit-react-native-foundation'; +import { Icon, useHeaderStyle } from '@sendbird/uikit-react-native-foundation'; import { OpenChannelContexts } from '../module/moduleContext'; import type { OpenChannelProps } from '../types'; -import { useLocalization } from "@sendbird/uikit-react-native"; -const OpenChannelHeader = (_: OpenChannelProps['Header']) => { +const OpenChannelHeader = ({ onPressHeaderLeft, onPressHeaderRight, rightIconName }: OpenChannelProps['Header']) => { const { headerTitle } = useContext(OpenChannelContexts.Fragment); const { HeaderComponent } = useHeaderStyle(); - const {STRINGS} = useLocalization(); - return ; + + return ( + } + onPressLeft={onPressHeaderLeft} + right={} + onPressRight={onPressHeaderRight} + /> + ); }; -export default OpenChannelHeader; +export default React.memo(OpenChannelHeader); diff --git a/packages/uikit-react-native/src/domain/openChannel/component/OpenChannelInput.tsx b/packages/uikit-react-native/src/domain/openChannel/component/OpenChannelInput.tsx index 7b32ab928..41a4e031d 100644 --- a/packages/uikit-react-native/src/domain/openChannel/component/OpenChannelInput.tsx +++ b/packages/uikit-react-native/src/domain/openChannel/component/OpenChannelInput.tsx @@ -36,23 +36,28 @@ const OpenChannelInput = (props: OpenChannelProps['Input']) => { }, [channel, updateChatAvailableState]); const id = useUniqId('OpenChannelInput'); - useChannelHandler(sdk, `OpenChannelInput_${id}`, { - onChannelFrozen(channel) { - updateChatAvailableState(channel); + useChannelHandler( + sdk, + `OpenChannelInput_${id}`, + { + onChannelFrozen(channel) { + updateChatAvailableState(channel); + }, + onChannelUnfrozen(channel) { + updateChatAvailableState(channel); + }, + onUserMuted(channel) { + updateChatAvailableState(channel); + }, + onUserUnmuted(channel) { + updateChatAvailableState(channel); + }, + onOperatorUpdated(channel) { + updateChatAvailableState(channel); + }, }, - onChannelUnfrozen(channel) { - updateChatAvailableState(channel); - }, - onUserMuted(channel) { - updateChatAvailableState(channel); - }, - onUserUnmuted(channel) { - updateChatAvailableState(channel); - }, - onOperatorUpdated(channel) { - updateChatAvailableState(channel); - }, - }); + 'open', + ); return ( { onSendUserMessage={props.onSendUserMessage} onUpdateFileMessage={props.onUpdateFileMessage} onUpdateUserMessage={props.onUpdateUserMessage} - SuggestedMentionList={props.SuggestedMentionList} /> ); }; diff --git a/packages/uikit-react-native/src/domain/openChannel/component/OpenChannelMessageList.tsx b/packages/uikit-react-native/src/domain/openChannel/component/OpenChannelMessageList.tsx new file mode 100644 index 000000000..665c980eb --- /dev/null +++ b/packages/uikit-react-native/src/domain/openChannel/component/OpenChannelMessageList.tsx @@ -0,0 +1,322 @@ +import React, { useCallback, useContext, useEffect, useRef, useState } from 'react'; +import { ListRenderItem, Platform, View } from 'react-native'; + +import type { BottomSheetItem } from '@sendbird/uikit-react-native-foundation'; +import { + ChannelFrozenBanner, + createStyleSheet, + useAlert, + useBottomSheet, + useToast, + useUIKitTheme, +} from '@sendbird/uikit-react-native-foundation'; +import type { SendbirdFileMessage, SendbirdMessage, SendbirdUserMessage } from '@sendbird/uikit-utils'; +import { + Logger, + getAvailableUriFromFileMessage, + getFileExtension, + getFileType, + isMyMessage, + messageKeyExtractor, + toMegabyte, + useFreshCallback, + useSafeAreaPadding, +} from '@sendbird/uikit-utils'; + +import type { ChatFlatListRef } from '../../../components/ChatFlatList'; +import ChatFlatList from '../../../components/ChatFlatList'; +import { useLocalization, usePlatformService } from '../../../hooks/useContext'; +import SBUUtils from '../../../libs/SBUUtils'; +import { OpenChannelContexts } from '../module/moduleContext'; +import type { OpenChannelProps } from '../types'; + +const HANDLE_NEXT_MSG_SEPARATELY = Platform.select({ default: true }); + +const OpenChannelMessageList = ({ + currentUserId, + channel, + messages, + renderMessage, + nextMessages, + newMessagesFromMembers, + onBottomReached, + onTopReached, + renderNewMessagesButton, + renderScrollToBottomButton, + onResendFailedMessage, + onDeleteMessage, + onPressMediaMessage, + flatListProps, + enableMessageGrouping, +}: OpenChannelProps['MessageList']) => { + const { STRINGS } = useLocalization(); + const { colors } = useUIKitTheme(); + + const [scrollLeaveBottom, setScrollLeaveBottom] = useState(false); + const [newMessagesInternalBuffer, setNewMessagesInternalBuffer] = useState(() => newMessagesFromMembers); + + const scrollRef = useRef(null); + + const safeAreaLayout = useSafeAreaPadding(['left', 'right']); + const getMessagePressActions = useGetMessagePressActions({ + channel, + currentUserId, + onDeleteMessage, + onResendFailedMessage, + onPressMediaMessage, + }); + + const renderItem: ListRenderItem = useFreshCallback(({ item, index }) => { + const { onPress, onLongPress } = getMessagePressActions(item); + return renderMessage({ + message: item, + prevMessage: messages[index + 1], + nextMessage: messages[index - 1], + onPress, + onLongPress, + enableMessageGrouping, + channel, + currentUserId, + }); + }); + + if (!HANDLE_NEXT_MSG_SEPARATELY) { + useEffect(() => { + if (newMessagesInternalBuffer.length !== 0) { + setNewMessagesInternalBuffer((prev) => prev.concat(newMessagesFromMembers)); + } + onBottomReached(); + }, [newMessagesFromMembers]); + } + + const onLeaveScrollBottom = useCallback((val: boolean) => { + if (!HANDLE_NEXT_MSG_SEPARATELY) setNewMessagesInternalBuffer([]); + setScrollLeaveBottom(val); + }, []); + + return ( + + {channel.isFrozen && ( + + )} + + {renderNewMessagesButton && ( + + {renderNewMessagesButton({ + visible: scrollLeaveBottom, + onPress: () => scrollRef.current?.scrollToBottom(false), + newMessages: !HANDLE_NEXT_MSG_SEPARATELY ? newMessagesInternalBuffer : newMessagesFromMembers, + })} + + )} + {renderScrollToBottomButton && ( + + {renderScrollToBottomButton({ + visible: scrollLeaveBottom, + onPress: () => scrollRef.current?.scrollToBottom(false), + })} + + )} + + ); +}; + +type HandleableMessage = SendbirdUserMessage | SendbirdFileMessage; +const useGetMessagePressActions = ({ + currentUserId, + onResendFailedMessage, + onDeleteMessage, + onPressMediaMessage, +}: Pick< + OpenChannelProps['MessageList'], + 'channel' | 'currentUserId' | 'onResendFailedMessage' | 'onDeleteMessage' | 'onPressMediaMessage' +>) => { + const { colors } = useUIKitTheme(); + const { STRINGS } = useLocalization(); + const toast = useToast(); + const { openSheet } = useBottomSheet(); + const { alert } = useAlert(); + const { clipboardService, fileService } = usePlatformService(); + const { setMessageToEdit } = useContext(OpenChannelContexts.Fragment); + + const handleFailedMessage = (message: HandleableMessage) => { + openSheet({ + sheetItems: [ + { + title: STRINGS.LABELS.CHANNEL_MESSAGE_FAILED_RETRY, + onPress: () => + onResendFailedMessage(message).catch(() => toast.show(STRINGS.TOAST.RESEND_MSG_ERROR, 'error')), + }, + { + title: STRINGS.LABELS.CHANNEL_MESSAGE_FAILED_REMOVE, + titleColor: colors.ui.dialog.default.none.destructive, + onPress: () => confirmDelete(message), + }, + ], + }); + }; + const confirmDelete = (message: HandleableMessage) => { + alert({ + title: STRINGS.LABELS.CHANNEL_MESSAGE_DELETE_CONFIRM_TITLE, + buttons: [ + { + text: STRINGS.LABELS.CHANNEL_MESSAGE_DELETE_CONFIRM_CANCEL, + }, + { + text: STRINGS.LABELS.CHANNEL_MESSAGE_DELETE_CONFIRM_OK, + style: 'destructive', + onPress: () => onDeleteMessage(message).catch(() => toast.show(STRINGS.TOAST.DELETE_MSG_ERROR, 'error')), + }, + ], + }); + }; + + return (msg: SendbirdMessage) => { + if (!msg.isUserMessage() && !msg.isFileMessage()) { + return { onPress: undefined, onLongPress: undefined }; + } + + const sheetItems: BottomSheetItem['sheetItems'] = []; + const response: { onPress?: () => void; onLongPress?: () => void } = { + onPress: undefined, + onLongPress: undefined, + }; + + if (msg.isUserMessage()) { + sheetItems.push({ + icon: 'copy', + title: STRINGS.LABELS.CHANNEL_MESSAGE_COPY, + onPress: () => { + clipboardService.setString(msg.message || ''); + toast.show(STRINGS.TOAST.COPY_OK, 'success'); + }, + }); + + if (isMyMessage(msg, currentUserId) && msg.sendingStatus === 'succeeded') { + sheetItems.push( + { + icon: 'edit', + title: STRINGS.LABELS.CHANNEL_MESSAGE_EDIT, + onPress: () => setMessageToEdit(msg), + }, + { + icon: 'delete', + title: STRINGS.LABELS.CHANNEL_MESSAGE_DELETE, + onPress: () => confirmDelete(msg), + }, + ); + } + } + + if (msg.isFileMessage()) { + sheetItems.push({ + icon: 'download', + title: STRINGS.LABELS.CHANNEL_MESSAGE_SAVE, + onPress: async () => { + if (toMegabyte(msg.size) > 4) { + toast.show(STRINGS.TOAST.DOWNLOAD_START, 'success'); + } + + fileService + .save({ fileUrl: msg.url, fileName: msg.name, fileType: msg.type }) + .then((response) => { + toast.show(STRINGS.TOAST.DOWNLOAD_OK, 'success'); + Logger.log('File saved to', response); + }) + .catch((err) => { + toast.show(STRINGS.TOAST.DOWNLOAD_ERROR, 'error'); + Logger.log('File save failure', err); + }); + }, + }); + + if (isMyMessage(msg, currentUserId) && msg.sendingStatus === 'succeeded') { + sheetItems.push({ + icon: 'delete', + title: STRINGS.LABELS.CHANNEL_MESSAGE_DELETE, + onPress: () => confirmDelete(msg), + }); + } + + const fileType = getFileType(msg.type || getFileExtension(msg.name)); + switch (fileType) { + case 'image': + case 'video': + case 'audio': { + response.onPress = () => { + onPressMediaMessage?.(msg, () => onDeleteMessage(msg), getAvailableUriFromFileMessage(msg)); + }; + break; + } + default: { + response.onPress = () => SBUUtils.openURL(msg.url); + break; + } + } + } + + if (sheetItems.length > 0) { + response.onLongPress = () => { + openSheet({ sheetItems }); + }; + } + + if (msg.sendingStatus === 'failed') { + response.onLongPress = () => handleFailedMessage(msg); + response.onPress = () => { + onResendFailedMessage(msg).catch(() => toast.show(STRINGS.TOAST.RESEND_MSG_ERROR, 'error')); + }; + } + + if (msg.sendingStatus === 'pending') { + response.onLongPress = undefined; + response.onPress = undefined; + } + + return response; + }; +}; + +const styles = createStyleSheet({ + frozenBanner: { + position: 'absolute', + zIndex: 999, + top: 8, + left: 8, + right: 8, + }, + frozenListPadding: { + paddingBottom: 32, + }, + newMsgButton: { + position: 'absolute', + zIndex: 999, + bottom: 10, + alignSelf: 'center', + }, + scrollButton: { + position: 'absolute', + zIndex: 998, + bottom: 10, + right: 16, + }, +}); + +export default React.memo(OpenChannelMessageList); diff --git a/packages/uikit-react-native/src/domain/openChannel/component/OpenChannelView.tsx b/packages/uikit-react-native/src/domain/openChannel/component/OpenChannelView.tsx deleted file mode 100644 index 1d0fb4ad0..000000000 --- a/packages/uikit-react-native/src/domain/openChannel/component/OpenChannelView.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import React from 'react'; - -import type { OpenChannelProps } from '../types'; - -const OpenChannelView = (_: OpenChannelProps['View']) => { - return <>; -}; - -export default OpenChannelView; diff --git a/packages/uikit-react-native/src/domain/openChannel/index.ts b/packages/uikit-react-native/src/domain/openChannel/index.ts index eda41c1e5..2cf4a83bc 100644 --- a/packages/uikit-react-native/src/domain/openChannel/index.ts +++ b/packages/uikit-react-native/src/domain/openChannel/index.ts @@ -1,5 +1,5 @@ -export { default as OpenChannelView } from './component/OpenChannelView'; export { default as OpenChannelHeader } from './component/OpenChannelHeader'; +export { default as OpenChannelInput } from './component/OpenChannelInput'; export { default as OpenChannelStatusLoading } from './component/OpenChannelStatusLoading'; export { default as OpenChannelStatusEmpty } from './component/OpenChannelStatusEmpty'; export { default as createOpenChannelModule } from './module/createOpenChannelModule'; diff --git a/packages/uikit-react-native/src/domain/openChannel/module/createOpenChannelModule.tsx b/packages/uikit-react-native/src/domain/openChannel/module/createOpenChannelModule.tsx index 7218c6900..ea7528662 100644 --- a/packages/uikit-react-native/src/domain/openChannel/module/createOpenChannelModule.tsx +++ b/packages/uikit-react-native/src/domain/openChannel/module/createOpenChannelModule.tsx @@ -1,19 +1,21 @@ import OpenChannelHeader from '../component/OpenChannelHeader'; +import OpenChannelInput from '../component/OpenChannelInput'; +import OpenChannelMessageList from '../component/OpenChannelMessageList'; import OpenChannelStatusEmpty from '../component/OpenChannelStatusEmpty'; import OpenChannelStatusLoading from '../component/OpenChannelStatusLoading'; -import OpenChannelView from '../component/OpenChannelView'; import type { OpenChannelModule } from '../types'; import { OpenChannelContextsProvider } from './moduleContext'; const createOpenChannelModule = ({ Header = OpenChannelHeader, - View = OpenChannelView, + MessageList = OpenChannelMessageList, + Input = OpenChannelInput, StatusLoading = OpenChannelStatusLoading, StatusEmpty = OpenChannelStatusEmpty, Provider = OpenChannelContextsProvider, ...module }: Partial = {}): OpenChannelModule => { - return { Header, View, Provider, StatusEmpty, StatusLoading, ...module }; + return { Header, MessageList, Input, Provider, StatusEmpty, StatusLoading, ...module }; }; export default createOpenChannelModule; diff --git a/packages/uikit-react-native/src/domain/openChannel/module/moduleContext.tsx b/packages/uikit-react-native/src/domain/openChannel/module/moduleContext.tsx index 114e7fdb8..0b0a9d5de 100644 --- a/packages/uikit-react-native/src/domain/openChannel/module/moduleContext.tsx +++ b/packages/uikit-react-native/src/domain/openChannel/module/moduleContext.tsx @@ -4,7 +4,7 @@ import type { SendbirdOpenChannel } from '@sendbird/uikit-utils'; import { NOOP, SendbirdFileMessage, SendbirdUserMessage } from '@sendbird/uikit-utils'; import ProviderLayout from '../../../components/ProviderLayout'; -import { useLocalization, useSendbirdChat } from '../../../hooks/useContext'; +import { useLocalization } from '../../../hooks/useContext'; import type { OpenChannelContextsType, OpenChannelModule } from '../types'; export const OpenChannelContexts: OpenChannelContextsType = { @@ -20,7 +20,6 @@ export const OpenChannelContextsProvider: OpenChannelModule['Provider'] = ({ channel, keyboardAvoidOffset, }) => { - const { currentUser } = useSendbirdChat(); const { STRINGS } = useLocalization(); const [messageToEdit, setMessageToEdit] = useState(); @@ -28,7 +27,7 @@ export const OpenChannelContextsProvider: OpenChannelModule['Provider'] = ({ = (params: T) => T | Promise; + export type OpenChannelProps = { Fragment: { + channel: SendbirdOpenChannel; + onChannelDeleted: () => void; onPressHeaderLeft: OpenChannelProps['Header']['onPressHeaderLeft']; + onPressHeaderRightWithSettings: OpenChannelProps['Header']['onPressHeaderRight']; + onPressHeaderRightWithParticipants: OpenChannelProps['Header']['onPressHeaderRight']; + + onBeforeSendFileMessage?: OnBeforeSendMessage; + onBeforeSendUserMessage?: OnBeforeSendMessage; + onPressMediaMessage?: OpenChannelProps['MessageList']['onPressMediaMessage']; + + renderMessage?: OpenChannelProps['MessageList']['renderMessage']; + renderNewMessagesButton?: OpenChannelProps['MessageList']['renderNewMessagesButton']; + renderScrollToBottomButton?: OpenChannelProps['MessageList']['renderScrollToBottomButton']; + + enableMessageGrouping?: OpenChannelProps['MessageList']['enableMessageGrouping']; + + keyboardAvoidOffset?: OpenChannelProps['Provider']['keyboardAvoidOffset']; + flatListProps?: OpenChannelProps['MessageList']['flatListProps']; + sortComparator?: UseOpenChannelMessagesOptions['sortComparator']; + queryCreator?: UseOpenChannelMessagesOptions['queryCreator']; }; Header: { + rightIconName: keyof typeof Icon.Assets; onPressHeaderLeft: () => void; + onPressHeaderRight: () => void; + }; + + MessageList: { + enableMessageGrouping: boolean; + currentUserId?: string; + channel: SendbirdOpenChannel; + messages: SendbirdMessage[]; + nextMessages: SendbirdMessage[]; + newMessagesFromMembers: SendbirdMessage[]; + onTopReached: () => void; + onBottomReached: () => void; + + onResendFailedMessage: (failedMessage: SendbirdUserMessage | SendbirdFileMessage) => Promise; + onDeleteMessage: (message: SendbirdUserMessage | SendbirdFileMessage) => Promise; + onPressMediaMessage?: (message: SendbirdFileMessage, deleteMessage: () => Promise, uri: string) => void; + + renderMessage: (props: { + message: SendbirdMessage; + prevMessage?: SendbirdMessage; + nextMessage?: SendbirdMessage; + onPress?: () => void; + onLongPress?: () => void; + channel: OpenChannelProps['MessageList']['channel']; + currentUserId?: OpenChannelProps['MessageList']['currentUserId']; + enableMessageGrouping: OpenChannelProps['MessageList']['enableMessageGrouping']; + }) => React.ReactElement | null; + renderNewMessagesButton: null | CommonComponent<{ + visible: boolean; + onPress: () => void; + newMessages: SendbirdMessage[]; + }>; + renderScrollToBottomButton: null | CommonComponent<{ + visible: boolean; + onPress: () => void; + }>; + flatListProps?: Omit, 'data' | 'renderItem'>; }; Input: Pick< ChannelInputProps, - | 'shouldRenderInput' - | 'onSendFileMessage' - | 'onSendUserMessage' - | 'onUpdateFileMessage' - | 'onUpdateUserMessage' - | 'SuggestedMentionList' + 'shouldRenderInput' | 'onSendFileMessage' | 'onSendUserMessage' | 'onUpdateFileMessage' | 'onUpdateUserMessage' >; Provider: { @@ -45,6 +109,7 @@ export type OpenChannelContextsType = { export interface OpenChannelModule { Provider: CommonComponent; Header: CommonComponent; + MessageList: CommonComponent; Input: CommonComponent; StatusEmpty: CommonComponent; StatusLoading: CommonComponent; diff --git a/packages/uikit-react-native/src/fragments/createOpenChannelFragment.tsx b/packages/uikit-react-native/src/fragments/createOpenChannelFragment.tsx index ccacfad64..6e9f3e1e9 100644 --- a/packages/uikit-react-native/src/fragments/createOpenChannelFragment.tsx +++ b/packages/uikit-react-native/src/fragments/createOpenChannelFragment.tsx @@ -1,22 +1,127 @@ -import React from 'react'; +import React, { useMemo } from 'react'; -import type { OpenChannelFragment, OpenChannelModule } from '../domain/openChannel/types'; +import { useOpenChannelMessages } from '@sendbird/uikit-chat-hooks'; +import { NOOP, PASS, messageComparator, useFreshCallback } from '@sendbird/uikit-utils'; + +import OpenChannelMessageRenderer from '../components/OpenChannelMessageRenderer'; +import ScrollToBottomButton from '../components/ScrollToBottomButton'; +import StatusComposition from '../components/StatusComposition'; +import { UNKNOWN_USER_ID } from '../constants'; import { createOpenChannelModule } from '../domain/openChannel'; -import { NOOP } from '@sendbird/uikit-utils'; +import type { OpenChannelFragment, OpenChannelModule, OpenChannelProps } from '../domain/openChannel/types'; +import { useSendbirdChat } from '../hooks/useContext'; const createOpenChannelFragment = (initModule?: Partial): OpenChannelFragment => { const OpenChannelModule = createOpenChannelModule(initModule); - return ({ onPressHeaderLeft = NOOP, children }) => { - // const { domainViewProps, loading } = useOpenChannel(); + return ({ + renderNewMessagesButton = () => null, + renderScrollToBottomButton = (props) => , + renderMessage, + enableMessageGrouping = true, + onPressHeaderLeft = NOOP, + onPressHeaderRightWithSettings = NOOP, + onPressHeaderRightWithParticipants = NOOP, + onPressMediaMessage = NOOP, + onChannelDeleted = NOOP, + onBeforeSendFileMessage = PASS, + onBeforeSendUserMessage = PASS, + channel, + keyboardAvoidOffset, + queryCreator, + sortComparator = messageComparator, + flatListProps, + }) => { + const { sdk, currentUser } = useSendbirdChat(); + const { + messages, + nextMessages, + newMessagesFromMembers, + next, + prev, + sendFileMessage, + sendUserMessage, + updateFileMessage, + updateUserMessage, + resendMessage, + deleteMessage, + loading, + } = useOpenChannelMessages(sdk, channel, currentUser?.userId, { + queryCreator, + sortComparator, + onChannelDeleted, + }); + + const isOperator = channel.isOperator(currentUser?.userId ?? UNKNOWN_USER_ID); - // if (loading) return ; + const _renderMessage: OpenChannelProps['MessageList']['renderMessage'] = useFreshCallback((props) => { + if (renderMessage) return renderMessage(props); + return ; + }); + + const memoizedFlatListProps = useMemo( + () => ({ + ListEmptyComponent: , + contentContainerStyle: { flexGrow: 1 }, + ...flatListProps, + }), + [loading, flatListProps], + ); + + const onSendFileMessage: OpenChannelProps['Input']['onSendFileMessage'] = useFreshCallback(async (file) => { + const processedParams = await onBeforeSendFileMessage({ file }); + await sendFileMessage(processedParams); + }); + const onSendUserMessage: OpenChannelProps['Input']['onSendUserMessage'] = useFreshCallback(async (text) => { + const processedParams = await onBeforeSendUserMessage({ message: text }); + await sendUserMessage(processedParams); + }); + const onUpdateFileMessage: OpenChannelProps['Input']['onUpdateFileMessage'] = useFreshCallback( + async (editedFile, message) => { + const processedParams = await onBeforeSendFileMessage({ file: editedFile }); + await updateFileMessage(message.messageId, processedParams); + }, + ); + const onUpdateUserMessage: OpenChannelProps['Input']['onUpdateUserMessage'] = useFreshCallback( + async (editedText, message) => { + const processedParams = await onBeforeSendUserMessage({ message: editedText }); + await updateUserMessage(message.messageId, processedParams); + }, + ); return ( - - - - {children} + + + }> + + + ); }; diff --git a/packages/uikit-react-native/src/index.ts b/packages/uikit-react-native/src/index.ts index ec102d525..7f5855d7e 100644 --- a/packages/uikit-react-native/src/index.ts +++ b/packages/uikit-react-native/src/index.ts @@ -17,7 +17,7 @@ export { default as TypedPlaceholder } from './components/TypedPlaceholder'; export { default as UserActionBar } from './components/UserActionBar'; export { default as UserSelectableBar } from './components/UserSelectableBar'; -/** Fragments **/ +/** Fragments - group channels **/ export { default as createGroupChannelCreateFragment } from './fragments/createGroupChannelCreateFragment'; export { default as createGroupChannelFragment } from './fragments/createGroupChannelFragment'; export { default as createGroupChannelSettingsFragment } from './fragments/createGroupChannelSettingsFragment'; @@ -31,6 +31,9 @@ export { default as createGroupChannelMutedMembersFragment } from './fragments/c export { default as createGroupChannelBannedUsersFragment } from './fragments/createGroupChannelBannedUsersFragment'; export { default as createGroupChannelNotificationsFragment } from './fragments/createGroupChannelNotificationsFragment'; +/** Fragments - open channels **/ +export { default as createOpenChannelFragment } from './fragments/createOpenChannelFragment'; + /** Context **/ export { SendbirdChatContext, SendbirdChatProvider } from './contexts/SendbirdChatCtx'; export { PlatformServiceContext, PlatformServiceProvider } from './contexts/PlatformServiceCtx'; @@ -75,7 +78,11 @@ export type { MediaServiceInterface, } from './platform/types'; -/** Domain **/ +/** Feature - shared **/ +export * from './domain/userList'; +export type { UserListProps, UserListModule, UserListContextsType } from './domain/userList/types'; + +/** Feature - group channels **/ export * from './domain/groupChannel'; export type { GroupChannelProps, @@ -104,19 +111,27 @@ export type { export * from './domain/groupChannelNotifications'; export { GroupChannelNotificationsProps, - GroupChannelNotificationsContextsType, - GroupChannelNotificationsFragment, GroupChannelNotificationsModule, + GroupChannelNotificationsFragment, + GroupChannelNotificationsContextsType, } from './domain/groupChannelNotifications/types'; export * from './domain/groupChannelUserList/types'; -export * from './domain/userList'; -export type { UserListProps, UserListModule, UserListContextsType } from './domain/userList/types'; + +/** Feature - open channels **/ +export * from './domain/openChannel'; +export { + OpenChannelProps, + OpenChannelModule, + OpenChannelFragment, + OpenChannelContextsType, +} from './domain/openChannel/types'; /** UIKit **/ export { default as SendbirdUIKitContainer, SendbirdUIKit } from './containers/SendbirdUIKitContainer'; export type { SendbirdUIKitContainerProps } from './containers/SendbirdUIKitContainer'; export { default as SBUError } from './libs/SBUError'; +export { default as SBUUtils } from './libs/SBUUtils'; export * from './types'; diff --git a/packages/uikit-react-native/src/localization/StringSet.type.ts b/packages/uikit-react-native/src/localization/StringSet.type.ts index e94c91b3f..6a3ecc679 100644 --- a/packages/uikit-react-native/src/localization/StringSet.type.ts +++ b/packages/uikit-react-native/src/localization/StringSet.type.ts @@ -6,6 +6,7 @@ import type { SendbirdGroupChannel, SendbirdMember, SendbirdMessage, + SendbirdOpenChannel, SendbirdUser, } from '@sendbird/uikit-utils'; import { @@ -15,12 +16,28 @@ import { getGroupChannelTitle, getMessageTimeFormat, } from '@sendbird/uikit-utils'; +import { getOpenChannelTitle } from '@sendbird/uikit-utils/src/ui-format/openChannel'; /** * StringSet interface * Do not configure over 3 depths (for overrides easy) * */ export interface StringSet { + OPEN_CHANNEL: { + /** OpenChannel > Header */ + HEADER_TITLE: (channel: SendbirdOpenChannel) => string; + + /** OpenChannel > List */ + LIST_BANNER_FROZEN: string; + LIST_DATE_SEPARATOR: (date: Date, locale?: Locale) => string; + + /** OpenChannel > Message bubble */ + MESSAGE_BUBBLE_TIME: (message: SendbirdMessage, locale?: Locale) => string; + MESSAGE_BUBBLE_FILE_TITLE: (message: SendbirdFileMessage) => string; + MESSAGE_BUBBLE_EDITED_POSTFIX: string; + MESSAGE_BUBBLE_UNKNOWN_TITLE: (message: SendbirdMessage) => string; + MESSAGE_BUBBLE_UNKNOWN_DESC: (message: SendbirdMessage) => string; + }; GROUP_CHANNEL: { /** GroupChannel > Header */ HEADER_TITLE: (currentUserId: string, channel: SendbirdGroupChannel) => string; @@ -37,33 +54,43 @@ export interface StringSet { MESSAGE_BUBBLE_UNKNOWN_TITLE: (message: SendbirdMessage) => string; MESSAGE_BUBBLE_UNKNOWN_DESC: (message: SendbirdMessage) => string; - /** GroupChannel > Input */ - INPUT_PLACEHOLDER_ACTIVE: string; - INPUT_PLACEHOLDER_DISABLED: string; - INPUT_PLACEHOLDER_MUTED: string; - INPUT_EDIT_OK: string; - INPUT_EDIT_CANCEL: string; - /** GroupChannel > Suggested mention list */ MENTION_LIMITED: (mentionLimit: number) => string; - /** GroupChannel > Dialog > Message */ + /** @deprecated Please use LABELS.CHANNEL_MESSAGE_COPY **/ DIALOG_MESSAGE_COPY: string; + /** @deprecated Please use LABELS.CHANNEL_MESSAGE_EDIT **/ DIALOG_MESSAGE_EDIT: string; + /** @deprecated Please use LABELS.CHANNEL_MESSAGE_SAVE **/ DIALOG_MESSAGE_SAVE: string; + /** @deprecated Please use LABELS.CHANNEL_MESSAGE_DELETE **/ DIALOG_MESSAGE_DELETE: string; - /** GroupChannel > Dialog > Message > Delete confirm */ + /** @deprecated Please use LABELS.CHANNEL_MESSAGE_DELETE_CONFIRM_TITLE **/ DIALOG_MESSAGE_DELETE_CONFIRM_TITLE: string; + /** @deprecated Please use LABELS.CHANNEL_MESSAGE_DELETE_CONFIRM_OK **/ DIALOG_MESSAGE_DELETE_CONFIRM_OK: string; + /** @deprecated Please use LABELS.CHANNEL_MESSAGE_DELETE_CONFIRM_CANCEL **/ DIALOG_MESSAGE_DELETE_CONFIRM_CANCEL: string; - /** GroupChannel > Dialog > Message > Failed */ + /** @deprecated Please use LABELS.CHANNEL_MESSAGE_FAILED_RETRY **/ DIALOG_MESSAGE_FAILED_RETRY: string; + /** @deprecated Please use LABELS.CHANNEL_MESSAGE_FAILED_REMOVE **/ DIALOG_MESSAGE_FAILED_REMOVE: string; - - /** GroupChannel > Dialog > Attachments */ + /** @deprecated Please use LABELS.CHANNEL_INPUT_ATTACHMENT_CAMERA **/ DIALOG_ATTACHMENT_CAMERA: string; + /** @deprecated Please use LABELS.CHANNEL_INPUT_ATTACHMENT_PHOTO_LIBRARY **/ DIALOG_ATTACHMENT_PHOTO_LIBRARY: string; + /** @deprecated Please use LABELS.CHANNEL_INPUT_ATTACHMENT_FILES **/ DIALOG_ATTACHMENT_FILES: string; + /** @deprecated Please use LABELS.CHANNEL_INPUT_PLACEHOLDER_ACTIVE **/ + INPUT_PLACEHOLDER_ACTIVE: string; + /** @deprecated Please use LABELS.CHANNEL_INPUT_PLACEHOLDER_DISABLED **/ + INPUT_PLACEHOLDER_DISABLED: string; + /** @deprecated Please use LABELS.CHANNEL_INPUT_PLACEHOLDER_MUTED **/ + INPUT_PLACEHOLDER_MUTED: string; + /** @deprecated Please use LABELS.CHANNEL_INPUT_EDIT_OK **/ + INPUT_EDIT_OK: string; + /** @deprecated Please use LABELS.CHANNEL_INPUT_EDIT_CANCEL **/ + INPUT_EDIT_CANCEL: string; }; GROUP_CHANNEL_SETTINGS: { /** GroupChannelSettings > Header */ @@ -152,9 +179,9 @@ export interface StringSet { /** GroupChannelMembers > Header */ HEADER_TITLE: string; - /** @deprecated Please use in LABELS **/ + /** @deprecated Please use LABELS.USER_BAR_ME_POSTFIX **/ USER_BAR_ME_POSTFIX: string; - /** @deprecated Please use in LABELS **/ + /** @deprecated Please use LABELS.USER_BAR_OPERATOR **/ USER_BAR_OPERATOR: string; }; GROUP_CHANNEL_INVITE: { @@ -186,6 +213,30 @@ export interface StringSet { UNMUTE: string; BAN: string; UNBAN: string; + + /** ChannelInput **/ + CHANNEL_INPUT_PLACEHOLDER_ACTIVE: string; + CHANNEL_INPUT_PLACEHOLDER_DISABLED: string; + CHANNEL_INPUT_PLACEHOLDER_MUTED: string; + CHANNEL_INPUT_EDIT_OK: string; + CHANNEL_INPUT_EDIT_CANCEL: string; + /** ChannelInput > Attachments **/ + CHANNEL_INPUT_ATTACHMENT_CAMERA: string; + CHANNEL_INPUT_ATTACHMENT_PHOTO_LIBRARY: string; + CHANNEL_INPUT_ATTACHMENT_FILES: string; + + /** Channel > Message **/ + CHANNEL_MESSAGE_COPY: string; + CHANNEL_MESSAGE_EDIT: string; + CHANNEL_MESSAGE_SAVE: string; + CHANNEL_MESSAGE_DELETE: string; + /** Channel > Message > Delete confirm **/ + CHANNEL_MESSAGE_DELETE_CONFIRM_TITLE: string; + CHANNEL_MESSAGE_DELETE_CONFIRM_OK: string; + CHANNEL_MESSAGE_DELETE_CONFIRM_CANCEL: string; + /** Channel > Message > Failed **/ + CHANNEL_MESSAGE_FAILED_RETRY: string; + CHANNEL_MESSAGE_FAILED_REMOVE: string; }; FILE_VIEWER: { TITLE: (message: SendbirdFileMessage) => string; @@ -253,9 +304,20 @@ export const createBaseStringSet = ({ dateLocale, overrides }: StringSetCreateOp const USER_NO_NAME = overrides?.LABELS?.USER_NO_NAME ?? '(No name)'; const CHANNEL_NO_MEMBERS = overrides?.LABELS?.CHANNEL_NO_MEMBERS ?? '(No members)'; return { + OPEN_CHANNEL: { + HEADER_TITLE: (channel) => getOpenChannelTitle(channel), + + LIST_BANNER_FROZEN: 'Channel is frozen', + LIST_DATE_SEPARATOR: (date, locale) => getDateSeparatorFormat(date, locale ?? dateLocale), + + MESSAGE_BUBBLE_TIME: (message, locale) => getMessageTimeFormat(new Date(message.createdAt), locale ?? dateLocale), + MESSAGE_BUBBLE_FILE_TITLE: (message) => message.name, + MESSAGE_BUBBLE_EDITED_POSTFIX: ' (edited)', + MESSAGE_BUBBLE_UNKNOWN_TITLE: () => '(Unknown message type)', + MESSAGE_BUBBLE_UNKNOWN_DESC: () => 'Cannot read this message.', + }, GROUP_CHANNEL: { - HEADER_TITLE: (currentUserId, channel) => - getGroupChannelTitle(currentUserId, channel, USER_NO_NAME, CHANNEL_NO_MEMBERS), + HEADER_TITLE: (uid, channel) => getGroupChannelTitle(uid, channel, USER_NO_NAME, CHANNEL_NO_MEMBERS), LIST_BANNER_FROZEN: 'Channel is frozen', LIST_DATE_SEPARATOR: (date, locale) => getDateSeparatorFormat(date, locale ?? dateLocale), LIST_BUTTON_NEW_MSG: (newMessages) => `${newMessages.length} new messages`, @@ -266,27 +328,42 @@ export const createBaseStringSet = ({ dateLocale, overrides }: StringSetCreateOp MESSAGE_BUBBLE_UNKNOWN_TITLE: () => '(Unknown message type)', MESSAGE_BUBBLE_UNKNOWN_DESC: () => 'Cannot read this message.', - INPUT_PLACEHOLDER_ACTIVE: 'Enter message', - INPUT_PLACEHOLDER_DISABLED: 'Chat not available in this channel.', - INPUT_PLACEHOLDER_MUTED: "You're muted by the operator.", - INPUT_EDIT_OK: 'Save', - INPUT_EDIT_CANCEL: 'Cancel', - MENTION_LIMITED: (mentionLimit) => `You can have up to ${mentionLimit} mentions per message.`, + /** @deprecated Please use LABELS.CHANNEL_MESSAGE_COPY **/ DIALOG_MESSAGE_COPY: 'Copy', + /** @deprecated Please use LABELS.CHANNEL_MESSAGE_EDIT **/ DIALOG_MESSAGE_EDIT: 'Edit', + /** @deprecated Please use LABELS.CHANNEL_MESSAGE_SAVE **/ DIALOG_MESSAGE_SAVE: 'Save', + /** @deprecated Please use LABELS.CHANNEL_MESSAGE_DELETE **/ DIALOG_MESSAGE_DELETE: 'Delete', + /** @deprecated Please use LABELS.CHANNEL_MESSAGE_DELETE_CONFIRM_TITLE **/ DIALOG_MESSAGE_DELETE_CONFIRM_TITLE: 'Delete message?', + /** @deprecated Please use LABELS.CHANNEL_MESSAGE_DELETE_CONFIRM_OK **/ DIALOG_MESSAGE_DELETE_CONFIRM_OK: 'Delete', + /** @deprecated Please use LABELS.CHANNEL_MESSAGE_DELETE_CONFIRM_CANCEL **/ DIALOG_MESSAGE_DELETE_CONFIRM_CANCEL: 'Cancel', + /** @deprecated Please use LABELS.CHANNEL_MESSAGE_FAILED_RETRY **/ DIALOG_MESSAGE_FAILED_RETRY: 'Retry', + /** @deprecated Please use LABELS.CHANNEL_MESSAGE_FAILED_REMOVE **/ DIALOG_MESSAGE_FAILED_REMOVE: 'Remove', - + /** @deprecated Please use LABELS.CHANNEL_INPUT_ATTACHMENT_CAMERA **/ DIALOG_ATTACHMENT_CAMERA: 'Camera', + /** @deprecated Please use LABELS.CHANNEL_INPUT_ATTACHMENT_PHOTO_LIBRARY **/ DIALOG_ATTACHMENT_PHOTO_LIBRARY: 'Photo library', + /** @deprecated Please use LABELS.CHANNEL_INPUT_ATTACHMENT_FILES **/ DIALOG_ATTACHMENT_FILES: 'Files', + /** @deprecated Please use LABELS.CHANNEL_INPUT_PLACEHOLDER_ACTIVE **/ + INPUT_PLACEHOLDER_ACTIVE: 'Enter message', + /** @deprecated Please use LABELS.CHANNEL_INPUT_PLACEHOLDER_DISABLED **/ + INPUT_PLACEHOLDER_DISABLED: 'Chat not available in this channel.', + /** @deprecated Please use LABELS.CHANNEL_INPUT_PLACEHOLDER_MUTED **/ + INPUT_PLACEHOLDER_MUTED: "You're muted by the operator.", + /** @deprecated Please use LABELS.CHANNEL_INPUT_EDIT_OK **/ + INPUT_EDIT_OK: 'Save', + /** @deprecated Please use LABELS.CHANNEL_INPUT_EDIT_CANCEL **/ + INPUT_EDIT_CANCEL: 'Cancel', ...overrides?.GROUP_CHANNEL, }, GROUP_CHANNEL_SETTINGS: { @@ -370,9 +447,9 @@ export const createBaseStringSet = ({ dateLocale, overrides }: StringSetCreateOp }, GROUP_CHANNEL_MEMBERS: { HEADER_TITLE: 'Members', - /** @deprecated Please use in LABELS **/ + /** @deprecated Please use LABELS.USER_BAR_ME_POSTFIX **/ USER_BAR_ME_POSTFIX: ' (You)', - /** @deprecated Please use in LABELS **/ + /** @deprecated Please use LABELS.USER_BAR_OPERATOR **/ USER_BAR_OPERATOR: 'Operator', ...overrides?.GROUP_CHANNEL_MEMBERS, }, @@ -415,6 +492,29 @@ export const createBaseStringSet = ({ dateLocale, overrides }: StringSetCreateOp UNMUTE: 'Unmute', BAN: 'Ban', UNBAN: 'Unban', + + // Deprecation backward + CHANNEL_MESSAGE_COPY: overrides?.GROUP_CHANNEL?.DIALOG_MESSAGE_COPY ?? 'Copy', + CHANNEL_MESSAGE_EDIT: overrides?.GROUP_CHANNEL?.DIALOG_MESSAGE_EDIT ?? 'Edit', + CHANNEL_MESSAGE_SAVE: overrides?.GROUP_CHANNEL?.DIALOG_MESSAGE_SAVE ?? 'Save', + CHANNEL_MESSAGE_DELETE: overrides?.GROUP_CHANNEL?.DIALOG_MESSAGE_DELETE ?? 'Delete', + CHANNEL_MESSAGE_DELETE_CONFIRM_TITLE: + overrides?.GROUP_CHANNEL?.DIALOG_MESSAGE_DELETE_CONFIRM_TITLE ?? 'Delete message?', + CHANNEL_MESSAGE_DELETE_CONFIRM_OK: overrides?.GROUP_CHANNEL?.DIALOG_MESSAGE_DELETE_CONFIRM_OK ?? 'Delete', + CHANNEL_MESSAGE_DELETE_CONFIRM_CANCEL: overrides?.GROUP_CHANNEL?.DIALOG_MESSAGE_DELETE_CONFIRM_CANCEL ?? 'Cancel', + CHANNEL_MESSAGE_FAILED_RETRY: overrides?.GROUP_CHANNEL?.DIALOG_MESSAGE_FAILED_RETRY ?? 'Retry', + CHANNEL_MESSAGE_FAILED_REMOVE: overrides?.GROUP_CHANNEL?.DIALOG_MESSAGE_FAILED_REMOVE ?? 'Remove', + CHANNEL_INPUT_ATTACHMENT_CAMERA: overrides?.GROUP_CHANNEL?.DIALOG_ATTACHMENT_CAMERA ?? 'Camera', + CHANNEL_INPUT_ATTACHMENT_PHOTO_LIBRARY: + overrides?.GROUP_CHANNEL?.DIALOG_ATTACHMENT_PHOTO_LIBRARY ?? 'Photo library', + CHANNEL_INPUT_ATTACHMENT_FILES: overrides?.GROUP_CHANNEL?.DIALOG_ATTACHMENT_FILES ?? 'Files', + CHANNEL_INPUT_PLACEHOLDER_ACTIVE: overrides?.GROUP_CHANNEL?.INPUT_PLACEHOLDER_ACTIVE ?? 'Enter message', + CHANNEL_INPUT_PLACEHOLDER_DISABLED: + overrides?.GROUP_CHANNEL?.INPUT_PLACEHOLDER_DISABLED ?? 'Chat not available in this channel.', + CHANNEL_INPUT_PLACEHOLDER_MUTED: + overrides?.GROUP_CHANNEL?.INPUT_PLACEHOLDER_MUTED ?? "You're muted by the operator.", + CHANNEL_INPUT_EDIT_OK: overrides?.GROUP_CHANNEL?.INPUT_EDIT_OK ?? 'Save', + CHANNEL_INPUT_EDIT_CANCEL: overrides?.GROUP_CHANNEL?.INPUT_EDIT_CANCEL ?? 'Cancel', ...overrides?.LABELS, }, FILE_VIEWER: { diff --git a/packages/uikit-utils/src/index.ts b/packages/uikit-utils/src/index.ts index 17014a35b..c87b6dec5 100644 --- a/packages/uikit-utils/src/index.ts +++ b/packages/uikit-utils/src/index.ts @@ -79,6 +79,7 @@ export type { SendbirdDataPayload, SendbirdPreviousMessageListQuery, SendbirdSendableMessage, + SendbirdOpenChannelListQuery, SendbirdGroupChannelListQuery, SendbirdGroupChannelUpdateParams, SendbirdUserMessageUpdateParams, diff --git a/packages/uikit-utils/src/sendbird/channel.ts b/packages/uikit-utils/src/sendbird/channel.ts index caf105a79..a8f37c62c 100644 --- a/packages/uikit-utils/src/sendbird/channel.ts +++ b/packages/uikit-utils/src/sendbird/channel.ts @@ -20,7 +20,7 @@ export const getGroupChannelChatAvailableState = (channel: SendbirdGroupChannel) }; export const getOpenChannelChatAvailableState = async (channel: SendbirdOpenChannel, userId: string) => { - const frozen = channel.isFrozen && channel.isOperator(userId); + const frozen = channel.isFrozen && !channel.isOperator(userId); const muted = (await channel.getMyMutedInfo()).isMuted; const disabled = frozen || muted; return { disabled, frozen, muted }; diff --git a/packages/uikit-utils/src/sendbird/message.ts b/packages/uikit-utils/src/sendbird/message.ts index 52fd601b7..4b6975945 100644 --- a/packages/uikit-utils/src/sendbird/message.ts +++ b/packages/uikit-utils/src/sendbird/message.ts @@ -114,7 +114,16 @@ export function parseSendbirdNotification(dataPayload: RawSendbirdDataPayload): } export function shouldRenderReaction(channel: SendbirdBaseChannel, reactionEnabled: boolean) { - return channel.isGroupChannel() && !channel.isBroadcast && !channel.isSuper && reactionEnabled; + if (channel.isOpenChannel()) { + return false; + } + + if (channel.isGroupChannel()) { + if (channel.isBroadcast) return false; + if (channel.isSuper) return false; + } + + return reactionEnabled; } export function getReactionCount(reaction: SendbirdReaction) { diff --git a/packages/uikit-utils/src/types.ts b/packages/uikit-utils/src/types.ts index 3f75694ba..cdbbecd05 100644 --- a/packages/uikit-utils/src/types.ts +++ b/packages/uikit-utils/src/types.ts @@ -37,7 +37,7 @@ import type { UserMessageCreateParams, UserMessageUpdateParams, } from '@sendbird/chat/message'; -import type { OpenChannel, OpenChannelModule } from '@sendbird/chat/openChannel'; +import type { OpenChannel, OpenChannelListQuery, OpenChannelModule } from '@sendbird/chat/openChannel'; export type FilterByValueType = { [K in keyof T as T[K] extends Type ? K : never]: T[K]; @@ -98,6 +98,7 @@ export type SendbirdEmojiContainer = EmojiContainer; export type SendbirdGroupChannelCollection = GroupChannelCollection; export type SendbirdGroupChannelListQuery = GroupChannelListQuery; +export type SendbirdOpenChannelListQuery = OpenChannelListQuery; export type SendbirdMessageCollection = MessageCollection; export type SendbirdPreviousMessageListQuery = PreviousMessageListQuery; diff --git a/packages/uikit-utils/src/ui-format/openChannel.ts b/packages/uikit-utils/src/ui-format/openChannel.ts new file mode 100644 index 000000000..60ead53b7 --- /dev/null +++ b/packages/uikit-utils/src/ui-format/openChannel.ts @@ -0,0 +1,7 @@ +import type { SendbirdOpenChannel } from '../types'; + +export const getOpenChannelTitle = (channel: SendbirdOpenChannel, DEFAULT_CHANNEL_NAME = 'Open Channel') => { + const trimmedChannelName = channel.name.trim(); + if (trimmedChannelName === '') return DEFAULT_CHANNEL_NAME; + return trimmedChannelName; +};