From 2df8c20c14330e517d7f97dcc45099407afe47bb Mon Sep 17 00:00:00 2001 From: OnestarLee Date: Wed, 21 May 2025 11:02:58 +0900 Subject: [PATCH 1/3] feat: add MarkAsUnread feature to GroupChannel --- .circleci/config.yml | 8 +- .github/workflows/publish-sample.yml | 30 ++- docs-validation/2_features/UnreadMessage.tsx | 35 +++ docs-validation/package.json | 2 +- package.json | 2 +- packages/uikit-chat-hooks/package.json | 2 +- .../src/assets/icon/icon-mark-as-unread.png | Bin 0 -> 405 bytes .../assets/icon/icon-mark-as-unread@2x.png | Bin 0 -> 682 bytes .../assets/icon/icon-mark-as-unread@3x.png | Bin 0 -> 938 bytes .../src/assets/icon/index.ts | 1 + packages/uikit-react-native/package.json | 5 +- .../components/ChannelMessageList/index.tsx | 76 ++++++- .../GroupChannelMessageNewLine.tsx | 45 ++++ .../GroupChannelMessageRenderer/index.tsx | 5 + .../src/components/UnreadMessagesFloating.tsx | 56 +++++ .../component/GroupChannelMessageList.tsx | 209 +++++++++++++++++- .../src/domain/groupChannel/types.ts | 15 ++ .../fragments/createGroupChannelFragment.tsx | 71 +++++- .../src/localization/StringSet.type.ts | 3 + .../src/localization/createBaseStringSet.ts | 20 +- packages/uikit-testing-tools/package.json | 2 +- packages/uikit-utils/package.json | 2 +- packages/uikit-utils/src/sendbird/channel.ts | 7 +- sample/android/Gemfile.lock | 10 +- sample/android/fastlane/Fastfile | 2 +- sample/src/context/uikitLocalConfigs.tsx | 4 +- yarn.lock | 18 +- 27 files changed, 579 insertions(+), 51 deletions(-) create mode 100644 docs-validation/2_features/UnreadMessage.tsx create mode 100644 packages/uikit-react-native-foundation/src/assets/icon/icon-mark-as-unread.png create mode 100644 packages/uikit-react-native-foundation/src/assets/icon/icon-mark-as-unread@2x.png create mode 100644 packages/uikit-react-native-foundation/src/assets/icon/icon-mark-as-unread@3x.png create mode 100644 packages/uikit-react-native/src/components/GroupChannelMessageRenderer/GroupChannelMessageNewLine.tsx create mode 100644 packages/uikit-react-native/src/components/UnreadMessagesFloating.tsx diff --git a/.circleci/config.yml b/.circleci/config.yml index 5fd95e3c8..abd133926 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -89,7 +89,7 @@ jobs: deploy-android: docker: - - image: cimg/android:2023.10-node + - image: cimg/android:2025.04.1-node resource_class: xlarge environment: APP_VERSION: << pipeline.parameters.version >> @@ -109,6 +109,12 @@ jobs: - save_cache: *save_node_modules_base - save_cache: *save_node_modules_packages - run: *create_app_env + - run: + name: Set up trusted certificates + command: | + sudo apt-get update + sudo apt-get install -y ca-certificates + echo 'export SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt' >> $BASH_ENV - run: name: Create service-account.json environment: diff --git a/.github/workflows/publish-sample.yml b/.github/workflows/publish-sample.yml index 1040fdb45..1f99671d7 100644 --- a/.github/workflows/publish-sample.yml +++ b/.github/workflows/publish-sample.yml @@ -21,15 +21,27 @@ jobs: execute: runs-on: ubuntu-latest steps: - - run: echo "BRANCH_NAME=${GITHUB_REF#refs/heads/}" >> $GITHUB_ENV - - name: '[${{ github.event.inputs.platform }}] Deploy trigger' - if: ${{github.event.inputs.platform}} - uses: promiseofcake/circleci-trigger-action@v1 - with: - user-token: ${{ secrets.CIRCLECI_PERSONAL_API_TOKEN }} - project-slug: sendbird/sendbird-uikit-react-native - branch: ${{ env.BRANCH_NAME }} - payload: '{"platform": "${{ github.event.inputs.platform }}", "version": "${{ github.event.inputs.version }}" }' + - name: Set BRANCH_NAME env + run: echo "BRANCH_NAME=${GITHUB_REF#refs/heads/}" >> $GITHUB_ENV + + - name: '[${{ github.event.inputs.platform }}] Deploy trigger (CircleCI REST API)' + if: ${{ github.event.inputs.platform }} + env: + CIRCLECI_PERSONAL_API_TOKEN: ${{ secrets.CIRCLECI_PERSONAL_API_TOKEN }} + BRANCH_NAME: ${{ env.BRANCH_NAME }} + PLATFORM: ${{ github.event.inputs.platform }} + VERSION: ${{ github.event.inputs.version }} + run: | + curl -u ${CIRCLECI_PERSONAL_API_TOKEN}: \ + -X POST "https://circleci.com/api/v2/project/gh/sendbird/sendbird-uikit-react-native/pipeline" \ + -H "Content-Type: application/json" \ + -d '{ + "branch": "'"${BRANCH_NAME}"'", + "parameters": { + "platform": "'"${PLATFORM}"'", + "version": "'"${VERSION}"'" + } + }' #env: # CACHE_NODE_MODULES_PATH: | diff --git a/docs-validation/2_features/UnreadMessage.tsx b/docs-validation/2_features/UnreadMessage.tsx new file mode 100644 index 000000000..516e0fb41 --- /dev/null +++ b/docs-validation/2_features/UnreadMessage.tsx @@ -0,0 +1,35 @@ +import type { StringSet } from '@sendbird/uikit-react-native'; + +/** + * String resource + * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/features/reactions} + * */ +function _stringResource(str: StringSet) { + str.GROUP_CHANNEL.LIST_NEW_LINE; + str.GROUP_CHANNEL.LIST_FLOATING_UNREAD_MSG; + str.LABELS.CHANNEL_MESSAGE_MARK_AS_UNREAD; +} +/** ------------------ **/ +// interface StringSet { +// GROUP_CHANNEL: { +// LIST_NEW_LINE: string; +// LIST_FLOATING_UNREAD_MSG: (unreadMessageCount: number) => string; +// }; +// } + +// interface StringSet { +// LABELS: { +// CHANNEL_MESSAGE_MARK_AS_UNREAD: string; +// }; +// } + +/** ------------------ **/ + +/** + * Icon resource + * {@link https://sendbird.com/docs/chat/uikit/v3/react-native/features/reactions#mark-as-unread-icon-resource} + * */ +import { Icon } from '@sendbird/uikit-react-native-foundation'; + +Icon.Assets['mark-as-unread'] = require('your_icons/icon-mark-as-unread.png'); +/** ------------------ **/ diff --git a/docs-validation/package.json b/docs-validation/package.json index 6fb398083..406bc7496 100644 --- a/docs-validation/package.json +++ b/docs-validation/package.json @@ -15,7 +15,7 @@ "@react-native-firebase/messaging": "^14.7.0", "@react-navigation/native": "^6.1.17", "@react-navigation/native-stack": "^6.10.0", - "@sendbird/chat": "^4.16.0", + "@sendbird/chat": "^4.19.2", "date-fns": "^4.1.0", "react": "18.2.0", "react-native": "0.74.3", diff --git a/package.json b/package.json index 87f924626..17c74b2a5 100644 --- a/package.json +++ b/package.json @@ -98,7 +98,7 @@ ] }, "resolutions": { - "@sendbird/chat": "4.16.5", + "@sendbird/chat": "4.19.2", "@types/react": "^18" } } diff --git a/packages/uikit-chat-hooks/package.json b/packages/uikit-chat-hooks/package.json index 4a76dd9a7..fe5030304 100644 --- a/packages/uikit-chat-hooks/package.json +++ b/packages/uikit-chat-hooks/package.json @@ -55,7 +55,7 @@ "typescript": "5.2.2" }, "peerDependencies": { - "@sendbird/chat": "^4.16.0", + "@sendbird/chat": "^4.19.2", "react": ">=16.13.1" }, "react-native-builder-bob": { diff --git a/packages/uikit-react-native-foundation/src/assets/icon/icon-mark-as-unread.png b/packages/uikit-react-native-foundation/src/assets/icon/icon-mark-as-unread.png new file mode 100644 index 0000000000000000000000000000000000000000..441ad1d2a3c815ee5b7e60ddfc20ba9c0de2afb0 GIT binary patch literal 405 zcmV;G0c!q7A-A$d@!ceHa7E)4W3XHmq`KnCB1+l+!dm>7i;fp5^|!WK&?3u_ z(;1;E6q&Ll#13hI-^!#&aQ+!S#{eGBixA)lp^Q6^8ZHFr5h8TCP6${6!m`y0qOZ%| zgQ;JsfU%5+pzsm^B^5icLuk;&^tOMVl|K#QGl6=(Wt@aMMYBKsdGSMFYCRsTFG1;F zWc(0zY&^pr&3;b?@dX*bs2Hb1k@(I+iyt;3Fgdv&A&4KaC-UC^BIE9p(!Zg;laixW z&+R;tl>%JHssP&OAD|)W*^tAk00eynO+kkQJOyn&n3CEZ1SI)0 zgt01MPWw4+s~>B3Pa*t;f0xXU>d~Bjm5AoMFoS5EZ`oZ{d5mx~Bu10YOHZ9c^kK1<}Gp={+ zGX7dS}+e20Y9T=|O> zB<=qieEGGjC~ueLG~v$mw-wkYGW|MT^k~}TV7{jB+rA%Cj@=m;prf8TIhOsa)qC52 zJ+*xM!Zs-NbVm0~4Ou4oW%>D=a+a(9))QPa$nXj-?!#^30xW9R!PW+ys37d>L{ zYndVA)s?b_k#(}j<}FRvTr*h>JF{0W?~lAE7P2}us^nR3WR&CQi^q1|@Zr`=o)q@$ z>AU6`KUi27KCA8A=%xE&-u3(sv)e@vN}T_(|MU#jrD7$EKUoHKh^^r`cHwE8m;IUe z-gq8~6L%)HOwjFFsJ;G(%?j(Pd%c0Kfa&7R=zQY+552u2^&KgaKQU}vC6 zu=0G7>DH^(t$g}t?K(%@u+&G*J+qdcNRHnV;#s|J(znk~oozTpv@+|b>t@EccNvLq z{FHL(=T?a{q1)^ax31f>N%zFDr0xtc+Z@5U>Q{^X0)8hHE&C+3e#(rH+u`rFDsW!^ zct}Zg3jZdDCY=ci+$txSI0-PDf}29jKF(d{QO%)L?)LE^&)2ZIEn64(su;a}=e|xX z#LJRRqGR7apXR*B8{!_vEq(G}=Yqt`m#3!ZIfhPd-v8I&>PejyPDM>iYs45^ssX5JLUMMNoHE}3|33M;~v%dml&HAIGPl^&g7SKx@a8J>d^saEe20l KKbLh*2~7Zf!=upv literal 0 HcmV?d00001 diff --git a/packages/uikit-react-native-foundation/src/assets/icon/index.ts b/packages/uikit-react-native-foundation/src/assets/icon/index.ts index 98997ed7f..29662a380 100644 --- a/packages/uikit-react-native-foundation/src/assets/icon/index.ts +++ b/packages/uikit-react-native-foundation/src/assets/icon/index.ts @@ -35,6 +35,7 @@ const IconAssets = { 'gif': require('./icon-gif.png'), 'info': require('./icon-info.png'), 'leave': require('./icon-leave.png'), + 'mark-as-unread': require('./icon-mark-as-unread.png'), 'members': require('./icon-members.png'), 'message': require('./icon-message.png'), 'moderation': require('./icon-moderation.png'), diff --git a/packages/uikit-react-native/package.json b/packages/uikit-react-native/package.json index 64554ada9..825a9f96c 100644 --- a/packages/uikit-react-native/package.json +++ b/packages/uikit-react-native/package.json @@ -62,7 +62,7 @@ "@openspacelabs/react-native-zoomable-view": "^2.1.5", "@sendbird/uikit-chat-hooks": "3.9.6", "@sendbird/uikit-react-native-foundation": "3.9.6", - "@sendbird/uikit-tools": "0.0.7", + "@sendbird/uikit-tools": "0.0.10", "@sendbird/uikit-utils": "3.9.6" }, "devDependencies": { @@ -111,7 +111,8 @@ "@react-native-community/netinfo": ">=9.3.0", "@react-native-documents/picker": ">=10.0.0", "@react-native-firebase/messaging": ">=14.4.0", - "@sendbird/chat": "^4.16.0", + "@sendbird/chat": "^4.19.2", + "@sendbird/uikit-tools": ">=0.0.10", "@sendbird/react-native-scrollview-enhancer": "*", "date-fns": ">=2.28.0", "expo-av": ">=12.0.4", diff --git a/packages/uikit-react-native/src/components/ChannelMessageList/index.tsx b/packages/uikit-react-native/src/components/ChannelMessageList/index.tsx index 9bcc8477e..1aeac53a0 100644 --- a/packages/uikit-react-native/src/components/ChannelMessageList/index.tsx +++ b/packages/uikit-react-native/src/components/ChannelMessageList/index.tsx @@ -40,6 +40,7 @@ import { import SBUUtils from '../../libs/SBUUtils'; import ChatFlatList from '../ChatFlatList'; import { ReactionAddons } from '../ReactionAddons'; +import { UnreadMessagesFloatingProps } from '../UnreadMessagesFloating'; type PressActions = { onPress?: () => void; onLongPress?: () => void; bottomSheetItem?: BottomSheetItem }; type HandleableMessage = SendbirdUserMessage | SendbirdFileMessage; @@ -50,6 +51,7 @@ export type ChannelMessageListProps Promise; onPressParentMessage?: (parentMessage: SendbirdMessage, childMessage: HandleableMessage) => void; onPressMediaMessage?: (message: SendbirdFileMessage, deleteMessage: () => Promise, uri: string) => void; + onPressMarkAsUnreadMessage?: (message: HandleableMessage) => void; renderMessage: (props: { focused: boolean; @@ -84,14 +87,18 @@ export type ChannelMessageListProps['enableMessageGrouping']; bottomSheetItem?: BottomSheetItem; isFirstItem: boolean; + isFirstUnreadMessage?: boolean; hideParentMessage?: boolean; }) => React.ReactElement | null; renderNewMessagesButton: | null | ((props: { visible: boolean; onPress: () => void; newMessages: SendbirdMessage[] }) => React.ReactElement | null); renderScrollToBottomButton: null | ((props: { visible: boolean; onPress: () => void }) => React.ReactElement | null); + renderUnreadMessagesFloating?: null | ((props: UnreadMessagesFloatingProps) => React.ReactElement | null); + unreadMessagesFloatingProps?: UnreadMessagesFloatingProps; flatListComponent?: React.ComponentType>; flatListProps?: Omit, 'data' | 'renderItem'>; + onViewableItemsChanged?: FlatListProps['onViewableItemsChanged']; } & { ref?: Ref> | undefined; }; @@ -108,12 +115,15 @@ const ChannelMessageList = , ref: React.ForwardedRef>, ) => { @@ -139,6 +151,7 @@ const ChannelMessageList = = useFreshCallback(({ item, index }) => { @@ -147,6 +160,7 @@ const ChannelMessageList = )} + {renderUnreadMessagesFloating && ( + + {renderUnreadMessagesFloating({ + visible: unreadMessagesFloatingProps?.visible ?? false, + onPressClose: () => unreadMessagesFloatingProps?.onPressClose(), + unreadMessageCount: unreadMessagesFloatingProps?.unreadMessageCount ?? 0, + })} + + )} , | 'channel' @@ -222,6 +249,7 @@ const useCreateMessagePressActions = ): CreateMessagePressActions => { const handlers = useSBUHandlers(); const { colors } = useUIKitTheme(); @@ -281,6 +309,10 @@ const useCreateMessagePressActions = { + onPressMarkAsUnreadMessage?.(message); + }; + const openSheetForFailedMessage = (message: HandleableMessage) => { openSheet({ sheetItems: [ @@ -323,6 +355,11 @@ const useCreateMessagePressActions = onCopyText(message), }), + markAsUnread: (message: HandleableMessage) => ({ + icon: 'mark-as-unread' as const, + title: STRINGS.LABELS.CHANNEL_MESSAGE_MARK_AS_UNREAD, + onPress: () => onMarkAsUnread(message), + }), edit: (message: HandleableMessage) => ({ icon: 'edit' as const, title: STRINGS.LABELS.CHANNEL_MESSAGE_EDIT, @@ -356,9 +393,19 @@ const useCreateMessagePressActions = { + if (!shouldRenderNewLine) return null; + + const { STRINGS } = useLocalization(); + const { colors } = useUIKitTheme(); + + return ( + + + + {STRINGS.GROUP_CHANNEL.LIST_NEW_LINE} + + + + ); +}; + +const styles = StyleSheet.create({ + container: { + width: '100%', + flexDirection: 'row', + alignItems: 'center', + marginBottom: 16, + }, + line: { + flex: 1, + height: 1, + }, + label: { + marginHorizontal: 4, + }, +}); + +export default React.memo(GroupChannelMessageNewLine); diff --git a/packages/uikit-react-native/src/components/GroupChannelMessageRenderer/index.tsx b/packages/uikit-react-native/src/components/GroupChannelMessageRenderer/index.tsx index 0bf1d38e5..3072f8d5c 100644 --- a/packages/uikit-react-native/src/components/GroupChannelMessageRenderer/index.tsx +++ b/packages/uikit-react-native/src/components/GroupChannelMessageRenderer/index.tsx @@ -30,6 +30,7 @@ import { TypingIndicatorType } from '../../types'; import { ReactionAddons } from '../ReactionAddons'; import GroupChannelMessageDateSeparator from './GroupChannelMessageDateSeparator'; import GroupChannelMessageFocusAnimation from './GroupChannelMessageFocusAnimation'; +import GroupChannelMessageNewLine from './GroupChannelMessageNewLine'; import GroupChannelMessageOutgoingStatus from './GroupChannelMessageOutgoingStatus'; import GroupChannelMessageParentMessage from './GroupChannelMessageParentMessage'; import GroupChannelMessageReplyInfo from './GroupChannelMessageReplyInfo'; @@ -46,6 +47,7 @@ const GroupChannelMessageRenderer: GroupChannelProps['Fragment']['renderMessage' focused, prevMessage, nextMessage, + isFirstUnreadMessage, hideParentMessage, }) => { const handlers = useSBUHandlers(); @@ -310,9 +312,12 @@ const GroupChannelMessageRenderer: GroupChannelProps['Fragment']['renderMessage' } }); + const shouldRenderNewLine = sbOptions.uikit.groupChannel.channel.enableMarkAsUnread && isFirstUnreadMessage; + return ( + {renderMessage()} ); diff --git a/packages/uikit-react-native/src/components/UnreadMessagesFloating.tsx b/packages/uikit-react-native/src/components/UnreadMessagesFloating.tsx new file mode 100644 index 000000000..0b5dd773b --- /dev/null +++ b/packages/uikit-react-native/src/components/UnreadMessagesFloating.tsx @@ -0,0 +1,56 @@ +import React from 'react'; +import { Platform, TouchableOpacity, View } from 'react-native'; + +import { Icon, Text, createStyleSheet, useUIKitTheme } from '@sendbird/uikit-react-native-foundation'; + +import { useLocalization } from '../hooks/useContext'; + +export type UnreadMessagesFloatingProps = { + unreadMessageCount: number; + visible: boolean; + onPressClose: () => void; +}; +const UnreadMessagesFloating = ({ unreadMessageCount, visible, onPressClose }: UnreadMessagesFloatingProps) => { + const { STRINGS } = useLocalization(); + const { select, palette, colors } = useUIKitTheme(); + if (unreadMessageCount <= 0 || !visible) return null; + return ( + + + {STRINGS.GROUP_CHANNEL.LIST_FLOATING_UNREAD_MSG(unreadMessageCount)} + + + + + + ); +}; + +const styles = createStyleSheet({ + container: { + flexDirection: 'row', + justifyContent: 'flex-start', + alignItems: 'center', + paddingHorizontal: 16, + paddingVertical: 12, + borderRadius: 20, + ...Platform.select({ + android: { + elevation: 3, + }, + ios: { + shadowColor: 'black', + shadowRadius: 3, + shadowOffset: { width: 0, height: 4 }, + shadowOpacity: 0.08, + }, + }), + }, +}); + +export default React.memo(UnreadMessagesFloating); 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 c1244f86a..6b5656636 100644 --- a/packages/uikit-react-native/src/domain/groupChannel/component/GroupChannelMessageList.tsx +++ b/packages/uikit-react-native/src/domain/groupChannel/component/GroupChannelMessageList.tsx @@ -1,16 +1,19 @@ -import React, { useContext, useEffect } from 'react'; +import type { ViewToken } from '@react-native/virtualized-lists'; +import React, { useCallback, useContext, useEffect, useRef, useState } from 'react'; import { useToast } from '@sendbird/uikit-react-native-foundation'; import { useGroupChannelHandler } from '@sendbird/uikit-tools'; import { SendbirdMessage, SendbirdSendableMessage, + confirmAndMarkAsRead, isDifferentChannel, useFreshCallback, useIsFirstMount, } from '@sendbird/uikit-utils'; import ChannelMessageList from '../../../components/ChannelMessageList'; +import { UnreadMessagesFloatingProps } from '../../../components/UnreadMessagesFloating'; import { MESSAGE_FOCUS_ANIMATION_DELAY, MESSAGE_SEARCH_SAFE_SCROLL_DELAY } from '../../../constants'; import { GroupChannelFragmentOptionsPubSubContextPayload } from '../../../contexts/SendbirdChatCtx'; import { useLocalization, useSendbirdChat } from '../../../hooks/useContext'; @@ -22,13 +25,42 @@ const GroupChannelMessageList = (props: GroupChannelProps['MessageList']) => { const { STRINGS } = useLocalization(); const { sdk, sbOptions, groupChannelFragmentOptions } = useSendbirdChat(); const { setMessageToEdit, setMessageToReply } = useContext(GroupChannelContexts.Fragment); - const { subscribe } = useContext(GroupChannelContexts.PubSub); + const groupChannelPubSub = useContext(GroupChannelContexts.PubSub); const { flatListRef, lazyScrollToBottom, lazyScrollToIndex, onPressReplyMessageInThread } = useContext( GroupChannelContexts.MessageList, ); const isFirstMount = useIsFirstMount(); + const hasSeenNewLineRef = useRef(false); + const isNewLineInViewportRef = useRef(false); + const isNewLineExistInChannelRef = useRef(false); + const scrolledAwayFromBottomRef = useRef(false); + const [isVisibleUnreadMessageFloating, setIsVisibleUnreadMessageFloating] = useState(false); + const viewableMessages = useRef(); + const hasUserMarkedAsUnreadRef = useRef(false); + const [unreadFirstMessage, setUnreadFirstMessage] = useState(undefined); + + const updateHasSeenNewLine = useCallback( + (hasSeenNewLine: boolean) => { + if (hasSeenNewLineRef.current !== hasSeenNewLine) { + hasSeenNewLineRef.current = hasSeenNewLine; + props.onNewLineSeenChange?.(hasSeenNewLine); + } + }, + [props.onNewLineSeenChange], + ); + + const updateHasUserMarkedAsUnread = useCallback( + (hasUserMarkedAsUnread: boolean) => { + if (hasUserMarkedAsUnreadRef.current !== hasUserMarkedAsUnread) { + hasUserMarkedAsUnreadRef.current = hasUserMarkedAsUnread; + props.onUserMarkedAsUnreadChange?.(hasUserMarkedAsUnread); + } + }, + [props.onUserMarkedAsUnreadChange], + ); + const scrollToMessageWithCreatedAt = useFreshCallback( (createdAt: number, focusAnimated: boolean, timeout: number): boolean => { const foundMessageIndex = props.messages.findIndex((it) => it.createdAt === createdAt); @@ -53,19 +85,164 @@ const GroupChannelMessageList = (props: GroupChannelProps['MessageList']) => { }, ); + const onScrolledAwayFromBottom = useFreshCallback((value: boolean) => { + scrolledAwayFromBottomRef.current = value; + props.onScrolledAwayFromBottom(value); + }); + const scrollToBottom = useFreshCallback(async (animated = false) => { if (props.hasNext()) { props.onUpdateSearchItem(undefined); - props.onScrolledAwayFromBottom(false); + onScrolledAwayFromBottom(false); await props.onResetMessageList().catch((_) => {}); - props.onScrolledAwayFromBottom(false); + onScrolledAwayFromBottom(false); lazyScrollToBottom({ animated }); } else { lazyScrollToBottom({ animated }); } }); + const onPressUnreadMessagesFloatingCloseButton = useCallback(() => { + updateHasSeenNewLine(true); + updateHasUserMarkedAsUnread(false); + props.resetNewMessages?.(); + confirmAndMarkAsRead([props.channel]); + }, [updateHasSeenNewLine, updateHasUserMarkedAsUnread, props.channel.url, props.resetNewMessages]); + + const getPrevNonSilentMessage = useCallback( + (messages: SendbirdMessage[], prevMessageIndex: number): SendbirdMessage | null => { + if (messages.length <= prevMessageIndex) { + return null; + } + + const prevMessage = messages[prevMessageIndex]; + if (prevMessage) { + if (prevMessage.silent) { + return getPrevNonSilentMessage(messages, prevMessageIndex + 1); + } else { + return prevMessage; + } + } + return null; + }, + [], + ); + + const findUnreadFirstMessage = useFreshCallback((isNewLineExistInChannel: boolean) => { + if (!sbOptions.uikit.groupChannel.channel.enableMarkAsUnread || !isNewLineExistInChannel) { + return; + } + + return props.messages.find((msg, index) => { + if (msg.silent) { + return false; + } + + const isMarkedAsUnreadMessage = props.channel.myLastRead === msg.createdAt - 1; + if (isMarkedAsUnreadMessage) { + return true; + } + + const prevNonSilentMessage = getPrevNonSilentMessage(props.messages, index + 1); + const hasNoPreviousAndNoPrevMessage = !props.hasPrevious?.() && prevNonSilentMessage == null; + const prevMessageIsRead = + prevNonSilentMessage != null && prevNonSilentMessage.createdAt <= props.channel.myLastRead; + const isMessageUnread = props.channel.myLastRead < msg.createdAt; + return (hasNoPreviousAndNoPrevMessage || prevMessageIsRead) && isMessageUnread; + }); + }); + + useEffect(() => { + if (!unreadFirstMessage) { + const foundUnreadFirstMessage = findUnreadFirstMessage(props.isNewLineExistInChannel ?? false); + if (foundUnreadFirstMessage) { + processNewLineVisibility(foundUnreadFirstMessage); + setUnreadFirstMessage(foundUnreadFirstMessage); + } + } + }, [props.messages, props.channel.myLastRead, sbOptions.uikit.groupChannel.channel.enableMarkAsUnread]); + + const processNewLineVisibility = useFreshCallback((unreadFirstMsg: SendbirdMessage | undefined) => { + const isNewLineInViewport = !!viewableMessages.current?.some( + (message) => message.messageId === unreadFirstMsg?.messageId, + ); + + if (isNewLineInViewportRef.current !== isNewLineInViewport) { + isNewLineInViewportRef.current = isNewLineInViewport; + updateUnreadMessagesFloatingProps(); + if (!isNewLineInViewport || hasSeenNewLineRef.current) { + return; + } + + updateHasSeenNewLine(true); + if (hasUserMarkedAsUnreadRef.current) { + return; + } + + if (0 < props.newMessages.length) { + props.channel.markAsUnread(props.newMessages[0]); + } else { + props.channel.markAsRead(); + } + } + }); + + const onViewableItemsChanged = useFreshCallback( + async (info: { viewableItems: Array>; changed: Array> }) => { + if (!sbOptions.uikit.groupChannel.channel.enableMarkAsUnread) { + return; + } + + viewableMessages.current = info.viewableItems.filter((token) => token.item).map((token) => token.item); + processNewLineVisibility(unreadFirstMessage); + }, + ); + + const onPressMarkAsUnreadMessage = useCallback( + async (message: SendbirdMessage) => { + if (sbOptions.uikit.groupChannel.channel.enableMarkAsUnread && message) { + await props.channel.markAsUnread(message); + updateHasUserMarkedAsUnread(true); + } + }, + [sbOptions.uikit.groupChannel.channel.enableMarkAsUnread, updateHasUserMarkedAsUnread], + ); + + useEffect(() => { + isNewLineExistInChannelRef.current = !!props.isNewLineExistInChannel && !!viewableMessages.current; + }, [props.isNewLineExistInChannel, viewableMessages.current]); + + const unreadMessagesFloatingPropsRef = useRef(); + const updateUnreadMessagesFloatingProps = useFreshCallback(() => { + const canAutoMarkAsRead = + !scrolledAwayFromBottomRef.current && + !hasUserMarkedAsUnreadRef.current && + (hasSeenNewLineRef.current || !isNewLineExistInChannelRef.current); + + unreadMessagesFloatingPropsRef.current = { + visible: + sbOptions.uikit.groupChannel.channel.enableMarkAsUnread && + !canAutoMarkAsRead && + isNewLineExistInChannelRef.current && + 0 < props.channel.unreadMessageCount && + !isNewLineInViewportRef.current, + onPressClose: onPressUnreadMessagesFloatingCloseButton, + unreadMessageCount: props.channel.unreadMessageCount, + }; + if (isVisibleUnreadMessageFloating !== unreadMessagesFloatingPropsRef.current.visible) { + setIsVisibleUnreadMessageFloating(unreadMessagesFloatingPropsRef.current.visible); + } + }); + + useEffect(() => { + updateUnreadMessagesFloatingProps(); + }, [ + isNewLineExistInChannelRef.current, + props.channel.unreadMessageCount, + sbOptions.uikit.groupChannel.channel.enableMarkAsUnread, + ]); + useGroupChannelHandler(sdk, { onReactionUpdated(channel, event) { if (isDifferentChannel(channel, props.channel)) return; @@ -76,10 +253,13 @@ const GroupChannelMessageList = (props: GroupChannelProps['MessageList']) => { lazyScrollToBottom({ animated: true, timeout: 250 }); } }, + onChannelChanged(channel) { + if (isDifferentChannel(channel, props.channel)) return; + }, }); useEffect(() => { - return subscribe(({ type, data }) => { + return groupChannelPubSub.subscribe(({ type, data }) => { switch (type) { case 'TYPING_BUBBLE_RENDERED': case 'MESSAGES_RECEIVED': { @@ -109,6 +289,20 @@ const GroupChannelMessageList = (props: GroupChannelProps['MessageList']) => { scrollToBottom(false); break; } + case 'ON_MARKED_AS_READ_BY_CURRENT_USER': { + updateUnreadMessagesFloatingProps(); + break; + } + case 'ON_MARKED_AS_UNREAD_BY_CURRENT_USER': { + isNewLineExistInChannelRef.current = true; + const foundUnreadFirstMessage = findUnreadFirstMessage(true); + processNewLineVisibility(foundUnreadFirstMessage); + setUnreadFirstMessage(foundUnreadFirstMessage); + if (!props.scrolledAwayFromBottom) { + scrollToBottom(true); + } + break; + } } }); }, [props.scrolledAwayFromBottom]); @@ -156,12 +350,17 @@ const GroupChannelMessageList = (props: GroupChannelProps['MessageList']) => { ); }; diff --git a/packages/uikit-react-native/src/domain/groupChannel/types.ts b/packages/uikit-react-native/src/domain/groupChannel/types.ts index ea9c2a824..961398e89 100644 --- a/packages/uikit-react-native/src/domain/groupChannel/types.ts +++ b/packages/uikit-react-native/src/domain/groupChannel/types.ts @@ -41,6 +41,7 @@ export interface GroupChannelProps { renderMessage?: GroupChannelProps['MessageList']['renderMessage']; renderNewMessagesButton?: GroupChannelProps['MessageList']['renderNewMessagesButton']; renderScrollToBottomButton?: GroupChannelProps['MessageList']['renderScrollToBottomButton']; + renderUnreadMessagesFloating?: GroupChannelProps['MessageList']['renderUnreadMessagesFloating']; enableTypingIndicator?: GroupChannelProps['Provider']['enableTypingIndicator']; enableMessageGrouping?: GroupChannelProps['MessageList']['enableMessageGrouping']; @@ -85,6 +86,7 @@ export interface GroupChannelProps { | 'renderMessage' | 'renderNewMessagesButton' | 'renderScrollToBottomButton' + | 'renderUnreadMessagesFloating' | 'flatListComponent' | 'flatListProps' | 'hasNext' @@ -95,6 +97,11 @@ export interface GroupChannelProps { // Changing the search item will trigger the focus animation on messages. onUpdateSearchItem: (searchItem?: GroupChannelProps['MessageList']['searchItem']) => void; + hasPrevious?: () => boolean; + resetNewMessages?: () => void; + isNewLineExistInChannel?: boolean; + onNewLineSeenChange?: (hasSeenNewLine: boolean) => void; + onUserMarkedAsUnreadChange?: (hasUserMarkedAsUnread: boolean) => void; }; Input: PickPartial< ChannelInputProps, @@ -208,4 +215,12 @@ export type GroupChannelPubSubContextPayload = | { type: 'TYPING_BUBBLE_RENDERED'; data?: undefined; + } + | { + type: 'ON_MARKED_AS_READ_BY_CURRENT_USER'; + data?: undefined; + } + | { + type: 'ON_MARKED_AS_UNREAD_BY_CURRENT_USER'; + data?: undefined; }; diff --git a/packages/uikit-react-native/src/fragments/createGroupChannelFragment.tsx b/packages/uikit-react-native/src/fragments/createGroupChannelFragment.tsx index 366b875c2..5afbe201a 100644 --- a/packages/uikit-react-native/src/fragments/createGroupChannelFragment.tsx +++ b/packages/uikit-react-native/src/fragments/createGroupChannelFragment.tsx @@ -1,6 +1,11 @@ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { MessageCollection, MessageFilter } from '@sendbird/chat/groupChannel'; +import { + type GroupChannel, + GroupChannelEventSource, + MessageCollection, + MessageFilter, +} from '@sendbird/chat/groupChannel'; import { ReplyType } from '@sendbird/chat/message'; import { Box, useToast } from '@sendbird/uikit-react-native-foundation'; import { useGroupChannelMessages } from '@sendbird/uikit-tools'; @@ -27,6 +32,7 @@ import GroupChannelMessageRenderer, { import NewMessagesButton from '../components/NewMessagesButton'; import ScrollToBottomButton from '../components/ScrollToBottomButton'; import StatusComposition from '../components/StatusComposition'; +import UnreadMessagesFloating from '../components/UnreadMessagesFloating'; import createGroupChannelModule from '../domain/groupChannel/module/createGroupChannelModule'; import type { GroupChannelFragment, @@ -43,6 +49,7 @@ const createGroupChannelFragment = (initModule?: Partial): G return ({ searchItem, + renderUnreadMessagesFloating = (props) => , renderNewMessagesButton = (props) => , renderScrollToBottomButton = (props) => , renderMessage, @@ -85,6 +92,36 @@ const createGroupChannelFragment = (initModule?: Partial): G } }); + const [isNewLineExistInChannel, setIsNewLineExistInChannel] = useState(false); + const hasSeenNewLineRef = useRef(false); + const hasUserMarkedAsUnreadRef = useRef(false); + + useEffect(() => { + setIsNewLineExistInChannel(channel.myLastRead < (channel.lastMessage?.createdAt ?? Number.MIN_SAFE_INTEGER)); + }, [channel.url]); + + const onNewLineSeenChange = useFreshCallback((hasSeenNewLine: boolean) => { + hasSeenNewLineRef.current = hasSeenNewLine; + }); + + const onUserMarkedAsUnreadChange = useFreshCallback((hasUserMarkedAsUnread: boolean) => { + hasUserMarkedAsUnreadRef.current = hasUserMarkedAsUnread; + }); + + const markAsRead = useFreshCallback((channels: GroupChannel[]) => { + if (sbOptions.uikit.groupChannel.channel.enableMarkAsUnread) { + if ( + !scrolledAwayFromBottom && + !hasUserMarkedAsUnreadRef.current && + (hasSeenNewLineRef.current || !isNewLineExistInChannel) + ) { + confirmAndMarkAsRead(channels, true); + } + } else { + confirmAndMarkAsRead(channels); + } + }); + const { loading, messages, @@ -93,6 +130,7 @@ const createGroupChannelFragment = (initModule?: Partial): G loadNext, loadPrevious, hasNext, + hasPrevious, sendFileMessage, sendUserMessage, updateFileMessage, @@ -108,11 +146,23 @@ const createGroupChannelFragment = (initModule?: Partial): G onMessagesUpdated(messages) { groupChannelPubSub.publish({ type: 'MESSAGES_UPDATED', data: { messages } }); }, + onChannelUpdated(_, ctx) { + if (ctx?.source === GroupChannelEventSource.EVENT_CHANNEL_READ) { + if (ctx.userIds.includes(currentUser?.userId ?? '')) { + groupChannelPubSub.publish({ type: 'ON_MARKED_AS_READ_BY_CURRENT_USER' }); + } + } else if (ctx?.source === GroupChannelEventSource.EVENT_CHANNEL_UNREAD) { + if (ctx.userIds.includes(currentUser?.userId ?? '')) { + setIsNewLineExistInChannel(true); + groupChannelPubSub.publish({ type: 'ON_MARKED_AS_UNREAD_BY_CURRENT_USER' }); + } + } + }, onChannelDeleted, onCurrentUserBanned: onChannelDeleted, collectionCreator: getCollectionCreator(channel, messageListQueryParams, collectionCreator), sortComparator, - markAsRead: confirmAndMarkAsRead, + markAsRead: markAsRead, replyType, startingPoint: internalSearchItem?.startingPoint, }); @@ -227,7 +277,14 @@ const createGroupChannelFragment = (initModule?: Partial): G }, ); const onScrolledAwayFromBottom = useFreshCallback((value: boolean) => { - if (!value) resetNewMessages(); + if (!value) { + resetNewMessages(); + if (sbOptions.uikit.groupChannel.channel.enableMarkAsUnread) { + if (!hasUserMarkedAsUnreadRef.current && (hasSeenNewLineRef.current || !isNewLineExistInChannel)) { + confirmAndMarkAsRead([channel]); + } + } + } setScrolledAwayFromBottom(value); }); @@ -261,15 +318,21 @@ const createGroupChannelFragment = (initModule?: Partial): G onTopReached={loadPrevious} onBottomReached={loadNext} hasNext={hasNext} + hasPrevious={hasPrevious} + resetNewMessages={resetNewMessages} scrolledAwayFromBottom={scrolledAwayFromBottom} onScrolledAwayFromBottom={onScrolledAwayFromBottom} renderNewMessagesButton={renderNewMessagesButton} renderScrollToBottomButton={renderScrollToBottomButton} + renderUnreadMessagesFloating={renderUnreadMessagesFloating} onResendFailedMessage={resendMessage} onDeleteMessage={deleteMessage} onPressMediaMessage={_onPressMediaMessage} flatListComponent={flatListComponent} flatListProps={memoizedFlatListProps} + isNewLineExistInChannel={isNewLineExistInChannel} + onNewLineSeenChange={onNewLineSeenChange} + onUserMarkedAsUnreadChange={onUserMarkedAsUnreadChange} /> List */ LIST_DATE_SEPARATOR: (date: Date, locale?: Locale) => string; LIST_BUTTON_NEW_MSG: (newMessages: SendbirdMessage[]) => string; + LIST_FLOATING_UNREAD_MSG: (unreadMessageCount: number) => string; + LIST_NEW_LINE: string; /** GroupChannel > Message bubble */ MESSAGE_BUBBLE_TIME: (message: SendbirdMessage, locale?: Locale) => string; @@ -312,6 +314,7 @@ export interface StringSet { CHANNEL_MESSAGE_DELETE: string; CHANNEL_MESSAGE_REPLY: string; CHANNEL_MESSAGE_THREAD: string; + CHANNEL_MESSAGE_MARK_AS_UNREAD: string; /** Channel > Message > Delete confirm **/ CHANNEL_MESSAGE_DELETE_CONFIRM_TITLE: string; CHANNEL_MESSAGE_DELETE_CONFIRM_OK: string; diff --git a/packages/uikit-react-native/src/localization/createBaseStringSet.ts b/packages/uikit-react-native/src/localization/createBaseStringSet.ts index d89122165..082a2bbaf 100644 --- a/packages/uikit-react-native/src/localization/createBaseStringSet.ts +++ b/packages/uikit-react-native/src/localization/createBaseStringSet.ts @@ -117,8 +117,16 @@ export const createBaseStringSet = ({ dateLocale, overrides }: StringSetCreateOp GROUP_CHANNEL: { HEADER_TITLE: (uid, channel) => getGroupChannelTitle(uid, channel, USER_NO_NAME, CHANNEL_NO_MEMBERS), LIST_DATE_SEPARATOR: (date, locale) => getDateSeparatorFormat(date, locale ?? dateLocale), - LIST_BUTTON_NEW_MSG: (newMessages) => `${newMessages.length} new messages`, - + LIST_BUTTON_NEW_MSG: (newMessages) => { + const count = newMessages.length; + const displayCount = count >= 100 ? '99+' : count; + return count === 1 ? `${displayCount} new message` : `${displayCount} new messages`; + }, + LIST_FLOATING_UNREAD_MSG: (unreadMessageCount) => { + const displayCount = unreadMessageCount >= 100 ? '99+' : unreadMessageCount; + return unreadMessageCount === 1 ? `${displayCount} unread message` : `${displayCount} unread messages`; + }, + LIST_NEW_LINE: 'New messages', MESSAGE_BUBBLE_TIME: (message, locale) => getMessageTimeFormat(new Date(message.createdAt), locale ?? dateLocale), MESSAGE_BUBBLE_FILE_TITLE: (message) => message.name, MESSAGE_BUBBLE_EDITED_POSTFIX: ' (edited)', @@ -132,8 +140,11 @@ export const createBaseStringSet = ({ dateLocale, overrides }: StringSetCreateOp HEADER_TITLE: 'Thread', HEADER_SUBTITLE: (uid, channel) => getGroupChannelTitle(uid, channel, USER_NO_NAME, CHANNEL_NO_MEMBERS), LIST_DATE_SEPARATOR: (date, locale) => getDateSeparatorFormat(date, locale ?? dateLocale), - LIST_BUTTON_NEW_MSG: (newMessages) => `${newMessages.length} new messages`, - + LIST_BUTTON_NEW_MSG: (newMessages) => { + const count = newMessages.length; + const displayCount = count >= 100 ? '99+' : count; + return count === 1 ? `${displayCount} new message` : `${displayCount} new messages`; + }, MESSAGE_BUBBLE_TIME: (message, locale) => getMessageTimeFormat(new Date(message.createdAt), locale ?? dateLocale), MESSAGE_BUBBLE_FILE_TITLE: (message) => message.name, MESSAGE_BUBBLE_EDITED_POSTFIX: ' (edited)', @@ -303,6 +314,7 @@ export const createBaseStringSet = ({ dateLocale, overrides }: StringSetCreateOp CHANNEL_MESSAGE_DELETE: 'Delete', CHANNEL_MESSAGE_REPLY: 'Reply', CHANNEL_MESSAGE_THREAD: 'Reply in thread', + CHANNEL_MESSAGE_MARK_AS_UNREAD: 'Mark as unread', CHANNEL_MESSAGE_DELETE_CONFIRM_TITLE: 'Delete message?', CHANNEL_MESSAGE_DELETE_CONFIRM_OK: 'Delete', CHANNEL_MESSAGE_DELETE_CONFIRM_CANCEL: 'Cancel', diff --git a/packages/uikit-testing-tools/package.json b/packages/uikit-testing-tools/package.json index 48b5c71e3..e93576e33 100644 --- a/packages/uikit-testing-tools/package.json +++ b/packages/uikit-testing-tools/package.json @@ -39,7 +39,7 @@ "access": "public" }, "devDependencies": { - "@sendbird/chat": "^4.16.0", + "@sendbird/chat": "^4.19.2", "@sendbird/uikit-utils": "3.9.6", "@types/jest": "^29.4.0", "@types/react": "*", diff --git a/packages/uikit-utils/package.json b/packages/uikit-utils/package.json index d1c365a21..76e5417ae 100644 --- a/packages/uikit-utils/package.json +++ b/packages/uikit-utils/package.json @@ -56,7 +56,7 @@ "typescript": "5.2.2" }, "peerDependencies": { - "@sendbird/chat": "^4.16.0", + "@sendbird/chat": "^4.19.2", "date-fns": ">=2.28.0", "react": ">=17.0.2", "react-native": ">=0.65.0" diff --git a/packages/uikit-utils/src/sendbird/channel.ts b/packages/uikit-utils/src/sendbird/channel.ts index 523355a89..3b159ac59 100644 --- a/packages/uikit-utils/src/sendbird/channel.ts +++ b/packages/uikit-utils/src/sendbird/channel.ts @@ -34,9 +34,12 @@ export const getOpenChannelChatAvailableState = async (channel: SendbirdOpenChan return { disabled, frozen, muted }; }; -export const confirmAndMarkAsRead = (channels: SendbirdBaseChannel[]) => { +export const confirmAndMarkAsRead = (channels: SendbirdBaseChannel[], skipUnreadCountCheck?: boolean) => { channels - .filter((it): it is SendbirdGroupChannel => it.isGroupChannel() && it.unreadMessageCount > 0) + .filter((it): it is SendbirdGroupChannel => { + if (!it.isGroupChannel()) return false; + return skipUnreadCountCheck ? true : it.unreadMessageCount > 0; + }) .forEach((it) => BufferedRequest.markAsRead.push(() => it.markAsRead(), it.url)); }; diff --git a/sample/android/Gemfile.lock b/sample/android/Gemfile.lock index b24be6923..5951e73b4 100644 --- a/sample/android/Gemfile.lock +++ b/sample/android/Gemfile.lock @@ -109,8 +109,10 @@ GEM xcodeproj (>= 1.13.0, < 2.0.0) xcpretty (~> 0.3.0) xcpretty-travis-formatter (>= 0.0.3, < 2.0.0) - fastlane-plugin-firebase_app_distribution (0.5.0) - fastlane-plugin-json (1.1.0) + fastlane-plugin-firebase_app_distribution (0.10.1) + google-apis-firebaseappdistribution_v1 (~> 0.3.0) + google-apis-firebaseappdistribution_v1alpha (~> 0.2.0) + fastlane-plugin-json (1.1.7) fastlane-plugin-versioning_android (0.1.1) gh_inspector (1.1.3) google-apis-androidpublisher_v3 (0.54.0) @@ -123,6 +125,10 @@ GEM representable (~> 3.0) retriable (>= 2.0, < 4.a) rexml + google-apis-firebaseappdistribution_v1 (0.3.0) + google-apis-core (>= 0.11.0, < 2.a) + google-apis-firebaseappdistribution_v1alpha (0.2.0) + google-apis-core (>= 0.11.0, < 2.a) google-apis-iamcredentials_v1 (0.17.0) google-apis-core (>= 0.11.0, < 2.a) google-apis-playcustomapp_v1 (0.13.0) diff --git a/sample/android/fastlane/Fastfile b/sample/android/fastlane/Fastfile index fbf01c5a9..f57f8dc89 100644 --- a/sample/android/fastlane/Fastfile +++ b/sample/android/fastlane/Fastfile @@ -14,6 +14,6 @@ platform :android do lane :deploy do android_set_version_name(gradle_file: "app/build.gradle", version_name: "#{VERSION}-#{DATE}") gradle(task: "assemble", build_type: "Release", flags: "--no-daemon") - firebase_app_distribution(groups: "sendbird, external") + firebase_app_distribution(groups: "sendbird, external", debug: true) end end diff --git a/sample/src/context/uikitLocalConfigs.tsx b/sample/src/context/uikitLocalConfigs.tsx index 191cf8a5d..c78643137 100644 --- a/sample/src/context/uikitLocalConfigs.tsx +++ b/sample/src/context/uikitLocalConfigs.tsx @@ -5,8 +5,8 @@ import { uikitLocalConfigStorage } from '../factory/mmkv'; const KEY = 'uikitOptions'; const defaultOptions = { rtl: false, - replyType: 'thread' as 'none' | 'thread' | 'quote_reply', - threadReplySelectType: 'thread' as 'thread' | 'parent', + replyType: 'quote_reply' as 'none' | 'thread' | 'quote_reply', + threadReplySelectType: 'parent' as 'thread' | 'parent', }; type ContextValue = typeof defaultOptions; diff --git a/yarn.lock b/yarn.lock index aa0124bf7..f2fc924a8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3716,15 +3716,15 @@ mkdirp "^1.0.4" rimraf "^3.0.2" -"@sendbird/chat@4.16.5", "@sendbird/chat@^4.16.0": - version "4.16.5" - resolved "https://registry.yarnpkg.com/@sendbird/chat/-/chat-4.16.5.tgz#ece60f33fed480028da79a142a75a3a5cef9a91a" - integrity sha512-fB3SgfF5mxpqOMD6Ah0anWN7wlgHyWVcGacVcX5K0pEj8K81wHCubmRNH622VDmuTnXWAU97mMZTXsEfO3YqXA== - -"@sendbird/uikit-tools@0.0.7": - version "0.0.7" - resolved "https://registry.yarnpkg.com/@sendbird/uikit-tools/-/uikit-tools-0.0.7.tgz#b8dd3c377969f59e39699f836091344b5ba7667b" - integrity sha512-MHRB2H88xsrlvgGlL0EbG/dhP2ILego/MNDdQtJqDk14YO/H1GWvqstn6ZuVxm/bQRVUzui+j6OSE1WthqACug== +"@sendbird/chat@4.19.2", "@sendbird/chat@^4.16.0", "@sendbird/chat@^4.19.2": + version "4.19.2" + resolved "https://registry.yarnpkg.com/@sendbird/chat/-/chat-4.19.2.tgz#a60346e6aaaa0d65697cfc46d619bdebb7c01999" + integrity sha512-scytefM8c9Ja+fm3oWkFHAZt7TqV2Q5EDbCJFtFmRvyRa+TYUjtHmSKpvrGZEyjjU/oBTK/JohZ1Zb2AixUANg== + +"@sendbird/uikit-tools@0.0.10": + version "0.0.10" + resolved "https://registry.yarnpkg.com/@sendbird/uikit-tools/-/uikit-tools-0.0.10.tgz#b522339426e0a6f859047ac1e4c1bf695f6dbef9" + integrity sha512-HRc7vLo2XASpbL0QtUR2r5py1pcy+1xTgIqUWxsJp8/ih+YbnrLMNz6hJgAwBEtpw5+d1fhY+ZzvWdRihQX5ew== "@sideway/address@^4.1.5": version "4.1.5" From 906eade725be3f0e9f95a72907d8cd7c22ec9a15 Mon Sep 17 00:00:00 2001 From: OnestarLee Date: Mon, 21 Jul 2025 10:46:23 +0900 Subject: [PATCH 2/3] chore: enhance touch target for close button in UnreadMessagesFloating --- .../src/components/UnreadMessagesFloating.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/uikit-react-native/src/components/UnreadMessagesFloating.tsx b/packages/uikit-react-native/src/components/UnreadMessagesFloating.tsx index 0b5dd773b..fbd84b5f8 100644 --- a/packages/uikit-react-native/src/components/UnreadMessagesFloating.tsx +++ b/packages/uikit-react-native/src/components/UnreadMessagesFloating.tsx @@ -24,7 +24,11 @@ const UnreadMessagesFloating = ({ unreadMessageCount, visible, onPressClose }: U {STRINGS.GROUP_CHANNEL.LIST_FLOATING_UNREAD_MSG(unreadMessageCount)} - + From 2af1af8a9d2db8bcd058ab4136d7dcf171a0dfbd Mon Sep 17 00:00:00 2001 From: OnestarLee Date: Tue, 22 Jul 2025 08:51:05 +0900 Subject: [PATCH 3/3] chore: rename findUnreadFirstMessage to findFirstUnreadMessage for clarity --- .../groupChannel/component/GroupChannelMessageList.tsx | 10 +++++----- sample/src/context/uikitLocalConfigs.tsx | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) 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 6b5656636..42b3c1856 100644 --- a/packages/uikit-react-native/src/domain/groupChannel/component/GroupChannelMessageList.tsx +++ b/packages/uikit-react-native/src/domain/groupChannel/component/GroupChannelMessageList.tsx @@ -129,7 +129,7 @@ const GroupChannelMessageList = (props: GroupChannelProps['MessageList']) => { [], ); - const findUnreadFirstMessage = useFreshCallback((isNewLineExistInChannel: boolean) => { + const findFirstUnreadMessage = useFreshCallback((isNewLineExistInChannel: boolean) => { if (!sbOptions.uikit.groupChannel.channel.enableMarkAsUnread || !isNewLineExistInChannel) { return; } @@ -155,7 +155,7 @@ const GroupChannelMessageList = (props: GroupChannelProps['MessageList']) => { useEffect(() => { if (!unreadFirstMessage) { - const foundUnreadFirstMessage = findUnreadFirstMessage(props.isNewLineExistInChannel ?? false); + const foundUnreadFirstMessage = findFirstUnreadMessage(props.isNewLineExistInChannel ?? false); if (foundUnreadFirstMessage) { processNewLineVisibility(foundUnreadFirstMessage); setUnreadFirstMessage(foundUnreadFirstMessage); @@ -295,9 +295,9 @@ const GroupChannelMessageList = (props: GroupChannelProps['MessageList']) => { } case 'ON_MARKED_AS_UNREAD_BY_CURRENT_USER': { isNewLineExistInChannelRef.current = true; - const foundUnreadFirstMessage = findUnreadFirstMessage(true); - processNewLineVisibility(foundUnreadFirstMessage); - setUnreadFirstMessage(foundUnreadFirstMessage); + const foundFirstUnreadMessage = findFirstUnreadMessage(true); + processNewLineVisibility(foundFirstUnreadMessage); + setUnreadFirstMessage(foundFirstUnreadMessage); if (!props.scrolledAwayFromBottom) { scrollToBottom(true); } diff --git a/sample/src/context/uikitLocalConfigs.tsx b/sample/src/context/uikitLocalConfigs.tsx index c78643137..191cf8a5d 100644 --- a/sample/src/context/uikitLocalConfigs.tsx +++ b/sample/src/context/uikitLocalConfigs.tsx @@ -5,8 +5,8 @@ import { uikitLocalConfigStorage } from '../factory/mmkv'; const KEY = 'uikitOptions'; const defaultOptions = { rtl: false, - replyType: 'quote_reply' as 'none' | 'thread' | 'quote_reply', - threadReplySelectType: 'parent' as 'thread' | 'parent', + replyType: 'thread' as 'none' | 'thread' | 'quote_reply', + threadReplySelectType: 'thread' as 'thread' | 'parent', }; type ContextValue = typeof defaultOptions;