use super::*;
impl AppCore {
pub(super) fn toggle_reaction(&mut self, chat_id: &str, message_id: &str, emoji: &str) {
let emoji = emoji.trim();
if chat_id.is_empty() || message_id.is_empty() || emoji.is_empty() {
return;
}
let Some(normalized_chat_id) = self.normalize_chat_id(chat_id) else {
return;
};
if self.reject_removed_group_send(&normalized_chat_id) {
return;
}
let Some(local_owner) = self
.logged_in
.as_ref()
.map(|logged_in| logged_in.owner_pubkey.to_hex())
else {
return;
};
let Some(thread) = self.threads.get_mut(&normalized_chat_id) else {
return;
};
let Some(message) = thread
.messages
.iter_mut()
.find(|message| message.id == message_id)
else {
return;
};
if message.deleted_for_everyone {
return;
}
let outgoing_emoji = toggle_local_reaction(message, &local_owner, emoji);
self.sync_request_acceptance(&normalized_chat_id);
self.send_reaction(&normalized_chat_id, message_id, &outgoing_emoji);
self.persist_best_effort();
self.rebuild_state();
self.emit_state();
}
pub(super) fn send_reaction(&mut self, chat_id: &str, message_id: &str, emoji: &str) {
let Some(owner_pubkey) = self
.logged_in
.as_ref()
.map(|logged_in| logged_in.owner_pubkey)
else {
return;
};
if parse_group_id_from_chat_id(chat_id).is_some() {
self.send_group_event(
chat_id,
REACTION_KIND,
emoji,
vec![vec!["e".to_string(), message_id.to_string()]],
None,
);
return;
}
if let Ok((_, peer)) = parse_peer_input(chat_id) {
let Ok(target_tag) = nostr::Tag::parse(["e", message_id]) else {
return;
};
let millis = self.next_device_sync_control_millis(
chat_id,
REACTION_KIND,
&[target_tag],
unix_now_ms(),
);
if let Ok(unsigned) = pairwise_codec::reaction_event(
owner_pubkey,
message_id.to_string(),
emoji.to_string(),
pairwise_codec::EncodeOptions::new(millis / 1000, millis),
) {
self.send_protocol_engine_unsigned_event(peer, chat_id, unsigned, "reaction");
} else if emoji.is_empty() {
if let Ok(e_tag) = nostr::Tag::parse(["e", message_id]) {
let Ok(ms_tag) = nostr::Tag::parse(["ms", &millis.to_string()]) else {
return;
};
let unsigned = UnsignedEvent::new(
owner_pubkey,
Timestamp::from_secs(millis / 1000),
Kind::Custom(REACTION_KIND as u16),
vec![e_tag, ms_tag],
String::new(),
);
self.send_protocol_engine_unsigned_event(peer, chat_id, unsigned, "reaction");
}
}
}
}
pub(super) fn apply_incoming_reaction_to_chat(
&mut self,
chat_id: &str,
message_id: &str,
sender_hex: &str,
emoji: &str,
) {
let local_owner = self
.logged_in
.as_ref()
.map(|logged_in| logged_in.owner_pubkey.to_hex());
let Some(thread) = self.threads.get_mut(chat_id) else {
return;
};
let Some(message) = thread
.messages
.iter_mut()
.find(|message| message.id == message_id)
else {
return;
};
if message.deleted_for_everyone {
return;
}
apply_reaction_from(message, sender_hex, emoji, local_owner.as_deref());
}
}
fn toggle_local_reaction(
message: &mut ChatMessageSnapshot,
local_owner: &str,
emoji: &str,
) -> String {
let emoji = emoji.trim();
if emoji.is_empty() {
apply_reaction_from(message, local_owner, "", Some(local_owner));
return String::new();
}
let already_picked = message
.reactors
.iter()
.any(|reactor| reactor.author == local_owner && reactor.emoji == emoji);
if already_picked {
apply_reaction_from(message, local_owner, "", Some(local_owner));
String::new()
} else {
apply_reaction_from(message, local_owner, emoji, Some(local_owner));
emoji.to_string()
}
}
fn apply_reaction_from(
message: &mut ChatMessageSnapshot,
sender: &str,
emoji: &str,
local_owner: Option<&str>,
) {
if sender.is_empty() {
return;
}
let emoji = emoji.trim().to_string();
if let Some(index) = message
.reactors
.iter()
.position(|reactor| reactor.author == sender)
{
if emoji.is_empty() {
message.reactors.remove(index);
} else if let Some(reactor) = message.reactors.get_mut(index) {
reactor.emoji = emoji;
}
} else if !emoji.is_empty() {
message.reactors.push(MessageReactor {
author: sender.to_string(),
display_name: String::new(),
picture_url: None,
emoji,
});
}
rebuild_reaction_aggregate(message, local_owner);
}
fn rebuild_reaction_aggregate(message: &mut ChatMessageSnapshot, local_owner: Option<&str>) {
use std::collections::BTreeMap;
let mut counts: BTreeMap<String, (u64, bool)> = BTreeMap::new();
for reactor in &message.reactors {
if reactor.emoji.is_empty() {
continue;
}
let entry = counts.entry(reactor.emoji.clone()).or_insert((0, false));
entry.0 = entry.0.saturating_add(1);
if local_owner.is_some_and(|me| me == reactor.author) {
entry.1 = true;
}
}
let mut reactions: Vec<MessageReactionSnapshot> = counts
.into_iter()
.map(|(emoji, (count, reacted_by_me))| MessageReactionSnapshot {
emoji,
count,
reacted_by_me,
})
.collect();
reactions.sort_by(|left, right| {
right
.reacted_by_me
.cmp(&left.reacted_by_me)
.then_with(|| right.count.cmp(&left.count))
.then_with(|| left.emoji.cmp(&right.emoji))
});
message.reactions = reactions;
}