Skip to content

Commit a14f01a

Browse files
committed
web/timeline: implement MSC2815
Fixes #510
1 parent 4885dab commit a14f01a

File tree

15 files changed

+89
-22
lines changed

15 files changed

+89
-22
lines changed

desktop/go.mod

+1-1
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ require (
7979
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
8080
gopkg.in/warnings.v0 v0.1.2 // indirect
8181
gopkg.in/yaml.v3 v3.0.1 // indirect
82-
maunium.net/go/mautrix v0.23.1 // indirect
82+
maunium.net/go/mautrix v0.23.2-0.20250223161309-1cc073cde6ca // indirect
8383
mvdan.cc/xurls/v2 v2.6.0 // indirect
8484
)
8585

desktop/go.sum

+2-2
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
261261
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
262262
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
263263
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
264-
maunium.net/go/mautrix v0.23.1 h1:xZtX43YZF3WRxkdR+oMUrpiQe+jbjc+LeXLxHuXP5IM=
265-
maunium.net/go/mautrix v0.23.1/go.mod h1:kldoZQDneV/jquIhwG1MmMw5j2A2M/MnQYRSWt863cY=
264+
maunium.net/go/mautrix v0.23.2-0.20250223161309-1cc073cde6ca h1:xPbRPallD4qh/XuQWheRsvxsf/5stfdA+uIj0S0P2kQ=
265+
maunium.net/go/mautrix v0.23.2-0.20250223161309-1cc073cde6ca/go.mod h1:kldoZQDneV/jquIhwG1MmMw5j2A2M/MnQYRSWt863cY=
266266
mvdan.cc/xurls/v2 v2.6.0 h1:3NTZpeTxYVWNSokW3MKeyVkz/j7uYXYiMtXRUfmjbgI=
267267
mvdan.cc/xurls/v2 v2.6.0/go.mod h1:bCvEZ1XvdA6wDnxY7jPPjEmigDtvtvPXAD/Exa9IMSk=

go.mod

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ require (
2727
golang.org/x/text v0.22.0
2828
gopkg.in/yaml.v3 v3.0.1
2929
maunium.net/go/mauflag v1.0.0
30-
maunium.net/go/mautrix v0.23.1
30+
maunium.net/go/mautrix v0.23.2-0.20250223161309-1cc073cde6ca
3131
mvdan.cc/xurls/v2 v2.6.0
3232
)
3333

go.sum

+2-2
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
100100
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
101101
maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M=
102102
maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA=
103-
maunium.net/go/mautrix v0.23.1 h1:xZtX43YZF3WRxkdR+oMUrpiQe+jbjc+LeXLxHuXP5IM=
104-
maunium.net/go/mautrix v0.23.1/go.mod h1:kldoZQDneV/jquIhwG1MmMw5j2A2M/MnQYRSWt863cY=
103+
maunium.net/go/mautrix v0.23.2-0.20250223161309-1cc073cde6ca h1:xPbRPallD4qh/XuQWheRsvxsf/5stfdA+uIj0S0P2kQ=
104+
maunium.net/go/mautrix v0.23.2-0.20250223161309-1cc073cde6ca/go.mod h1:kldoZQDneV/jquIhwG1MmMw5j2A2M/MnQYRSWt863cY=
105105
mvdan.cc/xurls/v2 v2.6.0 h1:3NTZpeTxYVWNSokW3MKeyVkz/j7uYXYiMtXRUfmjbgI=
106106
mvdan.cc/xurls/v2 v2.6.0/go.mod h1:bCvEZ1XvdA6wDnxY7jPPjEmigDtvtvPXAD/Exa9IMSk=

pkg/hicli/json-commands.go

+6-2
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,9 @@ func (h *HiClient) handleJSONCommand(ctx context.Context, req *JSONCommand) (any
123123
})
124124
case "get_event":
125125
return unmarshalAndCall(req.Data, func(params *getEventParams) (*database.Event, error) {
126+
if params.Unredact {
127+
return h.GetUnredactedEvent(ctx, params.RoomID, params.EventID)
128+
}
126129
return h.GetEvent(ctx, params.RoomID, params.EventID)
127130
})
128131
case "get_related_events":
@@ -311,8 +314,9 @@ type setProfileFieldParams struct {
311314
}
312315

313316
type getEventParams struct {
314-
RoomID id.RoomID `json:"room_id"`
315-
EventID id.EventID `json:"event_id"`
317+
RoomID id.RoomID `json:"room_id"`
318+
EventID id.EventID `json:"event_id"`
319+
Unredact bool `json:"unredact"`
316320
}
317321

318322
type getRelatedEventsParams struct {

pkg/hicli/paginate.go

+20
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,26 @@ func (h *HiClient) GetEvent(ctx context.Context, roomID id.RoomID, eventID id.Ev
3535
}
3636
}
3737

