1use std::cmp::Ordering;
18use std::collections::BTreeMap;
19use std::fmt;
20
21use anyhow::Result;
22use serde::{Deserialize, Serialize};
23
24use crate::chat::{Chat, ChatId, send_msg};
25use crate::chatlist_events;
26use crate::contact::ContactId;
27use crate::context::Context;
28use crate::events::EventType;
29use crate::message::{Message, MsgId, rfc724_mid_exists};
30use crate::param::Param;
31
32#[derive(Debug, Default, Clone, Deserialize, Eq, PartialEq, Serialize)]
36pub struct Reaction {
37 reaction: String,
39}
40
41impl From<&str> for Reaction {
45 fn from(reaction: &str) -> Self {
60 let mut emojis: Vec<&str> = reaction
61 .split_ascii_whitespace()
62 .filter(|&emoji| emoji.len() < 30)
63 .collect();
64 emojis.sort_unstable();
65 emojis.dedup();
66 let reaction = emojis.join(" ");
67 Self { reaction }
68 }
69}
70
71impl Reaction {
72 pub fn is_empty(&self) -> bool {
74 self.reaction.is_empty()
75 }
76
77 pub fn emojis(&self) -> Vec<&str> {
79 self.reaction.split(' ').collect()
80 }
81
82 pub fn as_str(&self) -> &str {
84 &self.reaction
85 }
86
87 pub fn add(&self, other: Self) -> Self {
89 let mut emojis: Vec<&str> = self.emojis();
90 emojis.append(&mut other.emojis());
91 emojis.sort_unstable();
92 emojis.dedup();
93 let reaction = emojis.join(" ");
94 Self { reaction }
95 }
96}
97
98#[derive(Debug)]
100pub struct Reactions {
101 reactions: BTreeMap<ContactId, Reaction>,
103}
104
105impl Reactions {
106 pub fn contacts(&self) -> Vec<ContactId> {
108 self.reactions.keys().copied().collect()
109 }
110
111 pub fn get(&self, contact_id: ContactId) -> Reaction {
116 self.reactions.get(&contact_id).cloned().unwrap_or_default()
117 }
118
119 pub fn is_empty(&self) -> bool {
121 self.reactions.is_empty()
122 }
123
124 #[expect(clippy::arithmetic_side_effects)]
126 pub fn emoji_frequencies(&self) -> BTreeMap<String, usize> {
127 let mut emoji_frequencies: BTreeMap<String, usize> = BTreeMap::new();
128 for reaction in self.reactions.values() {
129 for emoji in reaction.emojis() {
130 emoji_frequencies
131 .entry(emoji.to_string())
132 .and_modify(|x| *x += 1)
133 .or_insert(1);
134 }
135 }
136 emoji_frequencies
137 }
138
139 pub fn emoji_sorted_by_frequency(&self) -> Vec<(String, usize)> {
145 let mut emoji_frequencies: Vec<(String, usize)> =
146 self.emoji_frequencies().into_iter().collect();
147 emoji_frequencies.sort_by(|(a, a_count), (b, b_count)| {
148 match a_count.cmp(b_count).reverse() {
149 Ordering::Equal => a.cmp(b),
150 other => other,
151 }
152 });
153 emoji_frequencies
154 }
155}
156
157impl fmt::Display for Reactions {
158 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
159 let emoji_frequencies = self.emoji_sorted_by_frequency();
160 let mut first = true;
161 for (emoji, frequency) in emoji_frequencies {
162 if !first {
163 write!(f, " ")?;
164 }
165 first = false;
166 write!(f, "{emoji}{frequency}")?;
167 }
168 Ok(())
169 }
170}
171
172async fn set_msg_id_reaction(
173 context: &Context,
174 msg_id: MsgId,
175 chat_id: ChatId,
176 contact_id: ContactId,
177 timestamp: i64,
178 reaction: &Reaction,
179) -> Result<()> {
180 if reaction.is_empty() {
181 context
183 .sql
184 .execute(
185 "DELETE FROM reactions
186 WHERE msg_id = ?1
187 AND contact_id = ?2",
188 (msg_id, contact_id),
189 )
190 .await?;
191 } else {
192 context
193 .sql
194 .execute(
195 "INSERT INTO reactions (msg_id, contact_id, reaction)
196 VALUES (?1, ?2, ?3)
197 ON CONFLICT(msg_id, contact_id)
198 DO UPDATE SET reaction=excluded.reaction",
199 (msg_id, contact_id, reaction.as_str()),
200 )
201 .await?;
202 let mut chat = Chat::load_from_db(context, chat_id).await?;
203 if chat
204 .param
205 .update_timestamp(Param::LastReactionTimestamp, timestamp)?
206 {
207 chat.param
208 .set_i64(Param::LastReactionMsgId, i64::from(msg_id.to_u32()));
209 chat.param
210 .set_i64(Param::LastReactionContactId, i64::from(contact_id.to_u32()));
211 chat.update_param(context).await?;
212 }
213 }
214
215 context.emit_event(EventType::ReactionsChanged {
216 chat_id,
217 msg_id,
218 contact_id,
219 });
220 chatlist_events::emit_chatlist_item_changed(context, chat_id);
221 Ok(())
222}
223
224pub async fn send_reaction(context: &Context, msg_id: MsgId, reaction: &str) -> Result<MsgId> {
229 let msg = Message::load_from_db(context, msg_id).await?;
230 let chat_id = msg.chat_id;
231
232 let reaction: Reaction = reaction.into();
233 let mut reaction_msg = Message::new_text(reaction.as_str().to_string());
234 reaction_msg.set_reaction();
235 reaction_msg.in_reply_to = Some(msg.rfc724_mid);
236 reaction_msg.hidden = true;
237
238 let reaction_msg_id = send_msg(context, chat_id, &mut reaction_msg).await?;
240
241 set_msg_id_reaction(
243 context,
244 msg_id,
245 msg.chat_id,
246 ContactId::SELF,
247 reaction_msg.timestamp_sort,
248 &reaction,
249 )
250 .await?;
251 Ok(reaction_msg_id)
252}
253
254pub async fn add_reaction(context: &Context, msg_id: MsgId, reaction: &str) -> Result<MsgId> {
261 let self_reaction = get_self_reaction(context, msg_id).await?;
262 let reaction = self_reaction.add(Reaction::from(reaction));
263 send_reaction(context, msg_id, reaction.as_str()).await
264}
265
266pub(crate) async fn set_msg_reaction(
273 context: &Context,
274 in_reply_to: &str,
275 chat_id: ChatId,
276 contact_id: ContactId,
277 timestamp: i64,
278 reaction: Reaction,
279 is_incoming_fresh: bool,
280) -> Result<()> {
281 if let Some(msg_id) = rfc724_mid_exists(context, in_reply_to).await? {
282 set_msg_id_reaction(context, msg_id, chat_id, contact_id, timestamp, &reaction).await?;
283
284 if is_incoming_fresh
285 && !reaction.is_empty()
286 && msg_id.get_state(context).await?.is_outgoing()
287 {
288 context.emit_event(EventType::IncomingReaction {
289 chat_id,
290 contact_id,
291 msg_id,
292 reaction,
293 });
294 }
295 } else {
296 info!(
297 context,
298 "Can't assign reaction to unknown message with Message-ID {}", in_reply_to
299 );
300 }
301 Ok(())
302}
303
304async fn get_self_reaction(context: &Context, msg_id: MsgId) -> Result<Reaction> {
306 let reaction_str: Option<String> = context
307 .sql
308 .query_get_value(
309 "SELECT reaction
310 FROM reactions
311 WHERE msg_id=? AND contact_id=?",
312 (msg_id, ContactId::SELF),
313 )
314 .await?;
315 Ok(reaction_str
316 .as_deref()
317 .map(Reaction::from)
318 .unwrap_or_default())
319}
320
321pub async fn get_msg_reactions(context: &Context, msg_id: MsgId) -> Result<Reactions> {
323 let reactions: BTreeMap<ContactId, Reaction> = context
324 .sql
325 .query_map_collect(
326 "SELECT contact_id, reaction FROM reactions WHERE msg_id=?",
327 (msg_id,),
328 |row| {
329 let contact_id: ContactId = row.get(0)?;
330 let reaction: String = row.get(1)?;
331 Ok((contact_id, Reaction::from(reaction.as_str())))
332 },
333 )
334 .await?;
335 Ok(Reactions { reactions })
336}
337
338impl Chat {
339 pub async fn get_last_reaction_if_newer_than(
343 &self,
344 context: &Context,
345 timestamp: i64,
346 ) -> Result<Option<(Message, ContactId, String)>> {
347 if self
348 .param
349 .get_i64(Param::LastReactionTimestamp)
350 .is_none_or(|reaction_timestamp| reaction_timestamp <= timestamp)
351 {
352 return Ok(None);
353 };
354 let reaction_msg_id = MsgId::new(
355 self.param
356 .get_int(Param::LastReactionMsgId)
357 .unwrap_or_default() as u32,
358 );
359 let Some(reaction_msg) = Message::load_from_db_optional(context, reaction_msg_id).await?
360 else {
361 return Ok(None);
365 };
366 let reaction_contact_id = ContactId::new(
367 self.param
368 .get_int(Param::LastReactionContactId)
369 .unwrap_or_default() as u32,
370 );
371 if let Some(reaction) = context
372 .sql
373 .query_get_value(
374 "SELECT reaction FROM reactions WHERE msg_id=? AND contact_id=?",
375 (reaction_msg.id, reaction_contact_id),
376 )
377 .await?
378 {
379 Ok(Some((reaction_msg, reaction_contact_id, reaction)))
380 } else {
381 Ok(None)
382 }
383 }
384}
385
386#[cfg(test)]
387mod tests {
388 use deltachat_contact_tools::ContactAddress;
389
390 use super::*;
391 use crate::chat::{forward_msgs, get_chat_msgs, marknoticed_chat, send_text_msg};
392 use crate::chatlist::Chatlist;
393 use crate::config::Config;
394 use crate::contact::{Contact, Origin};
395 use crate::key::{load_self_public_key, load_self_secret_key};
396 use crate::message::{MessageState, Viewtype, delete_msgs, markseen_msgs};
397 use crate::pgp::{SeipdVersion, pk_encrypt};
398 use crate::receive_imf::receive_imf;
399 use crate::sql::housekeeping;
400 use crate::test_utils::E2EE_INFO_MSGS;
401 use crate::test_utils::TestContext;
402 use crate::test_utils::TestContextManager;
403 use crate::tools::SystemTime;
404 use std::time::Duration;
405
406 #[test]
407 fn test_parse_reaction() {
408 assert_eq!(Reaction::from("๐").emojis(), vec!["๐"]);
410 assert_eq!(Reaction::from("๐").emojis(), vec!["๐"]);
411 assert_eq!(Reaction::from("๐").emojis(), vec!["๐"]);
412 assert_eq!(Reaction::from("โน").emojis(), vec!["โน"]);
413 assert_eq!(Reaction::from("๐ข").emojis(), vec!["๐ข"]);
414
415 assert!(Reaction::from("").is_empty());
417
418 assert_eq!(Reaction::from(":deltacat:").emojis(), vec![":deltacat:"]);
421
422 assert!(
424 Reaction::from(":foobarbazquuxaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:").is_empty()
425 );
426
427 assert_eq!(Reaction::from("๐ โค").emojis(), vec!["โค", "๐"]);
429 assert_eq!(Reaction::from("๐\tโค").emojis(), vec!["โค", "๐"]);
430
431 assert_eq!(
433 Reaction::from("๐\t:foo: โค").emojis(),
434 vec![":foo:", "โค", "๐"]
435 );
436 assert_eq!(Reaction::from("๐\t:foo: โค").as_str(), ":foo: โค ๐");
437
438 assert_eq!(Reaction::from("๐ ๐").emojis(), vec!["๐"]);
440 }
441
442 #[test]
443 fn test_add_reaction() {
444 let reaction1 = Reaction::from("๐ ๐");
445 let reaction2 = Reaction::from("โค");
446 let reaction_sum = reaction1.add(reaction2);
447
448 assert_eq!(reaction_sum.emojis(), vec!["โค", "๐", "๐"]);
449 }
450
451 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
452 async fn test_receive_reaction() -> Result<()> {
453 let alice = TestContext::new_alice().await;
454
455 receive_imf(
457 &alice,
458 "To: bob@example.net\n\
459From: alice@example.org\n\
460Date: Today, 29 February 2021 00:00:00 -800\n\
461Message-ID: 12345@example.org\n\
462Subject: Meeting\n\
463\n\
464Can we chat at 1pm pacific, today?"
465 .as_bytes(),
466 false,
467 )
468 .await?;
469 let msg = alice.get_last_msg().await;
470 assert_eq!(msg.state, MessageState::OutDelivered);
471 let reactions = get_msg_reactions(&alice, msg.id).await?;
472 let contacts = reactions.contacts();
473 assert_eq!(contacts.len(), 0);
474
475 let bob_id = Contact::add_or_lookup(
476 &alice,
477 "",
478 &ContactAddress::new("bob@example.net")?,
479 Origin::ManuallyCreated,
480 )
481 .await?
482 .0;
483 let bob_reaction = reactions.get(bob_id);
484 assert!(bob_reaction.is_empty()); receive_imf(
488 &alice,
489 "To: alice@example.org\n\
490From: bob@example.net\n\
491Date: Today, 29 February 2021 00:00:10 -800\n\
492Message-ID: 56789@example.net\n\
493In-Reply-To: 12345@example.org\n\
494Subject: Meeting\n\
495Mime-Version: 1.0 (1.0)\n\
496Content-Type: text/plain; charset=utf-8\n\
497Content-Disposition: reaction\n\
498\n\
499\u{1F44D}"
500 .as_bytes(),
501 false,
502 )
503 .await?;
504
505 let reactions = get_msg_reactions(&alice, msg.id).await?;
506 assert_eq!(reactions.to_string(), "๐1");
507
508 let contacts = reactions.contacts();
509 assert_eq!(contacts.len(), 1);
510
511 assert_eq!(contacts.first(), Some(&bob_id));
512 let bob_reaction = reactions.get(bob_id);
513 assert_eq!(bob_reaction.is_empty(), false);
514 assert_eq!(bob_reaction.emojis(), vec!["๐"]);
515 assert_eq!(bob_reaction.as_str(), "๐");
516
517 receive_imf(
519 &alice,
520 "To: alice@example.org\n\
521From: bob@example.net\n\
522Date: Today, 29 February 2021 00:00:10 -800\n\
523Message-ID: 56790@example.net\n\
524In-Reply-To: 12345@example.org\n\
525Subject: Meeting\n\
526Mime-Version: 1.0 (1.0)\n\
527Content-Type: text/plain; charset=utf-8\n\
528Content-Disposition: reaction\n\
529\n\
530๐\n\
531\n\
532--\n\
533_______________________________________________\n\
534Here's my footer -- bob@example.net"
535 .as_bytes(),
536 false,
537 )
538 .await?;
539
540 let reactions = get_msg_reactions(&alice, msg.id).await?;
541 assert_eq!(reactions.to_string(), "๐1");
542
543 let msg_bob = receive_imf(
545 &alice,
546 "To: alice@example.org\n\
547From: bob@example.net\n\
548Date: Today, 29 February 2021 00:00:10 -800\n\
549Message-ID: 56791@example.net\n\
550In-Reply-To: 12345@example.org\n\
551Mime-Version: 1.0\n\
552Content-Type: multipart/mixed; boundary=\"YiEDa0DAkWCtVeE4\"\n\
553Content-Disposition: inline\n\
554\n\
555--YiEDa0DAkWCtVeE4\n\
556Content-Type: text/plain; charset=utf-8\n\
557Content-Disposition: inline\n\
558\n\
559Reply + reaction\n\
560\n\
561--YiEDa0DAkWCtVeE4\n\
562Content-Type: text/plain; charset=utf-8\n\
563Content-Disposition: reaction\n\
564\n\
565\u{1F44D}\n\
566\n\
567--YiEDa0DAkWCtVeE4--"
568 .as_bytes(),
569 false,
570 )
571 .await?
572 .unwrap();
573 let msg_bob = Message::load_from_db(&alice, msg_bob.msg_ids[0]).await?;
574 assert_eq!(msg_bob.from_id, bob_id);
575 assert_eq!(msg_bob.chat_id, msg.chat_id);
576 assert_eq!(msg_bob.viewtype, Viewtype::Text);
577 assert_eq!(msg_bob.state, MessageState::InFresh);
578 assert_eq!(msg_bob.hidden, false);
579 assert_eq!(msg_bob.text, "Reply + reaction");
580 let reactions = get_msg_reactions(&alice, msg.id).await?;
581 assert_eq!(reactions.to_string(), "๐1");
582
583 Ok(())
584 }
585
586 async fn expect_reactions_changed_event(
587 t: &TestContext,
588 expected_chat_id: ChatId,
589 expected_msg_id: MsgId,
590 expected_contact_id: ContactId,
591 ) -> Result<()> {
592 let event = t
593 .evtracker
594 .get_matching(|evt| {
595 matches!(
596 evt,
597 EventType::ReactionsChanged { .. } | EventType::IncomingMsg { .. }
598 )
599 })
600 .await;
601 match event {
602 EventType::ReactionsChanged {
603 chat_id,
604 msg_id,
605 contact_id,
606 } => {
607 assert_eq!(chat_id, expected_chat_id);
608 assert_eq!(msg_id, expected_msg_id);
609 assert_eq!(contact_id, expected_contact_id);
610 }
611 _ => panic!("Unexpected event {event:?}."),
612 }
613 Ok(())
614 }
615
616 async fn expect_incoming_reactions_event(
617 t: &TestContext,
618 expected_chat_id: ChatId,
619 expected_msg_id: MsgId,
620 expected_contact_id: ContactId,
621 expected_reaction: &str,
622 ) -> Result<()> {
623 let event = t
624 .evtracker
625 .get_matching(|evt| {
628 matches!(
629 evt,
630 EventType::IncomingReaction { .. } | EventType::IncomingMsg { .. }
631 )
632 })
633 .await;
634 match event {
635 EventType::IncomingReaction {
636 chat_id,
637 msg_id,
638 contact_id,
639 reaction,
640 } => {
641 assert_eq!(chat_id, expected_chat_id);
642 assert_eq!(msg_id, expected_msg_id);
643 assert_eq!(contact_id, expected_contact_id);
644 assert_eq!(reaction, Reaction::from(expected_reaction));
645 }
646 _ => panic!("Unexpected event {event:?}."),
647 }
648 Ok(())
649 }
650
651 async fn expect_no_unwanted_events(t: &TestContext) {
653 let ev = t
654 .evtracker
655 .get_matching_opt(t, |evt| {
656 matches!(
657 evt,
658 EventType::IncomingReaction { .. }
659 | EventType::IncomingMsg { .. }
660 | EventType::MsgsChanged { .. }
661 )
662 })
663 .await;
664 if let Some(ev) = ev {
665 panic!("Unwanted event {ev:?}.")
666 }
667 }
668
669 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
670 async fn test_send_reaction() -> Result<()> {
671 let alice = TestContext::new_alice().await;
672 let bob = TestContext::new_bob().await;
673
674 alice
676 .set_config(
677 Config::Selfstatus,
678 Some("Buy Delta Chat today and make this banner go away!"),
679 )
680 .await?;
681 bob.set_config(Config::Selfstatus, Some("Sent from my Delta Chat Pro. ๐"))
682 .await?;
683
684 let chat_alice = alice.create_chat(&bob).await;
685 let alice_msg = alice.send_text(chat_alice.id, "Hi!").await;
686 let bob_msg = bob.recv_msg(&alice_msg).await;
687 assert_eq!(
688 get_chat_msgs(&alice, chat_alice.id).await?.len(),
689 E2EE_INFO_MSGS + 1
690 );
691 assert_eq!(
692 get_chat_msgs(&bob, bob_msg.chat_id).await?.len(),
693 E2EE_INFO_MSGS + 1
694 );
695
696 let alice_msg2 = alice.send_text(chat_alice.id, "Hi again!").await;
697 bob.recv_msg(&alice_msg2).await;
698 assert_eq!(
699 get_chat_msgs(&alice, chat_alice.id).await?.len(),
700 E2EE_INFO_MSGS + 2
701 );
702 assert_eq!(
703 get_chat_msgs(&bob, bob_msg.chat_id).await?.len(),
704 E2EE_INFO_MSGS + 2
705 );
706
707 bob_msg.chat_id.accept(&bob).await?;
708
709 bob.evtracker.clear_events();
710 send_reaction(&bob, bob_msg.id, "๐").await.unwrap();
711 expect_reactions_changed_event(&bob, bob_msg.chat_id, bob_msg.id, ContactId::SELF).await?;
712 expect_no_unwanted_events(&bob).await;
713 assert_eq!(
714 get_chat_msgs(&bob, bob_msg.chat_id).await?.len(),
715 E2EE_INFO_MSGS + 2
716 );
717
718 let bob_reaction_msg = bob.pop_sent_msg().await;
719 let alice_reaction_msg = alice.recv_msg_hidden(&bob_reaction_msg).await;
720 assert_eq!(alice_reaction_msg.state, MessageState::InFresh);
721 assert_eq!(
722 get_chat_msgs(&alice, chat_alice.id).await?.len(),
723 E2EE_INFO_MSGS + 2
724 );
725
726 let reactions = get_msg_reactions(&alice, alice_msg.sender_msg_id).await?;
727 assert_eq!(reactions.to_string(), "๐1");
728 let contacts = reactions.contacts();
729 assert_eq!(contacts.len(), 1);
730 let bob_id = contacts.first().unwrap();
731 let bob_reaction = reactions.get(*bob_id);
732 assert_eq!(bob_reaction.is_empty(), false);
733 assert_eq!(bob_reaction.emojis(), vec!["๐"]);
734 assert_eq!(bob_reaction.as_str(), "๐");
735 expect_reactions_changed_event(&alice, chat_alice.id, alice_msg.sender_msg_id, *bob_id)
736 .await?;
737 expect_incoming_reactions_event(
738 &alice,
739 chat_alice.id,
740 alice_msg.sender_msg_id,
741 *bob_id,
742 "๐",
743 )
744 .await?;
745 expect_no_unwanted_events(&alice).await;
746
747 marknoticed_chat(&alice, chat_alice.id).await?;
748 assert_eq!(
749 alice_reaction_msg.id.get_state(&alice).await?,
750 MessageState::InSeen
751 );
752 assert_eq!(
754 alice
755 .sql
756 .count("SELECT COUNT(*) FROM smtp_mdns", ())
757 .await?,
758 1
759 );
760 assert_eq!(
761 alice
762 .sql
763 .count(
764 "SELECT COUNT(*) FROM smtp_mdns WHERE from_id=?",
765 (ContactId::SELF,)
766 )
767 .await?,
768 1
769 );
770
771 send_reaction(&alice, alice_msg.sender_msg_id, "๐ ๐")
773 .await
774 .unwrap();
775 let reactions = get_msg_reactions(&alice, alice_msg.sender_msg_id).await?;
776 assert_eq!(reactions.to_string(), "๐2 ๐1");
777
778 assert_eq!(
779 reactions.emoji_sorted_by_frequency(),
780 vec![("๐".to_string(), 2), ("๐".to_string(), 1)]
781 );
782
783 Ok(())
784 }
785
786 async fn assert_summary(t: &TestContext, expected: &str) {
787 let chatlist = Chatlist::try_load(t, 0, None, None).await.unwrap();
788 let summary = chatlist.get_summary(t, 0, None).await.unwrap();
789 assert_eq!(summary.text, expected);
790 }
791
792 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
793 async fn test_reaction_summary() -> Result<()> {
794 let mut tcm = TestContextManager::new();
795 let alice = tcm.alice().await;
796 let bob = tcm.bob().await;
797 alice.set_config(Config::Displayname, Some("ALICE")).await?;
798 bob.set_config(Config::Displayname, Some("BOB")).await?;
799 let alice_bob_id = alice.add_or_lookup_contact_id(&bob).await;
800
801 let alice_chat = alice.create_chat(&bob).await;
803 let alice_msg1 = alice.send_text(alice_chat.id, "Party?").await;
804 let bob_msg1 = bob.recv_msg(&alice_msg1).await;
805
806 SystemTime::shift(Duration::from_secs(10));
808 bob_msg1.chat_id.accept(&bob).await?;
809 send_reaction(&bob, bob_msg1.id, "๐").await?;
810 let bob_send_reaction = bob.pop_sent_msg().await;
811 alice.recv_msg_hidden(&bob_send_reaction).await;
812 expect_incoming_reactions_event(
813 &alice,
814 alice_chat.id,
815 alice_msg1.sender_msg_id,
816 alice_bob_id,
817 "๐",
818 )
819 .await?;
820 expect_no_unwanted_events(&alice).await;
821
822 let chatlist = Chatlist::try_load(&bob, 0, None, None).await?;
823 let summary = chatlist.get_summary(&bob, 0, None).await?;
824 assert_eq!(summary.text, "You reacted ๐ to \"Party?\"");
825 assert_eq!(summary.timestamp, bob_msg1.get_timestamp()); assert_eq!(summary.state, MessageState::InFresh); assert!(summary.prefix.is_none());
828 assert!(summary.thumbnail_path.is_none());
829 assert_summary(&alice, "BOB reacted ๐ to \"Party?\"").await;
830
831 SystemTime::shift(Duration::from_secs(10));
833 send_reaction(&alice, alice_msg1.sender_msg_id, "๐ฟ").await?;
834 let alice_send_reaction = alice.pop_sent_msg().await;
835 bob.evtracker.clear_events();
836 bob.recv_msg_opt(&alice_send_reaction).await;
837 expect_no_unwanted_events(&bob).await;
838
839 assert_summary(&alice, "You reacted ๐ฟ to \"Party?\"").await;
840 assert_summary(&bob, "ALICE reacted ๐ฟ to \"Party?\"").await;
841
842 SystemTime::shift(Duration::from_secs(10));
844 let alice_msg2 = alice.send_text(alice_chat.id, "kewl").await;
845 bob.recv_msg(&alice_msg2).await;
846
847 assert_summary(&alice, "kewl").await;
848 assert_summary(&bob, "kewl").await;
849
850 SystemTime::shift(Duration::from_secs(10));
852 send_reaction(&alice, alice_msg1.sender_msg_id, "๐ค").await?;
853 let alice_send_reaction = alice.pop_sent_msg().await;
854 bob.recv_msg_opt(&alice_send_reaction).await;
855
856 assert_summary(&alice, "You reacted ๐ค to \"Party?\"").await;
857 assert_summary(&bob, "ALICE reacted ๐ค to \"Party?\"").await;
858
859 SystemTime::shift(Duration::from_secs(10));
861 send_reaction(&alice, alice_msg1.sender_msg_id, "").await?;
862 let alice_remove_reaction = alice.pop_sent_msg().await;
863 bob.recv_msg_opt(&alice_remove_reaction).await;
864
865 assert_summary(&alice, "kewl").await;
866 assert_summary(&bob, "kewl").await;
867
868 SystemTime::shift(Duration::from_secs(10));
870 send_reaction(&alice, alice_msg1.sender_msg_id, "๐งน").await?;
871 assert_summary(&alice, "You reacted ๐งน to \"Party?\"").await;
872
873 delete_msgs(&alice, &[alice_msg1.sender_msg_id]).await?; assert_summary(&alice, "kewl").await;
875 housekeeping(&alice).await?; assert_summary(&alice, "kewl").await;
877
878 Ok(())
879 }
880
881 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
882 async fn test_reaction_forwarded_summary() -> Result<()> {
883 let alice = TestContext::new_alice().await;
884
885 let self_chat = alice.get_self_chat().await;
887 let msg_id = send_text_msg(&alice, self_chat.id, "foo".to_string()).await?;
888 assert_summary(&alice, "foo").await;
889
890 SystemTime::shift(Duration::from_secs(10));
892 send_reaction(&alice, msg_id, "๐ซ").await?;
893 assert_summary(&alice, "You reacted ๐ซ to \"foo\"").await;
894 let reactions = get_msg_reactions(&alice, msg_id).await?;
895 assert_eq!(reactions.reactions.len(), 1);
896
897 let bob_id = Contact::create(&alice, "", "bob@example.net").await?;
899 let bob_chat_id = ChatId::create_for_contact(&alice, bob_id).await?;
900 forward_msgs(&alice, &[msg_id], bob_chat_id).await?;
901 assert_summary(&alice, "Forwarded: foo").await; let chatlist = Chatlist::try_load(&alice, 0, None, None).await.unwrap();
903 let forwarded_msg_id = chatlist.get_msg_id(0)?.unwrap();
904 let reactions = get_msg_reactions(&alice, forwarded_msg_id).await?;
905 assert!(reactions.reactions.is_empty()); SystemTime::shift(Duration::from_secs(10));
910 send_reaction(&alice, forwarded_msg_id, "๐ณ").await?;
911 assert_summary(&alice, "You reacted ๐ณ to \"foo\"").await;
912 let reactions = get_msg_reactions(&alice, msg_id).await?;
913 assert_eq!(reactions.reactions.len(), 1);
914
915 Ok(())
916 }
917
918 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
919 async fn test_reaction_self_chat_multidevice_summary() -> Result<()> {
920 let alice0 = TestContext::new_alice().await;
921 let alice1 = TestContext::new_alice().await;
922 let chat = alice0.get_self_chat().await;
923
924 let msg_id = send_text_msg(&alice0, chat.id, "mom's birthday!".to_string()).await?;
925 alice1.recv_msg(&alice0.pop_sent_msg().await).await;
926
927 SystemTime::shift(Duration::from_secs(10));
928 send_reaction(&alice0, msg_id, "๐").await?;
929 let sync = alice0.pop_sent_msg().await;
930 receive_imf(&alice1, sync.payload().as_bytes(), false).await?;
931
932 assert_summary(&alice0, "You reacted ๐ to \"mom's birthday!\"").await;
933 assert_summary(&alice1, "You reacted ๐ to \"mom's birthday!\"").await;
934
935 Ok(())
936 }
937
938 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
939 async fn test_send_reaction_multidevice() -> Result<()> {
940 let mut tcm = TestContextManager::new();
941 let alice0 = tcm.alice().await;
942 let alice1 = tcm.alice().await;
943 let bob = tcm.bob().await;
944 let chat_id = alice0.create_chat(&bob).await.id;
945
946 let alice0_msg_id = send_text_msg(&alice0, chat_id, "foo".to_string()).await?;
947 let alice1_msg = alice1.recv_msg(&alice0.pop_sent_msg().await).await;
948
949 send_reaction(&alice0, alice0_msg_id, "๐").await?;
950 alice1.recv_msg_hidden(&alice0.pop_sent_msg().await).await;
951
952 expect_reactions_changed_event(&alice0, chat_id, alice0_msg_id, ContactId::SELF).await?;
953 expect_reactions_changed_event(&alice1, alice1_msg.chat_id, alice1_msg.id, ContactId::SELF)
954 .await?;
955 for a in [&alice0, &alice1] {
956 expect_no_unwanted_events(a).await;
957 }
958 Ok(())
959 }
960
961 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
970 async fn test_reaction_request_mdn() -> Result<()> {
971 let mut tcm = TestContextManager::new();
972 let alice = &tcm.alice().await;
973 let bob = &tcm.bob().await;
974
975 let alice_chat_id = alice.create_chat_id(bob).await;
976 let alice_sent_msg = alice.send_text(alice_chat_id, "Hello!").await;
977
978 let bob_msg = bob.recv_msg(&alice_sent_msg).await;
979 bob_msg.chat_id.accept(bob).await?;
980 assert_eq!(bob_msg.state, MessageState::InFresh);
981 let bob_chat_id = bob_msg.chat_id;
982 bob_chat_id.accept(bob).await?;
983
984 markseen_msgs(bob, vec![bob_msg.id]).await?;
985 assert_eq!(
986 bob.sql
987 .count(
988 "SELECT COUNT(*) FROM smtp_mdns WHERE from_id!=?",
989 (ContactId::SELF,)
990 )
991 .await?,
992 1
993 );
994 bob.sql.execute("DELETE FROM smtp_mdns", ()).await?;
995
996 let known_id = bob_msg.rfc724_mid;
999 let new_id = "e2b6e69e-4124-4e2a-b79f-e4f1be667165@localhost";
1000
1001 let plain_text = format!(
1002 "Content-Type: text/plain; charset=\"utf-8\"; protected-headers=\"v1\"; \r
1003 hp=\"cipher\"\r
1004Content-Disposition: reaction\r
1005From: \"Alice\" <alice@example.org>\r
1006To: \"Bob\" <bob@example.net>\r
1007Subject: Message from Alice\r
1008Date: Sat, 14 Mar 2026 01:02:03 +0000\r
1009In-Reply-To: <{known_id}>\r
1010References: <{known_id}>\r
1011Chat-Version: 1.0\r
1012Chat-Disposition-Notification-To: alice@example.org\r
1013Message-ID: <{new_id}>\r
1014HP-Outer: From: <alice@example.org>\r
1015HP-Outer: To: \"hidden-recipients\": ;\r
1016HP-Outer: Subject: [...]\r
1017HP-Outer: Date: Sat, 14 Mar 2026 01:02:03 +0000\r
1018HP-Outer: Message-ID: <{new_id}>\r
1019HP-Outer: In-Reply-To: <{known_id}>\r
1020HP-Outer: References: <{known_id}>\r
1021HP-Outer: Chat-Version: 1.0\r
1022Content-Transfer-Encoding: base64\r
1023\r
10248J+RgA==\r
1025"
1026 );
1027
1028 let alice_public_key = load_self_public_key(alice).await?;
1029 let bob_public_key = load_self_public_key(bob).await?;
1030 let alice_secret_key = load_self_secret_key(alice).await?;
1031 let public_keys_for_encryption = vec![alice_public_key, bob_public_key];
1032 let compress = true;
1033 let encrypted_payload = pk_encrypt(
1034 plain_text.as_bytes().to_vec(),
1035 public_keys_for_encryption,
1036 alice_secret_key,
1037 compress,
1038 SeipdVersion::V2,
1039 )
1040 .await?;
1041
1042 let boundary = "boundary123";
1043 let rcvd_mail = format!(
1044 "From: <alice@example.org>\r
1045To: \"hidden-recipients\": ;\r
1046Subject: [...]\r
1047Date: Sat, 14 Mar 2026 01:02:03 +0000\r
1048Message-ID: <{new_id}>\r
1049In-Reply-To: <{known_id}>\r
1050References: <{known_id}>\r
1051Content-Type: multipart/encrypted; protocol=\"application/pgp-encrypted\";\r
1052 boundary=\"{boundary}\"\r
1053MIME-Version: 1.0\r
1054\r
1055--{boundary}\r
1056Content-Type: application/pgp-encrypted; charset=\"utf-8\"\r
1057Content-Description: PGP/MIME version identification\r
1058Content-Transfer-Encoding: 7bit\r
1059\r
1060Version: 1\r
1061\r
1062--{boundary}\r
1063Content-Type: application/octet-stream; name=\"encrypted.asc\";\r
1064 charset=\"utf-8\"\r
1065Content-Description: OpenPGP encrypted message\r
1066Content-Disposition: inline; filename=\"encrypted.asc\";\r
1067Content-Transfer-Encoding: 7bit\r
1068\r
1069{encrypted_payload}
1070--{boundary}--\r
1071"
1072 );
1073
1074 let received = receive_imf(bob, rcvd_mail.as_bytes(), false)
1075 .await?
1076 .unwrap();
1077 let bob_hidden_msg = Message::load_from_db(bob, *received.msg_ids.last().unwrap())
1078 .await
1079 .unwrap();
1080 assert!(bob_hidden_msg.hidden);
1081 assert_eq!(bob_hidden_msg.chat_id, bob_chat_id);
1082
1083 marknoticed_chat(bob, bob_chat_id).await?;
1086
1087 assert_eq!(
1088 bob.sql
1089 .count(
1090 "SELECT COUNT(*) FROM smtp_mdns WHERE from_id!=?",
1091 (ContactId::SELF,)
1092 )
1093 .await?,
1094 0,
1095 "Bob should not send MDN to Alice"
1096 );
1097
1098 let reactions = get_msg_reactions(bob, bob_msg.id).await?;
1100 assert_eq!(reactions.reactions.len(), 1);
1101 assert_eq!(
1102 reactions.emoji_sorted_by_frequency(),
1103 vec![("๐".to_string(), 1)]
1104 );
1105
1106 Ok(())
1107 }
1108}