Skip to content

Commit 17338a5

Browse files
committed
fix: no more jumps on "show more" near the edge of container
1 parent 063ebc4 commit 17338a5

File tree

3 files changed

+116
-70
lines changed

3 files changed

+116
-70
lines changed

ts/components/conversation/SessionMessagesListContainer.tsx

Lines changed: 38 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import { SessionMessagesList } from './SessionMessagesList';
2929
import { TypingBubble } from './TypingBubble';
3030
import { StyledMessageBubble } from './message/message-content/MessageBubble';
3131
import { StyledMentionAnother } from './AddMentions';
32+
import { MessagesContainerRefContext } from '../../contexts/MessagesContainerRefContext';
3233

3334
export type SessionMessageListProps = {
3435
messageContainerRef: RefObject<HTMLDivElement>;
@@ -121,42 +122,44 @@ class SessionMessagesListContainerInner extends Component<Props> {
121122
}
122123

123124
return (
124-
<StyledMessagesContainer
125-
className="messages-container"
126-
id={messageContainerDomID}
127-
onScroll={this.handleScroll}
128-
ref={this.props.messageContainerRef}
129-
data-testid="messages-container"
130-
>
131-
<StyledTypingBubbleContainer>
132-
<TypingBubble
133-
conversationType={conversation.type}
134-
isTyping={!!conversation.isTyping}
135-
key="typing-bubble"
125+
<MessagesContainerRefContext.Provider value={this.props.messageContainerRef}>
126+
<StyledMessagesContainer
127+
className="messages-container"
128+
id={messageContainerDomID}
129+
onScroll={this.handleScroll}
130+
ref={this.props.messageContainerRef}
131+
data-testid="messages-container"
132+
>
133+
<StyledTypingBubbleContainer>
134+
<TypingBubble
135+
conversationType={conversation.type}
136+
isTyping={!!conversation.isTyping}
137+
key="typing-bubble"
138+
/>
139+
</StyledTypingBubbleContainer>
140+
141+
<ScrollToLoadedMessageContext.Provider value={this.scrollToLoadedMessage}>
142+
<SessionMessagesList
143+
scrollAfterLoadMore={(
144+
messageIdToScrollTo: string,
145+
type: 'load-more-top' | 'load-more-bottom'
146+
) => {
147+
this.scrollToMessage(messageIdToScrollTo, type);
148+
}}
149+
onPageDownPressed={this.scrollPgDown}
150+
onPageUpPressed={this.scrollPgUp}
151+
onHomePressed={this.scrollTop}
152+
onEndPressed={this.scrollEnd}
153+
/>
154+
</ScrollToLoadedMessageContext.Provider>
155+
156+
<SessionScrollButton
157+
// eslint-disable-next-line @typescript-eslint/no-misused-promises
158+
onClickScrollBottom={this.props.scrollToNow}
159+
key="scroll-down-button"
136160
/>
137-
</StyledTypingBubbleContainer>
138-
139-
<ScrollToLoadedMessageContext.Provider value={this.scrollToLoadedMessage}>
140-
<SessionMessagesList
141-
scrollAfterLoadMore={(
142-
messageIdToScrollTo: string,
143-
type: 'load-more-top' | 'load-more-bottom'
144-
) => {
145-
this.scrollToMessage(messageIdToScrollTo, type);
146-
}}
147-
onPageDownPressed={this.scrollPgDown}
148-
onPageUpPressed={this.scrollPgUp}
149-
onHomePressed={this.scrollTop}
150-
onEndPressed={this.scrollEnd}
151-
/>
152-
</ScrollToLoadedMessageContext.Provider>
153-
154-
<SessionScrollButton
155-
// eslint-disable-next-line @typescript-eslint/no-misused-promises
156-
onClickScrollBottom={this.props.scrollToNow}
157-
key="scroll-down-button"
158-
/>
159-
</StyledMessagesContainer>
161+
</StyledMessagesContainer>
162+
</MessagesContainerRefContext.Provider>
160163
);
161164
}
162165

ts/components/conversation/message/message-content/MessageBubble.tsx

Lines changed: 69 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { useState, useRef, type ReactNode, useLayoutEffect } from 'react';
22
import styled, { css } from 'styled-components';
33
import { Constants } from '../../../../session';
44
import { tr } from '../../../../localization/localeTools';
5+
import { useMessagesContainerRef } from '../../../../contexts/MessagesContainerRefContext';
56

