diff --git a/src/app/organisms/room/RoomTimeline.tsx b/src/app/organisms/room/RoomTimeline.tsx index f80a9f24..2d7214c4 100644 --- a/src/app/organisms/room/RoomTimeline.tsx +++ b/src/app/organisms/room/RoomTimeline.tsx @@ -480,7 +480,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli } const atBottomAnchorRef = useRef<HTMLElement>(null); - const [atBottom, setAtBottom] = useState<boolean>(); + const [atBottom, setAtBottom] = useState<boolean>(true); const atBottomRef = useRef(atBottom); atBottomRef.current = atBottom; @@ -539,6 +539,8 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli typeof timeline.linkedTimelines[0]?.getPaginationToken(Direction.Backward) === 'string'; const rangeAtStart = timeline.range.start === 0; const rangeAtEnd = timeline.range.end === eventsLength; + const atLiveEndRef = useRef(liveTimelineLinked && rangeAtEnd); + atLiveEndRef.current = liveTimelineLinked && rangeAtEnd; const handleTimelinePagination = useTimelinePagination( mx, @@ -600,6 +602,10 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli room, useCallback( (mEvt: MatrixEvent) => { + // if user is at bottom of timeline + // keep paginating timeline and conditionally mark as read + // otherwise we update timeline without paginating + // so timeline can be updated with evt like: edits, reactions etc if (atBottomRef.current && document.hasFocus()) { if (!unreadInfo) { markAsRead(mEvt.getRoomId()); @@ -636,8 +642,14 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli // Stay at bottom when room editor resize useResizeObserver( - useCallback( - (entries) => { + useMemo(() => { + let mounted = false; + return (entries) => { + if (!mounted) { + // skip initial mounting call + mounted = true; + return; + } if (!roomInputRef.current) return; const editorBaseEntry = getResizeObserverEntry(roomInputRef.current, entries); const scrollElement = getScrollElement(); @@ -646,12 +658,23 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli if (atBottomRef.current) { scrollToBottom(scrollElement); } - }, - [getScrollElement, roomInputRef] - ), + }; + }, [getScrollElement, roomInputRef]), useCallback(() => roomInputRef.current, [roomInputRef]) ); + const tryAutoMarkAsRead = useCallback(() => { + if (!unreadInfo) { + markAsRead(room.roomId); + return; + } + const evtTimeline = getEventTimeline(room, unreadInfo.readUptoEventId); + const latestTimeline = evtTimeline && getFirstLinkedTimeline(evtTimeline, Direction.Forward); + if (latestTimeline === room.getLiveTimeline()) { + markAsRead(room.roomId); + } + }, [room, unreadInfo]); + const debounceSetAtBottom = useDebounce( useCallback((entry: IntersectionObserverEntry) => { if (!entry.isIntersecting) setAtBottom(false); @@ -665,9 +688,12 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli if (!target) return; const targetEntry = getIntersectionObserverEntry(target, entries); if (targetEntry) debounceSetAtBottom(targetEntry); - if (targetEntry?.isIntersecting) setAtBottom(true); + if (targetEntry?.isIntersecting && atLiveEndRef.current) { + setAtBottom(true); + tryAutoMarkAsRead(); + } }, - [debounceSetAtBottom] + [debounceSetAtBottom, tryAutoMarkAsRead] ), useCallback( () => ({ @@ -712,10 +738,13 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli // Scroll to bottom on initial timeline load useLayoutEffect(() => { const scrollEl = scrollRef.current; - if (scrollEl) scrollToBottom(scrollEl); + if (scrollEl) { + scrollToBottom(scrollEl); + } }, []); - // Scroll to last read message if it is linked to live timeline + // if live timeline is linked and unreadInfo change + // Scroll to last read message useLayoutEffect(() => { const { readUptoEventId, inLiveTimeline, scrollTo } = unreadInfo ?? {}; if (readUptoEventId && inLiveTimeline && scrollTo) { @@ -723,12 +752,13 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli const evtTimeline = getEventTimeline(room, readUptoEventId); const absoluteIndex = evtTimeline && getEventIdAbsoluteIndex(linkedTimelines, evtTimeline, readUptoEventId); - if (absoluteIndex) + if (absoluteIndex) { scrollToItem(absoluteIndex, { behavior: 'instant', align: 'start', stopInView: true, }); + } } }, [room, unreadInfo, scrollToItem]); @@ -756,21 +786,6 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli } }, [scrollToBottomCount]); - // send readReceipts when reach bottom - useEffect(() => { - if (liveTimelineLinked && rangeAtEnd && atBottom && document.hasFocus()) { - if (!unreadInfo) { - markAsRead(room.roomId); - return; - } - const evtTimeline = getEventTimeline(room, unreadInfo.readUptoEventId); - const latestTimeline = evtTimeline && getFirstLinkedTimeline(evtTimeline, Direction.Forward); - if (latestTimeline === room.getLiveTimeline()) { - markAsRead(room.roomId); - } - } - }, [room, unreadInfo, liveTimelineLinked, rangeAtEnd, atBottom]); - // Remove unreadInfo on mark as read useEffect(() => { const handleFullRead = (rId: string) => { @@ -1733,7 +1748,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli <span ref={atBottomAnchorRef} /> </Box> </Scroll> - {(atBottom === false || !liveTimelineLinked || !rangeAtEnd) && ( + {!atBottom && ( <TimelineFloat position="Bottom"> <Chip variant="SurfaceVariant"