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) => (
}
const atBottomAnchorRef = useRef<HTMLElement>(null);
- const [atBottom, setAtBottom] = useState<boolean>();
+ const [atBottom, setAtBottom] = useState<boolean>(true);
const atBottomRef = useRef(atBottom);
atBottomRef.current = atBottom;
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,
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());
}
setUnreadInfo(getRoomUnreadInfo(room));
}
},
- [mx, room, unreadInfo]
+ [room, unreadInfo]
)
);
// 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();
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);
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(
() => ({
// 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) {
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]);
}
}, [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(() => {
const handleMarkAsRead = () => {
markAsRead(room.roomId);
- setUnreadInfo(undefined);
};
const handleOpenReply: MouseEventHandler<HTMLButtonElement> = useCallback(
<span ref={atBottomAnchorRef} />
</Box>
</Scroll>
- {(atBottom === false || !liveTimelineLinked || !rangeAtEnd) && (
+ {!atBottom && (
<TimelineFloat position="Bottom">
<Chip
variant="SurfaceVariant"