67
export const StyledMessageBubble = styled.div<{ expanded: boolean }>`
78
position: relative;
@@ -41,66 +42,99 @@ export function MessageBubble({ children }: { children: ReactNode }) {
4142
const [expanded, setExpanded] = useState(false);
4243
const [showReadMore, setShowReadMore] = useState(false);
4344
const hiddenHeight = useRef<number>(0);
44-
const containerRef = useRef<HTMLDivElement>(null);
45+
const msgBubbleRef = useRef<HTMLDivElement>(null);
46+
47+
const messagesContainerRef = useMessagesContainerRef();
48+
49+
const scrollBefore = useRef<{ scrollTop: number; scrollHeight: number }>({
50+
scrollTop: 0,
51+
scrollHeight: 0,
52+
});
4553

4654
useLayoutEffect(() => {
4755
if (expanded) {
48-
// TODO: find the perfect magic number, 1 is almost perfect
49-
// 21 is the ReadMore height, 10 is its vertical padding and 1 is from testing
50-
const scrollDownBy = hiddenHeight.current - 21 - 10 + 1;
56+
const msgContainerAfter = messagesContainerRef.current;
57+
if (!msgBubbleRef.current || !msgContainerAfter) {
58+
return;
59+
}
60+
const { scrollTop: scrollTopAfter, scrollHeight: scrollHeightAfter } = msgContainerAfter;
61+
62+
const { scrollTop: scrollTopBefore, scrollHeight: scrollHeightBefore } = scrollBefore.current;
5163

52-
document.getElementById('messages-container')?.scrollBy({
53-
top: -scrollDownBy,
64+
const topDidChange = scrollTopAfter !== scrollTopBefore;
65+
const heightDiff = scrollHeightAfter - scrollHeightBefore;
66+
const scrollTo = topDidChange ? scrollTopBefore - heightDiff : scrollTopAfter - heightDiff;
67+
68+
msgContainerAfter.scrollTo({
69+
top: scrollTo,
5470
behavior: 'instant',
5571
});
5672
}
57-
}, [expanded]);
73+
}, [expanded, messagesContainerRef]);
5874

59-
useLayoutEffect(() => {
60-
const container = containerRef.current;
61-
if (!container) {
75+
const onShowMore = () => {
76+
const el = msgBubbleRef.current;
77+
if (!el) {
6278
return;
6379
}
6480

65-
const el = container.firstElementChild;
66-
if (!el) {
81+
const msgContainerBefore = messagesContainerRef.current;
82+
83+
if (!msgContainerBefore) {
6784
return;
6885
}
6986

70-
// We need the body's child to find the line height as long as it exists
71-
const textEl = el.firstElementChild ?? el;
72-
const textStyle = window.getComputedStyle(textEl);
73-
const style = window.getComputedStyle(el);
87+
const { scrollTop: scrollTopBefore, scrollHeight: scrollHeightBefore } = msgContainerBefore;
88+
89+
scrollBefore.current = { scrollTop: scrollTopBefore, scrollHeight: scrollHeightBefore };
90+
91+
// we cannot "show less", only show more
92+
setExpanded(true);
93+
};
94+
95+
useLayoutEffect(
96+
() => {
97+
const el = msgBubbleRef?.current?.firstElementChild;
98+
if (!el) {
99+
return;
100+
}
101+
102+
// We need the body's child to find the line height as long as it exists
103+
const textEl = el.firstElementChild ?? el;
104+
const textStyle = window.getComputedStyle(textEl);
105+
const style = window.getComputedStyle(el);
74106

75-
const lineHeight = parseFloat(textStyle.lineHeight);
76-
const paddingTop = parseFloat(style.paddingTop);
77-
const paddingBottom = parseFloat(style.paddingBottom);
78-
const borderTopWidth = parseFloat(style.borderTopWidth);
79-
const borderBottomWidth = parseFloat(style.borderBottomWidth);
107+
const lineHeight = parseFloat(textStyle.lineHeight);
108+
const paddingTop = parseFloat(style.paddingTop);
109+
const paddingBottom = parseFloat(style.paddingBottom);
110+
const borderTopWidth = parseFloat(style.borderTopWidth);
111+
const borderBottomWidth = parseFloat(style.borderBottomWidth);
80112

81-
// We need to allow for a 1 pixel buffer in maxHeight
82-
const maxHeight =
83-
lineHeight * Constants.CONVERSATION.MAX_MESSAGE_MAX_LINES_BEFORE_READ_MORE + 1;
113+
// We need to allow for a 1 pixel buffer in maxHeight
114+
const maxHeight =
115+
lineHeight * Constants.CONVERSATION.MAX_MESSAGE_MAX_LINES_BEFORE_READ_MORE + 1;
84116

85-
const innerHeight =
86-
el.scrollHeight - (paddingTop + paddingBottom + borderTopWidth + borderBottomWidth);
117+
const innerHeight =
118+
el.scrollHeight - (paddingTop + paddingBottom + borderTopWidth + borderBottomWidth);
87119

88-
const overflowsLines = innerHeight > maxHeight;
120+
const overflowsLines = innerHeight > maxHeight;
89121

90-
hiddenHeight.current = innerHeight - maxHeight;
91-
setShowReadMore(overflowsLines);
92-
// eslint-disable-next-line react-hooks/exhaustive-deps -- children changing will change el.lineHeight and el.ScrollHeight
93-
}, [children]);
122+
hiddenHeight.current = innerHeight - maxHeight;
123+
124+
setShowReadMore(overflowsLines);
125+
},
126+
// Note: no need to provide a dependency here (and if we provide children, this hook reruns every second for every messages).
127+
// The only dependency is msgBubbleRef, but as it's a ref it's unneeded
128+
[]
129+
);
94130

95131
return (
96132
<>
97-
<StyledMessageBubble ref={containerRef} expanded={expanded}>
133+
<StyledMessageBubble ref={msgBubbleRef} expanded={expanded}>
98134
{children}
99135
</StyledMessageBubble>
100136
{showReadMore && !expanded ? (
101-
<ReadMoreButton onClick={() => setExpanded(prev => !prev)}>
102-
{tr('messageBubbleReadMore')}
103-
</ReadMoreButton>
137+
<ReadMoreButton onClick={onShowMore}>{tr('messageBubbleReadMore')}</ReadMoreButton>
104138
) : null}
105139
</>
106140
);
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { createContext, RefObject, useContext } from 'react';
2+
3+
export const MessagesContainerRefContext = createContext<RefObject<HTMLDivElement>>({
4+
current: null,
5+
});
6+
7+
export const useMessagesContainerRef = () => {
8+
return useContext(MessagesContainerRefContext);
9+
};

0 commit comments

Comments
 (0)