feat(web): add message edit flow in context menu and composer
All checks were successful
CI / test (push) Successful in 24s

This commit is contained in:
2026-03-08 13:22:57 +03:00
parent 041f7ac171
commit 704781e359
4 changed files with 82 additions and 5 deletions

View File

@@ -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() {
</div>
) : null}
{activeChatId && editingMessage ? (
<div className="mb-2 flex items-start justify-between rounded-lg border border-amber-500/40 bg-amber-500/10 px-3 py-2 text-xs">
<div className="min-w-0">
<p className="font-semibold text-amber-200">Editing message</p>
<p className="truncate text-amber-100/80">{editingMessage.text || "[empty]"}</p>
</div>
<button
className="ml-2 rounded bg-slate-700 px-2 py-1 text-[11px]"
onClick={() => {
setEditingMessage(activeChatId, null);
setText(draftsByChat[activeChatId] ?? "");
}}
type="button"
>
Cancel
</button>
</div>
) : null}
{recordingState !== "idle" ? (
<div className="mb-2 rounded-xl border border-slate-700/80 bg-slate-900/95 px-3 py-2 text-xs text-slate-200">
<div className="flex items-center justify-between">
@@ -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 ? "✓" : "↑"}
</button>
) : (
<button