mirror of
https://github.com/tulir/gomuks.git
synced 2025-04-20 10:33: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/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
import { CachedEventDispatcher } from "../util/eventdispatcher.ts"
|
import { CachedEventDispatcher } from "../util/eventdispatcher.ts"
|
||||||
import RPCClient, { SendMessageParams } from "./rpc.ts"
|
import RPCClient, { SendMessageParams } from "./rpc.ts"
|
||||||
import { StateStore } from "./statestore"
|
import { RoomStateStore, StateStore } from "./statestore"
|
||||||
import type {
|
import type { ClientState, EventID, EventRowID, EventType, RPCEvent, RoomID, UserID } from "./types"
|
||||||
ClientState,
|
|
||||||
EventRowID,
|
|
||||||
EventType,
|
|
||||||
RPCEvent,
|
|
||||||
RoomID,
|
|
||||||
UserID,
|
|
||||||
} from "./types"
|
|
||||||
|
|
||||||
export default class Client {
|
export default class Client {
|
||||||
readonly state = new CachedEventDispatcher<ClientState>()
|
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> {
|
async sendMessage(params: SendMessageParams): Promise<void> {
|
||||||
const room = this.store.rooms.get(params.room_id)
|
const room = this.store.rooms.get(params.room_id)
|
||||||
if (!room) {
|
if (!room) {
|
||||||
|
|
|
@ -14,12 +14,19 @@
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// 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/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
import { useSyncExternalStore } from "react"
|
import { useSyncExternalStore } from "react"
|
||||||
import type { MemDBEvent } from "../types"
|
import type { EventID, MemDBEvent } from "../types"
|
||||||
import { RoomStateStore } from "./room.ts"
|
import { RoomStateStore } from "./room.ts"
|
||||||
|
|
||||||
export function useRoomTimeline(room: RoomStateStore): (MemDBEvent | null)[] {
|
export function useRoomTimeline(room: RoomStateStore): (MemDBEvent | null)[] {
|
||||||
return useSyncExternalStore(
|
return useSyncExternalStore(
|
||||||
room.subscribeTimeline,
|
room.timelineSub.subscribe,
|
||||||
() => room.timelineCache,
|
() => 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
|
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 {
|
export class RoomStateStore {
|
||||||
readonly roomID: RoomID
|
readonly roomID: RoomID
|
||||||
|
@ -71,7 +99,8 @@ export class RoomStateStore {
|
||||||
stateLoaded = false
|
stateLoaded = false
|
||||||
readonly eventsByRowID: Map<EventRowID, MemDBEvent> = new Map()
|
readonly eventsByRowID: Map<EventRowID, MemDBEvent> = new Map()
|
||||||
readonly eventsByID: Map<EventID, 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[] = []
|
readonly pendingEvents: EventRowID[] = []
|
||||||
paginating = false
|
paginating = false
|
||||||
|
|
||||||
|
@ -80,11 +109,6 @@ export class RoomStateStore {
|
||||||
this.meta = new NonNullCachedEventDispatcher(meta)
|
this.meta = new NonNullCachedEventDispatcher(meta)
|
||||||
}
|
}
|
||||||
|
|
||||||
subscribeTimeline: SubscribeFunc = callback => {
|
|
||||||
this.timelineSubscribers.add(callback)
|
|
||||||
return () => this.timelineSubscribers.delete(callback)
|
|
||||||
}
|
|
||||||
|
|
||||||
notifyTimelineSubscribers() {
|
notifyTimelineSubscribers() {
|
||||||
this.timelineCache = this.timeline.map(rt => {
|
this.timelineCache = this.timeline.map(rt => {
|
||||||
const evt = this.eventsByRowID.get(rt.event_rowid)
|
const evt = this.eventsByRowID.get(rt.event_rowid)
|
||||||
|
@ -96,9 +120,16 @@ export class RoomStateStore {
|
||||||
}).concat(this.pendingEvents
|
}).concat(this.pendingEvents
|
||||||
.map(rowID => this.eventsByRowID.get(rowID))
|
.map(rowID => this.eventsByRowID.get(rowID))
|
||||||
.filter(evt => !!evt))
|
.filter(evt => !!evt))
|
||||||
for (const sub of this.timelineSubscribers) {
|
this.timelineSub.notify()
|
||||||
sub()
|
}
|
||||||
|
|
||||||
|
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 {
|
getStateEvent(type: EventType, stateKey: string): MemDBEvent | undefined {
|
||||||
|
@ -149,6 +180,7 @@ export class RoomStateStore {
|
||||||
this.pendingEvents.splice(pendingIdx, 1)
|
this.pendingEvents.splice(pendingIdx, 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
this.eventSubs.get(evt.event_id)?.notify()
|
||||||
}
|
}
|
||||||
|
|
||||||
applySendComplete(evt: RawDBEvent) {
|
applySendComplete(evt: RawDBEvent) {
|
||||||
|
|
|
@ -17,7 +17,7 @@ import React, { use, useCallback, useRef, useState } from "react"
|
||||||
import { RoomStateStore } from "@/api/statestore"
|
import { RoomStateStore } from "@/api/statestore"
|
||||||
import { MemDBEvent, Mentions } from "@/api/types"
|
import { MemDBEvent, Mentions } from "@/api/types"
|
||||||
import { ClientContext } from "./ClientContext.ts"
|
import { ClientContext } from "./ClientContext.ts"
|
||||||
import ReplyBody from "./timeline/ReplyBody.tsx"
|
import { ReplyBody } from "./timeline/ReplyBody.tsx"
|
||||||
import "./MessageComposer.css"
|
import "./MessageComposer.css"
|
||||||
|
|
||||||
interface MessageComposerProps {
|
interface MessageComposerProps {
|
||||||
|
|
|
@ -13,31 +13,39 @@
|
||||||
//
|
//
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// 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/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
import { use } from "react"
|
||||||
import { getAvatarURL } from "@/api/media.ts"
|
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 type { EventID, MemDBEvent, MemberEventContent } from "@/api/types"
|
||||||
|
import { ClientContext } from "../ClientContext.ts"
|
||||||
import { TextMessageBody } from "./content/MessageBody.tsx"
|
import { TextMessageBody } from "./content/MessageBody.tsx"
|
||||||
import CloseButton from "@/icons/close.svg?react"
|
import CloseButton from "@/icons/close.svg?react"
|
||||||
import "./ReplyBody.css"
|
import "./ReplyBody.css"
|
||||||
|
|
||||||
interface BaseReplyBodyProps {
|
interface ReplyBodyProps {
|
||||||
room: RoomStateStore
|
room: RoomStateStore
|
||||||
eventID?: EventID
|
event: MemDBEvent
|
||||||
event?: MemDBEvent
|
|
||||||
onClose?: () => void
|
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) {
|
if (!event) {
|
||||||
event = room.eventsByID.get(eventID!)
|
// This caches whether the event is requested or not, so it doesn't need to be wrapped in an effect.
|
||||||
if (!event) {
|
use(ClientContext)!.requestEvent(room, eventID)
|
||||||
return <blockquote className="reply-body">
|
return <blockquote className="reply-body">
|
||||||
Reply to {eventID}
|
Reply to {eventID}
|
||||||
</blockquote>
|
</blockquote>
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return <ReplyBody room={room} event={event}/>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ReplyBody = ({ room, event, onClose }: ReplyBodyProps) => {
|
||||||
const memberEvt = room.getStateEvent("m.room.member", event.sender)
|
const memberEvt = room.getStateEvent("m.room.member", event.sender)
|
||||||
const memberEvtContent = memberEvt?.content as MemberEventContent | undefined
|
const memberEvtContent = memberEvt?.content as MemberEventContent | undefined
|
||||||
return <blockquote className={`reply-body ${onClose ? "composer" : ""}`}>
|
return <blockquote className={`reply-body ${onClose ? "composer" : ""}`}>
|
||||||
|
@ -56,5 +64,3 @@ const ReplyBody = ({ room, eventID, event, onClose }: ReplyBodyProps) => {
|
||||||
<TextMessageBody room={room} event={event}/>
|
<TextMessageBody room={room} event={event}/>
|
||||||
</blockquote>
|
</blockquote>
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ReplyBody
|
|
||||||
|
|
|
@ -19,7 +19,7 @@ import { RoomStateStore } from "@/api/statestore"
|
||||||
import { MemDBEvent, MemberEventContent } from "@/api/types"
|
import { MemDBEvent, MemberEventContent } from "@/api/types"
|
||||||
import { ClientContext } from "../ClientContext.ts"
|
import { ClientContext } from "../ClientContext.ts"
|
||||||
import { LightboxContext } from "../Lightbox.tsx"
|
import { LightboxContext } from "../Lightbox.tsx"
|
||||||
import ReplyBody from "./ReplyBody.tsx"
|
import { ReplyIDBody } from "./ReplyBody.tsx"
|
||||||
import EncryptedBody from "./content/EncryptedBody.tsx"
|
import EncryptedBody from "./content/EncryptedBody.tsx"
|
||||||
import HiddenEvent from "./content/HiddenEvent.tsx"
|
import HiddenEvent from "./content/HiddenEvent.tsx"
|
||||||
import MemberBody from "./content/MemberBody.tsx"
|
import MemberBody from "./content/MemberBody.tsx"
|
||||||
|
@ -153,7 +153,7 @@ const TimelineEvent = ({ room, evt, prevEvt, setReplyTo }: TimelineEventProps) =
|
||||||
</div>
|
</div>
|
||||||
<div className="event-content">
|
<div className="event-content">
|
||||||
{typeof replyTo === "string" && BodyType !== HiddenEvent
|
{typeof replyTo === "string" && BodyType !== HiddenEvent
|
||||||
? <ReplyBody room={room} eventID={replyTo}/> : null}
|
? <ReplyIDBody room={room} eventID={replyTo}/> : null}
|
||||||
<BodyType room={room} sender={memberEvt} event={evt}/>
|
<BodyType room={room} sender={memberEvt} event={evt}/>
|
||||||
{evt.reactions ? <EventReactions reactions={evt.reactions}/> : null}
|
{evt.reactions ? <EventReactions reactions={evt.reactions}/> : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Add table
Reference in a new issue