diff --git a/src/app/features/room/OutboxView.tsx b/src/app/features/room/OutboxView.tsx index 1d572af9..27f48d0a 100644 --- a/src/app/features/room/OutboxView.tsx +++ b/src/app/features/room/OutboxView.tsx @@ -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; diff --git a/src/app/features/room/RoomTimeline.tsx b/src/app/features/room/RoomTimeline.tsx index a3c1dd69..ae816b26 100644 --- a/src/app/features/room/RoomTimeline.tsx +++ b/src/app/features/room/RoomTimeline.tsx @@ -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(); - // 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(); - // if (unreadInfo) { - // readUptoEventIdRef.current = unreadInfo.readUptoEventId; - // } - const scrollRef = useRef(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(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(); diff --git a/src/app/features/room/hooks/useTimeline.ts b/src/app/features/room/hooks/useTimeline.ts index 9941bcae..38f99b76 100644 --- a/src/app/features/room/hooks/useTimeline.ts +++ b/src/app/features/room/hooks/useTimeline.ts @@ -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(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 @@ -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] => { const atBottomAnchorRef = useRef(null); - const [atBottom, setAtBottom] = useState(true); + const [atBottom, setAtBottom] = useState(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, + 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]); +}; diff --git a/src/app/features/room/thread-view/TimelineView.tsx b/src/app/features/room/thread-view/TimelineView.tsx index df0db9f6..f8036414 100644 --- a/src/app/features/room/thread-view/TimelineView.tsx +++ b/src/app/features/room/thread-view/TimelineView.tsx @@ -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);