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