;
+ markAsUnread?: (message: SendableMessageType) => void;
renderFileViewer: (props: { message: FileMessage; onCancel: () => void }) => React.ReactElement;
renderRemoveMessageModal?: (props: { message: EveryMessage; onCancel: () => void }) => React.ReactElement;
@@ -107,6 +109,7 @@ const MessageView = (props: MessageViewProps) => {
message,
children,
hasSeparator,
+ hasNewMessageSeparator,
chainTop,
chainBottom,
handleScroll,
@@ -132,6 +135,7 @@ const MessageView = (props: MessageViewProps) => {
updateUserMessage,
resendMessage,
deleteMessage,
+ markAsUnread,
setAnimatedMessageId,
animatedMessageId,
@@ -288,6 +292,7 @@ const MessageView = (props: MessageViewProps) => {
onMessageHeightChange: handleScroll,
onBeforeDownloadFileMessage,
filterEmojiCategoryIds,
+ markAsUnread,
})}
{ /* Suggested Replies */ }
{
@@ -392,6 +397,16 @@ const MessageView = (props: MessageViewProps) => {
);
}
+ const seperatorLabelText = useMemo(() => {
+ if (!hasSeparator && hasNewMessageSeparator) {
+ return 'New Message';
+ } else {
+ return format(message.createdAt, stringSet.DATE_FORMAT__MESSAGE_LIST__DATE_SEPARATOR, {
+ locale: dateLocale,
+ });
+ }
+ }, [hasSeparator, hasNewMessageSeparator]);
+
return (
{
ref={messageScrollRef}
>
{/* date-separator */}
- {hasSeparator
+ {(hasSeparator || hasNewMessageSeparator)
&& (renderedCustomSeparator || (
-
+
))}
diff --git a/src/modules/GroupChannel/components/Message/index.tsx b/src/modules/GroupChannel/components/Message/index.tsx
index 749bb5716..a725365f6 100644
--- a/src/modules/GroupChannel/components/Message/index.tsx
+++ b/src/modules/GroupChannel/components/Message/index.tsx
@@ -29,6 +29,7 @@ export const Message = (props: MessageProps): React.ReactElement => {
onMessageAnimated,
onBeforeDownloadFileMessage,
messages,
+ markAsUnread,
},
actions: {
toggleReaction,
@@ -85,6 +86,7 @@ export const Message = (props: MessageProps): React.ReactElement => {
updateUserMessage={updateUserMessage}
resendMessage={resendMessage}
deleteMessage={deleteMessage as any}
+ markAsUnread={markAsUnread}
animatedMessageId={animatedMessageId}
setAnimatedMessageId={setAnimatedMessageId}
onMessageAnimated={onMessageAnimated}
diff --git a/src/modules/GroupChannel/components/MessageList/getMessagePartsInfo.ts b/src/modules/GroupChannel/components/MessageList/getMessagePartsInfo.ts
index 718e6cfac..fbf067ca4 100644
--- a/src/modules/GroupChannel/components/MessageList/getMessagePartsInfo.ts
+++ b/src/modules/GroupChannel/components/MessageList/getMessagePartsInfo.ts
@@ -14,12 +14,15 @@ export interface GetMessagePartsInfoProps {
currentMessage: CoreMessageType;
currentChannel?: GroupChannel | null;
replyType?: string;
+ firstUnreadMessageId?: number | string | undefined;
}
interface OutPuts {
chainTop: boolean,
chainBottom: boolean,
hasSeparator: boolean,
+ hasNewMessageSeparator: boolean,
+ newFirstUnreadMessageId?: number | string | undefined,
}
/**
@@ -33,6 +36,7 @@ export const getMessagePartsInfo = ({
currentMessage,
currentChannel = null,
replyType = '',
+ firstUnreadMessageId,
}: GetMessagePartsInfoProps): OutPuts => {
const previousMessage = allMessages[currentIndex - 1];
const nextMessage = allMessages[currentIndex + 1];
@@ -47,9 +51,21 @@ export const getMessagePartsInfo = ({
// https://stackoverflow.com/a/41855608
const hasSeparator = isLocalMessage ? false : !(previousMessageCreatedAt && (isSameDay(currentCreatedAt, previousMessageCreatedAt)));
+ let hasNewMessageSeparator = false;
+ let newFirstUnreadMessageId;
+ if (!firstUnreadMessageId) {
+ hasNewMessageSeparator = currentChannel.myLastRead === (currentCreatedAt - 1);
+ if (hasNewMessageSeparator) newFirstUnreadMessageId = currentMessage.messageId;
+ } else if (currentMessage.messageId === firstUnreadMessageId || currentChannel.myLastRead === (currentCreatedAt - 1)) {
+ hasNewMessageSeparator = true;
+ newFirstUnreadMessageId = currentMessage.messageId;
+ }
+
return {
chainTop,
chainBottom,
hasSeparator,
+ hasNewMessageSeparator,
+ newFirstUnreadMessageId,
};
};
diff --git a/src/modules/GroupChannel/components/MessageList/index.tsx b/src/modules/GroupChannel/components/MessageList/index.tsx
index d58cc534e..22c614a9b 100644
--- a/src/modules/GroupChannel/components/MessageList/index.tsx
+++ b/src/modules/GroupChannel/components/MessageList/index.tsx
@@ -1,15 +1,16 @@
import './index.scss';
-import React, { useEffect, useState } from 'react';
+import React, { useEffect, useState, useRef } from 'react';
import type { Member } from '@sendbird/chat/groupChannel';
import { useGroupChannelHandler } from '@sendbird/uikit-tools';
-import { CoreMessageType, isSendableMessage, getHTMLTextDirection } from '../../../../utils';
+import { CoreMessageType, isSendableMessage, getHTMLTextDirection, SendableMessageType } from '../../../../utils';
import { EveryMessage, RenderMessageParamsType, TypingIndicatorType } from '../../../../types';
import PlaceHolder, { PlaceHolderTypes } from '../../../../ui/PlaceHolder';
import Icon, { IconColors, IconTypes } from '../../../../ui/Icon';
import Message from '../Message';
import UnreadCount from '../UnreadCount';
+import NewMessageCount from '../NewMessageCount';
import FrozenNotification from '../FrozenNotification';
import { SCROLL_BUFFER } from '../../../../utils/consts';
import TypingIndicatorBubble from '../../../../ui/TypingIndicatorBubble';
@@ -85,10 +86,13 @@ export const MessageList = (props: GroupChannelMessageListProps) => {
scrollRef,
scrollPositionRef,
scrollDistanceFromBottomRef,
+ markAsUnreadSourceRef,
},
actions: {
scrollToBottom,
setIsScrollBottomReached,
+ markAsReadAll,
+ markAsUnread,
},
} = useGroupChannel();
@@ -97,6 +101,9 @@ export const MessageList = (props: GroupChannelMessageListProps) => {
const [unreadSinceDate, setUnreadSinceDate] = useState();
+ const firstUnreadMessageIdRef = useRef();
+ const displayedNewMessageSeparatorRef = useRef(false);
+
useEffect(() => {
if (isScrollBottomReached) {
setUnreadSinceDate(undefined);
@@ -105,6 +112,12 @@ export const MessageList = (props: GroupChannelMessageListProps) => {
}
}, [isScrollBottomReached]);
+ useEffect(() => {
+ firstUnreadMessageIdRef.current = undefined;
+ displayedNewMessageSeparatorRef.current = false;
+ markAsUnreadSourceRef.current = undefined;
+ }, [currentChannel]);
+
/**
* 1. Move the message list scroll
* when each message's height is changed by `reactions` OR `showEdit`
@@ -129,13 +142,16 @@ export const MessageList = (props: GroupChannelMessageListProps) => {
return renderFrozenNotification();
},
unreadMessagesNotification() {
- if (isScrollBottomReached || !unreadSinceDate) return null;
+ if (markAsUnreadSourceRef.current === 'menu' || displayedNewMessageSeparatorRef.current) return null;
return (
scrollToBottom()}
+ isFrozenChannel={currentChannel?.isFrozen || false}
+ onClick={() => {
+ markAsReadAll();
+ }}
/>
);
},
@@ -154,6 +170,49 @@ export const MessageList = (props: GroupChannelMessageListProps) => {
);
},
+ newMessageCount() {
+ if (isScrollBottomReached) return null;
+ return (
+ scrollToBottom()}
+ />
+ );
+ },
+ };
+
+ const checkNewMessageSeparatorVisibility = () => {
+ if (displayedNewMessageSeparatorRef.current) return false;
+ if (currentChannel.unreadMessageCount === 0) return false;
+
+ const newMessageSeparator = document.getElementById('new-message-separator');
+ if (!newMessageSeparator) return false;
+
+ const newMessageSeparatorRect = newMessageSeparator.getBoundingClientRect();
+ const messageListContainer = document.querySelector('.sendbird-conversation__messages-padding');
+ if (!messageListContainer) return false;
+
+ const messageListContainerRect = messageListContainer.getBoundingClientRect();
+ const isTopInContainer = newMessageSeparatorRect.top > messageListContainerRect.top;
+
+ if (isTopInContainer) {
+ displayedNewMessageSeparatorRef.current = true;
+ return true;
+ }
+
+ return false;
+ };
+
+ const checkDisplayNewMessageSeparator = () => {
+ if (!checkNewMessageSeparatorVisibility()) return;
+
+ if (displayedNewMessageSeparatorRef.current && newMessages.length > 0) {
+ markAsUnread(newMessages[0] as SendableMessageType, 'internal');
+ firstUnreadMessageIdRef.current = newMessages[0].messageId;
+ } else if (displayedNewMessageSeparatorRef.current) {
+ markAsReadAll();
+ }
};
if (loading) {
@@ -182,14 +241,22 @@ export const MessageList = (props: GroupChannelMessageListProps) => {
onLoadPrevious={loadPrevious}
onScrollPosition={(it) => {
const isScrollBottomReached = it === 'bottom';
- if (newMessages.length > 0 && isScrollBottomReached) {
- resetNewMessages();
+ if (isScrollBottomReached) {
+ if (newMessages.length > 0) {
+ resetNewMessages();
+ }
+ if (!state.config.groupChannel.enableMarkAsUnread || displayedNewMessageSeparatorRef.current) {
+ markAsReadAll();
+ }
+ } else if (!displayedNewMessageSeparatorRef.current) {
+ checkDisplayNewMessageSeparator();
}
+
setIsScrollBottomReached(isScrollBottomReached);
}}
messages={messages}
renderMessage={({ message, index }) => {
- const { chainTop, chainBottom, hasSeparator } = getMessagePartsInfo({
+ const { chainTop, chainBottom, hasSeparator, hasNewMessageSeparator, newFirstUnreadMessageId } = getMessagePartsInfo({
allMessages: messages as CoreMessageType[],
stringSet,
replyType: replyType ?? 'NONE',
@@ -197,7 +264,18 @@ export const MessageList = (props: GroupChannelMessageListProps) => {
currentIndex: index,
currentMessage: message as CoreMessageType,
currentChannel: currentChannel!,
+ firstUnreadMessageId: firstUnreadMessageIdRef.current,
});
+
+ if (hasNewMessageSeparator && newFirstUnreadMessageId && newFirstUnreadMessageId !== firstUnreadMessageIdRef.current) {
+ firstUnreadMessageIdRef.current = newFirstUnreadMessageId;
+ if (markAsUnreadSourceRef.current === 'menu') {
+ checkNewMessageSeparatorVisibility();
+ } else {
+ checkDisplayNewMessageSeparator();
+ }
+ }
+
const isOutgoingMessage = isSendableMessage(message) && message.sender.userId === state.config.userId;
return (
@@ -205,6 +283,7 @@ export const MessageList = (props: GroupChannelMessageListProps) => {
handleScroll: onMessageContentSizeChanged,
message: message as EveryMessage,
hasSeparator,
+ hasNewMessageSeparator,
chainTop,
chainBottom,
renderMessageContent,
@@ -225,6 +304,7 @@ export const MessageList = (props: GroupChannelMessageListProps) => {
<>{renderer.frozenNotification()}>
<>{renderer.unreadMessagesNotification()}>
<>{renderer.scrollToBottomButton()}>
+ <>{renderer.newMessageCount()}>
>
);
diff --git a/src/modules/GroupChannel/components/NewMessageCount/index.scss b/src/modules/GroupChannel/components/NewMessageCount/index.scss
new file mode 100644
index 000000000..1bd4df3fa
--- /dev/null
+++ b/src/modules/GroupChannel/components/NewMessageCount/index.scss
@@ -0,0 +1,63 @@
+/**
+ * We operate the CSS files for Channel&GroupChannel modules in the GroupChannel.
+ * So keep in mind that you should consider both components when you make changes in this file.
+ */
+@import '../../../../styles/variables';
+
+// Floating NewMessageCount button - positioned at the bottom of MessageList
+.sendbird-new-message-floating-button {
+ position: absolute;
+ bottom: 8px; // Bottom position for new message count
+ left: 50%;
+ transform: translateX(-50%);
+ width: 155px;
+ height: 40px;
+ border-radius: 20px;
+ gap: 4px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+ transition: all 0.2s ease-in-out;
+ z-index: 5;
+ background-color: #FFFFFF;
+
+ &:hover {
+ cursor: pointer;
+ transform: translateX(-50%) translateY(-1px);
+ box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2);
+ background-color: #F5F5F5;
+ }
+
+ .sendbird-new-message-floating-button__text {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex: 1;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ color: #742DDD;
+ line-height: 1;
+ font-size: 14px;
+ text-align: center;
+ height: 100%;
+ }
+
+ // Icon fixed to right side
+ .sendbird-icon {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+
+ svg {
+ display: block;
+ vertical-align: middle;
+ }
+ }
+}
+
+.sendbird-new-message-floating-button--hide {
+ display: none;
+}
\ No newline at end of file
diff --git a/src/modules/GroupChannel/components/NewMessageCount/index.tsx b/src/modules/GroupChannel/components/NewMessageCount/index.tsx
new file mode 100644
index 000000000..4b1c8ef84
--- /dev/null
+++ b/src/modules/GroupChannel/components/NewMessageCount/index.tsx
@@ -0,0 +1,58 @@
+import './index.scss';
+import React, { useContext, useMemo } from 'react';
+
+import { LocalizationContext } from '../../../../lib/LocalizationContext';
+import Label, { LabelTypography, LabelColors } from '../../../../ui/Label';
+import Icon, { IconTypes, IconColors } from '../../../../ui/Icon';
+import { classnames } from '../../../../utils/utils';
+
+export interface NewMessageCountProps {
+ className?: string;
+ count: number | undefined;
+ onClick(): void;
+}
+
+export const NewMessageCount: React.FC = ({
+ className = '',
+ count = 0,
+ onClick,
+}: NewMessageCountProps) => {
+ const { stringSet } = useContext(LocalizationContext);
+
+ const newMessageCountText = useMemo(() => {
+ if (count === 1) {
+ return stringSet.CHANNEL__MESSAGE_LIST__NOTIFICATION__NEW_MESSAGE;
+ } else if (count > 1) {
+ return stringSet.CHANNEL__MESSAGE_LIST__NOTIFICATION__NEW_MESSAGE_S;
+ }
+ }, [count]);
+
+ return (
+
+
+
+
+ );
+};
+
+export default NewMessageCount;
diff --git a/src/modules/GroupChannel/components/UnreadCount/index.scss b/src/modules/GroupChannel/components/UnreadCount/index.scss
index fcff9113a..3371a16a8 100644
--- a/src/modules/GroupChannel/components/UnreadCount/index.scss
+++ b/src/modules/GroupChannel/components/UnreadCount/index.scss
@@ -4,6 +4,70 @@
*/
@import '../../../../styles/variables';
+// Floating UnreadCount button - completely separate from notification styles
+.sendbird-unread-floating-button {
+ position: absolute;
+ top: 8px; // Default position when no frozen notification
+ left: 50%;
+ transform: translateX(-50%);
+ width: 181px;
+ height: 40px;
+ border-radius: 20px;
+ gap: 4px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+ transition: all 0.2s ease-in-out;
+ z-index: 5;
+ background-color: #FFFFFF;
+
+ // When frozen notification is present, position below it
+ &.sendbird-unread-floating-button--below-frozen {
+ top: 48px;
+ }
+
+ &:hover {
+ cursor: pointer;
+ transform: translateX(-50%) translateY(-1px);
+ box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2);
+ background-color: #F5F5F5;
+ }
+
+ .sendbird-unread-floating-button__text {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex: 1;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ color: #00000080;
+ line-height: 1;
+ font-size: 14px;
+ text-align: center;
+ height: 100%;
+ }
+
+ // Icon fixed to right side
+ .sendbird-icon {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+
+ svg {
+ display: block;
+ vertical-align: middle;
+ }
+ }
+}
+
+.sendbird-unread-floating-button--hide {
+ display: none;
+}
+
+// FrozenNotification styles (required for Channel&GroupChannel modules)
.sendbird-notification--hide,
.sendbird-notification {
position: absolute;
@@ -51,3 +115,6 @@
.sendbird-notification--hide {
display: none;
}
+
+
+
diff --git a/src/modules/GroupChannel/components/UnreadCount/index.tsx b/src/modules/GroupChannel/components/UnreadCount/index.tsx
index 972a350a4..75b4046d5 100644
--- a/src/modules/GroupChannel/components/UnreadCount/index.tsx
+++ b/src/modules/GroupChannel/components/UnreadCount/index.tsx
@@ -4,7 +4,6 @@ import React, { useContext, useMemo } from 'react';
import { LocalizationContext } from '../../../../lib/LocalizationContext';
import Label, { LabelTypography, LabelColors } from '../../../../ui/Label';
import Icon, { IconTypes, IconColors } from '../../../../ui/Icon';
-import format from 'date-fns/format';
import { classnames } from '../../../../utils/utils';
export interface UnreadCountProps {
@@ -14,48 +13,48 @@ export interface UnreadCountProps {
lastReadAt?: Date | null;
/** @deprecated Please use `lastReadAt` instead * */
time?: string;
+ isFrozenChannel?: boolean;
}
export const UnreadCount: React.FC = ({
className = '',
count = 0,
- time = '',
onClick,
- lastReadAt,
+ isFrozenChannel = false,
}: UnreadCountProps) => {
- const { stringSet, dateLocale } = useContext(LocalizationContext);
+ const { stringSet } = useContext(LocalizationContext);
- const unreadSince = useMemo(() => {
- // TODO: Remove this on v4
- if (stringSet.CHANNEL__MESSAGE_LIST__NOTIFICATION__ON !== 'on') {
- const timeArray = time?.toString?.()?.split(' ') || [];
- timeArray?.splice(-2, 0, stringSet.CHANNEL__MESSAGE_LIST__NOTIFICATION__ON);
- return timeArray.join(' ');
- } else if (lastReadAt) {
- return format(lastReadAt, stringSet.DATE_FORMAT__MESSAGE_LIST__NOTIFICATION__UNREAD_SINCE, { locale: dateLocale });
+ const unreadMessageCountText = useMemo(() => {
+ if (count === 1) {
+ return stringSet.CHANNEL__MESSAGE_LIST__NOTIFICATION__UNREAD_MESSAGE;
+ } else if (count > 1) {
+ return stringSet.CHANNEL__MESSAGE_LIST__NOTIFICATION__UNREAD_MESSAGE_S;
}
- }, [time, lastReadAt]);
+ }, [count]);
return (
diff --git a/src/modules/GroupChannel/context/GroupChannelProvider.tsx b/src/modules/GroupChannel/context/GroupChannelProvider.tsx
index 84649688e..8af16c418 100644
--- a/src/modules/GroupChannel/context/GroupChannelProvider.tsx
+++ b/src/modules/GroupChannel/context/GroupChannelProvider.tsx
@@ -32,6 +32,7 @@ import type {
import useSendbird from '../../../lib/Sendbird/context/hooks/useSendbird';
import useDeepCompareEffect from '../../../hooks/useDeepCompareEffect';
import { deleteNullish } from '../../../utils/utils';
+import { CollectionEventSource } from '@sendbird/chat';
const initialState = {
currentChannel: null,
@@ -177,6 +178,31 @@ const GroupChannelManager :React.FC(undefined);
+
+ const markAsUnread = useMemo(() => (message: any, source?: 'menu' | 'internal') => {
+ if (!config.groupChannel.enableMarkAsUnread) return;
+ if (!state.currentChannel) {
+ logger?.error?.('GroupChannelProvider: channel is required for markAsUnread');
+ return;
+ }
+
+ try {
+ if (state.currentChannel.markAsUnread) {
+ state.currentChannel.markAsUnread(message);
+ logger?.info?.('GroupChannelProvider: markAsUnread called for message', {
+ messageId: message.messageId,
+ source: source || 'unknown',
+ });
+ markAsUnreadSourceRef.current = source || 'internal';
+ } else {
+ logger?.error?.('GroupChannelProvider: markAsUnread method not available in current SDK version');
+ }
+ } catch (error) {
+ logger?.error?.('GroupChannelProvider: markAsUnread failed', error);
+ }
+ }, [state.currentChannel, logger]);
+
// Message Collection setup
const messageDataSource = useGroupChannelMessages(sdkStore.sdk, state.currentChannel!, {
startingPoint,
@@ -184,8 +210,10 @@ const GroupChannelManager :React.FC !isScrollBottomReached,
markAsRead: (channels) => {
- if (isScrollBottomReached && !disableMarkAsRead) {
- channels.forEach((it) => markAsReadScheduler.push(it));
+ if (!config.groupChannel.enableMarkAsUnread) {
+ if (isScrollBottomReached && !disableMarkAsRead) {
+ channels.forEach((it) => markAsReadScheduler.push(it));
+ }
}
},
onMessagesReceived: (messages) => {
@@ -206,7 +234,10 @@ const GroupChannelManager :React.FC {
+ onChannelUpdated: (channel, ctx) => {
+ if (ctx.source === CollectionEventSource.EVENT_CHANNEL_UNREAD) {
+ console.log('MADOKA #1 onChannelUpdated', ctx.source, ctx.userIds);
+ }
actions.setCurrentChannel(channel);
},
logger: logger as any,
@@ -345,6 +376,8 @@ const GroupChannelManager :React.FC void;
handleChannelError: (error: SendbirdError) => void;
+ markAsReadAll: () => void;
+ markAsUnread: (message: SendableMessageType, source?: 'menu' | 'internal') => void;
// Message actions
sendUserMessage: (params: UserMessageCreateParams) => Promise;
@@ -73,16 +75,28 @@ export const useGroupChannel = () => {
if (config.isOnline && state.hasNext()) {
await state.resetWithStartingPoint(Number.MAX_SAFE_INTEGER);
}
+
state.scrollPubSub.publish('scrollToBottom', { animated });
if (state.currentChannel && !state.hasNext()) {
state.resetNewMessages();
if (!state.disableMarkAsRead) {
- markAsReadScheduler.push(state.currentChannel);
+ if (!config.groupChannel.enableMarkAsUnread) {
+ markAsReadScheduler.push(state.currentChannel);
+ }
}
}
}, [state.scrollRef.current, config.isOnline, markAsReadScheduler]);
+ const markAsReadAll = useCallback(() => {
+ if (!state.markAsUnreadSourceRef?.current) {
+ state.resetNewMessages();
+ }
+ if (config.isOnline && !state.disableMarkAsRead) {
+ markAsReadScheduler.push(state.currentChannel);
+ }
+ }, [config.isOnline, state.disableMarkAsRead, state.currentChannel]);
+
const scrollToMessage = useCallback(async (
createdAt: number,
messageId: number,
@@ -188,6 +202,8 @@ export const useGroupChannel = () => {
return {
setCurrentChannel,
handleChannelError,
+ markAsReadAll,
+ markAsUnread: state.markAsUnread,
setQuoteMessage,
scrollToBottom,
scrollToMessage,
@@ -199,6 +215,8 @@ export const useGroupChannel = () => {
}, [
setCurrentChannel,
handleChannelError,
+ markAsReadAll,
+ state.markAsUnread,
setQuoteMessage,
scrollToBottom,
scrollToMessage,
diff --git a/src/modules/GroupChannel/context/types.ts b/src/modules/GroupChannel/context/types.ts
index 76cff319e..490a1e3ac 100644
--- a/src/modules/GroupChannel/context/types.ts
+++ b/src/modules/GroupChannel/context/types.ts
@@ -63,6 +63,10 @@ interface InternalGroupChannelState extends MessageDataSource {
disableMarkAsRead: boolean;
scrollBehavior: 'smooth' | 'auto';
+ // Actions (React UIKit specific)
+ markAsUnread?: (message: SendableMessageType) => void;
+ markAsUnreadSourceRef: React.MutableRefObject<'menu' | 'internal' | null>;
+
// Legacy - Will be removed after migration
scrollPubSub: PubSubTypes;
}
diff --git a/src/svgs/icon-floating-button-close.svg b/src/svgs/icon-floating-button-close.svg
new file mode 100644
index 000000000..bef36eba2
--- /dev/null
+++ b/src/svgs/icon-floating-button-close.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/svgs/icon-mark-as-unread.svg b/src/svgs/icon-mark-as-unread.svg
new file mode 100644
index 000000000..2eacb63f0
--- /dev/null
+++ b/src/svgs/icon-mark-as-unread.svg
@@ -0,0 +1,10 @@
+
diff --git a/src/ui/DateSeparator/index.tsx b/src/ui/DateSeparator/index.tsx
index 837976273..46d506bdc 100644
--- a/src/ui/DateSeparator/index.tsx
+++ b/src/ui/DateSeparator/index.tsx
@@ -13,14 +13,17 @@ export interface DateSeparatorProps {
children?: string | ReactElement;
className?: string | Array;
separatorColor?: Colors;
+ hasNewMessageSeparator?: boolean;
}
const DateSeparator = ({
children = undefined,
className = '',
- separatorColor = Colors.ONBACKGROUND_4,
+ hasNewMessageSeparator = false,
+ separatorColor = hasNewMessageSeparator ? Colors.PRIMARY : Colors.ONBACKGROUND_4,
}: DateSeparatorProps): ReactElement => {
return (
;
case Types.FEEDBACK_LIKE: return ;
case Types.FEEDBACK_DISLIKE: return ;
+ case Types.MARK_AS_UNREAD: return ;
+ case Types.FLOATING_BUTTON_CLOSE: return ;
default: return 'icon'; // If you see this text 'icon' replace icon for it
}
}
diff --git a/src/ui/Icon/type.ts b/src/ui/Icon/type.ts
index 5030719e5..b9622a596 100644
--- a/src/ui/Icon/type.ts
+++ b/src/ui/Icon/type.ts
@@ -32,6 +32,7 @@ export const Types = {
GIF: 'GIF',
INFO: 'INFO',
LEAVE: 'LEAVE',
+ MARK_AS_UNREAD: 'MARK_AS_UNREAD',
MEMBERS: 'MEMBERS',
MESSAGE: 'MESSAGE',
MODERATIONS: 'MODERATIONS',
@@ -60,5 +61,6 @@ export const Types = {
USER: 'USER',
FEEDBACK_LIKE: 'FEEDBACK_LIKE',
FEEDBACK_DISLIKE: 'FEEDBACK_DISLIKE',
+ FLOATING_BUTTON_CLOSE: 'FLOATING_BUTTON_CLOSE',
} as const;
export type Types = typeof Types[keyof typeof Types];
diff --git a/src/ui/Icon/utils.ts b/src/ui/Icon/utils.ts
index d1451d1f8..446320be9 100644
--- a/src/ui/Icon/utils.ts
+++ b/src/ui/Icon/utils.ts
@@ -59,6 +59,7 @@ export function changeTypeToIconClassName(type: Types): string {
case Types.GIF: return 'sendbird-icon-gif';
case Types.INFO: return 'sendbird-icon-info';
case Types.LEAVE: return 'sendbird-icon-leave';
+ case Types.MARK_AS_UNREAD: return 'sendbird-icon-mark-as-unread';
case Types.MEMBERS: return 'sendbird-icon-members';
case Types.MESSAGE: return 'sendbird-icon-message';
case Types.MODERATIONS: return 'sendbird-icon-moderations';
diff --git a/src/ui/Label/stringSet.ts b/src/ui/Label/stringSet.ts
index f099849aa..b1528e901 100644
--- a/src/ui/Label/stringSet.ts
+++ b/src/ui/Label/stringSet.ts
@@ -12,7 +12,10 @@ const stringSet = {
en: {
// Group Channel - Conversation
MESSAGE_STATUS__YESTERDAY: 'Yesterday',
- CHANNEL__MESSAGE_LIST__NOTIFICATION__NEW_MESSAGE: 'new message(s) since',
+ CHANNEL__MESSAGE_LIST__NOTIFICATION__NEW_MESSAGE: 'new message',
+ CHANNEL__MESSAGE_LIST__NOTIFICATION__NEW_MESSAGE_S: 'new message',
+ CHANNEL__MESSAGE_LIST__NOTIFICATION__UNREAD_MESSAGE: 'unread message',
+ CHANNEL__MESSAGE_LIST__NOTIFICATION__UNREAD_MESSAGE_S: 'unread messages',
/** @deprecated Please use `DATE_FORMAT__MESSAGE_LIST__NOTIFICATION__UNREAD_SINCE` instead * */
CHANNEL__MESSAGE_LIST__NOTIFICATION__ON: 'on',
// Channel List
@@ -168,6 +171,7 @@ const stringSet = {
MESSAGE_MENU__RESEND: 'Resend',
MESSAGE_MENU__DELETE: 'Delete',
MESSAGE_MENU__SAVE: 'Save',
+ MESSAGE_MENU__MARK_AS_UNREAD: 'Mark as unread',
// * FIXME: get back legacy, remove after refactoring open channel messages *
CONTEXT_MENU_DROPDOWN__COPY: 'Copy',
CONTEXT_MENU_DROPDOWN__EDIT: 'Edit',
diff --git a/src/ui/MessageContent/index.tsx b/src/ui/MessageContent/index.tsx
index 546f5685c..2d59432ab 100644
--- a/src/ui/MessageContent/index.tsx
+++ b/src/ui/MessageContent/index.tsx
@@ -77,6 +77,7 @@ export interface MessageContentProps extends MessageComponentRenderers {
deleteMessage?: (message: CoreMessageType) => Promise;
toggleReaction?: (message: SendableMessageType, reactionKey: string, isReacted: boolean) => void;
setQuoteMessage?: (message: SendableMessageType) => void;
+ markAsUnread?: (message: SendableMessageType) => void;
// onClick listener for thread replies view (for open thread module)
onReplyInThread?: (props: { message: SendableMessageType }) => void;
// onClick listener for thread quote message view (for open thread module)
@@ -120,6 +121,7 @@ export function MessageContent(props: MessageContentProps): ReactElement {
deleteMessage,
toggleReaction,
setQuoteMessage,
+ markAsUnread,
onReplyInThread,
onQuoteMessageClick,
onMessageHeightChange,
@@ -310,6 +312,7 @@ export function MessageContent(props: MessageContentProps): ReactElement {
showRemove,
resendMessage,
setQuoteMessage,
+ markAsUnread,
onReplyInThread: ({ message }) => {
if (threadReplySelectType === ThreadReplySelectType.THREAD) {
onReplyInThread?.({ message });
@@ -543,6 +546,7 @@ export function MessageContent(props: MessageContentProps): ReactElement {
showRemove,
resendMessage,
setQuoteMessage,
+ markAsUnread,
onReplyInThread: ({ message }) => {
if (threadReplySelectType === ThreadReplySelectType.THREAD) {
onReplyInThread?.({ message });
@@ -574,6 +578,7 @@ export function MessageContent(props: MessageContentProps): ReactElement {
setQuoteMessage,
toggleReaction,
showEdit,
+ markAsUnread,
onReplyInThread: ({ message }) => {
if (threadReplySelectType === ThreadReplySelectType.THREAD) {
onReplyInThread?.({ message });
diff --git a/src/ui/MessageMenu/MessageMenu.tsx b/src/ui/MessageMenu/MessageMenu.tsx
index ead309151..86bf00481 100644
--- a/src/ui/MessageMenu/MessageMenu.tsx
+++ b/src/ui/MessageMenu/MessageMenu.tsx
@@ -16,6 +16,7 @@ import {
EditMenuItem,
ResendMenuItem,
DeleteMenuItem,
+ MarkAsUnreadMenuItem,
} from './menuItems/MessageMenuItems';
import { ReplyType } from '../../types';
import {
@@ -23,6 +24,7 @@ import {
showMenuItemCopy,
showMenuItemDelete,
showMenuItemEdit,
+ showMenuItemMarkAsUnread,
showMenuItemOpenInChannel,
showMenuItemReply,
showMenuItemResend,
@@ -41,6 +43,7 @@ export type RenderMenuItemsParams = {
EditMenuItem: (props: PrebuildMenuItemPropsType) => ReactElement;
ResendMenuItem: (props: PrebuildMenuItemPropsType) => ReactElement;
DeleteMenuItem: (props: PrebuildMenuItemPropsType) => ReactElement;
+ MarkAsUnreadMenuItem: (props: PrebuildMenuItemPropsType) => ReactElement;
};
};
export interface MessageMenuProps {
@@ -56,6 +59,7 @@ export interface MessageMenuProps {
showRemove?: (bool: boolean) => void;
deleteMessage?: (message: SendableMessageType) => void;
resendMessage?: (message: SendableMessageType) => void;
+ markAsUnread?: (message: SendableMessageType) => void;
setQuoteMessage?: (message: SendableMessageType) => void;
onReplyInThread?: (props: { message: SendableMessageType }) => void;
onMoveToParentMessage?: () => void;
@@ -75,11 +79,12 @@ export const MessageMenu = ({
showRemove = noop,
deleteMessage,
resendMessage,
+ markAsUnread,
setQuoteMessage,
onReplyInThread,
onMoveToParentMessage,
}: MessageMenuProps) => {
- const { state: { config: { isOnline } } } = useSendbird();
+ const { state: { config: { isOnline, groupChannel: { enableMarkAsUnread } } } } = useSendbird();
const triggerRef = useRef(null);
const containerRef = useRef(null);
@@ -114,6 +119,7 @@ export const MessageMenu = ({
showRemove,
deleteMessage,
resendMessage,
+ markAsUnread,
isOnline,
disableDeleteMessage,
triggerRef,
@@ -135,6 +141,7 @@ export const MessageMenu = ({
ThreadMenuItem,
OpenInChannelMenuItem,
EditMenuItem,
+ MarkAsUnreadMenuItem,
ResendMenuItem,
DeleteMenuItem,
},
@@ -145,6 +152,7 @@ export const MessageMenu = ({
{showMenuItemThread(params) && }
{showMenuItemOpenInChannel(params) && }
{showMenuItemEdit(params) && }
+ {showMenuItemMarkAsUnread(params) && enableMarkAsUnread && }
{showMenuItemResend(params) && }
{showMenuItemDelete(params) && }
>
diff --git a/src/ui/MessageMenu/MessageMenuProvider.tsx b/src/ui/MessageMenu/MessageMenuProvider.tsx
index 8dc80c95d..78180229a 100644
--- a/src/ui/MessageMenu/MessageMenuProvider.tsx
+++ b/src/ui/MessageMenu/MessageMenuProvider.tsx
@@ -11,6 +11,7 @@ interface CommonMessageMenuContextProps {
showRemove: (bool: boolean) => void;
deleteMessage: (message: SendableMessageType) => void;
resendMessage: (message: SendableMessageType) => void;
+ markAsUnread?: (message: SendableMessageType, source?: 'menu' | 'internal') => void;
isOnline: boolean;
disableDeleteMessage: boolean | null;
triggerRef: MutableRefObject;
diff --git a/src/ui/MessageMenu/menuItems/BottomSheetMenuItems.tsx b/src/ui/MessageMenu/menuItems/BottomSheetMenuItems.tsx
index 81d934146..f5b10f6e8 100644
--- a/src/ui/MessageMenu/menuItems/BottomSheetMenuItems.tsx
+++ b/src/ui/MessageMenu/menuItems/BottomSheetMenuItems.tsx
@@ -208,3 +208,30 @@ export const DownloadMenuItem = (props: PrebuildMenuItemPropsType) => {
);
};
+
+export const MarkAsUnreadMenuItem = (props: PrebuildMenuItemPropsType) => {
+ const { stringSet } = useLocalization();
+ const { message, hideMenu, markAsUnread } = useMessageMenuContext();
+
+ return (
+ {
+ if (markAsUnread) {
+ markAsUnread(message, 'menu');
+ }
+ hideMenu();
+ props.onClick?.(e);
+ }}
+ >
+ {props.children ?? (
+ <>
+
+
+ >
+ )}
+
+ );
+};
diff --git a/src/ui/MessageMenu/menuItems/MessageMenuItems.tsx b/src/ui/MessageMenu/menuItems/MessageMenuItems.tsx
index 2b5867014..0d44fbfbb 100644
--- a/src/ui/MessageMenu/menuItems/MessageMenuItems.tsx
+++ b/src/ui/MessageMenu/menuItems/MessageMenuItems.tsx
@@ -179,3 +179,23 @@ export const DeleteMenuItem = (props: PrebuildMenuItemPropsType) => {
);
};
+
+export const MarkAsUnreadMenuItem = (props: PrebuildMenuItemPropsType) => {
+ const { stringSet } = useLocalization();
+ const { message, hideMenu, markAsUnread } = useMessageMenuContext();
+
+ return (
+
+ );
+};
diff --git a/src/ui/MessageMenu/menuItems/MobileMenuItems.tsx b/src/ui/MessageMenu/menuItems/MobileMenuItems.tsx
index af89a8d12..94b526840 100644
--- a/src/ui/MessageMenu/menuItems/MobileMenuItems.tsx
+++ b/src/ui/MessageMenu/menuItems/MobileMenuItems.tsx
@@ -207,3 +207,30 @@ export const DownloadMenuItem = (props: PrebuildMenuItemPropsType) => {
);
};
+
+export const MarkAsUnreadMenuItem = (props: PrebuildMenuItemPropsType) => {
+ const { stringSet } = useLocalization();
+ const { message, hideMenu, markAsUnread } = useMessageMenuContext();
+
+ return (
+
+ );
+};
diff --git a/src/ui/MobileMenu/MobileBottomSheet.tsx b/src/ui/MobileMenu/MobileBottomSheet.tsx
index c78959d2a..af48d6b44 100644
--- a/src/ui/MobileMenu/MobileBottomSheet.tsx
+++ b/src/ui/MobileMenu/MobileBottomSheet.tsx
@@ -15,6 +15,7 @@ import {
isVoiceMessage,
isThreadMessage,
} from '../../utils';
+import { showMenuItemMarkAsUnread } from '../../utils/menuConditions';
import BottomSheet from '../BottomSheet';
import ImageRenderer from '../ImageRenderer';
import ReactionButton from '../ReactionButton';
@@ -29,6 +30,7 @@ import {
ThreadMenuItem,
DeleteMenuItem,
DownloadMenuItem,
+ MarkAsUnreadMenuItem,
} from '../MessageMenu/menuItems/BottomSheetMenuItems';
import useSendbird from '../../lib/Sendbird/context/hooks/useSendbird';
@@ -56,7 +58,7 @@ const MobileBottomSheet: React.FunctionComponent = (prop
renderMenuItems,
} = props;
const isByMe = message?.sender?.userId === userId;
- const { state: { config: { isOnline } } } = useSendbird();
+ const { state: { config: { isOnline, groupChannel: { enableMarkAsUnread } } } } = useSendbird();
const showMenuItemCopy: boolean = isUserMessage(message as UserMessage);
const showMenuItemEdit: boolean = (isUserMessage(message as UserMessage) && isSentMessage(message) && isByMe);
const showMenuItemResend: boolean = (isOnline && isFailedMessage(message) && message?.isResendable && isByMe);
@@ -81,6 +83,13 @@ const MobileBottomSheet: React.FunctionComponent = (prop
&& !isPendingMessage(message)
&& !isThreadMessage(message)
&& (channel?.isGroupChannel() && !(channel as GroupChannel)?.isBroadcast);
+ // const showMenuItemMarkAsUnreadCondition = showMenuItemMarkAsUnread({
+ // message,
+ // channel,
+ // isByMe,
+ // replyType,
+ // onReplyInThread,
+ // });
const maxEmojisPerRow = Math.floor(window.innerWidth / EMOJI_SIZE) - 1;
const [showEmojisOnly, setShowEmojisOnly] = useState(false);
@@ -98,6 +107,7 @@ const MobileBottomSheet: React.FunctionComponent = (prop
showRemove,
deleteMessage,
resendMessage,
+ markAsUnread: props.markAsUnread,
isOnline,
disableDeleteMessage: disableDelete,
triggerRef: null,
@@ -192,11 +202,13 @@ const MobileBottomSheet: React.FunctionComponent = (prop
ReplyMenuItem,
ThreadMenuItem,
DeleteMenuItem,
+ MarkAsUnreadMenuItem,
},
}) ?? (
<>
{showMenuItemCopy && }
{showMenuItemEdit && }
+ {showMenuItemMarkAsUnread && enableMarkAsUnread && }
{showMenuItemResend && }
{showMenuItemReply && }
{showMenuItemThread && }
diff --git a/src/ui/MobileMenu/MobileContextMenu.tsx b/src/ui/MobileMenu/MobileContextMenu.tsx
index 97a116664..e5be8a3f4 100644
--- a/src/ui/MobileMenu/MobileContextMenu.tsx
+++ b/src/ui/MobileMenu/MobileContextMenu.tsx
@@ -12,6 +12,7 @@ import {
isThreadMessage,
isVoiceMessage,
} from '../../utils';
+import { showMenuItemMarkAsUnread } from '../../utils/menuConditions';
import { MessageMenuProvider } from '../MessageMenu';
import type { MobileMessageMenuContextProps } from '../MessageMenu/MessageMenuProvider';
@@ -23,6 +24,7 @@ import {
ResendMenuItem,
DeleteMenuItem,
DownloadMenuItem,
+ MarkAsUnreadMenuItem,
} from '../MessageMenu/menuItems/MobileMenuItems';
import { MenuItems } from '../ContextMenu';
import { noop } from '../../utils/utils';
@@ -48,7 +50,7 @@ const MobileContextMenu: React.FunctionComponent = (props: BaseMe
hideMenu: hideMobileMenu,
} = props;
const isByMe = message?.sender?.userId === userId;
- const { state: { config: { isOnline } } } = useSendbird();
+ const { state: { config: { isOnline, groupChannel: { enableMarkAsUnread } } } } = useSendbird();
// Menu Items condition
const showMenuItemCopy = isUserMessage(message as UserMessage);
@@ -60,6 +62,13 @@ const MobileContextMenu: React.FunctionComponent = (props: BaseMe
const showMenuItemDownload = !isPendingMessage(message) && isFileMessage(message) && !(isVoiceMessage(message) && (channel as GroupChannel)?.isSuper || (channel as GroupChannel)?.isBroadcast);
const showMenuItemReply = replyType === 'QUOTE_REPLY' && !isFailedMessage(message) && !isPendingMessage(message) && channel?.isGroupChannel();
const showMenuItemThread = replyType === 'THREAD' && !isOpenedFromThread && !isFailedMessage(message) && !isPendingMessage(message) && !isThreadMessage(message) && channel?.isGroupChannel();
+ const showMenuItemMarkAsUnreadCondition = showMenuItemMarkAsUnread({
+ message,
+ channel,
+ isByMe,
+ replyType,
+ onReplyInThread,
+ });
const disableDeleteMessage = (deleteMenuState !== undefined && deleteMenuState === 'DISABLE') || (message?.threadInfo?.replyCount ?? 0) > 0;
const contextValue: MobileMessageMenuContextProps = {
@@ -72,6 +81,7 @@ const MobileContextMenu: React.FunctionComponent = (props: BaseMe
showRemove,
deleteMessage,
resendMessage,
+ markAsUnread: props.markAsUnread,
isOnline,
disableDeleteMessage,
triggerRef: parentRef as MutableRefObject,
@@ -94,6 +104,7 @@ const MobileContextMenu: React.FunctionComponent = (props: BaseMe
ReplyMenuItem,
ThreadMenuItem,
EditMenuItem,
+ MarkAsUnreadMenuItem,
ResendMenuItem,
DeleteMenuItem,
},
@@ -103,6 +114,7 @@ const MobileContextMenu: React.FunctionComponent = (props: BaseMe
{showMenuItemReply && }
{showMenuItemThread && }
{showMenuItemEdit && }
+ {showMenuItemMarkAsUnreadCondition && enableMarkAsUnread && }
{showMenuItemResend && }
{showMenuItemDeleteFinal && }
{showMenuItemDownload && }
diff --git a/src/ui/MobileMenu/types.ts b/src/ui/MobileMenu/types.ts
index 444e85655..35f6ad9a8 100644
--- a/src/ui/MobileMenu/types.ts
+++ b/src/ui/MobileMenu/types.ts
@@ -26,6 +26,7 @@ export interface BaseMenuProps {
showRemove?: (bool: boolean) => void;
resendMessage?: (message: SendableMessageType) => void;
deleteMessage?: (message: CoreMessageType) => Promise;
+ markAsUnread?: (message: SendableMessageType) => void;
setQuoteMessage?: (message: SendableMessageType) => void;
isReactionEnabled?: boolean;
parentRef?: React.RefObject;
diff --git a/src/utils/menuConditions.ts b/src/utils/menuConditions.ts
index 5015c1928..d142c24ec 100644
--- a/src/utils/menuConditions.ts
+++ b/src/utils/menuConditions.ts
@@ -53,3 +53,7 @@ export const showMenuItemReply = ({ channel, message, replyType }: MenuCondition
export const showMenuItemThread = ({ channel, message, replyType, onReplyInThread }: MenuConditionsParams) => {
return isReplyTypeMessageEnabled({ channel, message }) && replyType === 'THREAD' && !message?.parentMessageId && typeof onReplyInThread === 'function';
};
+
+export const showMenuItemMarkAsUnread = ({ message, channel }: MenuConditionsParams) => {
+ return !isFailedMessage(message) && !isPendingMessage(message) && channel?.isGroupChannel?.();
+};
diff --git a/yarn.lock b/yarn.lock
index 5f8eca2e5..98de84965 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2747,9 +2747,9 @@ __metadata:
languageName: node
linkType: hard
-"@sendbird/chat@npm:^4.19.1":
- version: 4.19.1
- resolution: "@sendbird/chat@npm:4.19.1"
+"@sendbird/chat@npm:^4.19.2":
+ version: 4.19.2
+ resolution: "@sendbird/chat@npm:4.19.2"
peerDependencies:
"@react-native-async-storage/async-storage": ^1.17.6
react-native-mmkv: ">=2.0.0"
@@ -2758,29 +2758,29 @@ __metadata:
optional: true
react-native-mmkv:
optional: true
- checksum: d500cbd6d994dbfdc8754814b61e8011c2aceb0c5c2a19780fdb9692cdd91a83a750c59aa4dc605c563394abed8242a0a589aa726ed097f2e49e3b7ddef59a65
+ checksum: 6cb69158f19633252f474c6edd1ae1e55d1d46dc1dcf628a0a5c5108ee1b9cafd5a2cde63aeb011237d9fdc5789c8388209ced2fc97b0d9841d5c5aa2bb6d9da
languageName: node
linkType: hard
-"@sendbird/react-uikit-message-template-view@npm:^0.0.8":
- version: 0.0.8
- resolution: "@sendbird/react-uikit-message-template-view@npm:0.0.8"
+"@sendbird/react-uikit-message-template-view@npm:^0.0.10":
+ version: 0.0.10
+ resolution: "@sendbird/react-uikit-message-template-view@npm:0.0.10"
dependencies:
- "@sendbird/uikit-message-template": ^0.0.8
+ "@sendbird/uikit-message-template": ^0.0.10
peerDependencies:
"@sendbird/chat": ">=4.3.0 <5"
react: ">=16.8.6"
react-dom: ">=16.8.6"
- checksum: ac178463097f84e7953889b379d34af200a1dc950a3933ab02affedf6bfcf88918c745791ad99dc22720d762b64d2b63ef71fbad7e36287e3d4b1d0dd74c088a
+ checksum: aac92fb0b963c4ee3f31eea05839cd49c5ad8561c427b5778731fdf61bcbc18076b329ab2866f12b776e77e9b2a9bfbba12263bcf25d4949a34ab80120548d2f
languageName: node
linkType: hard
-"@sendbird/uikit-message-template@npm:^0.0.8":
- version: 0.0.8
- resolution: "@sendbird/uikit-message-template@npm:0.0.8"
+"@sendbird/uikit-message-template@npm:^0.0.10":
+ version: 0.0.10
+ resolution: "@sendbird/uikit-message-template@npm:0.0.10"
peerDependencies:
react: ">=16.8.6"
- checksum: 59c7f6f78fb6513d62b6fc4bde94bb8701c03d1f8a1e74cc4e9a0aff03d03d2ba813c04ea4c7b3c12a5d19d27f1270ff8e4f3a959329248a7b7d1e0dca4925c8
+ checksum: 18dfce8f7c83d37a94bc3cbca0f561d7ed10c4367bcb2d2bf9c9192fce8fa06cfd87e8f968c6f3ae6d464a5119338ee5167b1a055b777bd906664a225c7e486f
languageName: node
linkType: hard
@@ -2803,9 +2803,9 @@ __metadata:
"@rollup/plugin-node-resolve": ^15.2.3
"@rollup/plugin-replace": ^5.0.4
"@rollup/plugin-typescript": ^11.1.5
- "@sendbird/chat": ^4.19.1
- "@sendbird/react-uikit-message-template-view": ^0.0.8
- "@sendbird/uikit-tools": ^0.0.8
+ "@sendbird/chat": ^4.19.2
+ "@sendbird/react-uikit-message-template-view": ^0.0.10
+ "@sendbird/uikit-tools": ^0.0.10
"@storybook/addon-essentials": ^8.5.0
"@storybook/manager-api": ^8.5.0
"@storybook/react-vite": ^8.5.0
@@ -2870,13 +2870,13 @@ __metadata:
languageName: unknown
linkType: soft
-"@sendbird/uikit-tools@npm:^0.0.8":
- version: 0.0.8
- resolution: "@sendbird/uikit-tools@npm:0.0.8"
+"@sendbird/uikit-tools@npm:^0.0.10":
+ version: 0.0.10
+ resolution: "@sendbird/uikit-tools@npm:0.0.10"
peerDependencies:
"@sendbird/chat": ">=4.10.5 <5"
react: ">=16.8.6"
- checksum: 5c91ea3d628a317a62e28a6c8f78660844e7b07cb1a7aa7d4827798e08f8a381a19aa994645e92f37612e9deb87f7cef82dd45fc6f3ff0b143d7e0d6e162d663
+ checksum: 1b92fe4a5caced7efff4397fdf87912bc958b45f5eae1f986f78961759b18687901cc28bbc6f797a6fb0e1552e56ef5e04a16da0125883f18b00e99fbee5f44b
languageName: node
linkType: hard
From 443585b63134491bb1349d6858e450a6b64c4b1b Mon Sep 17 00:00:00 2001
From: danney-chun <63285271+danney-chun@users.noreply.github.com>
Date: Wed, 2 Jul 2025 11:47:42 +0900
Subject: [PATCH 02/19] added undefined check
---
src/modules/GroupChannel/components/MessageList/index.tsx | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/src/modules/GroupChannel/components/MessageList/index.tsx b/src/modules/GroupChannel/components/MessageList/index.tsx
index 22c614a9b..4ab48b5f5 100644
--- a/src/modules/GroupChannel/components/MessageList/index.tsx
+++ b/src/modules/GroupChannel/components/MessageList/index.tsx
@@ -115,7 +115,9 @@ export const MessageList = (props: GroupChannelMessageListProps) => {
useEffect(() => {
firstUnreadMessageIdRef.current = undefined;
displayedNewMessageSeparatorRef.current = false;
- markAsUnreadSourceRef.current = undefined;
+ if (markAsUnreadSourceRef !== undefined) {
+ markAsUnreadSourceRef.current = undefined;
+ }
}, [currentChannel]);
/**
@@ -142,7 +144,7 @@ export const MessageList = (props: GroupChannelMessageListProps) => {
return renderFrozenNotification();
},
unreadMessagesNotification() {
- if (markAsUnreadSourceRef.current === 'menu' || displayedNewMessageSeparatorRef.current) return null;
+ if (markAsUnreadSourceRef?.current === 'menu' || displayedNewMessageSeparatorRef.current) return null;
return (
Date: Fri, 4 Jul 2025 03:32:35 +0900
Subject: [PATCH 03/19] fixed QA
---
src/modules/App/AppLayout.tsx | 2 +-
.../Channel/components/MessageList/index.tsx | 2 +-
.../components/Message/MessageView.tsx | 7 +-
.../MessageList/getMessagePartsInfo.ts | 14 +-
.../components/MessageList/index.tsx | 193 ++++++++++++------
.../context/GroupChannelProvider.tsx | 18 +-
.../context/hooks/useGroupChannel.ts | 20 +-
src/modules/GroupChannel/context/types.ts | 3 +-
.../GroupChannelListItemView.tsx | 3 +-
src/ui/DateSeparator/index.tsx | 37 +++-
src/ui/MessageMenu/MessageMenuProvider.tsx | 2 +-
.../menuItems/BottomSheetMenuItems.tsx | 2 +-
.../menuItems/MessageMenuItems.tsx | 2 +-
.../MessageMenu/menuItems/MobileMenuItems.tsx | 2 +-
14 files changed, 210 insertions(+), 97 deletions(-)
diff --git a/src/modules/App/AppLayout.tsx b/src/modules/App/AppLayout.tsx
index 1947f215b..c0d160011 100644
--- a/src/modules/App/AppLayout.tsx
+++ b/src/modules/App/AppLayout.tsx
@@ -39,7 +39,7 @@ export const AppLayout = (props: AppLayoutProps) => {
const replyType = props.replyType ?? getCaseResolvedReplyType(globalConfigs.groupChannel.replyType).upperCase;
const isReactionEnabled = props.isReactionEnabled ?? globalConfigs.groupChannel.enableReactions;
const showSearchIcon = props.showSearchIcon ?? globalConfigs.groupChannelSettings.enableMessageSearch;
-
+
return (
<>
{
diff --git a/src/modules/Channel/components/MessageList/index.tsx b/src/modules/Channel/components/MessageList/index.tsx
index 6e6b36a3d..c863b3c1a 100644
--- a/src/modules/Channel/components/MessageList/index.tsx
+++ b/src/modules/Channel/components/MessageList/index.tsx
@@ -163,7 +163,7 @@ export const MessageList = (props: MessageListProps) => {
// markAsUnreadSourceRef의 현재 값을 확인
const currentSource = markAsUnreadSourceRef.current;
console.log('Channel MessageList: markAsUnreadSourceRef current value:', currentSource);
-
+
messagesDispatcher({
type: messageActionTypes.MARK_AS_READ,
payload: { channel: currentGroupChannel },
diff --git a/src/modules/GroupChannel/components/Message/MessageView.tsx b/src/modules/GroupChannel/components/Message/MessageView.tsx
index 85324ffc1..951802fd3 100644
--- a/src/modules/GroupChannel/components/Message/MessageView.tsx
+++ b/src/modules/GroupChannel/components/Message/MessageView.tsx
@@ -50,6 +50,10 @@ export interface MessageProps {
* A function that customizes the rendering of the edit input portion of the message component.
* */
renderEditInput?: () => React.ReactElement;
+ /**
+ * A function that is called when the new message separator visibility changes.
+ */
+ onNewMessageSeparatorVisibilityChange?: (isVisible: boolean) => void;
/**
* @deprecated Please use `children` instead
* @description Customizes all child components of the message.
@@ -113,6 +117,7 @@ const MessageView = (props: MessageViewProps) => {
chainTop,
chainBottom,
handleScroll,
+ onNewMessageSeparatorVisibilityChange,
// MessageViewProps
channel,
@@ -422,7 +427,7 @@ const MessageView = (props: MessageViewProps) => {
{/* date-separator */}
{(hasSeparator || hasNewMessageSeparator)
&& (renderedCustomSeparator || (
-
+
diff --git a/src/modules/GroupChannel/components/MessageList/getMessagePartsInfo.ts b/src/modules/GroupChannel/components/MessageList/getMessagePartsInfo.ts
index fbf067ca4..67dc4ecc2 100644
--- a/src/modules/GroupChannel/components/MessageList/getMessagePartsInfo.ts
+++ b/src/modules/GroupChannel/components/MessageList/getMessagePartsInfo.ts
@@ -14,6 +14,7 @@ export interface GetMessagePartsInfoProps {
currentMessage: CoreMessageType;
currentChannel?: GroupChannel | null;
replyType?: string;
+ hasPrevious?: boolean;
firstUnreadMessageId?: number | string | undefined;
}
@@ -22,7 +23,6 @@ interface OutPuts {
chainBottom: boolean,
hasSeparator: boolean,
hasNewMessageSeparator: boolean,
- newFirstUnreadMessageId?: number | string | undefined,
}
/**
@@ -51,21 +51,13 @@ export const getMessagePartsInfo = ({
// https://stackoverflow.com/a/41855608
const hasSeparator = isLocalMessage ? false : !(previousMessageCreatedAt && (isSameDay(currentCreatedAt, previousMessageCreatedAt)));
- let hasNewMessageSeparator = false;
- let newFirstUnreadMessageId;
- if (!firstUnreadMessageId) {
- hasNewMessageSeparator = currentChannel.myLastRead === (currentCreatedAt - 1);
- if (hasNewMessageSeparator) newFirstUnreadMessageId = currentMessage.messageId;
- } else if (currentMessage.messageId === firstUnreadMessageId || currentChannel.myLastRead === (currentCreatedAt - 1)) {
- hasNewMessageSeparator = true;
- newFirstUnreadMessageId = currentMessage.messageId;
- }
+
+ const hasNewMessageSeparator = firstUnreadMessageId === currentMessage.messageId;
return {
chainTop,
chainBottom,
hasSeparator,
hasNewMessageSeparator,
- newFirstUnreadMessageId,
};
};
diff --git a/src/modules/GroupChannel/components/MessageList/index.tsx b/src/modules/GroupChannel/components/MessageList/index.tsx
index 22c614a9b..a0e0bf2c7 100644
--- a/src/modules/GroupChannel/components/MessageList/index.tsx
+++ b/src/modules/GroupChannel/components/MessageList/index.tsx
@@ -1,6 +1,6 @@
import './index.scss';
-import React, { useEffect, useState, useRef } from 'react';
-import type { Member } from '@sendbird/chat/groupChannel';
+import React, { useEffect, useState, useRef, useMemo } from 'react';
+import type { GroupChannel, Member } from '@sendbird/chat/groupChannel';
import { useGroupChannelHandler } from '@sendbird/uikit-tools';
import { CoreMessageType, isSendableMessage, getHTMLTextDirection, SendableMessageType } from '../../../../utils';
@@ -72,6 +72,7 @@ export const MessageList = (props: GroupChannelMessageListProps) => {
state: {
channelUrl,
hasNext,
+ hasPrevious,
loading,
messages,
newMessages,
@@ -87,12 +88,14 @@ export const MessageList = (props: GroupChannelMessageListProps) => {
scrollPositionRef,
scrollDistanceFromBottomRef,
markAsUnreadSourceRef,
+ readState,
},
actions: {
scrollToBottom,
setIsScrollBottomReached,
markAsReadAll,
markAsUnread,
+ setReadStateChanged,
},
} = useGroupChannel();
@@ -100,23 +103,123 @@ export const MessageList = (props: GroupChannelMessageListProps) => {
const { stringSet } = useLocalization();
const [unreadSinceDate, setUnreadSinceDate] = useState();
+ const [isChangedChannel, setIsChangedChannel] = useState(false);
+ const [showUnreadCount, setShowUnreadCount] = useState(true);
const firstUnreadMessageIdRef = useRef();
- const displayedNewMessageSeparatorRef = useRef(false);
+ const hasInitializedRef = useRef(false);
+ // current channel url ref
+ const currentChannelUrlRef = useRef();
+ // current messages ref
+ const currentMessagesRef = useRef([]);
+
+ /**
+ * Find the first unread message in the message list
+ */
+ const findFirstUnreadMessage = (
+ message: CoreMessageType,
+ index: number,
+ allMessages: CoreMessageType[],
+ currentChannel: GroupChannel,
+ hasPrevious: boolean,
+ ): boolean => {
+ const currentCreatedAt = message.createdAt;
+
+ // condition 1: message.createdAt === (channel.myLastRead + 1)
+ if (currentCreatedAt === (currentChannel.myLastRead + 1)) {
+ return true;
+ }
+
+ // condition 2: there is no previous message that satisfies the condition, and the current message is the first message that satisfies the condition
+ if (!hasPrevious && currentCreatedAt > (currentChannel.myLastRead + 1)) {
+ const hasPreviousMatchingMessage = allMessages
+ .slice(0, index)
+ .some(msg => msg.createdAt === (currentChannel.myLastRead + 1));
+ return !hasPreviousMatchingMessage;
+ }
+ return false;
+ };
+
+ const getFirstUnreadMessage = (): number | string => {
+ if (state.config.groupChannel.enableMarkAsUnread) {
+ for (let i = 0; i < messages.length; i++) {
+ const message = messages[i] as CoreMessageType;
+ const isFind = findFirstUnreadMessage(message, i, messages as CoreMessageType[], currentChannel, hasPrevious());
+ if (isFind) {
+ return message.messageId;
+ }
+ }
+ }
+ return undefined;
+ };
+
+ // check changed channel
useEffect(() => {
- if (isScrollBottomReached) {
- setUnreadSinceDate(undefined);
+ if (currentChannel?.url !== currentChannelUrlRef.current) {
+ currentChannelUrlRef.current = currentChannel?.url;
+
+ // initialize
+ firstUnreadMessageIdRef.current = undefined;
+ setShowUnreadCount(true);
+ hasInitializedRef.current = false;
+ if (markAsUnreadSourceRef?.current !== undefined) {
+ markAsUnreadSourceRef.current = undefined;
+ }
+ setIsChangedChannel(true);
} else {
- setUnreadSinceDate(new Date());
+ setIsChangedChannel(false);
}
- }, [isScrollBottomReached]);
+ }, [currentChannel?.url]);
+ // check changed messages
useEffect(() => {
- firstUnreadMessageIdRef.current = undefined;
- displayedNewMessageSeparatorRef.current = false;
- markAsUnreadSourceRef.current = undefined;
- }, [currentChannel]);
+ if (isChangedChannel) {
+ if (!hasInitializedRef.current) {
+ if (currentMessagesRef.current !== messages) {
+ currentMessagesRef.current = messages as CoreMessageType[];
+ const firstUnreadMessageId = getFirstUnreadMessage();
+ if (firstUnreadMessageId) {
+ firstUnreadMessageIdRef.current = firstUnreadMessageId;
+ }
+ hasInitializedRef.current = true;
+ }
+ }
+ }
+ }, [messages]);
+
+ useMemo(() => {
+ if (hasInitializedRef.current) {
+ const firstUnreadMessageId = getFirstUnreadMessage();
+
+ if (firstUnreadMessageId && firstUnreadMessageIdRef.current !== firstUnreadMessageId) {
+ firstUnreadMessageIdRef.current = firstUnreadMessageId;
+ }
+ }
+ }, [messages.length]);
+
+ useEffect(() => {
+ if (hasInitializedRef.current) {
+ if (readState === 'unread') {
+ // when readState === 'unread' find first unread message
+ const firstUnreadMessageId = getFirstUnreadMessage();
+ if (firstUnreadMessageId !== firstUnreadMessageIdRef.current) {
+ firstUnreadMessageIdRef.current = firstUnreadMessageId;
+ }
+ }
+ setReadStateChanged(null);
+ }
+ }, [readState]);
+
+ useEffect(() => {
+ if (!state.config.groupChannel.enableMarkAsUnread) {
+ if (isScrollBottomReached) {
+ setUnreadSinceDate(undefined);
+ } else {
+ setUnreadSinceDate(new Date());
+ }
+ }
+ }, [isScrollBottomReached]);
/**
* 1. Move the message list scroll
@@ -142,7 +245,7 @@ export const MessageList = (props: GroupChannelMessageListProps) => {
return renderFrozenNotification();
},
unreadMessagesNotification() {
- if (markAsUnreadSourceRef.current === 'menu' || displayedNewMessageSeparatorRef.current) return null;
+ if (markAsUnreadSourceRef?.current === 'manual' || !showUnreadCount) return null;
return (
{
lastReadAt={unreadSinceDate}
isFrozenChannel={currentChannel?.isFrozen || false}
onClick={() => {
- markAsReadAll();
+ markAsReadAll(currentChannel);
}}
/>
);
@@ -182,36 +285,15 @@ export const MessageList = (props: GroupChannelMessageListProps) => {
},
};
- const checkNewMessageSeparatorVisibility = () => {
- if (displayedNewMessageSeparatorRef.current) return false;
- if (currentChannel.unreadMessageCount === 0) return false;
-
- const newMessageSeparator = document.getElementById('new-message-separator');
- if (!newMessageSeparator) return false;
-
- const newMessageSeparatorRect = newMessageSeparator.getBoundingClientRect();
- const messageListContainer = document.querySelector('.sendbird-conversation__messages-padding');
- if (!messageListContainer) return false;
-
- const messageListContainerRect = messageListContainer.getBoundingClientRect();
- const isTopInContainer = newMessageSeparatorRect.top > messageListContainerRect.top;
-
- if (isTopInContainer) {
- displayedNewMessageSeparatorRef.current = true;
- return true;
- }
-
- return false;
- };
-
- const checkDisplayNewMessageSeparator = () => {
- if (!checkNewMessageSeparatorVisibility()) return;
-
- if (displayedNewMessageSeparatorRef.current && newMessages.length > 0) {
- markAsUnread(newMessages[0] as SendableMessageType, 'internal');
- firstUnreadMessageIdRef.current = newMessages[0].messageId;
- } else if (displayedNewMessageSeparatorRef.current) {
- markAsReadAll();
+ const checkDisplayedNewMessageSeparator = (isNewMessageSeparatorVisible: boolean) => {
+ if (isNewMessageSeparatorVisible) {
+ setShowUnreadCount(false);
+ if (newMessages.length > 0) {
+ markAsUnread(newMessages[0] as SendableMessageType, 'internal');
+ firstUnreadMessageIdRef.current = newMessages[0].messageId;
+ } else if (markAsUnreadSourceRef?.current !== 'manual') {
+ markAsReadAll(currentChannel);
+ }
}
};
@@ -241,22 +323,14 @@ export const MessageList = (props: GroupChannelMessageListProps) => {
onLoadPrevious={loadPrevious}
onScrollPosition={(it) => {
const isScrollBottomReached = it === 'bottom';
- if (isScrollBottomReached) {
- if (newMessages.length > 0) {
- resetNewMessages();
- }
- if (!state.config.groupChannel.enableMarkAsUnread || displayedNewMessageSeparatorRef.current) {
- markAsReadAll();
- }
- } else if (!displayedNewMessageSeparatorRef.current) {
- checkDisplayNewMessageSeparator();
+ if (hasInitializedRef.current && isScrollBottomReached && newMessages.length > 0) {
+ resetNewMessages();
}
-
setIsScrollBottomReached(isScrollBottomReached);
}}
messages={messages}
renderMessage={({ message, index }) => {
- const { chainTop, chainBottom, hasSeparator, hasNewMessageSeparator, newFirstUnreadMessageId } = getMessagePartsInfo({
+ const { chainTop, chainBottom, hasSeparator, hasNewMessageSeparator } = getMessagePartsInfo({
allMessages: messages as CoreMessageType[],
stringSet,
replyType: replyType ?? 'NONE',
@@ -264,18 +338,10 @@ export const MessageList = (props: GroupChannelMessageListProps) => {
currentIndex: index,
currentMessage: message as CoreMessageType,
currentChannel: currentChannel!,
+ hasPrevious: hasPrevious(),
firstUnreadMessageId: firstUnreadMessageIdRef.current,
});
- if (hasNewMessageSeparator && newFirstUnreadMessageId && newFirstUnreadMessageId !== firstUnreadMessageIdRef.current) {
- firstUnreadMessageIdRef.current = newFirstUnreadMessageId;
- if (markAsUnreadSourceRef.current === 'menu') {
- checkNewMessageSeparatorVisibility();
- } else {
- checkDisplayNewMessageSeparator();
- }
- }
-
const isOutgoingMessage = isSendableMessage(message) && message.sender.userId === state.config.userId;
return (
@@ -289,6 +355,7 @@ export const MessageList = (props: GroupChannelMessageListProps) => {
renderMessageContent,
renderSuggestedReplies,
renderCustomSeparator,
+ onNewMessageSeparatorVisibilityChange: checkDisplayedNewMessageSeparator,
})}
);
diff --git a/src/modules/GroupChannel/context/GroupChannelProvider.tsx b/src/modules/GroupChannel/context/GroupChannelProvider.tsx
index 8af16c418..51cee1faa 100644
--- a/src/modules/GroupChannel/context/GroupChannelProvider.tsx
+++ b/src/modules/GroupChannel/context/GroupChannelProvider.tsx
@@ -46,6 +46,7 @@ const initialState = {
quoteMessage: null,
animatedMessageId: null,
isScrollBottomReached: true,
+ readState: null,
scrollRef: { current: null },
scrollDistanceFromBottomRef: { current: 0 },
@@ -150,7 +151,7 @@ const GroupChannelManager :React.FC(undefined);
+ const markAsUnreadSourceRef = useRef<'manual' | 'internal' | undefined>(undefined);
- const markAsUnread = useMemo(() => (message: any, source?: 'menu' | 'internal') => {
+ const markAsUnread = useMemo(() => (message: any, source?: 'manual' | 'internal') => {
if (!config.groupChannel.enableMarkAsUnread) return;
if (!state.currentChannel) {
logger?.error?.('GroupChannelProvider: channel is required for markAsUnread');
@@ -235,8 +236,15 @@ const GroupChannelManager :React.FC {
- if (ctx.source === CollectionEventSource.EVENT_CHANNEL_UNREAD) {
- console.log('MADOKA #1 onChannelUpdated', ctx.source, ctx.userIds);
+ if (ctx.source === CollectionEventSource.EVENT_CHANNEL_UNREAD
+ && ctx.userIds.includes(userId)
+ ) {
+ actions.setReadStateChanged('unread');
+ }
+ if (ctx.source === CollectionEventSource.EVENT_CHANNEL_READ
+ && ctx.userIds.includes(userId)
+ ) {
+ actions.setReadStateChanged('read');
}
actions.setCurrentChannel(channel);
},
diff --git a/src/modules/GroupChannel/context/hooks/useGroupChannel.ts b/src/modules/GroupChannel/context/hooks/useGroupChannel.ts
index e22ff3dc9..2033ccabe 100644
--- a/src/modules/GroupChannel/context/hooks/useGroupChannel.ts
+++ b/src/modules/GroupChannel/context/hooks/useGroupChannel.ts
@@ -24,8 +24,9 @@ export interface GroupChannelActions extends MessageActions {
// Channel actions
setCurrentChannel: (channel: GroupChannel) => void;
handleChannelError: (error: SendbirdError) => void;
- markAsReadAll: () => void;
- markAsUnread: (message: SendableMessageType, source?: 'menu' | 'internal') => void;
+ markAsReadAll: (channel: GroupChannel) => void;
+ markAsUnread: (message: SendableMessageType, source?: 'manual' | 'internal') => void;
+ setReadStateChanged: (state: string) => void;
// Message actions
sendUserMessage: (params: UserMessageCreateParams) => Promise;
@@ -88,14 +89,11 @@ export const useGroupChannel = () => {
}
}, [state.scrollRef.current, config.isOnline, markAsReadScheduler]);
- const markAsReadAll = useCallback(() => {
- if (!state.markAsUnreadSourceRef?.current) {
- state.resetNewMessages();
- }
+ const markAsReadAll = useCallback((channel: GroupChannel) => {
if (config.isOnline && !state.disableMarkAsRead) {
- markAsReadScheduler.push(state.currentChannel);
+ markAsReadScheduler.push(channel);
}
- }, [config.isOnline, state.disableMarkAsRead, state.currentChannel]);
+ }, [config.isOnline]);
const scrollToMessage = useCallback(async (
createdAt: number,
@@ -198,12 +196,17 @@ export const useGroupChannel = () => {
store.setState(state => ({ ...state, quoteMessage: message }));
}, []);
+ const setReadStateChanged = useCallback((readState: string) => {
+ store.setState(state => ({ ...state, readState }));
+ }, []);
+
const actions: GroupChannelActions = useMemo(() => {
return {
setCurrentChannel,
handleChannelError,
markAsReadAll,
markAsUnread: state.markAsUnread,
+ setReadStateChanged,
setQuoteMessage,
scrollToBottom,
scrollToMessage,
@@ -217,6 +220,7 @@ export const useGroupChannel = () => {
handleChannelError,
markAsReadAll,
state.markAsUnread,
+ setReadStateChanged,
setQuoteMessage,
scrollToBottom,
scrollToMessage,
diff --git a/src/modules/GroupChannel/context/types.ts b/src/modules/GroupChannel/context/types.ts
index 490a1e3ac..3c647e719 100644
--- a/src/modules/GroupChannel/context/types.ts
+++ b/src/modules/GroupChannel/context/types.ts
@@ -46,6 +46,7 @@ interface InternalGroupChannelState extends MessageDataSource {
quoteMessage: SendableMessageType | null;
animatedMessageId: number | null;
isScrollBottomReached: boolean;
+ readState: string | null;
// References - will be managed together
scrollRef: React.RefObject;
@@ -65,7 +66,7 @@ interface InternalGroupChannelState extends MessageDataSource {
// Actions (React UIKit specific)
markAsUnread?: (message: SendableMessageType) => void;
- markAsUnreadSourceRef: React.MutableRefObject<'menu' | 'internal' | null>;
+ markAsUnreadSourceRef: React.MutableRefObject<'manual' | 'internal' | null>;
// Legacy - Will be removed after migration
scrollPubSub: PubSubTypes;
diff --git a/src/modules/GroupChannelList/components/GroupChannelListItem/GroupChannelListItemView.tsx b/src/modules/GroupChannelList/components/GroupChannelListItem/GroupChannelListItemView.tsx
index 704a5fbae..4ff5f0b45 100644
--- a/src/modules/GroupChannelList/components/GroupChannelListItem/GroupChannelListItemView.tsx
+++ b/src/modules/GroupChannelList/components/GroupChannelListItem/GroupChannelListItemView.tsx
@@ -176,8 +176,9 @@ export const GroupChannelListItemView = ({
* Do not show unread count for focused channel. This is because of the limitation where
* isScrollBottom and hasNext states needs to be added globally but they are from channel context
* so channel list cannot see them with the current architecture.
+ * However, when enableMarkAsUnread is true, we show unread count even for selected channels.
*/
- !isSelected && !channel.isEphemeral && (
+ (!isSelected || config.groupChannel.enableMarkAsUnread) && !channel.isEphemeral && (
{isMentionEnabled && channel.unreadMentionCount > 0 ? (
;
separatorColor?: Colors;
hasNewMessageSeparator?: boolean;
+ onVisibilityChange?: (isVisible: boolean) => void;
}
+
const DateSeparator = ({
children = undefined,
className = '',
hasNewMessageSeparator = false,
+ onVisibilityChange,
separatorColor = hasNewMessageSeparator ? Colors.PRIMARY : Colors.ONBACKGROUND_4,
}: DateSeparatorProps): ReactElement => {
+ const separatorRef = useRef(null);
+
+ const handleVisibilityChange = useCallback((isVisible: boolean) => {
+ onVisibilityChange?.(isVisible);
+ }, [onVisibilityChange]);
+
+ useLayoutEffect(() => {
+ const element = separatorRef.current;
+ if (!element || !hasNewMessageSeparator || !onVisibilityChange) return;
+
+ const observer = new IntersectionObserver(
+ (entries) => {
+ entries.forEach((entry) => {
+ const visible = entry.isIntersecting;
+ handleVisibilityChange(visible);
+ });
+ },
+ {
+ threshold: 1.0,
+ rootMargin: '0px',
+ root: null, // viewport를 기준으로 관찰
+ },
+ );
+
+ observer.observe(element);
+
+ return () => {
+ observer.disconnect();
+ };
+ }, [hasNewMessageSeparator, handleVisibilityChange, onVisibilityChange]);
+
return (
void;
deleteMessage: (message: SendableMessageType) => void;
resendMessage: (message: SendableMessageType) => void;
- markAsUnread?: (message: SendableMessageType, source?: 'menu' | 'internal') => void;
+ markAsUnread?: (message: SendableMessageType, source?: 'manual' | 'internal') => void;
isOnline: boolean;
disableDeleteMessage: boolean | null;
triggerRef: MutableRefObject
;
diff --git a/src/ui/MessageMenu/menuItems/BottomSheetMenuItems.tsx b/src/ui/MessageMenu/menuItems/BottomSheetMenuItems.tsx
index f5b10f6e8..f37b9623b 100644
--- a/src/ui/MessageMenu/menuItems/BottomSheetMenuItems.tsx
+++ b/src/ui/MessageMenu/menuItems/BottomSheetMenuItems.tsx
@@ -218,7 +218,7 @@ export const MarkAsUnreadMenuItem = (props: PrebuildMenuItemPropsType) => {
{...props}
onClick={(e) => {
if (markAsUnread) {
- markAsUnread(message, 'menu');
+ markAsUnread(message, 'manual');
}
hideMenu();
props.onClick?.(e);
diff --git a/src/ui/MessageMenu/menuItems/MessageMenuItems.tsx b/src/ui/MessageMenu/menuItems/MessageMenuItems.tsx
index 0d44fbfbb..c1c2f3fed 100644
--- a/src/ui/MessageMenu/menuItems/MessageMenuItems.tsx
+++ b/src/ui/MessageMenu/menuItems/MessageMenuItems.tsx
@@ -189,7 +189,7 @@ export const MarkAsUnreadMenuItem = (props: PrebuildMenuItemPropsType) => {
{...props}
onClick={(e) => {
if (markAsUnread) {
- markAsUnread(message, 'menu');
+ markAsUnread(message, 'manual');
}
hideMenu();
props.onClick?.(e);
diff --git a/src/ui/MessageMenu/menuItems/MobileMenuItems.tsx b/src/ui/MessageMenu/menuItems/MobileMenuItems.tsx
index 94b526840..8e6cc8b75 100644
--- a/src/ui/MessageMenu/menuItems/MobileMenuItems.tsx
+++ b/src/ui/MessageMenu/menuItems/MobileMenuItems.tsx
@@ -217,7 +217,7 @@ export const MarkAsUnreadMenuItem = (props: PrebuildMenuItemPropsType) => {
{...props}
onClick={(e) => {
if (markAsUnread) {
- markAsUnread(message, 'menu');
+ markAsUnread(message, 'manual');
}
hideMenu();
props.onClick?.(e);
From 273bd7280e9558c89d11b33b5bea5eb64902bb7c Mon Sep 17 00:00:00 2001
From: danney-chun <63285271+danney-chun@users.noreply.github.com>
Date: Fri, 4 Jul 2025 11:29:48 +0900
Subject: [PATCH 04/19] fixed etc
---
.../components/Message/MessageView.tsx | 14 ++++++++------
.../GroupChannel/components/MessageList/index.tsx | 8 ++++----
2 files changed, 12 insertions(+), 10 deletions(-)
diff --git a/src/modules/GroupChannel/components/Message/MessageView.tsx b/src/modules/GroupChannel/components/Message/MessageView.tsx
index 951802fd3..6f4accd77 100644
--- a/src/modules/GroupChannel/components/Message/MessageView.tsx
+++ b/src/modules/GroupChannel/components/Message/MessageView.tsx
@@ -403,12 +403,14 @@ const MessageView = (props: MessageViewProps) => {
}
const seperatorLabelText = useMemo(() => {
- if (!hasSeparator && hasNewMessageSeparator) {
- return 'New Message';
- } else {
- return format(message.createdAt, stringSet.DATE_FORMAT__MESSAGE_LIST__DATE_SEPARATOR, {
- locale: dateLocale,
- });
+ if (hasSeparator) {
+ if (hasNewMessageSeparator) {
+ return 'New Message';
+ } else {
+ return format(message.createdAt, stringSet.DATE_FORMAT__MESSAGE_LIST__DATE_SEPARATOR, {
+ locale: dateLocale,
+ });
+ }
}
}, [hasSeparator, hasNewMessageSeparator]);
diff --git a/src/modules/GroupChannel/components/MessageList/index.tsx b/src/modules/GroupChannel/components/MessageList/index.tsx
index a0e0bf2c7..afe009914 100644
--- a/src/modules/GroupChannel/components/MessageList/index.tsx
+++ b/src/modules/GroupChannel/components/MessageList/index.tsx
@@ -172,9 +172,9 @@ export const MessageList = (props: GroupChannelMessageListProps) => {
}
}, [currentChannel?.url]);
- // check changed messages
+ // when enableMarkAsUnread is true, check changed messages
useEffect(() => {
- if (isChangedChannel) {
+ if (state.config.groupChannel.enableMarkAsUnread && isChangedChannel) {
if (!hasInitializedRef.current) {
if (currentMessagesRef.current !== messages) {
currentMessagesRef.current = messages as CoreMessageType[];
@@ -189,7 +189,7 @@ export const MessageList = (props: GroupChannelMessageListProps) => {
}, [messages]);
useMemo(() => {
- if (hasInitializedRef.current) {
+ if (state.config.groupChannel.enableMarkAsUnread && hasInitializedRef.current) {
const firstUnreadMessageId = getFirstUnreadMessage();
if (firstUnreadMessageId && firstUnreadMessageIdRef.current !== firstUnreadMessageId) {
@@ -199,7 +199,7 @@ export const MessageList = (props: GroupChannelMessageListProps) => {
}, [messages.length]);
useEffect(() => {
- if (hasInitializedRef.current) {
+ if (state.config.groupChannel.enableMarkAsUnread && hasInitializedRef.current) {
if (readState === 'unread') {
// when readState === 'unread' find first unread message
const firstUnreadMessageId = getFirstUnreadMessage();
From 7f80ed99299ba9ea0257e3ca5cc5ca76a2555d9f Mon Sep 17 00:00:00 2001
From: danney-chun <63285271+danney-chun@users.noreply.github.com>
Date: Fri, 4 Jul 2025 11:42:22 +0900
Subject: [PATCH 05/19] Update MessageView.tsx
---
.../components/Message/MessageView.tsx | 14 ++++++--------
1 file changed, 6 insertions(+), 8 deletions(-)
diff --git a/src/modules/GroupChannel/components/Message/MessageView.tsx b/src/modules/GroupChannel/components/Message/MessageView.tsx
index 6f4accd77..b3d7ac803 100644
--- a/src/modules/GroupChannel/components/Message/MessageView.tsx
+++ b/src/modules/GroupChannel/components/Message/MessageView.tsx
@@ -403,14 +403,12 @@ const MessageView = (props: MessageViewProps) => {
}
const seperatorLabelText = useMemo(() => {
- if (hasSeparator) {
- if (hasNewMessageSeparator) {
- return 'New Message';
- } else {
- return format(message.createdAt, stringSet.DATE_FORMAT__MESSAGE_LIST__DATE_SEPARATOR, {
- locale: dateLocale,
- });
- }
+ if (!hasSeparator && hasNewMessageSeparator) {
+ return 'New Message';
+ } else if (hasSeparator) {
+ return format(message.createdAt, stringSet.DATE_FORMAT__MESSAGE_LIST__DATE_SEPARATOR, {
+ locale: dateLocale,
+ });
}
}, [hasSeparator, hasNewMessageSeparator]);
From 66865f6b86d52f8e70986ca43eb9f84bb0e15f1a Mon Sep 17 00:00:00 2001
From: danney-chun <63285271+danney-chun@users.noreply.github.com>
Date: Thu, 10 Jul 2025 00:26:37 +0900
Subject: [PATCH 06/19] apply feed
---
.../Channel/components/MessageList/index.tsx | 88 +++++++---
.../index.tsx | 2 +-
.../UnreadCountFloatingButton/index.tsx | 3 +
.../components/Message/MessageView.tsx | 28 +--
.../MessageList/getMessagePartsInfo.ts | 4 +-
.../components/MessageList/index.tsx | 74 +++++---
.../index.scss | 0
.../index.tsx | 21 ++-
.../components/UnreadCount/index.scss | 165 ++++++------------
.../components/UnreadCount/index.tsx | 35 ++--
.../UnreadCountFloatingButton/index.scss | 120 +++++++++++++
.../UnreadCountFloatingButton/index.tsx | 61 +++++++
.../context/GroupChannelProvider.tsx | 7 +-
.../context/hooks/useGroupChannel.ts | 20 ++-
.../context/hooks/useMessageActions.ts | 17 +-
src/modules/GroupChannel/context/types.ts | 3 +
src/ui/DateSeparator/index.tsx | 40 +----
src/ui/Label/stringSet.ts | 4 +-
src/ui/NewMessageSeparator/index.scss | 27 +++
src/ui/NewMessageSeparator/index.tsx | 80 +++++++++
yarn.lock | 6 +-
21 files changed, 552 insertions(+), 253 deletions(-)
rename src/modules/Channel/components/{NewMessageCount => NewMessageCountFloatingButton}/index.tsx (76%)
create mode 100644 src/modules/Channel/components/UnreadCountFloatingButton/index.tsx
rename src/modules/GroupChannel/components/{NewMessageCount => NewMessageCountFloatingButton}/index.scss (100%)
rename src/modules/GroupChannel/components/{NewMessageCount => NewMessageCountFloatingButton}/index.tsx (79%)
create mode 100644 src/modules/GroupChannel/components/UnreadCountFloatingButton/index.scss
create mode 100644 src/modules/GroupChannel/components/UnreadCountFloatingButton/index.tsx
create mode 100644 src/ui/NewMessageSeparator/index.scss
create mode 100644 src/ui/NewMessageSeparator/index.tsx
diff --git a/src/modules/Channel/components/MessageList/index.tsx b/src/modules/Channel/components/MessageList/index.tsx
index c863b3c1a..050ea134d 100644
--- a/src/modules/Channel/components/MessageList/index.tsx
+++ b/src/modules/Channel/components/MessageList/index.tsx
@@ -11,7 +11,8 @@ import Message from '../Message';
import { EveryMessage, TypingIndicatorType } from '../../../../types';
import { isAboutSame } from '../../context/utils';
import UnreadCount from '../UnreadCount';
-import NewMessageCount from '../NewMessageCount';
+import UnreadCountFloatingButton from '../UnreadCountFloatingButton';
+import NewMessageCountFloatingButton from '../NewMessageCountFloatingButton';
import FrozenNotification from '../FrozenNotification';
import { SCROLL_BUFFER } from '../../../../utils/consts';
import { MessageProvider } from '../../../Message/context/MessageProvider';
@@ -184,8 +185,8 @@ export const MessageList = (props: MessageListProps) => {
const isShowUnreadCount = useMemo(() => {
if (store?.config?.groupChannel?.enableMarkAsUnread) {
- // markAsUnread is enabled
- if (currentGroupChannel?.unreadMessageCount > 0) {
+ // markAsUnread is enabled - 스크롤이 bottom에 있을 때는 표시하지 않음
+ if (currentGroupChannel?.unreadMessageCount > 0 && !isScrollBottom) {
return true;
}
return false;
@@ -199,8 +200,12 @@ export const MessageList = (props: MessageListProps) => {
}, [currentGroupChannel.unreadMessageCount, isScrollBottom]);
const isShowNewMessageCount = useMemo(() => {
+ // 스크롤이 bottom에 있을 때는 new message count를 표시하지 않음
+ if (isScrollBottom) {
+ return false;
+ }
if (!store?.config?.groupChannel?.enableMarkAsUnread
- && (!isScrollBottom || hasMoreNext)
+ && hasMoreNext
&& (unreadSince || unreadSinceDate)) {
return true;
}
@@ -215,6 +220,54 @@ export const MessageList = (props: MessageListProps) => {
return renderPlaceholderEmpty();
}
+ const renderUnreadCount = () => {
+ if (isShowUnreadCount) {
+ if (!store?.config?.groupChannel?.enableMarkAsUnread) {
+ return (
+ {
+ if (scrollRef?.current) scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
+ if (!disableMarkAsRead && !!currentGroupChannel) {
+ markAsReadScheduler.push(currentGroupChannel);
+ messagesDispatcher({
+ type: messageActionTypes.MARK_AS_READ,
+ payload: { channel: currentGroupChannel },
+ });
+ }
+ setInitialTimeStamp(null);
+ setAnimatedMessageId(null);
+ setHighLightedMessageId(null);
+ }}
+ />
+ );
+ } else {
+ return (
+ {
+ if (scrollRef?.current) scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
+ if (!disableMarkAsRead && !!currentGroupChannel) {
+ markAsReadScheduler.push(currentGroupChannel);
+ messagesDispatcher({
+ type: messageActionTypes.MARK_AS_READ,
+ payload: { channel: currentGroupChannel },
+ });
+ }
+ setInitialTimeStamp(null);
+ setAnimatedMessageId(null);
+ setHighLightedMessageId(null);
+ }}
+ />
+ );
+ }
+ }
+ };
+
return (
<>
{!isScrolled && }
@@ -302,30 +355,7 @@ export const MessageList = (props: MessageListProps) => {
{currentGroupChannel?.isFrozen && renderFrozenNotification()}
- {
- isShowUnreadCount && (
- {
- if (scrollRef?.current) scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
- if (!disableMarkAsRead && !!currentGroupChannel) {
- markAsReadScheduler.push(currentGroupChannel);
- messagesDispatcher({
- type: messageActionTypes.MARK_AS_READ,
- payload: { channel: currentGroupChannel },
- });
- }
- setInitialTimeStamp(null);
- setAnimatedMessageId(null);
- setHighLightedMessageId(null);
- }}
- />
- )
- }
+ {renderUnreadCount()}
{
// This flag is an unmatched variable
scrollBottom > SCROLL_BOTTOM_PADDING && (
@@ -343,7 +373,7 @@ export const MessageList = (props: MessageListProps) => {
{
/* NewMessageCount - positioned at the bottom of MessageList */
(isShowNewMessageCount) && (
- {
diff --git a/src/modules/Channel/components/NewMessageCount/index.tsx b/src/modules/Channel/components/NewMessageCountFloatingButton/index.tsx
similarity index 76%
rename from src/modules/Channel/components/NewMessageCount/index.tsx
rename to src/modules/Channel/components/NewMessageCountFloatingButton/index.tsx
index cc846e9ba..16efd3265 100644
--- a/src/modules/Channel/components/NewMessageCount/index.tsx
+++ b/src/modules/Channel/components/NewMessageCountFloatingButton/index.tsx
@@ -1,3 +1,3 @@
-import NewMessageCount from '../../../GroupChannel/components/NewMessageCount';
+import NewMessageCount from '../../../GroupChannel/components/NewMessageCountFloatingButton';
export default NewMessageCount;
diff --git a/src/modules/Channel/components/UnreadCountFloatingButton/index.tsx b/src/modules/Channel/components/UnreadCountFloatingButton/index.tsx
new file mode 100644
index 000000000..9f99d1be7
--- /dev/null
+++ b/src/modules/Channel/components/UnreadCountFloatingButton/index.tsx
@@ -0,0 +1,3 @@
+import UnreadCountFloatingButton from '../../../GroupChannel/components/UnreadCountFloatingButton';
+
+export default UnreadCountFloatingButton;
diff --git a/src/modules/GroupChannel/components/Message/MessageView.tsx b/src/modules/GroupChannel/components/Message/MessageView.tsx
index b3d7ac803..502f15818 100644
--- a/src/modules/GroupChannel/components/Message/MessageView.tsx
+++ b/src/modules/GroupChannel/components/Message/MessageView.tsx
@@ -22,6 +22,7 @@ import SuggestedMentionListView from '../SuggestedMentionList/SuggestedMentionLi
import type { OnBeforeDownloadFileMessageType } from '../../context/types';
import { classnames, deleteNullish } from '../../../../utils/utils';
import useSendbird from '../../../../lib/Sendbird/context/hooks/useSendbird';
+import NewMessageIndicator from '../../../../ui/NewMessageSeparator';
export interface MessageProps {
message: EveryMessage;
@@ -402,16 +403,6 @@ const MessageView = (props: MessageViewProps) => {
);
}
- const seperatorLabelText = useMemo(() => {
- if (!hasSeparator && hasNewMessageSeparator) {
- return 'New Message';
- } else if (hasSeparator) {
- return format(message.createdAt, stringSet.DATE_FORMAT__MESSAGE_LIST__DATE_SEPARATOR, {
- locale: dateLocale,
- });
- }
- }, [hasSeparator, hasNewMessageSeparator]);
-
return (
{
ref={messageScrollRef}
>
{/* date-separator */}
- {(hasSeparator || hasNewMessageSeparator)
+ {hasSeparator
&& (renderedCustomSeparator || (
-
+
))}
+ {/* new message indicator */}
+ {hasNewMessageSeparator
+ && (
+
+
+
+ )}
{renderChildren()}
);
diff --git a/src/modules/GroupChannel/components/MessageList/getMessagePartsInfo.ts b/src/modules/GroupChannel/components/MessageList/getMessagePartsInfo.ts
index 67dc4ecc2..c01dfc771 100644
--- a/src/modules/GroupChannel/components/MessageList/getMessagePartsInfo.ts
+++ b/src/modules/GroupChannel/components/MessageList/getMessagePartsInfo.ts
@@ -16,6 +16,7 @@ export interface GetMessagePartsInfoProps {
replyType?: string;
hasPrevious?: boolean;
firstUnreadMessageId?: number | string | undefined;
+ isCurrentDeviceMessage?: boolean;
}
interface OutPuts {
@@ -37,6 +38,7 @@ export const getMessagePartsInfo = ({
currentChannel = null,
replyType = '',
firstUnreadMessageId,
+ isCurrentDeviceMessage,
}: GetMessagePartsInfoProps): OutPuts => {
const previousMessage = allMessages[currentIndex - 1];
const nextMessage = allMessages[currentIndex + 1];
@@ -52,7 +54,7 @@ export const getMessagePartsInfo = ({
// https://stackoverflow.com/a/41855608
const hasSeparator = isLocalMessage ? false : !(previousMessageCreatedAt && (isSameDay(currentCreatedAt, previousMessageCreatedAt)));
- const hasNewMessageSeparator = firstUnreadMessageId === currentMessage.messageId;
+ const hasNewMessageSeparator = !isCurrentDeviceMessage && firstUnreadMessageId === currentMessage.messageId;
return {
chainTop,
diff --git a/src/modules/GroupChannel/components/MessageList/index.tsx b/src/modules/GroupChannel/components/MessageList/index.tsx
index afe009914..7155b801c 100644
--- a/src/modules/GroupChannel/components/MessageList/index.tsx
+++ b/src/modules/GroupChannel/components/MessageList/index.tsx
@@ -1,5 +1,5 @@
import './index.scss';
-import React, { useEffect, useState, useRef, useMemo } from 'react';
+import React, { useEffect, useState, useRef } from 'react';
import type { GroupChannel, Member } from '@sendbird/chat/groupChannel';
import { useGroupChannelHandler } from '@sendbird/uikit-tools';
@@ -10,7 +10,8 @@ import PlaceHolder, { PlaceHolderTypes } from '../../../../ui/PlaceHolder';
import Icon, { IconColors, IconTypes } from '../../../../ui/Icon';
import Message from '../Message';
import UnreadCount from '../UnreadCount';
-import NewMessageCount from '../NewMessageCount';
+import UnreadCountFloatingButton from '../UnreadCountFloatingButton';
+import NewMessageCountFloatingButton from '../NewMessageCountFloatingButton';
import FrozenNotification from '../FrozenNotification';
import { SCROLL_BUFFER } from '../../../../utils/consts';
import TypingIndicatorBubble from '../../../../ui/TypingIndicatorBubble';
@@ -89,6 +90,7 @@ export const MessageList = (props: GroupChannelMessageListProps) => {
scrollDistanceFromBottomRef,
markAsUnreadSourceRef,
readState,
+ currentDeviceMessageIdsRef,
},
actions: {
scrollToBottom,
@@ -96,6 +98,7 @@ export const MessageList = (props: GroupChannelMessageListProps) => {
markAsReadAll,
markAsUnread,
setReadStateChanged,
+ isFromCurrentDevice,
},
} = useGroupChannel();
@@ -109,8 +112,8 @@ export const MessageList = (props: GroupChannelMessageListProps) => {
const firstUnreadMessageIdRef = useRef();
const hasInitializedRef = useRef(false);
- // current channel url ref
- const currentChannelUrlRef = useRef();
+ // current channel ref
+ const currentChannelRef = useRef(undefined);
// current messages ref
const currentMessagesRef = useRef([]);
@@ -122,7 +125,6 @@ export const MessageList = (props: GroupChannelMessageListProps) => {
index: number,
allMessages: CoreMessageType[],
currentChannel: GroupChannel,
- hasPrevious: boolean,
): boolean => {
const currentCreatedAt = message.createdAt;
@@ -131,8 +133,8 @@ export const MessageList = (props: GroupChannelMessageListProps) => {
return true;
}
- // condition 2: there is no previous message that satisfies the condition, and the current message is the first message that satisfies the condition
- if (!hasPrevious && currentCreatedAt > (currentChannel.myLastRead + 1)) {
+ // condition 2: there is no message that satisfies the condition, and the current message is the first message that satisfies the condition
+ if (currentCreatedAt > (currentChannel.myLastRead + 1)) {
const hasPreviousMatchingMessage = allMessages
.slice(0, index)
.some(msg => msg.createdAt === (currentChannel.myLastRead + 1));
@@ -145,8 +147,9 @@ export const MessageList = (props: GroupChannelMessageListProps) => {
if (state.config.groupChannel.enableMarkAsUnread) {
for (let i = 0; i < messages.length; i++) {
const message = messages[i] as CoreMessageType;
- const isFind = findFirstUnreadMessage(message, i, messages as CoreMessageType[], currentChannel, hasPrevious());
- if (isFind) {
+ const isFind = findFirstUnreadMessage(message, i, messages as CoreMessageType[], currentChannel);
+
+ if (isFind && !isFromCurrentDevice(message.messageId)) {
return message.messageId;
}
}
@@ -156,8 +159,8 @@ export const MessageList = (props: GroupChannelMessageListProps) => {
// check changed channel
useEffect(() => {
- if (currentChannel?.url !== currentChannelUrlRef.current) {
- currentChannelUrlRef.current = currentChannel?.url;
+ if (currentChannel?.url !== currentChannelRef.current?.url) {
+ currentChannelRef.current = currentChannel;
// initialize
firstUnreadMessageIdRef.current = undefined;
@@ -167,6 +170,7 @@ export const MessageList = (props: GroupChannelMessageListProps) => {
markAsUnreadSourceRef.current = undefined;
}
setIsChangedChannel(true);
+ currentDeviceMessageIdsRef.current.clear();
} else {
setIsChangedChannel(false);
}
@@ -188,8 +192,10 @@ export const MessageList = (props: GroupChannelMessageListProps) => {
}
}, [messages]);
- useMemo(() => {
+ useEffect(() => {
if (state.config.groupChannel.enableMarkAsUnread && hasInitializedRef.current) {
+ if (firstUnreadMessageIdRef.current) return;
+
const firstUnreadMessageId = getFirstUnreadMessage();
if (firstUnreadMessageId && firstUnreadMessageIdRef.current !== firstUnreadMessageId) {
@@ -203,6 +209,7 @@ export const MessageList = (props: GroupChannelMessageListProps) => {
if (readState === 'unread') {
// when readState === 'unread' find first unread message
const firstUnreadMessageId = getFirstUnreadMessage();
+
if (firstUnreadMessageId !== firstUnreadMessageIdRef.current) {
firstUnreadMessageIdRef.current = firstUnreadMessageId;
}
@@ -218,6 +225,12 @@ export const MessageList = (props: GroupChannelMessageListProps) => {
} else {
setUnreadSinceDate(new Date());
}
+ } else if (isScrollBottomReached) {
+ if (!firstUnreadMessageIdRef.current && currentChannel?.unreadMessageCount === 0) {
+ markAsReadAll(currentChannel);
+ } else if (!firstUnreadMessageIdRef.current) {
+ markAsReadAll(currentChannel);
+ }
}
}, [isScrollBottomReached]);
@@ -245,16 +258,26 @@ export const MessageList = (props: GroupChannelMessageListProps) => {
return renderFrozenNotification();
},
unreadMessagesNotification() {
- if (markAsUnreadSourceRef?.current === 'manual' || !showUnreadCount) return null;
+ if (state.config.groupChannel.enableMarkAsUnread) {
+ if (!showUnreadCount) return null;
+ return (
+ {
+ markAsReadAll(currentChannel);
+ }}
+ />
+ );
+ }
+ if (isScrollBottomReached || !unreadSinceDate) return null;
return (
{
- markAsReadAll(currentChannel);
- }}
+ onClick={() => scrollToBottom()}
/>
);
},
@@ -274,9 +297,10 @@ export const MessageList = (props: GroupChannelMessageListProps) => {
);
},
newMessageCount() {
+ // 스크롤이 bottom에 있을 때는 new message count를 표시하지 않음
if (isScrollBottomReached) return null;
return (
- scrollToBottom()}
@@ -291,9 +315,13 @@ export const MessageList = (props: GroupChannelMessageListProps) => {
if (newMessages.length > 0) {
markAsUnread(newMessages[0] as SendableMessageType, 'internal');
firstUnreadMessageIdRef.current = newMessages[0].messageId;
- } else if (markAsUnreadSourceRef?.current !== 'manual') {
+ } else if (currentChannel?.lastMessage.createdAt > currentChannel.myLastRead
+ && markAsUnreadSourceRef?.current !== 'manual')
+ {
markAsReadAll(currentChannel);
}
+ } else if (currentChannel?.unreadMessageCount > 0) {
+ setShowUnreadCount(true);
}
};
@@ -330,6 +358,8 @@ export const MessageList = (props: GroupChannelMessageListProps) => {
}}
messages={messages}
renderMessage={({ message, index }) => {
+ const isCurrentDeviceMessage = isFromCurrentDevice(message.messageId);
+
const { chainTop, chainBottom, hasSeparator, hasNewMessageSeparator } = getMessagePartsInfo({
allMessages: messages as CoreMessageType[],
stringSet,
@@ -340,9 +370,11 @@ export const MessageList = (props: GroupChannelMessageListProps) => {
currentChannel: currentChannel!,
hasPrevious: hasPrevious(),
firstUnreadMessageId: firstUnreadMessageIdRef.current,
+ isCurrentDeviceMessage,
});
const isOutgoingMessage = isSendableMessage(message) && message.sender.userId === state.config.userId;
+
return (
{renderMessage({
diff --git a/src/modules/GroupChannel/components/NewMessageCount/index.scss b/src/modules/GroupChannel/components/NewMessageCountFloatingButton/index.scss
similarity index 100%
rename from src/modules/GroupChannel/components/NewMessageCount/index.scss
rename to src/modules/GroupChannel/components/NewMessageCountFloatingButton/index.scss
diff --git a/src/modules/GroupChannel/components/NewMessageCount/index.tsx b/src/modules/GroupChannel/components/NewMessageCountFloatingButton/index.tsx
similarity index 79%
rename from src/modules/GroupChannel/components/NewMessageCount/index.tsx
rename to src/modules/GroupChannel/components/NewMessageCountFloatingButton/index.tsx
index 4b1c8ef84..8b98d61eb 100644
--- a/src/modules/GroupChannel/components/NewMessageCount/index.tsx
+++ b/src/modules/GroupChannel/components/NewMessageCountFloatingButton/index.tsx
@@ -5,6 +5,7 @@ import { LocalizationContext } from '../../../../lib/LocalizationContext';
import Label, { LabelTypography, LabelColors } from '../../../../ui/Label';
import Icon, { IconTypes, IconColors } from '../../../../ui/Icon';
import { classnames } from '../../../../utils/utils';
+import { useMediaQueryContext } from '../../../../lib/MediaQueryContext';
export interface NewMessageCountProps {
className?: string;
@@ -18,6 +19,8 @@ export const NewMessageCount: React.FC = ({
onClick,
}: NewMessageCountProps) => {
const { stringSet } = useContext(LocalizationContext);
+ const { isMobile } = useMediaQueryContext();
+
const newMessageCountText = useMemo(() => {
if (count === 1) {
@@ -42,15 +45,19 @@ export const NewMessageCount: React.FC = ({
color={LabelColors.ONCONTENT_1}
type={LabelTypography.CAPTION_2}
>
- {`${count} `}
+ {`${count > 99 ? '99+' : count} `}
{newMessageCountText}
-
+ {
+ !isMobile && (
+
+ )
+ }
);
};
diff --git a/src/modules/GroupChannel/components/UnreadCount/index.scss b/src/modules/GroupChannel/components/UnreadCount/index.scss
index 3371a16a8..50850f31b 100644
--- a/src/modules/GroupChannel/components/UnreadCount/index.scss
+++ b/src/modules/GroupChannel/components/UnreadCount/index.scss
@@ -2,119 +2,52 @@
* We operate the CSS files for Channel&GroupChannel modules in the GroupChannel.
* So keep in mind that you should consider both components when you make changes in this file.
*/
-@import '../../../../styles/variables';
-
-// Floating UnreadCount button - completely separate from notification styles
-.sendbird-unread-floating-button {
- position: absolute;
- top: 8px; // Default position when no frozen notification
- left: 50%;
- transform: translateX(-50%);
- width: 181px;
- height: 40px;
- border-radius: 20px;
- gap: 4px;
- display: flex;
- align-items: center;
- justify-content: space-between;
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
- transition: all 0.2s ease-in-out;
- z-index: 5;
- background-color: #FFFFFF;
-
- // When frozen notification is present, position below it
- &.sendbird-unread-floating-button--below-frozen {
- top: 48px;
- }
-
- &:hover {
- cursor: pointer;
- transform: translateX(-50%) translateY(-1px);
- box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2);
- background-color: #F5F5F5;
- }
-
- .sendbird-unread-floating-button__text {
- display: flex;
- align-items: center;
- justify-content: center;
- flex: 1;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- color: #00000080;
- line-height: 1;
- font-size: 14px;
- text-align: center;
- height: 100%;
- }
-
- // Icon fixed to right side
- .sendbird-icon {
- display: flex;
- align-items: center;
- justify-content: center;
- flex-shrink: 0;
-
- svg {
- display: block;
- vertical-align: middle;
- }
- }
-}
-
-.sendbird-unread-floating-button--hide {
- display: none;
-}
-
-// FrozenNotification styles (required for Channel&GroupChannel modules)
-.sendbird-notification--hide,
-.sendbird-notification {
- position: absolute;
-}
-
-.sendbird-notification {
- margin-top: 8px;
- margin-left: 24px;
- margin-right: 24px;
- border-radius: 4px;
- padding: 0px 2px;
- height: 32px;
- display: flex;
- align-items: center;
- justify-content: center;
- @include themed() {
- background-color: t(primary-3);
- }
- &:hover {
- cursor: pointer;
- @include themed() {
- background-color: t(primary-4);
- }
- }
-
- &.sendbird-notification--frozen {
- @include themed() {
- background-color: t(information-1);
- }
- .sendbird-notification__text {
- @include themed() {
- color: t(on-information-1);
- }
- }
- }
-
- .sendbird-notification__text {
- display: flex;
- align-items: center;
- justify-content: center;
- margin-right: 8px;
- }
-}
-
-.sendbird-notification--hide {
- display: none;
-}
-
-
-
+ @import '../../../../styles/variables';
+
+ .sendbird-notification--hide,
+ .sendbird-notification {
+ position: absolute;
+ }
+
+ .sendbird-notification {
+ margin-top: 8px;
+ margin-left: 24px;
+ margin-right: 24px;
+ border-radius: 4px;
+ padding: 0px 2px;
+ height: 32px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ @include themed() {
+ background-color: t(primary-3);
+ }
+ &:hover {
+ cursor: pointer;
+ @include themed() {
+ background-color: t(primary-4);
+ }
+ }
+
+ &.sendbird-notification--frozen {
+ @include themed() {
+ background-color: t(information-1);
+ }
+ .sendbird-notification__text {
+ @include themed() {
+ color: t(on-information-1);
+ }
+ }
+ }
+
+ .sendbird-notification__text {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-right: 8px;
+ }
+ }
+
+ .sendbird-notification--hide {
+ display: none;
+ }
\ No newline at end of file
diff --git a/src/modules/GroupChannel/components/UnreadCount/index.tsx b/src/modules/GroupChannel/components/UnreadCount/index.tsx
index 75b4046d5..972a350a4 100644
--- a/src/modules/GroupChannel/components/UnreadCount/index.tsx
+++ b/src/modules/GroupChannel/components/UnreadCount/index.tsx
@@ -4,6 +4,7 @@ import React, { useContext, useMemo } from 'react';
import { LocalizationContext } from '../../../../lib/LocalizationContext';
import Label, { LabelTypography, LabelColors } from '../../../../ui/Label';
import Icon, { IconTypes, IconColors } from '../../../../ui/Icon';
+import format from 'date-fns/format';
import { classnames } from '../../../../utils/utils';
export interface UnreadCountProps {
@@ -13,48 +14,48 @@ export interface UnreadCountProps {
lastReadAt?: Date | null;
/** @deprecated Please use `lastReadAt` instead * */
time?: string;
- isFrozenChannel?: boolean;
}
export const UnreadCount: React.FC = ({
className = '',
count = 0,
+ time = '',
onClick,
- isFrozenChannel = false,
+ lastReadAt,
}: UnreadCountProps) => {
- const { stringSet } = useContext(LocalizationContext);
+ const { stringSet, dateLocale } = useContext(LocalizationContext);
- const unreadMessageCountText = useMemo(() => {
- if (count === 1) {
- return stringSet.CHANNEL__MESSAGE_LIST__NOTIFICATION__UNREAD_MESSAGE;
- } else if (count > 1) {
- return stringSet.CHANNEL__MESSAGE_LIST__NOTIFICATION__UNREAD_MESSAGE_S;
+ const unreadSince = useMemo(() => {
+ // TODO: Remove this on v4
+ if (stringSet.CHANNEL__MESSAGE_LIST__NOTIFICATION__ON !== 'on') {
+ const timeArray = time?.toString?.()?.split(' ') || [];
+ timeArray?.splice(-2, 0, stringSet.CHANNEL__MESSAGE_LIST__NOTIFICATION__ON);
+ return timeArray.join(' ');
+ } else if (lastReadAt) {
+ return format(lastReadAt, stringSet.DATE_FORMAT__MESSAGE_LIST__NOTIFICATION__UNREAD_SINCE, { locale: dateLocale });
}
- }, [count]);
+ }, [time, lastReadAt]);
return (
diff --git a/src/modules/GroupChannel/components/UnreadCountFloatingButton/index.scss b/src/modules/GroupChannel/components/UnreadCountFloatingButton/index.scss
new file mode 100644
index 000000000..3371a16a8
--- /dev/null
+++ b/src/modules/GroupChannel/components/UnreadCountFloatingButton/index.scss
@@ -0,0 +1,120 @@
+/**
+ * We operate the CSS files for Channel&GroupChannel modules in the GroupChannel.
+ * So keep in mind that you should consider both components when you make changes in this file.
+ */
+@import '../../../../styles/variables';
+
+// Floating UnreadCount button - completely separate from notification styles
+.sendbird-unread-floating-button {
+ position: absolute;
+ top: 8px; // Default position when no frozen notification
+ left: 50%;
+ transform: translateX(-50%);
+ width: 181px;
+ height: 40px;
+ border-radius: 20px;
+ gap: 4px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+ transition: all 0.2s ease-in-out;
+ z-index: 5;
+ background-color: #FFFFFF;
+
+ // When frozen notification is present, position below it
+ &.sendbird-unread-floating-button--below-frozen {
+ top: 48px;
+ }
+
+ &:hover {
+ cursor: pointer;
+ transform: translateX(-50%) translateY(-1px);
+ box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2);
+ background-color: #F5F5F5;
+ }
+
+ .sendbird-unread-floating-button__text {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex: 1;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ color: #00000080;
+ line-height: 1;
+ font-size: 14px;
+ text-align: center;
+ height: 100%;
+ }
+
+ // Icon fixed to right side
+ .sendbird-icon {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+
+ svg {
+ display: block;
+ vertical-align: middle;
+ }
+ }
+}
+
+.sendbird-unread-floating-button--hide {
+ display: none;
+}
+
+// FrozenNotification styles (required for Channel&GroupChannel modules)
+.sendbird-notification--hide,
+.sendbird-notification {
+ position: absolute;
+}
+
+.sendbird-notification {
+ margin-top: 8px;
+ margin-left: 24px;
+ margin-right: 24px;
+ border-radius: 4px;
+ padding: 0px 2px;
+ height: 32px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ @include themed() {
+ background-color: t(primary-3);
+ }
+ &:hover {
+ cursor: pointer;
+ @include themed() {
+ background-color: t(primary-4);
+ }
+ }
+
+ &.sendbird-notification--frozen {
+ @include themed() {
+ background-color: t(information-1);
+ }
+ .sendbird-notification__text {
+ @include themed() {
+ color: t(on-information-1);
+ }
+ }
+ }
+
+ .sendbird-notification__text {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-right: 8px;
+ }
+}
+
+.sendbird-notification--hide {
+ display: none;
+}
+
+
+
diff --git a/src/modules/GroupChannel/components/UnreadCountFloatingButton/index.tsx b/src/modules/GroupChannel/components/UnreadCountFloatingButton/index.tsx
new file mode 100644
index 000000000..35e4eafcd
--- /dev/null
+++ b/src/modules/GroupChannel/components/UnreadCountFloatingButton/index.tsx
@@ -0,0 +1,61 @@
+import './index.scss';
+import React, { useContext, useMemo } from 'react';
+
+import { LocalizationContext } from '../../../../lib/LocalizationContext';
+import Label, { LabelTypography, LabelColors } from '../../../../ui/Label';
+import Icon, { IconTypes, IconColors } from '../../../../ui/Icon';
+import { classnames } from '../../../../utils/utils';
+
+export interface UnreadCountProps {
+ className?: string;
+ count: number | undefined;
+ onClick(): void;
+ isFrozenChannel?: boolean;
+}
+
+export const UnreadCount: React.FC = ({
+ className = '',
+ count = 0,
+ onClick,
+ isFrozenChannel = false,
+}: UnreadCountProps) => {
+ const { stringSet } = useContext(LocalizationContext);
+
+ const unreadMessageCountText = useMemo(() => {
+ if (count === 1) {
+ return stringSet.CHANNEL__MESSAGE_LIST__NOTIFICATION__UNREAD_MESSAGE;
+ } else if (count > 1) {
+ return stringSet.CHANNEL__MESSAGE_LIST__NOTIFICATION__UNREAD_MESSAGE_S;
+ }
+ }, [count]);
+
+ return (
+
+
+
+
+ );
+};
+
+export default UnreadCount;
diff --git a/src/modules/GroupChannel/context/GroupChannelProvider.tsx b/src/modules/GroupChannel/context/GroupChannelProvider.tsx
index 51cee1faa..f058fc824 100644
--- a/src/modules/GroupChannel/context/GroupChannelProvider.tsx
+++ b/src/modules/GroupChannel/context/GroupChannelProvider.tsx
@@ -153,6 +153,8 @@ const GroupChannelManager :React.FC>(new Set());
+
// ScrollHandler initialization
const {
scrollRef,
@@ -224,7 +226,7 @@ const GroupChannelManager :React.FC actions.scrollToBottom(true), 10);
+ setTimeout(async () => actions.scrollToBottom(true), 10);
}
},
onChannelDeleted: () => {
@@ -256,6 +258,7 @@ const GroupChannelManager :React.FC {
if (data.channel.url === state.currentChannel?.url) {
+ currentDeviceMessageIdsRef.current.add(data.message.messageId);
actions.scrollToBottom(true);
}
};
@@ -386,6 +390,7 @@ const GroupChannelManager :React.FC void;
markAsUnread: (message: SendableMessageType, source?: 'manual' | 'internal') => void;
setReadStateChanged: (state: string) => void;
+ setFirstUnreadMessageId: (messageId: number | string | null) => void;
// Message actions
sendUserMessage: (params: UserMessageCreateParams) => Promise;
@@ -50,6 +51,9 @@ export interface GroupChannelActions extends MessageActions {
// Reaction action
toggleReaction: (message: SendableMessageType, emojiKey: string, isReacted: boolean) => void;
+
+ // Current device message tracking
+ isFromCurrentDevice: (messageId: string | number) => boolean;
}
export const useGroupChannel = () => {
@@ -90,8 +94,8 @@ export const useGroupChannel = () => {
}, [state.scrollRef.current, config.isOnline, markAsReadScheduler]);
const markAsReadAll = useCallback((channel: GroupChannel) => {
- if (config.isOnline && !state.disableMarkAsRead) {
- markAsReadScheduler.push(channel);
+ if (config.isOnline && !state.disableMarkAsRead && channel) {
+ markAsReadScheduler?.push(channel);
}
}, [config.isOnline]);
@@ -200,6 +204,14 @@ export const useGroupChannel = () => {
store.setState(state => ({ ...state, readState }));
}, []);
+ const setFirstUnreadMessageId = useCallback((messageId: number | null) => {
+ store.setState(state => ({ ...state, firstUnreadMessageId: messageId }));
+ }, []);
+
+ const isFromCurrentDevice = useCallback((messageId: string | number): boolean => {
+ return state.currentDeviceMessageIdsRef?.current.has(messageId) ?? false;
+ }, [state.currentDeviceMessageIdsRef]);
+
const actions: GroupChannelActions = useMemo(() => {
return {
setCurrentChannel,
@@ -207,12 +219,14 @@ export const useGroupChannel = () => {
markAsReadAll,
markAsUnread: state.markAsUnread,
setReadStateChanged,
+ setFirstUnreadMessageId,
setQuoteMessage,
scrollToBottom,
scrollToMessage,
toggleReaction,
setAnimatedMessageId,
setIsScrollBottomReached,
+ isFromCurrentDevice,
...messageActions,
};
}, [
@@ -221,12 +235,14 @@ export const useGroupChannel = () => {
markAsReadAll,
state.markAsUnread,
setReadStateChanged,
+ setFirstUnreadMessageId,
setQuoteMessage,
scrollToBottom,
scrollToMessage,
toggleReaction,
setAnimatedMessageId,
setIsScrollBottomReached,
+ isFromCurrentDevice,
messageActions,
]);
diff --git a/src/modules/GroupChannel/context/hooks/useMessageActions.ts b/src/modules/GroupChannel/context/hooks/useMessageActions.ts
index d10e17568..c903c0dbf 100644
--- a/src/modules/GroupChannel/context/hooks/useMessageActions.ts
+++ b/src/modules/GroupChannel/context/hooks/useMessageActions.ts
@@ -151,7 +151,14 @@ export function useMessageActions(params: Params): MessageActions {
async (params) => {
const internalParams = buildInternalMessageParams(params);
const processedParams = await processParams(onBeforeSendUserMessage, internalParams, 'user') as UserMessageCreateParams;
- return sendUserMessage(processedParams, asyncScrollToBottom);
+ // return sendUserMessage(processedParams, asyncScrollToBottom);
+ const message = await sendUserMessage(processedParams, asyncScrollToBottom);
+ pubSub.publish(PUBSUB_TOPICS.SEND_USER_MESSAGE, {
+ channel: currentChannel,
+ message,
+ publishingModules: [PublishingModuleType.CHANNEL],
+ });
+ return message;
},
[buildInternalMessageParams, sendUserMessage, scrollToBottom, processParams],
),
@@ -159,7 +166,13 @@ export function useMessageActions(params: Params): MessageActions {
async (params) => {
const internalParams = buildInternalMessageParams(params);
const processedParams = await processParams(onBeforeSendFileMessage, internalParams, 'file') as FileMessageCreateParams;
- return sendFileMessage(processedParams, asyncScrollToBottom);
+ const message = await sendFileMessage(processedParams, asyncScrollToBottom);
+ pubSub.publish(PUBSUB_TOPICS.SEND_FILE_MESSAGE, {
+ channel: currentChannel,
+ message,
+ publishingModules: [PublishingModuleType.CHANNEL],
+ });
+ return message;
},
[buildInternalMessageParams, sendFileMessage, scrollToBottom, processParams],
),
diff --git a/src/modules/GroupChannel/context/types.ts b/src/modules/GroupChannel/context/types.ts
index 3c647e719..75ea79afc 100644
--- a/src/modules/GroupChannel/context/types.ts
+++ b/src/modules/GroupChannel/context/types.ts
@@ -48,6 +48,9 @@ interface InternalGroupChannelState extends MessageDataSource {
isScrollBottomReached: boolean;
readState: string | null;
+ // Current device message tracking
+ currentDeviceMessageIdsRef: React.MutableRefObject>;
+
// References - will be managed together
scrollRef: React.RefObject;
scrollDistanceFromBottomRef: React.MutableRefObject;
diff --git a/src/ui/DateSeparator/index.tsx b/src/ui/DateSeparator/index.tsx
index 05f4a3fcd..65df3a29c 100644
--- a/src/ui/DateSeparator/index.tsx
+++ b/src/ui/DateSeparator/index.tsx
@@ -1,4 +1,4 @@
-import React, { ReactElement, useLayoutEffect, useRef, useCallback } from 'react';
+import React, { ReactElement } from 'react';
import './index.scss';
@@ -13,52 +13,16 @@ export interface DateSeparatorProps {
children?: string | ReactElement;
className?: string | Array;
separatorColor?: Colors;
- hasNewMessageSeparator?: boolean;
- onVisibilityChange?: (isVisible: boolean) => void;
}
const DateSeparator = ({
children = undefined,
className = '',
- hasNewMessageSeparator = false,
- onVisibilityChange,
- separatorColor = hasNewMessageSeparator ? Colors.PRIMARY : Colors.ONBACKGROUND_4,
+ separatorColor = Colors.ONBACKGROUND_4,
}: DateSeparatorProps): ReactElement => {
- const separatorRef = useRef(null);
-
- const handleVisibilityChange = useCallback((isVisible: boolean) => {
- onVisibilityChange?.(isVisible);
- }, [onVisibilityChange]);
-
- useLayoutEffect(() => {
- const element = separatorRef.current;
- if (!element || !hasNewMessageSeparator || !onVisibilityChange) return;
-
- const observer = new IntersectionObserver(
- (entries) => {
- entries.forEach((entry) => {
- const visible = entry.isIntersecting;
- handleVisibilityChange(visible);
- });
- },
- {
- threshold: 1.0,
- rootMargin: '0px',
- root: null, // viewport를 기준으로 관찰
- },
- );
-
- observer.observe(element);
-
- return () => {
- observer.disconnect();
- };
- }, [hasNewMessageSeparator, handleVisibilityChange, onVisibilityChange]);
return (
;
+ separatorColor?: Colors;
+ onVisibilityChange?: (isVisible: boolean) => void;
+}
+
+const NewMessageIndicator = ({
+ children = undefined,
+ className = '',
+ onVisibilityChange,
+ separatorColor = Colors.PRIMARY,
+}: NewMessageIndicatorProps): ReactElement => {
+ const separatorRef = useRef
(null);
+
+ const handleVisibilityChange = useCallback((isVisible: boolean) => {
+ onVisibilityChange?.(isVisible);
+ }, [onVisibilityChange]);
+
+ useLayoutEffect(() => {
+ const element = separatorRef.current;
+ if (!element || !onVisibilityChange) return;
+
+ const observer = new IntersectionObserver(
+ (entries) => {
+ entries.forEach((entry) => {
+ const visible = entry.isIntersecting;
+ handleVisibilityChange(visible);
+ });
+ },
+ {
+ threshold: 1.0,
+ rootMargin: '0px',
+ root: null, // viewport를 기준으로 관찰
+ },
+ );
+
+ observer.observe(element);
+
+ return () => {
+ observer.disconnect();
+ };
+ }, [handleVisibilityChange, onVisibilityChange]);
+
+ return (
+
+
+
+ {
+ children
+ || (
+
+ )
+ }
+
+
+
+ );
+};
+
+export default NewMessageIndicator;
diff --git a/yarn.lock b/yarn.lock
index 98de84965..0b83b9053 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2748,8 +2748,8 @@ __metadata:
linkType: hard
"@sendbird/chat@npm:^4.19.2":
- version: 4.19.2
- resolution: "@sendbird/chat@npm:4.19.2"
+ version: 4.19.4
+ resolution: "@sendbird/chat@npm:4.19.4"
peerDependencies:
"@react-native-async-storage/async-storage": ^1.17.6
react-native-mmkv: ">=2.0.0"
@@ -2758,7 +2758,7 @@ __metadata:
optional: true
react-native-mmkv:
optional: true
- checksum: 6cb69158f19633252f474c6edd1ae1e55d1d46dc1dcf628a0a5c5108ee1b9cafd5a2cde63aeb011237d9fdc5789c8388209ced2fc97b0d9841d5c5aa2bb6d9da
+ checksum: 3b7c5aa799e1f6794a4c035699a578e346dc437b42368152032f212a1568b8784356953bc2e73e77b990abdc33e27b6e9eb57114f36b2a0164dc0914558d3657
languageName: node
linkType: hard
From 5e816212a7e26141a698fd2ce5f696a22d2af767 Mon Sep 17 00:00:00 2001
From: danney-chun <63285271+danney-chun@users.noreply.github.com>
Date: Thu, 10 Jul 2025 00:34:40 +0900
Subject: [PATCH 07/19] fixed lint
---
.../components/NewMessageCountFloatingButton/index.tsx | 1 -
1 file changed, 1 deletion(-)
diff --git a/src/modules/GroupChannel/components/NewMessageCountFloatingButton/index.tsx b/src/modules/GroupChannel/components/NewMessageCountFloatingButton/index.tsx
index 8b98d61eb..fa3218c7a 100644
--- a/src/modules/GroupChannel/components/NewMessageCountFloatingButton/index.tsx
+++ b/src/modules/GroupChannel/components/NewMessageCountFloatingButton/index.tsx
@@ -20,7 +20,6 @@ export const NewMessageCount: React.FC = ({
}: NewMessageCountProps) => {
const { stringSet } = useContext(LocalizationContext);
const { isMobile } = useMediaQueryContext();
-
const newMessageCountText = useMemo(() => {
if (count === 1) {
From 7b6f6e2ba5f8b9ebd8dedbf70c87b40d9cfa2dd9 Mon Sep 17 00:00:00 2001
From: danney-chun <63285271+danney-chun@users.noreply.github.com>
Date: Thu, 10 Jul 2025 16:41:34 +0900
Subject: [PATCH 08/19] clnp 7139 (#1353)
## For Internal Contributors
* Follow the [Scaled Trunk-Based Development
workflow](https://trunkbaseddevelopment.com/)
* Branch naming format: `{type}/TICKET_ID/description`
* Type: `feat` / `fix` / `chore` / `doc` / `release`
* Receive PR review approvals
* Rebase your branch with the main branch and wait for CI to pass
* Squash merge your commit
* Use imperative language in the title and description
* Follow the provided template for PR description and squashing
### Template
```
// PR title (Required)
[type]: A short description of the changes in imperative language.
// PR description (Optional)
Add a brief description of the changes in this PR. Bullet points are also fine.
// Footer (Recommended)
Fixes [](https://sendbird.atlassian.net/browse/)
// Changelogs (Recommended)
// Add (internal) at the end of each changelog if internal.
### Changelogs
// Co-authors
// Add this if you pair programmed or they made significant contributions to the ideas in the code and you want to thank them.
Co-authored-by: Name name@example.com, Name2 name@example.com
```
### Checklist
Put an `x` in the boxes that apply. You can also fill these out after
creating the PR. If unsure, ask the members.
This is a reminder of what we look for before merging your code.
- [ ] **All tests pass locally with my changes**
- [ ] **I have added tests that prove my fix is effective or that my
feature works**
- [ ] **Public components / utils / props are appropriately exported**
- [ ] I have added necessary documentation (if appropriate)
## External Contributions
This project is not yet set up to accept pull requests from external
contributors.
If you have a pull request that you believe should be accepted, please
contact
the Developer Relations team with
details
and we'll evaluate if we can set up a
[CLA](https://en.wikipedia.org/wiki/Contributor_License_Agreement) to
allow for the contribution.
---
.../Channel/components/MessageList/index.tsx | 133 ++++--------------
.../Channel/context/ChannelProvider.tsx | 17 +++
.../Channel/context/dux/actionTypes.ts | 6 +
.../context/hooks/useHandleChannelEvents.ts | 15 +-
.../MessageInputWrapperView.tsx | 6 +-
.../components/MessageList/index.tsx | 13 +-
src/ui/MessageMenu/MessageMenu.tsx | 2 +-
src/ui/MobileMenu/MobileBottomSheet.tsx | 17 +--
src/ui/MobileMenu/MobileContextMenu.tsx | 11 +-
src/utils/menuConditions.ts | 4 +-
10 files changed, 83 insertions(+), 141 deletions(-)
diff --git a/src/modules/Channel/components/MessageList/index.tsx b/src/modules/Channel/components/MessageList/index.tsx
index 050ea134d..365c382ee 100644
--- a/src/modules/Channel/components/MessageList/index.tsx
+++ b/src/modules/Channel/components/MessageList/index.tsx
@@ -1,7 +1,7 @@
/* We operate the CSS files for Channel&GroupChannel modules in the GroupChannel */
import '../../../GroupChannel/components/MessageList/index.scss';
-import React, { useMemo, useState } from 'react';
+import React, { useState } from 'react';
import type { UserMessage } from '@sendbird/chat/message';
import { useChannelContext } from '../../context/ChannelProvider';
@@ -11,8 +11,6 @@ import Message from '../Message';
import { EveryMessage, TypingIndicatorType } from '../../../../types';
import { isAboutSame } from '../../context/utils';
import UnreadCount from '../UnreadCount';
-import UnreadCountFloatingButton from '../UnreadCountFloatingButton';
-import NewMessageCountFloatingButton from '../NewMessageCountFloatingButton';
import FrozenNotification from '../FrozenNotification';
import { SCROLL_BUFFER } from '../../../../utils/consts';
import { MessageProvider } from '../../../Message/context/MessageProvider';
@@ -29,7 +27,6 @@ import { deleteNullish } from '../../../../utils/utils';
import { getHTMLTextDirection } from '../../../../utils';
import useSendbird from '../../../../lib/Sendbird/context/hooks/useSendbird';
import { useLocalization } from '../../../../lib/LocalizationContext';
-import { useGroupChannel } from '../../../GroupChannel/context/hooks/useGroupChannel';
const SCROLL_BOTTOM_PADDING = 50;
@@ -55,7 +52,7 @@ export const MessageList = (props: MessageListProps) => {
renderCustomSeparator,
renderPlaceholderLoader = () => ,
renderPlaceholderEmpty = () => ,
- renderFrozenNotification = () => ,
+ renderFrozenNotification = () => ,
} = deleteNullish(props);
const {
@@ -90,8 +87,6 @@ export const MessageList = (props: MessageListProps) => {
const [isScrollBottom, setIsScrollBottom] = useState(false);
- const { state: { newMessages, markAsUnreadSourceRef } } = useGroupChannel();
-
useScrollBehavior();
/**
@@ -161,10 +156,6 @@ export const MessageList = (props: MessageListProps) => {
* hasMoreNext is true but it needs to be called when hasNext is false when reached bottom as well.
*/
if (!hasMoreNext && !disableMarkAsRead && !!currentGroupChannel) {
- // markAsUnreadSourceRef의 현재 값을 확인
- const currentSource = markAsUnreadSourceRef.current;
- console.log('Channel MessageList: markAsUnreadSourceRef current value:', currentSource);
-
messagesDispatcher({
type: messageActionTypes.MARK_AS_READ,
payload: { channel: currentGroupChannel },
@@ -183,35 +174,6 @@ export const MessageList = (props: MessageListProps) => {
const { scrollToBottomHandler, scrollBottom } = useSetScrollToBottom({ loading });
- const isShowUnreadCount = useMemo(() => {
- if (store?.config?.groupChannel?.enableMarkAsUnread) {
- // markAsUnread is enabled - 스크롤이 bottom에 있을 때는 표시하지 않음
- if (currentGroupChannel?.unreadMessageCount > 0 && !isScrollBottom) {
- return true;
- }
- return false;
- } else {
- // markAsUnread is disable
- if (currentGroupChannel?.unreadMessageCount > 0 && !isScrollBottom && hasMoreNext) {
- return true;
- }
- return false;
- }
- }, [currentGroupChannel.unreadMessageCount, isScrollBottom]);
-
- const isShowNewMessageCount = useMemo(() => {
- // 스크롤이 bottom에 있을 때는 new message count를 표시하지 않음
- if (isScrollBottom) {
- return false;
- }
- if (!store?.config?.groupChannel?.enableMarkAsUnread
- && hasMoreNext
- && (unreadSince || unreadSinceDate)) {
- return true;
- }
- return false;
- }, [newMessages.length, isScrollBottom]);
-
if (loading) {
return renderPlaceholderLoader();
}
@@ -220,54 +182,6 @@ export const MessageList = (props: MessageListProps) => {
return renderPlaceholderEmpty();
}
- const renderUnreadCount = () => {
- if (isShowUnreadCount) {
- if (!store?.config?.groupChannel?.enableMarkAsUnread) {
- return (
- {
- if (scrollRef?.current) scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
- if (!disableMarkAsRead && !!currentGroupChannel) {
- markAsReadScheduler.push(currentGroupChannel);
- messagesDispatcher({
- type: messageActionTypes.MARK_AS_READ,
- payload: { channel: currentGroupChannel },
- });
- }
- setInitialTimeStamp(null);
- setAnimatedMessageId(null);
- setHighLightedMessageId(null);
- }}
- />
- );
- } else {
- return (
- {
- if (scrollRef?.current) scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
- if (!disableMarkAsRead && !!currentGroupChannel) {
- markAsReadScheduler.push(currentGroupChannel);
- messagesDispatcher({
- type: messageActionTypes.MARK_AS_READ,
- payload: { channel: currentGroupChannel },
- });
- }
- setInitialTimeStamp(null);
- setAnimatedMessageId(null);
- setHighLightedMessageId(null);
- }}
- />
- );
- }
- }
- };
-
return (
<>
{!isScrolled && }
@@ -355,27 +269,16 @@ export const MessageList = (props: MessageListProps) => {
{currentGroupChannel?.isFrozen && renderFrozenNotification()}
- {renderUnreadCount()}
{
- // This flag is an unmatched variable
- scrollBottom > SCROLL_BOTTOM_PADDING && (
-
-
-
- )
- }
- {
- /* NewMessageCount - positioned at the bottom of MessageList */
- (isShowNewMessageCount) && (
- {
if (scrollRef?.current) scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
if (!disableMarkAsRead && !!currentGroupChannel) {
@@ -392,6 +295,20 @@ export const MessageList = (props: MessageListProps) => {
/>
)
}
+ {
+ // This flag is an unmatched variable
+ scrollBottom > SCROLL_BOTTOM_PADDING && (
+
+
+
+ )
+ }
>
);
diff --git a/src/modules/Channel/context/ChannelProvider.tsx b/src/modules/Channel/context/ChannelProvider.tsx
index bd7cce620..0030bc9c5 100644
--- a/src/modules/Channel/context/ChannelProvider.tsx
+++ b/src/modules/Channel/context/ChannelProvider.tsx
@@ -5,6 +5,7 @@ import React, {
useRef,
useMemo,
} from 'react';
+import { UIKitConfigProvider, useUIKitConfig } from '@sendbird/uikit-tools';
import type { GroupChannel, Member } from '@sendbird/chat/groupChannel';
import type {
@@ -52,6 +53,7 @@ import { PublishingModuleType } from '../../internalInterfaces';
import { ChannelActionTypes } from './dux/actionTypes';
import useSendbird from '../../../lib/Sendbird/context/hooks/useSendbird';
import { useLocalization } from '../../../lib/LocalizationContext';
+import { uikitConfigStorage } from '../../../lib/utils/uikitConfigStorage';
export { ThreadReplySelectType } from './const'; // export for external usage
@@ -215,6 +217,7 @@ const ChannelProvider = (props: ChannelContextProps) => {
const sdk = globalStore?.stores?.sdkStore?.sdk;
const sdkInit = globalStore?.stores?.sdkStore?.initialized;
const globalConfigs = globalStore?.config;
+ const { configs: uikitConfig } = useUIKitConfig();
const [initialTimeStamp, setInitialTimeStamp] = useState(startingPoint);
useEffect(() => {
@@ -441,6 +444,19 @@ const ChannelProvider = (props: ChannelContextProps) => {
});
return (
+
{
{children}
+
);
};
diff --git a/src/modules/Channel/context/dux/actionTypes.ts b/src/modules/Channel/context/dux/actionTypes.ts
index 98b04e47c..113a798ef 100644
--- a/src/modules/Channel/context/dux/actionTypes.ts
+++ b/src/modules/Channel/context/dux/actionTypes.ts
@@ -27,6 +27,7 @@ export const ON_MESSAGE_DELETED_BY_REQ_ID = 'ON_MESSAGE_DELETED_BY_REQ_ID';
export const SET_CURRENT_CHANNEL = 'SET_CURRENT_CHANNEL';
export const SET_CHANNEL_INVALID = 'SET_CHANNEL_INVALID';
export const MARK_AS_READ = 'MARK_AS_READ';
+export const MARK_AS_UNREAD = 'MARK_AS_UNREAD';
export const ON_REACTION_UPDATED = 'ON_REACTION_UPDATED';
export const SET_EMOJI_CONTAINER = 'SET_EMOJI_CONTAINER';
export const MESSAGE_LIST_PARAMS_CHANGED = 'MESSAGE_LIST_PARAMS_CHANGED';
@@ -77,6 +78,11 @@ type CHANNEL_PAYLOAD_TYPES = {
[RESEND_MESSAGE_START]: SendableMessageType;
[MARK_AS_READ]: {
channel: null | GroupChannel;
+ userIds?: null |string[];
+ };
+ [MARK_AS_UNREAD]: {
+ channel: null | GroupChannel;
+ userIds?: null |string[];
};
[ON_MESSAGE_DELETED]: MessageId;
[ON_MESSAGE_DELETED_BY_REQ_ID]: RequestId;
diff --git a/src/modules/Channel/context/hooks/useHandleChannelEvents.ts b/src/modules/Channel/context/hooks/useHandleChannelEvents.ts
index 481d3e0eb..cddd6807c 100644
--- a/src/modules/Channel/context/hooks/useHandleChannelEvents.ts
+++ b/src/modules/Channel/context/hooks/useHandleChannelEvents.ts
@@ -111,8 +111,11 @@ function useHandleChannelEvents({
logger.info('Channel | useHandleChannelEvents: onUserMarkedAsRead', channel, userIds);
if (compareIds(channel?.url, channelUrl)) {
messagesDispatcher({
- type: messageActions.SET_CURRENT_CHANNEL,
- payload: channel,
+ type: messageActions.MARK_AS_READ,
+ payload: {
+ channel,
+ userIds,
+ },
});
}
},
@@ -121,8 +124,11 @@ function useHandleChannelEvents({
// TODO:: MADOKA 이 부분에 대해서 명확하게 확인해야 함.
if (compareIds(channel?.url, channelUrl)) {
messagesDispatcher({
- type: messageActions.SET_CURRENT_CHANNEL,
- payload: channel,
+ type: messageActions.MARK_AS_UNREAD,
+ payload: {
+ channel,
+ userIds,
+ },
});
}
},
@@ -156,6 +162,7 @@ function useHandleChannelEvents({
},
onMessageDeleted: (channel, messageId) => {
logger.info('Channel | useHandleChannelEvents: onMessageDeleted', { channel, messageId });
+ console.log('MADOKA setQuoteMessage null #6');
setQuoteMessage(null);
messagesDispatcher({
type: messageActions.ON_MESSAGE_DELETED,
diff --git a/src/modules/GroupChannel/components/MessageInputWrapper/MessageInputWrapperView.tsx b/src/modules/GroupChannel/components/MessageInputWrapper/MessageInputWrapperView.tsx
index 9e9b38520..272a38866 100644
--- a/src/modules/GroupChannel/components/MessageInputWrapper/MessageInputWrapperView.tsx
+++ b/src/modules/GroupChannel/components/MessageInputWrapper/MessageInputWrapperView.tsx
@@ -183,7 +183,9 @@ export const MessageInputWrapperView = React.forwardRef((
)}
{quoteMessage && (
- setQuoteMessage(null)} />
+ {
+ setQuoteMessage(null);
+ }} />
)}
{showVoiceMessageInput ? (
@@ -244,11 +246,13 @@ export const MessageInputWrapperView = React.forwardRef((
});
setMentionNickname('');
setMentionedUsers([]);
+ console.log('MADOKA setQuoteMessage null #3');
setQuoteMessage(null);
currentChannel?.endTyping?.();
}}
onFileUpload={(fileList) => {
handleUploadFiles(fileList);
+ console.log('MADOKA setQuoteMessage null #4');
setQuoteMessage(null);
}}
onUserMentioned={(user) => {
diff --git a/src/modules/GroupChannel/components/MessageList/index.tsx b/src/modules/GroupChannel/components/MessageList/index.tsx
index 7155b801c..9c174e462 100644
--- a/src/modules/GroupChannel/components/MessageList/index.tsx
+++ b/src/modules/GroupChannel/components/MessageList/index.tsx
@@ -1,5 +1,5 @@
import './index.scss';
-import React, { useEffect, useState, useRef } from 'react';
+import React, { useEffect, useState, useRef, useMemo } from 'react';
import type { GroupChannel, Member } from '@sendbird/chat/groupChannel';
import { useGroupChannelHandler } from '@sendbird/uikit-tools';
@@ -150,7 +150,7 @@ export const MessageList = (props: GroupChannelMessageListProps) => {
const isFind = findFirstUnreadMessage(message, i, messages as CoreMessageType[], currentChannel);
if (isFind && !isFromCurrentDevice(message.messageId)) {
- return message.messageId;
+ return firstUnreadMessageIdRef.current ? undefined : message.messageId;
}
}
}
@@ -159,6 +159,7 @@ export const MessageList = (props: GroupChannelMessageListProps) => {
// check changed channel
useEffect(() => {
+ if (!currentChannel?.url) return;
if (currentChannel?.url !== currentChannelRef.current?.url) {
currentChannelRef.current = currentChannel;
@@ -169,8 +170,9 @@ export const MessageList = (props: GroupChannelMessageListProps) => {
if (markAsUnreadSourceRef?.current !== undefined) {
markAsUnreadSourceRef.current = undefined;
}
- setIsChangedChannel(true);
+ currentMessagesRef.current = [];
currentDeviceMessageIdsRef.current.clear();
+ setIsChangedChannel(true);
} else {
setIsChangedChannel(false);
}
@@ -187,15 +189,14 @@ export const MessageList = (props: GroupChannelMessageListProps) => {
firstUnreadMessageIdRef.current = firstUnreadMessageId;
}
hasInitializedRef.current = true;
+ setIsChangedChannel(false);
}
}
}
}, [messages]);
- useEffect(() => {
+ useMemo(() => {
if (state.config.groupChannel.enableMarkAsUnread && hasInitializedRef.current) {
- if (firstUnreadMessageIdRef.current) return;
-
const firstUnreadMessageId = getFirstUnreadMessage();
if (firstUnreadMessageId && firstUnreadMessageIdRef.current !== firstUnreadMessageId) {
diff --git a/src/ui/MessageMenu/MessageMenu.tsx b/src/ui/MessageMenu/MessageMenu.tsx
index 86bf00481..4057d33cb 100644
--- a/src/ui/MessageMenu/MessageMenu.tsx
+++ b/src/ui/MessageMenu/MessageMenu.tsx
@@ -152,7 +152,7 @@ export const MessageMenu = ({
{showMenuItemThread(params) && }
{showMenuItemOpenInChannel(params) && }
{showMenuItemEdit(params) && }
- {showMenuItemMarkAsUnread(params) && enableMarkAsUnread && }
+ {enableMarkAsUnread && showMenuItemMarkAsUnread(params) && }
{showMenuItemResend(params) && }
{showMenuItemDelete(params) && }
>
diff --git a/src/ui/MobileMenu/MobileBottomSheet.tsx b/src/ui/MobileMenu/MobileBottomSheet.tsx
index af48d6b44..f3a0efe97 100644
--- a/src/ui/MobileMenu/MobileBottomSheet.tsx
+++ b/src/ui/MobileMenu/MobileBottomSheet.tsx
@@ -15,7 +15,6 @@ import {
isVoiceMessage,
isThreadMessage,
} from '../../utils';
-import { showMenuItemMarkAsUnread } from '../../utils/menuConditions';
import BottomSheet from '../BottomSheet';
import ImageRenderer from '../ImageRenderer';
import ReactionButton from '../ReactionButton';
@@ -83,13 +82,11 @@ const MobileBottomSheet: React.FunctionComponent = (prop
&& !isPendingMessage(message)
&& !isThreadMessage(message)
&& (channel?.isGroupChannel() && !(channel as GroupChannel)?.isBroadcast);
- // const showMenuItemMarkAsUnreadCondition = showMenuItemMarkAsUnread({
- // message,
- // channel,
- // isByMe,
- // replyType,
- // onReplyInThread,
- // });
+
+ const showMenuItemMarkAsUnread: boolean = !isFailedMessage(message)
+ && !isPendingMessage(message)
+ && channel?.isGroupChannel?.()
+ && replyType !== 'THREAD';
const maxEmojisPerRow = Math.floor(window.innerWidth / EMOJI_SIZE) - 1;
const [showEmojisOnly, setShowEmojisOnly] = useState(false);
@@ -208,14 +205,14 @@ const MobileBottomSheet: React.FunctionComponent = (prop
<>
{showMenuItemCopy && }
{showMenuItemEdit && }
- {showMenuItemMarkAsUnread && enableMarkAsUnread && }
+ {enableMarkAsUnread && showMenuItemMarkAsUnread && }
{showMenuItemResend && }
{showMenuItemReply && }
{showMenuItemThread && }
{showMenuItemDeleteFinal && }
{showMenuItemDownload && }
>
- )}
+ )}ß
)}
diff --git a/src/ui/MobileMenu/MobileContextMenu.tsx b/src/ui/MobileMenu/MobileContextMenu.tsx
index e5be8a3f4..e2798fa22 100644
--- a/src/ui/MobileMenu/MobileContextMenu.tsx
+++ b/src/ui/MobileMenu/MobileContextMenu.tsx
@@ -12,7 +12,6 @@ import {
isThreadMessage,
isVoiceMessage,
} from '../../utils';
-import { showMenuItemMarkAsUnread } from '../../utils/menuConditions';
import { MessageMenuProvider } from '../MessageMenu';
import type { MobileMessageMenuContextProps } from '../MessageMenu/MessageMenuProvider';
@@ -62,13 +61,7 @@ const MobileContextMenu: React.FunctionComponent = (props: BaseMe
const showMenuItemDownload = !isPendingMessage(message) && isFileMessage(message) && !(isVoiceMessage(message) && (channel as GroupChannel)?.isSuper || (channel as GroupChannel)?.isBroadcast);
const showMenuItemReply = replyType === 'QUOTE_REPLY' && !isFailedMessage(message) && !isPendingMessage(message) && channel?.isGroupChannel();
const showMenuItemThread = replyType === 'THREAD' && !isOpenedFromThread && !isFailedMessage(message) && !isPendingMessage(message) && !isThreadMessage(message) && channel?.isGroupChannel();
- const showMenuItemMarkAsUnreadCondition = showMenuItemMarkAsUnread({
- message,
- channel,
- isByMe,
- replyType,
- onReplyInThread,
- });
+ const showMenuItemMarkAsUnread = !isFailedMessage(message) && !isPendingMessage(message) && message.parentMessageId <= 0 && channel?.isGroupChannel?.();
const disableDeleteMessage = (deleteMenuState !== undefined && deleteMenuState === 'DISABLE') || (message?.threadInfo?.replyCount ?? 0) > 0;
const contextValue: MobileMessageMenuContextProps = {
@@ -114,7 +107,7 @@ const MobileContextMenu: React.FunctionComponent = (props: BaseMe
{showMenuItemReply && }
{showMenuItemThread && }
{showMenuItemEdit && }
- {showMenuItemMarkAsUnreadCondition && enableMarkAsUnread && }
+ {enableMarkAsUnread && showMenuItemMarkAsUnread && }
{showMenuItemResend && }
{showMenuItemDeleteFinal && }
{showMenuItemDownload && }
diff --git a/src/utils/menuConditions.ts b/src/utils/menuConditions.ts
index d142c24ec..8b1ae908a 100644
--- a/src/utils/menuConditions.ts
+++ b/src/utils/menuConditions.ts
@@ -54,6 +54,6 @@ export const showMenuItemThread = ({ channel, message, replyType, onReplyInThrea
return isReplyTypeMessageEnabled({ channel, message }) && replyType === 'THREAD' && !message?.parentMessageId && typeof onReplyInThread === 'function';
};
-export const showMenuItemMarkAsUnread = ({ message, channel }: MenuConditionsParams) => {
- return !isFailedMessage(message) && !isPendingMessage(message) && channel?.isGroupChannel?.();
+export const showMenuItemMarkAsUnread = ({ message, channel, replyType }: MenuConditionsParams) => {
+ return !isFailedMessage(message) && !isPendingMessage(message) && channel?.isGroupChannel?.() && replyType !== 'THREAD';
};
From 8e7971d52e436f2d63dd0a9bd27e814e2252d0c7 Mon Sep 17 00:00:00 2001
From: danney-chun <63285271+danney-chun@users.noreply.github.com>
Date: Tue, 15 Jul 2025 12:22:29 +0900
Subject: [PATCH 09/19] removed console log
---
src/modules/Channel/context/hooks/useHandleChannelEvents.ts | 1 -
.../components/MessageInputWrapper/MessageInputWrapperView.tsx | 2 --
2 files changed, 3 deletions(-)
diff --git a/src/modules/Channel/context/hooks/useHandleChannelEvents.ts b/src/modules/Channel/context/hooks/useHandleChannelEvents.ts
index cddd6807c..99df31eca 100644
--- a/src/modules/Channel/context/hooks/useHandleChannelEvents.ts
+++ b/src/modules/Channel/context/hooks/useHandleChannelEvents.ts
@@ -162,7 +162,6 @@ function useHandleChannelEvents({
},
onMessageDeleted: (channel, messageId) => {
logger.info('Channel | useHandleChannelEvents: onMessageDeleted', { channel, messageId });
- console.log('MADOKA setQuoteMessage null #6');
setQuoteMessage(null);
messagesDispatcher({
type: messageActions.ON_MESSAGE_DELETED,
diff --git a/src/modules/GroupChannel/components/MessageInputWrapper/MessageInputWrapperView.tsx b/src/modules/GroupChannel/components/MessageInputWrapper/MessageInputWrapperView.tsx
index 272a38866..ee6b3e44c 100644
--- a/src/modules/GroupChannel/components/MessageInputWrapper/MessageInputWrapperView.tsx
+++ b/src/modules/GroupChannel/components/MessageInputWrapper/MessageInputWrapperView.tsx
@@ -246,13 +246,11 @@ export const MessageInputWrapperView = React.forwardRef((
});
setMentionNickname('');
setMentionedUsers([]);
- console.log('MADOKA setQuoteMessage null #3');
setQuoteMessage(null);
currentChannel?.endTyping?.();
}}
onFileUpload={(fileList) => {
handleUploadFiles(fileList);
- console.log('MADOKA setQuoteMessage null #4');
setQuoteMessage(null);
}}
onUserMentioned={(user) => {
From 0fe074decf3979f0fcb13daf28b8cf585bc53d92 Mon Sep 17 00:00:00 2001
From: danney-chun <63285271+danney-chun@users.noreply.github.com>
Date: Wed, 16 Jul 2025 14:18:30 +0900
Subject: [PATCH 10/19] fixed QA Issue (CLNP-7115)
---
.../components/MessageList/index.tsx | 45 ++++++++++---------
1 file changed, 23 insertions(+), 22 deletions(-)
diff --git a/src/modules/GroupChannel/components/MessageList/index.tsx b/src/modules/GroupChannel/components/MessageList/index.tsx
index 9c174e462..b37708fe8 100644
--- a/src/modules/GroupChannel/components/MessageList/index.tsx
+++ b/src/modules/GroupChannel/components/MessageList/index.tsx
@@ -107,9 +107,9 @@ export const MessageList = (props: GroupChannelMessageListProps) => {
const [unreadSinceDate, setUnreadSinceDate] = useState();
const [isChangedChannel, setIsChangedChannel] = useState(false);
- const [showUnreadCount, setShowUnreadCount] = useState(true);
+ const [showUnreadCount, setShowUnreadCount] = useState(false);
- const firstUnreadMessageIdRef = useRef();
+ const firstUnreadMessageRef = useRef(undefined);
const hasInitializedRef = useRef(false);
// current channel ref
@@ -143,14 +143,14 @@ export const MessageList = (props: GroupChannelMessageListProps) => {
return false;
};
- const getFirstUnreadMessage = (): number | string => {
+ const getFirstUnreadMessage = (): CoreMessageType | undefined => {
if (state.config.groupChannel.enableMarkAsUnread) {
for (let i = 0; i < messages.length; i++) {
const message = messages[i] as CoreMessageType;
const isFind = findFirstUnreadMessage(message, i, messages as CoreMessageType[], currentChannel);
if (isFind && !isFromCurrentDevice(message.messageId)) {
- return firstUnreadMessageIdRef.current ? undefined : message.messageId;
+ return message;
}
}
}
@@ -164,8 +164,8 @@ export const MessageList = (props: GroupChannelMessageListProps) => {
currentChannelRef.current = currentChannel;
// initialize
- firstUnreadMessageIdRef.current = undefined;
- setShowUnreadCount(true);
+ firstUnreadMessageRef.current = undefined;
+ setShowUnreadCount(false);
hasInitializedRef.current = false;
if (markAsUnreadSourceRef?.current !== undefined) {
markAsUnreadSourceRef.current = undefined;
@@ -184,9 +184,9 @@ export const MessageList = (props: GroupChannelMessageListProps) => {
if (!hasInitializedRef.current) {
if (currentMessagesRef.current !== messages) {
currentMessagesRef.current = messages as CoreMessageType[];
- const firstUnreadMessageId = getFirstUnreadMessage();
- if (firstUnreadMessageId) {
- firstUnreadMessageIdRef.current = firstUnreadMessageId;
+ const firstUnreadMessage = getFirstUnreadMessage();
+ if (firstUnreadMessage) {
+ firstUnreadMessageRef.current = firstUnreadMessage;
}
hasInitializedRef.current = true;
setIsChangedChannel(false);
@@ -197,10 +197,13 @@ export const MessageList = (props: GroupChannelMessageListProps) => {
useMemo(() => {
if (state.config.groupChannel.enableMarkAsUnread && hasInitializedRef.current) {
- const firstUnreadMessageId = getFirstUnreadMessage();
-
- if (firstUnreadMessageId && firstUnreadMessageIdRef.current !== firstUnreadMessageId) {
- firstUnreadMessageIdRef.current = firstUnreadMessageId;
+ const firstUnreadMessage = getFirstUnreadMessage();
+ if (firstUnreadMessage) {
+ if (!firstUnreadMessageRef.current) {
+ firstUnreadMessageRef.current = firstUnreadMessage;
+ } else if (firstUnreadMessageRef.current.messageId !== firstUnreadMessage.messageId && firstUnreadMessage.createdAt < firstUnreadMessageRef.current.createdAt) {
+ firstUnreadMessageRef.current = firstUnreadMessage;
+ }
}
}
}, [messages.length]);
@@ -208,11 +211,9 @@ export const MessageList = (props: GroupChannelMessageListProps) => {
useEffect(() => {
if (state.config.groupChannel.enableMarkAsUnread && hasInitializedRef.current) {
if (readState === 'unread') {
- // when readState === 'unread' find first unread message
- const firstUnreadMessageId = getFirstUnreadMessage();
-
- if (firstUnreadMessageId !== firstUnreadMessageIdRef.current) {
- firstUnreadMessageIdRef.current = firstUnreadMessageId;
+ const firstUnreadMessage = getFirstUnreadMessage();
+ if (firstUnreadMessage && firstUnreadMessageRef.current?.messageId !== firstUnreadMessage.messageId) {
+ firstUnreadMessageRef.current = firstUnreadMessage;
}
}
setReadStateChanged(null);
@@ -227,9 +228,9 @@ export const MessageList = (props: GroupChannelMessageListProps) => {
setUnreadSinceDate(new Date());
}
} else if (isScrollBottomReached) {
- if (!firstUnreadMessageIdRef.current && currentChannel?.unreadMessageCount === 0) {
+ if (!firstUnreadMessageRef.current && currentChannel?.unreadMessageCount === 0) {
markAsReadAll(currentChannel);
- } else if (!firstUnreadMessageIdRef.current) {
+ } else if (!firstUnreadMessageRef.current) {
markAsReadAll(currentChannel);
}
}
@@ -315,7 +316,7 @@ export const MessageList = (props: GroupChannelMessageListProps) => {
setShowUnreadCount(false);
if (newMessages.length > 0) {
markAsUnread(newMessages[0] as SendableMessageType, 'internal');
- firstUnreadMessageIdRef.current = newMessages[0].messageId;
+ firstUnreadMessageRef.current = newMessages[0] as CoreMessageType;
} else if (currentChannel?.lastMessage.createdAt > currentChannel.myLastRead
&& markAsUnreadSourceRef?.current !== 'manual')
{
@@ -370,7 +371,7 @@ export const MessageList = (props: GroupChannelMessageListProps) => {
currentMessage: message as CoreMessageType,
currentChannel: currentChannel!,
hasPrevious: hasPrevious(),
- firstUnreadMessageId: firstUnreadMessageIdRef.current,
+ firstUnreadMessageId: firstUnreadMessageRef.current?.messageId, // firstUnreadMessageIdRef.current,
isCurrentDeviceMessage,
});
From ec05334dcb74642ce93cbf2ef32f65ea40a60a8d Mon Sep 17 00:00:00 2001
From: danney-chun <63285271+danney-chun@users.noreply.github.com>
Date: Wed, 16 Jul 2025 14:52:45 +0900
Subject: [PATCH 11/19] New Message -> New Messages
---
src/ui/NewMessageSeparator/index.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/ui/NewMessageSeparator/index.tsx b/src/ui/NewMessageSeparator/index.tsx
index e7b78e374..b2a544574 100644
--- a/src/ui/NewMessageSeparator/index.tsx
+++ b/src/ui/NewMessageSeparator/index.tsx
@@ -67,7 +67,7 @@ const NewMessageIndicator = ({
children
|| (
)
}
From 826421c9c276c2c198cb5fa51b8871f0fdbbdc3d Mon Sep 17 00:00:00 2001
From: danney-chun <63285271+danney-chun@users.noreply.github.com>
Date: Fri, 18 Jul 2025 12:43:32 +0900
Subject: [PATCH 12/19] Fixed QA Issue (#1356)
QA
---
.../components/Message/MessageView.tsx | 2 +-
.../MessageList/getMessagePartsInfo.ts | 6 +-
.../components/MessageList/index.tsx | 202 ++++++------------
.../UnreadCountFloatingButton/index.tsx | 2 +-
.../context/GroupChannelProvider.tsx | 11 +-
.../context/hooks/useGroupChannel.ts | 9 -
.../context/hooks/useMessageActions.ts | 10 -
src/modules/GroupChannel/context/types.ts | 3 -
src/ui/NewMessageSeparator/index.tsx | 2 +-
9 files changed, 79 insertions(+), 168 deletions(-)
diff --git a/src/modules/GroupChannel/components/Message/MessageView.tsx b/src/modules/GroupChannel/components/Message/MessageView.tsx
index 502f15818..a514db2af 100644
--- a/src/modules/GroupChannel/components/Message/MessageView.tsx
+++ b/src/modules/GroupChannel/components/Message/MessageView.tsx
@@ -431,7 +431,7 @@ const MessageView = (props: MessageViewProps) => {
&& (
)}
diff --git a/src/modules/GroupChannel/components/MessageList/getMessagePartsInfo.ts b/src/modules/GroupChannel/components/MessageList/getMessagePartsInfo.ts
index c01dfc771..8adc6eeb9 100644
--- a/src/modules/GroupChannel/components/MessageList/getMessagePartsInfo.ts
+++ b/src/modules/GroupChannel/components/MessageList/getMessagePartsInfo.ts
@@ -3,7 +3,7 @@ import isSameDay from 'date-fns/isSameDay';
import { compareMessagesForGrouping } from '../../../../utils/messages';
import { ReplyType } from '../../../../types';
-import { CoreMessageType } from '../../../../utils';
+import { CoreMessageType, isAdminMessage } from '../../../../utils';
import { StringSet } from '../../../../ui/Label/stringSet';
export interface GetMessagePartsInfoProps {
@@ -16,7 +16,6 @@ export interface GetMessagePartsInfoProps {
replyType?: string;
hasPrevious?: boolean;
firstUnreadMessageId?: number | string | undefined;
- isCurrentDeviceMessage?: boolean;
}
interface OutPuts {
@@ -38,7 +37,6 @@ export const getMessagePartsInfo = ({
currentChannel = null,
replyType = '',
firstUnreadMessageId,
- isCurrentDeviceMessage,
}: GetMessagePartsInfoProps): OutPuts => {
const previousMessage = allMessages[currentIndex - 1];
const nextMessage = allMessages[currentIndex + 1];
@@ -54,7 +52,7 @@ export const getMessagePartsInfo = ({
// https://stackoverflow.com/a/41855608
const hasSeparator = isLocalMessage ? false : !(previousMessageCreatedAt && (isSameDay(currentCreatedAt, previousMessageCreatedAt)));
- const hasNewMessageSeparator = !isCurrentDeviceMessage && firstUnreadMessageId === currentMessage.messageId;
+ const hasNewMessageSeparator = isLocalMessage ? false : (!isAdminMessage(currentMessage) && firstUnreadMessageId === currentMessage.messageId);
return {
chainTop,
diff --git a/src/modules/GroupChannel/components/MessageList/index.tsx b/src/modules/GroupChannel/components/MessageList/index.tsx
index b37708fe8..cf6fd0c7d 100644
--- a/src/modules/GroupChannel/components/MessageList/index.tsx
+++ b/src/modules/GroupChannel/components/MessageList/index.tsx
@@ -1,6 +1,6 @@
import './index.scss';
-import React, { useEffect, useState, useRef, useMemo } from 'react';
-import type { GroupChannel, Member } from '@sendbird/chat/groupChannel';
+import React, { useEffect, useState, useRef, useMemo, useCallback } from 'react';
+import type { Member } from '@sendbird/chat/groupChannel';
import { useGroupChannelHandler } from '@sendbird/uikit-tools';
import { CoreMessageType, isSendableMessage, getHTMLTextDirection, SendableMessageType } from '../../../../utils';
@@ -73,7 +73,6 @@ export const MessageList = (props: GroupChannelMessageListProps) => {
state: {
channelUrl,
hasNext,
- hasPrevious,
loading,
messages,
newMessages,
@@ -90,152 +89,111 @@ export const MessageList = (props: GroupChannelMessageListProps) => {
scrollDistanceFromBottomRef,
markAsUnreadSourceRef,
readState,
- currentDeviceMessageIdsRef,
},
actions: {
scrollToBottom,
setIsScrollBottomReached,
markAsReadAll,
markAsUnread,
- setReadStateChanged,
- isFromCurrentDevice,
},
} = useGroupChannel();
- const { state } = useSendbird();
+ const { state, state: { config: { groupChannel: { enableMarkAsUnread } } } } = useSendbird();
const { stringSet } = useLocalization();
const [unreadSinceDate, setUnreadSinceDate] = useState();
- const [isChangedChannel, setIsChangedChannel] = useState(false);
const [showUnreadCount, setShowUnreadCount] = useState(false);
- const firstUnreadMessageRef = useRef(undefined);
- const hasInitializedRef = useRef(false);
+ const isInitializedRef = useRef(false);
+ const separatorMessageRef = useRef(undefined);
- // current channel ref
- const currentChannelRef = useRef(undefined);
- // current messages ref
- const currentMessagesRef = useRef([]);
-
- /**
- * Find the first unread message in the message list
- */
- const findFirstUnreadMessage = (
- message: CoreMessageType,
- index: number,
- allMessages: CoreMessageType[],
- currentChannel: GroupChannel,
- ): boolean => {
- const currentCreatedAt = message.createdAt;
-
- // condition 1: message.createdAt === (channel.myLastRead + 1)
- if (currentCreatedAt === (currentChannel.myLastRead + 1)) {
- return true;
+ // Find the first unread message
+ const firstUnreadMessage = useMemo(() => {
+ if (!enableMarkAsUnread || !isInitializedRef.current || messages.length === 0 || readState === 'read') {
+ return undefined;
}
- // condition 2: there is no message that satisfies the condition, and the current message is the first message that satisfies the condition
- if (currentCreatedAt > (currentChannel.myLastRead + 1)) {
- const hasPreviousMatchingMessage = allMessages
- .slice(0, index)
- .some(msg => msg.createdAt === (currentChannel.myLastRead + 1));
- return !hasPreviousMatchingMessage;
- }
- return false;
- };
+ if (readState === 'unread') separatorMessageRef.current = undefined;
- const getFirstUnreadMessage = (): CoreMessageType | undefined => {
- if (state.config.groupChannel.enableMarkAsUnread) {
- for (let i = 0; i < messages.length; i++) {
- const message = messages[i] as CoreMessageType;
- const isFind = findFirstUnreadMessage(message, i, messages as CoreMessageType[], currentChannel);
+ const myLastRead = currentChannel.myLastRead;
+ const willFindMessageCreatedAt = myLastRead + 1;
- if (isFind && !isFromCurrentDevice(message.messageId)) {
- return message;
- }
- }
+ // 조건 1: 정확히 myLastRead + 1인 메시지 찾기
+ const exactMatchMessage = messages.find((message) => message.createdAt === willFindMessageCreatedAt,
+ );
+
+ if (exactMatchMessage) {
+ return exactMatchMessage as CoreMessageType;
}
- return undefined;
- };
- // check changed channel
- useEffect(() => {
- if (!currentChannel?.url) return;
- if (currentChannel?.url !== currentChannelRef.current?.url) {
- currentChannelRef.current = currentChannel;
+ // 조건 2: myLastRead + 1보다 큰 첫 번째 메시지 찾기
+ for (let i = 0; i < messages.length; i++) {
+ const message = messages[i] as CoreMessageType;
- // initialize
- firstUnreadMessageRef.current = undefined;
- setShowUnreadCount(false);
- hasInitializedRef.current = false;
- if (markAsUnreadSourceRef?.current !== undefined) {
- markAsUnreadSourceRef.current = undefined;
- }
- currentMessagesRef.current = [];
- currentDeviceMessageIdsRef.current.clear();
- setIsChangedChannel(true);
- } else {
- setIsChangedChannel(false);
- }
- }, [currentChannel?.url]);
+ if (message.createdAt > willFindMessageCreatedAt) {
+ // 이전에 정확히 myLastRead + 1인 메시지가 있는지 확인
+ const hasPreviousExactMatch = messages
+ .slice(0, i)
+ .some(msg => msg.createdAt === willFindMessageCreatedAt);
- // when enableMarkAsUnread is true, check changed messages
- useEffect(() => {
- if (state.config.groupChannel.enableMarkAsUnread && isChangedChannel) {
- if (!hasInitializedRef.current) {
- if (currentMessagesRef.current !== messages) {
- currentMessagesRef.current = messages as CoreMessageType[];
- const firstUnreadMessage = getFirstUnreadMessage();
- if (firstUnreadMessage) {
- firstUnreadMessageRef.current = firstUnreadMessage;
- }
- hasInitializedRef.current = true;
- setIsChangedChannel(false);
- }
- }
- }
- }, [messages]);
-
- useMemo(() => {
- if (state.config.groupChannel.enableMarkAsUnread && hasInitializedRef.current) {
- const firstUnreadMessage = getFirstUnreadMessage();
- if (firstUnreadMessage) {
- if (!firstUnreadMessageRef.current) {
- firstUnreadMessageRef.current = firstUnreadMessage;
- } else if (firstUnreadMessageRef.current.messageId !== firstUnreadMessage.messageId && firstUnreadMessage.createdAt < firstUnreadMessageRef.current.createdAt) {
- firstUnreadMessageRef.current = firstUnreadMessage;
+ if (!hasPreviousExactMatch) {
+ return message;
}
}
}
- }, [messages.length]);
+
+ return undefined;
+ }, [messages, currentChannel?.myLastRead, readState]);
useEffect(() => {
- if (state.config.groupChannel.enableMarkAsUnread && hasInitializedRef.current) {
- if (readState === 'unread') {
- const firstUnreadMessage = getFirstUnreadMessage();
- if (firstUnreadMessage && firstUnreadMessageRef.current?.messageId !== firstUnreadMessage.messageId) {
- firstUnreadMessageRef.current = firstUnreadMessage;
- }
- }
- setReadStateChanged(null);
+ if (currentChannel?.url && loading) {
+ // done get channel and messages
+ setShowUnreadCount(currentChannel?.unreadMessageCount > 0);
+ isInitializedRef.current = true;
}
- }, [readState]);
+ }, [currentChannel?.url, loading]);
useEffect(() => {
- if (!state.config.groupChannel.enableMarkAsUnread) {
+ if (!isInitializedRef.current) return;
+
+ if (!enableMarkAsUnread) {
+ // backward compatibility
if (isScrollBottomReached) {
setUnreadSinceDate(undefined);
} else {
setUnreadSinceDate(new Date());
}
} else if (isScrollBottomReached) {
- if (!firstUnreadMessageRef.current && currentChannel?.unreadMessageCount === 0) {
- markAsReadAll(currentChannel);
- } else if (!firstUnreadMessageRef.current) {
- markAsReadAll(currentChannel);
+ if (markAsUnreadSourceRef?.current !== 'manual') {
+ if (currentChannel?.unreadMessageCount > 0) {
+ if (separatorMessageRef.current) {
+ // called markAsReadAll as after the first unread message is displayed
+ markAsReadAll(currentChannel);
+ }
+ }
}
}
}, [isScrollBottomReached]);
+ const checkDisplayedNewMessageSeparator = useCallback((isNewMessageSeparatorVisible: boolean) => {
+ if (!isInitializedRef.current || !firstUnreadMessage) return;
+
+ if (isNewMessageSeparatorVisible) {
+ setShowUnreadCount(false);
+ if (newMessages?.length > 0) {
+ markAsUnread(newMessages[0] as SendableMessageType, 'internal');
+ separatorMessageRef.current = undefined;
+ } else if (firstUnreadMessage && markAsUnreadSourceRef?.current !== 'manual') {
+ if (!separatorMessageRef.current) {
+ separatorMessageRef.current = firstUnreadMessage;
+ }
+ markAsReadAll(currentChannel);
+ }
+ } else if (currentChannel?.unreadMessageCount > 0) {
+ setShowUnreadCount(true);
+ }
+ }, [firstUnreadMessage]);
+
/**
* 1. Move the message list scroll
* when each message's height is changed by `reactions` OR `showEdit`
@@ -260,8 +218,8 @@ export const MessageList = (props: GroupChannelMessageListProps) => {
return renderFrozenNotification();
},
unreadMessagesNotification() {
- if (state.config.groupChannel.enableMarkAsUnread) {
- if (!showUnreadCount) return null;
+ if (enableMarkAsUnread) {
+ if (!showUnreadCount || currentChannel?.unreadMessageCount === 0) return null;
return (
{
},
};
- const checkDisplayedNewMessageSeparator = (isNewMessageSeparatorVisible: boolean) => {
- if (isNewMessageSeparatorVisible) {
- setShowUnreadCount(false);
- if (newMessages.length > 0) {
- markAsUnread(newMessages[0] as SendableMessageType, 'internal');
- firstUnreadMessageRef.current = newMessages[0] as CoreMessageType;
- } else if (currentChannel?.lastMessage.createdAt > currentChannel.myLastRead
- && markAsUnreadSourceRef?.current !== 'manual')
- {
- markAsReadAll(currentChannel);
- }
- } else if (currentChannel?.unreadMessageCount > 0) {
- setShowUnreadCount(true);
- }
- };
-
if (loading) {
return renderPlaceholderLoader();
}
@@ -353,14 +295,14 @@ export const MessageList = (props: GroupChannelMessageListProps) => {
onLoadPrevious={loadPrevious}
onScrollPosition={(it) => {
const isScrollBottomReached = it === 'bottom';
- if (hasInitializedRef.current && isScrollBottomReached && newMessages.length > 0) {
+ if (isInitializedRef.current && isScrollBottomReached && newMessages.length > 0) {
resetNewMessages();
}
setIsScrollBottomReached(isScrollBottomReached);
}}
messages={messages}
renderMessage={({ message, index }) => {
- const isCurrentDeviceMessage = isFromCurrentDevice(message.messageId);
+ const finalFirstUnreadMessageId = separatorMessageRef.current?.messageId || firstUnreadMessage?.messageId;
const { chainTop, chainBottom, hasSeparator, hasNewMessageSeparator } = getMessagePartsInfo({
allMessages: messages as CoreMessageType[],
@@ -370,9 +312,7 @@ export const MessageList = (props: GroupChannelMessageListProps) => {
currentIndex: index,
currentMessage: message as CoreMessageType,
currentChannel: currentChannel!,
- hasPrevious: hasPrevious(),
- firstUnreadMessageId: firstUnreadMessageRef.current?.messageId, // firstUnreadMessageIdRef.current,
- isCurrentDeviceMessage,
+ firstUnreadMessageId: finalFirstUnreadMessageId,
});
const isOutgoingMessage = isSendableMessage(message) && message.sender.userId === state.config.userId;
diff --git a/src/modules/GroupChannel/components/UnreadCountFloatingButton/index.tsx b/src/modules/GroupChannel/components/UnreadCountFloatingButton/index.tsx
index 35e4eafcd..31b2e236e 100644
--- a/src/modules/GroupChannel/components/UnreadCountFloatingButton/index.tsx
+++ b/src/modules/GroupChannel/components/UnreadCountFloatingButton/index.tsx
@@ -37,7 +37,6 @@ export const UnreadCount: React.FC = ({
className,
)}
data-testid="sendbird-notification"
- onClick={onClick}
>