mirror of
https://github.com/tulir/gomuks.git
synced 2025-04-20 10:33:41 -05:00
web/timeline: add edit history modal
This commit is contained in:
parent
a9e459b448
commit
4fc9b88ec6
11 changed files with 181 additions and 55 deletions
|
@ -36,7 +36,11 @@ const (
|
||||||
getEventByID = getEventBaseQuery + `WHERE event_id = $1`
|
getEventByID = getEventBaseQuery + `WHERE event_id = $1`
|
||||||
getEventByTransactionID = getEventBaseQuery + `WHERE transaction_id = $1`
|
getEventByTransactionID = getEventBaseQuery + `WHERE transaction_id = $1`
|
||||||
getFailedEventsByMegolmSessionID = getEventBaseQuery + `WHERE room_id = $1 AND megolm_session_id = $2 AND decryption_error IS NOT NULL`
|
getFailedEventsByMegolmSessionID = getEventBaseQuery + `WHERE room_id = $1 AND megolm_session_id = $2 AND decryption_error IS NOT NULL`
|
||||||
insertEventBaseQuery = `
|
getRelatedEventsQuery = getEventBaseQuery + `
|
||||||
|
WHERE room_id = $1 AND relates_to = $2 AND ($3 = '' OR relation_type = $3)
|
||||||
|
ORDER BY timestamp ASC
|
||||||
|
`
|
||||||
|
insertEventBaseQuery = `
|
||||||
INSERT INTO event (
|
INSERT INTO event (
|
||||||
room_id, event_id, sender, type, state_key, timestamp, content, decrypted, decrypted_type,
|
room_id, event_id, sender, type, state_key, timestamp, content, decrypted, decrypted_type,
|
||||||
unsigned, local_content, transaction_id, redacted_by, relates_to, relation_type,
|
unsigned, local_content, transaction_id, redacted_by, relates_to, relation_type,
|
||||||
|
@ -112,6 +116,10 @@ func (eq *EventQuery) GetByRowID(ctx context.Context, rowID EventRowID) (*Event,
|
||||||
return eq.QueryOne(ctx, getEventByRowID, rowID)
|
return eq.QueryOne(ctx, getEventByRowID, rowID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (eq *EventQuery) GetRelatedEvents(ctx context.Context, roomID id.RoomID, eventID id.EventID, relationType event.RelationType) ([]*Event, error) {
|
||||||
|
return eq.QueryMany(ctx, getRelatedEventsQuery, roomID, eventID, relationType)
|
||||||
|
}
|
||||||
|
|
||||||
func (eq *EventQuery) GetByRowIDs(ctx context.Context, rowIDs ...EventRowID) ([]*Event, error) {
|
func (eq *EventQuery) GetByRowIDs(ctx context.Context, rowIDs ...EventRowID) ([]*Event, error) {
|
||||||
query, params := buildMultiEventGetFunction(nil, rowIDs, getManyEventsByRowID)
|
query, params := buildMultiEventGetFunction(nil, rowIDs, getManyEventsByRowID)
|
||||||
return eq.QueryMany(ctx, query, params...)
|
return eq.QueryMany(ctx, query, params...)
|
||||||
|
|
|
@ -125,10 +125,10 @@ func (h *HiClient) handleJSONCommand(ctx context.Context, req *JSONCommand) (any
|
||||||
return unmarshalAndCall(req.Data, func(params *getEventParams) (*database.Event, error) {
|
return unmarshalAndCall(req.Data, func(params *getEventParams) (*database.Event, error) {
|
||||||
return h.GetEvent(ctx, params.RoomID, params.EventID)
|
return h.GetEvent(ctx, params.RoomID, params.EventID)
|
||||||
})
|
})
|
||||||
//case "get_events_by_rowids":
|
case "get_related_events":
|
||||||
// return unmarshalAndCall(req.Data, func(params *getEventsByRowIDsParams) ([]*database.Event, error) {
|
return unmarshalAndCall(req.Data, func(params *getRelatedEventsParams) ([]*database.Event, error) {
|
||||||
// return h.GetEventsByRowIDs(ctx, params.RowIDs)
|
return h.DB.Event.GetRelatedEvents(ctx, params.RoomID, params.EventID, params.RelationType)
|
||||||
// })
|
})
|
||||||
case "get_room_state":
|
case "get_room_state":
|
||||||
return unmarshalAndCall(req.Data, func(params *getRoomStateParams) ([]*database.Event, error) {
|
return unmarshalAndCall(req.Data, func(params *getRoomStateParams) ([]*database.Event, error) {
|
||||||
return h.GetRoomState(ctx, params.RoomID, params.IncludeMembers, params.FetchMembers, params.Refetch)
|
return h.GetRoomState(ctx, params.RoomID, params.IncludeMembers, params.FetchMembers, params.Refetch)
|
||||||
|
@ -315,9 +315,12 @@ type getEventParams struct {
|
||||||
EventID id.EventID `json:"event_id"`
|
EventID id.EventID `json:"event_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
//type getEventsByRowIDsParams struct {
|
type getRelatedEventsParams struct {
|
||||||
// RowIDs []database.EventRowID `json:"row_ids"`
|
RoomID id.RoomID `json:"room_id"`
|
||||||
//}
|
EventID id.EventID `json:"event_id"`
|
||||||
|
|
||||||
|
RelationType event.RelationType `json:"relation_type"`
|
||||||
|
}
|
||||||
|
|
||||||
type getRoomStateParams struct {
|
type getRoomStateParams struct {
|
||||||
RoomID id.RoomID `json:"room_id"`
|
RoomID id.RoomID `json:"room_id"`
|
||||||
|
|
|
@ -22,37 +22,6 @@ import (
|
||||||
|
|
||||||
var ErrPaginationAlreadyInProgress = errors.New("pagination is already in progress")
|
var ErrPaginationAlreadyInProgress = errors.New("pagination is already in progress")
|
||||||
|
|
||||||
/*func (h *HiClient) GetEventsByRowIDs(ctx context.Context, rowIDs []database.EventRowID) ([]*database.Event, error) {
|
|
||||||
events, err := h.DB.Event.GetByRowIDs(ctx, rowIDs...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
} else if len(events) == 0 {
|
|
||||||
return events, nil
|
|
||||||
}
|
|
||||||
firstRoomID := events[0].RoomID
|
|
||||||
allInSameRoom := true
|
|
||||||
for _, evt := range events {
|
|
||||||
h.ReprocessExistingEvent(ctx, evt)
|
|
||||||
if evt.RoomID != firstRoomID {
|
|
||||||
allInSameRoom = false
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if allInSameRoom {
|
|
||||||
err = h.DB.Event.FillLastEditRowIDs(ctx, firstRoomID, events)
|
|
||||||
if err != nil {
|
|
||||||
return events, fmt.Errorf("failed to fill last edit row IDs: %w", err)
|
|
||||||
}
|
|
||||||
err = h.DB.Event.FillReactionCounts(ctx, firstRoomID, events)
|
|
||||||
if err != nil {
|
|
||||||
return events, fmt.Errorf("failed to fill reaction counts: %w", err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// TODO slow path where events are collected and filling is done one room at a time?
|
|
||||||
}
|
|
||||||
return events, nil
|
|
||||||
}*/
|
|
||||||
|
|
||||||
func (h *HiClient) GetEvent(ctx context.Context, roomID id.RoomID, eventID id.EventID) (*database.Event, error) {
|
func (h *HiClient) GetEvent(ctx context.Context, roomID id.RoomID, eventID id.EventID) (*database.Event, error) {
|
||||||
if evt, err := h.DB.Event.GetByID(ctx, eventID); err != nil {
|
if evt, err := h.DB.Event.GetByID(ctx, eventID); err != nil {
|
||||||
return nil, fmt.Errorf("failed to get event from database: %w", err)
|
return nil, fmt.Errorf("failed to get event from database: %w", err)
|
||||||
|
|
|
@ -26,6 +26,7 @@ import type {
|
||||||
ImagePackRooms,
|
ImagePackRooms,
|
||||||
RPCEvent,
|
RPCEvent,
|
||||||
RawDBEvent,
|
RawDBEvent,
|
||||||
|
RelationType,
|
||||||
RoomID,
|
RoomID,
|
||||||
RoomStateGUID,
|
RoomStateGUID,
|
||||||
SyncStatus,
|
SyncStatus,
|
||||||
|
@ -235,6 +236,17 @@ export default class Client {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getRelatedEvents(room: RoomStateStore | RoomID | undefined, eventID: EventID, relationType?: RelationType) {
|
||||||
|
if (typeof room === "string") {
|
||||||
|
room = this.store.rooms.get(room)
|
||||||
|
}
|
||||||
|
if (!room) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
const events = await this.rpc.getRelatedEvents(room.roomID, eventID, relationType)
|
||||||
|
return events.map(evt => room.getOrApplyEvent(evt))
|
||||||
|
}
|
||||||
|
|
||||||
async pinMessage(room: RoomStateStore, evtID: EventID, wantPinned: boolean) {
|
async pinMessage(room: RoomStateStore, evtID: EventID, wantPinned: boolean) {
|
||||||
const pinnedEvents = room.getPinnedEvents()
|
const pinnedEvents = room.getPinnedEvents()
|
||||||
const currentlyPinned = pinnedEvents.includes(evtID)
|
const currentlyPinned = pinnedEvents.includes(evtID)
|
||||||
|
|
|
@ -19,7 +19,6 @@ import type {
|
||||||
ClientWellKnown,
|
ClientWellKnown,
|
||||||
DBPushRegistration,
|
DBPushRegistration,
|
||||||
EventID,
|
EventID,
|
||||||
EventRowID,
|
|
||||||
EventType,
|
EventType,
|
||||||
JSONValue,
|
JSONValue,
|
||||||
LoginFlowsResponse,
|
LoginFlowsResponse,
|
||||||
|
@ -34,6 +33,7 @@ import type {
|
||||||
RawDBEvent,
|
RawDBEvent,
|
||||||
ReceiptType,
|
ReceiptType,
|
||||||
RelatesTo,
|
RelatesTo,
|
||||||
|
RelationType,
|
||||||
ResolveAliasResponse,
|
ResolveAliasResponse,
|
||||||
RespOpenIDToken,
|
RespOpenIDToken,
|
||||||
RespRoomJoin,
|
RespRoomJoin,
|
||||||
|
@ -222,8 +222,8 @@ export default abstract class RPCClient {
|
||||||
return this.request("get_event", { room_id, event_id })
|
return this.request("get_event", { room_id, event_id })
|
||||||
}
|
}
|
||||||
|
|
||||||
getEventsByRowIDs(row_ids: EventRowID[]): Promise<RawDBEvent[]> {
|
getRelatedEvents(room_id: RoomID, event_id: EventID, relation_type?: RelationType): Promise<RawDBEvent[]> {
|
||||||
return this.request("get_events_by_row_ids", { row_ids })
|
return this.request("get_related_events", { room_id, event_id, relation_type })
|
||||||
}
|
}
|
||||||
|
|
||||||
paginate(room_id: RoomID, max_timeline_id: TimelineRowID, limit: number): Promise<PaginationResponse> {
|
paginate(room_id: RoomID, max_timeline_id: TimelineRowID, limit: number): Promise<PaginationResponse> {
|
||||||
|
|
|
@ -344,6 +344,14 @@ export class RoomStateStore {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getOrApplyEvent(evt: RawDBEvent) {
|
||||||
|
const existing = this.eventsByRowID.get(evt.rowid)
|
||||||
|
if (existing) {
|
||||||
|
return existing
|
||||||
|
}
|
||||||
|
return this.applyEvent(evt)
|
||||||
|
}
|
||||||
|
|
||||||
applyEvent(evt: RawDBEvent, pending: boolean = false) {
|
applyEvent(evt: RawDBEvent, pending: boolean = false) {
|
||||||
const memEvt = evt as MemDBEvent
|
const memEvt = evt as MemDBEvent
|
||||||
memEvt.mem = true
|
memEvt.mem = true
|
||||||
|
@ -362,6 +370,7 @@ export class RoomStateStore {
|
||||||
memEvt.last_edit = this.eventsByRowID.get(memEvt.last_edit_rowid)
|
memEvt.last_edit = this.eventsByRowID.get(memEvt.last_edit_rowid)
|
||||||
if (memEvt.last_edit) {
|
if (memEvt.last_edit) {
|
||||||
memEvt.orig_content = memEvt.content
|
memEvt.orig_content = memEvt.content
|
||||||
|
memEvt.orig_local_content = memEvt.local_content
|
||||||
memEvt.content = memEvt.last_edit.content["m.new_content"]
|
memEvt.content = memEvt.last_edit.content["m.new_content"]
|
||||||
memEvt.local_content = memEvt.last_edit.local_content
|
memEvt.local_content = memEvt.last_edit.local_content
|
||||||
}
|
}
|
||||||
|
@ -371,6 +380,7 @@ export class RoomStateStore {
|
||||||
this.eventsByRowID.set(editTarget.rowid, {
|
this.eventsByRowID.set(editTarget.rowid, {
|
||||||
...editTarget,
|
...editTarget,
|
||||||
last_edit: memEvt,
|
last_edit: memEvt,
|
||||||
|
orig_local_content: editTarget.local_content,
|
||||||
orig_content: editTarget.content,
|
orig_content: editTarget.content,
|
||||||
content: memEvt.content["m.new_content"],
|
content: memEvt.content["m.new_content"],
|
||||||
local_content: memEvt.local_content,
|
local_content: memEvt.local_content,
|
||||||
|
|
|
@ -156,6 +156,7 @@ export interface MemDBEvent extends BaseDBEvent {
|
||||||
pending: boolean
|
pending: boolean
|
||||||
encrypted?: EncryptedEventContent
|
encrypted?: EncryptedEventContent
|
||||||
orig_content?: UnknownEventContent
|
orig_content?: UnknownEventContent
|
||||||
|
orig_local_content?: LocalContent
|
||||||
last_edit?: MemDBEvent
|
last_edit?: MemDBEvent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
14
web/src/ui/timeline/EventEditHistory.css
Normal file
14
web/src/ui/timeline/EventEditHistory.css
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
div.event-edit-history-wrapper {
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.event-edit-history-modal {
|
||||||
|
--timeline-status-size: 0;
|
||||||
|
--timeline-horizontal-padding: 0;
|
||||||
|
min-width: 20rem;
|
||||||
|
padding: 1rem;
|
||||||
|
|
||||||
|
> p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
81
web/src/ui/timeline/EventEditHistory.tsx
Normal file
81
web/src/ui/timeline/EventEditHistory.tsx
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
// gomuks - A Matrix client written in Go.
|
||||||
|
// Copyright (C) 2025 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 { use, useEffect, useState } from "react"
|
||||||
|
import { ScaleLoader } from "react-spinners"
|
||||||
|
import { MemDBEvent } from "@/api/types"
|
||||||
|
import ClientContext from "../ClientContext.ts"
|
||||||
|
import { RoomContext, RoomContextData } from "../roomview/roomcontext.ts"
|
||||||
|
import TimelineEvent from "./TimelineEvent.tsx"
|
||||||
|
import "./EventEditHistory.css"
|
||||||
|
|
||||||
|
interface EventEditHistoryProps {
|
||||||
|
evt: MemDBEvent
|
||||||
|
roomCtx: RoomContextData
|
||||||
|
}
|
||||||
|
|
||||||
|
const EventEditHistory = ({ evt, roomCtx }: EventEditHistoryProps) => {
|
||||||
|
const client = use(ClientContext)!
|
||||||
|
const [revisions, setRevisions] = useState<MemDBEvent[]>([])
|
||||||
|
const [error, setError] = useState("")
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
useEffect(() => {
|
||||||
|
setLoading(true)
|
||||||
|
setError("")
|
||||||
|
setRevisions([])
|
||||||
|
client.getRelatedEvents(roomCtx.store, evt.event_id, "m.replace").then(
|
||||||
|
edits => {
|
||||||
|
setRevisions([{
|
||||||
|
...evt,
|
||||||
|
content: evt.orig_content ?? evt.content,
|
||||||
|
local_content: evt.orig_local_content ?? evt.local_content,
|
||||||
|
last_edit: undefined,
|
||||||
|
reactions: undefined,
|
||||||
|
orig_content: undefined,
|
||||||
|
orig_local_content: undefined,
|
||||||
|
}, ...edits.map(editEvt => ({
|
||||||
|
...editEvt,
|
||||||
|
content: editEvt.content["m.new_content"] ?? editEvt.content,
|
||||||
|
orig_content: editEvt.content,
|
||||||
|
relation_type: undefined,
|
||||||
|
reactions: undefined,
|
||||||
|
}))])
|
||||||
|
},
|
||||||
|
err => {
|
||||||
|
console.error("Failed to get event edit history", err)
|
||||||
|
setError(`${err}`)
|
||||||
|
},
|
||||||
|
).finally(() => setLoading(false))
|
||||||
|
}, [client, roomCtx, evt])
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <ScaleLoader color="var(--primary-color)"/>
|
||||||
|
} else if (error) {
|
||||||
|
return <div>Failed to load :( {error}</div>
|
||||||
|
}
|
||||||
|
return <>
|
||||||
|
<RoomContext value={roomCtx}>
|
||||||
|
<p>Event has {revisions.length} revisions</p>
|
||||||
|
{revisions.map((rev, i) => <TimelineEvent
|
||||||
|
key={rev.rowid}
|
||||||
|
evt={rev}
|
||||||
|
prevEvt={revisions[i-1] ?? null}
|
||||||
|
editHistoryView={true}
|
||||||
|
/>)}
|
||||||
|
</RoomContext>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EventEditHistory
|
|
@ -79,7 +79,7 @@ div.timeline-event {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> span.event-time, > span.event-edited {
|
> span.event-time {
|
||||||
font-size: .7rem;
|
font-size: .7rem;
|
||||||
color: var(--secondary-text-color);
|
color: var(--secondary-text-color);
|
||||||
}
|
}
|
||||||
|
@ -100,6 +100,13 @@ div.timeline-event {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
contain: content;
|
contain: content;
|
||||||
|
|
||||||
|
> div.event-edited {
|
||||||
|
font-size: .7rem;
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
user-select: none;
|
||||||
|
cursor: var(--clickable-cursor);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> div.event-send-status {
|
> div.event-send-status {
|
||||||
|
|
|
@ -22,8 +22,9 @@ import { isMobileDevice } from "@/util/ismobile.ts"
|
||||||
import { getDisplayname, isEventID } from "@/util/validation.ts"
|
import { getDisplayname, isEventID } from "@/util/validation.ts"
|
||||||
import ClientContext from "../ClientContext.ts"
|
import ClientContext from "../ClientContext.ts"
|
||||||
import MainScreenContext from "../MainScreenContext.ts"
|
import MainScreenContext from "../MainScreenContext.ts"
|
||||||
import { ModalContext } from "../modal"
|
import { ModalContext, NestableModalContext } from "../modal"
|
||||||
import { useRoomContext } from "../roomview/roomcontext.ts"
|
import { useRoomContext } from "../roomview/roomcontext.ts"
|
||||||
|
import EventEditHistory from "./EventEditHistory.tsx"
|
||||||
import ReadReceipts from "./ReadReceipts.tsx"
|
import ReadReceipts from "./ReadReceipts.tsx"
|
||||||
import { ReplyIDBody } from "./ReplyBody.tsx"
|
import { ReplyIDBody } from "./ReplyBody.tsx"
|
||||||
import URLPreviews from "./URLPreviews.tsx"
|
import URLPreviews from "./URLPreviews.tsx"
|
||||||
|
@ -40,6 +41,7 @@ export interface TimelineEventProps {
|
||||||
disableMenu?: boolean
|
disableMenu?: boolean
|
||||||
smallReplies?: boolean
|
smallReplies?: boolean
|
||||||
isFocused?: boolean
|
isFocused?: boolean
|
||||||
|
editHistoryView?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const fullTimeFormatter = new Intl.DateTimeFormat("en-GB", { dateStyle: "full", timeStyle: "medium" })
|
const fullTimeFormatter = new Intl.DateTimeFormat("en-GB", { dateStyle: "full", timeStyle: "medium" })
|
||||||
|
@ -75,11 +77,14 @@ const EventSendStatus = ({ evt }: { evt: MemDBEvent }) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const TimelineEvent = ({ evt, prevEvt, disableMenu, smallReplies, isFocused }: TimelineEventProps) => {
|
const TimelineEvent = ({
|
||||||
|
evt, prevEvt, disableMenu, smallReplies, isFocused, editHistoryView,
|
||||||
|
}: TimelineEventProps) => {
|
||||||
const roomCtx = useRoomContext()
|
const roomCtx = useRoomContext()
|
||||||
const client = use(ClientContext)!
|
const client = use(ClientContext)!
|
||||||
const mainScreen = use(MainScreenContext)
|
const mainScreen = use(MainScreenContext)
|
||||||
const openModal = use(ModalContext)
|
const openModal = use(ModalContext)
|
||||||
|
const openNestableModal = use(NestableModalContext)
|
||||||
const [forceContextMenuOpen, setForceContextMenuOpen] = useState(false)
|
const [forceContextMenuOpen, setForceContextMenuOpen] = useState(false)
|
||||||
const onContextMenu = (mouseEvt: React.MouseEvent) => {
|
const onContextMenu = (mouseEvt: React.MouseEvent) => {
|
||||||
const targetElem = mouseEvt.target as HTMLElement
|
const targetElem = mouseEvt.target as HTMLElement
|
||||||
|
@ -115,6 +120,15 @@ const TimelineEvent = ({ evt, prevEvt, disableMenu, smallReplies, isFocused }: T
|
||||||
mouseEvt.stopPropagation()
|
mouseEvt.stopPropagation()
|
||||||
roomCtx.setFocusedEventRowID(roomCtx.focusedEventRowID === evt.rowid ? null : evt.rowid)
|
roomCtx.setFocusedEventRowID(roomCtx.focusedEventRowID === evt.rowid ? null : evt.rowid)
|
||||||
}
|
}
|
||||||
|
const openEditHistory = () => {
|
||||||
|
openNestableModal({
|
||||||
|
content: <EventEditHistory evt={evt} roomCtx={roomCtx}/>,
|
||||||
|
dimmed: true,
|
||||||
|
boxed: true,
|
||||||
|
boxClass: "event-edit-history-wrapper",
|
||||||
|
innerBoxClass: "event-edit-history-modal",
|
||||||
|
})
|
||||||
|
}
|
||||||
const memberEvt = useRoomMember(client, roomCtx.store, evt.sender)
|
const memberEvt = useRoomMember(client, roomCtx.store, evt.sender)
|
||||||
const memberEvtContent = memberEvt?.content as MemberEventContent | undefined
|
const memberEvtContent = memberEvt?.content as MemberEventContent | undefined
|
||||||
const BodyType = getBodyType(evt)
|
const BodyType = getBodyType(evt)
|
||||||
|
@ -136,7 +150,7 @@ const TimelineEvent = ({ evt, prevEvt, disableMenu, smallReplies, isFocused }: T
|
||||||
if (evt.sender === client.userID) {
|
if (evt.sender === client.userID) {
|
||||||
wrapperClassNames.push("own-event")
|
wrapperClassNames.push("own-event")
|
||||||
}
|
}
|
||||||
if (isMobileDevice || disableMenu) {
|
if ((isMobileDevice && !editHistoryView) || disableMenu) {
|
||||||
wrapperClassNames.push("no-hover")
|
wrapperClassNames.push("no-hover")
|
||||||
}
|
}
|
||||||
if (isFocused) {
|
if (isFocused) {
|
||||||
|
@ -159,7 +173,7 @@ const TimelineEvent = ({ evt, prevEvt, disableMenu, smallReplies, isFocused }: T
|
||||||
const replyTo = relatesTo?.["m.in_reply_to"]?.event_id
|
const replyTo = relatesTo?.["m.in_reply_to"]?.event_id
|
||||||
let replyAboveMessage: JSX.Element | null = null
|
let replyAboveMessage: JSX.Element | null = null
|
||||||
let replyInMessage: JSX.Element | null = null
|
let replyInMessage: JSX.Element | null = null
|
||||||
if (isEventID(replyTo) && BodyType !== HiddenEvent && !evt.redacted_by) {
|
if (isEventID(replyTo) && BodyType !== HiddenEvent && !evt.redacted_by && !editHistoryView) {
|
||||||
const replyElem = <ReplyIDBody
|
const replyElem = <ReplyIDBody
|
||||||
room={roomCtx.store}
|
room={roomCtx.store}
|
||||||
eventID={replyTo}
|
eventID={replyTo}
|
||||||
|
@ -204,6 +218,9 @@ const TimelineEvent = ({ evt, prevEvt, disableMenu, smallReplies, isFocused }: T
|
||||||
eventTimeOnly = true
|
eventTimeOnly = true
|
||||||
renderAvatar = false
|
renderAvatar = false
|
||||||
}
|
}
|
||||||
|
if (editHistoryView) {
|
||||||
|
wrapperClassNames.push("edit-history-event")
|
||||||
|
}
|
||||||
const fullTime = fullTimeFormatter.format(eventTS)
|
const fullTime = fullTimeFormatter.format(eventTS)
|
||||||
const shortTime = formatShortTime(eventTS)
|
const shortTime = formatShortTime(eventTS)
|
||||||
const editTime = editEventTS ? `Edited at ${fullTimeFormatter.format(editEventTS)}` : null
|
const editTime = editEventTS ? `Edited at ${fullTimeFormatter.format(editEventTS)}` : null
|
||||||
|
@ -211,9 +228,9 @@ const TimelineEvent = ({ evt, prevEvt, disableMenu, smallReplies, isFocused }: T
|
||||||
data-event-id={evt.event_id}
|
data-event-id={evt.event_id}
|
||||||
className={wrapperClassNames.join(" ")}
|
className={wrapperClassNames.join(" ")}
|
||||||
onContextMenu={onContextMenu}
|
onContextMenu={onContextMenu}
|
||||||
onClick={!disableMenu && isMobileDevice ? onClick : undefined}
|
onClick={!disableMenu && !editHistoryView && isMobileDevice ? onClick : undefined}
|
||||||
>
|
>
|
||||||
{!disableMenu && !isMobileDevice && <div
|
{!disableMenu && (!isMobileDevice || editHistoryView) && <div
|
||||||
className={`context-menu-container ${forceContextMenuOpen ? "force-open" : ""}`}
|
className={`context-menu-container ${forceContextMenuOpen ? "force-open" : ""}`}
|
||||||
>
|
>
|
||||||
<EventHoverMenu evt={evt} roomCtx={roomCtx} setForceOpen={setForceContextMenuOpen}/>
|
<EventHoverMenu evt={evt} roomCtx={roomCtx} setForceOpen={setForceContextMenuOpen}/>
|
||||||
|
@ -258,11 +275,8 @@ const TimelineEvent = ({ evt, prevEvt, disableMenu, smallReplies, isFocused }: T
|
||||||
</span>
|
</span>
|
||||||
</div>}
|
</div>}
|
||||||
<span className="event-time" title={fullTime}>{shortTime}</span>
|
<span className="event-time" title={fullTime}>{shortTime}</span>
|
||||||
{(editEventTS && editTime) ? <span className="event-edited" title={editTime}>
|
|
||||||
(edited at {formatShortTime(editEventTS)})
|
|
||||||
</span> : null}
|
|
||||||
</div> : <div className="event-time-only">
|
</div> : <div className="event-time-only">
|
||||||
<span className="event-time" title={editTime ? `${fullTime} - ${editTime}` : fullTime}>{shortTime}</span>
|
<span className="event-time" title={fullTime}>{shortTime}</span>
|
||||||
</div>}
|
</div>}
|
||||||
<div className="event-content">
|
<div className="event-content">
|
||||||
{replyInMessage}
|
{replyInMessage}
|
||||||
|
@ -270,11 +284,18 @@ const TimelineEvent = ({ evt, prevEvt, disableMenu, smallReplies, isFocused }: T
|
||||||
<BodyType room={roomCtx.store} sender={memberEvt} event={evt}/>
|
<BodyType room={roomCtx.store} sender={memberEvt} event={evt}/>
|
||||||
{!isSmallBodyType && <URLPreviews room={roomCtx.store} event={evt}/>}
|
{!isSmallBodyType && <URLPreviews room={roomCtx.store} event={evt}/>}
|
||||||
</ContentErrorBoundary>
|
</ContentErrorBoundary>
|
||||||
|
{(!editHistoryView && editEventTS && editTime) ? <div
|
||||||
|
className="event-edited"
|
||||||
|
title={editTime}
|
||||||
|
onClick={openEditHistory}
|
||||||
|
>
|
||||||
|
(edited at {formatShortTime(editEventTS)})
|
||||||
|
</div> : null}
|
||||||
{evt.reactions ? <EventReactions reactions={evt.reactions}/> : null}
|
{evt.reactions ? <EventReactions reactions={evt.reactions}/> : null}
|
||||||
</div>
|
</div>
|
||||||
{!evt.event_id.startsWith("~") && roomCtx.store.preferences.display_read_receipts &&
|
{!evt.event_id.startsWith("~") && roomCtx.store.preferences.display_read_receipts && !editHistoryView &&
|
||||||
<ReadReceipts room={roomCtx.store} eventID={evt.event_id} />}
|
<ReadReceipts room={roomCtx.store} eventID={evt.event_id} />}
|
||||||
{evt.sender === client.userID && evt.transaction_id ? <EventSendStatus evt={evt}/> : null}
|
{evt.sender === client.userID && evt.transaction_id && !editHistoryView ? <EventSendStatus evt={evt}/> : null}
|
||||||
</div>
|
</div>
|
||||||
return <>
|
return <>
|
||||||
{dateSeparator}
|
{dateSeparator}
|
||||||
|
|
Loading…
Add table
Reference in a new issue