deltachat/
chatlist.rs

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