Skip to content

Commit 2299279

Browse files
authored
Merge pull request #1505 from Bilb/fix/SES-4197/show-more-jumps
fix: no more jumps on "show more" near the edge of container
2 parents 063ebc4 + e6eb23e commit 2299279

File tree

3 files changed

+113
-72
lines changed

3 files changed

+113
-72
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: 66 additions & 37 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;
@@ -40,67 +41,95 @@ const ReadMoreButton = styled.button`
4041
export function MessageBubble({ children }: { children: ReactNode }) {
4142
const [expanded, setExpanded] = useState(false);
4243
const [showReadMore, setShowReadMore] = useState(false);
43-
const hiddenHeight = useRef<number>(0);
44-
const containerRef = useRef<HTMLDivElement>(null);
44+
const msgBubbleRef = useRef<HTMLDivElement>(null);
45+
46+
const messagesContainerRef = useMessagesContainerRef();
47+
48+
const scrollBefore = useRef<{ scrollTop: number; scrollHeight: number }>({
49+
scrollTop: 0,
50+
scrollHeight: 0,
51+
});
4552

4653
useLayoutEffect(() => {
4754
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;
55+
const msgContainerAfter = messagesContainerRef.current;
56+
if (!msgBubbleRef.current || !msgContainerAfter) {
57+
return;
58+
}
59+
const { scrollTop: scrollTopAfter, scrollHeight: scrollHeightAfter } = msgContainerAfter;
60+
61+
const { scrollTop: scrollTopBefore, scrollHeight: scrollHeightBefore } = scrollBefore.current;
5162

52-
document.getElementById('messages-container')?.scrollBy({
53-
top: -scrollDownBy,
63+
const topDidChange = scrollTopAfter !== scrollTopBefore;
64+
const heightDiff = scrollHeightAfter - scrollHeightBefore;
65+
const scrollTo = topDidChange ? scrollTopBefore - heightDiff : scrollTopAfter - heightDiff;
66+
67+
msgContainerAfter.scrollTo({
68+
top: scrollTo,
5469
behavior: 'instant',
5570
});
5671
}
57-
}, [expanded]);
72+
}, [expanded, messagesContainerRef]);
5873

59-
useLayoutEffect(() => {
60-
const container = containerRef.current;
61-
if (!container) {
74+
const onShowMore = () => {
75+
const el = msgBubbleRef.current;
76+
if (!el) {
6277
return;
6378
}
6479

65-
const el = container.firstElementChild;
66-
if (!el) {
67-
return;
80+
const msgContainerBefore = messagesContainerRef.current;
81+
82+
if (msgContainerBefore) {
83+
const { scrollTop: scrollTopBefore, scrollHeight: scrollHeightBefore } = msgContainerBefore;
84+
85+
scrollBefore.current = { scrollTop: scrollTopBefore, scrollHeight: scrollHeightBefore };
6886
}
6987

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);
88+
// we cannot "show less", only show more
89+
setExpanded(true);
90+
};
7491

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);
92+
useLayoutEffect(
93+
() => {
94+
const el = msgBubbleRef?.current?.firstElementChild;
95+
if (!el) {
96+
return;
97+
}
8098

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;
99+
// We need the body's child to find the line height as long as it exists
100+
const textEl = el.firstElementChild ?? el;
101+
const textStyle = window.getComputedStyle(textEl);
102+
const style = window.getComputedStyle(el);
84103

85-
const innerHeight =
86-
el.scrollHeight - (paddingTop + paddingBottom + borderTopWidth + borderBottomWidth);
104+
const lineHeight = parseFloat(textStyle.lineHeight);
105+
const paddingTop = parseFloat(style.paddingTop);
106+
const paddingBottom = parseFloat(style.paddingBottom);
107+
const borderTopWidth = parseFloat(style.borderTopWidth);
108+
const borderBottomWidth = parseFloat(style.borderBottomWidth);
87109

88-
const overflowsLines = innerHeight > maxHeight;
110+
// We need to allow for a 1 pixel buffer in maxHeight
111+
const maxHeight =
112+
lineHeight * Constants.CONVERSATION.MAX_MESSAGE_MAX_LINES_BEFORE_READ_MORE + 1;
89113

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]);
114+
const innerHeight =
115+
el.scrollHeight - (paddingTop + paddingBottom + borderTopWidth + borderBottomWidth);
116+
117+
const overflowsLines = innerHeight > maxHeight;
118+
119+
setShowReadMore(overflowsLines);
120+
},
121+
// Note: no need to provide a dependency here (and if we provide children, this hook reruns every second for every messages).
122+
// The only dependency is msgBubbleRef, but as it's a ref it's unneeded
123+
[]
124+
);
94125

95126
return (
96127
<>
97-
<StyledMessageBubble ref={containerRef} expanded={expanded}>
128+
<StyledMessageBubble ref={msgBubbleRef} expanded={expanded}>
98129
{children}
99130
</StyledMessageBubble>
100131
{showReadMore && !expanded ? (
101-
<ReadMoreButton onClick={() => setExpanded(prev => !prev)}>
102-
{tr('messageBubbleReadMore')}
103-
</ReadMoreButton>
132+
<ReadMoreButton onClick={onShowMore}>{tr('messageBubbleReadMore')}</ReadMoreButton>
104133
) : null}
105134
</>
106135
);
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)