Skip to content

Commit 4e680d1

Browse files
authored
fix: scroll issues (#895)
- fix: maintain scroll position when loading previous messages - fix: maintain scroll position when loading next messages - fix: move the logic that delay for rendering mmf to the correct location - fix: scroll position was not adjusting correctly when the message content size was updated. (Issue caused by debouncing scroll events, leading to delayed updates of related events and values.) - fix: use animatedMessage instead of highlightedMessage in smart app component ticket: [CLNP-1735], [CLNP-1773] [CLNP-1735]: https://sendbird.atlassian.net/browse/CLNP-1735?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ [CLNP-1773]: https://sendbird.atlassian.net/browse/CLNP-1773?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
1 parent ce9d64e commit 4e680d1

File tree

16 files changed

+128
-101
lines changed

16 files changed

+128
-101
lines changed
Lines changed: 31 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
import React, { useCallback } from 'react';
1+
import React from 'react';
22
import { SCROLL_BUFFER } from '../../utils/consts';
3-
import { useDebounce } from '../useDebounce';
3+
import { useThrottleCallback } from '../useThrottleCallback';
4+
import { isAboutSame } from '../../modules/Channel/context/utils';
5+
import { usePreservedCallback } from '@sendbird/uikit-tools';
46

5-
const DELAY = 500;
7+
const DELAY = 100;
68

79
export interface UseHandleOnScrollCallbackProps {
810
hasMore: boolean;
@@ -13,7 +15,10 @@ export interface UseHandleOnScrollCallbackProps {
1315
setIsScrolled?: React.Dispatch<React.SetStateAction<boolean>>;
1416
}
1517

16-
export function calcScrollBottom(scrollHeight: number, scrollTop: number): number {
18+
export function calcScrollBottom(
19+
scrollHeight: number,
20+
scrollTop: number,
21+
): number {
1722
return scrollHeight - scrollTop;
1823
}
1924

@@ -24,17 +29,11 @@ export function useHandleOnScrollCallback({
2429
scrollRef,
2530
setShowScrollDownButton,
2631
}: UseHandleOnScrollCallbackProps): () => void {
27-
const scrollCb = useCallback(() => {
28-
const element = scrollRef?.current;
29-
if (element == null) {
30-
return;
31-
}
3232

33-
const {
34-
scrollTop,
35-
scrollHeight,
36-
clientHeight,
37-
} = element;
33+
const scrollCb = usePreservedCallback(() => {
34+
const element = scrollRef?.current;
35+
if (element == null) return;
36+
const { scrollTop, scrollHeight, clientHeight } = element;
3837
// https://sendbird.atlassian.net/browse/SBISSUE-11759
3938
// the edge case where channel is inside a page that already has scroll
4039
// scrollintoView will move the whole page, which we dont want
@@ -44,24 +43,29 @@ export function useHandleOnScrollCallback({
4443
if (typeof setShowScrollDownButton === 'function') {
4544
setShowScrollDownButton(scrollHeight > scrollTop + clientHeight + 1);
4645
}
47-
if (hasMore && scrollTop < SCROLL_BUFFER) {
46+
47+
// Load previous messages
48+
// 1. check if hasMore(hasPrevious) and reached to top
49+
// 2. load previous messages (onScroll)
50+
// 3. maintain scroll position (sets the scroll position to the bottom of the new messages)
51+
if (hasMore && isAboutSame(scrollTop, 0, SCROLL_BUFFER)) {
4852
onScroll(() => {
49-
// sets the scroll position to the bottom of the new messages
50-
element.scrollTop = element.scrollHeight - scrollBottom;
53+
const messagesAreAddedToView = element.scrollHeight > scrollHeight;
54+
if (messagesAreAddedToView) element.scrollTop = element.scrollHeight - scrollBottom;
5155
});
5256
}
53-
if (hasNext) {
57+
58+
// Load next messages
59+
// 1. check if hasNext and reached to bottom
60+
// 2. load next messages (onScroll)
61+
// 3. maintain scroll position (sets the scroll position to the top of the new messages)
62+
if (hasNext && isAboutSame(clientHeight + scrollTop, scrollHeight, SCROLL_BUFFER)) {
5463
onScroll(() => {
55-
// sets the scroll position to the top of the new messages
56-
element.scrollTop = scrollTop - (scrollHeight - element.scrollHeight);
64+
const messagesAreAddedToView = element.scrollHeight > scrollHeight;
65+
if (messagesAreAddedToView) element.scrollTop = scrollTop;
5766
});
5867
}
59-
}, [
60-
setShowScrollDownButton,
61-
hasMore,
62-
onScroll,
63-
scrollRef,
64-
]);
68+
});
6569

66-
return useDebounce(scrollCb, DELAY);
70+
return useThrottleCallback(scrollCb, DELAY, { trailing: true });
6771
}

src/hooks/useOnScrollReachedEndDetector/index.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import React from 'react';
22
import { SCROLL_BUFFER } from '../../utils/consts';
33
import { isAboutSame } from '../../modules/Channel/context/utils';
4-
import { useDebounce } from '../useDebounce';
54
import { usePreservedCallback } from '@sendbird/uikit-tools';
5+
import { useThrottleCallback } from '../useThrottleCallback';
66

7-
const BUFFER_DELAY = 500;
7+
const BUFFER_DELAY = 100;
88

99
export interface UseOnScrollReachedEndDetectorProps {
1010
onReachedTop?: () => void;
@@ -38,5 +38,5 @@ export function useOnScrollPositionChangeDetector(
3838
}
3939
});
4040

41-
return useDebounce(cb, BUFFER_DELAY);
41+
return useThrottleCallback(cb, BUFFER_DELAY, { trailing: true });
4242
}

src/hooks/useThrottleCallback.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { useEffect, useRef } from 'react';
2+
import { usePreservedCallback } from '@sendbird/uikit-tools';
3+
4+
export function useThrottleCallback<T extends(...args: any[]) => void>(
5+
callback: T,
6+
delay: number,
7+
options: { leading?: boolean; trailing?: boolean } = {
8+
leading: true,
9+
trailing: false,
10+
},
11+
) {
12+
const timer = useRef(null);
13+
const trailingArgs = useRef(null);
14+
15+
useEffect(() => {
16+
return () => {
17+
if (timer.current) clearTimeout(timer.current);
18+
};
19+
}, []);
20+
21+
return usePreservedCallback((...args: any[]) => {
22+
if (timer.current) {
23+
trailingArgs.current = args;
24+
return;
25+
}
26+
27+
if (options.leading) {
28+
callback(...args);
29+
} else {
30+
trailingArgs.current = args;
31+
}
32+
33+
const invoke = () => {
34+
if (options.trailing && trailingArgs.current) {
35+
callback(...trailingArgs.current);
36+
trailingArgs.current = null;
37+
timer.current = setTimeout(invoke, delay);
38+
} else {
39+
timer.current = null;
40+
}
41+
};
42+
43+
timer.current = setTimeout(invoke, delay);
44+
});
45+
}

src/modules/App/DesktopLayout.tsx

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useState } from 'react';
1+
import React from 'react';
22

33
import type { DesktopLayoutProps } from './types';
44

@@ -35,7 +35,6 @@ export const DesktopLayout: React.FC<DesktopLayoutProps> = (
3535
threadTargetMessage,
3636
setThreadTargetMessage,
3737
} = props;
38-
const [animatedMessageId, setAnimatedMessageId] = useState<number | null>(null);
3938
return (
4039
<div className="sendbird-app__wrap">
4140
<div className="sendbird-app__channellist-wrap">
@@ -91,15 +90,14 @@ export const DesktopLayout: React.FC<DesktopLayoutProps> = (
9190
}
9291
}}
9392
onMessageAnimated={() => {
94-
setAnimatedMessageId(null);
93+
setHighlightedMessage(null);
9594
}}
9695
onMessageHighlighted={() => {
9796
setHighlightedMessage?.(null);
9897
}}
9998
showSearchIcon={showSearchIcon}
10099
startingPoint={startingPoint}
101-
animatedMessage={animatedMessageId}
102-
highlightedMessage={highlightedMessage}
100+
animatedMessage={highlightedMessage}
103101
isReactionEnabled={isReactionEnabled}
104102
replyType={replyType}
105103
isMessageGroupingEnabled={isMessageGroupingEnabled}
@@ -150,11 +148,11 @@ export const DesktopLayout: React.FC<DesktopLayoutProps> = (
150148
if (channel?.url !== currentChannel?.url) {
151149
setCurrentChannel(channel);
152150
}
153-
if (message?.messageId !== animatedMessageId) {
151+
if (message?.messageId !== highlightedMessage) {
154152
setStartingPoint?.(message?.createdAt);
155153
}
156154
setTimeout(() => {
157-
setAnimatedMessageId(message?.messageId);
155+
setHighlightedMessage(message?.messageId);
158156
}, 500);
159157
}}
160158
/>

src/modules/App/MobileLayout.tsx

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,13 @@ export const MobileLayout: React.FC<MobileLayoutProps> = (
3636
onProfileEditSuccess,
3737
currentChannel,
3838
setCurrentChannel,
39-
highlightedMessage,
40-
setHighlightedMessage,
4139
startingPoint,
4240
setStartingPoint,
4341
threadTargetMessage,
4442
setThreadTargetMessage,
43+
highlightedMessage, setHighlightedMessage,
4544
} = props;
4645
const [panel, setPanel] = useState(PANELS.CHANNEL_LIST);
47-
const [animatedMessageId, setAnimatedMessageId] = useState<number | null>(null);
4846

4947
const store = useSendbirdStateContext();
5048
const sdk = store?.stores?.sdkStore?.sdk;
@@ -61,7 +59,7 @@ export const MobileLayout: React.FC<MobileLayoutProps> = (
6159

6260
useEffect(() => {
6361
if (panel !== PANELS.CHANNEL) {
64-
goToMessage(null, () => setAnimatedMessageId(null));
62+
goToMessage(null, () => setHighlightedMessage(null));
6563
}
6664
}, [panel]);
6765

@@ -136,8 +134,7 @@ export const MobileLayout: React.FC<MobileLayoutProps> = (
136134
isMessageGroupingEnabled={isMessageGroupingEnabled}
137135
isMultipleFilesMessageEnabled={isMultipleFilesMessageEnabled}
138136
startingPoint={startingPoint}
139-
animatedMessage={animatedMessageId}
140-
highlightedMessage={highlightedMessage}
137+
animatedMessage={highlightedMessage}
141138
onChatHeaderActionClick={() => {
142139
setPanel(PANELS.CHANNEL_SETTINGS);
143140
}}
@@ -204,7 +201,7 @@ export const MobileLayout: React.FC<MobileLayoutProps> = (
204201
setCurrentChannel(channel);
205202
goToMessage(message, (messageId) => {
206203
setPanel(PANELS.CHANNEL);
207-
setAnimatedMessageId(messageId);
204+
setHighlightedMessage(messageId);
208205
});
209206
}}
210207
/>

src/modules/App/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export interface AppLayoutProps {
2525

2626
interface SubLayoutCommonProps {
2727
highlightedMessage?: number | null;
28-
setHighlightedMessage: React.Dispatch<number | null>;
28+
setHighlightedMessage?: React.Dispatch<number | null>;
2929
startingPoint?: number | null;
3030
setStartingPoint: React.Dispatch<number | null>;
3131
threadTargetMessage: SendableMessageType | null;

src/modules/Channel/components/Message/index.tsx

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ import RemoveMessageModal from '../RemoveMessageModal';
2828
import { MessageInputKeys } from '../../../../ui/MessageInput/const';
2929
import { EveryMessage, RenderCustomSeparatorProps, RenderMessageProps } from '../../../../types';
3030
import { useLocalization } from '../../../../lib/LocalizationContext';
31-
import { useHandleOnScrollCallback } from '../../../../hooks/useHandleOnScrollCallback';
3231
import { useDirtyGetMentions } from '../../../Message/hooks/useDirtyGetMentions';
3332
import SuggestedReplies from '../SuggestedReplies';
3433

@@ -93,8 +92,6 @@ const Message = ({
9392
onQuoteMessageClick,
9493
onMessageAnimated,
9594
onMessageHighlighted,
96-
onScrollCallback,
97-
setIsScrolled,
9895
sendMessage,
9996
localMessages,
10097
} = useChannelContext();
@@ -121,13 +118,6 @@ const Message = ({
121118
|| isDisabledBecauseMuted(currentGroupChannel)
122119
|| !isOnline;
123120

124-
const handleOnScroll = useHandleOnScrollCallback({
125-
hasMore: false,
126-
onScroll: onScrollCallback,
127-
scrollRef: messageScrollRef,
128-
setIsScrolled,
129-
});
130-
131121
const mentionNodes = useDirtyGetMentions({ ref: editMessageInputRef }, { logger });
132122
const ableMention = mentionNodes?.length < maxUserMentionCount;
133123

@@ -162,7 +152,7 @@ const Message = ({
162152
let animationTimeout = null;
163153
let messageHighlightedTimeout = null;
164154
if (highLightedMessageId === message.messageId && messageScrollRef?.current) {
165-
handleOnScroll();
155+
messageScrollRef.current.scrollIntoView({ block: 'center', inline: 'center' });
166156
setIsAnimated(false);
167157
animationTimeout = setTimeout(() => {
168158
setIsHighlighted(true);
@@ -184,7 +174,7 @@ const Message = ({
184174
let animationTimeout = null;
185175
let messageAnimatedTimeout = null;
186176
if (animatedMessageId === message.messageId && messageScrollRef?.current) {
187-
handleOnScroll();
177+
messageScrollRef.current.scrollIntoView({ block: 'center', inline: 'center' });
188178
setIsHighlighted(false);
189179
animationTimeout = setTimeout(() => {
190180
setIsAnimated(true);

src/modules/Channel/components/MessageList/hooks/__test__/useSetScrollToBottom.spec.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import { render, fireEvent, renderHook } from '@testing-library/react';
33

44
import { useSetScrollToBottom } from '../useSetScrollToBottom';
55

6-
jest.mock('../../../../../../hooks/useDebounce', () => ({
7-
useDebounce: (callback: () => void) => callback,
6+
jest.mock('../../../../../../hooks/useThrottleCallback', () => ({
7+
useThrottleCallback: (callback: () => void) => callback,
88
}));
99

1010
const ScrollComponent = () => {
Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,12 @@
11
import React, { useEffect, useState } from 'react';
2-
import { useDebounce } from '../../../../../hooks/useDebounce';
2+
import { useThrottleCallback } from '../../../../../hooks/useThrottleCallback';
33

4-
const DELAY = 500;
4+
const DELAY = 100;
55

6-
export function useSetScrollToBottom({
7-
loading,
8-
}: {
9-
loading: boolean;
10-
}): ({
6+
export function useSetScrollToBottom({ loading }: { loading: boolean }): {
117
scrollBottom: number;
128
scrollToBottomHandler: (e: React.UIEvent<HTMLDivElement, UIEvent>) => void;
13-
}) {
9+
} {
1410
const [scrollBottom, setScrollBottom] = useState(0);
1511
useEffect(() => {
1612
if (loading) {
@@ -20,13 +16,15 @@ export function useSetScrollToBottom({
2016
const scrollCb = (e: React.UIEvent<HTMLDivElement, UIEvent>) => {
2117
const element = e.target as HTMLDivElement;
2218
try {
23-
setScrollBottom(element.scrollHeight - element.scrollTop - element.offsetHeight);
19+
setScrollBottom(
20+
element.scrollHeight - element.scrollTop - element.offsetHeight,
21+
);
2422
} catch {
2523
//
2624
}
2725
};
2826
return {
2927
scrollBottom,
30-
scrollToBottomHandler: useDebounce(scrollCb, DELAY),
28+
scrollToBottomHandler: useThrottleCallback(scrollCb, DELAY, { trailing: true }),
3129
};
3230
}

0 commit comments

Comments
 (0)