From c980fddfa1e927927981941ef7e7d6952d9d494f Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Thu, 19 Oct 2023 17:40:01 +1100 Subject: [PATCH] Fix unread bug (#1454) * remove unread info on mark as read * fix roomId is not provided to markAsRead * fix auto mark as read --- src/app/organisms/room/RoomTimeline.tsx | 84 ++++++++++++++++--------- 1 file changed, 55 insertions(+), 29 deletions(-) diff --git a/src/app/organisms/room/RoomTimeline.tsx b/src/app/organisms/room/RoomTimeline.tsx index 65ea9ac..2d7214c 100644 --- a/src/app/organisms/room/RoomTimeline.tsx +++ b/src/app/organisms/room/RoomTimeline.tsx @@ -132,6 +132,7 @@ import { usePowerLevelsAPI } from '../../hooks/usePowerLevels'; import { MessageEvent } from '../../../types/matrix/room'; import initMatrix from '../../../client/initMatrix'; import { useKeyDown } from '../../hooks/useKeyDown'; +import cons from '../../../client/state/cons'; const TimelineFloat = as<'div', css.TimelineFloatVariants>( ({ position, className, ...props }, ref) => ( @@ -479,7 +480,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli } const atBottomAnchorRef = useRef(null); - const [atBottom, setAtBottom] = useState(); + const [atBottom, setAtBottom] = useState(true); const atBottomRef = useRef(atBottom); atBottomRef.current = atBottom; @@ -538,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, @@ -599,8 +602,12 @@ 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 && mEvt.getSender() !== mx.getUserId()) { + if (!unreadInfo) { markAsRead(mEvt.getRoomId()); } @@ -620,7 +627,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli setUnreadInfo(getRoomUnreadInfo(room)); } }, - [mx, room, unreadInfo] + [room, unreadInfo] ) ); @@ -635,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(); @@ -645,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); @@ -664,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( () => ({ @@ -711,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) { @@ -722,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]); @@ -755,21 +786,17 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli } }, [scrollToBottomCount]); - // send readReceipts when reach bottom + // Remove unreadInfo on mark as read 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(); - setUnreadInfo(undefined); - } - } - }, [room, unreadInfo, liveTimelineLinked, rangeAtEnd, atBottom]); + const handleFullRead = (rId: string) => { + if (rId !== room.roomId) return; + setUnreadInfo(undefined); + }; + initMatrix.notifications?.on(cons.events.notifications.FULL_READ, handleFullRead); + return () => { + initMatrix.notifications?.removeListener(cons.events.notifications.FULL_READ, handleFullRead); + }; + }, [room]); // scroll out of view msg editor in view. useEffect(() => { @@ -802,7 +829,6 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli const handleMarkAsRead = () => { markAsRead(room.roomId); - setUnreadInfo(undefined); }; const handleOpenReply: MouseEventHandler = useCallback( @@ -1722,7 +1748,7 @@ export function RoomTimeline({ room, eventId, roomInputRef, editor }: RoomTimeli - {(atBottom === false || !liveTimelineLinked || !rangeAtEnd) && ( + {!atBottom && (