deltachat/
reaction.rs

1//! # Reactions.
2//!
3//! Reactions are short messages consisting of emojis sent in reply to
4//! messages. Unlike normal messages which are added to the end of the chat,
5//! reactions are supposed to be displayed near the original messages.
6//!
7//! RFC 9078 specifies how reactions are transmitted in MIME messages.
8//!
9//! Reaction update semantics is not well-defined in RFC 9078, so
10//! Delta Chat uses the same semantics as in
11//! [XEP-0444](https://xmpp.org/extensions/xep-0444.html) section
12//! "3.2 Updating reactions to a message". Received reactions override
13//! all previously received reactions from the same user and it is
14//! possible to remove all reactions by sending an empty string as a reaction,
15//! even though RFC 9078 requires at least one emoji to be sent.
16
17use 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/// A single reaction consisting of multiple emoji sequences.
33///
34/// It is guaranteed to have all emojis sorted and deduplicated inside.
35#[derive(Debug, Default, Clone, Deserialize, Eq, PartialEq, Serialize)]
36pub struct Reaction {
37    /// Canonical representation of reaction as a string of space-separated emojis.
38    reaction: String,
39}
40
41// We implement From<&str> instead of std::str::FromStr, because
42// FromStr requires error type and reaction parsing never returns an
43// error.
44impl From<&str> for Reaction {
45    /// Parses a string containing a reaction.
46    ///
47    /// Reaction string is separated by spaces or tabs (`WSP` in ABNF),
48    /// but this function accepts any ASCII whitespace, so even a CRLF at
49    /// the end of string is acceptable.
50    ///
51    /// Any short enough string is accepted as a reaction to avoid the
52    /// complexity of validating emoji sequences as required by RFC
53    /// 9078. On the sender side UI is responsible to provide only
54    /// valid emoji sequences via reaction picker. On the receiver
55    /// side, abuse of the possibility to use arbitrary strings as
56    /// reactions is not different from other kinds of spam attacks
57    /// such as sending large numbers of large messages, and should be
58    /// dealt with the same way, e.g. by blocking the user.
59    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    /// Returns true if reaction contains no emojis.
73    pub fn is_empty(&self) -> bool {
74        self.reaction.is_empty()
75    }
76
77    /// Returns a vector of emojis composing a reaction.
78    pub fn emojis(&self) -> Vec<&str> {
79        self.reaction.split(' ').collect()
80    }
81
82    /// Returns space-separated string of emojis
83    pub fn as_str(&self) -> &str {
84        &self.reaction
85    }
86
87    /// Appends emojis from another reaction to this reaction.
88    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/// Structure representing all reactions to a particular message.
99#[derive(Debug)]
100pub struct Reactions {
101    /// Map from a contact to its reaction to message.
102    reactions: BTreeMap<ContactId, Reaction>,
103}
104
105impl Reactions {
106    /// Returns vector of contacts that reacted to the message.
107    pub fn contacts(&self) -> Vec<ContactId> {
108        self.reactions.keys().copied().collect()
109    }
110
111    /// Returns reaction of a given contact to message.
112    ///
113    /// If contact did not react to message or removed the reaction,
114    /// this method returns an empty reaction.
115    pub fn get(&self, contact_id: ContactId) -> Reaction {
116        self.reactions.get(&contact_id).cloned().unwrap_or_default()
117    }
118
119    /// Returns true if the message has no reactions.
120    pub fn is_empty(&self) -> bool {
121        self.reactions.is_empty()
122    }
123
124    /// Returns a map from emojis to their frequencies.
125    #[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    /// Returns a vector of emojis
140    /// sorted in descending order of frequencies.
141    ///
142    /// This function can be used to display the reactions in
143    /// the message bubble in the UIs.
144    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        // Simply remove the record instead of setting it to empty string.
182        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
224/// Sends a reaction to message `msg_id`, overriding previously sent reactions.
225///
226/// `reaction` is a string consisting of space-separated emoji. Use
227/// empty string to retract a reaction.
228pub 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    // Send message first.
239    let reaction_msg_id = send_msg(context, chat_id, &mut reaction_msg).await?;
240
241    // Only set reaction if we successfully sent the message.
242    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
254/// Adds given reaction to message `msg_id` and sends an update.
255///
256/// This can be used to implement advanced clients that allow reacting
257/// with multiple emojis. For a simple messenger UI, you probably want
258/// to use [`send_reaction()`] instead so reacting with a new emoji
259/// removes previous emoji at the same time.
260pub 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
266/// Updates reaction of `contact_id` on the message with `in_reply_to`
267/// Message-ID. If no such message is found in the database, reaction
268/// is ignored.
269///
270/// `reaction` is a space-separated string of emojis. It can be empty
271/// if contact wants to remove all reactions.
272pub(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
304/// Get our own reaction for a given message.
305async 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
321/// Returns a structure containing all reactions to the message.
322pub 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    /// Check if there is a reaction newer than the given timestamp.
340    ///
341    /// If so, reaction details are returned and can be used to create a summary string.
342    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            .filter(|&reaction_timestamp| reaction_timestamp > timestamp)
351            .is_none()
352        {
353            return Ok(None);
354        };
355        let reaction_msg_id = MsgId::new(
356            self.param
357                .get_int(Param::LastReactionMsgId)
358                .unwrap_or_default() as u32,
359        );
360        let Some(reaction_msg) = Message::load_from_db_optional(context, reaction_msg_id).await?
361        else {
362            // The message reacted to may be deleted.
363            // These are no errors as `Param::LastReaction*` are just weak pointers.
364            // Instead, just return `Ok(None)` and let the caller create another summary.
365            return Ok(None);
366        };
367        let reaction_contact_id = ContactId::new(
368            self.param
369                .get_int(Param::LastReactionContactId)
370                .unwrap_or_default() as u32,
371        );
372        if let Some(reaction) = context
373            .sql
374            .query_get_value(
375                "SELECT reaction FROM reactions WHERE msg_id=? AND contact_id=?",
376                (reaction_msg.id, reaction_contact_id),
377            )
378            .await?
379        {
380            Ok(Some((reaction_msg, reaction_contact_id, reaction)))
381        } else {
382            Ok(None)
383        }
384    }
385}
386
387#[cfg(test)]
388mod tests {
389    use deltachat_contact_tools::ContactAddress;
390
391    use super::*;
392    use crate::chat::{forward_msgs, get_chat_msgs, marknoticed_chat, send_text_msg};
393    use crate::chatlist::Chatlist;
394    use crate::config::Config;
395    use crate::contact::{Contact, Origin};
396    use crate::message::{MessageState, Viewtype, delete_msgs};
397    use crate::receive_imf::receive_imf;
398    use crate::sql::housekeeping;
399    use crate::test_utils::E2EE_INFO_MSGS;
400    use crate::test_utils::TestContext;
401    use crate::test_utils::TestContextManager;
402    use crate::tools::SystemTime;
403    use std::time::Duration;
404
405    #[test]
406    fn test_parse_reaction() {
407        // Check that basic set of emojis from RFC 9078 is supported.
408        assert_eq!(Reaction::from("๐Ÿ‘").emojis(), vec!["๐Ÿ‘"]);
409        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
414        // Empty string can be used to remove all reactions.
415        assert!(Reaction::from("").is_empty());
416
417        // Short strings can be used as emojis, could be used to add
418        // support for custom emojis via emoji shortcodes.
419        assert_eq!(Reaction::from(":deltacat:").emojis(), vec![":deltacat:"]);
420
421        // Check that long strings are not valid emojis.
422        assert!(
423            Reaction::from(":foobarbazquuxaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:").is_empty()
424        );
425
426        // Multiple reactions separated by spaces or tabs are supported.
427        assert_eq!(Reaction::from("๐Ÿ‘ โค").emojis(), vec!["โค", "๐Ÿ‘"]);
428        assert_eq!(Reaction::from("๐Ÿ‘\tโค").emojis(), vec!["โค", "๐Ÿ‘"]);
429
430        // Invalid emojis are removed, but valid emojis are retained.
431        assert_eq!(
432            Reaction::from("๐Ÿ‘\t:foo: โค").emojis(),
433            vec![":foo:", "โค", "๐Ÿ‘"]
434        );
435        assert_eq!(Reaction::from("๐Ÿ‘\t:foo: โค").as_str(), ":foo: โค ๐Ÿ‘");
436
437        // Duplicates are removed.
438        assert_eq!(Reaction::from("๐Ÿ‘ ๐Ÿ‘").emojis(), vec!["๐Ÿ‘"]);
439    }
440
441    #[test]
442    fn test_add_reaction() {
443        let reaction1 = Reaction::from("๐Ÿ‘ ๐Ÿ˜€");
444        let reaction2 = Reaction::from("โค");
445        let reaction_sum = reaction1.add(reaction2);
446
447        assert_eq!(reaction_sum.emojis(), vec!["โค", "๐Ÿ‘", "๐Ÿ˜€"]);
448    }
449
450    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
451    async fn test_receive_reaction() -> Result<()> {
452        let alice = TestContext::new_alice().await;
453
454        // Alice receives BCC-self copy of a message sent to Bob.
455        receive_imf(
456            &alice,
457            "To: bob@example.net\n\
458From: alice@example.org\n\
459Date: Today, 29 February 2021 00:00:00 -800\n\
460Message-ID: 12345@example.org\n\
461Subject: Meeting\n\
462\n\
463Can we chat at 1pm pacific, today?"
464                .as_bytes(),
465            false,
466        )
467        .await?;
468        let msg = alice.get_last_msg().await;
469        assert_eq!(msg.state, MessageState::OutDelivered);
470        let reactions = get_msg_reactions(&alice, msg.id).await?;
471        let contacts = reactions.contacts();
472        assert_eq!(contacts.len(), 0);
473
474        let bob_id = Contact::add_or_lookup(
475            &alice,
476            "",
477            &ContactAddress::new("bob@example.net")?,
478            Origin::ManuallyCreated,
479        )
480        .await?
481        .0;
482        let bob_reaction = reactions.get(bob_id);
483        assert!(bob_reaction.is_empty()); // Bob has not reacted to message yet.
484
485        // Alice receives reaction to her message from Bob.
486        receive_imf(
487            &alice,
488            "To: alice@example.org\n\
489From: bob@example.net\n\
490Date: Today, 29 February 2021 00:00:10 -800\n\
491Message-ID: 56789@example.net\n\
492In-Reply-To: 12345@example.org\n\
493Subject: Meeting\n\
494Mime-Version: 1.0 (1.0)\n\
495Content-Type: text/plain; charset=utf-8\n\
496Content-Disposition: reaction\n\
497\n\
498\u{1F44D}"
499                .as_bytes(),
500            false,
501        )
502        .await?;
503
504        let reactions = get_msg_reactions(&alice, msg.id).await?;
505        assert_eq!(reactions.to_string(), "๐Ÿ‘1");
506
507        let contacts = reactions.contacts();
508        assert_eq!(contacts.len(), 1);
509
510        assert_eq!(contacts.first(), Some(&bob_id));
511        let bob_reaction = reactions.get(bob_id);
512        assert_eq!(bob_reaction.is_empty(), false);
513        assert_eq!(bob_reaction.emojis(), vec!["๐Ÿ‘"]);
514        assert_eq!(bob_reaction.as_str(), "๐Ÿ‘");
515
516        // Alice receives reaction to her message from Bob with a footer.
517        receive_imf(
518            &alice,
519            "To: alice@example.org\n\
520From: bob@example.net\n\
521Date: Today, 29 February 2021 00:00:10 -800\n\
522Message-ID: 56790@example.net\n\
523In-Reply-To: 12345@example.org\n\
524Subject: Meeting\n\
525Mime-Version: 1.0 (1.0)\n\
526Content-Type: text/plain; charset=utf-8\n\
527Content-Disposition: reaction\n\
528\n\
529๐Ÿ˜€\n\
530\n\
531--\n\
532_______________________________________________\n\
533Here's my footer -- bob@example.net"
534                .as_bytes(),
535            false,
536        )
537        .await?;
538
539        let reactions = get_msg_reactions(&alice, msg.id).await?;
540        assert_eq!(reactions.to_string(), "๐Ÿ˜€1");
541
542        // Alice receives a message with reaction to her message from Bob.
543        let msg_bob = receive_imf(
544            &alice,
545            "To: alice@example.org\n\
546From: bob@example.net\n\
547Date: Today, 29 February 2021 00:00:10 -800\n\
548Message-ID: 56791@example.net\n\
549In-Reply-To: 12345@example.org\n\
550Mime-Version: 1.0\n\
551Content-Type: multipart/mixed; boundary=\"YiEDa0DAkWCtVeE4\"\n\
552Content-Disposition: inline\n\
553\n\
554--YiEDa0DAkWCtVeE4\n\
555Content-Type: text/plain; charset=utf-8\n\
556Content-Disposition: inline\n\
557\n\
558Reply + reaction\n\
559\n\
560--YiEDa0DAkWCtVeE4\n\
561Content-Type: text/plain; charset=utf-8\n\
562Content-Disposition: reaction\n\
563\n\
564\u{1F44D}\n\
565\n\
566--YiEDa0DAkWCtVeE4--"
567                .as_bytes(),
568            false,
569        )
570        .await?
571        .unwrap();
572        let msg_bob = Message::load_from_db(&alice, msg_bob.msg_ids[0]).await?;
573        assert_eq!(msg_bob.from_id, bob_id);
574        assert_eq!(msg_bob.chat_id, msg.chat_id);
575        assert_eq!(msg_bob.viewtype, Viewtype::Text);
576        assert_eq!(msg_bob.state, MessageState::InFresh);
577        assert_eq!(msg_bob.hidden, false);
578        assert_eq!(msg_bob.text, "Reply + reaction");
579        let reactions = get_msg_reactions(&alice, msg.id).await?;
580        assert_eq!(reactions.to_string(), "๐Ÿ‘1");
581
582        Ok(())
583    }
584
585    async fn expect_reactions_changed_event(
586        t: &TestContext,
587        expected_chat_id: ChatId,
588        expected_msg_id: MsgId,
589        expected_contact_id: ContactId,
590    ) -> Result<()> {
591        let event = t
592            .evtracker
593            .get_matching(|evt| {
594                matches!(
595                    evt,
596                    EventType::ReactionsChanged { .. } | EventType::IncomingMsg { .. }
597                )
598            })
599            .await;
600        match event {
601            EventType::ReactionsChanged {
602                chat_id,
603                msg_id,
604                contact_id,
605            } => {
606                assert_eq!(chat_id, expected_chat_id);
607                assert_eq!(msg_id, expected_msg_id);
608                assert_eq!(contact_id, expected_contact_id);
609            }
610            _ => panic!("Unexpected event {event:?}."),
611        }
612        Ok(())
613    }
614
615    async fn expect_incoming_reactions_event(
616        t: &TestContext,
617        expected_chat_id: ChatId,
618        expected_msg_id: MsgId,
619        expected_contact_id: ContactId,
620        expected_reaction: &str,
621    ) -> Result<()> {
622        let event = t
623            .evtracker
624            // Check for absence of `IncomingMsg` events -- it appeared that it's quite easy to make
625            // bugs when `IncomingMsg` is issued for reactions.
626            .get_matching(|evt| {
627                matches!(
628                    evt,
629                    EventType::IncomingReaction { .. } | EventType::IncomingMsg { .. }
630                )
631            })
632            .await;
633        match event {
634            EventType::IncomingReaction {
635                chat_id,
636                msg_id,
637                contact_id,
638                reaction,
639            } => {
640                assert_eq!(chat_id, expected_chat_id);
641                assert_eq!(msg_id, expected_msg_id);
642                assert_eq!(contact_id, expected_contact_id);
643                assert_eq!(reaction, Reaction::from(expected_reaction));
644            }
645            _ => panic!("Unexpected event {event:?}."),
646        }
647        Ok(())
648    }
649
650    /// Checks that no unwanted events remain after expecting "wanted" reaction events.
651    async fn expect_no_unwanted_events(t: &TestContext) {
652        let ev = t
653            .evtracker
654            .get_matching_opt(t, |evt| {
655                matches!(
656                    evt,
657                    EventType::IncomingReaction { .. }
658                        | EventType::IncomingMsg { .. }
659                        | EventType::MsgsChanged { .. }
660                )
661            })
662            .await;
663        if let Some(ev) = ev {
664            panic!("Unwanted event {ev:?}.")
665        }
666    }
667
668    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
669    async fn test_send_reaction() -> Result<()> {
670        let alice = TestContext::new_alice().await;
671        let bob = TestContext::new_bob().await;
672
673        // Test that the status does not get mixed up into reactions.
674        alice
675            .set_config(
676                Config::Selfstatus,
677                Some("Buy Delta Chat today and make this banner go away!"),
678            )
679            .await?;
680        bob.set_config(Config::Selfstatus, Some("Sent from my Delta Chat Pro. ๐Ÿ‘"))
681            .await?;
682
683        let chat_alice = alice.create_chat(&bob).await;
684        let alice_msg = alice.send_text(chat_alice.id, "Hi!").await;
685        let bob_msg = bob.recv_msg(&alice_msg).await;
686        assert_eq!(
687            get_chat_msgs(&alice, chat_alice.id).await?.len(),
688            E2EE_INFO_MSGS + 1
689        );
690        assert_eq!(
691            get_chat_msgs(&bob, bob_msg.chat_id).await?.len(),
692            E2EE_INFO_MSGS + 1
693        );
694
695        let alice_msg2 = alice.send_text(chat_alice.id, "Hi again!").await;
696        bob.recv_msg(&alice_msg2).await;
697        assert_eq!(
698            get_chat_msgs(&alice, chat_alice.id).await?.len(),
699            E2EE_INFO_MSGS + 2
700        );
701        assert_eq!(
702            get_chat_msgs(&bob, bob_msg.chat_id).await?.len(),
703            E2EE_INFO_MSGS + 2
704        );
705
706        bob_msg.chat_id.accept(&bob).await?;
707
708        bob.evtracker.clear_events();
709        send_reaction(&bob, bob_msg.id, "๐Ÿ‘").await.unwrap();
710        expect_reactions_changed_event(&bob, bob_msg.chat_id, bob_msg.id, ContactId::SELF).await?;
711        expect_no_unwanted_events(&bob).await;
712        assert_eq!(
713            get_chat_msgs(&bob, bob_msg.chat_id).await?.len(),
714            E2EE_INFO_MSGS + 2
715        );
716
717        let bob_reaction_msg = bob.pop_sent_msg().await;
718        let alice_reaction_msg = alice.recv_msg_hidden(&bob_reaction_msg).await;
719        assert_eq!(alice_reaction_msg.state, MessageState::InFresh);
720        assert_eq!(
721            get_chat_msgs(&alice, chat_alice.id).await?.len(),
722            E2EE_INFO_MSGS + 2
723        );
724
725        let reactions = get_msg_reactions(&alice, alice_msg.sender_msg_id).await?;
726        assert_eq!(reactions.to_string(), "๐Ÿ‘1");
727        let contacts = reactions.contacts();
728        assert_eq!(contacts.len(), 1);
729        let bob_id = contacts.first().unwrap();
730        let bob_reaction = reactions.get(*bob_id);
731        assert_eq!(bob_reaction.is_empty(), false);
732        assert_eq!(bob_reaction.emojis(), vec!["๐Ÿ‘"]);
733        assert_eq!(bob_reaction.as_str(), "๐Ÿ‘");
734        expect_reactions_changed_event(&alice, chat_alice.id, alice_msg.sender_msg_id, *bob_id)
735            .await?;
736        expect_incoming_reactions_event(
737            &alice,
738            chat_alice.id,
739            alice_msg.sender_msg_id,
740            *bob_id,
741            "๐Ÿ‘",
742        )
743        .await?;
744        expect_no_unwanted_events(&alice).await;
745
746        marknoticed_chat(&alice, chat_alice.id).await?;
747        assert_eq!(
748            alice_reaction_msg.id.get_state(&alice).await?,
749            MessageState::InSeen
750        );
751        // Reactions don't request MDNs, but an MDN to self is sent.
752        assert_eq!(
753            alice
754                .sql
755                .count("SELECT COUNT(*) FROM smtp_mdns", ())
756                .await?,
757            1
758        );
759        assert_eq!(
760            alice
761                .sql
762                .count(
763                    "SELECT COUNT(*) FROM smtp_mdns WHERE from_id=?",
764                    (ContactId::SELF,)
765                )
766                .await?,
767            1
768        );
769
770        // Alice reacts to own message.
771        send_reaction(&alice, alice_msg.sender_msg_id, "๐Ÿ‘ ๐Ÿ˜€")
772            .await
773            .unwrap();
774        let reactions = get_msg_reactions(&alice, alice_msg.sender_msg_id).await?;
775        assert_eq!(reactions.to_string(), "๐Ÿ‘2 ๐Ÿ˜€1");
776
777        assert_eq!(
778            reactions.emoji_sorted_by_frequency(),
779            vec![("๐Ÿ‘".to_string(), 2), ("๐Ÿ˜€".to_string(), 1)]
780        );
781
782        Ok(())
783    }
784
785    async fn assert_summary(t: &TestContext, expected: &str) {
786        let chatlist = Chatlist::try_load(t, 0, None, None).await.unwrap();
787        let summary = chatlist.get_summary(t, 0, None).await.unwrap();
788        assert_eq!(summary.text, expected);
789    }
790
791    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
792    async fn test_reaction_summary() -> Result<()> {
793        let mut tcm = TestContextManager::new();
794        let alice = tcm.alice().await;
795        let bob = tcm.bob().await;
796        alice.set_config(Config::Displayname, Some("ALICE")).await?;
797        bob.set_config(Config::Displayname, Some("BOB")).await?;
798        let alice_bob_id = alice.add_or_lookup_contact_id(&bob).await;
799
800        // Alice sends message to Bob
801        let alice_chat = alice.create_chat(&bob).await;
802        let alice_msg1 = alice.send_text(alice_chat.id, "Party?").await;
803        let bob_msg1 = bob.recv_msg(&alice_msg1).await;
804
805        // Bob reacts to Alice's message, this is shown in the summaries
806        SystemTime::shift(Duration::from_secs(10));
807        bob_msg1.chat_id.accept(&bob).await?;
808        send_reaction(&bob, bob_msg1.id, "๐Ÿ‘").await?;
809        let bob_send_reaction = bob.pop_sent_msg().await;
810        alice.recv_msg_hidden(&bob_send_reaction).await;
811        expect_incoming_reactions_event(
812            &alice,
813            alice_chat.id,
814            alice_msg1.sender_msg_id,
815            alice_bob_id,
816            "๐Ÿ‘",
817        )
818        .await?;
819        expect_no_unwanted_events(&alice).await;
820
821        let chatlist = Chatlist::try_load(&bob, 0, None, None).await?;
822        let summary = chatlist.get_summary(&bob, 0, None).await?;
823        assert_eq!(summary.text, "You reacted ๐Ÿ‘ to \"Party?\"");
824        assert_eq!(summary.timestamp, bob_msg1.get_timestamp()); // time refers to message, not to reaction
825        assert_eq!(summary.state, MessageState::InFresh); // state refers to message, not to reaction
826        assert!(summary.prefix.is_none());
827        assert!(summary.thumbnail_path.is_none());
828        assert_summary(&alice, "BOB reacted ๐Ÿ‘ to \"Party?\"").await;
829
830        // Alice reacts to own message as well
831        SystemTime::shift(Duration::from_secs(10));
832        send_reaction(&alice, alice_msg1.sender_msg_id, "๐Ÿฟ").await?;
833        let alice_send_reaction = alice.pop_sent_msg().await;
834        bob.evtracker.clear_events();
835        bob.recv_msg_opt(&alice_send_reaction).await;
836        expect_no_unwanted_events(&bob).await;
837
838        assert_summary(&alice, "You reacted ๐Ÿฟ to \"Party?\"").await;
839        assert_summary(&bob, "ALICE reacted ๐Ÿฟ to \"Party?\"").await;
840
841        // Alice sends a newer message, this overwrites reaction summaries
842        SystemTime::shift(Duration::from_secs(10));
843        let alice_msg2 = alice.send_text(alice_chat.id, "kewl").await;
844        bob.recv_msg(&alice_msg2).await;
845
846        assert_summary(&alice, "kewl").await;
847        assert_summary(&bob, "kewl").await;
848
849        // Reactions to older messages still overwrite newer messages
850        SystemTime::shift(Duration::from_secs(10));
851        send_reaction(&alice, alice_msg1.sender_msg_id, "๐Ÿค˜").await?;
852        let alice_send_reaction = alice.pop_sent_msg().await;
853        bob.recv_msg_opt(&alice_send_reaction).await;
854
855        assert_summary(&alice, "You reacted ๐Ÿค˜ to \"Party?\"").await;
856        assert_summary(&bob, "ALICE reacted ๐Ÿค˜ to \"Party?\"").await;
857
858        // Retracted reactions remove all summary reactions
859        SystemTime::shift(Duration::from_secs(10));
860        send_reaction(&alice, alice_msg1.sender_msg_id, "").await?;
861        let alice_remove_reaction = alice.pop_sent_msg().await;
862        bob.recv_msg_opt(&alice_remove_reaction).await;
863
864        assert_summary(&alice, "kewl").await;
865        assert_summary(&bob, "kewl").await;
866
867        // Alice adds another reaction and then deletes the message reacted to; this will also delete reaction summary
868        SystemTime::shift(Duration::from_secs(10));
869        send_reaction(&alice, alice_msg1.sender_msg_id, "๐Ÿงน").await?;
870        assert_summary(&alice, "You reacted ๐Ÿงน to \"Party?\"").await;
871
872        delete_msgs(&alice, &[alice_msg1.sender_msg_id]).await?; // this will leave a tombstone
873        assert_summary(&alice, "kewl").await;
874        housekeeping(&alice).await?; // this will delete the tombstone
875        assert_summary(&alice, "kewl").await;
876
877        Ok(())
878    }
879
880    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
881    async fn test_reaction_forwarded_summary() -> Result<()> {
882        let alice = TestContext::new_alice().await;
883
884        // Alice adds a message to "Saved Messages"
885        let self_chat = alice.get_self_chat().await;
886        let msg_id = send_text_msg(&alice, self_chat.id, "foo".to_string()).await?;
887        assert_summary(&alice, "foo").await;
888
889        // Alice reacts to that message
890        SystemTime::shift(Duration::from_secs(10));
891        send_reaction(&alice, msg_id, "๐Ÿซ").await?;
892        assert_summary(&alice, "You reacted ๐Ÿซ to \"foo\"").await;
893        let reactions = get_msg_reactions(&alice, msg_id).await?;
894        assert_eq!(reactions.reactions.len(), 1);
895
896        // Alice forwards that message to Bob: Reactions are not forwarded, the message is prefixed by "Forwarded".
897        let bob_id = Contact::create(&alice, "", "bob@example.net").await?;
898        let bob_chat_id = ChatId::create_for_contact(&alice, bob_id).await?;
899        forward_msgs(&alice, &[msg_id], bob_chat_id).await?;
900        assert_summary(&alice, "Forwarded: foo").await; // forwarded messages are prefixed
901        let chatlist = Chatlist::try_load(&alice, 0, None, None).await.unwrap();
902        let forwarded_msg_id = chatlist.get_msg_id(0)?.unwrap();
903        let reactions = get_msg_reactions(&alice, forwarded_msg_id).await?;
904        assert!(reactions.reactions.is_empty()); // reactions are not forwarded
905
906        // Alice reacts to forwarded message:
907        // For reaction summary neither original message author nor "Forwarded" prefix is shown
908        SystemTime::shift(Duration::from_secs(10));
909        send_reaction(&alice, forwarded_msg_id, "๐Ÿณ").await?;
910        assert_summary(&alice, "You reacted ๐Ÿณ to \"foo\"").await;
911        let reactions = get_msg_reactions(&alice, msg_id).await?;
912        assert_eq!(reactions.reactions.len(), 1);
913
914        Ok(())
915    }
916
917    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
918    async fn test_reaction_self_chat_multidevice_summary() -> Result<()> {
919        let alice0 = TestContext::new_alice().await;
920        let alice1 = TestContext::new_alice().await;
921        let chat = alice0.get_self_chat().await;
922
923        let msg_id = send_text_msg(&alice0, chat.id, "mom's birthday!".to_string()).await?;
924        alice1.recv_msg(&alice0.pop_sent_msg().await).await;
925
926        SystemTime::shift(Duration::from_secs(10));
927        send_reaction(&alice0, msg_id, "๐Ÿ‘†").await?;
928        let sync = alice0.pop_sent_msg().await;
929        receive_imf(&alice1, sync.payload().as_bytes(), false).await?;
930
931        assert_summary(&alice0, "You reacted ๐Ÿ‘† to \"mom's birthday!\"").await;
932        assert_summary(&alice1, "You reacted ๐Ÿ‘† to \"mom's birthday!\"").await;
933
934        Ok(())
935    }
936
937    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
938    async fn test_send_reaction_multidevice() -> Result<()> {
939        let mut tcm = TestContextManager::new();
940        let alice0 = tcm.alice().await;
941        let alice1 = tcm.alice().await;
942        let bob = tcm.bob().await;
943        let chat_id = alice0.create_chat(&bob).await.id;
944
945        let alice0_msg_id = send_text_msg(&alice0, chat_id, "foo".to_string()).await?;
946        let alice1_msg = alice1.recv_msg(&alice0.pop_sent_msg().await).await;
947
948        send_reaction(&alice0, alice0_msg_id, "๐Ÿ‘€").await?;
949        alice1.recv_msg_hidden(&alice0.pop_sent_msg().await).await;
950
951        expect_reactions_changed_event(&alice0, chat_id, alice0_msg_id, ContactId::SELF).await?;
952        expect_reactions_changed_event(&alice1, alice1_msg.chat_id, alice1_msg.id, ContactId::SELF)
953            .await?;
954        for a in [&alice0, &alice1] {
955            expect_no_unwanted_events(a).await;
956        }
957        Ok(())
958    }
959}