Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/testing/src/pages/PlaygroundPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ export function PlaygroundPage() {
replyType: 'thread',
}
}}
// autoscrollMessageOverflowToTop={true}
/>;
}
4 changes: 4 additions & 0 deletions src/lib/Sendbird/context/SendbirdProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ const SendbirdContextManager = ({
sdkInitParams,
customExtensionParams,
isMultipleFilesMessageEnabled = false,
autoscrollMessageOverflowToTop = false,
eventHandlers,
htmlTextDirection = 'ltr',
forceLeftToRightMessageLayout = false,
Expand Down Expand Up @@ -288,6 +289,7 @@ const SendbirdContextManager = ({
setCurrentTheme,
setCurrenttheme: setCurrentTheme, // deprecated: typo
isMultipleFilesMessageEnabled,
autoscrollMessageOverflowToTop,
uikitMultipleFilesMessageLimit,
logger,
pubSub,
Expand Down Expand Up @@ -316,6 +318,7 @@ const SendbirdContextManager = ({
currentTheme,
setCurrentTheme,
isMultipleFilesMessageEnabled,
autoscrollMessageOverflowToTop,
uikitMultipleFilesMessageLimit,
logger,
pubSub,
Expand Down Expand Up @@ -395,6 +398,7 @@ const InternalSendbirdProvider = (props: SendbirdProviderProps & { logger: Logge
},
disableMarkAsDelivered: props?.disableMarkAsDelivered,
isMultipleFilesMessageEnabled: props?.isMultipleFilesMessageEnabled,
autoscrollMessageOverflowToTop: props?.autoscrollMessageOverflowToTop,
},
eventHandlers: props?.eventHandlers,
});
Expand Down
1 change: 1 addition & 0 deletions src/lib/Sendbird/context/initialState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const config: SendbirdStateConfig = {
forceLeftToRightMessageLayout: false,
disableMarkAsDelivered: false,
isMultipleFilesMessageEnabled: false,
autoscrollMessageOverflowToTop: false,
htmlTextDirection: 'ltr',
uikitUploadSizeLimit: DEFAULT_UPLOAD_SIZE_LIMIT,
uikitMultipleFilesMessageLimit: DEFAULT_MULTIPLE_FILES_MESSAGE_LIMIT,
Expand Down
2 changes: 2 additions & 0 deletions src/lib/Sendbird/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ export interface SendbirdProviderProps extends CommonUIKitConfigProps, React.Pro
sdkInitParams?: SendbirdChatInitParams;
customExtensionParams?: CustomExtensionParams;
isMultipleFilesMessageEnabled?: boolean;
autoscrollMessageOverflowToTop?: boolean;
// UserProfile
renderUserProfile?: (props: RenderUserProfileProps) => React.ReactElement;
onStartDirectMessage?: (channel: GroupChannel) => void;
Expand Down Expand Up @@ -260,6 +261,7 @@ export interface SendbirdStateConfig {
markAsDeliveredScheduler: MarkAsDeliveredSchedulerType;
disableMarkAsDelivered: boolean;
isMultipleFilesMessageEnabled: boolean;
autoscrollMessageOverflowToTop: boolean;
// Remote configs set from dashboard by UIKit feature configuration
common: {
enableUsingDefaultUserProfile: SBUConfig['common']['enableUsingDefaultUserProfile'];
Expand Down
3 changes: 3 additions & 0 deletions src/modules/App/AppLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import useSendbird from '../../lib/Sendbird/context/hooks/useSendbird';
export const AppLayout = (props: AppLayoutProps) => {
const {
isMessageGroupingEnabled,
autoscrollMessageOverflowToTop,
allowProfileEdit,
onProfileEditSuccess,
disableAutoSelect,
Expand Down Expand Up @@ -50,6 +51,7 @@ export const AppLayout = (props: AppLayoutProps) => {
showSearchIcon={showSearchIcon}
isReactionEnabled={isReactionEnabled}
isMessageGroupingEnabled={isMessageGroupingEnabled}
autoscrollMessageOverflowToTop={autoscrollMessageOverflowToTop}
allowProfileEdit={allowProfileEdit}
onProfileEditSuccess={onProfileEditSuccess}
currentChannel={currentChannel}
Expand All @@ -69,6 +71,7 @@ export const AppLayout = (props: AppLayoutProps) => {
isReactionEnabled={isReactionEnabled}
showSearchIcon={showSearchIcon}
isMessageGroupingEnabled={isMessageGroupingEnabled}
autoscrollMessageOverflowToTop={autoscrollMessageOverflowToTop}
allowProfileEdit={allowProfileEdit}
onProfileEditSuccess={onProfileEditSuccess}
disableAutoSelect={disableAutoSelect}
Expand Down
2 changes: 2 additions & 0 deletions src/modules/App/DesktopLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export const DesktopLayout: React.FC<DesktopLayoutProps> = (props: DesktopLayout
replyType,
isMessageGroupingEnabled,
isMultipleFilesMessageEnabled,
autoscrollMessageOverflowToTop,
allowProfileEdit,
showSearchIcon,
onProfileEditSuccess,
Expand Down Expand Up @@ -104,6 +105,7 @@ export const DesktopLayout: React.FC<DesktopLayoutProps> = (props: DesktopLayout
replyType: replyType,
isMessageGroupingEnabled: isMessageGroupingEnabled,
isMultipleFilesMessageEnabled: isMultipleFilesMessageEnabled,
autoscrollMessageOverflowToTop: autoscrollMessageOverflowToTop,
// for GroupChannel
animatedMessageId: highlightedMessage,
onReplyInThreadClick: onClickThreadReply,
Expand Down
2 changes: 2 additions & 0 deletions src/modules/App/MobileLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export const MobileLayout: React.FC<MobileLayoutProps> = (props: MobileLayoutPro
replyType,
isMessageGroupingEnabled,
isMultipleFilesMessageEnabled,
autoscrollMessageOverflowToTop,
allowProfileEdit,
isReactionEnabled,
showSearchIcon,
Expand Down Expand Up @@ -155,6 +156,7 @@ export const MobileLayout: React.FC<MobileLayoutProps> = (props: MobileLayoutPro
replyType,
isMessageGroupingEnabled,
isMultipleFilesMessageEnabled,
autoscrollMessageOverflowToTop,
// for GroupChannel
animatedMessageId: highlightedMessage,
onReplyInThreadClick: ({ message }) => {
Expand Down
4 changes: 4 additions & 0 deletions src/modules/App/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export interface AppProps {
config?: SendbirdProviderProps['config'];
voiceRecord?: SendbirdProviderProps['voiceRecord'];
isMultipleFilesMessageEnabled?: SendbirdProviderProps['isMultipleFilesMessageEnabled'];
autoscrollMessageOverflowToTop?: SendbirdProviderProps['autoscrollMessageOverflowToTop'];
colorSet?: SendbirdProviderProps['colorSet'];
stringSet?: SendbirdProviderProps['stringSet'];
allowProfileEdit?: SendbirdProviderProps['allowProfileEdit'];
Expand Down Expand Up @@ -99,6 +100,7 @@ export default function App(props: AppProps) {
customExtensionParams,
eventHandlers,
isMultipleFilesMessageEnabled,
autoscrollMessageOverflowToTop = false,
isUserIdUsedForNickname = true,
enableLegacyChannelModules = false,
uikitOptions,
Expand Down Expand Up @@ -138,6 +140,7 @@ export default function App(props: AppProps) {
renderUserProfile={renderUserProfile}
imageCompression={imageCompression}
isMultipleFilesMessageEnabled={isMultipleFilesMessageEnabled}
autoscrollMessageOverflowToTop={autoscrollMessageOverflowToTop}
voiceRecord={voiceRecord}
onStartDirectMessage={(channel) => {
setCurrentChannel(channel);
Expand All @@ -159,6 +162,7 @@ export default function App(props: AppProps) {
forceLeftToRightMessageLayout={forceLeftToRightMessageLayout}
>
<AppLayout
autoscrollMessageOverflowToTop={autoscrollMessageOverflowToTop}
isMessageGroupingEnabled={isMessageGroupingEnabled}
allowProfileEdit={allowProfileEdit}
onProfileEditSuccess={onProfileEditSuccess}
Expand Down
1 change: 1 addition & 0 deletions src/modules/App/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export interface AppLayoutProps {
forceLeftToRightMessageLayout?: boolean;
isMessageGroupingEnabled?: boolean;
isMultipleFilesMessageEnabled?: boolean;
autoscrollMessageOverflowToTop?: boolean;
allowProfileEdit?: boolean;
showSearchIcon?: boolean;
onProfileEditSuccess?(user: User): void; // TODO: Unused props, deprecate it
Expand Down
29 changes: 29 additions & 0 deletions src/modules/GroupChannel/components/Message/MessageView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ export interface MessageProps {
* A function that is called when the new message separator visibility changes.
*/
onNewMessageSeparatorVisibilityChange?: (isVisible: boolean) => void;
/**
* A function that forces scroll to message when new message is received.
*/
scrollMessageOverflowToTop?: (ref: React.MutableRefObject<any>, message: CoreMessageType) => void;
/**
* @deprecated Please use `children` instead
* @description Customizes all child components of the message.
Expand Down Expand Up @@ -97,6 +101,10 @@ export interface MessageViewProps extends MessageProps {

animatedMessageId: number | null;
setAnimatedMessageId: React.Dispatch<React.SetStateAction<number | null>>;

newMessageIds?: number[] | null;
setNewMessageIds?: (ids: number[]) => void;

onMessageAnimated?: () => void;
/** @deprecated * */
highLightedMessageId?: number | null;
Expand All @@ -119,6 +127,7 @@ const MessageView = (props: MessageViewProps) => {
chainBottom,
handleScroll,
onNewMessageSeparatorVisibilityChange,
scrollMessageOverflowToTop,

// MessageViewProps
channel,
Expand Down Expand Up @@ -147,6 +156,9 @@ const MessageView = (props: MessageViewProps) => {
animatedMessageId,
onMessageAnimated,
usedInLegacy = true,

newMessageIds,
setNewMessageIds,
} = props;

const {
Expand Down Expand Up @@ -257,6 +269,23 @@ const MessageView = (props: MessageViewProps) => {
};
}, [animatedMessageId, messageScrollRef.current, message.messageId]);

useLayoutEffect(() => {
if (newMessageIds?.length > 0 && newMessageIds.includes(message.messageId)) {
let rafId: number | null = null;

rafId = requestAnimationFrame(() => {
scrollMessageOverflowToTop(messageScrollRef, message);
setNewMessageIds([]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

newMessageIds 가 여러개 일때 MessageView를 가지고있는 List에서 렌더링순서나 아니면 메세지 정렬방식, 또는 requestAnimationFrame 의 함수가 실제로 호출되는 순서에 따라서 race condition으로 버그 가능성이 있어 보입니다. scroll 해야할 메세지를 MessageView 에서 선택하는것보다는 상위 MessageList 같은곳에서 선택해서 처리하는게 좀더 안전한 방식일것 같은데 의견 부탁드립니다

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

일단 newMessageIds를 onMessagesReceived가 호출이 되었을때만 set을 하고 있는데, 실제로는 1개만 들어 오고 있긴 합니다.
채널 진입시에는 newMessageIds = [] 또는 undefined 일거라..

Scroll의 조건이 Message Content의 height가 현재 message list의 height보다 넘어가는지를 체크를 해야 하는데, 이럴러면 Content가 다 그려졌는지에 대한 보장이 필요해서 messageView쪽에서 다 그려진 여부만 체크를 했고, 실제 움직이는건 MessageList에서 처리를 하긴 했습니다.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

onMessagesReceived 가 1개의 메세지를 보장하지않고 서버에서 여러개의 메세지를 보낼수 있는 상태라 아래와 같은 케이스를 고민해볼수 있을것 같습니다.

  • 1,2 두개의 새로운 메세지가 왔을때
  • useLayoutEffect 에서 호출되는 requestAnimationFrame 이 바로 호출되는게 아니라 큐잉후 특정 시점에 한번에 호출
  • if (newMessageIds?.length > 0 && newMessageIds.includes(message.messageId)) 조건을 1,2 메세지 둘다 통과 하여 requestAnimationFrame 호출
  • requestAnimationFrame 큐잉순서에따라서 1번 2번 메세지 모두 forceScrollToMessage(messageScrollRef, message);
    setNewMessageIds([]); 호출
  • 1번 메세지의 top 에 포커싱이 되어야하지만 2번메세지의 top 에 포커싱 되는현상 발생

위 케이스에 대해서 문제없다면 괜찮을것 같습니다

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

흠 .. 이게 저희 onMessagesReceived는 message를 []로 받기는 하는데요..
이게 MESG가 내려와야 발생을 하는거라.. MESG에서는 Message Payload가 1개만 존재를 해서.... 2개 이상의 MEssage가 오지는 않을 거 같습니다.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵 이해했습니다

});

return () => {
if (rafId !== null) {
cancelAnimationFrame(rafId);
}
};
}
}, [newMessageIds]);

const renderedCustomSeparator = useMemo(() => renderCustomSeparator?.({ message }) ?? null, [message, renderCustomSeparator]);

const renderChildren = () => {
Expand Down
4 changes: 4 additions & 0 deletions src/modules/GroupChannel/components/Message/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export const Message = (props: MessageProps): React.ReactElement => {
onBeforeDownloadFileMessage,
messages,
markAsUnread,
newMessageIds,
},
actions: {
toggleReaction,
Expand All @@ -40,6 +41,7 @@ export const Message = (props: MessageProps): React.ReactElement => {
sendUserMessage,
resendMessage,
deleteMessage,
setNewMessageIds,
},
} = useGroupChannel();

Expand Down Expand Up @@ -94,6 +96,8 @@ export const Message = (props: MessageProps): React.ReactElement => {
renderRemoveMessageModal={(props) => <RemoveMessageModal {...props} />}
usedInLegacy={false}
onBeforeDownloadFileMessage={onBeforeDownloadFileMessage}
newMessageIds={newMessageIds}
setNewMessageIds={setNewMessageIds}
/>
);
};
Expand Down
21 changes: 21 additions & 0 deletions src/modules/GroupChannel/components/MessageList/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,12 +89,14 @@ export const MessageList = (props: GroupChannelMessageListProps) => {
scrollDistanceFromBottomRef,
markAsUnreadSourceRef,
readState,
autoscrollMessageOverflowToTop,
},
actions: {
scrollToBottom,
setIsScrollBottomReached,
markAsReadAll,
markAsUnread,
scrollToMessage,
},
} = useGroupChannel();

Expand Down Expand Up @@ -204,6 +206,24 @@ export const MessageList = (props: GroupChannelMessageListProps) => {
}
};

/**
* Force scroll to message
* when new message is received
* and the message content height is over the current scroll height
*/
const scrollMessageOverflowToTop = (ref: React.MutableRefObject<any>, message: CoreMessageType) => {
if (!autoscrollMessageOverflowToTop) return;
const messageComponent = ref.current;
const messageComponentHeight = messageComponent?.clientHeight;
const currentScrollHeight = scrollRef.current?.offsetHeight;

if (messageComponentHeight > currentScrollHeight) {
scrollToMessage(message.createdAt, message.messageId);
} else if (isScrollBottomReached) {
scrollToBottom();
}
};

const renderer = {
frozenNotification() {
if (!currentChannel || !currentChannel.isFrozen) return null;
Expand Down Expand Up @@ -331,6 +351,7 @@ export const MessageList = (props: GroupChannelMessageListProps) => {
renderSuggestedReplies,
renderCustomSeparator,
onNewMessageSeparatorVisibilityChange: checkDisplayedNewMessageSeparator,
scrollMessageOverflowToTop,
})}
</MessageProvider>
);
Expand Down
12 changes: 11 additions & 1 deletion src/modules/GroupChannel/context/GroupChannelProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ const initialState = () => ({
isReactionEnabled: false,
isMessageGroupingEnabled: true,
isMultipleFilesMessageEnabled: false,
autoscrollMessageOverflowToTop: false,
showSearchIcon: true,
replyType: 'NONE',
threadReplySelectType: ThreadReplySelectType.PARENT,
Expand Down Expand Up @@ -83,6 +84,7 @@ export const InternalGroupChannelProvider = (props: GroupChannelProviderProps) =
isReactionEnabled: props?.isReactionEnabled,
isMessageGroupingEnabled: props?.isMessageGroupingEnabled,
isMultipleFilesMessageEnabled: props?.isMultipleFilesMessageEnabled,
autoscrollMessageOverflowToTop: props?.autoscrollMessageOverflowToTop,
showSearchIcon: props?.showSearchIcon,
threadReplySelectType: props?.threadReplySelectType,
disableMarkAsRead: props?.disableMarkAsRead,
Expand Down Expand Up @@ -125,6 +127,7 @@ const GroupChannelManager :React.FC<React.PropsWithChildren<GroupChannelProvider
threadReplySelectType: moduleThreadReplySelectType,
isMessageGroupingEnabled = true,
isMultipleFilesMessageEnabled,
autoscrollMessageOverflowToTop,
showSearchIcon,
disableMarkAsRead = false,
scrollBehavior = 'auto',
Expand Down Expand Up @@ -224,7 +227,11 @@ const GroupChannelManager :React.FC<React.PropsWithChildren<GroupChannelProvider
// even though the next messages and the current messages length are the same.
// So added this condition to check if they are the same to prevent unnecessary calling scrollToBottom action
&& messages.length !== state.messages.length) {
setTimeout(async () => actions.scrollToBottom(true), 10);
if (!autoscrollMessageOverflowToTop) {
setTimeout(async () => actions.scrollToBottom(true), 10);
} else {
actions.setNewMessageIds(messages.map(it => it.messageId));
}
}
},
onChannelDeleted: () => {
Expand Down Expand Up @@ -271,6 +278,7 @@ const GroupChannelManager :React.FC<React.PropsWithChildren<GroupChannelProvider
}

const handleExternalMessage = (data) => {
// send message
if (data.channel.url === state.currentChannel?.url) {
actions.scrollToBottom(true);
}
Expand Down Expand Up @@ -339,6 +347,7 @@ const GroupChannelManager :React.FC<React.PropsWithChildren<GroupChannelProvider
isReactionEnabled: resolvedIsReactionEnabled,
isMessageGroupingEnabled,
isMultipleFilesMessageEnabled,
autoscrollMessageOverflowToTop: autoscrollMessageOverflowToTop ?? false,
replyType: resolvedReplyType,
threadReplySelectType: resolvedThreadReplySelectType,
showSearchIcon: showSearchIcon ?? config.groupChannelSettings.enableMessageSearch,
Expand All @@ -348,6 +357,7 @@ const GroupChannelManager :React.FC<React.PropsWithChildren<GroupChannelProvider
resolvedIsReactionEnabled,
isMessageGroupingEnabled,
isMultipleFilesMessageEnabled,
autoscrollMessageOverflowToTop,
resolvedReplyType,
resolvedThreadReplySelectType,
showSearchIcon,
Expand Down
7 changes: 7 additions & 0 deletions src/modules/GroupChannel/context/hooks/useGroupChannel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ export interface GroupChannelActions extends MessageActions {
sendMultipleFilesMessage: (params: MultipleFilesMessageCreateParams) => Promise<MultipleFilesMessage>;
updateUserMessage: (messageId: number, params: UserMessageUpdateParams) => Promise<UserMessage>;

setNewMessageIds: (ids: number[]) => void;

// UI actions
setQuoteMessage: (message: SendableMessageType | null) => void;
setAnimatedMessageId: (messageId: number | null) => void;
Expand Down Expand Up @@ -205,6 +207,10 @@ export const useGroupChannel = () => {
store.setState(state => ({ ...state, firstUnreadMessageId: messageId }));
}, []);

const setNewMessageIds = useCallback((newMessageIds: number[]) => {
store.setState(state => ({ ...state, newMessageIds }));
}, []);

const actions: GroupChannelActions = useMemo(() => {
return {
setCurrentChannel,
Expand All @@ -213,6 +219,7 @@ export const useGroupChannel = () => {
markAsUnread: state.markAsUnread,
setReadStateChanged,
setFirstUnreadMessageId,
setNewMessageIds,
setQuoteMessage,
scrollToBottom,
scrollToMessage,
Expand Down
Loading