deltachat/
chatlist.rs

1//! # Chat list module.
2
3use anyhow::{ensure, Context as _, Result};
4use std::sync::LazyLock;
5
6use crate::chat::{update_special_chat_names, Chat, ChatId, ChatVisibility};
7use crate::constants::{
8    Blocked, Chattype, DC_CHAT_ID_ALLDONE_HINT, DC_CHAT_ID_ARCHIVED_LINK, DC_GCL_ADD_ALLDONE_HINT,
9    DC_GCL_ARCHIVED_ONLY, DC_GCL_FOR_FORWARDING, DC_GCL_NO_SPECIALS,
10};
11use crate::contact::{Contact, ContactId};
12use crate::context::Context;
13use crate::message::{Message, MessageState, MsgId};
14use crate::param::{Param, Params};
15use crate::stock_str;
16use crate::summary::Summary;
17use crate::tools::IsNoneOrEmpty;
18
19/// Regex to find out if a query should filter by unread messages.
20pub static IS_UNREAD_FILTER: LazyLock<regex::Regex> =
21    LazyLock::new(|| regex::Regex::new(r"\bis:unread\b").unwrap());
22
23/// An object representing a single chatlist in memory.
24///
25/// Chatlist objects contain chat IDs and, if possible, message IDs belonging to them.
26/// The chatlist object is not updated; if you want an update, you have to recreate the object.
27///
28/// For a **typical chat overview**, the idea is to get the list of all chats via dc_get_chatlist()
29/// without any listflags (see below) and to implement a "virtual list" or so
30/// (the count of chats is known by chatlist.len()).
31///
32/// Only for the items that are in view (the list may have several hundreds chats),
33/// the UI should call chatlist.get_summary() then.
34/// chatlist.get_summary() provides all elements needed for painting the item.
35///
36/// On a click of such an item, the UI should change to the chat view
37/// and get all messages from this view via dc_get_chat_msgs().
38/// Again, a "virtual list" is created (the count of messages is known)
39/// and for each messages that is scrolled into view, dc_get_msg() is called then.
40///
41/// Why no listflags?
42/// Without listflags, dc_get_chatlist() adds the archive "link" automatically as needed.
43/// The UI can just render these items differently then.
44#[derive(Debug)]
45pub struct Chatlist {
46    /// Stores pairs of `chat_id, message_id`
47    ids: Vec<(ChatId, Option<MsgId>)>,
48}
49
50impl Chatlist {
51    /// Get a list of chats.
52    /// The list can be filtered by query parameters.
53    ///
54    /// The list is already sorted and starts with the most recent chat in use.
55    /// The sorting takes care of invalid sending dates, drafts and chats without messages.
56    /// Clients should not try to re-sort the list as this would be an expensive action
57    /// and would result in inconsistencies between clients.
58    ///
59    /// To get information about each entry, use eg. chatlist.get_summary().
60    ///
61    /// By default, the function adds some special entries to the list.
62    /// These special entries can be identified by the ID returned by chatlist.get_chat_id():
63    /// - DC_CHAT_ID_ARCHIVED_LINK (6) - this special chat is present if the user has
64    ///   archived *any* chat using dc_set_chat_visibility(). The UI should show a link as
65    ///   "Show archived chats", if the user clicks this item, the UI should show a
66    ///   list of all archived chats that can be created by this function hen using
67    ///   the DC_GCL_ARCHIVED_ONLY flag.
68    /// - DC_CHAT_ID_ALLDONE_HINT (7) - this special chat is present
69    ///   if DC_GCL_ADD_ALLDONE_HINT is added to listflags
70    ///   and if there are only archived chats.
71    ///
72    /// The `listflags` is a combination of flags:
73    /// - if the flag DC_GCL_ARCHIVED_ONLY is set, only archived chats are returned.
74    ///   if DC_GCL_ARCHIVED_ONLY is not set, only unarchived chats are returned and
75    ///   the pseudo-chat DC_CHAT_ID_ARCHIVED_LINK is added if there are *any* archived
76    ///   chats
77    /// - the flag DC_GCL_FOR_FORWARDING sorts "Saved messages" to the top of the chatlist
78    ///   and hides the device-chat and contact requests
79    ///   typically used on forwarding, may be combined with DC_GCL_NO_SPECIALS
80    /// - if the flag DC_GCL_NO_SPECIALS is set, archive link is not added
81    ///   to the list (may be used eg. for selecting chats on forwarding, the flag is
82    ///   not needed when DC_GCL_ARCHIVED_ONLY is already set)
83    /// - if the flag DC_GCL_ADD_ALLDONE_HINT is set, DC_CHAT_ID_ALLDONE_HINT
84    ///   is added as needed.
85    ///
86    /// `query`: An optional query for filtering the list. Only chats matching this query
87    /// are returned. When `is:unread` is contained in the query, the chatlist is
88    /// filtered such that only chats with unread messages show up.
89    ///
90    /// `query_contact_id`: An optional contact ID for filtering the list. Only chats including this contact ID
91    /// are returned.
92    pub async fn try_load(
93        context: &Context,
94        listflags: usize,
95        query: Option<&str>,
96        query_contact_id: Option<ContactId>,
97    ) -> Result<Self> {
98        let flag_archived_only = 0 != listflags & DC_GCL_ARCHIVED_ONLY;
99        let flag_for_forwarding = 0 != listflags & DC_GCL_FOR_FORWARDING;
100        let flag_no_specials = 0 != listflags & DC_GCL_NO_SPECIALS;
101        let flag_add_alldone_hint = 0 != listflags & DC_GCL_ADD_ALLDONE_HINT;
102
103        let process_row = |row: &rusqlite::Row| {
104            let chat_id: ChatId = row.get(0)?;
105            let msg_id: Option<MsgId> = row.get(1)?;
106            Ok((chat_id, msg_id))
107        };
108
109        let process_rows = |rows: rusqlite::MappedRows<_>| {
110            rows.collect::<std::result::Result<Vec<_>, _>>()
111                .map_err(Into::into)
112        };
113
114        let skip_id = if flag_for_forwarding {
115            ChatId::lookup_by_contact(context, ContactId::DEVICE)
116                .await?
117                .unwrap_or_default()
118        } else {
119            ChatId::new(0)
120        };
121
122        // select with left join and minimum:
123        //
124        // - the inner select must use `hidden` and _not_ `m.hidden`
125        //   which would refer the outer select and take a lot of time
126        // - `GROUP BY` is needed several messages may have the same
127        //   timestamp
128        // - the list starts with the newest chats
129        //
130        // The query shows messages from blocked contacts in
131        // groups. Otherwise it would be hard to follow conversations.
132        let ids = if let Some(query_contact_id) = query_contact_id {
133            // show chats shared with a given contact
134            context.sql.query_map(
135                "SELECT c.id, m.id
136                 FROM chats c
137                 LEFT JOIN msgs m
138                        ON c.id=m.chat_id
139                       AND m.id=(
140                               SELECT id
141                                 FROM msgs
142                                WHERE chat_id=c.id
143                                  AND (hidden=0 OR state=?1)
144                                  ORDER BY timestamp DESC, id DESC LIMIT 1)
145                 WHERE c.id>9
146                   AND c.blocked!=1
147                   AND c.id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?2 AND add_timestamp >= remove_timestamp)
148                 GROUP BY c.id
149                 ORDER BY c.archived=?3 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
150                (MessageState::OutDraft, query_contact_id, ChatVisibility::Pinned),
151                process_row,
152                process_rows,
153            ).await?
154        } else if flag_archived_only {
155            // show archived chats
156            // (this includes the archived device-chat; we could skip it,
157            // however, then the number of archived chats do not match, which might be even more irritating.
158            // and adapting the number requires larger refactorings and seems not to be worth the effort)
159            context
160                .sql
161                .query_map(
162                    "SELECT c.id, m.id
163                 FROM chats c
164                 LEFT JOIN msgs m
165                        ON c.id=m.chat_id
166                       AND m.id=(
167                               SELECT id
168                                 FROM msgs
169                                WHERE chat_id=c.id
170                                  AND (hidden=0 OR state=?)
171                                  ORDER BY timestamp DESC, id DESC LIMIT 1)
172                 WHERE c.id>9
173                   AND c.blocked!=1
174                   AND c.archived=1
175                 GROUP BY c.id
176                 ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
177                    (MessageState::OutDraft,),
178                    process_row,
179                    process_rows,
180                )
181                .await?
182        } else if let Some(query) = query {
183            let mut query = query.trim().to_string();
184            ensure!(!query.is_empty(), "query mustn't be empty");
185            let only_unread = IS_UNREAD_FILTER.find(&query).is_some();
186            query = IS_UNREAD_FILTER.replace(&query, "").trim().to_string();
187
188            // allow searching over special names that may change at any time
189            // when the ui calls set_stock_translation()
190            if let Err(err) = update_special_chat_names(context).await {
191                warn!(context, "Cannot update special chat names: {err:#}.")
192            }
193
194            let str_like_cmd = format!("%{query}%");
195            context
196                .sql
197                .query_map(
198                    "SELECT c.id, m.id
199                 FROM chats c
200                 LEFT JOIN msgs m
201                        ON c.id=m.chat_id
202                       AND m.id=(
203                               SELECT id
204                                 FROM msgs
205                                WHERE chat_id=c.id
206                                  AND (hidden=0 OR state=?1)
207                                  ORDER BY timestamp DESC, id DESC LIMIT 1)
208                 WHERE c.id>9 AND c.id!=?2
209                   AND c.blocked!=1
210                   AND c.name LIKE ?3
211                   AND (NOT ?4 OR EXISTS (SELECT 1 FROM msgs m WHERE m.chat_id = c.id AND m.state == ?5 AND hidden=0))
212                 GROUP BY c.id
213                 ORDER BY IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
214                    (MessageState::OutDraft, skip_id, str_like_cmd, only_unread, MessageState::InFresh),
215                    process_row,
216                    process_rows,
217                )
218                .await?
219        } else {
220            let mut ids = if flag_for_forwarding {
221                let sort_id_up = ChatId::lookup_by_contact(context, ContactId::SELF)
222                    .await?
223                    .unwrap_or_default();
224                let process_row = |row: &rusqlite::Row| {
225                    let chat_id: ChatId = row.get(0)?;
226                    let typ: Chattype = row.get(1)?;
227                    let param: Params = row.get::<_, String>(2)?.parse().unwrap_or_default();
228                    let msg_id: Option<MsgId> = row.get(3)?;
229                    Ok((chat_id, typ, param, msg_id))
230                };
231                let process_rows = |rows: rusqlite::MappedRows<_>| {
232                    rows.filter_map(|row: std::result::Result<(_, _, Params, _), _>| match row {
233                        Ok((chat_id, typ, param, msg_id)) => {
234                            if typ == Chattype::Mailinglist
235                                && param.get(Param::ListPost).is_none_or_empty()
236                            {
237                                None
238                            } else {
239                                Some(Ok((chat_id, msg_id)))
240                            }
241                        }
242                        Err(e) => Some(Err(e)),
243                    })
244                    .collect::<std::result::Result<Vec<_>, _>>()
245                    .map_err(Into::into)
246                };
247                // Return ProtectionBroken chats also, as that may happen to a verified chat at any
248                // time. It may be confusing if a chat that is normally in the list disappears
249                // suddenly. The UI need to deal with that case anyway.
250                context.sql.query_map(
251                    "SELECT c.id, c.type, c.param, m.id
252                     FROM chats c
253                     LEFT JOIN msgs m
254                            ON c.id=m.chat_id
255                           AND m.id=(
256                                   SELECT id
257                                     FROM msgs
258                                    WHERE chat_id=c.id
259                                      AND (hidden=0 OR state=?)
260                                      ORDER BY timestamp DESC, id DESC LIMIT 1)
261                     WHERE c.id>9 AND c.id!=?
262                       AND c.blocked=0
263                       AND NOT c.archived=?
264                       AND (c.type!=? OR c.id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=? AND add_timestamp >= remove_timestamp))
265                     GROUP BY c.id
266                     ORDER BY c.id=? DESC, c.archived=? DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
267                    (
268                        MessageState::OutDraft, skip_id, ChatVisibility::Archived,
269                        Chattype::Group, ContactId::SELF,
270                        sort_id_up, ChatVisibility::Pinned,
271                    ),
272                    process_row,
273                    process_rows,
274                ).await?
275            } else {
276                //  show normal chatlist
277                context.sql.query_map(
278                    "SELECT c.id, m.id
279                     FROM chats c
280                     LEFT JOIN msgs m
281                            ON c.id=m.chat_id
282                           AND m.id=(
283                                   SELECT id
284                                     FROM msgs
285                                    WHERE chat_id=c.id
286                                      AND (hidden=0 OR state=?)
287                                      ORDER BY timestamp DESC, id DESC LIMIT 1)
288                     WHERE c.id>9 AND c.id!=?
289                       AND (c.blocked=0 OR c.blocked=2)
290                       AND NOT c.archived=?
291                     GROUP BY c.id
292                     ORDER BY c.id=0 DESC, c.archived=? DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
293                    (MessageState::OutDraft, skip_id, ChatVisibility::Archived, ChatVisibility::Pinned),
294                    process_row,
295                    process_rows,
296                ).await?
297            };
298            if !flag_no_specials && get_archived_cnt(context).await? > 0 {
299                if ids.is_empty() && flag_add_alldone_hint {
300                    ids.push((DC_CHAT_ID_ALLDONE_HINT, None));
301                }
302                ids.insert(0, (DC_CHAT_ID_ARCHIVED_LINK, None));
303            }
304            ids
305        };
306
307        Ok(Chatlist { ids })
308    }
309
310    /// Converts list of chat IDs to a chatlist.
311    pub(crate) async fn from_chat_ids(context: &Context, chat_ids: &[ChatId]) -> Result<Self> {
312        let mut ids = Vec::new();
313        for &chat_id in chat_ids {
314            let msg_id: Option<MsgId> = context
315                .sql
316                .query_get_value(
317                    "SELECT id
318                   FROM msgs
319                  WHERE chat_id=?1
320                    AND (hidden=0 OR state=?2)
321                  ORDER BY timestamp DESC, id DESC LIMIT 1",
322                    (chat_id, MessageState::OutDraft),
323                )
324                .await
325                .with_context(|| format!("failed to get msg ID for chat {}", chat_id))?;
326            ids.push((chat_id, msg_id));
327        }
328        Ok(Chatlist { ids })
329    }
330
331    /// Find out the number of chats.
332    pub fn len(&self) -> usize {
333        self.ids.len()
334    }
335
336    /// Returns true if chatlist is empty.
337    pub fn is_empty(&self) -> bool {
338        self.ids.is_empty()
339    }
340
341    /// Get a single chat ID of a chatlist.
342    ///
343    /// To get the message object from the message ID, use dc_get_chat().
344    pub fn get_chat_id(&self, index: usize) -> Result<ChatId> {
345        let (chat_id, _msg_id) = self
346            .ids
347            .get(index)
348            .context("chatlist index is out of range")?;
349        Ok(*chat_id)
350    }
351
352    /// Get a single message ID of a chatlist.
353    ///
354    /// To get the message object from the message ID, use dc_get_msg().
355    pub fn get_msg_id(&self, index: usize) -> Result<Option<MsgId>> {
356        let (_chat_id, msg_id) = self
357            .ids
358            .get(index)
359            .context("chatlist index is out of range")?;
360        Ok(*msg_id)
361    }
362
363    /// Returns a summary for a given chatlist index.
364    pub async fn get_summary(
365        &self,
366        context: &Context,
367        index: usize,
368        chat: Option<&Chat>,
369    ) -> Result<Summary> {
370        // The summary is created by the chat, not by the last message.
371        // This is because we may want to display drafts here or stuff as
372        // "is typing".
373        // Also, sth. as "No messages" would not work if the summary comes from a message.
374        let (chat_id, lastmsg_id) = self
375            .ids
376            .get(index)
377            .context("chatlist index is out of range")?;
378        Chatlist::get_summary2(context, *chat_id, *lastmsg_id, chat).await
379    }
380
381    /// Returns a summary for a given chatlist item.
382    pub async fn get_summary2(
383        context: &Context,
384        chat_id: ChatId,
385        lastmsg_id: Option<MsgId>,
386        chat: Option<&Chat>,
387    ) -> Result<Summary> {
388        let chat_loaded: Chat;
389        let chat = if let Some(chat) = chat {
390            chat
391        } else {
392            let chat = Chat::load_from_db(context, chat_id).await?;
393            chat_loaded = chat;
394            &chat_loaded
395        };
396
397        let lastmsg = if let Some(lastmsg_id) = lastmsg_id {
398            // Message may be deleted by the time we try to load it,
399            // so use `load_from_db_optional` instead of `load_from_db`.
400            Message::load_from_db_optional(context, lastmsg_id)
401                .await
402                .context("Loading message failed")?
403        } else {
404            None
405        };
406
407        let lastcontact = if let Some(lastmsg) = &lastmsg {
408            if lastmsg.from_id == ContactId::SELF {
409                None
410            } else if chat.typ == Chattype::Group
411                || chat.typ == Chattype::Broadcast
412                || chat.typ == Chattype::Mailinglist
413                || chat.is_self_talk()
414            {
415                let lastcontact = Contact::get_by_id(context, lastmsg.from_id)
416                    .await
417                    .context("loading contact failed")?;
418                Some(lastcontact)
419            } else {
420                None
421            }
422        } else {
423            None
424        };
425
426        if chat.id.is_archived_link() {
427            Ok(Default::default())
428        } else if let Some(lastmsg) = lastmsg.filter(|msg| msg.from_id != ContactId::UNDEFINED) {
429            Summary::new_with_reaction_details(context, &lastmsg, chat, lastcontact.as_ref()).await
430        } else {
431            Ok(Summary {
432                text: stock_str::no_messages(context).await,
433                ..Default::default()
434            })
435        }
436    }
437
438    /// Returns chatlist item position for the given chat ID.
439    pub fn get_index_for_id(&self, id: ChatId) -> Option<usize> {
440        self.ids.iter().position(|(chat_id, _)| chat_id == &id)
441    }
442
443    /// An iterator visiting all chatlist items.
444    pub fn iter(&self) -> impl Iterator<Item = &(ChatId, Option<MsgId>)> {
445        self.ids.iter()
446    }
447}
448
449/// Returns the number of archived chats
450pub async fn get_archived_cnt(context: &Context) -> Result<usize> {
451    let count = context
452        .sql
453        .count(
454            "SELECT COUNT(*) FROM chats WHERE blocked!=? AND archived=?;",
455            (Blocked::Yes, ChatVisibility::Archived),
456        )
457        .await?;
458    Ok(count)
459}
460
461/// Gets the last message of a chat, the message that would also be displayed in the ChatList
462/// Used for passing to `deltachat::chatlist::Chatlist::get_summary2`
463pub async fn get_last_message_for_chat(
464    context: &Context,
465    chat_id: ChatId,
466) -> Result<Option<MsgId>> {
467    context
468        .sql
469        .query_get_value(
470            "SELECT id
471                FROM msgs
472                WHERE chat_id=?2
473                AND (hidden=0 OR state=?1)
474                ORDER BY timestamp DESC, id DESC LIMIT 1",
475            (MessageState::OutDraft, chat_id),
476        )
477        .await
478}
479
480#[cfg(test)]
481mod tests {
482    use super::*;
483    use crate::chat::save_msgs;
484    use crate::chat::{
485        add_contact_to_chat, create_group_chat, get_chat_contacts, remove_contact_from_chat,
486        send_text_msg, ProtectionStatus,
487    };
488    use crate::receive_imf::receive_imf;
489    use crate::stock_str::StockMessage;
490    use crate::test_utils::TestContext;
491    use crate::test_utils::TestContextManager;
492
493    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
494    async fn test_try_load() {
495        let mut tcm = TestContextManager::new();
496        let bob = &tcm.bob().await;
497        let chat_id1 = create_group_chat(bob, ProtectionStatus::Unprotected, "a chat")
498            .await
499            .unwrap();
500        let chat_id2 = create_group_chat(bob, ProtectionStatus::Unprotected, "b chat")
501            .await
502            .unwrap();
503        let chat_id3 = create_group_chat(bob, ProtectionStatus::Unprotected, "c chat")
504            .await
505            .unwrap();
506
507        // check that the chatlist starts with the most recent message
508        let chats = Chatlist::try_load(bob, 0, None, None).await.unwrap();
509        assert_eq!(chats.len(), 3);
510        assert_eq!(chats.get_chat_id(0).unwrap(), chat_id3);
511        assert_eq!(chats.get_chat_id(1).unwrap(), chat_id2);
512        assert_eq!(chats.get_chat_id(2).unwrap(), chat_id1);
513
514        // New drafts are sorted to the top
515        // We have to set a draft on the other two messages, too, as
516        // chat timestamps are only exact to the second and sorting by timestamp
517        // would not work.
518        // Message timestamps are "smeared" and unique, so we don't have this problem
519        // if we have any message (can be a draft) in all chats.
520        // Instead of setting drafts for chat_id1 and chat_id3, we could also sleep
521        // 2s here.
522        for chat_id in &[chat_id1, chat_id3, chat_id2] {
523            let mut msg = Message::new_text("hello".to_string());
524            chat_id.set_draft(bob, Some(&mut msg)).await.unwrap();
525        }
526
527        let chats = Chatlist::try_load(bob, 0, None, None).await.unwrap();
528        assert_eq!(chats.get_chat_id(0).unwrap(), chat_id2);
529
530        // check chatlist query and archive functionality
531        let chats = Chatlist::try_load(bob, 0, Some("b"), None).await.unwrap();
532        assert_eq!(chats.len(), 1);
533
534        // receive a message from alice
535        let alice = &tcm.alice().await;
536        let alice_chat_id = create_group_chat(alice, ProtectionStatus::Unprotected, "alice chat")
537            .await
538            .unwrap();
539        add_contact_to_chat(
540            alice,
541            alice_chat_id,
542            alice.add_or_lookup_contact_id(bob).await,
543        )
544        .await
545        .unwrap();
546        send_text_msg(alice, alice_chat_id, "hi".into())
547            .await
548            .unwrap();
549        let sent_msg = alice.pop_sent_msg().await;
550
551        bob.recv_msg(&sent_msg).await;
552        let chats = Chatlist::try_load(bob, 0, Some("is:unread"), None)
553            .await
554            .unwrap();
555        assert_eq!(chats.len(), 1);
556
557        let chats = Chatlist::try_load(bob, DC_GCL_ARCHIVED_ONLY, None, None)
558            .await
559            .unwrap();
560        assert_eq!(chats.len(), 0);
561
562        chat_id1
563            .set_visibility(bob, ChatVisibility::Archived)
564            .await
565            .ok();
566        let chats = Chatlist::try_load(bob, DC_GCL_ARCHIVED_ONLY, None, None)
567            .await
568            .unwrap();
569        assert_eq!(chats.len(), 1);
570    }
571
572    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
573    async fn test_sort_self_talk_up_on_forward() {
574        let t = TestContext::new().await;
575        t.update_device_chats().await.unwrap();
576        create_group_chat(&t, ProtectionStatus::Unprotected, "a chat")
577            .await
578            .unwrap();
579
580        let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
581        assert_eq!(chats.len(), 3);
582        assert!(!Chat::load_from_db(&t, chats.get_chat_id(0).unwrap())
583            .await
584            .unwrap()
585            .is_self_talk());
586
587        let chats = Chatlist::try_load(&t, DC_GCL_FOR_FORWARDING, None, None)
588            .await
589            .unwrap();
590        assert_eq!(chats.len(), 2); // device chat cannot be written and is skipped on forwarding
591        assert!(Chat::load_from_db(&t, chats.get_chat_id(0).unwrap())
592            .await
593            .unwrap()
594            .is_self_talk());
595
596        remove_contact_from_chat(&t, chats.get_chat_id(1).unwrap(), ContactId::SELF)
597            .await
598            .unwrap();
599        let chats = Chatlist::try_load(&t, DC_GCL_FOR_FORWARDING, None, None)
600            .await
601            .unwrap();
602        assert_eq!(chats.len(), 1);
603    }
604
605    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
606    async fn test_search_special_chat_names() {
607        let t = TestContext::new().await;
608        t.update_device_chats().await.unwrap();
609
610        let chats = Chatlist::try_load(&t, 0, Some("t-1234-s"), None)
611            .await
612            .unwrap();
613        assert_eq!(chats.len(), 0);
614        let chats = Chatlist::try_load(&t, 0, Some("t-5678-b"), None)
615            .await
616            .unwrap();
617        assert_eq!(chats.len(), 0);
618
619        t.set_stock_translation(StockMessage::SavedMessages, "test-1234-save".to_string())
620            .await
621            .unwrap();
622        let chats = Chatlist::try_load(&t, 0, Some("t-1234-s"), None)
623            .await
624            .unwrap();
625        assert_eq!(chats.len(), 1);
626
627        t.set_stock_translation(StockMessage::DeviceMessages, "test-5678-babbel".to_string())
628            .await
629            .unwrap();
630        let chats = Chatlist::try_load(&t, 0, Some("t-5678-b"), None)
631            .await
632            .unwrap();
633        assert_eq!(chats.len(), 1);
634    }
635
636    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
637    async fn test_search_single_chat() -> anyhow::Result<()> {
638        let t = TestContext::new_alice().await;
639
640        // receive a one-to-one-message
641        receive_imf(
642            &t,
643            b"From: Bob Authname <bob@example.org>\n\
644                 To: alice@example.org\n\
645                 Subject: foo\n\
646                 Message-ID: <msg1234@example.org>\n\
647                 Chat-Version: 1.0\n\
648                 Date: Sun, 22 Mar 2021 22:37:57 +0000\n\
649                 \n\
650                 hello foo\n",
651            false,
652        )
653        .await?;
654
655        let chats = Chatlist::try_load(&t, 0, Some("Bob Authname"), None).await?;
656        // Contact request should be searchable
657        assert_eq!(chats.len(), 1);
658
659        let msg = t.get_last_msg().await;
660        let chat_id = msg.get_chat_id();
661        chat_id.accept(&t).await.unwrap();
662
663        let contacts = get_chat_contacts(&t, chat_id).await?;
664        let contact_id = *contacts.first().unwrap();
665        let chat = Chat::load_from_db(&t, chat_id).await?;
666        assert_eq!(chat.get_name(), "Bob Authname");
667
668        // check, the one-to-one-chat can be found using chatlist search query
669        let chats = Chatlist::try_load(&t, 0, Some("bob authname"), None).await?;
670        assert_eq!(chats.len(), 1);
671        assert_eq!(chats.get_chat_id(0).unwrap(), chat_id);
672
673        // change the name of the contact; this also changes the name of the one-to-one-chat
674        let test_id = Contact::create(&t, "Bob Nickname", "bob@example.org").await?;
675        assert_eq!(contact_id, test_id);
676        let chat = Chat::load_from_db(&t, chat_id).await?;
677        assert_eq!(chat.get_name(), "Bob Nickname");
678        let chats = Chatlist::try_load(&t, 0, Some("bob authname"), None).await?;
679        assert_eq!(chats.len(), 0);
680        let chats = Chatlist::try_load(&t, 0, Some("bob nickname"), None).await?;
681        assert_eq!(chats.len(), 1);
682
683        // revert contact to authname, this again changes the name of the one-to-one-chat
684        let test_id = Contact::create(&t, "", "bob@example.org").await?;
685        assert_eq!(contact_id, test_id);
686        let chat = Chat::load_from_db(&t, chat_id).await?;
687        assert_eq!(chat.get_name(), "Bob Authname");
688        let chats = Chatlist::try_load(&t, 0, Some("bob authname"), None).await?;
689        assert_eq!(chats.len(), 1);
690        let chats = Chatlist::try_load(&t, 0, Some("bob nickname"), None).await?;
691        assert_eq!(chats.len(), 0);
692
693        Ok(())
694    }
695
696    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
697    async fn test_search_single_chat_without_authname() -> anyhow::Result<()> {
698        let t = TestContext::new_alice().await;
699
700        // receive a one-to-one-message without authname set
701        receive_imf(
702            &t,
703            b"From: bob@example.org\n\
704                 To: alice@example.org\n\
705                 Subject: foo\n\
706                 Message-ID: <msg5678@example.org>\n\
707                 Chat-Version: 1.0\n\
708                 Date: Sun, 22 Mar 2021 22:38:57 +0000\n\
709                 \n\
710                 hello foo\n",
711            false,
712        )
713        .await?;
714
715        let msg = t.get_last_msg().await;
716        let chat_id = msg.get_chat_id();
717        chat_id.accept(&t).await.unwrap();
718        let contacts = get_chat_contacts(&t, chat_id).await?;
719        let contact_id = *contacts.first().unwrap();
720        let chat = Chat::load_from_db(&t, chat_id).await?;
721        assert_eq!(chat.get_name(), "bob@example.org");
722
723        // check, the one-to-one-chat can be found using chatlist search query
724        let chats = Chatlist::try_load(&t, 0, Some("bob@example.org"), None).await?;
725        assert_eq!(chats.len(), 1);
726        assert_eq!(chats.get_chat_id(0)?, chat_id);
727
728        // change the name of the contact; this also changes the name of the one-to-one-chat
729        let test_id = Contact::create(&t, "Bob Nickname", "bob@example.org").await?;
730        assert_eq!(contact_id, test_id);
731        let chat = Chat::load_from_db(&t, chat_id).await?;
732        assert_eq!(chat.get_name(), "Bob Nickname");
733        let chats = Chatlist::try_load(&t, 0, Some("bob@example.org"), None).await?;
734        assert_eq!(chats.len(), 0); // email-addresses are searchable in contacts, not in chats
735        let chats = Chatlist::try_load(&t, 0, Some("Bob Nickname"), None).await?;
736        assert_eq!(chats.len(), 1);
737        assert_eq!(chats.get_chat_id(0)?, chat_id);
738
739        // revert name change, this again changes the name of the one-to-one-chat to the email-address
740        let test_id = Contact::create(&t, "", "bob@example.org").await?;
741        assert_eq!(contact_id, test_id);
742        let chat = Chat::load_from_db(&t, chat_id).await?;
743        assert_eq!(chat.get_name(), "bob@example.org");
744        let chats = Chatlist::try_load(&t, 0, Some("bob@example.org"), None).await?;
745        assert_eq!(chats.len(), 1);
746        let chats = Chatlist::try_load(&t, 0, Some("bob nickname"), None).await?;
747        assert_eq!(chats.len(), 0);
748
749        // finally, also check that a simple substring-search is working with email-addresses
750        let chats = Chatlist::try_load(&t, 0, Some("b@exa"), None).await?;
751        assert_eq!(chats.len(), 1);
752        let chats = Chatlist::try_load(&t, 0, Some("b@exac"), None).await?;
753        assert_eq!(chats.len(), 0);
754
755        Ok(())
756    }
757
758    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
759    async fn test_get_summary_unwrap() {
760        let t = TestContext::new().await;
761        let chat_id1 = create_group_chat(&t, ProtectionStatus::Unprotected, "a chat")
762            .await
763            .unwrap();
764
765        let mut msg = Message::new_text("foo:\nbar \r\n test".to_string());
766        chat_id1.set_draft(&t, Some(&mut msg)).await.unwrap();
767
768        let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
769        let summary = chats.get_summary(&t, 0, None).await.unwrap();
770        assert_eq!(summary.text, "foo: bar test"); // the linebreak should be removed from summary
771    }
772
773    /// Tests that summary does not fail to load
774    /// if the draft was deleted after loading the chatlist.
775    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
776    async fn test_get_summary_deleted_draft() {
777        let t = TestContext::new().await;
778
779        let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "a chat")
780            .await
781            .unwrap();
782        let mut msg = Message::new_text("Foobar".to_string());
783        chat_id.set_draft(&t, Some(&mut msg)).await.unwrap();
784
785        let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
786        chat_id.set_draft(&t, None).await.unwrap();
787
788        let summary_res = chats.get_summary(&t, 0, None).await;
789        assert!(summary_res.is_ok());
790    }
791
792    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
793    async fn test_get_summary_for_saved_messages() -> Result<()> {
794        let mut tcm = TestContextManager::new();
795        let alice = tcm.alice().await;
796        let bob = tcm.bob().await;
797        let chat_alice = alice.create_chat(&bob).await;
798
799        send_text_msg(&alice, chat_alice.id, "hi".into()).await?;
800        let sent1 = alice.pop_sent_msg().await;
801        save_msgs(&alice, &[sent1.sender_msg_id]).await?;
802        let chatlist = Chatlist::try_load(&alice, 0, None, None).await?;
803        let summary = chatlist.get_summary(&alice, 0, None).await?;
804        assert_eq!(summary.prefix.unwrap().to_string(), "Me");
805        assert_eq!(summary.text, "hi");
806
807        let msg = bob.recv_msg(&sent1).await;
808        save_msgs(&bob, &[msg.id]).await?;
809        let chatlist = Chatlist::try_load(&bob, 0, None, None).await?;
810        let summary = chatlist.get_summary(&bob, 0, None).await?;
811        assert_eq!(summary.prefix.unwrap().to_string(), "alice@example.org");
812        assert_eq!(summary.text, "hi");
813
814        Ok(())
815    }
816
817    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
818    async fn test_load_broken() {
819        let t = TestContext::new_bob().await;
820        let chat_id1 = create_group_chat(&t, ProtectionStatus::Unprotected, "a chat")
821            .await
822            .unwrap();
823        create_group_chat(&t, ProtectionStatus::Unprotected, "b chat")
824            .await
825            .unwrap();
826        create_group_chat(&t, ProtectionStatus::Unprotected, "c chat")
827            .await
828            .unwrap();
829
830        // check that the chatlist starts with the most recent message
831        let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
832        assert_eq!(chats.len(), 3);
833
834        // obfuscated one chat
835        t.sql
836            .execute("UPDATE chats SET type=10 WHERE id=?", (chat_id1,))
837            .await
838            .unwrap();
839
840        // obfuscated chat can't be loaded
841        assert!(Chat::load_from_db(&t, chat_id1).await.is_err());
842
843        // chatlist loads fine
844        let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
845
846        // only corrupted chat fails to create summary
847        assert!(chats.get_summary(&t, 0, None).await.is_ok());
848        assert!(chats.get_summary(&t, 1, None).await.is_ok());
849        assert!(chats.get_summary(&t, 2, None).await.is_err());
850        assert_eq!(chats.get_index_for_id(chat_id1).unwrap(), 2);
851    }
852}