1
0
Fork 0
forked from Mirrors/gomuks

web: support rendering edits and refactor timeline updates

This commit is contained in:
Tulir Asokan 2024-10-12 13:05:45 +03:00
parent 757c1b444e
commit 821701dec6
13 changed files with 229 additions and 55 deletions

2
go.mod
View file

@ -13,7 +13,7 @@ require (
golang.org/x/crypto v0.27.0 golang.org/x/crypto v0.27.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
maunium.net/go/mauflag v1.0.0 maunium.net/go/mauflag v1.0.0
maunium.net/go/mautrix v0.21.1-0.20241010172818-50f4a2eec193 maunium.net/go/mautrix v0.21.1-0.20241012091419-9e796dd66c0d
) )
require ( require (

4
go.sum
View file

@ -60,5 +60,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M= maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M=
maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA= maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA=
maunium.net/go/mautrix v0.21.1-0.20241010172818-50f4a2eec193 h1:WK4mjzzDZ+9cEthJHyc5h7yAYYK/syUPJDYjkn90ujs= maunium.net/go/mautrix v0.21.1-0.20241012091419-9e796dd66c0d h1:wiiX3GSf8/6o3HIfjlgxdZTq3EiXQ6sgeXm9rLkGGDU=
maunium.net/go/mautrix v0.21.1-0.20241010172818-50f4a2eec193/go.mod h1:+fF5qsmXRCEXQZgW5ececC0PI3c7gISHTLcyftP4Bh0= maunium.net/go/mautrix v0.21.1-0.20241012091419-9e796dd66c0d/go.mod h1:+fF5qsmXRCEXQZgW5ececC0PI3c7gISHTLcyftP4Bh0=

View file

@ -70,11 +70,19 @@ export default class Client {
if (!room) { if (!room) {
throw new Error("Room not found") throw new Error("Room not found")
} }
const oldestRowID = room.timeline.current[0]?.timeline_rowid if (room.paginating) {
const resp = await this.rpc.paginate(roomID, oldestRowID ?? 0, 100) return
if (room.timeline.current[0]?.timeline_rowid !== oldestRowID) { }
throw new Error("Timeline changed while loading history") room.paginating = true
try {
const oldestRowID = room.timeline[0]?.timeline_rowid
const resp = await this.rpc.paginate(roomID, oldestRowID ?? 0, 100)
if (room.timeline[0]?.timeline_rowid !== oldestRowID) {
throw new Error("Timeline changed while loading history")
}
room.applyPagination(resp.events)
} finally {
room.paginating = false
} }
room.applyPagination(resp.events)
} }
} }

View file

@ -14,6 +14,8 @@
// 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 { UserID } from "./types"
const mediaRegex = /^mxc:\/\/([a-zA-Z0-9.:-]+)\/([a-zA-Z0-9_-]+)$/ const mediaRegex = /^mxc:\/\/([a-zA-Z0-9.:-]+)\/([a-zA-Z0-9_-]+)$/
export const getMediaURL = (mxc?: string): string | undefined => { export const getMediaURL = (mxc?: string): string | undefined => {
@ -26,3 +28,17 @@ export const getMediaURL = (mxc?: string): string | undefined => {
} }
return `_gomuks/media/${match[1]}/${match[2]}` return `_gomuks/media/${match[1]}/${match[2]}`
} }
export const getAvatarURL = (_userID: UserID, mxc?: string): string | undefined => {
if (!mxc) {
return undefined
// return `_gomuks/avatar/${encodeURIComponent(userID)}`
}
const match = mxc.match(mediaRegex)
if (!match) {
return undefined
// return `_gomuks/avatar/${encodeURIComponent(userID)}`
}
return `_gomuks/media/${match[1]}/${match[2]}`
// return `_gomuks/avatar/${encodeURIComponent(userID)}/${match[1]}/${match[2]}`
}

View file

