Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion crates/state/src/materialize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,7 @@ fn apply_mutation(state: &mut ServerState, event: &Event) -> ApplyResult {
msg.reactions
.entry(emoji.clone())
.or_default()
.push(event.author);
.insert(event.author);
}
}

Expand Down
55 changes: 53 additions & 2 deletions crates/state/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -628,8 +628,59 @@ fn duplicate_reaction_from_same_peer() {
);

let state = materialize(&dag);
// Implementation-dependent: may deduplicate or not.
assert!(!state.messages[0].reactions.is_empty());
// The same author reacting twice with the same emoji collapses to a
// single entry — reactions are stored as a set keyed by author.
let reactors = state.messages[0]
.reactions
.get("👍")
.expect("emoji should be present");
assert_eq!(reactors.len(), 1);
assert!(reactors.contains(&admin.endpoint_id()));
}

#[test]
fn same_author_duplicate_reaction_is_idempotent() {
let admin = Identity::generate();
let mut dag = test_dag(&admin);

let msg = do_emit(
&mut dag,
&admin,
EventKind::Message {
channel_id: "ch-1".to_string(),
body: "react to me".to_string(),
reply_to: None,
},
);

// Apply two distinct Reaction events with the same emoji from the
// same author. The events themselves are unique (different hashes
// because of timestamps/parents), so dedup must happen at
// materialization time.
do_emit(
&mut dag,
&admin,
EventKind::Reaction {
message_id: msg.hash,
emoji: "🎉".to_string(),
},
);
do_emit(
&mut dag,
&admin,
EventKind::Reaction {
message_id: msg.hash,
emoji: "🎉".to_string(),
},
);

let state = materialize(&dag);
let reactors = state.messages[0]
.reactions
.get("🎉")
.expect("emoji should be present");
assert_eq!(reactors.len(), 1, "duplicate reactions must be deduped");
assert!(reactors.contains(&admin.endpoint_id()));
}

#[test]
Expand Down
6 changes: 4 additions & 2 deletions crates/state/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,10 @@ pub struct ChatMessage {
pub edited: bool,
/// Whether this message has been soft-deleted.
pub deleted: bool,
/// Reactions: emoji string -> list of reactor endpoint IDs.
pub reactions: BTreeMap<String, Vec<EndpointId>>,
/// Reactions: emoji string -> set of reactor endpoint IDs.
/// Stored as a `BTreeSet` so each peer can only react once with a
/// given emoji to a given message.
pub reactions: BTreeMap<String, BTreeSet<EndpointId>>,
/// If this is a reply, the EventHash of the parent message.
pub reply_to: Option<EventHash>,
}
Expand Down
Loading