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::log::info;
30use crate::message::{Message, MsgId, rfc724_mid_exists};
31use crate::param::Param;
32
33#[derive(Debug, Default, Clone, Deserialize, Eq, PartialEq, Serialize)]
37pub struct Reaction {
38 reaction: String,
40}
41
42impl From<&str> for Reaction {
46 fn from(reaction: &str) -> Self {
61 let mut emojis: Vec<&str> = reaction
62 .split_ascii_whitespace()
63 .filter(|&emoji| emoji.len() < 30)
64 .collect();
65 emojis.sort_unstable();
66 emojis.dedup();
67 let reaction = emojis.join(" ");
68 Self { reaction }
69 }
70}
71
72impl Reaction {
73 pub fn is_empty(&self) -> bool {
75 self.reaction.is_empty()
76 }
77
78 pub fn emojis(&self) -> Vec<&str> {
80 self.reaction.split(' ').collect()
81 }
82
83 pub fn as_str(&self) -> &str {
85 &self.reaction
86 }
87
88 pub fn add(&self, other: Self) -> Self {
90 let mut emojis: Vec<&str> = self.emojis();
91 emojis.append(&mut other.emojis());
92 emojis.sort_unstable();
93 emojis.dedup();
94 let reaction = emojis.join(" ");
95 Self { reaction }
96 }
97}
98
99#[derive(Debug)]
101pub struct Reactions {
102 reactions: BTreeMap<ContactId, Reaction>,
104}
105
106impl Reactions {
107 pub fn contacts(&self) -> Vec<ContactId> {
109 self.reactions.keys().copied().collect()
110 }
111
112 pub fn get(&self, contact_id: ContactId) -> Reaction {
117 self.reactions.get(&contact_id).cloned().unwrap_or_default()
118 }
119
120 pub fn is_empty(&self) -> bool {
122 self.reactions.is_empty()
123 }
124
125 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 = context
324 .sql
325 .query_map(
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))
332 },
333 |rows| {
334 let mut reactions = Vec::new();
335 for row in rows {
336 let (contact_id, reaction) = row?;
337 reactions.push((contact_id, Reaction::from(reaction.as_str())));
338 }
339 Ok(reactions)
340 },
341 )
342 .await?
343 .into_iter()
344 .collect();
345 Ok(Reactions { reactions })
346}
347
348impl Chat {
349 pub async fn get_last_reaction_if_newer_than(
353 &self,
354 context: &Context,
355 timestamp: i64,
356 ) -> Result<Option<(Message, ContactId, String)>> {
357 if self
358 .param
359 .get_i64(Param::LastReactionTimestamp)
360 .filter(|&reaction_timestamp| reaction_timestamp > timestamp)
361 .is_none()
362 {
363 return Ok(None);
364 };
365 let reaction_msg_id = MsgId::new(
366 self.param
367 .get_int(Param::LastReactionMsgId)
368 .unwrap_or_default() as u32,
369 );
370 let Some(reaction_msg) = Message::load_from_db_optional(context, reaction_msg_id).await?
371 else {
372 return Ok(None);
376 };
377 let reaction_contact_id = ContactId::new(
378 self.param
379 .get_int(Param::LastReactionContactId)
380 .unwrap_or_default() as u32,
381 );
382 if let Some(reaction) = context
383 .sql
384 .query_get_value(
385 "SELECT reaction FROM reactions WHERE msg_id=? AND contact_id=?",
386 (reaction_msg.id, reaction_contact_id),
387 )
388 .await?
389 {
390 Ok(Some((reaction_msg, reaction_contact_id, reaction)))
391 } else {
392 Ok(None)
393 }
394 }
395}
396
397#[cfg(test)]
398mod tests {
399 use deltachat_contact_tools::ContactAddress;
400
401 use super::*;
402 use crate::chat::{forward_msgs, get_chat_msgs, marknoticed_chat, send_text_msg};
403 use crate::chatlist::Chatlist;
404 use crate::config::Config;
405 use crate::contact::{Contact, Origin};
406 use crate::download::DownloadState;
407 use crate::message::{MessageState, Viewtype, delete_msgs};
408 use crate::receive_imf::{receive_imf, receive_imf_from_inbox};
409 use crate::sql::housekeeping;
410 use crate::test_utils::E2EE_INFO_MSGS;
411 use crate::test_utils::TestContext;
412 use crate::test_utils::TestContextManager;
413 use crate::tools::SystemTime;
414 use std::time::Duration;
415
416 #[test]
417 fn test_parse_reaction() {
418 assert_eq!(Reaction::from("๐").emojis(), vec!["๐"]);
420 assert_eq!(Reaction::from("๐").emojis(), vec!["๐"]);
421 assert_eq!(Reaction::from("๐").emojis(), vec!["๐"]);
422 assert_eq!(Reaction::from("โน").emojis(), vec!["โน"]);
423 assert_eq!(Reaction::from("๐ข").emojis(), vec!["๐ข"]);
424
425 assert!(Reaction::from("").is_empty());
427
428 assert_eq!(Reaction::from(":deltacat:").emojis(), vec![":deltacat:"]);
431
432 assert!(
434 Reaction::from(":foobarbazquuxaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:").is_empty()
435 );
436
437 assert_eq!(Reaction::from("๐ โค").emojis(), vec!["โค", "๐"]);
439 assert_eq!(Reaction::from("๐\tโค").emojis(), vec!["โค", "๐"]);
440
441 assert_eq!(
443 Reaction::from("๐\t:foo: โค").emojis(),
444 vec![":foo:", "โค", "๐"]
445 );
446 assert_eq!(Reaction::from("๐\t:foo: โค").as_str(), ":foo: โค ๐");
447
448 assert_eq!(Reaction::from("๐ ๐").emojis(), vec!["๐"]);
450 }
451
452 #[test]
453 fn test_add_reaction() {
454 let reaction1 = Reaction::from("๐ ๐");
455 let reaction2 = Reaction::from("โค");
456 let reaction_sum = reaction1.add(reaction2);
457
458 assert_eq!(reaction_sum.emojis(), vec!["โค", "๐", "๐"]);
459 }
460
461 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
462 async fn test_receive_reaction() -> Result<()> {
463 let alice = TestContext::new_alice().await;
464
465 receive_imf(
467 &alice,
468 "To: bob@example.net\n\
469From: alice@example.org\n\
470Date: Today, 29 February 2021 00:00:00 -800\n\
471Message-ID: 12345@example.org\n\
472Subject: Meeting\n\
473\n\
474Can we chat at 1pm pacific, today?"
475 .as_bytes(),
476 false,
477 )
478 .await?;
479 let msg = alice.get_last_msg().await;
480 assert_eq!(msg.state, MessageState::OutDelivered);
481 let reactions = get_msg_reactions(&alice, msg.id).await?;
482 let contacts = reactions.contacts();
483 assert_eq!(contacts.len(), 0);
484
485 let bob_id = Contact::add_or_lookup(
486 &alice,
487 "",
488 &ContactAddress::new("bob@example.net")?,
489 Origin::ManuallyCreated,
490 )
491 .await?
492 .0;
493 let bob_reaction = reactions.get(bob_id);
494 assert!(bob_reaction.is_empty()); receive_imf(
498 &alice,
499 "To: alice@example.org\n\
500From: bob@example.net\n\
501Date: Today, 29 February 2021 00:00:10 -800\n\
502Message-ID: 56789@example.net\n\
503In-Reply-To: 12345@example.org\n\
504Subject: Meeting\n\
505Mime-Version: 1.0 (1.0)\n\
506Content-Type: text/plain; charset=utf-8\n\
507Content-Disposition: reaction\n\
508\n\
509\u{1F44D}"
510 .as_bytes(),
511 false,
512 )
513 .await?;
514
515 let reactions = get_msg_reactions(&alice, msg.id).await?;
516 assert_eq!(reactions.to_string(), "๐1");
517
518 let contacts = reactions.contacts();
519 assert_eq!(contacts.len(), 1);
520
521 assert_eq!(contacts.first(), Some(&bob_id));
522 let bob_reaction = reactions.get(bob_id);
523 assert_eq!(bob_reaction.is_empty(), false);
524 assert_eq!(bob_reaction.emojis(), vec!["๐"]);
525 assert_eq!(bob_reaction.as_str(), "๐");
526
527 receive_imf(
529 &alice,
530 "To: alice@example.org\n\
531From: bob@example.net\n\
532Date: Today, 29 February 2021 00:00:10 -800\n\
533Message-ID: 56790@example.net\n\
534In-Reply-To: 12345@example.org\n\
535Subject: Meeting\n\
536Mime-Version: 1.0 (1.0)\n\
537Content-Type: text/plain; charset=utf-8\n\
538Content-Disposition: reaction\n\
539\n\
540๐\n\
541\n\
542--\n\
543_______________________________________________\n\
544Here's my footer -- bob@example.net"
545 .as_bytes(),
546 false,
547 )
548 .await?;
549
550 let reactions = get_msg_reactions(&alice, msg.id).await?;
551 assert_eq!(reactions.to_string(), "๐1");
552
553 let msg_bob = receive_imf(
555 &alice,
556 "To: alice@example.org\n\
557From: bob@example.net\n\
558Date: Today, 29 February 2021 00:00:10 -800\n\
559Message-ID: 56791@example.net\n\
560In-Reply-To: 12345@example.org\n\
561Mime-Version: 1.0\n\
562Content-Type: multipart/mixed; boundary=\"YiEDa0DAkWCtVeE4\"\n\
563Content-Disposition: inline\n\
564\n\
565--YiEDa0DAkWCtVeE4\n\
566Content-Type: text/plain; charset=utf-8\n\
567Content-Disposition: inline\n\
568\n\
569Reply + reaction\n\
570\n\
571--YiEDa0DAkWCtVeE4\n\
572Content-Type: text/plain; charset=utf-8\n\
573Content-Disposition: reaction\n\
574\n\
575\u{1F44D}\n\
576\n\
577--YiEDa0DAkWCtVeE4--"
578 .as_bytes(),
579 false,
580 )
581 .await?
582 .unwrap();
583 let msg_bob = Message::load_from_db(&alice, msg_bob.msg_ids[0]).await?;
584 assert_eq!(msg_bob.from_id, bob_id);
585 assert_eq!(msg_bob.chat_id, msg.chat_id);
586 assert_eq!(msg_bob.viewtype, Viewtype::Text);
587 assert_eq!(msg_bob.state, MessageState::InFresh);
588 assert_eq!(msg_bob.hidden, false);
589 assert_eq!(msg_bob.text, "Reply + reaction");
590 let reactions = get_msg_reactions(&alice, msg.id).await?;
591 assert_eq!(reactions.to_string(), "๐1");
592
593 Ok(())
594 }
595
596 async fn expect_reactions_changed_event(
597 t: &TestContext,
598 expected_chat_id: ChatId,
599 expected_msg_id: MsgId,
600 expected_contact_id: ContactId,
601 ) -> Result<()> {
602 let event = t
603 .evtracker
604 .get_matching(|evt| {
605 matches!(
606 evt,
607 EventType::ReactionsChanged { .. } | EventType::IncomingMsg { .. }
608 )
609 })
610 .await;
611 match event {
612 EventType::ReactionsChanged {
613 chat_id,
614 msg_id,
615 contact_id,
616 } => {
617 assert_eq!(chat_id, expected_chat_id);
618 assert_eq!(msg_id, expected_msg_id);
619 assert_eq!(contact_id, expected_contact_id);
620 }
621 _ => panic!("Unexpected event {event:?}."),
622 }
623 Ok(())
624 }
625
626 async fn expect_incoming_reactions_event(
627 t: &TestContext,
628 expected_chat_id: ChatId,
629 expected_msg_id: MsgId,
630 expected_contact_id: ContactId,
631 expected_reaction: &str,
632 ) -> Result<()> {
633 let event = t
634 .evtracker
635 .get_matching(|evt| {
638 matches!(
639 evt,
640 EventType::IncomingReaction { .. } | EventType::IncomingMsg { .. }
641 )
642 })
643 .await;
644 match event {
645 EventType::IncomingReaction {
646 chat_id,
647 msg_id,
648 contact_id,
649 reaction,
650 } => {
651 assert_eq!(chat_id, expected_chat_id);
652 assert_eq!(msg_id, expected_msg_id);
653 assert_eq!(contact_id, expected_contact_id);
654 assert_eq!(reaction, Reaction::from(expected_reaction));
655 }
656 _ => panic!("Unexpected event {event:?}."),
657 }
658 Ok(())
659 }
660
661 async fn expect_no_unwanted_events(t: &TestContext) {
663 let ev = t
664 .evtracker
665 .get_matching_opt(t, |evt| {
666 matches!(
667 evt,
668 EventType::IncomingReaction { .. }
669 | EventType::IncomingMsg { .. }
670 | EventType::MsgsChanged { .. }
671 )
672 })
673 .await;
674 if let Some(ev) = ev {
675 panic!("Unwanted event {ev:?}.")
676 }
677 }
678
679 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
680 async fn test_send_reaction() -> Result<()> {
681 let alice = TestContext::new_alice().await;
682 let bob = TestContext::new_bob().await;
683
684 alice
686 .set_config(
687 Config::Selfstatus,
688 Some("Buy Delta Chat today and make this banner go away!"),
689 )
690 .await?;
691 bob.set_config(Config::Selfstatus, Some("Sent from my Delta Chat Pro. ๐"))
692 .await?;
693
694 let chat_alice = alice.create_chat(&bob).await;
695 let alice_msg = alice.send_text(chat_alice.id, "Hi!").await;
696 let bob_msg = bob.recv_msg(&alice_msg).await;
697 assert_eq!(
698 get_chat_msgs(&alice, chat_alice.id).await?.len(),
699 E2EE_INFO_MSGS + 1
700 );
701 assert_eq!(
702 get_chat_msgs(&bob, bob_msg.chat_id).await?.len(),
703 E2EE_INFO_MSGS + 1
704 );
705
706 let alice_msg2 = alice.send_text(chat_alice.id, "Hi again!").await;
707 bob.recv_msg(&alice_msg2).await;
708 assert_eq!(
709 get_chat_msgs(&alice, chat_alice.id).await?.len(),
710 E2EE_INFO_MSGS + 2
711 );
712 assert_eq!(
713 get_chat_msgs(&bob, bob_msg.chat_id).await?.len(),
714 E2EE_INFO_MSGS + 2
715 );
716
717 bob_msg.chat_id.accept(&bob).await?;
718
719 bob.evtracker.clear_events();
720 send_reaction(&bob, bob_msg.id, "๐").await.unwrap();
721 expect_reactions_changed_event(&bob, bob_msg.chat_id, bob_msg.id, ContactId::SELF).await?;
722 expect_no_unwanted_events(&bob).await;
723 assert_eq!(
724 get_chat_msgs(&bob, bob_msg.chat_id).await?.len(),
725 E2EE_INFO_MSGS + 2
726 );
727
728 let bob_reaction_msg = bob.pop_sent_msg().await;
729 let alice_reaction_msg = alice.recv_msg_hidden(&bob_reaction_msg).await;
730 assert_eq!(alice_reaction_msg.state, MessageState::InFresh);
731 assert_eq!(
732 get_chat_msgs(&alice, chat_alice.id).await?.len(),
733 E2EE_INFO_MSGS + 2
734 );
735
736 let reactions = get_msg_reactions(&alice, alice_msg.sender_msg_id).await?;
737 assert_eq!(reactions.to_string(), "๐1");
738 let contacts = reactions.contacts();
739 assert_eq!(contacts.len(), 1);
740 let bob_id = contacts.first().unwrap();
741 let bob_reaction = reactions.get(*bob_id);
742 assert_eq!(bob_reaction.is_empty(), false);
743 assert_eq!(bob_reaction.emojis(), vec!["๐"]);
744 assert_eq!(bob_reaction.as_str(), "๐");
745 expect_reactions_changed_event(&alice, chat_alice.id, alice_msg.sender_msg_id, *bob_id)
746 .await?;
747 expect_incoming_reactions_event(
748 &alice,
749 chat_alice.id,
750 alice_msg.sender_msg_id,
751 *bob_id,
752 "๐",
753 )
754 .await?;
755 expect_no_unwanted_events(&alice).await;
756
757 marknoticed_chat(&alice, chat_alice.id).await?;
758 assert_eq!(
759 alice_reaction_msg.id.get_state(&alice).await?,
760 MessageState::InSeen
761 );
762 assert_eq!(
764 alice
765 .sql
766 .count("SELECT COUNT(*) FROM smtp_mdns", ())
767 .await?,
768 0
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_partial_download_and_reaction() -> Result<()> {
940 let alice = TestContext::new_alice().await;
941 let bob = TestContext::new_bob().await;
942
943 alice
944 .create_chat_with_contact("Bob", "bob@example.net")
945 .await;
946
947 let msg_header = "From: Bob <bob@example.net>\n\
948 To: Alice <alice@example.org>\n\
949 Chat-Version: 1.0\n\
950 Subject: subject\n\
951 Message-ID: <first@example.org>\n\
952 Date: Sun, 14 Nov 2021 00:10:00 +0000\
953 Content-Type: text/plain";
954 let msg_full = format!("{msg_header}\n\n100k text...");
955
956 let alice_received_message = receive_imf_from_inbox(
958 &alice,
959 "first@example.org",
960 msg_header.as_bytes(),
961 false,
962 Some(100000),
963 )
964 .await?
965 .unwrap();
966 let alice_msg_id = *alice_received_message.msg_ids.first().unwrap();
967
968 let bob_received_message = receive_imf(&bob, msg_full.as_bytes(), false)
970 .await?
971 .unwrap();
972 let bob_msg_id = *bob_received_message.msg_ids.first().unwrap();
973
974 send_reaction(&bob, bob_msg_id, "๐").await.unwrap();
976 let bob_reaction_msg = bob.pop_sent_msg().await;
977
978 alice.recv_msg_hidden(&bob_reaction_msg).await;
980
981 let reactions = get_msg_reactions(&alice, alice_msg_id).await?;
982 assert_eq!(reactions.to_string(), "๐1");
983 let msg = Message::load_from_db(&alice, alice_msg_id).await?;
984 assert_eq!(msg.download_state(), DownloadState::Available);
985
986 receive_imf_from_inbox(
988 &alice,
989 "first@example.org",
990 msg_full.as_bytes(),
991 false,
992 None,
993 )
994 .await?;
995
996 let msg = Message::load_from_db(&alice, alice_msg_id).await?;
998 assert_eq!(msg.download_state(), DownloadState::Done);
999 let reactions = get_msg_reactions(&alice, alice_msg_id).await?;
1000 assert_eq!(reactions.to_string(), "๐1");
1001
1002 Ok(())
1003 }
1004
1005 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1010 async fn test_reaction_status_multidevice() -> Result<()> {
1011 let mut tcm = TestContextManager::new();
1012 let alice1 = tcm.alice().await;
1013 let alice2 = tcm.alice().await;
1014
1015 alice1
1016 .set_config(Config::Selfstatus, Some("New status"))
1017 .await?;
1018
1019 let alice2_msg = tcm.send_recv(&alice1, &alice2, "Hi!").await;
1020 assert_eq!(
1021 alice2.get_config(Config::Selfstatus).await?.as_deref(),
1022 Some("New status")
1023 );
1024
1025 {
1028 send_reaction(&alice2, alice2_msg.id, "๐").await?;
1029 let msg = alice2.pop_sent_msg().await;
1030 alice1.recv_msg_hidden(&msg).await;
1031 }
1032
1033 assert_eq!(
1035 alice1.get_config(Config::Selfstatus).await?.as_deref(),
1036 Some("New status")
1037 );
1038 Ok(())
1039 }
1040
1041 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
1042 async fn test_send_reaction_multidevice() -> Result<()> {
1043 let mut tcm = TestContextManager::new();
1044 let alice0 = tcm.alice().await;
1045 let alice1 = tcm.alice().await;
1046 let bob = tcm.bob().await;
1047 let chat_id = alice0.create_chat(&bob).await.id;
1048
1049 let alice0_msg_id = send_text_msg(&alice0, chat_id, "foo".to_string()).await?;
1050 let alice1_msg = alice1.recv_msg(&alice0.pop_sent_msg().await).await;
1051
1052 send_reaction(&alice0, alice0_msg_id, "๐").await?;
1053 alice1.recv_msg_hidden(&alice0.pop_sent_msg().await).await;
1054
1055 expect_reactions_changed_event(&alice0, chat_id, alice0_msg_id, ContactId::SELF).await?;
1056 expect_reactions_changed_event(&alice1, alice1_msg.chat_id, alice1_msg.id, ContactId::SELF)
1057 .await?;
1058 for a in [&alice0, &alice1] {
1059 expect_no_unwanted_events(a).await;
1060 }
1061 Ok(())
1062 }
1063}