mirror of
https://github.com/tulir/gomuks.git
synced 2025-04-19 18:13:41 -05:00
web: support rendering edits and refactor timeline updates
This commit is contained in:
parent
757c1b444e
commit
821701dec6
13 changed files with 229 additions and 55 deletions
2
go.mod
2
go.mod
|
@ -13,7 +13,7 @@ require (
|
|||
golang.org/x/crypto v0.27.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
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 (
|
||||
|
|
4
go.sum
4
go.sum
|
@ -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=
|
||||
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/mautrix v0.21.1-0.20241010172818-50f4a2eec193 h1:WK4mjzzDZ+9cEthJHyc5h7yAYYK/syUPJDYjkn90ujs=
|
||||
maunium.net/go/mautrix v0.21.1-0.20241010172818-50f4a2eec193/go.mod h1:+fF5qsmXRCEXQZgW5ececC0PI3c7gISHTLcyftP4Bh0=
|
||||
maunium.net/go/mautrix v0.21.1-0.20241012091419-9e796dd66c0d h1:wiiX3GSf8/6o3HIfjlgxdZTq3EiXQ6sgeXm9rLkGGDU=
|
||||
maunium.net/go/mautrix v0.21.1-0.20241012091419-9e796dd66c0d/go.mod h1:+fF5qsmXRCEXQZgW5ececC0PI3c7gISHTLcyftP4Bh0=
|
||||
|
|
|
@ -70,11 +70,19 @@ export default class Client {
|
|||
if (!room) {
|
||||
throw new Error("Room not found")
|
||||
}
|
||||
const oldestRowID = room.timeline.current[0]?.timeline_rowid
|
||||
if (room.paginating) {
|
||||
return
|
||||
}
|
||||
room.paginating = true
|
||||
try {
|
||||
const oldestRowID = room.timeline[0]?.timeline_rowid
|
||||
const resp = await this.rpc.paginate(roomID, oldestRowID ?? 0, 100)
|
||||
if (room.timeline.current[0]?.timeline_rowid !== oldestRowID) {
|
||||
if (room.timeline[0]?.timeline_rowid !== oldestRowID) {
|
||||
throw new Error("Timeline changed while loading history")
|
||||
}
|
||||
room.applyPagination(resp.events)
|
||||
} finally {
|
||||
room.paginating = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,6 +14,8 @@
|
|||
// 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 { UserID } from "./types"
|
||||
|
||||
const mediaRegex = /^mxc:\/\/([a-zA-Z0-9.:-]+)\/([a-zA-Z0-9_-]+)$/
|
||||
|
||||
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]}`
|
||||
}
|
||||
|
||||
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]}`
|
||||
}
|
||||
|
|
|
@ -13,6 +13,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 { useSyncExternalStore } from "react"
|
||||
import { NonNullCachedEventDispatcher } from "../util/eventdispatcher.ts"
|
||||
import type {
|
||||
ContentURI,
|
||||
|
@ -62,20 +63,51 @@ function visibleMetaIsEqual(meta1: DBRoom, meta2: DBRoom): boolean {
|
|||
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 {
|
||||
readonly roomID: RoomID
|
||||
readonly meta: NonNullCachedEventDispatcher<DBRoom>
|
||||
readonly timeline = new NonNullCachedEventDispatcher<TimelineRowTuple[]>([])
|
||||
timeline: TimelineRowTuple[] = []
|
||||
timelineCache: (MemDBEvent | null)[] = []
|
||||
state: Map<EventType, Map<string, EventRowID>> = new Map()
|
||||
stateLoaded = false
|
||||
readonly eventsByRowID: Map<EventRowID, MemDBEvent> = new Map()
|
||||
readonly eventsByID: Map<EventID, MemDBEvent> = new Map()
|
||||
readonly timelineSubscribers: Set<() => void> = new Set()
|
||||
paginating = false
|
||||
|
||||
constructor(meta: DBRoom) {
|
||||
this.roomID = meta.room_id
|
||||
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 {
|
||||
const rowID = this.state.get(type)?.get(stateKey)
|
||||
if (!rowID) {
|
||||
|
@ -91,7 +123,8 @@ export class RoomStateStore {
|
|||
this.applyEvent(evt)
|
||||
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) {
|
||||
|
@ -104,6 +137,13 @@ export class RoomStateStore {
|
|||
}
|
||||
delete evt.decrypted
|
||||
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.eventsByID.set(memEvt.event_id, memEvt)
|
||||
}
|
||||
|
@ -128,20 +168,21 @@ export class RoomStateStore {
|
|||
}
|
||||
}
|
||||
if (sync.reset) {
|
||||
this.timeline.emit(sync.timeline)
|
||||
this.timeline = sync.timeline
|
||||
} else {
|
||||
this.timeline.emit([...this.timeline.current, ...sync.timeline])
|
||||
this.timeline.push(...sync.timeline)
|
||||
}
|
||||
this.notifyTimelineSubscribers()
|
||||
}
|
||||
|
||||
applyDecrypted(decrypted: EventsDecryptedData) {
|
||||
let timelineChanged = false
|
||||
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)
|
||||
}
|
||||
if (timelineChanged) {
|
||||
this.timeline.emit([...this.timeline.current])
|
||||
this.notifyTimelineSubscribers()
|
||||
}
|
||||
if (decrypted.preview_event_rowid) {
|
||||
this.meta.current.preview_event_rowid = decrypted.preview_event_rowid
|
||||
|
|
|
@ -62,6 +62,9 @@ export interface DBRoom {
|
|||
prev_batch: string
|
||||
}
|
||||
|
||||
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type UnknownEventContent = Record<string, any>
|
||||
|
||||
export interface BaseDBEvent {
|
||||
rowid: EventRowID
|
||||
timeline_rowid: TimelineRowID
|
||||
|
@ -73,8 +76,7 @@ export interface BaseDBEvent {
|
|||
state_key?: string
|
||||
timestamp: number
|
||||
|
||||
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
content: Record<string, any>
|
||||
content: UnknownEventContent
|
||||
unsigned: EventUnsigned
|
||||
|
||||
transaction_id?: string
|
||||
|
@ -90,14 +92,15 @@ export interface BaseDBEvent {
|
|||
}
|
||||
|
||||
export interface RawDBEvent extends BaseDBEvent {
|
||||
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
decrypted?: Record<string, any>
|
||||
decrypted?: UnknownEventContent
|
||||
decrypted_type?: EventType
|
||||
}
|
||||
|
||||
export interface MemDBEvent extends BaseDBEvent {
|
||||
mem: true
|
||||
encrypted?: EncryptedEventContent
|
||||
orig_content?: UnknownEventContent
|
||||
last_edit?: MemDBEvent
|
||||
}
|
||||
|
||||
export interface DBAccountData {
|
||||
|
|
|
@ -7,7 +7,7 @@ div.timeline-event {
|
|||
grid-template:
|
||||
"avatar gap sender" auto
|
||||
"avatar gap content" auto
|
||||
/ 40px .25rem 1fr;
|
||||
/ 2.5rem .25rem 1fr;
|
||||
|
||||
> div.sender-avatar {
|
||||
grid-area: avatar;
|
||||
|
@ -29,13 +29,13 @@ div.timeline-event {
|
|||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .5rem;
|
||||
gap: .25rem;
|
||||
|
||||
span.event-sender {
|
||||
> span.event-sender {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
span.event-time {
|
||||
> span.event-time, > span.event-edited {
|
||||
font-size: .8rem;
|
||||
}
|
||||
}
|
||||
|
@ -44,6 +44,32 @@ div.timeline-event {
|
|||
grid-area: content;
|
||||
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 {
|
||||
|
|
|
@ -14,27 +14,37 @@
|
|||
// 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 React from "react"
|
||||
import { getMediaURL } from "../../api/media.ts"
|
||||
import { getAvatarURL } from "../../api/media.ts"
|
||||
import { RoomStateStore } from "../../api/statestore.ts"
|
||||
import { MemDBEvent, MemberEventContent } from "../../api/types"
|
||||
import EncryptedBody from "./content/EncryptedBody.tsx"
|
||||
import HiddenEvent from "./content/HiddenEvent.tsx"
|
||||
import MessageBody from "./content/MessageBody.tsx"
|
||||
import RedactedBody from "./content/RedactedBody.tsx"
|
||||
import { EventContentProps } from "./content/props.ts"
|
||||
import "./TimelineEvent.css"
|
||||
|
||||
export interface TimelineEventProps {
|
||||
room: RoomStateStore
|
||||
eventRowID: number
|
||||
evt: MemDBEvent
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
switch (evt.type) {
|
||||
case "m.room.message":
|
||||
case "m.sticker":
|
||||
if (evt.redacted_by) {
|
||||
return RedactedBody
|
||||
}
|
||||
return MessageBody
|
||||
case "m.room.encrypted":
|
||||
if (evt.redacted_by) {
|
||||
return RedactedBody
|
||||
}
|
||||
return EncryptedBody
|
||||
}
|
||||
return HiddenEvent
|
||||
}
|
||||
|
@ -43,30 +53,34 @@ const fullTimeFormatter = new Intl.DateTimeFormat("en-GB", { dateStyle: "full",
|
|||
const formatShortTime = (time: Date) =>
|
||||
`${time.getHours().toString().padStart(2, "0")}:${time.getMinutes().toString().padStart(2, "0")}`
|
||||
|
||||
const TimelineEvent = ({ room, eventRowID }: TimelineEventProps) => {
|
||||
const evt = room.eventsByRowID.get(eventRowID)
|
||||
if (!evt) {
|
||||
return null
|
||||
const EventReactions = ({ reactions }: { reactions: Record<string, number> }) => {
|
||||
return <div className="event-reactions">
|
||||
{Object.entries(reactions).map(([reaction, count]) => <span key={reaction} className="reaction">
|
||||
{reaction} {count}
|
||||
</span>)}
|
||||
</div>
|
||||
}
|
||||
|
||||
const TimelineEvent = ({ room, evt }: TimelineEventProps) => {
|
||||
const memberEvt = room.getStateEvent("m.room.member", evt.sender)
|
||||
const memberEvtContent = memberEvt?.content as MemberEventContent | undefined
|
||||
const BodyType = getBodyType(evt)
|
||||
// if (BodyType === HiddenEvent) {
|
||||
// return <div className="timeline-event">
|
||||
// <BodyType room={room} event={evt}/>
|
||||
// </div>
|
||||
// }
|
||||
const eventTS = new Date(evt.timestamp)
|
||||
return <div className="timeline-event">
|
||||
<div className="sender-avatar">
|
||||
<img loading="lazy" src={getMediaURL(memberEvtContent?.avatar_url)} alt="" />
|
||||
const editEventTS = evt.last_edit ? new Date(evt.last_edit.timestamp) : null
|
||||
return <div className={`timeline-event ${BodyType === HiddenEvent ? "hidden-event" : ""}`}>
|
||||
<div className="sender-avatar" title={evt.sender}>
|
||||
<img loading="lazy" src={getAvatarURL(evt.sender, memberEvtContent?.avatar_url)} alt="" />
|
||||
</div>
|
||||
<div className="event-sender-and-time">
|
||||
<span className="event-sender">{memberEvtContent?.displayname ?? evt.sender}</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 className="event-content">
|
||||
<BodyType room={room} event={evt}/>
|
||||
{evt.reactions ? <EventReactions reactions={evt.reactions}/> : null}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
|
|
@ -14,8 +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 { use, useCallback, useEffect, useRef } from "react"
|
||||
import { RoomStateStore } from "../../api/statestore.ts"
|
||||
import { useNonNullEventAsState } from "../../util/eventdispatcher.ts"
|
||||
import { RoomStateStore, useRoomTimeline } from "../../api/statestore.ts"
|
||||
import { ClientContext } from "../ClientContext.ts"
|
||||
import TimelineEvent from "./TimelineEvent.tsx"
|
||||
import "./TimelineView.css"
|
||||
|
@ -25,15 +24,17 @@ interface TimelineViewProps {
|
|||
}
|
||||
|
||||
const TimelineView = ({ room }: TimelineViewProps) => {
|
||||
const timeline = useNonNullEventAsState(room.timeline)
|
||||
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.roomID])
|
||||
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)
|
||||
|
||||
|
@ -54,21 +55,40 @@ const TimelineView = ({ room }: TimelineViewProps) => {
|
|||
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) {
|
||||
} 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
|
||||
}, [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}>
|
||||
<div className="timeline-beginning">
|
||||
<button onClick={loadHistory}>Load history</button>
|
||||
</div>
|
||||
<div className="timeline-list">
|
||||
{timeline.map(entry => <TimelineEvent
|
||||
key={entry.event_rowid} room={room} eventRowID={entry.event_rowid}
|
||||
/>)}
|
||||
<div className="timeline-top-ref" ref={topRef}/>
|
||||
{timeline.map(entry => entry ? <TimelineEvent
|
||||
key={entry.rowid} room={room} evt={entry}
|
||||
/> : null)}
|
||||
<div className="timeline-bottom-ref" ref={bottomRef}/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
25
web/src/ui/timeline/content/EncryptedBody.tsx
Normal file
25
web/src/ui/timeline/content/EncryptedBody.tsx
Normal 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
|
|
@ -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
|
||||
|
|
20
web/src/ui/timeline/content/RedactedBody.tsx
Normal file
20
web/src/ui/timeline/content/RedactedBody.tsx
Normal 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
|
|
@ -252,10 +252,9 @@ func (gmx *Gomuks) sendInitialData(ctx context.Context, conn *websocket.Conn) {
|
|||
if err != nil {
|
||||
log.Err(err).Msg("Failed to get preview event for room")
|
||||
return
|
||||
} else if previewEvent != nil {
|
||||
syncRoom.Events = append(syncRoom.Events, previewEvent)
|
||||
}
|
||||
if previewEvent != nil && previewEvent.LastEditRowID != nil {
|
||||
if previewEvent != nil {
|
||||
if previewEvent.LastEditRowID != nil {
|
||||
lastEdit, err := gmx.Client.DB.Event.GetByRowID(ctx, *previewEvent.LastEditRowID)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Failed to get last edit for preview event")
|
||||
|
@ -264,6 +263,8 @@ func (gmx *Gomuks) sendInitialData(ctx context.Context, conn *websocket.Conn) {
|
|||
syncRoom.Events = append(syncRoom.Events, lastEdit)
|
||||
}
|
||||
}
|
||||
syncRoom.Events = append(syncRoom.Events, previewEvent)
|
||||
}
|
||||
}
|
||||
}
|
||||
marshaledPayload, err := json.Marshal(&payload)
|
||||
|
|
Loading…
Add table
Reference in a new issue