Add the ability to delete a message (#53)

Message.deleted_at has existed since the initial schema but was never
wired up -- no WS envelope, no permission check, no frontend concept of
it at all. Soft delete, author-only (mirrors the existing edit
permission exactly): content and any attached image/file are cleared
and the underlying MessageImage/MessageFile row and stored file are
actually removed, not just detached, so the message becomes a "This
message was deleted" tombstone with nothing left to recover through a
stale attachment URL. A deleted message can no longer be edited or
reacted to.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
ksmithandClaude Sonnet 5 committed 2026-08-28 16:35:00 -06:00
1 parent 157f1e30ac
commit ef615e1ef4
13 files changed
+477 -8

No files matched your search

+20
View File
@@ -171,6 +171,21 @@ export function ChatPane({
setLive((prev) =>
prev.map((m) => (m.id === envelope.id ? { ...m, reactions: envelope.reactions } : m)),
)
} else if (envelope.type === 'message_deleted' && envelope.room_id === room.id) {
// Mirrors what the server already did to the row (see
// message_service.delete_message) -- content/image/file/preview
// cleared, deleted_at set. `reactions` is left alone; MessageList
// just doesn't render it once deleted_at is set, same as it
// doesn't render anything else here.
const tombstone = {
content: null,
image_id: null,
file: null,
link_preview: null,
deleted_at: new Date().toISOString(),
}
setHistory((prev) => prev.map((m) => (m.id === envelope.id ? { ...m, ...tombstone } : m)))
setLive((prev) => prev.map((m) => (m.id === envelope.id ? { ...m, ...tombstone } : m)))
} else if (envelope.type === 'error') {
setWsError(envelope.detail)
}
@@ -212,6 +227,10 @@ export function ChatPane({
(messageId: string, emoji: string) => socket.sendReaction(room.id, messageId, emoji),
[socket, room.id],
)
const sendDelete = useCallback(
(messageId: string) => socket.sendDelete(room.id, messageId),
[socket, room.id],
)
return (
<section className="chat-pane">
@@ -264,6 +283,7 @@ export function ChatPane({
myRooms={myRooms}
onEdit={sendEdit}
onReact={sendReaction}
onDelete={sendDelete}
/>
<Composer
roomId={room.id}