improve timeline behaviour - WIP

This commit is contained in:
Ajay Bura 2026-10-24 13:26:28 +05:30
commit ef83de5c23
4 changed files with 52 additions and 62 deletions

View file

@ -273,8 +273,7 @@ export function OutboxView({
!!prevMsg &&
!dayDivider &&
prevMsg.original.type === sender.localEvent.original.type &&
minuteDifference(prevMsg.ts, sender.localEvent.ts) < 2 &&
!replyEventId;
minuteDifference(prevMsg.ts, sender.localEvent.ts) < 2;
prevMsg = sender.localEvent;

View file

@ -30,7 +30,6 @@ import { RoomIntro } from '../../components/room-intro';
import { usePowerLevelsContext } from '../../hooks/usePowerLevels';
import { MessageEvent, StateEvent } from '../../../types/matrix/room';
import { RenderMessageContent } from '../../components/RenderMessageContent';
import { useRoomNavigate } from '../../hooks/useRoomNavigate';
import { useIgnoredUsers } from '../../hooks/useIgnoredUsers';
import { useImagePackRooms } from '../../hooks/useImagePackRooms';
import { useIsDirectRoom, useRoom } from '../../hooks/useRoom';
@ -52,6 +51,7 @@ import {
useEditorResizeStickBottom,
useEventCollapse,
useEventDivider,
useMessageEditorOutOfViewFocus,
useReplyClickHandler,
useRestoreScrollToBottom,
useTimelinePaginate,
@ -139,18 +139,8 @@ export function RoomTimeline({ eventId, roomInputRef, editor, timeline }: RoomTi
const canPinEvent = permissions.stateEvent(StateEvent.RoomPinnedEvents, ct.session.userId);
const [editId, setEditId] = useState<string>();
// const roomToParents = useAtomValue(roomToParentsAtom);
// const unread = useRoomUnread(room.roomId, roomToUnreadAtom);
const { navigateRoom } = useRoomNavigate();
const imagePackRooms = useImagePackRooms(roomId);
// const [unreadInfo, setUnreadInfo] = useState(() => getRoomUnreadInfo(room, true));
const readUptoEventIdRef = useRef<string>();
// if (unreadInfo) {
// readUptoEventIdRef.current = unreadInfo.readUptoEventId;
// }
const scrollRef = useRef<HTMLDivElement>(null);
const [focusItem, setFocusItem] = useState<
@ -182,6 +172,8 @@ export function RoomTimeline({ eventId, roomInputRef, editor, timeline }: RoomTi
const getScrollElement = useCallback(() => scrollRef.current, []);
const readReceipt = useUserReadReceipt();
useTimelinePrefetch(timeline, PAGINATION_LIMIT);
const handlePaginate = useTimelinePaginate(timeline, PAGINATION_LIMIT);
useTimelineScrollRestore(timeline, scrollRef);
@ -204,7 +196,7 @@ export function RoomTimeline({ eventId, roomInputRef, editor, timeline }: RoomTi
});
const tryAutoMarkAsRead = useCallback(() => {
console.log('mark as read.');
console.log('%cmark as read.', 'background: blue; color: white; padding: 5px 10px;');
}, []);
const newDividerRef = useRef<HTMLDivElement>(null);
@ -254,21 +246,7 @@ export function RoomTimeline({ eventId, roomInputRef, editor, timeline }: RoomTi
}, 2000);
}, [alive, focusItem, scrollToItem]);
// scroll out of view msg editor in view.
useEffect(() => {
if (editId) {
const editMsgElement =
(scrollRef.current?.querySelector(`[data-message-id="${editId}"]`) as HTMLElement) ??
undefined;
if (editMsgElement) {
scrollToElement(editMsgElement, {
align: 'center',
behavior: 'smooth',
stopInView: true,
});
}
}
}, [scrollToElement, editId]);
useMessageEditorOutOfViewFocus(scrollRef, scrollToElement, editId);
const stickToBottom = useCallback(() => {
if (!atBottom) return;
@ -282,7 +260,15 @@ export function RoomTimeline({ eventId, roomInputRef, editor, timeline }: RoomTi
}, [getScrollElement, atBottom]);
useObservableValueListener(timeline.outbox.senders, stickToBottom);
useObservableValueListener(timeline.range.info, stickToBottom);
useObservableValueListener(
timeline.range.info,
useCallback(() => {
const batchInfo = timeline.batch.read;
if (batchInfo.change.dir === 'f' && batchInfo.next.end) {
stickToBottom();
}
}, [stickToBottom, timeline])
);
const handleOpenReply = useCallback(
(replyId: string) => {
@ -503,7 +489,6 @@ export function RoomTimeline({ eventId, roomInputRef, editor, timeline }: RoomTi
);
};
const readReceipt = useUserReadReceipt();
const { getDayDivider, getNewDivider, updateDivider, clearDayDivider, clearNewDivider } =
useEventDivider(readReceipt?.ts);
const { getCollapsed, updateCollapse } = useEventCollapse();

View file

@ -35,7 +35,8 @@ import { useRoomMembersAPI } from '../../../hooks/useRoomMembers';
import { useClient } from '../../../hooks/useClient';
import { createMentionElement, moveCursor } from '../../../components/editor';
import { inSameDay, minuteDifference } from '../../../utils/time';
import { fallbackReply, getReplyId, getThreadId } from '../../../utils/room';
import { getThreadId } from '../../../utils/room';
import { ScrollToElement } from '../../../hooks/useVirtualPaginator';
const TimelineViewContext = createContext<TimelineView | null>(null);
export const TimelineViewProvider = TimelineViewContext.Provider;
@ -77,6 +78,9 @@ export const useTimelinePaginate = (timeline: TimelineView, limit: number) =>
[timeline, limit]
);
/**
* Restore timeline scrolling position on room open.
*/
export const useTimelineScrollRestore = (
timeline: TimelineView,
scrollRef: RefObject<HTMLDivElement>
@ -88,7 +92,7 @@ export const useTimelineScrollRestore = (
if (scrollTop) {
scrollEl.scrollTo({ top: scrollTop, behavior: 'instant' });
} else {
scrollToBottom(scrollEl);
scrollToBottom(scrollEl, 'instant');
}
}
return () => {
@ -160,7 +164,7 @@ export const useAtBottomToggle = (
onBottom?: (bottom: boolean) => void
): [boolean, RefObject<HTMLElement>] => {
const atBottomAnchorRef = useRef<HTMLElement>(null);
const [atBottom, setAtBottom] = useState<boolean>(true);
const [atBottom, setAtBottom] = useState<boolean>(atLiveEnd);
const debounceSetAtBottom = useDebounce(
useCallback(
@ -180,7 +184,11 @@ export const useAtBottomToggle = (
const target = atBottomAnchorRef.current;
if (!target) return;
const targetEntry = getIntersectionObserverEntry(target, entries);
// set to false with delay
if (targetEntry) debounceSetAtBottom(targetEntry);
// set to true instantly
if (targetEntry?.isIntersecting && atLiveEnd) {
setAtBottom(true);
onBottom?.(true);
@ -344,8 +352,6 @@ export const useEventCollapse = (threadId?: string) => {
newDivider: boolean,
dayDivider: boolean
) => {
const replyEventId = fallbackReply(event.content) ? undefined : getReplyId(event.content);
const collapsed =
isPrevRendered &&
!dayDivider &&
@ -354,8 +360,7 @@ export const useEventCollapse = (threadId?: string) => {
prevEvent.sender === event.sender &&
prevEvent.type === event.type &&
(threadId ? prevEvent.event_id !== threadId : !getThreadId(prevEvent)) &&
minuteDifference(prevEvent.origin_server_ts, event.origin_server_ts) < 2 &&
!replyEventId;
minuteDifference(prevEvent.origin_server_ts, event.origin_server_ts) < 2;
return collapsed;
};
@ -367,3 +372,25 @@ export const useEventCollapse = (threadId?: string) => {
return { getCollapsed, updateCollapse };
};
export const useMessageEditorOutOfViewFocus = (
scrollRef: RefObject<HTMLDivElement>,
scrollToElement: ScrollToElement,
editId?: string
) => {
// scroll out of view msg editor in view.
useEffect(() => {
if (editId) {
const editMsgElement =
(scrollRef.current?.querySelector(`[data-message-id="${editId}"]`) as HTMLElement) ??
undefined;
if (editMsgElement) {
scrollToElement(editMsgElement, {
align: 'center',
behavior: 'smooth',
stopInView: true,
});
}
}
}, [scrollRef, scrollToElement, editId]);
};

View file

@ -1,12 +1,4 @@
import React, {
RefObject,
useCallback,
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState,
} from 'react';
import React, { RefObject, useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { ReactEditor } from 'slate-react';
import { Editor } from 'slate';
import { Box, Scroll, Text, config } from 'folds';
@ -52,6 +44,7 @@ import {
useEditorResizeStickBottom,
useEventCollapse,
useEventDivider,
useMessageEditorOutOfViewFocus,
useReplyClickHandler,
useRestoreScrollToBottom,
useTimelinePaginate,
@ -242,21 +235,7 @@ export function TimelineView({ threadTimeline, threadInputRef, editor }: Timelin
}
}, [scrollToBottomCount]);
// scroll out of view msg editor in view.
useEffect(() => {
if (editId) {
const editMsgElement =
(scrollRef.current?.querySelector(`[data-message-id="${editId}"]`) as HTMLElement) ??
undefined;
if (editMsgElement) {
scrollToElement(editMsgElement, {
align: 'center',
behavior: 'smooth',
stopInView: true,
});
}
}
}, [scrollToElement, editId]);
useMessageEditorOutOfViewFocus(scrollRef, scrollToElement, editId);
const handleOpenReply = useCallback((replyId: string) => {
console.log(replyId);