@ -13,6 +13,7 @@
// //
// 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 { NonNullCachedEventDispatcher } from "../util/eventdispatcher.ts" import { NonNullCachedEventDispatcher } from "../util/eventdispatcher.ts"
import type { import type {
ContentURI, ContentURI,
@ -62,20 +63,51 @@ function visibleMetaIsEqual(meta1: DBRoom, meta2: DBRoom): boolean {
meta1.has_member_list === meta2.has_member_list meta1.has_member_list === meta2.has_member_list
} }
export function useRoomTimeline(room: RoomStateStore): (MemDBEvent | null)[] {
return useSyncExternalStore(
room.subscribeTimeline,
() => room.timelineCache,
)
}
type SubscribeFunc = (callback: () => void) => () => void
export class RoomStateStore { export class RoomStateStore {
readonly roomID: RoomID readonly roomID: RoomID
readonly meta: NonNullCachedEventDispatcher<DBRoom> readonly meta: NonNullCachedEventDispatcher<DBRoom>
readonly timeline = new NonNullCachedEventDispatcher<TimelineRowTuple[]>([]) timeline: TimelineRowTuple[] = []
timelineCache: (MemDBEvent | null)[] = []
state: Map<EventType, Map<string, EventRowID>> = new Map() state: Map<EventType, Map<string, EventRowID>> = new Map()
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()
paginating = false
constructor(meta: DBRoom) { constructor(meta: DBRoom) {
this.roomID = meta.room_id this.roomID = meta.room_id
this.meta = new NonNullCachedEventDispatcher(meta) 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)
if (!evt) {
return null
}
evt.timeline_rowid = rt.timeline_rowid
return evt
})
for (const sub of this.timelineSubscribers) {
sub()
}
}
getStateEvent(type: EventType, stateKey: string): MemDBEvent | undefined { getStateEvent(type: EventType, stateKey: string): MemDBEvent | undefined {
const rowID = this.state.get(type)?.get(stateKey) const rowID = this.state.get(type)?.get(stateKey)
if (!rowID) { if (!rowID) {
@ -91,7 +123,8 @@ export class RoomStateStore {
this.applyEvent(evt) this.applyEvent(evt)
return { timeline_rowid: evt.timeline_rowid, event_rowid: evt.rowid } return { timeline_rowid: evt.timeline_rowid, event_rowid: evt.rowid }
}) })
this.timeline.emit([...newTimeline, ...this.timeline.current]) this.timeline.splice(0, 0, ...newTimeline)
this.notifyTimelineSubscribers()
} }
applyEvent(evt: RawDBEvent) { applyEvent(evt: RawDBEvent) {
@ -104,6 +137,13 @@ export class RoomStateStore {
} }
delete evt.decrypted delete evt.decrypted
delete evt.decrypted_type delete evt.decrypted_type
if (memEvt.last_edit_rowid) {
memEvt.last_edit = this.eventsByRowID.get(memEvt.last_edit_rowid)
if (memEvt.last_edit) {
memEvt.orig_content = memEvt.content
memEvt.content = memEvt.last_edit.content["m.new_content"]
}
}
this.eventsByRowID.set(memEvt.rowid, memEvt) this.eventsByRowID.set(memEvt.rowid, memEvt)
this.eventsByID.set(memEvt.event_id, memEvt) this.eventsByID.set(memEvt.event_id, memEvt)
} }
@ -128,20 +168,21 @@ export class RoomStateStore {
} }
} }
if (sync.reset) { if (sync.reset) {
this.timeline.emit(sync.timeline) this.timeline = sync.timeline
} else { } else {
this.timeline.emit([...this.timeline.current, ...sync.timeline]) this.timeline.push(...sync.timeline)
} }
this.notifyTimelineSubscribers()
} }
applyDecrypted(decrypted: EventsDecryptedData) { applyDecrypted(decrypted: EventsDecryptedData) {
let timelineChanged = false let timelineChanged = false
for (const evt of decrypted.events) { for (const evt of decrypted.events) {
timelineChanged = timelineChanged || !!this.timeline.current.find(rt => rt.event_rowid === evt.rowid) timelineChanged = timelineChanged || !!this.timeline.find(rt => rt.event_rowid === evt.rowid)
this.applyEvent(evt) this.applyEvent(evt)
} }
if (timelineChanged) { if (timelineChanged) {
this.timeline.emit([...this.timeline.current]) this.notifyTimelineSubscribers()
} }
if (decrypted.preview_event_rowid) { if (decrypted.preview_event_rowid) {
this.meta.current.preview_event_rowid = decrypted.preview_event_rowid this.meta.current.preview_event_rowid = decrypted.preview_event_rowid

View file

@ -62,6 +62,9 @@ export interface DBRoom {
prev_batch: string prev_batch: string
} }
//eslint-disable-next-line @typescript-eslint/no-explicit-any
export type UnknownEventContent = Record<string, any>
export interface BaseDBEvent { export interface BaseDBEvent {
rowid: EventRowID rowid: EventRowID
timeline_rowid: TimelineRowID timeline_rowid: TimelineRowID
@ -73,8 +76,7 @@ export interface BaseDBEvent {
state_key?: string state_key?: string
timestamp: number timestamp: number
//eslint-disable-next-line @typescript-eslint/no-explicit-any content: UnknownEventContent
content: Record<string, any>
unsigned: EventUnsigned unsigned: EventUnsigned
transaction_id?: string transaction_id?: string
@ -90,14 +92,15 @@ export interface BaseDBEvent {
} }
export interface RawDBEvent extends BaseDBEvent { export interface RawDBEvent extends BaseDBEvent {
//eslint-disable-next-line @typescript-eslint/no-explicit-any decrypted?: UnknownEventContent
decrypted?: Record<string, any>
decrypted_type?: EventType decrypted_type?: EventType
} }
export interface MemDBEvent extends BaseDBEvent { export interface MemDBEvent extends BaseDBEvent {
mem: true mem: true
encrypted?: EncryptedEventContent encrypted?: EncryptedEventContent
orig_content?: UnknownEventContent
last_edit?: MemDBEvent
} }
export interface DBAccountData { export interface DBAccountData {

View file

@ -7,7 +7,7 @@ div.timeline-event {
grid-template: grid-template:
"avatar gap sender" auto "avatar gap sender" auto
"avatar gap content" auto "avatar gap content" auto
/ 40px .25rem 1fr; / 2.5rem .25rem 1fr;
> div.sender-avatar { > div.sender-avatar {
grid-area: avatar; grid-area: avatar;
@ -29,13 +29,13 @@ div.timeline-event {
display: flex; display: flex;
align-items: center; align-items: center;
gap: .5rem; gap: .25rem;
span.event-sender { > span.event-sender {
font-weight: bold; font-weight: bold;
} }
span.event-time { > span.event-time, > span.event-edited {
font-size: .8rem; font-size: .8rem;
} }
} }
@ -44,6 +44,32 @@ div.timeline-event {
grid-area: content; grid-area: content;
overflow: hidden; overflow: hidden;
} }
&.hidden-event {
grid-template:
"sender avatar content" auto
/ 2.5rem 1.5rem 1fr;
margin-top: 0;
> div.sender-avatar {
margin-top: 0;
width: 1.5rem;
height: 1.5rem;
display: flex;
align-items: center;
> img {
width: 1rem;
height: 1rem;
}
}
> div.event-sender-and-time {
> span.event-sender, > span.event-edited {
display: none;
}
}
}
} }
div.html-body { div.html-body {

View file

@ -14,27 +14,37 @@
// 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 React from "react" import React from "react"
import { getMediaURL } from "../../api/media.ts" import { getAvatarURL } from "../../api/media.ts"
import { RoomStateStore } from "../../api/statestore.ts" import { RoomStateStore } from "../../api/statestore.ts"
import { MemDBEvent, MemberEventContent } from "../../api/types" import { MemDBEvent, MemberEventContent } from "../../api/types"
import EncryptedBody from "./content/EncryptedBody.tsx"
import HiddenEvent from "./content/HiddenEvent.tsx" import HiddenEvent from "./content/HiddenEvent.tsx"
import MessageBody from "./content/MessageBody.tsx" import MessageBody from "./content/MessageBody.tsx"
import RedactedBody from "./content/RedactedBody.tsx"
import { EventContentProps } from "./content/props.ts" import { EventContentProps } from "./content/props.ts"
import "./TimelineEvent.css" import "./TimelineEvent.css"
export interface TimelineEventProps { export interface TimelineEventProps {
room: RoomStateStore room: RoomStateStore
eventRowID: number evt: MemDBEvent
} }
function getBodyType(evt: MemDBEvent): React.FunctionComponent<EventContentProps> { function getBodyType(evt: MemDBEvent): React.FunctionComponent<EventContentProps> {
if (evt.content["m.relates_to"]?.relation_type === "m.replace") { if (evt.relation_type === "m.replace") {
return HiddenEvent return HiddenEvent
} }
switch (evt.type) { switch (evt.type) {
case "m.room.message": case "m.room.message":
case "m.sticker": case "m.sticker":
if (evt.redacted_by) {
return RedactedBody
}
return MessageBody return MessageBody
case "m.room.encrypted":
if (evt.redacted_by) {
return RedactedBody
}
return EncryptedBody
} }
return HiddenEvent return HiddenEvent
} }
@ -43,30 +53,34 @@ const fullTimeFormatter = new Intl.DateTimeFormat("en-GB", { dateStyle: "full",
const formatShortTime = (time: Date) => const formatShortTime = (time: Date) =>
`${time.getHours().toString().padStart(2, "0")}:${time.getMinutes().toString().padStart(2, "0")}` `${time.getHours().toString().padStart(2, "0")}:${time.getMinutes().toString().padStart(2, "0")}`
const TimelineEvent = ({ room, eventRowID }: TimelineEventProps) => { const EventReactions = ({ reactions }: { reactions: Record<string, number> }) => {
const evt = room.eventsByRowID.get(eventRowID) return <div className="event-reactions">
if (!evt) { {Object.entries(reactions).map(([reaction, count]) => <span key={reaction} className="reaction">
return null {reaction} {count}
} </span>)}
</div>
}
const TimelineEvent = ({ room, evt }: TimelineEventProps) => {
const memberEvt = room.getStateEvent("m.room.member", evt.sender) const memberEvt = room.getStateEvent("m.room.member", evt.sender)
const memberEvtContent = memberEvt?.content as MemberEventContent | undefined const memberEvtContent = memberEvt?.content as MemberEventContent | undefined
const BodyType = getBodyType(evt) const BodyType = getBodyType(evt)
// if (BodyType === HiddenEvent) {
// return <div className="timeline-event">
// <BodyType room={room} event={evt}/>
// </div>
// }
const eventTS = new Date(evt.timestamp) const eventTS = new Date(evt.timestamp)
return <div className="timeline-event"> const editEventTS = evt.last_edit ? new Date(evt.last_edit.timestamp) : null
<div className="sender-avatar"> return <div className={`timeline-event ${BodyType === HiddenEvent ? "hidden-event" : ""}`}>
<img loading="lazy" src={getMediaURL(memberEvtContent?.avatar_url)} alt="" /> <div className="sender-avatar" title={evt.sender}>
<img loading="lazy" src={getAvatarURL(evt.sender, memberEvtContent?.avatar_url)} alt="" />
</div> </div>
<div className="event-sender-and-time"> <div className="event-sender-and-time">
<span className="event-sender">{memberEvtContent?.displayname ?? evt.sender}</span> <span className="event-sender">{memberEvtContent?.displayname ?? evt.sender}</span>
<span className="event-time" title={fullTimeFormatter.format(eventTS)}>{formatShortTime(eventTS)}</span> <span className="event-time" title={fullTimeFormatter.format(eventTS)}>{formatShortTime(eventTS)}</span>
{editEventTS ? <span className="event-edited" title={`Edited at ${fullTimeFormatter.format(editEventTS)}`}>
(edited at {formatShortTime(editEventTS)})
</span> : null}
</div> </div>
<div className="event-content"> <div className="event-content">
<BodyType room={room} event={evt}/> <BodyType room={room} event={evt}/>
{evt.reactions ? <EventReactions reactions={evt.reactions}/> : null}
</div> </div>
</div> </div>
} }

View file

@ -14,8 +14,7 @@
// 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, useCallback, useEffect, useRef } from "react" import { use, useCallback, useEffect, useRef } from "react"
import { RoomStateStore } from "../../api/statestore.ts" import { RoomStateStore, useRoomTimeline } from "../../api/statestore.ts"
import { useNonNullEventAsState } from "../../util/eventdispatcher.ts"
import { ClientContext } from "../ClientContext.ts" import { ClientContext } from "../ClientContext.ts"
import TimelineEvent from "./TimelineEvent.tsx" import TimelineEvent from "./TimelineEvent.tsx"
import "./TimelineView.css" import "./TimelineView.css"
@ -25,15 +24,17 @@ interface TimelineViewProps {
} }
const TimelineView = ({ room }: TimelineViewProps) => { const TimelineView = ({ room }: TimelineViewProps) => {
const timeline = useNonNullEventAsState(room.timeline) const timeline = useRoomTimeline(room)
const client = use(ClientContext)! const client = use(ClientContext)!
const loadHistory = useCallback(() => { const loadHistory = useCallback(() => {
client.loadMoreHistory(room.roomID) client.loadMoreHistory(room.roomID)
.catch(err => console.error("Failed to load history", err)) .catch(err => console.error("Failed to load history", err))
}, [client, room.roomID]) }, [client, room.roomID])
const bottomRef = useRef<HTMLDivElement>(null) const bottomRef = useRef<HTMLDivElement>(null)
const topRef = useRef<HTMLDivElement>(null)
const timelineViewRef = useRef<HTMLDivElement>(null) const timelineViewRef = useRef<HTMLDivElement>(null)
const prevOldestTimelineRow = useRef(0) const prevOldestTimelineRow = useRef(0)
const paginationRequestedForRow = useRef(-1)
const oldScrollHeight = useRef(0) const oldScrollHeight = useRef(0)
const scrolledToBottom = useRef(true) const scrolledToBottom = useRef(true)
@ -54,21 +55,40 @@ const TimelineView = ({ room }: TimelineViewProps) => {
if (bottomRef.current && scrolledToBottom.current) { if (bottomRef.current && scrolledToBottom.current) {
// For any timeline changes, if we were at the bottom, scroll to the new bottom // For any timeline changes, if we were at the bottom, scroll to the new bottom
bottomRef.current.scrollIntoView() bottomRef.current.scrollIntoView()
} else if (timelineViewRef.current && prevOldestTimelineRow.current > timeline[0]?.timeline_rowid) { } 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 // 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 timelineViewRef.current.scrollTop += timelineViewRef.current.scrollHeight - oldScrollHeight.current
} }
prevOldestTimelineRow.current = timeline[0]?.timeline_rowid ?? 0 prevOldestTimelineRow.current = timeline[0]?.timeline_rowid ?? 0
}, [timeline]) }, [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
loadHistory()
}
}, {
root: topElem.parentElement!.parentElement,
rootMargin: "0px",
threshold: 1.0,
})
observer.observe(topElem)
return () => observer.unobserve(topElem)
}, [loadHistory, topRef])
return <div className="timeline-view" onScroll={handleScroll} ref={timelineViewRef}> return <div className="timeline-view" onScroll={handleScroll} ref={timelineViewRef}>
<div className="timeline-beginning"> <div className="timeline-beginning">
<button onClick={loadHistory}>Load history</button> <button onClick={loadHistory}>Load history</button>
</div> </div>
<div className="timeline-list"> <div className="timeline-list">
{timeline.map(entry => <TimelineEvent <div className="timeline-top-ref" ref={topRef}/>
key={entry.event_rowid} room={room} eventRowID={entry.event_rowid} {timeline.map(entry => entry ? <TimelineEvent
/>)} key={entry.rowid} room={room} evt={entry}
/> : null)}
<div className="timeline-bottom-ref" ref={bottomRef}/> <div className="timeline-bottom-ref" ref={bottomRef}/>
</div> </div>
</div> </div>

View file

@ -0,0 +1,25 @@
// 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 { EventContentProps } from "./props.ts"
const EncryptedBody = ({ event }: EventContentProps) => {
if (event.decryption_error) {
return `Failed to decrypt: ${event.decryption_error}`
}
return `Waiting for message`
}
export default EncryptedBody

View file

@ -104,7 +104,7 @@ const MessageBody = ({ event }: EventContentProps) => {
</> </>
} }
} }
return <code>{`{ "type": "${event.type}" }`}</code> return <code>{`{ "type": "${event.type}", "content": { "msgtype": "${content.msgtype}" } }`}</code>
} }
export default MessageBody export default MessageBody

View file

@ -0,0 +1,20 @@
// 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/>.
const RedactedBody = () => {
return `Message deleted`
}
export default RedactedBody

View file

@ -252,17 +252,18 @@ func (gmx *Gomuks) sendInitialData(ctx context.Context, conn *websocket.Conn) {
if err != nil { if err != nil {
log.Err(err).Msg("Failed to get preview event for room") log.Err(err).Msg("Failed to get preview event for room")
return return
} else if previewEvent != nil {
syncRoom.Events = append(syncRoom.Events, previewEvent)
} }
if previewEvent != nil && previewEvent.LastEditRowID != nil { if previewEvent != nil {
lastEdit, err := gmx.Client.DB.Event.GetByRowID(ctx, *previewEvent.LastEditRowID) if previewEvent.LastEditRowID != nil {
if err != nil { lastEdit, err := gmx.Client.DB.Event.GetByRowID(ctx, *previewEvent.LastEditRowID)
log.Err(err).Msg("Failed to get last edit for preview event") if err != nil {
return log.Err(err).Msg("Failed to get last edit for preview event")
} else if lastEdit != nil { return
syncRoom.Events = append(syncRoom.Events, lastEdit) } else if lastEdit != nil {
syncRoom.Events = append(syncRoom.Events, lastEdit)
}
} }
syncRoom.Events = append(syncRoom.Events, previewEvent)
} }
} }
} }