feat: message deletion support (delete for everyone + delete for me) - #519
Merged
Conversation
Implements full message deletion via the Signal protocol. ## Delete for everyone (DataMessage.delete) - Handle incoming remote deletes from other users - Handle sync deletes from our own devices (uses message_channel() to resolve the channel in a single code path for all channel types) - Add send_delete() to SignalManager trait for outgoing deletes - Add DeleteMessage command with ctrl+d keybinding in message_selected mode - Deleted messages render as '[message deleted]' in italic gray ## Delete for me (SyncMessage.deleteForMe) - Handle incoming deleteForMe sync messages (full message removal) - Add send_delete_for_me() to SignalManager for outgoing deleteForMe - Note to Self and tombstone deletion use deleteForMe (full removal) - Regular channels use delete-for-everyone (tombstone) - Second ctrl+d on a tombstone sends deleteForMe to clean it up ## Storage changes - Add 'deleted' field to Message struct and migration 004 - Add delete_message() (tombstone) and remove_message() (full removal) to Storage trait with implementations for Sqlite, MemCache, Forgetful - Update sqlx offline query cache ## UI changes - Track message removal in the view index to prevent off-by-one selection - Document delete_message command in README Closes boxdot#458
cwedgwood
force-pushed
the
cw/message-deletion
branch
from
April 3, 2026 05:31
4274515 to
17c1405
Compare
This was referenced Apr 5, 2026
|
Tested. Lgtm |
boxdot
pushed a commit
that referenced
this pull request
Apr 26, 2026
Implements Signal's disappearing messages (expiring message timers). Depends on #519 (message deletion) — uses `remove_message()` from that PR. Closes #79 ## Outgoing messages - Read `expire_timer` from Channel, set on outgoing `DataMessage` - Messages sent from gurk now carry the correct timer value ## Incoming messages - Extract `expire_timer` from `DataMessage` (both direct and sync-sent) - Update per-channel timer (stored in DB, persists across restarts) - Handle timer disable (`expire_timer: None` clears channel timer) - Per-message `expire_timer` persisted for correct activation ## Timer activation (start-on-view) - Messages show `[pending]` until channel is viewed for 10+ seconds - After dwell, `expires_at` is set and countdown begins - New messages arriving in an already-viewed channel activate eagerly - Activation runs once per channel selection (no repeated scanning) - Outgoing messages activate immediately (sender starts on send) ## Expiration - Sweep every 5 seconds removes messages past `expires_at` - Only scans channels with `expire_timer` enabled - Partial index on `expires_at` for efficient queries ## UI - Channel title shows timer setting (e.g. `[1m]`, `[1w]`) - Per-message countdown (`[pending]`, `[45s]`, `[expired]`) - `format_duration_short()` helper for consistent formatting ## Schema (migration 005) - `channels`: add `expire_timer` column - `messages`: add `expire_timer` and `expires_at` columns - Partial index on `messages.expires_at` ## Known limitations - Timer starts on receive for viewed channels, not on first read of each individual message (matches Signal Desktop behavior) - Attachment files on disk are not cleaned up when messages expire (tracked separately) - Works around whisperfish/presage#392 (group timer clobbered during upsert) by storing the timer independently on the channel Tested: incoming/outgoing timers, timer disable, persistence across restarts, start-on-view with 10s dwell, countdown UI, expiration sweep.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements full message deletion via the Signal protocol, with parity
to the official Android/iOS clients.
Closes #458
Delete for everyone (
DataMessage.delete)message_channel()to resolve the channel in a single code path for all channel types)
send_delete()toSignalManagertrait for outgoing deletesDeleteMessagecommand withctrl+dkeybinding inmessage_selectedmode[message deleted]in italic grayDelete for me (
SyncMessage.deleteForMe)deleteForMesync messages (full message removal)send_delete_for_me()toSignalManagerfor outgoingdeleteForMedeleteForMe(full removal, no tombstone)ctrl+don a tombstone sendsdeleteForMeto clean it up,matching Android behavior where tombstones can be dismissed
Storage changes
deletedfield toMessagestruct and migration 004delete_message()(tombstone) andremove_message()(full removal)to
Storagetrait with implementations for Sqlite, MemCache, ForgetfulUI changes
when multiple messages are deleted in sequence
delete_messagecommand in READMETested manually: receiving deletions in group chats, direct messages,
and Note to Self; sending deletions from gurk; delete-for-me on
tombstones; verified other users' messages cannot be deleted.