forked from Mirrors/gomuks
web/timeline: send read receipts
This commit is contained in:
parent
3c596a200f
commit
89ece7fb45
4 changed files with 92 additions and 33 deletions
|
@ -14,6 +14,7 @@
|
|||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
import { NonNullCachedEventDispatcher } from "@/util/eventdispatcher.ts"
|
||||
import Subscribable from "@/util/subscribable.ts"
|
||||
import type {
|
||||
DBRoom,
|
||||
EncryptedEventContent,
|
||||
|
@ -60,32 +61,6 @@ function visibleMetaIsEqual(meta1: DBRoom, meta2: DBRoom): boolean {
|
|||
meta1.has_member_list === meta2.has_member_list
|
||||
}
|
||||
|
||||
type Subscriber = () => void
|
||||
type SubscribeFunc = (callback: Subscriber) => () => void
|
||||
|
||||
class Subscribable {
|
||||
readonly subscribers: Set<Subscriber> = new Set()
|
||||
|
||||
constructor(private onEmpty?: () => void) {
|
||||
}
|
||||
|
||||
subscribe: SubscribeFunc = callback => {
|
||||
this.subscribers.add(callback)
|
||||
return () => {
|
||||
this.subscribers.delete(callback)
|
||||
if (this.subscribers.size === 0) {
|
||||
this.onEmpty?.()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
notify() {
|
||||
for (const sub of this.subscribers) {
|
||||
sub()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class EventSubscribable extends Subscribable {
|
||||
requested: boolean = false
|
||||
}
|
||||
|
@ -103,6 +78,8 @@ export class RoomStateStore {
|
|||
readonly eventSubs: Map<EventID, EventSubscribable> = new Map()
|
||||
readonly pendingEvents: EventRowID[] = []
|
||||
paginating = false
|
||||
paginationRequestedForRow = -1
|
||||
readUpToRow = -1
|
||||
|
||||
constructor(meta: DBRoom) {
|
||||
this.roomID = meta.room_id
|
||||
|
|
|
@ -13,9 +13,10 @@
|
|||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
import { use, useCallback, useEffect, useRef } from "react"
|
||||
import { use, useCallback, useEffect, useLayoutEffect, useRef } from "react"
|
||||
import { RoomStateStore, useRoomTimeline } from "@/api/statestore"
|
||||
import { MemDBEvent } from "@/api/types"
|
||||
import useFocus from "@/util/focus.ts"
|
||||
import { ClientContext } from "../ClientContext.ts"
|
||||
import TimelineEvent from "./TimelineEvent.tsx"
|
||||
import "./TimelineView.css"
|
||||
|
@ -33,14 +34,14 @@ const TimelineView = ({ room, textRows, replyTo, setReplyTo }: TimelineViewProps
|
|||
const loadHistory = useCallback(() => {
|
||||
client.loadMoreHistory(room.roomID)
|
||||
.catch(err => console.error("Failed to load history", err))
|
||||
}, [client, room.roomID])
|
||||
}, [client, room])
|
||||
const bottomRef = useRef<HTMLDivElement>(null)
|
||||
const topRef = useRef<HTMLDivElement>(null)
|
||||
const timelineViewRef = useRef<HTMLDivElement>(null)
|
||||
const prevOldestTimelineRow = useRef(0)
|
||||
const paginationRequestedForRow = useRef(-1)
|
||||
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.
|
||||
|
@ -55,7 +56,7 @@ const TimelineView = ({ room, textRows, replyTo, setReplyTo }: TimelineViewProps
|
|||
if (timelineViewRef.current) {
|
||||
oldScrollHeight.current = timelineViewRef.current.scrollHeight
|
||||
}
|
||||
useEffect(() => {
|
||||
useLayoutEffect(() => {
|
||||
if (bottomRef.current && scrolledToBottom.current) {
|
||||
// For any timeline changes, if we were at the bottom, scroll to the new bottom
|
||||
bottomRef.current.scrollIntoView()
|
||||
|
@ -65,14 +66,30 @@ const TimelineView = ({ room, textRows, replyTo, setReplyTo }: TimelineViewProps
|
|||
}
|
||||
prevOldestTimelineRow.current = timeline[0]?.timeline_rowid ?? 0
|
||||
}, [textRows, replyTo, timeline])
|
||||
useEffect(() => {
|
||||
const newestEvent = timeline[timeline.length - 1]
|
||||
if (
|
||||
scrolledToBottom.current
|
||||
&& focused
|
||||
&& newestEvent
|
||||
&& newestEvent.timeline_rowid > 0
|
||||
&& room.readUpToRow < newestEvent.timeline_rowid
|
||||
) {
|
||||
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 && paginationRequestedForRow.current !== prevOldestTimelineRow.current) {
|
||||
paginationRequestedForRow.current = prevOldestTimelineRow.current
|
||||
if (entries[0]?.isIntersecting && room.paginationRequestedForRow !== prevOldestTimelineRow.current) {
|
||||
room.paginationRequestedForRow = prevOldestTimelineRow.current
|
||||
loadHistory()
|
||||
}
|
||||
}, {
|
||||
|
@ -82,7 +99,7 @@ const TimelineView = ({ room, textRows, replyTo, setReplyTo }: TimelineViewProps
|
|||
})
|
||||
observer.observe(topElem)
|
||||
return () => observer.unobserve(topElem)
|
||||
}, [loadHistory, topRef])
|
||||
}, [room, loadHistory])
|
||||
|
||||
let prevEvt: MemDBEvent | null = null
|
||||
return <div className="timeline-view" onScroll={handleScroll} ref={timelineViewRef}>
|
||||
|
|
24
web/src/util/focus.ts
Normal file
24
web/src/util/focus.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
// 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 <https://www.gnu.org/licenses/>.
|
||||
import { NonNullCachedEventDispatcher, useNonNullEventAsState } from "@/util/eventdispatcher.ts"
|
||||
|
||||
const focused = new NonNullCachedEventDispatcher(document.hasFocus())
|
||||
window.addEventListener("focus", () => focused.emit(true))
|
||||
window.addEventListener("blur", () => focused.emit(true))
|
||||
|
||||
export default function useFocus() {
|
||||
return useNonNullEventAsState(focused)
|
||||
}
|
41
web/src/util/subscribable.ts
Normal file
41
web/src/util/subscribable.ts
Normal file
|
@ -0,0 +1,41 @@
|
|||
// 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
export type Subscriber = () => void
|
||||
export type SubscribeFunc = (callback: Subscriber) => () => void
|
||||
|
||||
export default class Subscribable {
|
||||
readonly subscribers: Set<Subscriber> = new Set()
|
||||
|
||||
constructor(private onEmpty?: () => void) {
|
||||
}
|
||||
|
||||
subscribe: SubscribeFunc = callback => {
|
||||
this.subscribers.add(callback)
|
||||
return () => {
|
||||
this.subscribers.delete(callback)
|
||||
if (this.subscribers.size === 0) {
|
||||
this.onEmpty?.()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
notify() {
|
||||
for (const sub of this.subscribers) {
|
||||
sub()
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue