@@ -2,6 +2,7 @@ import { useState, useRef, type ReactNode, useLayoutEffect } from 'react';
22import styled , { css } from 'styled-components' ;
33import { Constants } from '../../../../session' ;
44import { tr } from '../../../../localization/localeTools' ;
5+ import { useMessagesContainerRef } from '../../../../contexts/MessagesContainerRefContext' ;
56
67export const StyledMessageBubble = styled . div < { expanded : boolean } > `
78 position: relative;
@@ -40,67 +41,95 @@ const ReadMoreButton = styled.button`
4041export 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 ) ;
0 commit comments