-
Notifications
You must be signed in to change notification settings - Fork 8
Description
Overview
Add support for editing messages, deleting messages, and emoji reactions in River. These actions are implemented as special message types in the normal message timeline.
Design Principles
- Actions are messages: Edit/Delete/Reaction are special
RoomMessageBodyvariants - Actions reference targets by MessageId: Each action includes the target message's ID
- Actions are signed: The actor signs the action message (author for edit/delete, reactor for reactions)
- Actions stay in timeline: Action messages remain visible until they naturally expire via
max_recent_messages - Effects persist on target: Edited content and reactions are stored on the target message itself, persisting even after the action message expires
- Deleted messages removed: Delete action causes target to be removed from state (action message remains)
Backwards Compatibility
Adding new RoomMessageBody variants will break deserialization for older clients. Options:
- Accept breaking change (acceptable during pre-release development)
- Add
#[serde(other)]fallback variant so old clients skip unknown message types
Recommend: Accept breaking change for now, add fallback before production release.
Data Model Changes
File: common/src/room_state/message.rs
Extend RoomMessageBody enum:
pub enum RoomMessageBody {
// Existing
Public { plaintext: String },
Private { ciphertext: Vec<u8>, nonce: [u8; 12], secret_version: SecretVersion },
// New action types
Edit {
target: MessageId,
new_content: Box<RoomMessageBody>, // Public or Private only (not another action)
},
Delete {
target: MessageId,
},
Reaction {
target: MessageId,
emoji: String, // Single emoji (e.g., "👍")
},
RemoveReaction {
target: MessageId,
emoji: String, // Which reaction to remove
},
}Add reactions and edited flag to MessageV1:
pub struct MessageV1 {
pub room_owner: MemberId,
pub author: MemberId,
pub time: SystemTime,
pub content: RoomMessageBody,
// New fields (with serde defaults for backwards compat)
#[serde(default)]
pub edited: bool,
#[serde(default)]
pub reactions: HashMap<String, Vec<MemberId>>, // emoji -> list of reactors
}Authorization Rules
| Action | Who can perform |
|---|---|
| Edit | Original message author only |
| Delete | Original message author only |
| Reaction | Any room member |
| RemoveReaction | The member who added that specific reaction |
Processing Logic
When applying deltas in MessagesV1::apply_delta():
- Add incoming messages to the list (existing behavior)
- Process action messages (new step, before sorting):
- Edit: Find target by ID, verify actor == target author, replace content, set
edited = true - Delete: Find target by ID, verify actor == target author, remove target from state
- Reaction: Find target by ID, add reactor's MemberId to
reactions[emoji] - RemoveReaction: Find target by ID, remove reactor from
reactions[emoji]
- Edit: Find target by ID, verify actor == target author, replace content, set
- Sort messages by time (existing)
- Prune oldest beyond max_recent_messages (existing)
Action messages remain in the timeline as regular messages (they just have action content instead of text).
Implementation Steps
Phase 1: Data Model (common crate)
File: common/src/room_state/message.rs
- Add new
RoomMessageBodyvariants:Edit,Delete,Reaction,RemoveReaction - Add
edited: boolandreactions: HashMap<String, Vec<MemberId>>toMessageV1 - Add
#[serde(default)]to new fields for partial backwards compat - Add helper methods:
RoomMessageBody::is_action(),RoomMessageBody::target_id() - Update
content_len()to handle action variants (they have no "content" size in traditional sense)
Phase 2: Delta Processing (common crate)
File: common/src/room_state/message.rs - MessagesV1::apply_delta()
- After adding delta messages, iterate through all messages looking for action types
- For each action message:
- Find target message by
MessageIdin current state - Validate authorization (action author must match required permissions)
- Apply the effect to the target message
- If target not found (expired/deleted), action is no-op but stays in timeline
- Find target message by
- Helper functions:
apply_edit(messages, action_msg)- replace target content, set edited=trueapply_delete(messages, action_msg)- remove target from vecapply_reaction(messages, action_msg)- add reactor to target's reactions mapapply_remove_reaction(messages, action_msg)- remove reactor from reactions map
Phase 3: Verification (common crate)
File: common/src/room_state/message.rs - MessagesV1::verify()
- Validate signatures for action messages (already happens)
- For Edit/Delete actions: verify action author == target message author
- For RemoveReaction: verify action author is in the reaction list being removed
- Reactions from any member are valid (no additional check needed)
Phase 4: UI - Display (ui crate)
File: ui/src/components/conversation.rs
- Update message rendering to show "(edited)" indicator when
message.edited == true - Display reactions below message content: show emoji with count, highlight if current user reacted
- Filter out action messages from display OR show them as system messages (e.g., "Alice deleted a message")
- Handle click on own reaction to remove it
Phase 5: UI - Actions (ui crate)
Files: ui/src/components/conversation.rs, possibly new message_actions.rs
- Add context menu (right-click or "..." button) on messages
- Show Edit/Delete options only on own messages
- Add emoji picker for reactions (can start with small fixed set: 👍 ❤️ 😂 😮 😢 😡)
- Implement callbacks:
handle_edit_message(message_id, new_text)- creates Edit action messagehandle_delete_message(message_id)- creates Delete action messagehandle_toggle_reaction(message_id, emoji)- creates Reaction or RemoveReaction
Phase 6: Tests (common crate)
File: common/src/room_state/message.rs tests module
- Test edit: send message, send edit action, verify content updated and edited=true
- Test delete: send message, send delete action, verify message removed
- Test reaction: send message, send reaction, verify reactions map updated
- Test remove reaction: add reaction, remove it, verify reactions map updated
- Test authorization: verify edit/delete by non-author fails
- Test expired target: action on non-existent message is no-op
Key Files
| File | Changes |
|---|---|
common/src/room_state/message.rs |
Data model, delta processing, verification |
ui/src/components/conversation.rs |
Display edited indicator, reactions, action handlers |
ui/src/components/conversation/message_actions.rs |
New file for context menu/emoji picker (optional) |
Edge Cases
- Target expired/deleted: Action is valid signature-wise, but has no effect - stays in timeline
- Multiple edits: Each edit replaces content; edits processed in timestamp order
- Duplicate reactions: Idempotent - adding same reaction twice is no-op
- Private rooms: Edit's
new_contentmust be encrypted with current secret version - Action on action: Should be rejected - cannot edit/delete/react to action messages
- Self-reaction removal: User can only remove their own reactions
Verification Plan
- Unit tests: Test each action type in
common/with authorization checks - Manual UI testing: Create room, send messages, test edit/delete/reactions
- Multi-peer: Verify actions sync correctly between two peers (use
cargo make devwith two browsers)
[AI-assisted - Claude]