38+
func (h *HiClient) GetUnredactedEvent(ctx context.Context, roomID id.RoomID, eventID id.EventID) (*database.Event, error) {
39+
if evt, err := h.DB.Event.GetByID(ctx, eventID); err != nil {
40+
return nil, fmt.Errorf("failed to get event from database: %w", err)
41+
// TODO this check doesn't handle events which keep some fields on redaction
42+
} else if evt != nil && len(evt.Content) > 2 {
43+
h.ReprocessExistingEvent(ctx, evt)
44+
return evt, nil
45+
} else if serverEvt, err := h.Client.GetUnredactedEventContent(ctx, roomID, eventID); err != nil {
46+
return nil, fmt.Errorf("failed to get event from server: %w", err)
47+
} else if redactedServerEvt, err := h.Client.GetEvent(ctx, roomID, eventID); err != nil {
48+
return nil, fmt.Errorf("failed to get redacted event from server: %w", err)
49+
// TODO this check will have false positives on actually empty events
50+
} else if len(serverEvt.Content.VeryRaw) == 2 {
51+
return nil, fmt.Errorf("server didn't return content")
52+
} else {
53+
serverEvt.Unsigned.RedactedBecause = redactedServerEvt.Unsigned.RedactedBecause
54+
return h.processEvent(ctx, serverEvt, nil, nil, false)
55+
}
56+
}
57+
3858
func (h *HiClient) processGetRoomState(ctx context.Context, roomID id.RoomID, fetchMembers, refetch, dispatchEvt bool) error {
3959
var evts []*event.Event
4060
if refetch {

web/src/api/client.ts

+15-5
Original file line numberDiff line numberDiff line change
@@ -222,17 +222,27 @@ export default class Client {
222222
})
223223
}
224224

