mirror of
https://github.com/tulir/gomuks.git
synced 2025-04-19 18:13:41 -05:00
web: request replied-to event if it's not cached
This commit is contained in:
parent
bbc59a2f89
commit
e9834fd987
6 changed files with 93 additions and 37 deletions
|
@ -15,15 +15,8 @@
|
|||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
import { CachedEventDispatcher } from "../util/eventdispatcher.ts"
|
||||
import RPCClient, { SendMessageParams } from "./rpc.ts"
|
||||
import { StateStore } from "./statestore"
|
||||
import type {
|
||||
ClientState,
|
||||
EventRowID,
|
||||
EventType,
|
||||
RPCEvent,
|
||||
RoomID,
|
||||
UserID,
|
||||
} from "./types"
|
||||
import { RoomStateStore, StateStore } from "./statestore"
|
||||
import type { ClientState, EventID, EventRowID, EventType, RPCEvent, RoomID, UserID } from "./types"
|
||||
|
||||
export default class Client {
|
||||
readonly state = new CachedEventDispatcher<ClientState>()
|
||||
|
@ -49,6 +42,24 @@ export default class Client {
|
|||
}
|
||||
}
|
||||
|
||||
requestEvent(room: RoomStateStore | RoomID | undefined, eventID: EventID) {
|
||||
if (typeof room === "string") {
|
||||
room = this.store.rooms.get(room)
|
||||
}
|
||||
if (!room || room.eventsByID.has(eventID)) {
|
||||
return
|
||||
}
|
||||
const sub = room.getEventSubscriber(eventID)
|
||||
if (sub.requested) {
|
||||
return
|
||||
}
|
||||
sub.requested = true
|
||||
this.rpc.getEvent(room.roomID, eventID).then(
|
||||
evt => room.applyEvent(evt),
|
||||
err => console.error(`Failed to fetch event ${eventID}`, err),
|
||||
)
|
||||
}
|
||||
|
||||
async sendMessage(params: SendMessageParams): Promise<void> {
|
||||
const room = this.store.rooms.get(params.room_id)
|
||||
if (!room) {
|
||||
|
|
|
@ -14,12 +14,19 @@
|
|||
// 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 { useSyncExternalStore } from "react"
|
||||
import type { MemDBEvent } from "../types"
|
||||
import type { EventID, MemDBEvent } from "../types"
|
||||
import { RoomStateStore } from "./room.ts"
|
||||
|
||||
export function useRoomTimeline(room: RoomStateStore): (MemDBEvent | null)[] {
|
||||
return useSyncExternalStore(
|
||||
room.subscribeTimeline,
|
||||
room.timelineSub.subscribe,
|
||||
() => room.timelineCache,
|
||||
)
|
||||
}
|
||||
|
||||
export function useRoomEvent(room: RoomStateStore, eventID: EventID): MemDBEvent | null {
|
||||
return useSyncExternalStore(
|
||||
room.getEventSubscriber(eventID).subscribe,
|
||||
() => room.eventsByID.get(eventID) ?? null,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -60,7 +60,35 @@ function visibleMetaIsEqual(meta1: DBRoom, meta2: DBRoom): boolean {
|
|||
meta1.has_member_list === meta2.has_member_list
|
||||
}
|
||||
|
||||
type SubscribeFunc = (callback: () => void) => () => void
|
||||
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
|
||||
}
|
||||
|
||||
export class RoomStateStore {
|
||||
readonly roomID: RoomID
|
||||
|
@ -71,7 +99,8 @@ export class RoomStateStore {
|
|||
stateLoaded = false
|
||||
readonly eventsByRowID: Map<EventRowID, MemDBEvent> = new Map()
|
||||
readonly eventsByID: Map<EventID, MemDBEvent> = new Map()
|
||||
readonly timelineSubscribers: Set<() => void> = new Set()
|
||||
readonly timelineSub = new Subscribable()
|
||||
readonly eventSubs: Map<EventID, EventSubscribable> = new Map()
|
||||
readonly pendingEvents: EventRowID[] = []
|
||||
paginating = false
|
||||
|
||||
|
@ -80,11 +109,6 @@ export class RoomStateStore {
|
|||
this.meta = new NonNullCachedEventDispatcher(meta)
|
||||
}
|
||||
|
||||
subscribeTimeline: SubscribeFunc = callback => {
|
||||
this.timelineSubscribers.add(callback)
|
||||
return () => this.timelineSubscribers.delete(callback)
|
||||
}
|
||||
|
||||
notifyTimelineSubscribers() {
|
||||
this.timelineCache = this.timeline.map(rt => {
|
||||
const evt = this.eventsByRowID.get(rt.event_rowid)
|
||||
|
@ -96,9 +120,16 @@ export class RoomStateStore {
|
|||
}).concat(this.pendingEvents
|
||||
.map(rowID => this.eventsByRowID.get(rowID))
|
||||
.filter(evt => !!evt))
|
||||
for (const sub of this.timelineSubscribers) {
|
||||
sub()
|
||||
this.timelineSub.notify()
|
||||
}
|
||||
|
||||
getEventSubscriber(eventID: EventID): EventSubscribable {
|
||||
let sub = this.eventSubs.get(eventID)
|
||||
if (!sub) {
|
||||
sub = new EventSubscribable(() => this.eventsByID.has(eventID) && this.eventSubs.delete(eventID))
|
||||
this.eventSubs.set(eventID, sub)
|
||||
}
|
||||
return sub
|
||||
}
|
||||
|
||||
getStateEvent(type: EventType, stateKey: string): MemDBEvent | undefined {
|
||||
|
@ -149,6 +180,7 @@ export class RoomStateStore {
|
|||
this.pendingEvents.splice(pendingIdx, 1)
|
||||
}
|
||||
}
|
||||
this.eventSubs.get(evt.event_id)?.notify()
|
||||
}
|
||||
|
||||
applySendComplete(evt: RawDBEvent) {
|
||||
|
|
|
@ -17,7 +17,7 @@ import React, { use, useCallback, useRef, useState } from "react"
|
|||
import { RoomStateStore } from "@/api/statestore"
|
||||
import { MemDBEvent, Mentions } from "@/api/types"
|
||||
import { ClientContext } from "./ClientContext.ts"
|
||||
import ReplyBody from "./timeline/ReplyBody.tsx"
|
||||
import { ReplyBody } from "./timeline/ReplyBody.tsx"
|
||||
import "./MessageComposer.css"
|
||||
|
||||
interface MessageComposerProps {
|
||||
|
|
|
@ -13,31 +13,39 @@
|
|||
//
|
||||
// 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 } from "react"
|
||||
import { getAvatarURL } from "@/api/media.ts"
|
||||
import type { RoomStateStore } from "@/api/statestore"
|
||||
import { RoomStateStore, useRoomEvent } from "@/api/statestore"
|
||||
import type { EventID, MemDBEvent, MemberEventContent } from "@/api/types"
|
||||
import { ClientContext } from "../ClientContext.ts"
|
||||
import { TextMessageBody } from "./content/MessageBody.tsx"
|
||||
import CloseButton from "@/icons/close.svg?react"
|
||||
import "./ReplyBody.css"
|
||||
|
||||
interface BaseReplyBodyProps {
|
||||
interface ReplyBodyProps {
|
||||
room: RoomStateStore
|
||||
eventID?: EventID
|
||||
event?: MemDBEvent
|
||||
event: MemDBEvent
|
||||
onClose?: () => void
|
||||
}
|
||||
|
||||
type ReplyBodyProps = BaseReplyBodyProps & ({eventID: EventID } | {event: MemDBEvent })
|
||||
interface ReplyIDBodyProps {
|
||||
room: RoomStateStore
|
||||
eventID: EventID
|
||||
}
|
||||
|
||||
const ReplyBody = ({ room, eventID, event, onClose }: ReplyBodyProps) => {
|
||||
export const ReplyIDBody = ({ room, eventID }: ReplyIDBodyProps) => {
|
||||
const event = useRoomEvent(room, eventID)
|
||||
if (!event) {
|
||||
event = room.eventsByID.get(eventID!)
|
||||
if (!event) {
|
||||
return <blockquote className="reply-body">
|
||||
Reply to {eventID}
|
||||
</blockquote>
|
||||
}
|
||||
// This caches whether the event is requested or not, so it doesn't need to be wrapped in an effect.
|
||||
use(ClientContext)!.requestEvent(room, eventID)
|
||||
return <blockquote className="reply-body">
|
||||
Reply to {eventID}
|
||||
</blockquote>
|
||||
}
|
||||
return <ReplyBody room={room} event={event}/>
|
||||
}
|
||||
|
||||
export const ReplyBody = ({ room, event, onClose }: ReplyBodyProps) => {
|
||||
const memberEvt = room.getStateEvent("m.room.member", event.sender)
|
||||
const memberEvtContent = memberEvt?.content as MemberEventContent | undefined
|
||||
return <blockquote className={`reply-body ${onClose ? "composer" : ""}`}>
|
||||
|
@ -56,5 +64,3 @@ const ReplyBody = ({ room, eventID, event, onClose }: ReplyBodyProps) => {
|
|||
<TextMessageBody room={room} event={event}/>
|
||||
</blockquote>
|
||||
}
|
||||
|
||||
export default ReplyBody
|
||||
|
|
|
@ -19,7 +19,7 @@ import { RoomStateStore } from "@/api/statestore"
|
|||
import { MemDBEvent, MemberEventContent } from "@/api/types"
|
||||
import { ClientContext } from "../ClientContext.ts"
|
||||
import { LightboxContext } from "../Lightbox.tsx"
|
||||
import ReplyBody from "./ReplyBody.tsx"
|
||||
import { ReplyIDBody } from "./ReplyBody.tsx"
|
||||
import EncryptedBody from "./content/EncryptedBody.tsx"
|
||||
import HiddenEvent from "./content/HiddenEvent.tsx"
|
||||
import MemberBody from "./content/MemberBody.tsx"
|
||||
|
@ -153,7 +153,7 @@ const TimelineEvent = ({ room, evt, prevEvt, setReplyTo }: TimelineEventProps) =
|
|||
</div>
|
||||
<div className="event-content">
|
||||
{typeof replyTo === "string" && BodyType !== HiddenEvent
|
||||
? <ReplyBody room={room} eventID={replyTo}/> : null}
|
||||
? <ReplyIDBody room={room} eventID={replyTo}/> : null}
|
||||
<BodyType room={room} sender={memberEvt} event={evt}/>
|
||||
{evt.reactions ? <EventReactions reactions={evt.reactions}/> : null}
|
||||
</div>
|
||||
|
|
Loading…
Add table
Reference in a new issue