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::message::{rfc724_mid_exists, Message, MsgId};
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    pub fn emoji_frequencies(&self) -> BTreeMap<String, usize> {
126        let mut emoji_frequencies: BTreeMap<String, usize> = BTreeMap::new();
127        for reaction in self.reactions.values() {
128            for emoji in reaction.emojis() {
129                emoji_frequencies
130                    .entry(emoji.to_string())
131                    .and_modify(|x| *x += 1)
132                    .or_insert(1);
133            }
134        }
135        emoji_frequencies
136    }
137
138    /// Returns a vector of emojis
139    /// sorted in descending order of frequencies.
140    ///
141    /// This function can be used to display the reactions in
142    /// the message bubble in the UIs.
143    pub fn emoji_sorted_by_frequency(&self) -> Vec<(String, usize)> {
144        let mut emoji_frequencies: Vec<(String, usize)> =
145            self.emoji_frequencies().into_iter().collect();
146        emoji_frequencies.sort_by(|(a, a_count), (b, b_count)| {
147            match a_count.cmp(b_count).reverse() {
148                Ordering::Equal => a.cmp(b),
149                other => other,
150            }
151        });
152        emoji_frequencies
153    }
154}
155
156impl fmt::Display for Reactions {
157    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
158        let emoji_frequencies = self.emoji_sorted_by_frequency();
159        let mut first = true;
160        for (emoji, frequency) in emoji_frequencies {
161            if !first {
162                write!(f, " ")?;
163            }
164            first = false;
165            write!(f, "{emoji}{frequency}")?;
166        }
167        Ok(())
168    }
169}
170
171async fn set_msg_id_reaction(
172    context: &Context,
173    msg_id: MsgId,
174    chat_id: ChatId,
175    contact_id: ContactId,
176    timestamp: i64,
177    reaction: &Reaction,
178) -> Result<()> {
179    if reaction.is_empty() {
180        // Simply remove the record instead of setting it to empty string.
181        context
182            .sql
183            .execute(
184                "DELETE FROM reactions
185                 WHERE msg_id = ?1
186                 AND contact_id = ?2",
187                (msg_id, contact_id),
188            )
189            .await?;
190    } else {
191        context
192            .sql
193            .execute(
194                "INSERT INTO reactions (msg_id, contact_id, reaction)
195                 VALUES (?1, ?2, ?3)
196                 ON CONFLICT(msg_id, contact_id)
197                 DO UPDATE SET reaction=excluded.reaction",
198                (msg_id, contact_id, reaction.as_str()),
199            )
200            .await?;
201        let mut chat = Chat::load_from_db(context, chat_id).await?;
202        if chat
203            .param
204            .update_timestamp(Param::LastReactionTimestamp, timestamp)?
205        {
206            chat.param
207                .set_i64(Param::LastReactionMsgId, i64::from(msg_id.to_u32()));
208            chat.param
209                .set_i64(Param::LastReactionContactId, i64::from(contact_id.to_u32()));
210            chat.update_param(context).await?;
211        }
212    }
213
214    context.emit_event(EventType::ReactionsChanged {
215        chat_id,
216        msg_id,
217        contact_id,
218    });
219    chatlist_events::emit_chatlist_item_changed(context, chat_id);
220    Ok(())
221}
222
223/// Sends a reaction to message `msg_id`, overriding previously sent reactions.
224///
225/// `reaction` is a string consisting of space-separated emoji. Use
226/// empty string to retract a reaction.
227pub async fn send_reaction(context: &Context, msg_id: MsgId, reaction: &str) -> Result<MsgId> {
228    let msg = Message::load_from_db(context, msg_id).await?;
229    let chat_id = msg.chat_id;
230
231    let reaction: Reaction = reaction.into();
232    let mut reaction_msg = Message::new_text(reaction.as_str().to_string());
233    reaction_msg.set_reaction();
234    reaction_msg.in_reply_to = Some(msg.rfc724_mid);
235    reaction_msg.hidden = true;
236
237    // Send message first.
238    let reaction_msg_id = send_msg(context, chat_id, &mut reaction_msg).await?;
239
240    // Only set reaction if we successfully sent the message.
241    set_msg_id_reaction(
242        context,
243        msg_id,
244        msg.chat_id,
245        ContactId::SELF,
246        reaction_msg.timestamp_sort,
247        &reaction,
248    )
249    .await?;
250    Ok(reaction_msg_id)
251}
252
253/// Adds given reaction to message `msg_id` and sends an update.
254///
255/// This can be used to implement advanced clients that allow reacting
256/// with multiple emojis. For a simple messenger UI, you probably want
257/// to use [`send_reaction()`] instead so reacting with a new emoji
258/// removes previous emoji at the same time.
259pub async fn add_reaction(context: &Context, msg_id: MsgId, reaction: &str) -> Result<MsgId> {
260    let self_reaction = get_self_reaction(context, msg_id).await?;
261    let reaction = self_reaction.add(Reaction::from(reaction));
262    send_reaction(context, msg_id, reaction.as_str()).await
263}
264
265/// Updates reaction of `contact_id` on the message with `in_reply_to`
266/// Message-ID. If no such message is found in the database, reaction
267/// is ignored.
268///
269/// `reaction` is a space-separated string of emojis. It can be empty
270/// if contact wants to remove all reactions.
271pub(crate) async fn set_msg_reaction(
272    context: &Context,
273    in_reply_to: &str,
274    chat_id: ChatId,
275    contact_id: ContactId,
276    timestamp: i64,
277    reaction: Reaction,
278    is_incoming_fresh: bool,
279) -> Result<()> {
280    if let Some((msg_id, _)) = rfc724_mid_exists(context, in_reply_to).await? {
281        set_msg_id_reaction(context, msg_id, chat_id, contact_id, timestamp, &reaction).await?;
282
283        if is_incoming_fresh
284            && !reaction.is_empty()
285            && msg_id.get_state(context).await?.is_outgoing()
286        {
287            context.emit_event(EventType::IncomingReaction {
288                chat_id,
289                contact_id,
290                msg_id,
291                reaction,
292            });
293        }
294    } else {
295        info!(
296            context,
297            "Can't assign reaction to unknown message with Message-ID {}", in_reply_to
298        );
299    }
300    Ok(())
301}
302
303/// Get our own reaction for a given message.
304async fn get_self_reaction(context: &Context, msg_id: MsgId) -> Result<Reaction> {
305    let reaction_str: Option<String> = context
306        .sql
307        .query_get_value(
308            "SELECT reaction
309             FROM reactions
310             WHERE msg_id=? AND contact_id=?",
311            (msg_id, ContactId::SELF),
312        )
313        .await?;
314    Ok(reaction_str
315        .as_deref()
316        .map(Reaction::from)
317        .unwrap_or_default())
318}
319
320/// Returns a structure containing all reactions to the message.
321pub async fn get_msg_reactions(context: &Context, msg_id: MsgId) -> Result<Reactions> {
322    let reactions = context
323        .sql
324        .query_map(
325            "SELECT contact_id, reaction FROM reactions WHERE msg_id=?",
326            (msg_id,),
327            |row| {
328                let contact_id: ContactId = row.get(0)?;
329                let reaction: String = row.get(1)?;
330                Ok((contact_id, reaction))
331            },
332            |rows| {
333                let mut reactions = Vec::new();
334                for row in rows {
335                    let (contact_id, reaction) = row?;
336                    reactions.push((contact_id, Reaction::from(reaction.as_str())));
337                }
338                Ok(reactions)
339            },
340        )
341        .await?
342        .into_iter()
343        .collect();
344    Ok(Reactions { reactions })
345}
346
347impl Chat {
348    /// Check if there is a reaction newer than the given timestamp.
349    ///
350    /// If so, reaction details are returned and can be used to create a summary string.
351    pub async fn get_last_reaction_if_newer_than(
352        &self,
353        context: &Context,
354        timestamp: i64,
355    ) -> Result<Option<(Message, ContactId, String)>> {
356        if self
357            .param
358            .get_i64(Param::LastReactionTimestamp)
359            .filter(|&reaction_timestamp| reaction_timestamp > timestamp)
360            .is_none()
361        {
362            return Ok(None);
363        };
364        let reaction_msg_id = MsgId::new(
365            self.param
366                .get_int(Param::LastReactionMsgId)
367                .unwrap_or_default() as u32,
368        );
369        let Some(reaction_msg) = Message::load_from_db_optional(context, reaction_msg_id).await?
370        else {
371            // The message reacted to may be deleted.
372            // These are no errors as `Param::LastReaction*` are just weak pointers.
373            // Instead, just return `Ok(None)` and let the caller create another summary.
374            return Ok(None);
375        };
376        let reaction_contact_id = ContactId::new(
377            self.param
378                .get_int(Param::LastReactionContactId)
379                .unwrap_or_default() as u32,
380        );
381        if let Some(reaction) = context
382            .sql
383            .query_get_value(
384                "SELECT reaction FROM reactions WHERE msg_id=? AND contact_id=?",
385                (reaction_msg.id, reaction_contact_id),
386            )
387            .await?
388        {
389            Ok(Some((reaction_msg, reaction_contact_id, reaction)))
390        } else {
391            Ok(None)
392        }
393    }
394}
395
396#[cfg(test)]
397mod tests {
398    use deltachat_contact_tools::ContactAddress;
399
400    use super::*;
401    use crate::chat::{forward_msgs, get_chat_msgs, marknoticed_chat, send_text_msg};
402    use crate::chatlist::Chatlist;
403    use crate::config::Config;
404    use crate::contact::{Contact, Origin};
405    use crate::download::DownloadState;
406    use crate::message::{delete_msgs, MessageState};
407    use crate::receive_imf::{receive_imf, receive_imf_from_inbox};
408    use crate::sql::housekeeping;
409    use crate::test_utils::TestContext;
410    use crate::test_utils::TestContextManager;
411    use crate::tools::SystemTime;
412    use std::time::Duration;
413
414    #[test]
415    fn test_parse_reaction() {
416        // Check that basic set of emojis from RFC 9078 is supported.
417        assert_eq!(Reaction::from("๐Ÿ‘").emojis(), vec!["๐Ÿ‘"]);
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
423        // Empty string can be used to remove all reactions.
424        assert!(Reaction::from("").is_empty());
425
426        // Short strings can be used as emojis, could be used to add
427        // support for custom emojis via emoji shortcodes.
428        assert_eq!(Reaction::from(":deltacat:").emojis(), vec![":deltacat:"]);
429
430        // Check that long strings are not valid emojis.
431        assert!(
432            Reaction::from(":foobarbazquuxaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa:").is_empty()
433        );
434
435        // Multiple reactions separated by spaces or tabs are supported.
436        assert_eq!(Reaction::from("๐Ÿ‘ โค").emojis(), vec!["โค", "๐Ÿ‘"]);
437        assert_eq!(Reaction::from("๐Ÿ‘\tโค").emojis(), vec!["โค", "๐Ÿ‘"]);
438
439        // Invalid emojis are removed, but valid emojis are retained.
440        assert_eq!(
441            Reaction::from("๐Ÿ‘\t:foo: โค").emojis(),
442            vec![":foo:", "โค", "๐Ÿ‘"]
443        );
444        assert_eq!(Reaction::from("๐Ÿ‘\t:foo: โค").as_str(), ":foo: โค ๐Ÿ‘");
445
446        // Duplicates are removed.
447        assert_eq!(Reaction::from("๐Ÿ‘ ๐Ÿ‘").emojis(), vec!["๐Ÿ‘"]);
448    }
449
450    #[test]
451    fn test_add_reaction() {
452        let reaction1 = Reaction::from("๐Ÿ‘ ๐Ÿ˜€");
453        let reaction2 = Reaction::from("โค");
454        let reaction_sum = reaction1.add(reaction2);
455
456        assert_eq!(reaction_sum.emojis(), vec!["โค", "๐Ÿ‘", "๐Ÿ˜€"]);
457    }
458
459    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
460    async fn test_receive_reaction() -> Result<()> {
461        let alice = TestContext::new_alice().await;
462
463        // Alice receives BCC-self copy of a message sent to Bob.
464        receive_imf(
465            &alice,
466            "To: bob@example.net\n\
467From: alice@example.org\n\
468Date: Today, 29 February 2021 00:00:00 -800\n\
469Message-ID: 12345@example.org\n\
470Subject: Meeting\n\
471\n\
472Can we chat at 1pm pacific, today?"
473                .as_bytes(),
474            false,
475        )
476        .await?;
477        let msg = alice.get_last_msg().await;
478        assert_eq!(msg.state, MessageState::OutDelivered);
479        let reactions = get_msg_reactions(&alice, msg.id).await?;
480        let contacts = reactions.contacts();
481        assert_eq!(contacts.len(), 0);
482
483        let bob_id = Contact::add_or_lookup(
484            &alice,
485            "",
486            &ContactAddress::new("bob@example.net")?,
487            Origin::ManuallyCreated,
488        )
489        .await?
490        .0;
491        let bob_reaction = reactions.get(bob_id);
492        assert!(bob_reaction.is_empty()); // Bob has not reacted to message yet.
493
494        // Alice receives reaction to her message from Bob.
495        receive_imf(
496            &alice,
497            "To: alice@example.org\n\
498From: bob@example.net\n\
499Date: Today, 29 February 2021 00:00:10 -800\n\
500Message-ID: 56789@example.net\n\
501In-Reply-To: 12345@example.org\n\
502Subject: Meeting\n\
503Mime-Version: 1.0 (1.0)\n\
504Content-Type: text/plain; charset=utf-8\n\
505Content-Disposition: reaction\n\
506\n\
507\u{1F44D}"
508                .as_bytes(),
509            false,
510        )
511        .await?;
512
513        let reactions = get_msg_reactions(&alice, msg.id).await?;
514        assert_eq!(reactions.to_string(), "๐Ÿ‘1");
515
516        let contacts = reactions.contacts();
517        assert_eq!(contacts.len(), 1);
518
519        assert_eq!(contacts.first(), Some(&bob_id));
520        let bob_reaction = reactions.get(bob_id);
521        assert_eq!(bob_reaction.is_empty(), false);
522        assert_eq!(bob_reaction.emojis(), vec!["๐Ÿ‘"]);
523        assert_eq!(bob_reaction.as_str(), "๐Ÿ‘");
524
525        // Alice receives reaction to her message from Bob with a footer.
526        receive_imf(
527            &alice,
528            "To: alice@example.org\n\
529From: bob@example.net\n\
530Date: Today, 29 February 2021 00:00:10 -800\n\
531Message-ID: 56790@example.net\n\
532In-Reply-To: 12345@example.org\n\
533Subject: Meeting\n\
534Mime-Version: 1.0 (1.0)\n\
535Content-Type: text/plain; charset=utf-8\n\
536Content-Disposition: reaction\n\
537\n\
538๐Ÿ˜€\n\
539\n\
540--\n\
541_______________________________________________\n\
542Here's my footer -- bob@example.net"
543                .as_bytes(),
544            false,
545        )
546        .await?;
547
548        let reactions = get_msg_reactions(&alice, msg.id).await?;
549        assert_eq!(reactions.to_string(), "๐Ÿ˜€1");
550
551        Ok(())
552    }
553
554    async fn expect_reactions_changed_event(
555        t: &TestContext,
556        expected_chat_id: ChatId,
557        expected_msg_id: MsgId,
558        expected_contact_id: ContactId,
559    ) -> Result<()> {
560        let event = t
561            .evtracker
562            .get_matching(|evt| {
563                matches!(
564                    evt,
565                    EventType::ReactionsChanged { .. } | EventType::IncomingMsg { .. }
566                )
567            })
568            .await;
569        match event {
570            EventType::ReactionsChanged {
571                chat_id,
572                msg_id,
573                contact_id,
574            } => {
575                assert_eq!(chat_id, expected_chat_id);
576                assert_eq!(msg_id, expected_msg_id);
577                assert_eq!(contact_id, expected_contact_id);
578            }
579            _ => panic!("Unexpected event {event:?}."),
580        }
581        Ok(())
582    }
583
584    async fn expect_incoming_reactions_event(
585        t: &TestContext,
586        expected_chat_id: ChatId,
587        expected_msg_id: MsgId,
588        expected_contact_id: ContactId,
589        expected_reaction: &str,
590    ) -> Result<()> {
591        let event = t
592            .evtracker
593            // Check for absence of `IncomingMsg` events -- it appeared that it's quite easy to make
594            // bugs when `IncomingMsg` is issued for reactions.
595            .get_matching(|evt| {
596                matches!(
597                    evt,
598                    EventType::IncomingReaction { .. } | EventType::IncomingMsg { .. }
599                )
600            })
601            .await;
602        match event {
603            EventType::IncomingReaction {
604                chat_id,
605                msg_id,
606                contact_id,
607                reaction,
608            } => {
609                assert_eq!(chat_id, expected_chat_id);
610                assert_eq!(msg_id, expected_msg_id);
611                assert_eq!(contact_id, expected_contact_id);
612                assert_eq!(reaction, Reaction::from(expected_reaction));
613            }
614            _ => panic!("Unexpected event {event:?}."),
615        }
616        Ok(())
617    }
618
619    /// Checks that no unwanted events remain after expecting "wanted" reaction events.
620    async fn expect_no_unwanted_events(t: &TestContext) {
621        let ev = t
622            .evtracker
623            .get_matching_opt(t, |evt| {
624                matches!(
625                    evt,
626                    EventType::IncomingReaction { .. }
627                        | EventType::IncomingMsg { .. }
628                        | EventType::MsgsChanged { .. }
629                )
630            })
631            .await;
632        if let Some(ev) = ev {
633            panic!("Unwanted event {ev:?}.")
634        }
635    }
636
637    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
638    async fn test_send_reaction() -> Result<()> {
639        let alice = TestContext::new_alice().await;
640        let bob = TestContext::new_bob().await;
641
642        // Test that the status does not get mixed up into reactions.
643        alice
644            .set_config(
645                Config::Selfstatus,
646                Some("Buy Delta Chat today and make this banner go away!"),
647            )
648            .await?;
649        bob.set_config(Config::Selfstatus, Some("Sent from my Delta Chat Pro. ๐Ÿ‘"))
650            .await?;
651
652        let chat_alice = alice.create_chat(&bob).await;
653        let alice_msg = alice.send_text(chat_alice.id, "Hi!").await;
654        let bob_msg = bob.recv_msg(&alice_msg).await;
655        assert_eq!(get_chat_msgs(&alice, chat_alice.id).await?.len(), 1);
656        assert_eq!(get_chat_msgs(&bob, bob_msg.chat_id).await?.len(), 1);
657
658        let alice_msg2 = alice.send_text(chat_alice.id, "Hi again!").await;
659        bob.recv_msg(&alice_msg2).await;
660        assert_eq!(get_chat_msgs(&alice, chat_alice.id).await?.len(), 2);
661        assert_eq!(get_chat_msgs(&bob, bob_msg.chat_id).await?.len(), 2);
662
663        bob_msg.chat_id.accept(&bob).await?;
664
665        bob.evtracker.clear_events();
666        send_reaction(&bob, bob_msg.id, "๐Ÿ‘").await.unwrap();
667        expect_reactions_changed_event(&bob, bob_msg.chat_id, bob_msg.id, ContactId::SELF).await?;
668        expect_no_unwanted_events(&bob).await;
669        assert_eq!(get_chat_msgs(&bob, bob_msg.chat_id).await?.len(), 2);
670
671        let bob_reaction_msg = bob.pop_sent_msg().await;
672        let alice_reaction_msg = alice.recv_msg_hidden(&bob_reaction_msg).await;
673        assert_eq!(alice_reaction_msg.state, MessageState::InFresh);
674        assert_eq!(get_chat_msgs(&alice, chat_alice.id).await?.len(), 2);
675
676        let reactions = get_msg_reactions(&alice, alice_msg.sender_msg_id).await?;
677        assert_eq!(reactions.to_string(), "๐Ÿ‘1");
678        let contacts = reactions.contacts();
679        assert_eq!(contacts.len(), 1);
680        let bob_id = contacts.first().unwrap();
681        let bob_reaction = reactions.get(*bob_id);
682        assert_eq!(bob_reaction.is_empty(), false);
683        assert_eq!(bob_reaction.emojis(), vec!["๐Ÿ‘"]);
684        assert_eq!(bob_reaction.as_str(), "๐Ÿ‘");
685        expect_reactions_changed_event(&alice, chat_alice.id, alice_msg.sender_msg_id, *bob_id)
686            .await?;
687        expect_incoming_reactions_event(
688            &alice,
689            chat_alice.id,
690            alice_msg.sender_msg_id,
691            *bob_id,
692            "๐Ÿ‘",
693        )
694        .await?;
695        expect_no_unwanted_events(&alice).await;
696
697        marknoticed_chat(&alice, chat_alice.id).await?;
698        assert_eq!(
699            alice_reaction_msg.id.get_state(&alice).await?,
700            MessageState::InSeen
701        );
702        // Reactions don't request MDNs.
703        assert_eq!(
704            alice
705                .sql
706                .count("SELECT COUNT(*) FROM smtp_mdns", ())
707                .await?,
708            0
709        );
710
711        // Alice reacts to own message.
712        send_reaction(&alice, alice_msg.sender_msg_id, "๐Ÿ‘ ๐Ÿ˜€")
713            .await
714            .unwrap();
715        let reactions = get_msg_reactions(&alice, alice_msg.sender_msg_id).await?;
716        assert_eq!(reactions.to_string(), "๐Ÿ‘2 ๐Ÿ˜€1");
717
718        assert_eq!(
719            reactions.emoji_sorted_by_frequency(),
720            vec![("๐Ÿ‘".to_string(), 2), ("๐Ÿ˜€".to_string(), 1)]
721        );
722
723        Ok(())
724    }
725
726    async fn assert_summary(t: &TestContext, expected: &str) {
727        let chatlist = Chatlist::try_load(t, 0, None, None).await.unwrap();
728        let summary = chatlist.get_summary(t, 0, None).await.unwrap();
729        assert_eq!(summary.text, expected);
730    }
731
732    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
733    async fn test_reaction_summary() -> Result<()> {
734        let mut tcm = TestContextManager::new();
735        let alice = tcm.alice().await;
736        let bob = tcm.bob().await;
737        alice.set_config(Config::Displayname, Some("ALICE")).await?;
738        bob.set_config(Config::Displayname, Some("BOB")).await?;
739        let alice_bob_id = alice.add_or_lookup_contact_id(&bob).await;
740
741        // Alice sends message to Bob
742        let alice_chat = alice.create_chat(&bob).await;
743        let alice_msg1 = alice.send_text(alice_chat.id, "Party?").await;
744        let bob_msg1 = bob.recv_msg(&alice_msg1).await;
745
746        // Bob reacts to Alice's message, this is shown in the summaries
747        SystemTime::shift(Duration::from_secs(10));
748        bob_msg1.chat_id.accept(&bob).await?;
749        send_reaction(&bob, bob_msg1.id, "๐Ÿ‘").await?;
750        let bob_send_reaction = bob.pop_sent_msg().await;
751        alice.recv_msg_hidden(&bob_send_reaction).await;
752        expect_incoming_reactions_event(
753            &alice,
754            alice_chat.id,
755            alice_msg1.sender_msg_id,
756            alice_bob_id,
757            "๐Ÿ‘",
758        )
759        .await?;
760        expect_no_unwanted_events(&alice).await;
761
762        let chatlist = Chatlist::try_load(&bob, 0, None, None).await?;
763        let summary = chatlist.get_summary(&bob, 0, None).await?;
764        assert_eq!(summary.text, "You reacted ๐Ÿ‘ to \"Party?\"");
765        assert_eq!(summary.timestamp, bob_msg1.get_timestamp()); // time refers to message, not to reaction
766        assert_eq!(summary.state, MessageState::InFresh); // state refers to message, not to reaction
767        assert!(summary.prefix.is_none());
768        assert!(summary.thumbnail_path.is_none());
769        assert_summary(&alice, "BOB reacted ๐Ÿ‘ to \"Party?\"").await;
770
771        // Alice reacts to own message as well
772        SystemTime::shift(Duration::from_secs(10));
773        send_reaction(&alice, alice_msg1.sender_msg_id, "๐Ÿฟ").await?;
774        let alice_send_reaction = alice.pop_sent_msg().await;
775        bob.evtracker.clear_events();
776        bob.recv_msg_opt(&alice_send_reaction).await;
777        expect_no_unwanted_events(&bob).await;
778
779        assert_summary(&alice, "You reacted ๐Ÿฟ to \"Party?\"").await;
780        assert_summary(&bob, "ALICE reacted ๐Ÿฟ to \"Party?\"").await;
781
782        // Alice sends a newer message, this overwrites reaction summaries
783        SystemTime::shift(Duration::from_secs(10));
784        let alice_msg2 = alice.send_text(alice_chat.id, "kewl").await;
785        bob.recv_msg(&alice_msg2).await;
786
787        assert_summary(&alice, "kewl").await;
788        assert_summary(&bob, "kewl").await;
789
790        // Reactions to older messages still overwrite newer messages
791        SystemTime::shift(Duration::from_secs(10));
792        send_reaction(&alice, alice_msg1.sender_msg_id, "๐Ÿค˜").await?;
793        let alice_send_reaction = alice.pop_sent_msg().await;
794        bob.recv_msg_opt(&alice_send_reaction).await;
795
796        assert_summary(&alice, "You reacted ๐Ÿค˜ to \"Party?\"").await;
797        assert_summary(&bob, "ALICE reacted ๐Ÿค˜ to \"Party?\"").await;
798
799        // Retracted reactions remove all summary reactions
800        SystemTime::shift(Duration::from_secs(10));
801        send_reaction(&alice, alice_msg1.sender_msg_id, "").await?;
802        let alice_remove_reaction = alice.pop_sent_msg().await;
803        bob.recv_msg_opt(&alice_remove_reaction).await;
804
805        assert_summary(&alice, "kewl").await;
806        assert_summary(&bob, "kewl").await;
807
808        // Alice adds another reaction and then deletes the message reacted to; this will also delete reaction summary
809        SystemTime::shift(Duration::from_secs(10));
810        send_reaction(&alice, alice_msg1.sender_msg_id, "๐Ÿงน").await?;
811        assert_summary(&alice, "You reacted ๐Ÿงน to \"Party?\"").await;
812
813        delete_msgs(&alice, &[alice_msg1.sender_msg_id]).await?; // this will leave a tombstone
814        assert_summary(&alice, "kewl").await;
815        housekeeping(&alice).await?; // this will delete the tombstone
816        assert_summary(&alice, "kewl").await;
817
818        Ok(())
819    }
820
821    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
822    async fn test_reaction_forwarded_summary() -> Result<()> {
823        let alice = TestContext::new_alice().await;
824
825        // Alice adds a message to "Saved Messages"
826        let self_chat = alice.get_self_chat().await;
827        let msg_id = send_text_msg(&alice, self_chat.id, "foo".to_string()).await?;
828        assert_summary(&alice, "foo").await;
829
830        // Alice reacts to that message
831        SystemTime::shift(Duration::from_secs(10));
832        send_reaction(&alice, msg_id, "๐Ÿซ").await?;
833        assert_summary(&alice, "You reacted ๐Ÿซ to \"foo\"").await;
834        let reactions = get_msg_reactions(&alice, msg_id).await?;
835        assert_eq!(reactions.reactions.len(), 1);
836
837        // Alice forwards that message to Bob: Reactions are not forwarded, the message is prefixed by "Forwarded".
838        let bob_id = Contact::create(&alice, "", "bob@example.net").await?;
839        let bob_chat_id = ChatId::create_for_contact(&alice, bob_id).await?;
840        forward_msgs(&alice, &[msg_id], bob_chat_id).await?;
841        assert_summary(&alice, "Forwarded: foo").await; // forwarded messages are prefixed
842        let chatlist = Chatlist::try_load(&alice, 0, None, None).await.unwrap();
843        let forwarded_msg_id = chatlist.get_msg_id(0)?.unwrap();
844        let reactions = get_msg_reactions(&alice, forwarded_msg_id).await?;
845        assert!(reactions.reactions.is_empty()); // reactions are not forwarded
846
847        // Alice reacts to forwarded message:
848        // For reaction summary neither original message author nor "Forwarded" prefix is shown
849        SystemTime::shift(Duration::from_secs(10));
850        send_reaction(&alice, forwarded_msg_id, "๐Ÿณ").await?;
851        assert_summary(&alice, "You reacted ๐Ÿณ to \"foo\"").await;
852        let reactions = get_msg_reactions(&alice, msg_id).await?;
853        assert_eq!(reactions.reactions.len(), 1);
854
855        Ok(())
856    }
857
858    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
859    async fn test_reaction_self_chat_multidevice_summary() -> Result<()> {
860        let alice0 = TestContext::new_alice().await;
861        let alice1 = TestContext::new_alice().await;
862        let chat = alice0.get_self_chat().await;
863
864        let msg_id = send_text_msg(&alice0, chat.id, "mom's birthday!".to_string()).await?;
865        alice1.recv_msg(&alice0.pop_sent_msg().await).await;
866
867        SystemTime::shift(Duration::from_secs(10));
868        send_reaction(&alice0, msg_id, "๐Ÿ‘†").await?;
869        let sync = alice0.pop_sent_msg().await;
870        receive_imf(&alice1, sync.payload().as_bytes(), false).await?;
871
872        assert_summary(&alice0, "You reacted ๐Ÿ‘† to \"mom's birthday!\"").await;
873        assert_summary(&alice1, "You reacted ๐Ÿ‘† to \"mom's birthday!\"").await;
874
875        Ok(())
876    }
877
878    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
879    async fn test_partial_download_and_reaction() -> Result<()> {
880        let alice = TestContext::new_alice().await;
881        let bob = TestContext::new_bob().await;
882
883        alice
884            .create_chat_with_contact("Bob", "bob@example.net")
885            .await;
886
887        let msg_header = "From: Bob <bob@example.net>\n\
888                    To: Alice <alice@example.org>\n\
889                    Chat-Version: 1.0\n\
890                    Subject: subject\n\
891                    Message-ID: <first@example.org>\n\
892                    Date: Sun, 14 Nov 2021 00:10:00 +0000\
893                    Content-Type: text/plain";
894        let msg_full = format!("{msg_header}\n\n100k text...");
895
896        // Alice downloads message from Bob partially.
897        let alice_received_message = receive_imf_from_inbox(
898            &alice,
899            "first@example.org",
900            msg_header.as_bytes(),
901            false,
902            Some(100000),
903        )
904        .await?
905        .unwrap();
906        let alice_msg_id = *alice_received_message.msg_ids.first().unwrap();
907
908        // Bob downloads own message on the other device.
909        let bob_received_message = receive_imf(&bob, msg_full.as_bytes(), false)
910            .await?
911            .unwrap();
912        let bob_msg_id = *bob_received_message.msg_ids.first().unwrap();
913
914        // Bob reacts to own message.
915        send_reaction(&bob, bob_msg_id, "๐Ÿ‘").await.unwrap();
916        let bob_reaction_msg = bob.pop_sent_msg().await;
917
918        // Alice receives a reaction.
919        alice.recv_msg_hidden(&bob_reaction_msg).await;
920
921        let reactions = get_msg_reactions(&alice, alice_msg_id).await?;
922        assert_eq!(reactions.to_string(), "๐Ÿ‘1");
923        let msg = Message::load_from_db(&alice, alice_msg_id).await?;
924        assert_eq!(msg.download_state(), DownloadState::Available);
925
926        // Alice downloads full message.
927        receive_imf_from_inbox(
928            &alice,
929            "first@example.org",
930            msg_full.as_bytes(),
931            false,
932            None,
933        )
934        .await?;
935
936        // Check that reaction is still on the message after full download.
937        let msg = Message::load_from_db(&alice, alice_msg_id).await?;
938        assert_eq!(msg.download_state(), DownloadState::Done);
939        let reactions = get_msg_reactions(&alice, alice_msg_id).await?;
940        assert_eq!(reactions.to_string(), "๐Ÿ‘1");
941
942        Ok(())
943    }
944
945    /// Regression test for reaction resetting self-status.
946    ///
947    /// Reactions do not contain the status,
948    /// but should not result in self-status being reset on other devices.
949    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
950    async fn test_reaction_status_multidevice() -> Result<()> {
951        let mut tcm = TestContextManager::new();
952        let alice1 = tcm.alice().await;
953        let alice2 = tcm.alice().await;
954
955        alice1
956            .set_config(Config::Selfstatus, Some("New status"))
957            .await?;
958
959        let alice2_msg = tcm.send_recv(&alice1, &alice2, "Hi!").await;
960        assert_eq!(
961            alice2.get_config(Config::Selfstatus).await?.as_deref(),
962            Some("New status")
963        );
964
965        // Alice reacts to own message from second device,
966        // first device receives rection.
967        {
968            send_reaction(&alice2, alice2_msg.id, "๐Ÿ‘").await?;
969            let msg = alice2.pop_sent_msg().await;
970            alice1.recv_msg_hidden(&msg).await;
971        }
972
973        // Check that the status is still the same.
974        assert_eq!(
975            alice1.get_config(Config::Selfstatus).await?.as_deref(),
976            Some("New status")
977        );
978        Ok(())
979    }
980
981    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
982    async fn test_send_reaction_multidevice() -> Result<()> {
983        let alice0 = TestContext::new_alice().await;
984        let alice1 = TestContext::new_alice().await;
985        let bob_id = Contact::create(&alice0, "", "bob@example.net").await?;
986        let chat_id = ChatId::create_for_contact(&alice0, bob_id).await?;
987
988        let alice0_msg_id = send_text_msg(&alice0, chat_id, "foo".to_string()).await?;
989        let alice1_msg = alice1.recv_msg(&alice0.pop_sent_msg().await).await;
990
991        send_reaction(&alice0, alice0_msg_id, "๐Ÿ‘€").await?;
992        alice1.recv_msg_hidden(&alice0.pop_sent_msg().await).await;
993
994        expect_reactions_changed_event(&alice0, chat_id, alice0_msg_id, ContactId::SELF).await?;
995        expect_reactions_changed_event(&alice1, alice1_msg.chat_id, alice1_msg.id, ContactId::SELF)
996            .await?;
997        for a in [&alice0, &alice1] {
998            expect_no_unwanted_events(a).await;
999        }
1000        Ok(())
1001    }
1002}