Let authors edit and delete their own chat messages
The editMessage/deleteMessage IPC and their audit events existed but had no UI. Add an inline editor and delete control on a member's own, non-deleted messages; show an edited marker and render deleted messages as a muted placeholder. Thread the handlers from ChatPage through the timeline.
This commit is contained in:
1 parent
7c5978d5df
commit
b65e059da6
4 files changed
+86
-7
No files matched your search
@@ -207,6 +207,23 @@ export function ChatPage({ snapshot, actions }: PageProps) {
|
||||
decidedByEmployeeId: employee.id,
|
||||
}));
|
||||
|
||||
const editChatMessage = (messageId: string, bodyMd: string) =>
|
||||
void actions.run(() => window.agentCompany.editMessage({
|
||||
orgId,
|
||||
messageId,
|
||||
actorType: "employee",
|
||||
actorId: employee.id,
|
||||
bodyMd,
|
||||
}));
|
||||
|
||||
const deleteChatMessage = (messageId: string) =>
|
||||
void actions.run(() => window.agentCompany.deleteMessage({
|
||||
orgId,
|
||||
messageId,
|
||||
actorType: "employee",
|
||||
actorId: employee.id,
|
||||
}));
|
||||
|
||||
return (
|
||||
<div className="split-main">
|
||||
<ChatSidebar
|
||||
@@ -236,10 +253,13 @@ export function ChatPage({ snapshot, actions }: PageProps) {
|
||||
files={files}
|
||||
context={{ employees, agents }}
|
||||
attachmentFileId={attachmentFileId}
|
||||
currentEmployeeId={employee.id}
|
||||
onAttachmentChange={setAttachmentFileId}
|
||||
onOpenThread={setOpenThreadId}
|
||||
onApprove={(approvalId) => decideApproval(approvalId, "approved")}
|
||||
onDecline={(approvalId) => decideApproval(approvalId, "declined")}
|
||||
onEditMessage={editChatMessage}
|
||||
onDeleteMessage={deleteChatMessage}
|
||||
onSubmit={sendInChannel}
|
||||
composerRef={composerRef}
|
||||
/>
|
||||
|
||||
@@ -27,10 +27,13 @@ export function ChatConversation({
|
||||
files,
|
||||
context,
|
||||
attachmentFileId,
|
||||
currentEmployeeId,
|
||||
onAttachmentChange,
|
||||
onOpenThread,
|
||||
onApprove,
|
||||
onDecline,
|
||||
onEditMessage,
|
||||
onDeleteMessage,
|
||||
onSubmit,
|
||||
composerRef,
|
||||
}: {
|
||||
@@ -46,10 +49,13 @@ export function ChatConversation({
|
||||
files: FileObjectRecord[];
|
||||
context: MessageContext;
|
||||
attachmentFileId: string;
|
||||
currentEmployeeId: string;
|
||||
onAttachmentChange: (next: string) => void;
|
||||
onOpenThread: (threadId: string) => void;
|
||||
onApprove: (approvalId: string) => void;
|
||||
onDecline: (approvalId: string) => void;
|
||||
onEditMessage: (messageId: string, bodyMd: string) => void;
|
||||
onDeleteMessage: (messageId: string) => void;
|
||||
onSubmit: (event: FormEvent<HTMLFormElement>) => void;
|
||||
composerRef: MutableRefObject<HTMLTextAreaElement | null>;
|
||||
}) {
|
||||
@@ -73,9 +79,12 @@ export function ChatConversation({
|
||||
slashInvocations={slashInvocations}
|
||||
files={files}
|
||||
context={context}
|
||||
currentEmployeeId={currentEmployeeId}
|
||||
onOpenThread={onOpenThread}
|
||||
onApprove={onApprove}
|
||||
onDecline={onDecline}
|
||||
onEditMessage={onEditMessage}
|
||||
onDeleteMessage={onDeleteMessage}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { Avatar, Icon, IconButton } from "../../shared/ui";
|
||||
import { useState, type FormEvent, type ReactNode } from "react";
|
||||
import { Avatar, Button, Icon, IconButton } from "../../shared/ui";
|
||||
import type {
|
||||
ChatAttachmentRecord,
|
||||
ChatMentionRecord,
|
||||
@@ -20,6 +20,9 @@ export function ChatMessageRow({
|
||||
onOpenThread,
|
||||
context,
|
||||
compact = false,
|
||||
canModify = false,
|
||||
onEdit,
|
||||
onDelete,
|
||||
}: {
|
||||
message: ChatMessageRecord;
|
||||
attachments: ChatAttachmentRecord[];
|
||||
@@ -30,12 +33,24 @@ export function ChatMessageRow({
|
||||
onOpenThread: (() => void) | null;
|
||||
context: MessageContext;
|
||||
compact?: boolean;
|
||||
canModify?: boolean;
|
||||
onEdit?: (bodyMd: string) => void;
|
||||
onDelete?: () => void;
|
||||
}) {
|
||||
const [editing, setEditing] = useState(false);
|
||||
const name = senderName(message, context);
|
||||
const bot = message.senderType === "agent";
|
||||
const messageFiles = messageAttachments(message.id, attachments, files);
|
||||
const deleted = Boolean(message.deletedAt);
|
||||
const messageFiles = deleted ? [] : messageAttachments(message.id, attachments, files);
|
||||
const messageMentions = mentions.filter((mention) => mention.messageId === message.id);
|
||||
const slashInvocation = slashInvocations.find((invocation) => invocation.messageId === message.id) ?? null;
|
||||
const slashInvocation = deleted ? null : slashInvocations.find((invocation) => invocation.messageId === message.id) ?? null;
|
||||
const submitEdit = (event: FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
const next = new FormData(event.currentTarget).get("body");
|
||||
const value = typeof next === "string" ? next.trim() : "";
|
||||
if (value && onEdit) onEdit(value);
|
||||
setEditing(false);
|
||||
};
|
||||
return (
|
||||
<div style={{ display: "flex", gap: compact ? 10 : 12, marginBottom: compact ? 12 : 18 }}>
|
||||
<Avatar name={name} size={compact ? "sm" : ""} bot={bot} />
|
||||
@@ -43,15 +58,41 @@ export function ChatMessageRow({
|
||||
<div className="row" style={{ alignItems: "baseline", gap: 8, marginBottom: 2 }}>
|
||||
<span style={{ fontSize: 13, fontWeight: 540 }}>{name}</span>
|
||||
<span className="mono" style={{ fontSize: 11, color: "var(--text-3)" }}>{formatTime(message.createdAt)}</span>
|
||||
{message.editedAt && !deleted && <span style={{ fontSize: 11, color: "var(--text-3)" }}>(edited)</span>}
|
||||
{canModify && !editing && (
|
||||
<span style={{ marginLeft: "auto", display: "flex", gap: 2 }}>
|
||||
<IconButton icon="more" label="Edit message" size={13} onClick={() => setEditing(true)} />
|
||||
<IconButton icon="x" label="Delete message" size={13} onClick={onDelete} />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{slashInvocation && (
|
||||
<div className="pill mono" style={{ marginBottom: 6, width: "fit-content" }}>
|
||||
/{slashInvocation.command}
|
||||
</div>
|
||||
)}
|
||||
<div style={{ fontSize: 13, color: "var(--text-1)", lineHeight: 1.5, whiteSpace: "pre-wrap", wordBreak: "break-word" }}>
|
||||
{renderBody(message.bodyMd, messageMentions, context)}
|
||||
</div>
|
||||
{deleted ? (
|
||||
<div style={{ fontSize: 13, color: "var(--text-3)", fontStyle: "italic" }}>Message deleted</div>
|
||||
) : editing ? (
|
||||
<form onSubmit={submitEdit}>
|
||||
<textarea
|
||||
name="body"
|
||||
defaultValue={message.bodyMd}
|
||||
autoFocus
|
||||
aria-label="Edit message"
|
||||
className="input"
|
||||
style={{ minHeight: 60, marginBottom: 6 }}
|
||||
/>
|
||||
<div className="row" style={{ gap: 6 }}>
|
||||
<Button tone="primary" size="small" type="submit">Save</Button>
|
||||
<Button tone="ghost" size="small" onClick={() => setEditing(false)}>Cancel</Button>
|
||||
</div>
|
||||
</form>
|
||||
) : (
|
||||
<div style={{ fontSize: 13, color: "var(--text-1)", lineHeight: 1.5, whiteSpace: "pre-wrap", wordBreak: "break-word" }}>
|
||||
{renderBody(message.bodyMd, messageMentions, context)}
|
||||
</div>
|
||||
)}
|
||||
{messageFiles.length > 0 && (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 8, marginTop: 8 }}>
|
||||
{messageFiles.map((file) => {
|
||||
|
||||
@@ -22,9 +22,12 @@ export function ChatTimeline({
|
||||
slashInvocations,
|
||||
files,
|
||||
context,
|
||||
currentEmployeeId,
|
||||
onOpenThread,
|
||||
onApprove,
|
||||
onDecline,
|
||||
onEditMessage,
|
||||
onDeleteMessage,
|
||||
}: {
|
||||
rootMessages: ChatMessageRecord[];
|
||||
messagesInSpace: ChatMessageRecord[];
|
||||
@@ -35,9 +38,12 @@ export function ChatTimeline({
|
||||
slashInvocations: ChatSlashInvocationRecord[];
|
||||
files: FileObjectRecord[];
|
||||
context: MessageContext;
|
||||
currentEmployeeId: string;
|
||||
onOpenThread: (threadId: string) => void;
|
||||
onApprove: (approvalId: string) => void;
|
||||
onDecline: (approvalId: string) => void;
|
||||
onEditMessage: (messageId: string, bodyMd: string) => void;
|
||||
onDeleteMessage: (messageId: string) => void;
|
||||
}) {
|
||||
const elements: ReactNode[] = [];
|
||||
let lastDay = "";
|
||||
@@ -62,6 +68,9 @@ export function ChatTimeline({
|
||||
replyCount={replyCount}
|
||||
onOpenThread={thread ? () => onOpenThread(thread.id) : null}
|
||||
context={context}
|
||||
canModify={message.senderType === "employee" && message.senderId === currentEmployeeId && !message.deletedAt}
|
||||
onEdit={(bodyMd) => onEditMessage(message.id, bodyMd)}
|
||||
onDelete={() => onDeleteMessage(message.id)}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user