mirror of
https://github.com/tulir/gomuks.git
synced 2025-04-19 18:13:41 -05:00
web/timeline: add resend button for failed messages
This commit is contained in:
parent
529ffda4ed
commit
678743703c
7 changed files with 120 additions and 60 deletions
|
@ -34,6 +34,7 @@ const (
|
|||
getEventByRowID = getEventBaseQuery + `WHERE rowid = $1`
|
||||
getManyEventsByRowID = getEventBaseQuery + `WHERE rowid IN (%s)`
|
||||
getEventByID = getEventBaseQuery + `WHERE event_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`
|
||||
insertEventBaseQuery = `
|
||||
INSERT INTO event (
|
||||
|
@ -103,6 +104,10 @@ func (eq *EventQuery) GetByID(ctx context.Context, eventID id.EventID) (*Event,
|
|||
return eq.QueryOne(ctx, getEventByID, eventID)
|
||||
}
|
||||
|
||||
func (eq *EventQuery) GetByTransactionID(ctx context.Context, txnID string) (*Event, error) {
|
||||
return eq.QueryOne(ctx, getEventByTransactionID, txnID)
|
||||
}
|
||||
|
||||
func (eq *EventQuery) GetByRowID(ctx context.Context, rowID EventRowID) (*Event, error) {
|
||||
return eq.QueryOne(ctx, getEventByRowID, rowID)
|
||||
}
|
||||
|
|
|
@ -48,6 +48,10 @@ func (h *HiClient) handleJSONCommand(ctx context.Context, req *JSONCommand) (any
|
|||
return unmarshalAndCall(req.Data, func(params *sendEventParams) (*database.Event, error) {
|
||||
return h.Send(ctx, params.RoomID, params.EventType, params.Content)
|
||||
})
|
||||
case "resend_event":
|
||||
return unmarshalAndCall(req.Data, func(params *resendEventParams) (*database.Event, error) {
|
||||
return h.Resend(ctx, params.TransactionID)
|
||||
})
|
||||
case "report_event":
|
||||
return unmarshalAndCall(req.Data, func(params *reportEventParams) (bool, error) {
|
||||
return true, h.Client.ReportEvent(ctx, params.RoomID, params.EventID, params.Reason)
|
||||
|
@ -190,6 +194,10 @@ type sendEventParams struct {
|
|||
Content json.RawMessage `json:"content"`
|
||||
}
|
||||
|
||||
type resendEventParams struct {
|
||||
TransactionID string `json:"transaction_id"`
|
||||
}
|
||||
|
||||
type reportEventParams struct {
|
||||
RoomID id.RoomID `json:"room_id"`
|
||||
EventID id.EventID `json:"event_id"`
|
||||
|
|
|
@ -215,6 +215,26 @@ func (h *HiClient) Send(
|
|||
return h.send(ctx, roomID, evtType, content, "")
|
||||
}
|
||||
|
||||
func (h *HiClient) Resend(ctx context.Context, txnID string) (*database.Event, error) {
|
||||
dbEvt, err := h.DB.Event.GetByTransactionID(ctx, txnID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get event by transaction ID: %w", err)
|
||||
} else if dbEvt == nil {
|
||||
return nil, fmt.Errorf("unknown transaction ID")
|
||||
} else if dbEvt.ID != "" && !strings.HasPrefix(dbEvt.ID.String(), "~") {
|
||||
return nil, fmt.Errorf("event was already sent successfully")
|
||||
}
|
||||
room, err := h.DB.Room.Get(ctx, dbEvt.RoomID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get room metadata: %w", err)
|
||||
} else if room == nil {
|
||||
return nil, fmt.Errorf("unknown room")
|
||||
}
|
||||
dbEvt.SendError = ""
|
||||
go h.actuallySend(context.WithoutCancel(ctx), room, dbEvt, event.Type{Type: dbEvt.Type, Class: event.MessageEventType})
|
||||
return dbEvt, nil
|
||||
}
|
||||
|
||||
func (h *HiClient) send(
|
||||
ctx context.Context,
|
||||
roomID id.RoomID,
|
||||
|
@ -279,62 +299,64 @@ func (h *HiClient) send(
|
|||
zerolog.Ctx(ctx).Err(err).Msg("Failed to stop typing while sending message")
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
var err error
|
||||
defer func() {
|
||||
if dbEvt.SendError != "" {
|
||||
err2 := h.DB.Event.UpdateSendError(ctx, dbEvt.RowID, dbEvt.SendError)
|
||||
if err2 != nil {
|
||||
zerolog.Ctx(ctx).Err(err2).AnErr("send_error", err).
|
||||
Msg("Failed to update send error in database after sending failed")
|
||||
}
|
||||
}
|
||||
h.EventHandler(&SendComplete{
|
||||
Event: dbEvt,
|
||||
Error: err,
|
||||
})
|
||||
}()
|
||||
if dbEvt.Decrypted != nil {
|
||||
var encryptedContent *event.EncryptedEventContent
|
||||
encryptedContent, err = h.Encrypt(ctx, room, evtType, dbEvt.Decrypted)
|
||||
if err != nil {
|
||||
dbEvt.SendError = fmt.Sprintf("failed to encrypt: %v", err)
|
||||
zerolog.Ctx(ctx).Err(err).Msg("Failed to encrypt event")
|
||||
return
|
||||
}
|
||||
evtType = event.EventEncrypted
|
||||
dbEvt.MegolmSessionID = encryptedContent.SessionID
|
||||
dbEvt.Content, err = json.Marshal(encryptedContent)
|
||||
if err != nil {
|
||||
dbEvt.SendError = fmt.Sprintf("failed to marshal encrypted content: %v", err)
|
||||
zerolog.Ctx(ctx).Err(err).Msg("Failed to marshal encrypted content")
|
||||
return
|
||||
}
|
||||
err = h.DB.Event.UpdateEncryptedContent(ctx, dbEvt)
|
||||
if err != nil {
|
||||
dbEvt.SendError = fmt.Sprintf("failed to save event after encryption: %v", err)
|
||||
zerolog.Ctx(ctx).Err(err).Msg("Failed to save event after encryption")
|
||||
return
|
||||
go h.actuallySend(ctx, room, dbEvt, evtType)
|
||||
return dbEvt, nil
|
||||
}
|
||||
|
||||
func (h *HiClient) actuallySend(ctx context.Context, room *database.Room, dbEvt *database.Event, evtType event.Type) {
|
||||
var err error
|
||||
defer func() {
|
||||
if dbEvt.SendError != "" {
|
||||
err2 := h.DB.Event.UpdateSendError(ctx, dbEvt.RowID, dbEvt.SendError)
|
||||
if err2 != nil {
|
||||
zerolog.Ctx(ctx).Err(err2).AnErr("send_error", err).
|
||||
Msg("Failed to update send error in database after sending failed")
|
||||
}
|
||||
}
|
||||
var resp *mautrix.RespSendEvent
|
||||
resp, err = h.Client.SendMessageEvent(ctx, room.ID, evtType, dbEvt.Content, mautrix.ReqSendEvent{
|
||||
Timestamp: dbEvt.Timestamp.UnixMilli(),
|
||||
TransactionID: txnID,
|
||||
DontEncrypt: true,
|
||||
h.EventHandler(&SendComplete{
|
||||
Event: dbEvt,
|
||||
Error: err,
|
||||
})
|
||||
}()
|
||||
if dbEvt.Decrypted != nil && len(dbEvt.Content) <= 2 {
|
||||
var encryptedContent *event.EncryptedEventContent
|
||||
encryptedContent, err = h.Encrypt(ctx, room, evtType, dbEvt.Decrypted)
|
||||
if err != nil {
|
||||
dbEvt.SendError = err.Error()
|
||||
err = fmt.Errorf("failed to send event: %w", err)
|
||||
dbEvt.SendError = fmt.Sprintf("failed to encrypt: %v", err)
|
||||
zerolog.Ctx(ctx).Err(err).Msg("Failed to encrypt event")
|
||||
return
|
||||
}
|
||||
dbEvt.ID = resp.EventID
|
||||
err = h.DB.Event.UpdateID(ctx, dbEvt.RowID, dbEvt.ID)
|
||||
evtType = event.EventEncrypted
|
||||
dbEvt.MegolmSessionID = encryptedContent.SessionID
|
||||
dbEvt.Content, err = json.Marshal(encryptedContent)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("failed to update event ID in database: %w", err)
|
||||
dbEvt.SendError = fmt.Sprintf("failed to marshal encrypted content: %v", err)
|
||||
zerolog.Ctx(ctx).Err(err).Msg("Failed to marshal encrypted content")
|
||||
return
|
||||
}
|
||||
}()
|
||||
return dbEvt, nil
|
||||
err = h.DB.Event.UpdateEncryptedContent(ctx, dbEvt)
|
||||
if err != nil {
|
||||
dbEvt.SendError = fmt.Sprintf("failed to save event after encryption: %v", err)
|
||||
zerolog.Ctx(ctx).Err(err).Msg("Failed to save event after encryption")
|
||||
return
|
||||
}
|
||||
}
|
||||
var resp *mautrix.RespSendEvent
|
||||
resp, err = h.Client.SendMessageEvent(ctx, room.ID, evtType, dbEvt.Content, mautrix.ReqSendEvent{
|
||||
Timestamp: dbEvt.Timestamp.UnixMilli(),
|
||||
TransactionID: dbEvt.TransactionID,
|
||||
DontEncrypt: true,
|
||||
})
|
||||
if err != nil {
|
||||
dbEvt.SendError = err.Error()
|
||||
err = fmt.Errorf("failed to send event: %w", err)
|
||||
return
|
||||
}
|
||||
dbEvt.ID = resp.EventID
|
||||
err = h.DB.Event.UpdateID(ctx, dbEvt.RowID, dbEvt.ID)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("failed to update event ID in database: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *HiClient) Encrypt(ctx context.Context, room *database.Room, evtType event.Type, content any) (encrypted *event.EncryptedEventContent, err error) {
|
||||
|
|
|
@ -23,6 +23,7 @@ import type {
|
|||
EventType,
|
||||
ImagePackRooms,
|
||||
RPCEvent,
|
||||
RawDBEvent,
|
||||
RoomID,
|
||||
RoomStateGUID,
|
||||
SyncStatus,
|
||||
|
@ -155,17 +156,30 @@ export default class Client {
|
|||
await this.rpc.setState(room.roomID, "m.room.pinned_events", "", { pinned: pinnedEvents })
|
||||
}
|
||||
|
||||
async resendEvent(txnID: string): Promise<void> {
|
||||
const dbEvent = await this.rpc.resendEvent(txnID)
|
||||
const room = this.store.rooms.get(dbEvent.room_id)
|
||||
room?.applyEvent(dbEvent, true)
|
||||
room?.notifyTimelineSubscribers()
|
||||
}
|
||||
|
||||
#handleOutgoingEvent(dbEvent: RawDBEvent, room: RoomStateStore) {
|
||||
if (!room.eventsByRowID.has(dbEvent.rowid)) {
|
||||
if (!room.pendingEvents.includes(dbEvent.rowid)) {
|
||||
room.pendingEvents.push(dbEvent.rowid)
|
||||
}
|
||||
room.applyEvent(dbEvent, true)
|
||||
room.notifyTimelineSubscribers()
|
||||
}
|
||||
}
|
||||
|
||||
async sendEvent(roomID: RoomID, type: EventType, content: unknown): Promise<void> {
|
||||
const room = this.store.rooms.get(roomID)
|
||||
if (!room) {
|
||||
throw new Error("Room not found")
|
||||
}
|
||||
const dbEvent = await this.rpc.sendEvent(roomID, type, content)
|
||||
if (!room.eventsByRowID.has(dbEvent.rowid)) {
|
||||
room.pendingEvents.push(dbEvent.rowid)
|
||||
room.applyEvent(dbEvent, true)
|
||||
room.notifyTimelineSubscribers()
|
||||
}
|
||||
this.#handleOutgoingEvent(dbEvent, room)
|
||||
}
|
||||
|
||||
async sendMessage(params: SendMessageParams): Promise<void> {
|
||||
|
@ -174,11 +188,7 @@ export default class Client {
|
|||
throw new Error("Room not found")
|
||||
}
|
||||
const dbEvent = await this.rpc.sendMessage(params)
|
||||
if (!room.eventsByRowID.has(dbEvent.rowid)) {
|
||||
room.pendingEvents.push(dbEvent.rowid)
|
||||
room.applyEvent(dbEvent, true)
|
||||
room.notifyTimelineSubscribers()
|
||||
}
|
||||
this.#handleOutgoingEvent(dbEvent, room)
|
||||
}
|
||||
|
||||
async subscribeToEmojiPack(pack: RoomStateGUID, subscribe: boolean = true) {
|
||||
|
|
|
@ -141,6 +141,10 @@ export default abstract class RPCClient {
|
|||
return this.request("send_event", { room_id, type, content })
|
||||
}
|
||||
|
||||
resendEvent(transaction_id: string): Promise<RawDBEvent> {
|
||||
return this.request("resend_event", { transaction_id })
|
||||
}
|
||||
|
||||
reportEvent(room_id: RoomID, event_id: EventID, reason: string): Promise<boolean> {
|
||||
return this.request("report_event", { room_id, event_id, reason })
|
||||
}
|
||||
|
|
1
web/src/icons/refresh.svg
Normal file
1
web/src/icons/refresh.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#5f6368"><path d="M480-160q-134 0-227-93t-93-227q0-134 93-227t227-93q69 0 132 28.5T720-690v-110h80v280H520v-80h168q-32-56-87.5-88T480-720q-100 0-170 70t-70 170q0 100 70 170t170 70q77 0 139-44t87-116h84q-28 106-114 173t-196 67Z"/></svg>
|
After Width: | Height: | Size: 334 B |
|
@ -25,6 +25,7 @@ import EventExtraMenu from "./EventExtraMenu.tsx"
|
|||
import EditIcon from "@/icons/edit.svg?react"
|
||||
import MoreIcon from "@/icons/more.svg?react"
|
||||
import ReactIcon from "@/icons/react.svg?react"
|
||||
import RefreshIcon from "@/icons/refresh.svg?react"
|
||||
import ReplyIcon from "@/icons/reply.svg?react"
|
||||
import "./index.css"
|
||||
|
||||
|
@ -71,6 +72,13 @@ const EventMenu = ({ evt, setForceOpen }: EventHoverMenuProps) => {
|
|||
const onClickEdit = useCallback(() => {
|
||||
roomCtx.setEditing(evt)
|
||||
}, [roomCtx, evt])
|
||||
const onClickResend = useCallback(() => {
|
||||
if (!evt.transaction_id) {
|
||||
return
|
||||
}
|
||||
client.resendEvent(evt.transaction_id)
|
||||
.catch(err => window.alert(`Failed to resend message: ${err}`))
|
||||
}, [client, evt.transaction_id])
|
||||
const onClickMore = useCallback((mevt: React.MouseEvent<HTMLButtonElement>) => {
|
||||
const moreMenuHeight = 10 * 16
|
||||
setForceOpen(true)
|
||||
|
@ -96,13 +104,14 @@ const EventMenu = ({ evt, setForceOpen }: EventHoverMenuProps) => {
|
|||
const evtSendType = isEncrypted ? "m.room.encrypted" : evt.type === "m.sticker" ? "m.sticker" : "m.room.message"
|
||||
const messageSendPL = pls.events?.[evtSendType] ?? pls.events_default ?? 0
|
||||
|
||||
const canSend = ownPL >= messageSendPL
|
||||
const didFail = !!evt.send_error && evt.send_error !== "not sent" && !!evt.transaction_id
|
||||
const canSend = !didFail && ownPL >= messageSendPL
|
||||
const canEdit = canSend
|
||||
&& evt.sender === userID
|
||||
&& evt.type === "m.room.message"
|
||||
&& evt.relation_type !== "m.replace"
|
||||
&& !evt.redacted_by
|
||||
const canReact = ownPL >= reactPL
|
||||
const canReact = !didFail && ownPL >= reactPL
|
||||
|
||||
return <div className="event-hover-menu" ref={contextMenuRef}>
|
||||
{canReact && <button disabled={isPending} title={pendingTitle} onClick={onClickReact}><ReactIcon/></button>}
|
||||
|
@ -112,6 +121,7 @@ const EventMenu = ({ evt, setForceOpen }: EventHoverMenuProps) => {
|
|||
onClick={onClickReply}
|
||||
><ReplyIcon/></button>}
|
||||
{canEdit && <button onClick={onClickEdit} disabled={isPending} title={pendingTitle}><EditIcon/></button>}
|
||||
{didFail && <button onClick={onClickResend} title="Resend message"><RefreshIcon/></button>}
|
||||
<button onClick={onClickMore}><MoreIcon/></button>
|
||||
</div>
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue