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::{send_msg, Chat, ChatId};
25use crate::chatlist_events;
26use crate::contact::ContactId;
27use crate::context::Context;
28use crate::events::EventType;
29use crate::log::info;
30use crate::message::{rfc724_mid_exists, Message, MsgId};
31use crate::param::Param;
32
33/// A single reaction consisting of multiple emoji sequences.
34///
35/// It is guaranteed to have all emojis sorted and deduplicated inside.
36#[derive(Debug, Default, Clone, Deserialize, Eq, PartialEq, Serialize)]
37pub struct Reaction {
38    /// Canonical representation of reaction as a string of space-separated emojis.
39    reaction: String,
40}
41
42// We implement From<&str> instead of std::str::FromStr, because
43// FromStr requires error type and reaction parsing never returns an
44// error.
45impl From<&str> for Reaction {
46    /// Parses a string containing a reaction.
47    ///
48    /// Reaction string is separated by spaces or tabs (`WSP` in ABNF),
49    /// but this function accepts any ASCII whitespace, so even a CRLF at
50    /// the end of string is acceptable.
51    ///
52    /// Any short enough string is accepted as a reaction to avoid the
53    /// complexity of validating emoji sequences as required by RFC
54    /// 9078. On the sender side UI is responsible to provide only
55    /// valid emoji sequences via reaction picker. On the receiver
56    /// side, abuse of the possibility to use arbitrary strings as
57    /// reactions is not different from other kinds of spam attacks
58    /// such as sending large numbers of large messages, and should be
59    /// dealt with the same way, e.g. by blocking the user.
60    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    /// Returns true if reaction contains no emojis.
74    pub fn is_empty(&self) -> bool {
75        self.reaction.is_empty()
76    }
77
78    /// Returns a vector of emojis composing a reaction.
79    pub fn emojis(&self) -> Vec<&str> {
80        self.reaction.split(' ').collect()
81    }
82
83    /// Returns space-separated string of emojis
84    pub fn as_str(&self) -> &str {
85        &self.reaction
86    }
87
88    /// Appends emojis from another reaction to this reaction.
89    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/// Structure representing all reactions to a particular message.
100#[derive(Debug)]
101pub struct Reactions {
102    /// Map from a contact to its reaction to message.
103    reactions: BTreeMap<ContactId, Reaction>,
104}
105
106impl Reactions {
107    /// Returns vector of contacts that reacted to the message.
108    pub fn contacts(&self) -> Vec<ContactId> {
109        self.reactions.keys().copied().collect()
110    }
111
112    /// Returns reaction of a given contact to message.
113    ///
114    /// If contact did not react to message or removed the reaction,
115    /// this method returns an empty reaction.
116    pub fn get(&self, contact_id: ContactId) -> Reaction {
117        self.reactions.get(&contact_id).cloned().unwrap_or_default()
118    }
119
120    /// Returns true if the message has no reactions.
121    pub fn is_empty(&self) -> bool {
122        self.reactions.is_empty()
123    }
124
125    /// Returns a map from emojis to their frequencies.
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 = 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    /// Check if there is a reaction newer than the given timestamp.
350    ///
351    /// If so, reaction details are returned and can be used to create a summary string.
352    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            // The message reacted to may be deleted.
373            // These are no errors as `Param::LastReaction*` are just weak pointers.
374            // Instead, just return `Ok(None)` and let the caller create another summary.
375            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::{delete_msgs, MessageState};
408    use crate::receive_imf::{receive_imf, receive_imf_from_inbox};
409    use crate::sql::housekeeping;
410    use crate::test_utils::TestContext;
411    use crate::test_utils::TestContextManager;
412    use crate::tools::SystemTime;
413    use std::time::Duration;
414
415    #[test]
416    fn test_parse_reaction() {
417        // Check that basic set of emojis from RFC 9078 is supported.
418        assert_eq!(Reaction::from("๐Ÿ‘").emojis(), vec!["๐Ÿ‘"]);
419        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
424        // Empty string can be used to remove all reactions.
425        assert!(Reaction::from("").is_empty());
426
427        // Short strings can be used as emojis, could be used to add
428        // support for custom emojis via emoji shortcodes.
429        assert_eq!(Reaction::from(":deltacat:").emojis(), vec![":deltacat:"]);
430
431        // Check that long strings are not valid emojis.
432        assert!(
433            Reaction::from(":foobarbazquuxaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:").is_empty()
434        );
435
436        // Multiple reactions separated by spaces or tabs are supported.
437        assert_eq!(Reaction::from("๐Ÿ‘ โค").emojis(), vec!["โค", "๐Ÿ‘"]);
438        assert_eq!(Reaction::from("๐Ÿ‘\tโค").emojis(), vec!["โค", "๐Ÿ‘"]);
439
440        // Invalid emojis are removed, but valid emojis are retained.
441        assert_eq!(
442            Reaction::from("๐Ÿ‘\t:foo: โค").emojis(),
443            vec![":foo:", "โค", "๐Ÿ‘"]
444        );
445        assert_eq!(Reaction::from("๐Ÿ‘\t:foo: โค").as_str(), ":foo: โค ๐Ÿ‘");
446
447        // Duplicates are removed.
448        assert_eq!(Reaction::from("๐Ÿ‘ ๐Ÿ‘").emojis(), vec!["๐Ÿ‘"]);
449    }
450
451    #[test]
452    fn test_add_reaction() {
453        let reaction1 = Reaction::from("๐Ÿ‘ ๐Ÿ˜€");
454        let reaction2 = Reaction::from("โค");
455        let reaction_sum = reaction1.add(reaction2);
456
457        assert_eq!(reaction_sum.emojis(), vec!["โค", "๐Ÿ‘", "๐Ÿ˜€"]);
458    }
459
460    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
461    async fn test_receive_reaction() -> Result<()> {
462        let alice = TestContext::new_alice().await;
463
464        // Alice receives BCC-self copy of a message sent to Bob.
465        receive_imf(
466            &alice,
467            "To: bob@example.net\n\
468From: alice@example.org\n\
469Date: Today, 29 February 2021 00:00:00 -800\n\
470Message-ID: 12345@example.org\n\
471Subject: Meeting\n\
472\n\
473Can we chat at 1pm pacific, today?"
474                .as_bytes(),
475            false,
476        )
477        .await?;
478        let msg = alice.get_last_msg().await;
479        assert_eq!(msg.state, MessageState::OutDelivered);
480        let reactions = get_msg_reactions(&alice, msg.id).await?;
481        let contacts = reactions.contacts();
482        assert_eq!(contacts.len(), 0);
483
484        let bob_id = Contact::add_or_lookup(
485            &alice,
486            "",
487            &ContactAddress::new("bob@example.net")?,
488            Origin::ManuallyCreated,
489        )
490        .await?
491        .0;
492        let bob_reaction = reactions.get(bob_id);
493        assert!(bob_reaction.is_empty()); // Bob has not reacted to message yet.
494
495        // Alice receives reaction to her message from Bob.
496        receive_imf(
497            &alice,
498            "To: alice@example.org\n\
499From: bob@example.net\n\
500Date: Today, 29 February 2021 00:00:10 -800\n\
501Message-ID: 56789@example.net\n\
502In-Reply-To: 12345@example.org\n\
503Subject: Meeting\n\
504Mime-Version: 1.0 (1.0)\n\
505Content-Type: text/plain; charset=utf-8\n\
506Content-Disposition: reaction\n\
507\n\
508\u{1F44D}"
509                .as_bytes(),
510            false,
511        )
512        .await?;
513
514        let reactions = get_msg_reactions(&alice, msg.id).await?;
515        assert_eq!(reactions.to_string(), "๐Ÿ‘1");
516
517        let contacts = reactions.contacts();
518        assert_eq!(contacts.len(), 1);
519
520        assert_eq!(contacts.first(), Some(&bob_id));
521        let bob_reaction = reactions.get(bob_id);
522        assert_eq!(bob_reaction.is_empty(), false);
523        assert_eq!(bob_reaction.emojis(), vec!["๐Ÿ‘"]);
524        assert_eq!(bob_reaction.as_str(), "๐Ÿ‘");
525
526        // Alice receives reaction to her message from Bob with a footer.
527        receive_imf(
528            &alice,
529            "To: alice@example.org\n\
530From: bob@example.net\n\
531Date: Today, 29 February 2021 00:00:10 -800\n\
532Message-ID: 56790@example.net\n\
533In-Reply-To: 12345@example.org\n\
534Subject: Meeting\n\
535Mime-Version: 1.0 (1.0)\n\
536Content-Type: text/plain; charset=utf-8\n\
537Content-Disposition: reaction\n\
538\n\
539๐Ÿ˜€\n\
540\n\
541--\n\
542_______________________________________________\n\
543Here's my footer -- bob@example.net"
544                .as_bytes(),
545            false,
546        )
547        .await?;
548
549        let reactions = get_msg_reactions(&alice, msg.id).await?;
550        assert_eq!(reactions.to_string(), "๐Ÿ˜€1");
551
552        Ok(())
553    }
554
555    async fn expect_reactions_changed_event(
556        t: &TestContext,
557        expected_chat_id: ChatId,
558        expected_msg_id: MsgId,
559        expected_contact_id: ContactId,
560    ) -> Result<()> {
561        let event = t
562            .evtracker
563            .get_matching(|evt| {
564                matches!(
565                    evt,
566                    EventType::ReactionsChanged { .. } | EventType::IncomingMsg { .. }
567                )
568            })
569            .await;
570        match event {
571            EventType::ReactionsChanged {
572                chat_id,
573                msg_id,
574                contact_id,
575            } => {
576                assert_eq!(chat_id, expected_chat_id);
577                assert_eq!(msg_id, expected_msg_id);
578                assert_eq!(contact_id, expected_contact_id);
579            }
580            _ => panic!("Unexpected event {event:?}."),
581        }
582        Ok(())
583    }
584
585    async fn expect_incoming_reactions_event(
586        t: &TestContext,
587        expected_chat_id: ChatId,
588        expected_msg_id: MsgId,
589        expected_contact_id: ContactId,
590        expected_reaction: &str,
591    ) -> Result<()> {
592        let event = t
593            .evtracker
594            // Check for absence of `IncomingMsg` events -- it appeared that it's quite easy to make
595            // bugs when `IncomingMsg` is issued for reactions.
596            .get_matching(|evt| {
597                matches!(
598                    evt,
599                    EventType::IncomingReaction { .. } | EventType::IncomingMsg { .. }
600                )
601            })
602            .await;
603        match event {
604            EventType::IncomingReaction {
605                chat_id,
606                msg_id,
607                contact_id,
608                reaction,
609            } => {
610                assert_eq!(chat_id, expected_chat_id);
611                assert_eq!(msg_id, expected_msg_id);
612                assert_eq!(contact_id, expected_contact_id);
613                assert_eq!(reaction, Reaction::from(expected_reaction));
614            }
615            _ => panic!("Unexpected event {event:?}."),
616        }
617        Ok(())
618    }
619
620    /// Checks that no unwanted events remain after expecting "wanted" reaction events.
621    async fn expect_no_unwanted_events(t: &TestContext) {
622        let ev = t
623            .evtracker
624            .get_matching_opt(t, |evt| {
625                matches!(
626                    evt,
627                    EventType::IncomingReaction { .. }
628                        | EventType::IncomingMsg { .. }
629                        | EventType::MsgsChanged { .. }
630                )
631            })
632            .await;
633        if let Some(ev) = ev {
634            panic!("Unwanted event {ev:?}.")
635        }
636    }
637
638    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
639    async fn test_send_reaction() -> Result<()> {
640        let alice = TestContext::new_alice().await;
641        let bob = TestContext::new_bob().await;
642
643        // Test that the status does not get mixed up into reactions.
644        alice
645            .set_config(
646                Config::Selfstatus,
647                Some("Buy Delta Chat today and make this banner go away!"),
648            )
649            .await?;
650        bob.set_config(Config::Selfstatus, Some("Sent from my Delta Chat Pro. ๐Ÿ‘"))
651            .await?;
652
653        let chat_alice = alice.create_chat(&bob).await;
654        let alice_msg = alice.send_text(chat_alice.id, "Hi!").await;
655        let bob_msg = bob.recv_msg(&alice_msg).await;
656        assert_eq!(get_chat_msgs(&alice, chat_alice.id).await?.len(), 1);
657        assert_eq!(get_chat_msgs(&bob, bob_msg.chat_id).await?.len(), 1);
658
659        let alice_msg2 = alice.send_text(chat_alice.id, "Hi again!").await;
660        bob.recv_msg(&alice_msg2).await;
661        assert_eq!(get_chat_msgs(&alice, chat_alice.id).await?.len(), 2);
662        assert_eq!(get_chat_msgs(&bob, bob_msg.chat_id).await?.len(), 2);
663
664        bob_msg.chat_id.accept(&bob).await?;
665
666        bob.evtracker.clear_events();
667        send_reaction(&bob, bob_msg.id, "๐Ÿ‘").await.unwrap();
668        expect_reactions_changed_event(&bob, bob_msg.chat_id, bob_msg.id, ContactId::SELF).await?;
669        expect_no_unwanted_events(&bob).await;
670        assert_eq!(get_chat_msgs(&bob, bob_msg.chat_id).await?.len(), 2);
671
672        let bob_reaction_msg = bob.pop_sent_msg().await;
673        let alice_reaction_msg = alice.recv_msg_hidden(&bob_reaction_msg).await;
674        assert_eq!(alice_reaction_msg.state, MessageState::InFresh);
675        assert_eq!(get_chat_msgs(&alice, chat_alice.id).await?.len(), 2);
676
677        let reactions = get_msg_reactions(&alice, alice_msg.sender_msg_id).await?;
678        assert_eq!(reactions.to_string(), "๐Ÿ‘1");
679        let contacts = reactions.contacts();
680        assert_eq!(contacts.len(), 1);
681        let bob_id = contacts.first().unwrap();
682        let bob_reaction = reactions.get(*bob_id);
683        assert_eq!(bob_reaction.is_empty(), false);
684        assert_eq!(bob_reaction.emojis(), vec!["๐Ÿ‘"]);
685        assert_eq!(bob_reaction.as_str(), "๐Ÿ‘");
686        expect_reactions_changed_event(&alice, chat_alice.id, alice_msg.sender_msg_id, *bob_id)
687            .await?;
688        expect_incoming_reactions_event(
689            &alice,
690            chat_alice.id,
691            alice_msg.sender_msg_id,
692            *bob_id,
693            "๐Ÿ‘",
694        )
695        .await?;
696        expect_no_unwanted_events(&alice).await;
697
698        marknoticed_chat(&alice, chat_alice.id).await?;
699        assert_eq!(
700            alice_reaction_msg.id.get_state(&alice).await?,
701            MessageState::InSeen
702        );
703        // Reactions don't request MDNs.
704        assert_eq!(
705            alice
706                .sql
707                .count("SELECT COUNT(*) FROM smtp_mdns", ())
708                .await?,
709            0
710        );
711
712        // Alice reacts to own message.
713        send_reaction(&alice, alice_msg.sender_msg_id, "๐Ÿ‘ ๐Ÿ˜€")
714            .await
715            .unwrap();
716        let reactions = get_msg_reactions(&alice, alice_msg.sender_msg_id).await?;
717        assert_eq!(reactions.to_string(), "๐Ÿ‘2 ๐Ÿ˜€1");
718
719        assert_eq!(
720            reactions.emoji_sorted_by_frequency(),
721            vec![("๐Ÿ‘".to_string(), 2), ("๐Ÿ˜€".to_string(), 1)]
722        );
723
724        Ok(())
725    }
726
727    async fn assert_summary(t: &TestContext, expected: &str) {
728        let chatlist = Chatlist::try_load(t, 0, None, None).await.unwrap();
729        let summary = chatlist.get_summary(t, 0, None).await.unwrap();
730        assert_eq!(summary.text, expected);
731    }
732
733    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
734    async fn test_reaction_summary() -> Result<()> {
735        let mut tcm = TestContextManager::new();
736        let alice = tcm.alice().await;
737        let bob = tcm.bob().await;
738        alice.set_config(Config::Displayname, Some("ALICE")).await?;
739        bob.set_config(Config::Displayname, Some("BOB")).await?;
740        let alice_bob_id = alice.add_or_lookup_contact_id(&bob).await;
741
742        // Alice sends message to Bob
743        let alice_chat = alice.create_chat(&bob).await;
744        let alice_msg1 = alice.send_text(alice_chat.id, "Party?").await;
745        let bob_msg1 = bob.recv_msg(&alice_msg1).await;
746
747        // Bob reacts to Alice's message, this is shown in the summaries
748        SystemTime::shift(Duration::from_secs(10));
749        bob_msg1.chat_id.accept(&bob).await?;
750        send_reaction(&bob, bob_msg1.id, "๐Ÿ‘").await?;
751        let bob_send_reaction = bob.pop_sent_msg().await;
752        alice.recv_msg_hidden(&bob_send_reaction).await;
753        expect_incoming_reactions_event(
754            &alice,
755            alice_chat.id,
756            alice_msg1.sender_msg_id,
757            alice_bob_id,
758            "๐Ÿ‘",
759        )
760        .await?;
761        expect_no_unwanted_events(&alice).await;
762
763        let chatlist = Chatlist::try_load(&bob, 0, None, None).await?;
764        let summary = chatlist.get_summary(&bob, 0, None).await?;
765        assert_eq!(summary.text, "You reacted ๐Ÿ‘ to \"Party?\"");
766        assert_eq!(summary.timestamp, bob_msg1.get_timestamp()); // time refers to message, not to reaction
767        assert_eq!(summary.state, MessageState::InFresh); // state refers to message, not to reaction
768        assert!(summary.prefix.is_none());
769        assert!(summary.thumbnail_path.is_none());
770        assert_summary(&alice, "BOB reacted ๐Ÿ‘ to \"Party?\"").await;
771
772        // Alice reacts to own message as well
773        SystemTime::shift(Duration::from_secs(10));
774        send_reaction(&alice, alice_msg1.sender_msg_id, "๐Ÿฟ").await?;
775        let alice_send_reaction = alice.pop_sent_msg().await;
776        bob.evtracker.clear_events();
777        bob.recv_msg_opt(&alice_send_reaction).await;
778        expect_no_unwanted_events(&bob).await;
779
780        assert_summary(&alice, "You reacted ๐Ÿฟ to \"Party?\"").await;
781        assert_summary(&bob, "ALICE reacted ๐Ÿฟ to \"Party?\"").await;
782
783        // Alice sends a newer message, this overwrites reaction summaries
784        SystemTime::shift(Duration::from_secs(10));
785        let alice_msg2 = alice.send_text(alice_chat.id, "kewl").await;
786        bob.recv_msg(&alice_msg2).await;
787
788        assert_summary(&alice, "kewl").await;
789        assert_summary(&bob, "kewl").await;
790
791        // Reactions to older messages still overwrite newer messages
792        SystemTime::shift(Duration::from_secs(10));
793        send_reaction(&alice, alice_msg1.sender_msg_id, "๐Ÿค˜").await?;
794        let alice_send_reaction = alice.pop_sent_msg().await;
795        bob.recv_msg_opt(&alice_send_reaction).await;
796
797        assert_summary(&alice, "You reacted ๐Ÿค˜ to \"Party?\"").await;
798        assert_summary(&bob, "ALICE reacted ๐Ÿค˜ to \"Party?\"").await;
799
800        // Retracted reactions remove all summary reactions
801        SystemTime::shift(Duration::from_secs(10));
802        send_reaction(&alice, alice_msg1.sender_msg_id, "").await?;
803        let alice_remove_reaction = alice.pop_sent_msg().await;
804        bob.recv_msg_opt(&alice_remove_reaction).await;
805
806        assert_summary(&alice, "kewl").await;
807        assert_summary(&bob, "kewl").await;
808
809        // Alice adds another reaction and then deletes the message reacted to; this will also delete reaction summary
810        SystemTime::shift(Duration::from_secs(10));
811        send_reaction(&alice, alice_msg1.sender_msg_id, "๐Ÿงน").await?;
812        assert_summary(&alice, "You reacted ๐Ÿงน to \"Party?\"").await;
813
814        delete_msgs(&alice, &[alice_msg1.sender_msg_id]).await?; // this will leave a tombstone
815        assert_summary(&alice, "kewl").await;
816        housekeeping(&alice).await?; // this will delete the tombstone
817        assert_summary(&alice, "kewl").await;
818
819        Ok(())
820    }
821
822    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
823    async fn test_reaction_forwarded_summary() -> Result<()> {
824        let alice = TestContext::new_alice().await;
825
826        // Alice adds a message to "Saved Messages"
827        let self_chat = alice.get_self_chat().await;
828        let msg_id = send_text_msg(&alice, self_chat.id, "foo".to_string()).await?;
829        assert_summary(&alice, "foo").await;
830
831        // Alice reacts to that message
832        SystemTime::shift(Duration::from_secs(10));
833        send_reaction(&alice, msg_id, "๐Ÿซ").await?;
834        assert_summary(&alice, "You reacted ๐Ÿซ to \"foo\"").await;
835        let reactions = get_msg_reactions(&alice, msg_id).await?;
836        assert_eq!(reactions.reactions.len(), 1);
837
838        // Alice forwards that message to Bob: Reactions are not forwarded, the message is prefixed by "Forwarded".
839        let bob_id = Contact::create(&alice, "", "bob@example.net").await?;
840        let bob_chat_id = ChatId::create_for_contact(&alice, bob_id).await?;
841        forward_msgs(&alice, &[msg_id], bob_chat_id).await?;
842        assert_summary(&alice, "Forwarded: foo").await; // forwarded messages are prefixed
843        let chatlist = Chatlist::try_load(&alice, 0, None, None).await.unwrap();
844        let forwarded_msg_id = chatlist.get_msg_id(0)?.unwrap();
845        let reactions = get_msg_reactions(&alice, forwarded_msg_id).await?;
846        assert!(reactions.reactions.is_empty()); // reactions are not forwarded
847
848        // Alice reacts to forwarded message:
849        // For reaction summary neither original message author nor "Forwarded" prefix is shown
850        SystemTime::shift(Duration::from_secs(10));
851        send_reaction(&alice, forwarded_msg_id, "๐Ÿณ").await?;
852        assert_summary(&alice, "You reacted ๐Ÿณ to \"foo\"").await;
853        let reactions = get_msg_reactions(&alice, msg_id).await?;
854        assert_eq!(reactions.reactions.len(), 1);
855
856        Ok(())
857    }
858
859    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
860    async fn test_reaction_self_chat_multidevice_summary() -> Result<()> {
861        let alice0 = TestContext::new_alice().await;
862        let alice1 = TestContext::new_alice().await;
863        let chat = alice0.get_self_chat().await;
864
865        let msg_id = send_text_msg(&alice0, chat.id, "mom's birthday!".to_string()).await?;
866        alice1.recv_msg(&alice0.pop_sent_msg().await).await;
867
868        SystemTime::shift(Duration::from_secs(10));
869        send_reaction(&alice0, msg_id, "๐Ÿ‘†").await?;
870        let sync = alice0.pop_sent_msg().await;
871        receive_imf(&alice1, sync.payload().as_bytes(), false).await?;
872
873        assert_summary(&alice0, "You reacted ๐Ÿ‘† to \"mom's birthday!\"").await;
874        assert_summary(&alice1, "You reacted ๐Ÿ‘† to \"mom's birthday!\"").await;
875
876        Ok(())
877    }
878
879    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
880    async fn test_partial_download_and_reaction() -> Result<()> {
881        let alice = TestContext::new_alice().await;
882        let bob = TestContext::new_bob().await;
883
884        alice
885            .create_chat_with_contact("Bob", "bob@example.net")
886            .await;
887
888        let msg_header = "From: Bob <bob@example.net>\n\
889                    To: Alice <alice@example.org>\n\
890                    Chat-Version: 1.0\n\
891                    Subject: subject\n\
892                    Message-ID: <first@example.org>\n\
893                    Date: Sun, 14 Nov 2021 00:10:00 +0000\
894                    Content-Type: text/plain";
895        let msg_full = format!("{msg_header}\n\n100k text...");
896
897        // Alice downloads message from Bob partially.
898        let alice_received_message = receive_imf_from_inbox(
899            &alice,
900            "first@example.org",
901            msg_header.as_bytes(),
902            false,
903            Some(100000),
904        )
905        .await?
906        .unwrap();
907        let alice_msg_id = *alice_received_message.msg_ids.first().unwrap();
908
909        // Bob downloads own message on the other device.
910        let bob_received_message = receive_imf(&bob, msg_full.as_bytes(), false)
911            .await?
912            .unwrap();
913        let bob_msg_id = *bob_received_message.msg_ids.first().unwrap();
914
915        // Bob reacts to own message.
916        send_reaction(&bob, bob_msg_id, "๐Ÿ‘").await.unwrap();
917        let bob_reaction_msg = bob.pop_sent_msg().await;
918
919        // Alice receives a reaction.
920        alice.recv_msg_hidden(&bob_reaction_msg).await;
921
922        let reactions = get_msg_reactions(&alice, alice_msg_id).await?;
923        assert_eq!(reactions.to_string(), "๐Ÿ‘1");
924        let msg = Message::load_from_db(&alice, alice_msg_id).await?;
925        assert_eq!(msg.download_state(), DownloadState::Available);
926
927        // Alice downloads full message.
928        receive_imf_from_inbox(
929            &alice,
930            "first@example.org",
931            msg_full.as_bytes(),
932            false,
933            None,
934        )
935        .await?;
936
937        // Check that reaction is still on the message after full download.
938        let msg = Message::load_from_db(&alice, alice_msg_id).await?;
939        assert_eq!(msg.download_state(), DownloadState::Done);
940        let reactions = get_msg_reactions(&alice, alice_msg_id).await?;
941        assert_eq!(reactions.to_string(), "๐Ÿ‘1");
942
943        Ok(())
944    }
945
946    /// Regression test for reaction resetting self-status.
947    ///
948    /// Reactions do not contain the status,
949    /// but should not result in self-status being reset on other devices.
950    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
951    async fn test_reaction_status_multidevice() -> Result<()> {
952        let mut tcm = TestContextManager::new();
953        let alice1 = tcm.alice().await;
954        let alice2 = tcm.alice().await;
955
956        alice1
957            .set_config(Config::Selfstatus, Some("New status"))
958            .await?;
959
960        let alice2_msg = tcm.send_recv(&alice1, &alice2, "Hi!").await;
961        assert_eq!(
962            alice2.get_config(Config::Selfstatus).await?.as_deref(),
963            Some("New status")
964        );
965
966        // Alice reacts to own message from second device,
967        // first device receives rection.
968        {
969            send_reaction(&alice2, alice2_msg.id, "๐Ÿ‘").await?;
970            let msg = alice2.pop_sent_msg().await;
971            alice1.recv_msg_hidden(&msg).await;
972        }
973
974        // Check that the status is still the same.
975        assert_eq!(
976            alice1.get_config(Config::Selfstatus).await?.as_deref(),
977            Some("New status")
978        );
979        Ok(())
980    }
981
982    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
983    async fn test_send_reaction_multidevice() -> Result<()> {
984        let mut tcm = TestContextManager::new();
985        let alice0 = tcm.alice().await;
986        let alice1 = tcm.alice().await;
987        let bob = tcm.bob().await;
988        let chat_id = alice0.create_chat(&bob).await.id;
989
990        let alice0_msg_id = send_text_msg(&alice0, chat_id, "foo".to_string()).await?;
991        let alice1_msg = alice1.recv_msg(&alice0.pop_sent_msg().await).await;
992
993        send_reaction(&alice0, alice0_msg_id, "๐Ÿ‘€").await?;
994        alice1.recv_msg_hidden(&alice0.pop_sent_msg().await).await;
995
996        expect_reactions_changed_event(&alice0, chat_id, alice0_msg_id, ContactId::SELF).await?;
997        expect_reactions_changed_event(&alice1, alice1_msg.chat_id, alice1_msg.id, ContactId::SELF)
998            .await?;
999        for a in [&alice0, &alice1] {
1000            expect_no_unwanted_events(a).await;
1001        }
1002        Ok(())
1003    }
1004}