diff --git a/packages/uikit-chat-hooks/src/channel/useGroupChannelMessages/useGroupChannelMessagesWithCollection.ts b/packages/uikit-chat-hooks/src/channel/useGroupChannelMessages/useGroupChannelMessagesWithCollection.ts index 512d83797..a75a2949e 100644 --- a/packages/uikit-chat-hooks/src/channel/useGroupChannelMessages/useGroupChannelMessagesWithCollection.ts +++ b/packages/uikit-chat-hooks/src/channel/useGroupChannelMessages/useGroupChannelMessagesWithCollection.ts @@ -19,7 +19,7 @@ import { useChannelMessagesReducer } from '../useChannelMessagesReducer'; const createMessageCollection = (channel: SendbirdGroupChannel, options?: UseGroupChannelMessagesOptions) => { if (options?.collectionCreator) return options?.collectionCreator({ startingPoint: options?.startingPoint }); const filter = new MessageFilter(); - return channel.createMessageCollection({ filter, limit: 20, startingPoint: options?.startingPoint }); + return channel.createMessageCollection({ filter, limit: 30, startingPoint: options?.startingPoint }); }; function isNotEmpty(arr?: unknown[]): arr is unknown[] { @@ -28,6 +28,8 @@ function isNotEmpty(arr?: unknown[]): arr is unknown[] { } export const useGroupChannelMessagesWithCollection: UseGroupChannelMessages = (sdk, channel, userId, options) => { + const initialStartingPoint = options?.startingPoint ?? Number.MAX_SAFE_INTEGER; + const forceUpdate = useForceUpdate(); const collectionRef = useRef(); const handlerId = useUniqHandlerId('useGroupChannelMessagesWithCollection'); @@ -66,107 +68,109 @@ export const useGroupChannelMessagesWithCollection: UseGroupChannelMessages = (s if (isNotEmpty(failedMessages)) updateMessages(failedMessages, false, sdk.currentUser.userId); }; - const init = useFreshCallback(async (uid?: string, callback?: () => void) => { + const init = useFreshCallback(async (startingPoint: number, callback?: () => void) => { if (collectionRef.current) collectionRef.current?.dispose(); - if (uid) { - channelMarkAsRead(); - updateNewMessages([], true, sdk.currentUser.userId); - - collectionRef.current = createMessageCollection(channel, options); - collectionRef.current?.setMessageCollectionHandler({ - onMessagesAdded: (_, __, messages) => { - channelMarkAsRead(_.source); - - const incomingMessages = messages.filter((it) => { - switch (_.source) { - case MessageEventSource.EVENT_MESSAGE_SENT_PENDING: - case MessageEventSource.EVENT_MESSAGE_SENT_SUCCESS: - case MessageEventSource.EVENT_MESSAGE_SENT_FAILED: - return !isMyMessage(it, sdk.currentUser.userId); - default: - return true; - } - }); + channelMarkAsRead(); + updateNewMessages([], true, sdk.currentUser.userId); - if (incomingMessages.length > 0) { - updateMessages(incomingMessages, false, sdk.currentUser.userId); + collectionRef.current = createMessageCollection(channel, { + collectionCreator: options?.collectionCreator, + startingPoint, + }); + + collectionRef.current?.setMessageCollectionHandler({ + onMessagesAdded: (_, __, messages) => { + channelMarkAsRead(_.source); + + const incomingMessages = messages.filter((it) => { + switch (_.source) { + case MessageEventSource.EVENT_MESSAGE_SENT_PENDING: + case MessageEventSource.EVENT_MESSAGE_SENT_SUCCESS: + case MessageEventSource.EVENT_MESSAGE_SENT_FAILED: + return !isMyMessage(it, sdk.currentUser.userId); + default: + return true; + } + }); - if (options?.shouldCountNewMessages?.()) { - updateNewMessages(incomingMessages, false, sdk.currentUser.userId); - } + if (incomingMessages.length > 0) { + updateMessages(incomingMessages, false, sdk.currentUser.userId); - switch (_.source) { - case MessageEventSource.EVENT_MESSAGE_RECEIVED: - case MessageEventSource.SYNC_MESSAGE_FILL: { - options?.onMessagesReceived?.(incomingMessages); - } - } + if (options?.shouldCountNewMessages?.()) { + updateNewMessages(incomingMessages, false, sdk.currentUser.userId); } - }, - onMessagesUpdated: (_, __, messages) => { - channelMarkAsRead(_.source); - - const incomingMessages = messages.filter((it) => { - switch (_.source) { - case MessageEventSource.EVENT_MESSAGE_UPDATED: - return !isMyMessage(it, sdk.currentUser.userId); - default: - return true; - } - }); - if (incomingMessages.length > 0) { - // NOTE: admin message is not added via onMessagesAdded handler, not checked yet is this a bug. - updateMessages(messages, false, sdk.currentUser.userId); - - if (options?.shouldCountNewMessages?.()) { - if (_.source === MessageEventSource.EVENT_MESSAGE_RECEIVED) { - updateNewMessages(messages, false, sdk.currentUser.userId); - } + switch (_.source) { + case MessageEventSource.EVENT_MESSAGE_RECEIVED: + case MessageEventSource.SYNC_MESSAGE_FILL: { + options?.onMessagesReceived?.(incomingMessages); } } - }, - onMessagesDeleted: (_, __, messageIds) => { - deleteMessages(messageIds, []); - deleteNewMessages(messageIds, []); - }, - onChannelDeleted: () => { - options?.onChannelDeleted?.(); - }, - onChannelUpdated: (_, eventChannel) => { - if (eventChannel.isGroupChannel() && !isDifferentChannel(eventChannel, channel)) { - forceUpdate(); + } + }, + onMessagesUpdated: (_, __, messages) => { + channelMarkAsRead(_.source); + + const incomingMessages = messages.filter((it) => { + switch (_.source) { + case MessageEventSource.EVENT_MESSAGE_UPDATED: + return !isMyMessage(it, sdk.currentUser.userId); + default: + return true; } - }, - onHugeGapDetected: () => { - init(uid); - }, - }); + }); - collectionRef.current - .initialize(MessageCollectionInitPolicy.CACHE_AND_REPLACE_BY_API) - .onCacheResult((err, messages) => { - if (err) sdk.isCacheEnabled && Logger.error('[useGroupChannelMessagesWithCollection/onCacheResult]', err); - else { - Logger.debug('[useGroupChannelMessagesWithCollection/onCacheResult]', 'message length:', messages.length); + if (incomingMessages.length > 0) { + // NOTE: admin message is not added via onMessagesAdded handler, not checked yet is this a bug. + updateMessages(messages, false, sdk.currentUser.userId); - updateMessages(messages, true, sdk.currentUser.userId); - updateUnsendMessages(); - } - callback?.(); - }) - .onApiResult((err, messages) => { - if (err) Logger.warn('[useGroupChannelMessagesWithCollection/onApiResult]', err); - else { - Logger.debug('[useGroupChannelMessagesWithCollection/onApiResult]', 'message length:', messages.length); - - updateMessages(messages, true, sdk.currentUser.userId); - if (sdk.isCacheEnabled) updateUnsendMessages(); + if (options?.shouldCountNewMessages?.()) { + if (_.source === MessageEventSource.EVENT_MESSAGE_RECEIVED) { + updateNewMessages(messages, false, sdk.currentUser.userId); + } } - callback?.(); - }); - } + } + }, + onMessagesDeleted: (_, __, messageIds) => { + deleteMessages(messageIds, []); + deleteNewMessages(messageIds, []); + }, + onChannelDeleted: () => { + options?.onChannelDeleted?.(); + }, + onChannelUpdated: (_, eventChannel) => { + if (eventChannel.isGroupChannel() && !isDifferentChannel(eventChannel, channel)) { + forceUpdate(); + } + }, + onHugeGapDetected: () => { + init(Number.MAX_SAFE_INTEGER); + }, + }); + + collectionRef.current + .initialize(MessageCollectionInitPolicy.CACHE_AND_REPLACE_BY_API) + .onCacheResult((err, messages) => { + if (err) sdk.isCacheEnabled && Logger.error('[useGroupChannelMessagesWithCollection/onCacheResult]', err); + else { + Logger.debug('[useGroupChannelMessagesWithCollection/onCacheResult]', 'message length:', messages.length); + + updateMessages(messages, true, sdk.currentUser.userId); + updateUnsendMessages(); + } + callback?.(); + }) + .onApiResult((err, messages) => { + if (err) Logger.warn('[useGroupChannelMessagesWithCollection/onApiResult]', err); + else { + Logger.debug('[useGroupChannelMessagesWithCollection/onApiResult]', 'message length:', messages.length); + + updateMessages(messages, true, sdk.currentUser.userId); + if (sdk.isCacheEnabled) updateUnsendMessages(); + } + callback?.(); + }); }); useChannelHandler(sdk, handlerId, { @@ -185,7 +189,7 @@ export const useGroupChannelMessagesWithCollection: UseGroupChannelMessages = (s // NOTE: Cache read is heavy task, and it prevents smooth ui transition setTimeout(async () => { updateLoading(true); - init(userId, () => updateLoading(false)); + init(initialStartingPoint, () => updateLoading(false)); }, 0); }, [channel.url, userId]); @@ -197,7 +201,7 @@ export const useGroupChannelMessagesWithCollection: UseGroupChannelMessages = (s const refresh: ReturnType['refresh'] = useFreshCallback(async () => { updateRefreshing(true); - init(userId, () => updateRefreshing(false)); + init(Number.MAX_SAFE_INTEGER, () => updateRefreshing(false)); }); const prev: ReturnType['prev'] = useFreshCallback(async () => { @@ -314,6 +318,16 @@ export const useGroupChannelMessagesWithCollection: UseGroupChannelMessages = (s const resetNewMessages: ReturnType['resetNewMessages'] = useFreshCallback(() => { updateNewMessages([], true, sdk.currentUser.userId); }); + const resetWithStartingPoint: ReturnType['resetWithStartingPoint'] = useFreshCallback( + (startingPoint, callback) => { + updateLoading(true); + updateMessages([], true, sdk.currentUser.userId); + init(startingPoint, () => { + updateLoading(false); + callback?.(); + }); + }, + ); return { loading, @@ -332,6 +346,7 @@ export const useGroupChannelMessagesWithCollection: UseGroupChannelMessages = (s updateFileMessage, resendMessage, deleteMessage, + resetWithStartingPoint, nextMessages: newMessages, newMessagesFromMembers: newMessages, }; diff --git a/packages/uikit-chat-hooks/src/channel/useGroupChannelMessages/useGroupChannelMessagesWithQuery.ts b/packages/uikit-chat-hooks/src/channel/useGroupChannelMessages/useGroupChannelMessagesWithQuery.ts index 35a7582f9..8ed459f8d 100644 --- a/packages/uikit-chat-hooks/src/channel/useGroupChannelMessages/useGroupChannelMessagesWithQuery.ts +++ b/packages/uikit-chat-hooks/src/channel/useGroupChannelMessages/useGroupChannelMessagesWithQuery.ts @@ -278,6 +278,9 @@ export const useGroupChannelMessagesWithQuery: UseGroupChannelMessages = (sdk, c updateFileMessage, resendMessage, deleteMessage, + resetWithStartingPoint() { + Logger.warn('resetWithStartingPoint is not supported in Query, please use Collection instead.'); + }, nextMessages: newMessages, newMessagesFromMembers: newMessages, diff --git a/packages/uikit-chat-hooks/src/types.ts b/packages/uikit-chat-hooks/src/types.ts index b070a88f0..264ef18e6 100644 --- a/packages/uikit-chat-hooks/src/types.ts +++ b/packages/uikit-chat-hooks/src/types.ts @@ -136,6 +136,11 @@ export interface UseGroupChannelMessages { * */ newMessages: SendbirdMessage[]; + /** + * reset message list with starting point + * */ + resetWithStartingPoint: (startingPoint: number, callback?: () => void) => void; + /** * Reset new messages * */ diff --git a/packages/uikit-react-native/src/components/ChannelMessageList/index.tsx b/packages/uikit-react-native/src/components/ChannelMessageList/index.tsx index 350a15853..fe2a8a61e 100644 --- a/packages/uikit-react-native/src/components/ChannelMessageList/index.tsx +++ b/packages/uikit-react-native/src/components/ChannelMessageList/index.tsx @@ -165,7 +165,7 @@ const ChannelMessageList = {renderNewMessagesButton({ - visible: !hasNext() && scrolledAwayFromBottom, + visible: newMessages.length > 0 && (hasNext() || scrolledAwayFromBottom), onPress: () => onPressNewMessagesButton(), newMessages, })} @@ -174,7 +174,7 @@ const ChannelMessageList = {renderScrollToBottomButton({ - visible: !hasNext() && scrolledAwayFromBottom, + visible: hasNext() || scrolledAwayFromBottom, onPress: () => onPressScrollToBottomButton(), })} diff --git a/packages/uikit-react-native/src/components/MessageSearchListItem.tsx b/packages/uikit-react-native/src/components/MessageSearchListItem.tsx new file mode 100644 index 000000000..df4563cb4 --- /dev/null +++ b/packages/uikit-react-native/src/components/MessageSearchListItem.tsx @@ -0,0 +1,107 @@ +import React from 'react'; + +import { + Avatar, + Box, + Icon, + PressBox, + Text, + createStyleSheet, + useUIKitTheme, +} from '@sendbird/uikit-react-native-foundation'; +import type { SendbirdBaseMessage, SendbirdGroupChannel } from '@sendbird/uikit-utils'; +import { getFileExtension, getFileType, useIIFE } from '@sendbird/uikit-utils'; + +import { useLocalization } from '../hooks/useContext'; + +const iconMapper = { audio: 'file-audio', image: 'photo', video: 'play', file: 'file-document' } as const; + +type Props = { + onPressMessage: (param: { channel: SendbirdGroupChannel; message: SendbirdBaseMessage }) => void; + channel: SendbirdGroupChannel; + message: SendbirdBaseMessage; +}; +export const MessageSearchListItem = ({ onPressMessage, channel, message }: Props) => { + const { colors, select, palette } = useUIKitTheme(); + const { STRINGS } = useLocalization(); + + const bodyIcon = useIIFE(() => { + if (!message?.isFileMessage()) return undefined; + return iconMapper[getFileType(message.type || getFileExtension(message.name))]; + }); + + return ( + onPressMessage({ channel, message })}> + + + + + + + + {STRINGS.MESSAGE_SEARCH.MESSAGE_PREVIEW_TITLE(message)} + + + + + {STRINGS.MESSAGE_SEARCH.MESSAGE_PREVIEW_TITLE_CAPTION(message)} + + + + + + + {bodyIcon && ( + + )} + + + {STRINGS.MESSAGE_SEARCH.MESSAGE_PREVIEW_BODY(message)} + + + + + + + + + ); +}; + +function getSenderProfile(message: SendbirdBaseMessage) { + if (message.isUserMessage() || message.isFileMessage()) { + return message.sender.profileUrl; + } else { + return undefined; + } +} + +const styles = createStyleSheet({ + avatar: { + marginHorizontal: 16, + }, + title: { + marginBottom: 4, + }, + bodyIcon: { + borderRadius: 8, + width: 26, + height: 26, + marginRight: 4, + }, + separator: { + position: 'absolute', + left: 0, + right: -16, + bottom: 0, + height: 1, + }, +}); diff --git a/packages/uikit-react-native/src/domain/groupChannel/component/GroupChannelHeader.tsx b/packages/uikit-react-native/src/domain/groupChannel/component/GroupChannelHeader.tsx index 9f051aa2a..046af5be2 100644 --- a/packages/uikit-react-native/src/domain/groupChannel/component/GroupChannelHeader.tsx +++ b/packages/uikit-react-native/src/domain/groupChannel/component/GroupChannelHeader.tsx @@ -8,13 +8,19 @@ import { useLocalization } from '../../../hooks/useContext'; import { GroupChannelContexts } from '../module/moduleContext'; import type { GroupChannelProps } from '../types'; -const GroupChannelHeader = ({ onPressHeaderLeft, onPressHeaderRight }: GroupChannelProps['Header']) => { +const GroupChannelHeader = ({ + shouldHideRight, + onPressHeaderLeft, + onPressHeaderRight, +}: GroupChannelProps['Header']) => { const { headerTitle, channel } = useContext(GroupChannelContexts.Fragment); const { typingUsers } = useContext(GroupChannelContexts.TypingIndicator); const { STRINGS } = useLocalization(); const { HeaderComponent } = useHeaderStyle(); const subtitle = STRINGS.LABELS.TYPING_INDICATOR_TYPINGS(typingUsers); + const isHidden = shouldHideRight(); + return ( } onPressLeft={onPressHeaderLeft} - right={} - onPressRight={onPressHeaderRight} + right={isHidden ? null : } + onPressRight={isHidden ? undefined : onPressHeaderRight} /> ); }; 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 6b750dae1..e8147e4fd 100644 --- a/packages/uikit-react-native/src/domain/groupChannel/component/GroupChannelMessageList.tsx +++ b/packages/uikit-react-native/src/domain/groupChannel/component/GroupChannelMessageList.tsx @@ -18,15 +18,21 @@ const GroupChannelMessageList = (props: GroupChannelProps['MessageList']) => { const id = useUniqHandlerId('GroupChannelMessageList'); const ref = useRef>(null); + // FIXME: Workaround, should run after data has been applied to UI. + const lazyScrollToBottom = (animated = false, timeout = 0) => { + setTimeout(() => { + ref.current?.scrollToOffset({ offset: 0, animated }); + }, timeout); + }; + const scrollToBottom = useFreshCallback((animated = false) => { if (props.hasNext()) { - // TODO: Add startingPoint reset logic - // props.onChangeStartingPoint?.(); + props.onResetMessageList(() => { + lazyScrollToBottom(animated); + props.onScrolledAwayFromBottom(false); + }); } else { - // FIXME: Workaround, should run after data has been applied to UI. - setTimeout(() => { - ref.current?.scrollToOffset({ offset: 0, animated }); - }, 0); + lazyScrollToBottom(animated); } }); @@ -37,19 +43,18 @@ const GroupChannelMessageList = (props: GroupChannelProps['MessageList']) => { const isRecentMessage = recentMessage && recentMessage.messageId === event.messageId; const scrollReachedBottomAndCanScroll = !props.scrolledAwayFromBottom && !props.hasNext(); if (isRecentMessage && scrollReachedBottomAndCanScroll) { - // FIXME: Workaround, should run after data has been applied to UI. - setTimeout(() => { - ref.current?.scrollToOffset({ offset: 0, animated: true }); - }, 250); + lazyScrollToBottom(true, 250); } }, }); useEffect(() => { - subscribe(({ type }) => { + return subscribe(({ type }) => { switch (type) { case 'MESSAGES_RECEIVED': { - scrollToBottom(true); + if (!props.scrolledAwayFromBottom) { + scrollToBottom(true); + } break; } case 'MESSAGE_SENT_PENDING': { @@ -58,7 +63,7 @@ const GroupChannelMessageList = (props: GroupChannelProps['MessageList']) => { } } }); - }, []); + }, [props.scrolledAwayFromBottom]); return ( boolean; onPressHeaderLeft: () => void; onPressHeaderRight: () => void; }; @@ -74,6 +76,8 @@ export interface GroupChannelProps { | 'onPressImageMessage' | 'hasNext' > & { + onResetMessageList: (callback?: () => void) => void; + /** @deprecated Please use `newMessages` instead */ newMessagesFromMembers: SendbirdMessage[]; /** @deprecated Please use `newMessages` instead */ diff --git a/packages/uikit-react-native/src/domain/groupChannelSettings/component/GroupChannelSettingsMenu.tsx b/packages/uikit-react-native/src/domain/groupChannelSettings/component/GroupChannelSettingsMenu.tsx index 735f0adf5..2082eb571 100644 --- a/packages/uikit-react-native/src/domain/groupChannelSettings/component/GroupChannelSettingsMenu.tsx +++ b/packages/uikit-react-native/src/domain/groupChannelSettings/component/GroupChannelSettingsMenu.tsx @@ -15,6 +15,7 @@ let WARN_onPressMenuNotification = false; const GroupChannelSettingsMenu = ({ onPressMenuModeration, onPressMenuMembers, + onPressMenuSearchInChannel, onPressMenuLeaveChannel, onPressMenuNotification, menuItemsCreator = (menu) => menu, @@ -88,6 +89,11 @@ const GroupChannelSettingsMenu = ({ actionLabel: String(channel.memberCount), actionItem: , }, + { + icon: 'search', + name: STRINGS.GROUP_CHANNEL_SETTINGS.MENU_SEARCH, + onPress: () => onPressMenuSearchInChannel(), + }, { icon: 'leave', iconColor: colors.error, diff --git a/packages/uikit-react-native/src/domain/groupChannelSettings/types.ts b/packages/uikit-react-native/src/domain/groupChannelSettings/types.ts index 3cf930ec9..9b770d08f 100644 --- a/packages/uikit-react-native/src/domain/groupChannelSettings/types.ts +++ b/packages/uikit-react-native/src/domain/groupChannelSettings/types.ts @@ -11,6 +11,7 @@ export interface GroupChannelSettingsProps { onPressHeaderLeft: GroupChannelSettingsProps['Header']['onPressHeaderLeft']; onPressMenuModeration: GroupChannelSettingsProps['Menu']['onPressMenuModeration']; onPressMenuMembers: GroupChannelSettingsProps['Menu']['onPressMenuMembers']; + onPressMenuSearchInChannel: GroupChannelSettingsProps['Menu']['onPressMenuSearchInChannel']; onPressMenuLeaveChannel: GroupChannelSettingsProps['Menu']['onPressMenuLeaveChannel']; onPressMenuNotification?: GroupChannelSettingsProps['Menu']['onPressMenuNotification']; menuItemsCreator?: GroupChannelSettingsProps['Menu']['menuItemsCreator']; @@ -22,6 +23,7 @@ export interface GroupChannelSettingsProps { Menu: { onPressMenuModeration: () => void; onPressMenuMembers: () => void; + onPressMenuSearchInChannel: () => void; onPressMenuLeaveChannel: () => void; onPressMenuNotification?: () => void; menuItemsCreator?: (defaultMenuItems: MenuBarProps[]) => MenuBarProps[]; diff --git a/packages/uikit-react-native/src/domain/messageSearch/component/MessageSearchHeader.tsx b/packages/uikit-react-native/src/domain/messageSearch/component/MessageSearchHeader.tsx new file mode 100644 index 000000000..877caf452 --- /dev/null +++ b/packages/uikit-react-native/src/domain/messageSearch/component/MessageSearchHeader.tsx @@ -0,0 +1,96 @@ +import React, { useEffect, useRef } from 'react'; +import { Platform, TextInput } from 'react-native'; + +import { + Box, + Icon, + PressBox, + Text, + createStyleSheet, + useHeaderStyle, + useUIKitTheme, +} from '@sendbird/uikit-react-native-foundation'; + +import { useLocalization } from '../../../hooks/useContext'; +import type { MessageSearchProps } from '../types'; + +const MessageSearchHeader = ({ + keyword, + onChangeKeyword, + onPressHeaderLeft, + onPressHeaderRight, +}: MessageSearchProps['Header']) => { + const { HeaderComponent } = useHeaderStyle(); + const { colors } = useUIKitTheme(); + const { STRINGS } = useLocalization(); + + const inputRef = useRef(null); + const inputColor = colors.ui.input.default.active; + const searchEnabled = keyword.length > 0; + + useEffect(() => { + setTimeout(() => { + inputRef.current?.focus(); + }, Platform.select({ ios: 500, default: 0 })); + }, []); + + return ( + + + onPressHeaderRight()} + selectionColor={colors.primary} + placeholder={STRINGS.MESSAGE_SEARCH.HEADER_INPUT_PLACEHOLDER} + placeholderTextColor={inputColor.placeholder} + style={[styles.input, { color: inputColor.text }]} + value={keyword} + onChangeText={onChangeKeyword} + /> + {searchEnabled && ( + onChangeKeyword('')}> + + + )} + + } + left={} + onPressLeft={onPressHeaderLeft} + right={ + + {STRINGS.MESSAGE_SEARCH.HEADER_RIGHT} + + } + onPressRight={searchEnabled ? onPressHeaderRight : undefined} + /> + ); +}; + +const styles = createStyleSheet({ + searchIcon: { + marginRight: 8, + }, + clearIcon: { + marginLeft: 8, + }, + input: { + flex: 1, + height: '100%', + fontSize: 14, + marginBottom: Platform.select({ android: -2, default: 0 }), + }, +}); + +export default MessageSearchHeader; diff --git a/packages/uikit-react-native/src/domain/messageSearch/component/MessageSearchList.tsx b/packages/uikit-react-native/src/domain/messageSearch/component/MessageSearchList.tsx new file mode 100644 index 000000000..a7e9f1840 --- /dev/null +++ b/packages/uikit-react-native/src/domain/messageSearch/component/MessageSearchList.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import { FlatList, ListRenderItem } from 'react-native'; + +import { SendbirdBaseMessage, useFreshCallback } from '@sendbird/uikit-utils'; + +import type { MessageSearchProps } from '../types'; + +const MessageSearchList = ({ messages, renderMessage, flatlistProps }: MessageSearchProps['List']) => { + const renderItem: ListRenderItem = useFreshCallback(({ item }) => + renderMessage({ message: item }), + ); + + return ; +}; + +export default MessageSearchList; diff --git a/packages/uikit-react-native/src/domain/messageSearch/component/MessageSearchStatusEmpty.tsx b/packages/uikit-react-native/src/domain/messageSearch/component/MessageSearchStatusEmpty.tsx new file mode 100644 index 000000000..64efc0f0e --- /dev/null +++ b/packages/uikit-react-native/src/domain/messageSearch/component/MessageSearchStatusEmpty.tsx @@ -0,0 +1,15 @@ +import React from 'react'; + +import { Box } from '@sendbird/uikit-react-native-foundation'; + +import TypedPlaceholder from '../../../components/TypedPlaceholder'; + +const MessageSearchStatusEmpty = () => { + return ( + + + + ); +}; + +export default MessageSearchStatusEmpty; diff --git a/packages/uikit-react-native/src/domain/messageSearch/component/MessageSearchStatusError.tsx b/packages/uikit-react-native/src/domain/messageSearch/component/MessageSearchStatusError.tsx new file mode 100644 index 000000000..cf1b38071 --- /dev/null +++ b/packages/uikit-react-native/src/domain/messageSearch/component/MessageSearchStatusError.tsx @@ -0,0 +1,16 @@ +import React from 'react'; + +import { Box } from '@sendbird/uikit-react-native-foundation'; + +import TypedPlaceholder from '../../../components/TypedPlaceholder'; +import type { MessageSearchModule } from '../types'; + +const MessageSearchStatusError: MessageSearchModule['StatusError'] = ({ onPressRetry }) => { + return ( + + + + ); +}; + +export default MessageSearchStatusError; diff --git a/packages/uikit-react-native/src/domain/messageSearch/component/MessageSearchStatusLoading.tsx b/packages/uikit-react-native/src/domain/messageSearch/component/MessageSearchStatusLoading.tsx new file mode 100644 index 000000000..21649548d --- /dev/null +++ b/packages/uikit-react-native/src/domain/messageSearch/component/MessageSearchStatusLoading.tsx @@ -0,0 +1,15 @@ +import React from 'react'; + +import { Box } from '@sendbird/uikit-react-native-foundation'; + +import TypedPlaceholder from '../../../components/TypedPlaceholder'; + +const MessageSearchStatusLoading = () => { + return ( + + + + ); +}; + +export default MessageSearchStatusLoading; diff --git a/packages/uikit-react-native/src/domain/messageSearch/index.ts b/packages/uikit-react-native/src/domain/messageSearch/index.ts new file mode 100644 index 000000000..4c1f31669 --- /dev/null +++ b/packages/uikit-react-native/src/domain/messageSearch/index.ts @@ -0,0 +1,7 @@ +export { default as MessageSearchHeader } from './component/MessageSearchHeader'; +export { default as MessageSearchList } from './component/MessageSearchList'; +export { default as MessageSearchStatusLoading } from './component/MessageSearchStatusLoading'; +export { default as MessageSearchStatusEmpty } from './component/MessageSearchStatusEmpty'; +export { default as MessageSearchStatusError } from './component/MessageSearchStatusError'; +export { default as createMessageSearchModule } from './module/createMessageSearchModule'; +export { MessageSearchContextsProvider, MessageSearchContexts } from './module/moduleContext'; diff --git a/packages/uikit-react-native/src/domain/messageSearch/module/createMessageSearchModule.tsx b/packages/uikit-react-native/src/domain/messageSearch/module/createMessageSearchModule.tsx new file mode 100644 index 000000000..683bb045f --- /dev/null +++ b/packages/uikit-react-native/src/domain/messageSearch/module/createMessageSearchModule.tsx @@ -0,0 +1,21 @@ +import MessageSearchHeader from '../component/MessageSearchHeader'; +import MessageSearchList from '../component/MessageSearchList'; +import MessageSearchStatusEmpty from '../component/MessageSearchStatusEmpty'; +import MessageSearchStatusError from '../component/MessageSearchStatusError'; +import MessageSearchStatusLoading from '../component/MessageSearchStatusLoading'; +import type { MessageSearchModule } from '../types'; +import { MessageSearchContextsProvider } from './moduleContext'; + +const createMessageSearchModule = ({ + Header = MessageSearchHeader, + List = MessageSearchList, + StatusError = MessageSearchStatusError, + StatusLoading = MessageSearchStatusLoading, + StatusEmpty = MessageSearchStatusEmpty, + Provider = MessageSearchContextsProvider, + ...module +}: Partial = {}): MessageSearchModule => { + return { Header, List, Provider, StatusError, StatusEmpty, StatusLoading, ...module }; +}; + +export default createMessageSearchModule; diff --git a/packages/uikit-react-native/src/domain/messageSearch/module/moduleContext.tsx b/packages/uikit-react-native/src/domain/messageSearch/module/moduleContext.tsx new file mode 100644 index 000000000..d848ac410 --- /dev/null +++ b/packages/uikit-react-native/src/domain/messageSearch/module/moduleContext.tsx @@ -0,0 +1,16 @@ +import React, { createContext } from 'react'; + +import ProviderLayout from '../../../components/ProviderLayout'; +import type { MessageSearchContextsType, MessageSearchModule } from '../types'; + +export const MessageSearchContexts: MessageSearchContextsType = { + Fragment: createContext(null), +}; + +export const MessageSearchContextsProvider: MessageSearchModule['Provider'] = ({ children }) => { + return ( + + {children} + + ); +}; diff --git a/packages/uikit-react-native/src/domain/messageSearch/types.ts b/packages/uikit-react-native/src/domain/messageSearch/types.ts new file mode 100644 index 000000000..449525f57 --- /dev/null +++ b/packages/uikit-react-native/src/domain/messageSearch/types.ts @@ -0,0 +1,48 @@ +import type React from 'react'; +import type { FlatListProps } from 'react-native'; + +import type { SendbirdBaseMessage, SendbirdGroupChannel, SendbirdMessageSearchQuery } from '@sendbird/uikit-utils'; + +import type { CommonComponent } from '../../types'; + +export type MessageSearchProps = { + Fragment: { + channel: SendbirdGroupChannel; + onPressHeaderLeft: MessageSearchProps['Header']['onPressHeaderLeft']; + onPressMessage: (params: { channel: SendbirdGroupChannel; message: SendbirdBaseMessage }) => void; + queryCreator?: () => SendbirdMessageSearchQuery; + }; + Header: { + keyword: string; + onChangeKeyword: (value: string) => void; + onPressHeaderLeft: () => void; + onPressHeaderRight: () => void; + }; + List: { + messages: SendbirdBaseMessage[]; + renderMessage: (props: { message: SendbirdBaseMessage; onPress?: () => void }) => React.ReactElement | null; + flatlistProps?: Partial>; + }; + StatusError: { + onPressRetry: () => void; + }; +}; + +/** + * Internal context for MessageSearch + * For example, the developer can create a custom header + * with getting data from the domain context + * */ +export type MessageSearchContextsType = { + Fragment: React.Context; +}; +export interface MessageSearchModule { + Provider: CommonComponent; + Header: CommonComponent; + List: CommonComponent; + StatusEmpty: CommonComponent; + StatusLoading: CommonComponent; + StatusError: CommonComponent; +} + +export type MessageSearchFragment = CommonComponent; diff --git a/packages/uikit-react-native/src/domain/openChannel/component/OpenChannelMessageList.tsx b/packages/uikit-react-native/src/domain/openChannel/component/OpenChannelMessageList.tsx index 73ab9b497..c2f0e43c7 100644 --- a/packages/uikit-react-native/src/domain/openChannel/component/OpenChannelMessageList.tsx +++ b/packages/uikit-react-native/src/domain/openChannel/component/OpenChannelMessageList.tsx @@ -22,7 +22,7 @@ const OpenChannelMessageList = (props: OpenChannelProps['MessageList']) => { }); useEffect(() => { - subscribe(({ type }) => { + return subscribe(({ type }) => { switch (type) { case 'MESSAGES_RECEIVED': { scrollToBottom(false); diff --git a/packages/uikit-react-native/src/fragments/createGroupChannelFragment.tsx b/packages/uikit-react-native/src/fragments/createGroupChannelFragment.tsx index 93f464d86..e693e77db 100644 --- a/packages/uikit-react-native/src/fragments/createGroupChannelFragment.tsx +++ b/packages/uikit-react-native/src/fragments/createGroupChannelFragment.tsx @@ -1,4 +1,4 @@ -import React, { useMemo, useState } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import { useGroupChannelMessages } from '@sendbird/uikit-chat-hooks'; import { @@ -30,6 +30,7 @@ const createGroupChannelFragment = (initModule?: Partial): G const GroupChannelModule = createGroupChannelModule(initModule); return ({ + searchItem, renderNewMessagesButton = (props) => , renderScrollToBottomButton = (props) => , renderMessage, @@ -71,6 +72,7 @@ const createGroupChannelFragment = (initModule?: Partial): G updateUserMessage, resendMessage, deleteMessage, + resetWithStartingPoint, } = useGroupChannelMessages(sdk, channel, currentUser?.userId, { collectionCreator, queryCreator, @@ -81,7 +83,7 @@ const createGroupChannelFragment = (initModule?: Partial): G onMessagesReceived(messages) { groupChannelPubSub.publish({ type: 'MESSAGES_RECEIVED', data: { messages } }); }, - // startingPoint: 1681828275410, //1681828362945 + startingPoint: searchItem?.startingPoint, }); const _renderMessage: GroupChannelProps['MessageList']['renderMessage'] = useFreshCallback((props) => { @@ -98,6 +100,11 @@ const createGroupChannelFragment = (initModule?: Partial): G [flatListProps], ); + const onResetMessageList = useCallback( + (callback?: () => void) => resetWithStartingPoint(Number.MAX_SAFE_INTEGER, callback), + [], + ); + const onPending = (message: SendbirdFileMessage | SendbirdUserMessage) => { groupChannelPubSub.publish({ type: 'MESSAGE_SENT_PENDING', data: { message } }); }; @@ -175,10 +182,15 @@ const createGroupChannelFragment = (initModule?: Partial): G enableTypingIndicator={enableTypingIndicator} keyboardAvoidOffset={keyboardAvoidOffset} > - + Boolean(searchItem)} + onPressHeaderLeft={onPressHeaderLeft} + onPressHeaderRight={onPressHeaderRight} + /> }> diff --git a/packages/uikit-react-native/src/fragments/createMessageSearchFragment.tsx b/packages/uikit-react-native/src/fragments/createMessageSearchFragment.tsx new file mode 100644 index 000000000..9d055e60e --- /dev/null +++ b/packages/uikit-react-native/src/fragments/createMessageSearchFragment.tsx @@ -0,0 +1,118 @@ +import React, { useState } from 'react'; + +import type { MessageSearchOrder } from '@sendbird/chat/message'; +import { + NOOP, + SendbirdBaseMessage, + SendbirdChatSDK, + SendbirdMessageSearchQuery, + getDefaultMessageSearchQueryParams, + useFreshCallback, + useSafeAreaPadding, +} from '@sendbird/uikit-utils'; + +import { MessageSearchListItem } from '../components/MessageSearchListItem'; +import StatusComposition from '../components/StatusComposition'; +import { createMessageSearchModule } from '../domain/messageSearch'; +import type { MessageSearchFragment, MessageSearchModule, MessageSearchProps } from '../domain/messageSearch/types'; +import { useSendbirdChat } from '../hooks/useContext'; + +type DefaultMessageSearchQueryParams = { + keyword: string; + channelUrl: string; + messageTimestampFrom: number; + order: MessageSearchOrder; +}; + +type SearchQueryOptions = DefaultMessageSearchQueryParams & { + queryCreator?: (params: DefaultMessageSearchQueryParams) => SendbirdMessageSearchQuery; +}; + +function createMessageSearchQuery(sdk: SendbirdChatSDK, options: SearchQueryOptions) { + if (options.queryCreator) return options.queryCreator(options); + return sdk.createMessageSearchQuery(options); +} + +const createMessageSearchFragment = (initModule?: Partial): MessageSearchFragment => { + const MessageSearchModule = createMessageSearchModule(initModule); + + return ({ onPressHeaderLeft = NOOP, onPressMessage, channel, queryCreator }) => { + const { sdk } = useSendbirdChat(); + const padding = useSafeAreaPadding(['left', 'right', 'bottom']); + + const [query, setQuery] = useState(); + const [keyword, setKeyword] = useState(''); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [searchResults, setSearchResults] = useState([]); + + const buildQuery = () => { + const params = getDefaultMessageSearchQueryParams(channel, keyword); + return createMessageSearchQuery(sdk, { ...params, queryCreator }); + }; + + const search = useFreshCallback(async () => { + const query = buildQuery(); + setQuery(query); + setLoading(true); + setError(null); + + try { + const result = await query.next(); + setSearchResults(result); + } catch (err) { + setError(err); + } finally { + setLoading(false); + } + }); + + const next = useFreshCallback(async () => { + if (query?.hasNext) { + try { + const result = await query.next(); + setSearchResults((prev) => [...prev, ...result]); + } catch (err) { + setError(err); + } + } + }); + + const renderMessage: MessageSearchProps['List']['renderMessage'] = useFreshCallback(({ message }) => { + return ; + }); + + return ( + + + } + error={Boolean(error)} + ErrorComponent={} + > + {query && ( + + )} + + + ); + }; +}; + +export default createMessageSearchFragment; diff --git a/packages/uikit-react-native/src/index.ts b/packages/uikit-react-native/src/index.ts index ddd3adc54..f88f4d55b 100644 --- a/packages/uikit-react-native/src/index.ts +++ b/packages/uikit-react-native/src/index.ts @@ -32,6 +32,7 @@ export { default as createGroupChannelRegisterOperatorFragment } from './fragmen export { default as createGroupChannelMutedMembersFragment } from './fragments/createGroupChannelMutedMembersFragment'; export { default as createGroupChannelBannedUsersFragment } from './fragments/createGroupChannelBannedUsersFragment'; export { default as createGroupChannelNotificationsFragment } from './fragments/createGroupChannelNotificationsFragment'; +export { default as createMessageSearchFragment } from './fragments/createMessageSearchFragment'; /** Fragments - open channels **/ export { default as createOpenChannelFragment } from './fragments/createOpenChannelFragment'; diff --git a/packages/uikit-react-native/src/localization/StringSet.type.ts b/packages/uikit-react-native/src/localization/StringSet.type.ts index 8b39cf626..550b5cd6a 100644 --- a/packages/uikit-react-native/src/localization/StringSet.type.ts +++ b/packages/uikit-react-native/src/localization/StringSet.type.ts @@ -1,6 +1,7 @@ import type { Locale } from 'date-fns'; import type { + SendbirdBaseMessage, SendbirdFileMessage, SendbirdGroupChannel, SendbirdMember, @@ -173,6 +174,7 @@ export interface StringSet { /** GroupChannelSettings > Menu */ MENU_MODERATION: string; MENU_MEMBERS: string; + MENU_SEARCH: string; MENU_LEAVE_CHANNEL: string; MENU_NOTIFICATION: string; MENU_NOTIFICATION_LABEL_ON: string; @@ -267,6 +269,16 @@ export interface StringSet { HEADER_TITLE: string; HEADER_RIGHT: (params: { selectedUsers: Array }) => string; }; + MESSAGE_SEARCH: { + /** MessageSearch > Header */ + HEADER_INPUT_PLACEHOLDER: string; + HEADER_RIGHT: string; + + /** MessageSearch > Message preview */ + MESSAGE_PREVIEW_TITLE: (message: SendbirdBaseMessage) => string; + MESSAGE_PREVIEW_TITLE_CAPTION: (message: SendbirdBaseMessage, locale?: Locale) => string; + MESSAGE_PREVIEW_BODY: (message: SendbirdBaseMessage) => string; + }; // UI LABELS: { PERMISSION_APP_NAME: string; diff --git a/packages/uikit-react-native/src/localization/createBaseStringSet.ts b/packages/uikit-react-native/src/localization/createBaseStringSet.ts index 8a75a19bc..8f009760f 100644 --- a/packages/uikit-react-native/src/localization/createBaseStringSet.ts +++ b/packages/uikit-react-native/src/localization/createBaseStringSet.ts @@ -6,6 +6,9 @@ import { getGroupChannelLastMessage, getGroupChannelPreviewTime, getGroupChannelTitle, + getMessagePreviewBody, + getMessagePreviewTime, + getMessagePreviewTitle, getMessageTimeFormat, getOpenChannelParticipants, getOpenChannelTitle, @@ -166,6 +169,7 @@ export const createBaseStringSet = ({ dateLocale, overrides }: StringSetCreateOp HEADER_RIGHT: 'Edit', MENU_MODERATION: 'Moderation', MENU_MEMBERS: 'Members', + MENU_SEARCH: 'Search in channel', MENU_LEAVE_CHANNEL: 'Leave channel', MENU_NOTIFICATION: 'Notifications', MENU_NOTIFICATION_LABEL_ON: 'On', @@ -266,6 +270,15 @@ export const createBaseStringSet = ({ dateLocale, overrides }: StringSetCreateOp }, ...overrides?.GROUP_CHANNEL_INVITE, }, + MESSAGE_SEARCH: { + HEADER_INPUT_PLACEHOLDER: 'Search', + HEADER_RIGHT: 'Search', + MESSAGE_PREVIEW_TITLE: (message) => getMessagePreviewTitle(message), + MESSAGE_PREVIEW_BODY: (message) => getMessagePreviewBody(message), + MESSAGE_PREVIEW_TITLE_CAPTION: (message, locale) => { + return getMessagePreviewTime(message.createdAt, locale ?? dateLocale); + }, + }, LABELS: { PERMISSION_APP_NAME: 'Application', PERMISSION_CAMERA: 'camera', diff --git a/packages/uikit-utils/src/index.ts b/packages/uikit-utils/src/index.ts index 2f2629e3b..3845c4399 100644 --- a/packages/uikit-utils/src/index.ts +++ b/packages/uikit-utils/src/index.ts @@ -41,52 +41,4 @@ export const SBErrorMessage = { 'To manage your access control settings, you can turn on or off each setting on Sendbird Dashboard.', }; -export type { - UserStruct, - FilterByValueType, - UnionToIntersection, - OmittedValues, - PartialDeep, - Optional, - ContextValue, - OnBeforeHandler, - SendbirdMessage, - SendbirdChatSDK, - SendbirdChannel, - SendbirdBaseChannel, - SendbirdGroupChannel, - SendbirdOpenChannel, - SendbirdUser, - SendbirdMember, - SendbirdBaseMessage, - SendbirdUserMessage, - SendbirdFileMessage, - SendbirdAdminMessage, - SendbirdGroupChannelCreateParams, - SendbirdFileMessageCreateParams, - SendbirdUserMessageCreateParams, - SendbirdError, - SendbirdMessageCollection, - SendbirdGroupChannelCollection, - NotificationFiles, - NotificationMentionedUsers, - NotificationTranslations, - PartialNullable, - SendbirdDataPayload, - SendbirdPreviousMessageListQuery, - SendbirdSendableMessage, - SendbirdOpenChannelListQuery, - SendbirdGroupChannelListQuery, - SendbirdGroupChannelUpdateParams, - SendbirdUserMessageUpdateParams, - SendbirdFileMessageUpdateParams, - SendbirdUserUpdateParams, - SendbirdRestrictedUser, - SendbirdReaction, - SendbirdEmojiContainer, - SendbirdEmoji, - SendbirdEmojiCategory, - SendbirdOpenChannelUpdateParams, - SendbirdOpenChannelCreateParams, - SendbirdParticipant, -} from './types'; +export * from './types'; diff --git a/packages/uikit-utils/src/sendbird/message.ts b/packages/uikit-utils/src/sendbird/message.ts index 266ed6f06..ea0ad8b41 100644 --- a/packages/uikit-utils/src/sendbird/message.ts +++ b/packages/uikit-utils/src/sendbird/message.ts @@ -1,9 +1,12 @@ +import { MessageSearchOrder } from '@sendbird/chat/message'; + import { getFileExtension, getFileType } from '../shared/file'; import type { SendbirdBaseChannel, SendbirdBaseMessage, SendbirdDataPayload, SendbirdFileMessage, + SendbirdGroupChannel, SendbirdMessage, SendbirdReaction, SendbirdSendableMessage, @@ -173,3 +176,12 @@ export function getMessageType(message: SendbirdMessage): MessageType { return 'unknown'; } + +export function getDefaultMessageSearchQueryParams(channel: SendbirdGroupChannel, keyword: string) { + return { + keyword, + channelUrl: channel.url, + messageTimestampFrom: Math.max(channel.joinedAt, channel.invitedAt), + order: MessageSearchOrder.TIMESTAMP, + }; +} diff --git a/packages/uikit-utils/src/types.ts b/packages/uikit-utils/src/types.ts index 7f421199f..c8f58aca6 100644 --- a/packages/uikit-utils/src/types.ts +++ b/packages/uikit-utils/src/types.ts @@ -37,6 +37,7 @@ import type { FileMessage, FileMessageCreateParams, FileMessageUpdateParams, + MessageSearchQuery, PreviousMessageListQuery, Reaction, UserMessage, @@ -115,6 +116,7 @@ export type SendbirdGroupChannelListQuery = GroupChannelListQuery; export type SendbirdOpenChannelListQuery = OpenChannelListQuery; export type SendbirdMessageCollection = MessageCollection; export type SendbirdPreviousMessageListQuery = PreviousMessageListQuery; +export type SendbirdMessageSearchQuery = MessageSearchQuery; export type SendbirdError = SBError; diff --git a/packages/uikit-utils/src/ui-format/common.ts b/packages/uikit-utils/src/ui-format/common.ts index 0859c1403..49cd8e843 100644 --- a/packages/uikit-utils/src/ui-format/common.ts +++ b/packages/uikit-utils/src/ui-format/common.ts @@ -1,5 +1,7 @@ import type { Locale } from 'date-fns'; -import { format, isThisYear } from 'date-fns'; +import { format, isThisYear, isToday, isYesterday } from 'date-fns'; + +import type { SendbirdBaseMessage } from '../types'; type TruncateMode = 'head' | 'mid' | 'tail'; type TruncateOption = { mode: TruncateMode; maxLen: number; separator: string }; @@ -73,3 +75,53 @@ export const getDateSeparatorFormat = (date: Date, locale?: Locale): string => { export const getMessageTimeFormat = (date: Date, locale?: Locale): string => { return format(date, 'p', { locale }); }; + +/** + * Message preview title text + * */ +export const getMessagePreviewTitle = (message: SendbirdBaseMessage, EMPTY_USERNAME = '(No name)') => { + if (message.isFileMessage() || message.isUserMessage()) { + return message.sender.nickname || EMPTY_USERNAME; + } + if (message.isAdminMessage()) { + return 'Admin'; + } + return EMPTY_USERNAME; +}; + +/** + * Message preview body text + * */ +export const getMessagePreviewBody = (message: SendbirdBaseMessage, EMPTY_MESSAGE = '', MAX_LEN = 15) => { + if (message.isFileMessage()) { + const extIdx = message.name.lastIndexOf('.'); + if (extIdx > -1) { + const file = message.name.slice(0, extIdx); + const ext = message.name.slice(extIdx); + return truncate(file, { maxLen: MAX_LEN }) + ext; + } + + return truncate(message.name, { maxLen: MAX_LEN }); + } + + if (message.isUserMessage()) { + return message.message ?? EMPTY_MESSAGE; + } + + if (message.isAdminMessage()) { + return message.message ?? EMPTY_MESSAGE; + } + + return EMPTY_MESSAGE; +}; + +/** + * Message preview time format + * */ +export const getMessagePreviewTime = (timestamp: number, locale?: Locale) => { + if (isToday(timestamp)) return format(timestamp, 'p', { locale }); + if (isYesterday(timestamp)) return 'Yesterday'; + if (isThisYear(timestamp)) return format(timestamp, 'MMM dd', { locale }); + + return format(timestamp, 'yyyy/MM/dd', { locale }); +}; diff --git a/packages/uikit-utils/src/ui-format/groupChannel.ts b/packages/uikit-utils/src/ui-format/groupChannel.ts index 2f9360171..d2956e2c0 100644 --- a/packages/uikit-utils/src/ui-format/groupChannel.ts +++ b/packages/uikit-utils/src/ui-format/groupChannel.ts @@ -1,8 +1,7 @@ import type { Locale } from 'date-fns'; -import { format, isThisYear, isToday, isYesterday } from 'date-fns'; import type { SendbirdGroupChannel } from '../types'; -import { truncate } from './common'; +import { getMessagePreviewBody, getMessagePreviewTime } from './common'; export const getGroupChannelTitle = ( currentUserId: string, @@ -20,37 +19,12 @@ export const getGroupChannelTitle = ( }; export const getGroupChannelPreviewTime = (channel: SendbirdGroupChannel, locale?: Locale) => { - const timestamp = channel.lastMessage?.createdAt || channel.joinedAt * 1000 || channel.createdAt; - - if (isToday(timestamp)) return format(timestamp, 'p', { locale }); - if (isYesterday(timestamp)) return 'Yesterday'; - if (isThisYear(timestamp)) return format(timestamp, 'MMM dd', { locale }); - - return format(timestamp, 'yyyy/MM/dd', { locale }); + return getMessagePreviewTime(channel.lastMessage?.createdAt || channel.joinedAt * 1000 || channel.createdAt, locale); }; export const getGroupChannelLastMessage = (channel: SendbirdGroupChannel, EMPTY_MESSAGE = '', MAX_LEN = 15) => { const message = channel.lastMessage; if (!message) return EMPTY_MESSAGE; - if (message.isFileMessage()) { - const extIdx = message.name.lastIndexOf('.'); - if (extIdx > -1) { - const file = message.name.slice(0, extIdx); - const ext = message.name.slice(extIdx); - return truncate(file, { maxLen: MAX_LEN }) + ext; - } - - return truncate(message.name, { maxLen: MAX_LEN }); - } - - if (message.isUserMessage()) { - return message.message ?? EMPTY_MESSAGE; - } - - if (message.isAdminMessage()) { - return message.message ?? EMPTY_MESSAGE; - } - - return EMPTY_MESSAGE; + return getMessagePreviewBody(message, EMPTY_MESSAGE, MAX_LEN); }; diff --git a/sample/src/App.tsx b/sample/src/App.tsx index 04c99be69..c9733d0c3 100644 --- a/sample/src/App.tsx +++ b/sample/src/App.tsx @@ -36,6 +36,7 @@ import { GroupChannelSettingsScreen, GroupChannelTabs, HomeScreen, + MessageSearchScreen, OpenChannelBannedUsersScreen, OpenChannelCreateScreen, OpenChannelLiveStreamScreen, @@ -149,6 +150,7 @@ const Navigations = () => { + {/** Open channels **/} diff --git a/sample/src/libs/navigation.ts b/sample/src/libs/navigation.ts index 024969f4c..f1b017cc7 100644 --- a/sample/src/libs/navigation.ts +++ b/sample/src/libs/navigation.ts @@ -45,6 +45,7 @@ export enum Routes { GroupChannelInvite = 'GroupChannelInvite', Settings = 'Settings', FileViewer = 'FileViewer', + MessageSearch = 'MessageSearch', } type ChannelUrlParams = { @@ -99,7 +100,10 @@ export type RouteParamsUnion = } | { route: Routes.GroupChannel; - params: ChannelUrlParams; + params: { + channelUrl: string; + searchItem?: { startingPoint: number }; + }; } | { route: Routes.GroupChannelSettings; @@ -137,6 +141,10 @@ export type RouteParamsUnion = route: Routes.GroupChannelInvite; params: ChannelUrlParams; } + | { + route: Routes.MessageSearch; + params: ChannelUrlParams; + } /** OpenChannel screens **/ | { route: Routes.OpenChannelTabs; @@ -215,6 +223,7 @@ export const navigationActions = { // navigationRef.setParams(params); navigationRef.dispatch(StackActions.replace(name, params)); } else { + // @ts-ignore navigationRef.navigate(name, params); } } diff --git a/sample/src/screens/uikit/MessageSearchScreen.tsx b/sample/src/screens/uikit/MessageSearchScreen.tsx new file mode 100644 index 000000000..1f2bcdfec --- /dev/null +++ b/sample/src/screens/uikit/MessageSearchScreen.tsx @@ -0,0 +1,31 @@ +import React from 'react'; + +import { useGroupChannel } from '@sendbird/uikit-chat-hooks'; +import { createMessageSearchFragment, useSendbirdChat } from '@sendbird/uikit-react-native'; + +import { useAppNavigation } from '../../hooks/useAppNavigation'; +import { Routes } from '../../libs/navigation'; + +const MessageSearchFragment = createMessageSearchFragment(); +const MessageSearchScreen = () => { + const { sdk } = useSendbirdChat(); + const { navigation, params } = useAppNavigation(); + + const { channel } = useGroupChannel(sdk, params.channelUrl); + if (!channel) return null; + + return ( + navigation.goBack()} + onPressMessage={({ message, channel }) => { + navigation.push(Routes.GroupChannel, { + channelUrl: channel.url, + searchItem: { startingPoint: message.createdAt }, + }); + }} + /> + ); +}; + +export default MessageSearchScreen; diff --git a/sample/src/screens/uikit/groupChannel/GroupChannelScreen.tsx b/sample/src/screens/uikit/groupChannel/GroupChannelScreen.tsx index 8c220ddfe..db161c14c 100644 --- a/sample/src/screens/uikit/groupChannel/GroupChannelScreen.tsx +++ b/sample/src/screens/uikit/groupChannel/GroupChannelScreen.tsx @@ -77,6 +77,7 @@ const GroupChannelScreen = () => { return ( { // Navigate to media viewer navigation.navigate(Routes.FileViewer, { diff --git a/sample/src/screens/uikit/groupChannel/GroupChannelSettingsScreen.tsx b/sample/src/screens/uikit/groupChannel/GroupChannelSettingsScreen.tsx index 71052f846..02c358ad9 100644 --- a/sample/src/screens/uikit/groupChannel/GroupChannelSettingsScreen.tsx +++ b/sample/src/screens/uikit/groupChannel/GroupChannelSettingsScreen.tsx @@ -29,6 +29,10 @@ const GroupChannelSettingsScreen = () => { // Navigate to group channel members navigation.push(Routes.GroupChannelMembers, params); }} + onPressMenuSearchInChannel={() => { + // Navigate to group channel search + navigation.push(Routes.MessageSearch, params); + }} onPressMenuLeaveChannel={() => { // Navigate to group channel list navigation.navigate(Routes.GroupChannelList); diff --git a/sample/src/screens/uikit/index.ts b/sample/src/screens/uikit/index.ts index d3d5b48fb..4ebe8e305 100644 --- a/sample/src/screens/uikit/index.ts +++ b/sample/src/screens/uikit/index.ts @@ -2,3 +2,4 @@ export * from './groupChannel'; export * from './openChannel'; export { default as FileViewerScreen } from './FileViewerScreen'; export { default as SettingsScreen } from './SettingsScreen'; +export { default as MessageSearchScreen } from './MessageSearchScreen';