Skip to content

fix/mark-as-unread-qa #248

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 26 commits into from
Jul 18, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
c240f1d
chore: refine unread message visibility logic and remove unnecessary …
OnestarLee Jul 8, 2025
7b7cb8f
chore: enhance unread message handling and visibility logic in GroupC…
OnestarLee Jul 9, 2025
a227237
refactor: show 99+ when new message count reaches 100 or higher
OnestarLee Jul 9, 2025
10d6390
Update packages/uikit-react-native/src/localization/createBaseStringS…
OnestarLee Jul 10, 2025
5fc7c18
chore: streamline unread message detection logic in GroupChannelMessa…
OnestarLee Jul 10, 2025
e3707e6
Merge remote-tracking branch 'origin/fix/mark-as-unread-qa' into fix/…
OnestarLee Jul 10, 2025
0108698
chore: rename UnreadMessage.ts to UnreadMessage.tsx and update string…
OnestarLee Jul 14, 2025
b89c18d
chore: replace ref with state for new line existence in GroupChannelM…
OnestarLee Jul 17, 2025
b258e09
chore: adjust scrolling behavior and update reply type options in Gro…
OnestarLee Jul 17, 2025
5338bc8
chore: correct variable reference for previous message in GroupChanne…
OnestarLee Jul 17, 2025
0428ef7
chore: enable debug mode for Firebase App Distribution in deploy lane
OnestarLee Jul 17, 2025
b14fe0b
chore: update CA certificates in config.yml
OnestarLee Jul 17, 2025
6e2a7d6
chore: update gem dependencies in Gemfile.lock
OnestarLee Jul 17, 2025
8d4f2d6
chore: update Ruby version to 3.2.2 in config.yml
OnestarLee Jul 17, 2025
55b1e8c
chore: update Ruby version to 3.0.6 in config.yml
OnestarLee Jul 17, 2025
1ef2159
chore: update Ruby version to 3.1.0 in config.yml
OnestarLee Jul 17, 2025
ba6b884
Revert "chore: update Ruby version to 3.1.0 in config.yml"
OnestarLee Jul 17, 2025
ec62d01
Revert "chore: update Ruby version to 3.0.6 in config.yml"
OnestarLee Jul 17, 2025
c39c8b8
Revert "chore: update Ruby version to 3.2.2 in config.yml"
OnestarLee Jul 17, 2025
13214ed
Revert "chore: update gem dependencies in Gemfile.lock"
OnestarLee Jul 17, 2025
fc8ed41
Revert "chore: update CA certificates in config.yml"
OnestarLee Jul 17, 2025
e59a8ef
chore: update Android Docker image version to 2025.04.1-node
OnestarLee Jul 17, 2025
ae0e2dc
chore: update android fastlane plugin
bang9 Jul 17, 2025
1686a71
chore: add setup for trusted certificates in config.yml
OnestarLee Jul 17, 2025
eeef021
chore: add setup for trusted certificates in config.yml
OnestarLee Jul 17, 2025
18f61d6
chore: update confirmAndMarkAsRead to optionally skip unread count check
OnestarLee Jul 17, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 >>
Expand All @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ const GroupChannelMessageNewLine = ({ shouldRenderNewLine }: Props) => {
const styles = StyleSheet.create({
container: {
width: '100%',
height: 12,
flexDirection: 'row',
alignItems: 'center',
marginBottom: 16,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<SendbirdMessage[]>();
const hasUserMarkedAsUnreadRef = useRef(false);
const [unreadFirstMessage, setUnreadFirstMessage] = useState<SendbirdMessage | undefined>(undefined);

const updateHasSeenNewLine = useCallback(
(hasSeenNewLine: boolean) => {
Expand All @@ -46,10 +51,6 @@ const GroupChannelMessageList = (props: GroupChannelProps['MessageList']) => {
[props.onNewLineSeenChange],
);

const viewableMessages = useRef<SendbirdMessage[]>();
const hasUserMarkedAsUnreadRef = useRef(false);
const [unreadFirstMessage, setUnreadFirstMessage] = useState<SendbirdMessage | undefined>(undefined);

const updateHasUserMarkedAsUnread = useCallback(
(hasUserMarkedAsUnread: boolean) => {
if (hasUserMarkedAsUnreadRef.current !== hasUserMarkedAsUnread) {
Expand Down Expand Up @@ -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 });
Expand All @@ -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);
Expand Down Expand Up @@ -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<UnreadMessagesFloatingProps>();
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,
Expand All @@ -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) {
Expand Down Expand Up @@ -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;
}
}
Expand Down Expand Up @@ -326,6 +350,7 @@ const GroupChannelMessageList = (props: GroupChannelProps['MessageList']) => {
<ChannelMessageList
{...props}
ref={flatListRef}
onScrolledAwayFromBottom={onScrolledAwayFromBottom}
onReplyMessage={setMessageToReply}
onReplyInThreadMessage={setMessageToReply}
onEditMessage={setMessageToEdit}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,13 +92,12 @@ const createGroupChannelFragment = (initModule?: Partial<GroupChannelModule>): 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) => {
Expand All @@ -114,9 +113,9 @@ const createGroupChannelFragment = (initModule?: Partial<GroupChannelModule>): G
if (
!scrolledAwayFromBottom &&
!hasUserMarkedAsUnreadRef.current &&
(hasSeenNewLineRef.current || !isNewLineExistInChannelRef.current)
(hasSeenNewLineRef.current || !isNewLineExistInChannel)
) {
confirmAndMarkAsRead(channels);
confirmAndMarkAsRead(channels, true);
}
} else {
confirmAndMarkAsRead(channels);
Expand Down Expand Up @@ -154,7 +153,7 @@ const createGroupChannelFragment = (initModule?: Partial<GroupChannelModule>): 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' });
}
}
Expand Down Expand Up @@ -281,7 +280,7 @@ const createGroupChannelFragment = (initModule?: Partial<GroupChannelModule>): 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]);
}
}
Expand Down Expand Up @@ -331,7 +330,7 @@ const createGroupChannelFragment = (initModule?: Partial<GroupChannelModule>): G
onPressMediaMessage={_onPressMediaMessage}
flatListComponent={flatListComponent}
flatListProps={memoizedFlatListProps}
isNewLineExistInChannel={isNewLineExistInChannelRef.current}
isNewLineExistInChannel={isNewLineExistInChannel}
onNewLineSeenChange={onNewLineSeenChange}
onUserMarkedAsUnreadChange={onUserMarkedAsUnreadChange}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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)',
Expand Down
7 changes: 5 additions & 2 deletions packages/uikit-utils/src/sendbird/channel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
};

Expand Down
10 changes: 8 additions & 2 deletions sample/android/Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion sample/android/fastlane/Fastfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 2 additions & 2 deletions sample/src/context/uikitLocalConfigs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down