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/docs-validation/2_features/UnreadMessage.ts b/docs-validation/2_features/UnreadMessage.tsx similarity index 93% rename from docs-validation/2_features/UnreadMessage.ts rename to docs-validation/2_features/UnreadMessage.tsx index e562f5a78..516e0fb41 100644 --- a/docs-validation/2_features/UnreadMessage.ts +++ b/docs-validation/2_features/UnreadMessage.tsx @@ -5,16 +5,18 @@ import type { StringSet } from '@sendbird/uikit-react-native'; * {@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; diff --git a/packages/uikit-react-native/src/components/GroupChannelMessageRenderer/GroupChannelMessageNewLine.tsx b/packages/uikit-react-native/src/components/GroupChannelMessageRenderer/GroupChannelMessageNewLine.tsx index 7abf9c287..d94fdd5f6 100644 --- a/packages/uikit-react-native/src/components/GroupChannelMessageRenderer/GroupChannelMessageNewLine.tsx +++ b/packages/uikit-react-native/src/components/GroupChannelMessageRenderer/GroupChannelMessageNewLine.tsx @@ -29,7 +29,6 @@ const GroupChannelMessageNewLine = ({ shouldRenderNewLine }: Props) => { const styles = StyleSheet.create({ container: { width: '100%', - height: 12, flexDirection: 'row', alignItems: 'center', marginBottom: 16, 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 852b2ae3f..6b5656636 100644 --- a/packages/uikit-react-native/src/domain/groupChannel/component/GroupChannelMessageList.tsx +++ b/packages/uikit-react-native/src/domain/groupChannel/component/GroupChannelMessageList.tsx @@ -34,7 +34,12 @@ const GroupChannelMessageList = (props: GroupChannelProps['MessageList']) => { 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) => { @@ -46,10 +51,6 @@ const GroupChannelMessageList = (props: GroupChannelProps['MessageList']) => { [props.onNewLineSeenChange], ); - const viewableMessages = useRef(); - const hasUserMarkedAsUnreadRef = useRef(false); - const [unreadFirstMessage, setUnreadFirstMessage] = useState(undefined); - const updateHasUserMarkedAsUnread = useCallback( (hasUserMarkedAsUnread: boolean) => { if (hasUserMarkedAsUnreadRef.current !== hasUserMarkedAsUnread) { @@ -84,13 +85,18 @@ 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 }); @@ -110,7 +116,7 @@ const GroupChannelMessageList = (props: GroupChannelProps['MessageList']) => { return null; } - const prevMessage = props.messages[prevMessageIndex]; + const prevMessage = messages[prevMessageIndex]; if (prevMessage) { if (prevMessage.silent) { return getPrevNonSilentMessage(messages, prevMessageIndex + 1); @@ -203,12 +209,22 @@ const GroupChannelMessageList = (props: GroupChannelProps['MessageList']) => { [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 && - !!props.isNewLineExistInChannel && + !canAutoMarkAsRead && + isNewLineExistInChannelRef.current && 0 < props.channel.unreadMessageCount && !isNewLineInViewportRef.current, onPressClose: onPressUnreadMessagesFloatingCloseButton, @@ -221,7 +237,11 @@ const GroupChannelMessageList = (props: GroupChannelProps['MessageList']) => { useEffect(() => { updateUnreadMessagesFloatingProps(); - }, [props.isNewLineExistInChannel, sbOptions.uikit.groupChannel.channel.enableMarkAsUnread]); + }, [ + isNewLineExistInChannelRef.current, + props.channel.unreadMessageCount, + sbOptions.uikit.groupChannel.channel.enableMarkAsUnread, + ]); useGroupChannelHandler(sdk, { onReactionUpdated(channel, event) { @@ -274,9 +294,13 @@ const GroupChannelMessageList = (props: GroupChannelProps['MessageList']) => { 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; } } @@ -326,6 +350,7 @@ const GroupChannelMessageList = (props: GroupChannelProps['MessageList']) => { ): G } }); - const isNewLineExistInChannelRef = useRef(false); + const [isNewLineExistInChannel, setIsNewLineExistInChannel] = useState(false); const hasSeenNewLineRef = useRef(false); const hasUserMarkedAsUnreadRef = useRef(false); useEffect(() => { - isNewLineExistInChannelRef.current = - channel.myLastRead < (channel.lastMessage?.createdAt ?? Number.MIN_SAFE_INTEGER); + setIsNewLineExistInChannel(channel.myLastRead < (channel.lastMessage?.createdAt ?? Number.MIN_SAFE_INTEGER)); }, [channel.url]); const onNewLineSeenChange = useFreshCallback((hasSeenNewLine: boolean) => { @@ -114,9 +113,9 @@ const createGroupChannelFragment = (initModule?: Partial): G if ( !scrolledAwayFromBottom && !hasUserMarkedAsUnreadRef.current && - (hasSeenNewLineRef.current || !isNewLineExistInChannelRef.current) + (hasSeenNewLineRef.current || !isNewLineExistInChannel) ) { - confirmAndMarkAsRead(channels); + confirmAndMarkAsRead(channels, true); } } else { confirmAndMarkAsRead(channels); @@ -154,7 +153,7 @@ const createGroupChannelFragment = (initModule?: Partial): G } } else if (ctx?.source === GroupChannelEventSource.EVENT_CHANNEL_UNREAD) { if (ctx.userIds.includes(currentUser?.userId ?? '')) { - isNewLineExistInChannelRef.current = true; + setIsNewLineExistInChannel(true); groupChannelPubSub.publish({ type: 'ON_MARKED_AS_UNREAD_BY_CURRENT_USER' }); } } @@ -281,7 +280,7 @@ const createGroupChannelFragment = (initModule?: Partial): G if (!value) { resetNewMessages(); if (sbOptions.uikit.groupChannel.channel.enableMarkAsUnread) { - if (!hasUserMarkedAsUnreadRef.current && (hasSeenNewLineRef.current || !isNewLineExistInChannelRef.current)) { + if (!hasUserMarkedAsUnreadRef.current && (hasSeenNewLineRef.current || !isNewLineExistInChannel)) { confirmAndMarkAsRead([channel]); } } @@ -331,7 +330,7 @@ const createGroupChannelFragment = (initModule?: Partial): G onPressMediaMessage={_onPressMediaMessage} flatListComponent={flatListComponent} flatListProps={memoizedFlatListProps} - isNewLineExistInChannel={isNewLineExistInChannelRef.current} + isNewLineExistInChannel={isNewLineExistInChannel} onNewLineSeenChange={onNewLineSeenChange} onUserMarkedAsUnreadChange={onUserMarkedAsUnreadChange} /> diff --git a/packages/uikit-react-native/src/localization/createBaseStringSet.ts b/packages/uikit-react-native/src/localization/createBaseStringSet.ts index 97b4bfb49..082a2bbaf 100644 --- a/packages/uikit-react-native/src/localization/createBaseStringSet.ts +++ b/packages/uikit-react-native/src/localization/createBaseStringSet.ts @@ -117,10 +117,15 @@ 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 === 1 ? `${newMessages.length} new message` : `${newMessages.length} new messages`, - LIST_FLOATING_UNREAD_MSG: (unreadMessageCount) => - unreadMessageCount === 1 ? `${unreadMessageCount} unread message` : `${unreadMessageCount} unread 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, @@ -135,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)', 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;