225-
requestEvent(room: RoomStateStore | RoomID | undefined, eventID: EventID) {
225+
requestEvent(room: RoomStateStore | RoomID | undefined, eventID: EventID, unredact?: boolean) {
226226
if (typeof room === "string") {
227227
room = this.store.rooms.get(room)
228228
}
229-
if (!room || room.eventsByID.has(eventID) || room.requestedEvents.has(eventID)) {
229+
if (!room || (!unredact && room.eventsByID.has(eventID)) ||room.requestedEvents.has(eventID)) {
230230
return
231231
}
232232
room.requestedEvents.add(eventID)
233-
this.rpc.getEvent(room.roomID, eventID).then(
234-
evt => room.applyEvent(evt),
235-
err => console.error(`Failed to fetch event ${eventID}`, err),
233+
this.rpc.getEvent(room.roomID, eventID, unredact).then(
234+
evt => {
235+
room.applyEvent(evt, false, unredact)
236+
if (unredact) {
237+
room.notifyTimelineSubscribers()
238+
}
239+
},
240+
err => {
241+
console.error(`Failed to fetch event ${eventID}`, err)
242+
if (unredact) {
243+
window.alert(`Failed to get unredacted content: ${err}`)
244+
}
245+
},
236246
)
237247
}
238248

web/src/api/rpc.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -218,8 +218,8 @@ export default abstract class RPCClient {
218218
return this.request("get_room_state", { room_id, include_members, fetch_members, refetch })
219219
}
220220

221-
getEvent(room_id: RoomID, event_id: EventID): Promise<RawDBEvent> {
222-
return this.request("get_event", { room_id, event_id })
221+
getEvent(room_id: RoomID, event_id: EventID, unredact?: boolean): Promise<RawDBEvent> {
222+
return this.request("get_event", { room_id, event_id, unredact })
223223
}
224224

225225
getRelatedEvents(room_id: RoomID, event_id: EventID, relation_type?: RelationType): Promise<RawDBEvent[]> {

web/src/api/statestore/room.ts

+10-1
Original file line numberDiff line numberDiff line change
@@ -352,13 +352,22 @@ export class RoomStateStore {
352352
return this.applyEvent(evt)
353353
}
354354

355-
applyEvent(evt: RawDBEvent, pending: boolean = false) {
355+
setViewingRedacted(evt: MemDBEvent, view: boolean) {
356+
evt.viewing_redacted = view
357+
this.eventSubs.notify(evt.event_id)
358+
this.notifyTimelineSubscribers()
359+
}
360+
361+
applyEvent(evt: RawDBEvent, pending: boolean = false, viewRedacted: boolean = false) {
356362
const memEvt = evt as MemDBEvent
357363
memEvt.mem = true
358364
memEvt.pending = pending
359365
if (pending) {
360366
memEvt.timeline_rowid = UNSENT_TIMELINE_ROWID_BASE + memEvt.timestamp
361367
}
368+
if (viewRedacted) {
369+
memEvt.viewing_redacted = true
370+
}
362371
if (evt.type === "m.room.encrypted" && evt.decrypted && evt.decrypted_type) {
363372
memEvt.type = evt.decrypted_type
364373
memEvt.encrypted = evt.content as EncryptedEventContent

web/src/api/types/hitypes.ts

+1
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ export interface MemDBEvent extends BaseDBEvent {
158158
orig_content?: UnknownEventContent
159159
orig_local_content?: LocalContent
160160
last_edit?: MemDBEvent
161+
viewing_redacted?: boolean
161162
}
162163

163164
export interface DBAccountData {

web/src/icons/restore-trash.svg

+1
Loading

web/src/ui/timeline/TimelineEvent.tsx

+3-2
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,8 @@ const TimelineEvent = ({
138138
if (evt.unread_type & UnreadType.Highlight) {
139139
wrapperClassNames.push("highlight")
140140
}
141-
if (evt.redacted_by) {
141+
const isRedacted = evt.redacted_by && !evt.viewing_redacted
142+
if (isRedacted) {
142143
wrapperClassNames.push("redacted-event")
143144
}
144145
if (evt.type === "m.room.member") {
@@ -173,7 +174,7 @@ const TimelineEvent = ({
173174
const replyTo = relatesTo?.["m.in_reply_to"]?.event_id
174175
let replyAboveMessage: JSX.Element | null = null
175176
let replyInMessage: JSX.Element | null = null
176-
if (isEventID(replyTo) && BodyType !== HiddenEvent && !evt.redacted_by && !editHistoryView) {
177+
if (isEventID(replyTo) && BodyType !== HiddenEvent && !isRedacted && !editHistoryView) {
177178
const replyElem = <ReplyIDBody
178179
room={roomCtx.store}
179180
eventID={replyTo}

web/src/ui/timeline/content/index.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -65,10 +65,11 @@ export function getBodyType(evt: MemDBEvent, forReply = false): React.FunctionCo
6565
return PolicyRuleBody
6666
}
6767
} else {
68+
const isRedacted = evt.redacted_by && !evt.viewing_redacted
6869
// Non-state events
6970
switch (evt.type) {
7071
case "m.room.message":
71-
if (evt.redacted_by) {
72+
if (isRedacted) {
7273
return RedactedBody
7374
}
7475
switch (evt.content?.msgtype) {
@@ -93,14 +94,14 @@ export function getBodyType(evt: MemDBEvent, forReply = false): React.FunctionCo
9394
return UnknownMessageBody
9495
}
9596
case "m.sticker":
96-
if (evt.redacted_by) {
97+
if (isRedacted) {
9798
return RedactedBody
9899
} else if (forReply) {
99100
return TextMessageBody
100101
}
101102
return MediaMessageBody
102103
case "m.room.encrypted":
103-
if (evt.redacted_by) {
104+
if (isRedacted) {
104105
return RedactedBody
105106
}
106107
return EncryptedBody

web/src/ui/timeline/menu/usePrimaryItems.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ export const usePrimaryItems = (
7979
.catch(err => window.alert(`Failed to resend message: ${err}`))
8080
}
8181
const onClickMore = (mevt: React.MouseEvent<HTMLButtonElement>) => {
82-
const moreMenuHeight = 4 * 40
82+
const moreMenuHeight = 5 * 40
8383
setForceOpen!(true)
8484
openModal({
8585
content: <EventExtraMenu

web/src/ui/timeline/menu/useSecondaryItems.tsx

+20
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import ViewSourceIcon from "@/icons/code.svg?react"
2727
import DeleteIcon from "@/icons/delete.svg?react"
2828
import PinIcon from "@/icons/pin.svg?react"
2929
import ReportIcon from "@/icons/report.svg?react"
30+
import RestoreTrashIcon from "@/icons/restore-trash.svg?react"
3031
import ShareIcon from "@/icons/share.svg?react"
3132
import UnpinIcon from "@/icons/unpin.svg?react"
3233

@@ -85,6 +86,18 @@ export const useSecondaryItems = (
8586
</RoomContext>,
8687
})
8788
}
89+
const onClickHideUnredacted = () => {
90+
closeModal()
91+
roomCtx.store.setViewingRedacted(evt, false)
92+
}
93+
const onClickUnredact = () => {
94+
closeModal()
95+
if (Object.entries(evt.content).length > 0) {
96+
roomCtx.store.setViewingRedacted(evt, true)
97+
} else {
98+
client.requestEvent(roomCtx.store, evt.event_id, true)
99+
}
100+
}
88101
const onClickPin = (pin: boolean) => () => {
89102
closeModal()
90103
client.pinMessage(roomCtx.store, evt.event_id, pin)
@@ -146,6 +159,8 @@ export const useSecondaryItems = (
146159
const canRedact = !evt.redacted_by
147160
&& ownPL >= redactEvtPL
148161
&& (evt.sender === client.userID || ownPL >= redactOtherPL)
162+
// TODO check server admin status and room PLs
163+
const canUnredact = Boolean(evt.redacted_by)
149164

150165
return <>
151166
<button onClick={onClickViewSource}><ViewSourceIcon/>{names && "View source"}</button>
@@ -166,5 +181,10 @@ export const useSecondaryItems = (
166181
title={pendingTitle}
167182
className="redact-button"
168183
><DeleteIcon/>{names && "Remove"}</button>}
184+
{canUnredact && (evt.viewing_redacted ? <button onClick={onClickHideUnredacted}>
185+
<DeleteIcon/>{names && "Hide content"}
186+
</button> : <button onClick={onClickUnredact}>
187+
<RestoreTrashIcon/>{names && "View content"}
188+
</button>)}
169189
</>
170190
}

0 commit comments

Comments
 (0)