// gomuks - A Matrix client written in Go. // Copyright (C) 2024 Tulir Asokan // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . import { use, useCallback, useEffect, useLayoutEffect, useRef } from "react" import { useRoomTimeline } from "@/api/statestore" import { MemDBEvent } from "@/api/types" import useFocus from "@/util/focus.ts" import { ClientContext } from "../ClientContext.ts" import { useRoomContext } from "../roomcontext.ts" import TimelineEvent from "./TimelineEvent.tsx" import "./TimelineView.css" const TimelineView = () => { const roomCtx = useRoomContext() const room = roomCtx.store const timeline = useRoomTimeline(room) const client = use(ClientContext)! const loadHistory = useCallback(() => { client.loadMoreHistory(room.roomID) .catch(err => console.error("Failed to load history", err)) }, [client, room]) const bottomRef = roomCtx.timelineBottomRef const topRef = useRef(null) const timelineViewRef = useRef(null) const prevOldestTimelineRow = useRef(0) const oldScrollHeight = useRef(0) const scrolledToBottom = useRef(true) const focused = useFocus() // When the user scrolls the timeline manually, remember if they were at the bottom, // so that we can keep them at the bottom when new events are added. const handleScroll = useCallback(() => { if (!timelineViewRef.current) { return } const timelineView = timelineViewRef.current scrolledToBottom.current = timelineView.scrollTop + timelineView.clientHeight + 1 >= timelineView.scrollHeight }, []) // Save the scroll height prior to updating the timeline, so that we can adjust the scroll position if needed. if (timelineViewRef.current) { oldScrollHeight.current = timelineViewRef.current.scrollHeight } useLayoutEffect(() => { if (bottomRef.current && scrolledToBottom.current) { // For any timeline changes, if we were at the bottom, scroll to the new bottom bottomRef.current.scrollIntoView() } else if (timelineViewRef.current && prevOldestTimelineRow.current > (timeline[0]?.timeline_rowid ?? 0)) { // When new entries are added to the top of the timeline, scroll down to keep the same position timelineViewRef.current.scrollTop += timelineViewRef.current.scrollHeight - oldScrollHeight.current } prevOldestTimelineRow.current = timeline[0]?.timeline_rowid ?? 0 }, [bottomRef, timeline]) useEffect(() => { const newestEvent = timeline[timeline.length - 1] if ( scrolledToBottom.current && focused && newestEvent && newestEvent.timeline_rowid > 0 && room.readUpToRow < newestEvent.timeline_rowid && newestEvent.sender !== client.userID ) { room.readUpToRow = newestEvent.timeline_rowid client.rpc.markRead(room.roomID, newestEvent.event_id, "m.read").then( () => console.log("Marked read up to", newestEvent.event_id, newestEvent.timeline_rowid), err => console.error(`Failed to send read receipt for ${newestEvent.event_id}:`, err), ) } }, [focused, client, room, timeline]) useEffect(() => { const topElem = topRef.current if (!topElem) { return } const observer = new IntersectionObserver(entries => { if (entries[0]?.isIntersecting && room.paginationRequestedForRow !== prevOldestTimelineRow.current) { room.paginationRequestedForRow = prevOldestTimelineRow.current loadHistory() } }, { root: topElem.parentElement!.parentElement, rootMargin: "0px", threshold: 1.0, }) observer.observe(topElem) return () => observer.unobserve(topElem) }, [room, loadHistory]) let prevEvt: MemDBEvent | null = null return
{timeline.map(entry => { if (!entry) { return null } const thisEvt = prevEvt = entry return thisEvt })}
} export default TimelineView