From 1241ac637ab26f096efb92be3f8eb5ca8cdc1fb0 Mon Sep 17 00:00:00 2001 From: EunSeo-Baek Date: Wed, 8 Mar 2023 14:58:42 +0900 Subject: [PATCH 01/14] Add props className to the ChannelHeader component --- .../Channel/components/ChannelHeader/index.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/smart-components/Channel/components/ChannelHeader/index.tsx b/src/smart-components/Channel/components/ChannelHeader/index.tsx index ba5dc4fcb..002447ad6 100644 --- a/src/smart-components/Channel/components/ChannelHeader/index.tsx +++ b/src/smart-components/Channel/components/ChannelHeader/index.tsx @@ -13,7 +13,13 @@ import { useChannelContext } from '../../context/ChannelProvider'; import { useMediaQueryContext } from '../../../../lib/MediaQueryContext'; import { noop } from '../../../../utils/utils' -const ChannelHeader: React.FC = () => { +interface ChannelHeaderProps { + className?: string; +} + +const ChannelHeader: React.FC = ({ + className = '', +}: ChannelHeaderProps): React.ReactElement => { const globalStore = useSendbirdStateContext(); const userId = globalStore?.config?.userId; const theme = globalStore?.config?.theme; @@ -33,7 +39,7 @@ const ChannelHeader: React.FC = () => { const { stringSet } = useContext(LocalizationContext); return ( -
+
{ isMobile && ( From 366b4e3d167982d150eddb126b7db5de2b8cb838 Mon Sep 17 00:00:00 2001 From: EunSeo-Baek Date: Wed, 8 Mar 2023 15:09:57 +0900 Subject: [PATCH 02/14] Add props className to the ChannelHeader and MessageList componenets --- .../Channel/components/ChannelHeader/index.tsx | 4 ++-- .../Channel/components/MessageList/index.tsx | 17 +++++++++-------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/smart-components/Channel/components/ChannelHeader/index.tsx b/src/smart-components/Channel/components/ChannelHeader/index.tsx index 002447ad6..ed0512fc6 100644 --- a/src/smart-components/Channel/components/ChannelHeader/index.tsx +++ b/src/smart-components/Channel/components/ChannelHeader/index.tsx @@ -17,9 +17,9 @@ interface ChannelHeaderProps { className?: string; } -const ChannelHeader: React.FC = ({ +const ChannelHeader: React.FC = ({ className = '', -}: ChannelHeaderProps): React.ReactElement => { +}) => { const globalStore = useSendbirdStateContext(); const userId = globalStore?.config?.userId; const theme = globalStore?.config?.theme; diff --git a/src/smart-components/Channel/components/MessageList/index.tsx b/src/smart-components/Channel/components/MessageList/index.tsx index 6073b5ffa..ce2c1afac 100644 --- a/src/smart-components/Channel/components/MessageList/index.tsx +++ b/src/smart-components/Channel/components/MessageList/index.tsx @@ -11,7 +11,8 @@ import Message from '../Message'; import { RenderCustomSeparatorProps, RenderMessageProps } from '../../../../types'; import { isAboutSame } from '../../context/utils'; -export type MessageListProps = { +export interface MessageListProps { + className?: string; renderMessage?: (props: RenderMessageProps) => React.ReactElement; renderPlaceholderEmpty?: () => React.ReactElement; renderCustomSeparator?: (props: RenderCustomSeparatorProps) => React.ReactElement; @@ -19,12 +20,12 @@ export type MessageListProps = { const SCROLL_REF_CLASS_NAME = '.sendbird-msg--scroll-ref'; -const MessageList: React.FC = (props: MessageListProps) => { - const { - renderMessage, - renderPlaceholderEmpty, - renderCustomSeparator, - } = props; +const MessageList: React.FC = ({ + className = '', + renderMessage, + renderPlaceholderEmpty, + renderCustomSeparator, +}) => { const { allMessages, hasMorePrev, @@ -170,7 +171,7 @@ const MessageList: React.FC = (props: MessageListProps) => { ); } return ( -
+
Date: Wed, 8 Mar 2023 15:27:49 +0900 Subject: [PATCH 03/14] Remove Notifications and LoadingPlacholder from ChannelUI --- .../Channel/components/ChannelUI/index.tsx | 96 +++---------------- .../Channel/components/MessageList/index.tsx | 24 ++--- 2 files changed, 27 insertions(+), 93 deletions(-) diff --git a/src/smart-components/Channel/components/ChannelUI/index.tsx b/src/smart-components/Channel/components/ChannelUI/index.tsx index 5b58b7645..ce2a5a30f 100644 --- a/src/smart-components/Channel/components/ChannelUI/index.tsx +++ b/src/smart-components/Channel/components/ChannelUI/index.tsx @@ -1,6 +1,6 @@ import './channel-ui.scss'; -import React, { useEffect, useState } from 'react'; +import React from 'react'; import useSendbirdStateContext from '../../../../hooks/useSendbirdStateContext'; import { useChannelContext } from '../../context/ChannelProvider'; @@ -9,11 +9,8 @@ import ConnectionStatus from '../../../../ui/ConnectionStatus'; import ChannelHeader from '../ChannelHeader'; import MessageList from '../MessageList'; import TypingIndicator from '../TypingIndicator'; -import FrozenNotification from '../FrozenNotification'; -import UnreadCount from '../UnreadCount'; import MessageInputWrapper from '../MessageInput'; import { RenderCustomSeparatorProps, RenderMessageProps } from '../../../../types'; -import * as messageActionTypes from '../../context/dux/actionTypes'; export interface ChannelUIProps { isLoading?: boolean; @@ -23,9 +20,9 @@ export interface ChannelUIProps { renderChannelHeader?: () => React.ReactElement; renderMessage?: (props: RenderMessageProps) => React.ReactElement; renderMessageInput?: () => React.ReactElement; - renderFileUploadIcon?: () => React.ReactElement; - renderVoiceMessageIcon?: () => React.ReactElement; - renderSendMessageIcon?: () => React.ReactElement; + renderFileUploadIcon?: () => React.ReactElement; + renderVoiceMessageIcon?: () => React.ReactElement; + renderSendMessageIcon?: () => React.ReactElement; renderTypingIndicator?: () => React.ReactElement; renderCustomSeparator?: (props: RenderCustomSeparatorProps) => React.ReactElement; } @@ -45,28 +42,9 @@ const ChannelUI: React.FC = ({ renderSendMessageIcon, }: ChannelUIProps) => { const { - currentGroupChannel, channelUrl, isInvalid, - unreadSince, - loading, - setInitialTimeStamp, - setAnimatedMessageId, - setHighLightedMessageId, - scrollRef, - messagesDispatcher, - disableMarkAsRead, } = useChannelContext(); - const [unreadCount, setUnreadCount] = useState(0); - useEffect(() => { - // simple debounce to avoid flicker of UnreadCount badge - const handler = setTimeout(() => { - setUnreadCount(currentGroupChannel?.unreadMessageCount); - }, 1000); - return () => { - clearTimeout(handler); - } - }, [currentGroupChannel?.unreadMessageCount]); const globalStore = useSendbirdStateContext(); const sdkError = globalStore?.stores?.sdkStore?.error; @@ -122,62 +100,16 @@ const ChannelUI: React.FC = ({ } return (
- { - renderChannelHeader?.() || ( - - ) - } - { - currentGroupChannel?.isFrozen && ( - - ) - } - { - unreadCount > 0 && ( - { - setUnreadCount(0); - if (scrollRef?.current?.scrollTop) { - scrollRef.current.scrollTop = scrollRef?.current?.scrollHeight - scrollRef?.current?.offsetHeight; - } - if (!disableMarkAsRead) { - try { - currentGroupChannel?.markAsRead(); - } catch { - // - } - messagesDispatcher({ - type: messageActionTypes.MARK_AS_READ, - payload: { channel: currentGroupChannel }, - }); - } - setInitialTimeStamp(null); - setAnimatedMessageId(null); - setHighLightedMessageId(null); - }} - /> - ) - } - { - loading - ? ( -
- { - renderPlaceholderLoader?.() || ( - - ) - } -
- ) : ( - - ) - } + {renderChannelHeader?.() || ( + + )} +
{ renderMessageInput?.() || ( diff --git a/src/smart-components/Channel/components/MessageList/index.tsx b/src/smart-components/Channel/components/MessageList/index.tsx index ce2c1afac..2b116157b 100644 --- a/src/smart-components/Channel/components/MessageList/index.tsx +++ b/src/smart-components/Channel/components/MessageList/index.tsx @@ -16,6 +16,7 @@ export interface MessageListProps { renderMessage?: (props: RenderMessageProps) => React.ReactElement; renderPlaceholderEmpty?: () => React.ReactElement; renderCustomSeparator?: (props: RenderCustomSeparatorProps) => React.ReactElement; + renderPlaceholderLoader?: () => React.ReactElement; }; const SCROLL_REF_CLASS_NAME = '.sendbird-msg--scroll-ref'; @@ -25,6 +26,7 @@ const MessageList: React.FC = ({ renderMessage, renderPlaceholderEmpty, renderCustomSeparator, + renderPlaceholderLoader, }) => { const { allMessages, @@ -41,6 +43,7 @@ const MessageList: React.FC = ({ currentGroupChannel, disableMarkAsRead, replyType, + loading, } = useChannelContext(); const [scrollBottom, setScrollBottom] = useState(0); @@ -157,18 +160,17 @@ const MessageList: React.FC = ({ ); }, [allMessages]); + if (loading) { + if (renderPlaceholderLoader && typeof renderPlaceholderLoader === 'function') { + return renderPlaceholderLoader(); + } + return ; + } if (allMessages.length < 1) { - return ( - <> - { - renderPlaceholderEmpty?.() || ( - ) - } - - ); + if (renderPlaceholderEmpty && typeof renderPlaceholderEmpty === 'function') { + return renderPlaceholderEmpty(); + } + return ; } return (
From 4ccfdf0f17c843f9f60c09e3002859f673d9b5a5 Mon Sep 17 00:00:00 2001 From: EunSeo-Baek Date: Wed, 8 Mar 2023 15:44:25 +0900 Subject: [PATCH 04/14] Make new function getMessagePartsInfo to clean up the calculation logic in allMessages.map --- .../MessageList/getMessagePartsInfo.ts | 45 +++++++++++++++++++ .../Channel/components/MessageList/index.tsx | 26 +++++------ 2 files changed, 58 insertions(+), 13 deletions(-) create mode 100644 src/smart-components/Channel/components/MessageList/getMessagePartsInfo.ts diff --git a/src/smart-components/Channel/components/MessageList/getMessagePartsInfo.ts b/src/smart-components/Channel/components/MessageList/getMessagePartsInfo.ts new file mode 100644 index 000000000..69c44f6c1 --- /dev/null +++ b/src/smart-components/Channel/components/MessageList/getMessagePartsInfo.ts @@ -0,0 +1,45 @@ +import { AdminMessage, BaseMessage, FileMessage } from '@sendbird/chat/message'; +import isSameDay from 'date-fns/isSameDay'; + +import { compareMessagesForGrouping } from '../../context/utils'; + +export interface GetMessagePartsInfoProps { + allMessages: Array, + isMessageGroupingEnabled, + currentIndex, + currentMessage, + currentChannel, + replyType, +} + +interface OutPuts { + chainTop: boolean, + chainBottom: boolean, + hasSeparator: boolean, +} + +export const getMessagePartsInfo = ({ + allMessages, + isMessageGroupingEnabled, + currentIndex, + currentMessage, + currentChannel, + replyType, +}): OutPuts => { + const previousMessage = allMessages[currentIndex - 1]; + const nextMessage = allMessages[currentIndex + 1]; + const [chainTop, chainBottom] = isMessageGroupingEnabled + ? compareMessagesForGrouping(previousMessage, currentMessage, nextMessage, currentChannel, replyType) + : [false, false]; + const previousMessageCreatedAt = previousMessage?.createdAt; + const currentCreatedAt = currentMessage.createdAt; + // https://stackoverflow.com/a/41855608 + const hasSeparator = !(previousMessageCreatedAt && ( + isSameDay(currentCreatedAt, previousMessageCreatedAt) + )); + return { + chainTop, + chainBottom, + hasSeparator, + } +}; diff --git a/src/smart-components/Channel/components/MessageList/index.tsx b/src/smart-components/Channel/components/MessageList/index.tsx index 2b116157b..19ab3e380 100644 --- a/src/smart-components/Channel/components/MessageList/index.tsx +++ b/src/smart-components/Channel/components/MessageList/index.tsx @@ -1,15 +1,14 @@ import './message-list.scss'; import React, { useState, useMemo } from 'react'; -import isSameDay from 'date-fns/isSameDay'; import { useChannelContext } from '../../context/ChannelProvider'; import PlaceHolder, { PlaceHolderTypes } from '../../../../ui/PlaceHolder'; import Icon, { IconTypes, IconColors } from '../../../../ui/Icon'; -import { compareMessagesForGrouping } from '../../context/utils'; import Message from '../Message'; import { RenderCustomSeparatorProps, RenderMessageProps } from '../../../../types'; import { isAboutSame } from '../../context/utils'; +import { getMessagePartsInfo } from './getMessagePartsInfo'; export interface MessageListProps { className?: string; @@ -122,17 +121,18 @@ const MessageList: React.FC = ({ const memoizedAllMessages = useMemo(() => { return ( allMessages.map((m, idx) => { - const previousMessage = allMessages[idx - 1]; - const nextMessage = allMessages[idx + 1]; - const [chainTop, chainBottom] = isMessageGroupingEnabled - ? compareMessagesForGrouping(previousMessage, m, nextMessage, currentGroupChannel, replyType) - : [false, false]; - const previousMessageCreatedAt = previousMessage?.createdAt; - const currentCreatedAt = m.createdAt; - // https://stackoverflow.com/a/41855608 - const hasSeparator = !(previousMessageCreatedAt && ( - isSameDay(currentCreatedAt, previousMessageCreatedAt) - )); + const { + chainTop, + chainBottom, + hasSeparator, + } = getMessagePartsInfo({ + allMessages, + replyType, + isMessageGroupingEnabled, + currentIndex: idx, + currentMessage: m, + currentChannel: currentGroupChannel, + }); const handleScroll = () => { const current = scrollRef?.current; From fd876767236d8e25fe43811dbb3a9105edde5b3c Mon Sep 17 00:00:00 2001 From: EunSeo-Baek Date: Wed, 8 Mar 2023 15:55:40 +0900 Subject: [PATCH 05/14] Do not apply useMemo for all messages --- .../Channel/components/MessageList/index.tsx | 80 +++++++++---------- 1 file changed, 36 insertions(+), 44 deletions(-) diff --git a/src/smart-components/Channel/components/MessageList/index.tsx b/src/smart-components/Channel/components/MessageList/index.tsx index 19ab3e380..3c7bf9ab9 100644 --- a/src/smart-components/Channel/components/MessageList/index.tsx +++ b/src/smart-components/Channel/components/MessageList/index.tsx @@ -1,6 +1,6 @@ import './message-list.scss'; -import React, { useState, useMemo } from 'react'; +import React, { useState } from 'react'; import { useChannelContext } from '../../context/ChannelProvider'; import PlaceHolder, { PlaceHolderTypes } from '../../../../ui/PlaceHolder'; @@ -117,48 +117,15 @@ const MessageList: React.FC = ({ } }; - // Because every message components are re-rendered everytime by every scroll events - const memoizedAllMessages = useMemo(() => { - return ( - allMessages.map((m, idx) => { - const { - chainTop, - chainBottom, - hasSeparator, - } = getMessagePartsInfo({ - allMessages, - replyType, - isMessageGroupingEnabled, - currentIndex: idx, - currentMessage: m, - currentChannel: currentGroupChannel, - }); - - const handleScroll = () => { - const current = scrollRef?.current; - if (current) { - const bottom = current.scrollHeight - current.scrollTop - current.offsetHeight; - if (scrollBottom < bottom) { - current.scrollTop += bottom - scrollBottom; - } - } - }; - - return ( - - ); - }) - ); - }, [allMessages]); + const handleScroll = () => { + const current = scrollRef?.current; + if (current) { + const bottom = current.scrollHeight - current.scrollTop - current.offsetHeight; + if (scrollBottom < bottom) { + current.scrollTop += bottom - scrollBottom; + } + } + }; if (loading) { if (renderPlaceholderLoader && typeof renderPlaceholderLoader === 'function') { @@ -181,7 +148,32 @@ const MessageList: React.FC = ({ ref={scrollRef} onScroll={onScroll} > - {memoizedAllMessages} + {allMessages.map((m, idx) => { + const { + chainTop, + chainBottom, + hasSeparator, + } = getMessagePartsInfo({ + allMessages, + replyType, + isMessageGroupingEnabled, + currentIndex: idx, + currentMessage: m, + currentChannel: currentGroupChannel, + }); + return ( + + ); + })}
{ From 7b28daf5cc99c15a6eab261c55c0f55d743c7e4d Mon Sep 17 00:00:00 2001 From: EunSeo-Baek Date: Wed, 8 Mar 2023 16:09:35 +0900 Subject: [PATCH 06/14] Undisplay UnreadCount component using CSS instead of JS condition --- .../Channel/components/UnreadCount/index.tsx | 9 ++++----- .../Channel/components/UnreadCount/unread-count.scss | 4 ++++ 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/smart-components/Channel/components/UnreadCount/index.tsx b/src/smart-components/Channel/components/UnreadCount/index.tsx index abaa385b4..834f631f3 100644 --- a/src/smart-components/Channel/components/UnreadCount/index.tsx +++ b/src/smart-components/Channel/components/UnreadCount/index.tsx @@ -22,12 +22,11 @@ const UnreadCount: React.FC = (props: UnreadCountProps) => { const timeArray = time?.toString?.()?.split(' ') || []; timeArray?.splice(-2, 0, stringSet.CHANNEL__MESSAGE_LIST__NOTIFICATION__ON); - if (count < 1) { - return; - } - return ( -
+
+ {currentGroupChannel?.isFrozen && ( + + )} + { + if (scrollRef?.current?.scrollTop) { + scrollRef.current.scrollTop = scrollRef?.current?.scrollHeight - scrollRef?.current?.offsetHeight; + } + if (!disableMarkAsRead) { + try { + currentGroupChannel?.markAsRead(); + } catch { + // + } + messagesDispatcher({ + type: messageActionTypes.MARK_AS_READ, + payload: { channel: currentGroupChannel }, + }); + } + setInitialTimeStamp(null); + setAnimatedMessageId(null); + setHighLightedMessageId(null); + }} + /> { // This flag is an unmatched variable (scrollBottom > 1) && ( diff --git a/src/smart-components/Channel/components/MessageList/message-list.scss b/src/smart-components/Channel/components/MessageList/message-list.scss index 519ba0298..1ae751ca8 100644 --- a/src/smart-components/Channel/components/MessageList/message-list.scss +++ b/src/smart-components/Channel/components/MessageList/message-list.scss @@ -1,7 +1,9 @@ @import '../../../../styles/variables'; .sendbird-conversation__messages { + position: relative; .sendbird-conversation__messages-padding { + position: relative; padding-left: 24px; padding-right: 24px; height: 100%; @@ -15,6 +17,13 @@ } } +.sendbird-conversation__messages__notification { + position: fixed; + top: 0px; + width: calc(100% - 50px); + margin-left: 25px; +} + .sendbird-conversation__scroll-bottom-button { position: sticky; display: flex; diff --git a/src/smart-components/Channel/components/UnreadCount/index.tsx b/src/smart-components/Channel/components/UnreadCount/index.tsx index 7baa4001c..6844a61de 100644 --- a/src/smart-components/Channel/components/UnreadCount/index.tsx +++ b/src/smart-components/Channel/components/UnreadCount/index.tsx @@ -13,7 +13,7 @@ export interface UnreadCountProps { } const UnreadCount: React.FC = ({ - className, + className = '', count, time = '', onClick, diff --git a/src/smart-components/Channel/components/UnreadCount/unread-count.scss b/src/smart-components/Channel/components/UnreadCount/unread-count.scss index 1a7bd647e..d991f669d 100644 --- a/src/smart-components/Channel/components/UnreadCount/unread-count.scss +++ b/src/smart-components/Channel/components/UnreadCount/unread-count.scss @@ -1,5 +1,10 @@ @import '../../../../styles/variables'; +.sendbird-notification--undisplay, +.sendbird-notification { + position: absolute; +} + .sendbird-notification { margin-top: 8px; margin-left: 24px; From 2a5d94c8f1883efc55a49bd987d5abebdacb6f87 Mon Sep 17 00:00:00 2001 From: EunSeo-Baek Date: Thu, 9 Mar 2023 17:41:56 +0900 Subject: [PATCH 10/14] Add default values of getMessagePartsInfo props & Add tests for it --- .../__test__/getMessagePartsInfo.spec.js | 173 ++++++++++++++++++ .../MessageList/getMessagePartsInfo.ts | 27 +-- 2 files changed, 187 insertions(+), 13 deletions(-) create mode 100644 src/smart-components/Channel/components/MessageList/__test__/getMessagePartsInfo.spec.js diff --git a/src/smart-components/Channel/components/MessageList/__test__/getMessagePartsInfo.spec.js b/src/smart-components/Channel/components/MessageList/__test__/getMessagePartsInfo.spec.js new file mode 100644 index 000000000..f256eb229 --- /dev/null +++ b/src/smart-components/Channel/components/MessageList/__test__/getMessagePartsInfo.spec.js @@ -0,0 +1,173 @@ +import { getMessagePartsInfo } from "../getMessagePartsInfo"; + +const mockChannel = { + isGroupChannel: () => true, + getUnreadMemberCount: () => 0, + getUndeliveredMemberCount: () => 0, +}; + +const currentTime = Date.now(); +const timeList = [1, 2, 3].map((gap) => { + const time = new Date(currentTime); + time.setMinutes(time.getMinutes() + gap); + return time.valueOf(); +}); +const users = [{ userId: 1 }, { userId: 2 }]; + +// same sender & same sent at +const messageGroup1 = [1, 2, 3].map((n) => ({ + messageId: n, + sendingStatus: 'succeeded', + createdAt: timeList[0], + messageType: 'user', + sender: users[0], +})); +// same sender & different sent at +const messageGroup2 = [1, 2, 3].map((n, i) => ({ + messageId: n, + sendingStatus: 'succeeded', + createdAt: timeList[i], + messageType: 'user', + sender: users[0], +})); +// different sender && same sent at +const messageGroup3 = [1, 2, 3].map((n, i) => ({ + messageId: n, + sendingStatus: 'succeeded', + createdAt: timeList[0], + messageType: 'user', + sender: users[i], +})); + +describe('getMessagePartsInfo', () => { + it('should group messages that are sent at same time', () => { + const defaultSetting = { + allMessages: messageGroup1, + isMessageGroupingEnabled: true, + currentChannel: mockChannel, + replyType: 'THREAD', + }; + const firstGroupingInfo = getMessagePartsInfo({ + ...defaultSetting, + currentIndex: 0, + currentMessage: defaultSetting.allMessages[0], + }); + expect(firstGroupingInfo.chainTop).toBe(false); + expect(firstGroupingInfo.chainBottom).toBe(true); + expect(firstGroupingInfo.hasSeparator).toBe(true); + const secondGroupingInfo = getMessagePartsInfo({ + ...defaultSetting, + currentIndex: 1, + currentMessage: defaultSetting.allMessages[1], + }); + expect(secondGroupingInfo.chainTop).toBe(true); + expect(secondGroupingInfo.chainBottom).toBe(true); + expect(secondGroupingInfo.hasSeparator).toBe(false); + const thirdGroupingInfo = getMessagePartsInfo({ + ...defaultSetting, + currentIndex: 2, + currentMessage: defaultSetting.allMessages[2], + }); + expect(thirdGroupingInfo.chainTop).toBe(true); + expect(thirdGroupingInfo.chainBottom).toBe(false); + expect(thirdGroupingInfo.hasSeparator).toBe(false); + }); + + it('should not group messages if isMessageGroupingEnabled is false', () => { + const defaultSetting = { + allMessages: messageGroup1, + isMessageGroupingEnabled: false, + currentChannel: mockChannel, + replyType: 'THREAD', + }; + const firstGroupingInfo = getMessagePartsInfo({ + ...defaultSetting, + currentIndex: 0, + currentMessage: defaultSetting.allMessages[0], + }); + expect(firstGroupingInfo.chainTop).toBe(false); + expect(firstGroupingInfo.chainBottom).toBe(false); + expect(firstGroupingInfo.hasSeparator).toBe(true); + const secondGroupingInfo = getMessagePartsInfo({ + ...defaultSetting, + currentIndex: 1, + currentMessage: defaultSetting.allMessages[1], + }); + expect(secondGroupingInfo.chainTop).toBe(false); + expect(secondGroupingInfo.chainBottom).toBe(false); + expect(secondGroupingInfo.hasSeparator).toBe(false); + const thirdGroupingInfo = getMessagePartsInfo({ + ...defaultSetting, + currentIndex: 2, + currentMessage: defaultSetting.allMessages[2], + }); + expect(thirdGroupingInfo.chainTop).toBe(false); + expect(thirdGroupingInfo.chainBottom).toBe(false); + expect(thirdGroupingInfo.hasSeparator).toBe(false); + }); + + it('should not group messages if sent time are different', () => { + const defaultSetting = { + allMessages: messageGroup2, + isMessageGroupingEnabled: true, + currentChannel: mockChannel, + replyType: 'THREAD', + }; + const firstGroupingInfo = getMessagePartsInfo({ + ...defaultSetting, + currentIndex: 0, + currentMessage: defaultSetting.allMessages[0], + }); + expect(firstGroupingInfo.chainTop).toBe(false); + expect(firstGroupingInfo.chainBottom).toBe(false); + expect(firstGroupingInfo.hasSeparator).toBe(true); + const secondGroupingInfo = getMessagePartsInfo({ + ...defaultSetting, + currentIndex: 1, + currentMessage: defaultSetting.allMessages[1], + }); + expect(secondGroupingInfo.chainTop).toBe(false); + expect(secondGroupingInfo.chainBottom).toBe(false); + expect(secondGroupingInfo.hasSeparator).toBe(false); + const thirdGroupingInfo = getMessagePartsInfo({ + ...defaultSetting, + currentIndex: 2, + currentMessage: defaultSetting.allMessages[2], + }); + expect(thirdGroupingInfo.chainTop).toBe(false); + expect(thirdGroupingInfo.chainBottom).toBe(false); + expect(thirdGroupingInfo.hasSeparator).toBe(false); + }); + it('should not group messages if sender is different', () => { + const defaultSetting = { + allMessages: messageGroup3, + isMessageGroupingEnabled: true, + currentChannel: mockChannel, + replyType: 'THREAD', + }; + const firstGroupingInfo = getMessagePartsInfo({ + ...defaultSetting, + currentIndex: 0, + currentMessage: defaultSetting.allMessages[0], + }); + expect(firstGroupingInfo.chainTop).toBe(false); + expect(firstGroupingInfo.chainBottom).toBe(false); + expect(firstGroupingInfo.hasSeparator).toBe(true); + const secondGroupingInfo = getMessagePartsInfo({ + ...defaultSetting, + currentIndex: 1, + currentMessage: defaultSetting.allMessages[1], + }); + expect(secondGroupingInfo.chainTop).toBe(false); + expect(secondGroupingInfo.chainBottom).toBe(false); + expect(secondGroupingInfo.hasSeparator).toBe(false); + const thirdGroupingInfo = getMessagePartsInfo({ + ...defaultSetting, + currentIndex: 2, + currentMessage: defaultSetting.allMessages[2], + }); + expect(thirdGroupingInfo.chainTop).toBe(false); + expect(thirdGroupingInfo.chainBottom).toBe(false); + expect(thirdGroupingInfo.hasSeparator).toBe(false); + }); +}); diff --git a/src/smart-components/Channel/components/MessageList/getMessagePartsInfo.ts b/src/smart-components/Channel/components/MessageList/getMessagePartsInfo.ts index 8e8ad3840..6239cf953 100644 --- a/src/smart-components/Channel/components/MessageList/getMessagePartsInfo.ts +++ b/src/smart-components/Channel/components/MessageList/getMessagePartsInfo.ts @@ -1,15 +1,16 @@ -import { AdminMessage, BaseMessage, FileMessage } from '@sendbird/chat/message'; +import { GroupChannel } from '@sendbird/chat/groupChannel'; +import { AdminMessage, FileMessage, UserMessage } from '@sendbird/chat/message'; import isSameDay from 'date-fns/isSameDay'; import { compareMessagesForGrouping } from '../../context/utils'; export interface GetMessagePartsInfoProps { - allMessages: Array, - isMessageGroupingEnabled, - currentIndex, - currentMessage, - currentChannel, - replyType, + allMessages: Array; + isMessageGroupingEnabled: boolean; + currentIndex: number; + currentMessage: UserMessage | FileMessage | AdminMessage; + currentChannel: GroupChannel; + replyType: string; } interface OutPuts { @@ -19,12 +20,12 @@ interface OutPuts { } export const getMessagePartsInfo = ({ - allMessages, - isMessageGroupingEnabled, - currentIndex, - currentMessage, - currentChannel, - replyType, + allMessages = [], + isMessageGroupingEnabled = true, + currentIndex = 0, + currentMessage = null, + currentChannel = null, + replyType = '', }: GetMessagePartsInfoProps): OutPuts => { const previousMessage = allMessages[currentIndex - 1]; const nextMessage = allMessages[currentIndex + 1]; From 781a28bdc53c2dad14e4c15927102a87e1641710 Mon Sep 17 00:00:00 2001 From: EunSeo-Baek Date: Thu, 9 Mar 2023 19:58:13 +0900 Subject: [PATCH 11/14] Do not scroll into bottom when message is initially mounted --- .../Channel/components/Message/index.tsx | 7 +++++-- .../Channel/components/MessageList/index.tsx | 7 ++++--- src/smart-components/Channel/context/const.ts | 2 ++ src/utils/useDidMountEffect.ts | 14 ++++++++++++++ 4 files changed, 25 insertions(+), 5 deletions(-) create mode 100644 src/utils/useDidMountEffect.ts diff --git a/src/smart-components/Channel/components/Message/index.tsx b/src/smart-components/Channel/components/Message/index.tsx index cfd4bde6b..a57ebf2d6 100644 --- a/src/smart-components/Channel/components/Message/index.tsx +++ b/src/smart-components/Channel/components/Message/index.tsx @@ -8,6 +8,7 @@ import React, { import type { FileMessage } from '@sendbird/chat/message'; import format from 'date-fns/format'; +import useDidMountEffect from '../../../../utils/useDidMountEffect'; import SuggestedMentionList from '../SuggestedMentionList'; import useSendbirdStateContext from '../../../../hooks/useSendbirdStateContext'; import { useChannelContext } from '../../context/ChannelProvider'; @@ -128,8 +129,10 @@ const Message = ({ })); }, [mentionedUserIds]); - useLayoutEffect(() => { - handleScroll?.(); + useDidMountEffect(() => { + if (currentGroupChannel?.lastMessage?.messageId === message?.messageId) { + handleScroll?.(); + } }, [showEdit, message?.reactions?.length]); useLayoutEffect(() => { diff --git a/src/smart-components/Channel/components/MessageList/index.tsx b/src/smart-components/Channel/components/MessageList/index.tsx index d246516c5..27e3f1873 100644 --- a/src/smart-components/Channel/components/MessageList/index.tsx +++ b/src/smart-components/Channel/components/MessageList/index.tsx @@ -11,6 +11,7 @@ import { isAboutSame } from '../../context/utils'; import { getMessagePartsInfo } from './getMessagePartsInfo'; import UnreadCount from '../UnreadCount'; import FrozenNotification from '../FrozenNotification'; +import { MESSAGE_SCROLL_BUFFER } from '../../context/const'; export interface MessageListProps { className?: string; @@ -76,7 +77,7 @@ const MessageList: React.FC = ({ }); } - if (isAboutSame(clientHeight + scrollTop, scrollHeight, 10)) { + if (isAboutSame(clientHeight + scrollTop, scrollHeight, MESSAGE_SCROLL_BUFFER)) { onScrollDownCallback(([messages]) => { if (messages) { try { @@ -95,7 +96,7 @@ const MessageList: React.FC = ({ setScrollBottom(current.scrollHeight - current.scrollTop - current.offsetHeight) } - if (!disableMarkAsRead && isAboutSame(clientHeight + scrollTop, scrollHeight, 10)) { + if (!disableMarkAsRead && isAboutSame(clientHeight + scrollTop, scrollHeight, MESSAGE_SCROLL_BUFFER)) { // Mark as read if scroll is at end setTimeout(() => { messagesDispatcher({ @@ -124,7 +125,7 @@ const MessageList: React.FC = ({ const current = scrollRef?.current; if (current) { const bottom = current.scrollHeight - current.scrollTop - current.offsetHeight; - if (scrollBottom < bottom) { + if (scrollBottom < bottom && scrollBottom <= MESSAGE_SCROLL_BUFFER) { current.scrollTop += bottom - scrollBottom; } } diff --git a/src/smart-components/Channel/context/const.ts b/src/smart-components/Channel/context/const.ts index 410881a1a..804e4d205 100644 --- a/src/smart-components/Channel/context/const.ts +++ b/src/smart-components/Channel/context/const.ts @@ -1,3 +1,5 @@ +export const MESSAGE_SCROLL_BUFFER = 10; + export const PREV_RESULT_SIZE = 30; export const NEXT_RESULT_SIZE = 15; diff --git a/src/utils/useDidMountEffect.ts b/src/utils/useDidMountEffect.ts new file mode 100644 index 000000000..0937e9b0c --- /dev/null +++ b/src/utils/useDidMountEffect.ts @@ -0,0 +1,14 @@ +import { useEffect, useState } from 'react'; + +const useDidMountEffect = (func: Function, deps: Array): void => { + const [didMount, setDidmount] = useState(false); + useEffect(() => { + if (didMount) { + func(); + } else { + setDidmount(true); + } + }, deps); +}; + +export default useDidMountEffect; From f37a09d22e1f710164eeee1a3eaa27f6ff3749fa Mon Sep 17 00:00:00 2001 From: EunSeo-Baek Date: Thu, 9 Mar 2023 20:07:20 +0900 Subject: [PATCH 12/14] Do not scroll into bottom when context menu or reaction menu is activated --- .../Channel/context/hooks/useHandleChannelEvents.ts | 7 +++++-- src/ui/ContextMenu/index.tsx | 2 ++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/smart-components/Channel/context/hooks/useHandleChannelEvents.ts b/src/smart-components/Channel/context/hooks/useHandleChannelEvents.ts index 990b9cdc1..fa1786e68 100644 --- a/src/smart-components/Channel/context/hooks/useHandleChannelEvents.ts +++ b/src/smart-components/Channel/context/hooks/useHandleChannelEvents.ts @@ -64,8 +64,11 @@ function useHandleChannelEvents({ type: messageActions.ON_MESSAGE_RECEIVED, payload: { channel, message }, }); - - if (scrollToEnd) { + if (scrollToEnd + && document.getElementById('sendbird-dropdown-portal').childElementCount === 0 + && document.getElementById('sendbird-emoji-list-portal').childElementCount === 0 + ) { + // and !openContextMenu try { setTimeout(() => { if (!disableMarkAsRead) { diff --git a/src/ui/ContextMenu/index.tsx b/src/ui/ContextMenu/index.tsx index 28c76cb88..59fb01b80 100644 --- a/src/ui/ContextMenu/index.tsx +++ b/src/ui/ContextMenu/index.tsx @@ -55,6 +55,8 @@ export const MenuRoot = (): ReactElement => ( className="sendbird-dropdown-portal" /> ); + +// For the test environment export const EmojiReactionListRoot = (): ReactElement =>
; type MenuDisplayingFunc = () => void; From a4e4fd9d02efc95fa642846e08445c3f18dc9fa3 Mon Sep 17 00:00:00 2001 From: EunSeo-Baek Date: Thu, 9 Mar 2023 20:13:22 +0900 Subject: [PATCH 13/14] Fix lint type warning of function parameter --- src/utils/useDidMountEffect.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/useDidMountEffect.ts b/src/utils/useDidMountEffect.ts index 0937e9b0c..1310ca5e0 100644 --- a/src/utils/useDidMountEffect.ts +++ b/src/utils/useDidMountEffect.ts @@ -1,6 +1,6 @@ import { useEffect, useState } from 'react'; -const useDidMountEffect = (func: Function, deps: Array): void => { +const useDidMountEffect = (func: () => void, deps: Array): void => { const [didMount, setDidmount] = useState(false); useEffect(() => { if (didMount) { From 9eaf65725f0fb86fc9dd1679d552e62bee6a4448 Mon Sep 17 00:00:00 2001 From: EunSeo-Baek Date: Fri, 10 Mar 2023 09:57:16 +0900 Subject: [PATCH 14/14] Apply review --- .../Channel/components/MessageList/index.tsx | 7 +++---- .../Channel/components/UnreadCount/index.tsx | 2 +- .../Channel/components/UnreadCount/unread-count.scss | 4 ++-- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/smart-components/Channel/components/MessageList/index.tsx b/src/smart-components/Channel/components/MessageList/index.tsx index 27e3f1873..6937f9666 100644 --- a/src/smart-components/Channel/components/MessageList/index.tsx +++ b/src/smart-components/Channel/components/MessageList/index.tsx @@ -132,10 +132,9 @@ const MessageList: React.FC = ({ }; if (loading) { - if (renderPlaceholderLoader && typeof renderPlaceholderLoader === 'function') { - return renderPlaceholderLoader(); - } - return ; + return (typeof renderPlaceholderLoader === 'function') + ? renderPlaceholderLoader() + : ; } if (allMessages.length < 1) { if (renderPlaceholderEmpty && typeof renderPlaceholderEmpty === 'function') { diff --git a/src/smart-components/Channel/components/UnreadCount/index.tsx b/src/smart-components/Channel/components/UnreadCount/index.tsx index 6844a61de..8d78ead2d 100644 --- a/src/smart-components/Channel/components/UnreadCount/index.tsx +++ b/src/smart-components/Channel/components/UnreadCount/index.tsx @@ -24,7 +24,7 @@ const UnreadCount: React.FC = ({ return (