diff --git a/web/src/api/chats.ts b/web/src/api/chats.ts index e0b464f..b227d23 100644 --- a/web/src/api/chats.ts +++ b/web/src/api/chats.ts @@ -258,6 +258,11 @@ export async function deleteMessage(messageId: number, forAll = false): Promise< await http.delete(`/messages/${messageId}`, { params: { for_all: forAll } }); } +export async function editMessage(messageId: number, text: string): Promise { + const { data } = await http.put(`/messages/${messageId}`, { text }); + return data; +} + export async function discoverChats(query?: string): Promise { const { data } = await http.get("/chats/discover", { params: query?.trim() ? { query: query.trim() } : undefined diff --git a/web/src/components/MessageComposer.tsx b/web/src/components/MessageComposer.tsx index 10a99a1..ea78885 100644 --- a/web/src/components/MessageComposer.tsx +++ b/web/src/components/MessageComposer.tsx @@ -1,5 +1,5 @@ import { useEffect, useRef, useState, type KeyboardEvent, type PointerEvent } from "react"; -import { attachFile, requestUploadUrl, sendMessageWithClientId, uploadToPresignedUrl } from "../api/chats"; +import { attachFile, editMessage, requestUploadUrl, sendMessageWithClientId, uploadToPresignedUrl } from "../api/chats"; import { useAuthStore } from "../store/authStore"; import { useChatStore } from "../store/chatStore"; import { buildWsUrl } from "../utils/ws"; @@ -19,6 +19,9 @@ export function MessageComposer() { const removeOptimisticMessage = useChatStore((s) => s.removeOptimisticMessage); const replyToByChat = useChatStore((s) => s.replyToByChat); const setReplyToMessage = useChatStore((s) => s.setReplyToMessage); + const editingByChat = useChatStore((s) => s.editingByChat); + const setEditingMessage = useChatStore((s) => s.setEditingMessage); + const upsertMessage = useChatStore((s) => s.upsertMessage); const accessToken = useAuthStore((s) => s.accessToken); const [text, setText] = useState(""); @@ -54,6 +57,7 @@ export function MessageComposer() { const [dragHint, setDragHint] = useState<"idle" | "lock" | "cancel">("idle"); const hasTextToSend = text.trim().length > 0; const activeChat = chats.find((chat) => chat.id === activeChatId); + const editingMessage = activeChatId ? (editingByChat[activeChatId] ?? null) : null; const canSendInActiveChat = Boolean( activeChatId && activeChat && @@ -71,11 +75,18 @@ export function MessageComposer() { } return; } + if (editingMessage) { + const editingText = editingMessage.text ?? ""; + if (text !== editingText) { + setText(editingText); + } + return; + } const draft = draftsByChat[activeChatId] ?? ""; if (draft !== text) { setText(draft); } - }, [activeChatId, draftsByChat, text]); + }, [activeChatId, draftsByChat, text, editingMessage]); useEffect(() => { return () => { @@ -156,6 +167,18 @@ export function MessageComposer() { if (!activeChatId || !text.trim() || !me || !canSendInActiveChat) { return; } + if (editingMessage) { + try { + const updated = await editMessage(editingMessage.id, text.trim()); + upsertMessage(activeChatId, updated); + setEditingMessage(activeChatId, null); + setText(""); + clearDraft(activeChatId); + } catch { + setUploadError("Edit failed. Message may be older than 7 days."); + } + return; + } const clientMessageId = makeClientMessageId(); const textValue = text.trim(); const replyToMessageId = activeChatId ? (replyToByChat[activeChatId]?.id ?? undefined) : undefined; @@ -650,6 +673,25 @@ export function MessageComposer() { ) : null} + {activeChatId && editingMessage ? ( +
+
+

Editing message

+

{editingMessage.text || "[empty]"}

+
+ +
+ ) : null} + {recordingState !== "idle" ? (
@@ -801,9 +843,9 @@ export function MessageComposer() { disabled={recordingState !== "idle" || !activeChatId || !canSendInActiveChat} onClick={handleSend} type="button" - title="Send message" + title={editingMessage ? "Save edit" : "Send message"} > - ↑ + {editingMessage ? "✓" : "↑"} ) : ( + {canEditMessage(messagesMap.get(ctx.messageId), me?.id) ? ( + + ) : null}