From 77f01564b00e7428cac8c070593c72c325b02c00 Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Sun, 11 Jan 2026 07:52:44 +0100 Subject: [PATCH 01/13] api(rust and jsonrpc): marknoticed_all_chats method to mark all chats as notices, including muted ones. made for solving https://github.com/deltachat/deltachat-desktop/issues/5891#issuecomment-3687566470 --- deltachat-jsonrpc/src/api.rs | 14 +++++++++++++ src/chat.rs | 39 ++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/deltachat-jsonrpc/src/api.rs b/deltachat-jsonrpc/src/api.rs index 4c97a3103d..a529bd2b80 100644 --- a/deltachat-jsonrpc/src/api.rs +++ b/deltachat-jsonrpc/src/api.rs @@ -1164,6 +1164,20 @@ impl CommandApi { Ok(None) } + /// Mark all messages in all chats as _noticed_. + /// Skips messages from blocked contacts, but does not skip messages in muted chats. + /// + /// _Noticed_ messages are no longer _fresh_ and do not count as being unseen + /// but are still waiting for being marked as "seen" using markseen_msgs() + /// (IMAP/MDNs is not done for noticed messages). + /// + /// Calling this function usually results in the event #DC_EVENT_MSGS_NOTICED. + /// See also markseen_msgs(). + pub async fn marknoticed_all_chats(&self, account_id: u32) -> Result<()> { + let ctx = self.get_context(account_id).await?; + marknoticed_all_chats(&ctx).await + } + /// Mark all messages in a chat as _noticed_. /// _Noticed_ messages are no longer _fresh_ and do not count as being unseen /// but are still waiting for being marked as "seen" using markseen_msgs() diff --git a/src/chat.rs b/src/chat.rs index 0b6444a16d..ba74c8f6c7 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -3222,6 +3222,45 @@ pub async fn get_chat_msgs_ex( Ok(items) } +/// Marks all unread messages in all chat as noticed. +/// Ignores messages from blocked contacts, but does not ignore messages in muted chats. +pub async fn marknoticed_all_chats(context: &Context) -> Result<()> { + let list = context + .sql + .query_map_vec( + concat!( + "SELECT c.id", + " FROM msgs m", + " LEFT JOIN contacts ct", + " ON m.from_id=ct.id", + " LEFT JOIN chats c", + " ON m.chat_id=c.id", + " WHERE m.state=?", + " AND m.hidden=0", + " AND m.chat_id>9", + " AND ct.blocked=0", + " AND c.blocked=0", + " GROUP BY c.id;" + ), + (MessageState::InFresh,), + |row| { + let msg_id: ChatId = row.get(0)?; + Ok(msg_id) + }, + ) + .await?; + + for chat_id in list { + marknoticed_chat(context, chat_id).await?; + } + + context.emit_event(EventType::MsgsNoticed(DC_CHAT_ID_ARCHIVED_LINK)); + chatlist_events::emit_chatlist_item_changed(context, DC_CHAT_ID_ARCHIVED_LINK); + context.on_archived_chats_maybe_noticed(); + + Ok(()) +} + /// Marks all messages in the chat as noticed. /// If the given chat-id is the archive-link, marks all messages in all archived chats as noticed. pub async fn marknoticed_chat(context: &Context, chat_id: ChatId) -> Result<()> { From f56581ce50a4b7a8eca36489c0bd12b0046bbb0b Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Sun, 11 Jan 2026 07:58:25 +0100 Subject: [PATCH 02/13] fix missing import --- deltachat-jsonrpc/src/api.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deltachat-jsonrpc/src/api.rs b/deltachat-jsonrpc/src/api.rs index a529bd2b80..571570e55c 100644 --- a/deltachat-jsonrpc/src/api.rs +++ b/deltachat-jsonrpc/src/api.rs @@ -11,8 +11,8 @@ use deltachat::blob::BlobObject; use deltachat::calls::ice_servers; use deltachat::chat::{ self, add_contact_to_chat, forward_msgs, forward_msgs_2ctx, get_chat_media, get_chat_msgs, - get_chat_msgs_ex, marknoticed_chat, remove_contact_from_chat, Chat, ChatId, ChatItem, - MessageListOptions, + get_chat_msgs_ex, marknoticed_all_chats, marknoticed_chat, remove_contact_from_chat, Chat, + ChatId, ChatItem, MessageListOptions, }; use deltachat::chatlist::Chatlist; use deltachat::config::{get_all_ui_config_keys, Config}; From e7981bf290a0373eee4394045760fadd16cb9e5f Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Mon, 12 Jan 2026 12:51:54 +0000 Subject: [PATCH 03/13] Update src/chat.rs Co-authored-by: WofWca --- src/chat.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/chat.rs b/src/chat.rs index ba74c8f6c7..197fb019e5 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -3222,7 +3222,7 @@ pub async fn get_chat_msgs_ex( Ok(items) } -/// Marks all unread messages in all chat as noticed. +/// Marks all unread messages in all chats as noticed. /// Ignores messages from blocked contacts, but does not ignore messages in muted chats. pub async fn marknoticed_all_chats(context: &Context) -> Result<()> { let list = context From d9e47b0596b8310575e438f18cca36223d857323 Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Mon, 12 Jan 2026 13:50:45 +0100 Subject: [PATCH 04/13] add test --- src/chat/chat_tests.rs | 97 ++++++++++++++++++++++++++++++++++++++++++ src/test_utils.rs | 9 ++++ 2 files changed, 106 insertions(+) diff --git a/src/chat/chat_tests.rs b/src/chat/chat_tests.rs index 1e8cff82fb..c6dcad240e 100644 --- a/src/chat/chat_tests.rs +++ b/src/chat/chat_tests.rs @@ -1,6 +1,7 @@ use std::sync::Arc; use super::*; +use crate::Event; use crate::chatlist::get_archived_cnt; use crate::constants::{DC_GCL_ARCHIVED_ONLY, DC_GCL_NO_SPECIALS}; use crate::ephemeral::Timer; @@ -1240,6 +1241,102 @@ async fn test_unarchive_if_muted() -> Result<()> { Ok(()) } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn test_marknoticed_all_chats() -> Result<()> { + let mut t = TestContextManager::new(); + let alice = &t.alice().await; + let bob = &t.bob().await; + + t.section("alice: create chats & promote them by sending a message"); + + let alice_chat_normal = alice + .create_group_with_members("Chat (normal)", &[alice, bob]) + .await; + send_text_msg(alice, alice_chat_normal, "Hi".to_string()).await?; + + let alice_chat_muted = alice + .create_group_with_members("Chat (muted)", &[alice, bob]) + .await; + send_text_msg(alice, alice_chat_muted, "Hi".to_string()).await?; + set_muted(&alice.ctx, alice_chat_muted, MuteDuration::Forever).await?; + + let alice_chat_archived_and_muted = alice + .create_group_with_members("Chat (archived and muted)", &[alice, bob]) + .await; + send_text_msg(alice, alice_chat_archived_and_muted, "Hi".to_string()).await?; + set_muted( + &alice.ctx, + alice_chat_archived_and_muted, + MuteDuration::Forever, + ) + .await?; + alice_chat_archived_and_muted + .set_visibility(&alice.ctx, ChatVisibility::Archived) + .await?; + + t.section("bob: receive messages, accept all chats and send a reply to each messsage"); + + while let Some(sent_msg) = alice.pop_sent_msg_opt(Duration::default()).await { + let bob_message = bob.recv_msg(&sent_msg).await; + let bob_chat_id = bob_message.chat_id; + bob_chat_id.accept(bob).await?; + send_text_msg(bob, bob_chat_id, "reply".to_string()).await?; + } + + t.section("alice: receive replies from bob"); + while let Some(sent_msg) = bob.pop_sent_msg_opt(Duration::default()).await { + alice.recv_msg(&sent_msg).await; + } + // ensure chats have unread messages + assert_eq!(alice_chat_normal.get_fresh_msg_cnt(alice).await?, 1); + assert_eq!(alice_chat_muted.get_fresh_msg_cnt(alice).await?, 1); + assert_eq!( + alice_chat_archived_and_muted + .get_fresh_msg_cnt(alice) + .await?, + 1 + ); + + t.section("alice: mark as read"); + alice.evtracker.clear_events(); + marknoticed_all_chats(alice).await?; + t.section("alice: check that chats are no longer unread and that chatlist update events were received"); + assert_eq!(alice_chat_normal.get_fresh_msg_cnt(alice).await?, 0); + assert_eq!(alice_chat_muted.get_fresh_msg_cnt(alice).await?, 0); + assert_eq!( + alice_chat_archived_and_muted + .get_fresh_msg_cnt(alice) + .await?, + 0 + ); + + let emitted_events = alice.evtracker.take_events(); + macro_rules! contains_event { + ($event: expr) => { + assert!( + emitted_events + .iter() + .find(|Event { typ, .. }| typ == &$event) + .is_some() + ); + }; + } + contains_event!(EventType::ChatlistItemChanged { + chat_id: Some(alice_chat_normal) + }); + contains_event!(EventType::ChatlistItemChanged { + chat_id: Some(alice_chat_muted) + }); + contains_event!(EventType::ChatlistItemChanged { + chat_id: Some(alice_chat_archived_and_muted) + }); + contains_event!(EventType::ChatlistItemChanged { + chat_id: Some(DC_CHAT_ID_ARCHIVED_LINK) + }); + + Ok(()) +} + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_archive_fresh_msgs() -> Result<()> { let t = TestContext::new_alice().await; diff --git a/src/test_utils.rs b/src/test_utils.rs index 54b1866e1e..8c5c97fabf 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -1492,6 +1492,15 @@ impl EventTracker { pub fn clear_events(&self) { while let Ok(_ev) = self.try_recv() {} } + + /// Takes all items from event queue and returns them. + pub fn take_events(&self) -> Vec { + let mut events = Vec::new(); + while let Ok(event) = self.try_recv() { + events.push(event); + } + events + } } /// Gets a specific message from a chat and asserts that the chat has a specific length. From dcfd1bf79b97403d23948be3b1d95a252d3a6c5a Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Wed, 14 Jan 2026 18:40:12 +0100 Subject: [PATCH 05/13] `SELECT DISTINCT(c.id)` instead of `GROUP BY c.id` --- src/chat.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/chat.rs b/src/chat.rs index 197fb019e5..c192385f18 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -3229,7 +3229,7 @@ pub async fn marknoticed_all_chats(context: &Context) -> Result<()> { .sql .query_map_vec( concat!( - "SELECT c.id", + "SELECT DISTINCT(c.id)", " FROM msgs m", " LEFT JOIN contacts ct", " ON m.from_id=ct.id", @@ -3239,8 +3239,7 @@ pub async fn marknoticed_all_chats(context: &Context) -> Result<()> { " AND m.hidden=0", " AND m.chat_id>9", " AND ct.blocked=0", - " AND c.blocked=0", - " GROUP BY c.id;" + " AND c.blocked=0;", ), (MessageState::InFresh,), |row| { From 8c6634012b170f28af7cee9224f273948d47e1dc Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Wed, 14 Jan 2026 18:42:32 +0100 Subject: [PATCH 06/13] don't use `concat!` macro --- src/chat.rs | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/src/chat.rs b/src/chat.rs index c192385f18..b986162d8e 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -3228,19 +3228,17 @@ pub async fn marknoticed_all_chats(context: &Context) -> Result<()> { let list = context .sql .query_map_vec( - concat!( - "SELECT DISTINCT(c.id)", - " FROM msgs m", - " LEFT JOIN contacts ct", - " ON m.from_id=ct.id", - " LEFT JOIN chats c", - " ON m.chat_id=c.id", - " WHERE m.state=?", - " AND m.hidden=0", - " AND m.chat_id>9", - " AND ct.blocked=0", - " AND c.blocked=0;", - ), + "SELECT DISTINCT(c.id) + FROM msgs m + LEFT JOIN contacts ct + ON m.from_id=ct.id + LEFT JOIN chats c + ON m.chat_id=c.id + WHERE m.state=? + AND m.hidden=0 + AND m.chat_id>9 + AND ct.blocked=0 + AND c.blocked=0;", (MessageState::InFresh,), |row| { let msg_id: ChatId = row.get(0)?; From c6e6598d604afbf07fc2c88f6e7c51752178ce99 Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Wed, 14 Jan 2026 18:42:56 +0100 Subject: [PATCH 07/13] comment that sql statement is similar to the one in `get_fresh_msgs` --- src/chat.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/chat.rs b/src/chat.rs index b986162d8e..fcbcf3eb61 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -3225,6 +3225,7 @@ pub async fn get_chat_msgs_ex( /// Marks all unread messages in all chats as noticed. /// Ignores messages from blocked contacts, but does not ignore messages in muted chats. pub async fn marknoticed_all_chats(context: &Context) -> Result<()> { + // The sql statement here is similar to the one in get_fresh_msgs let list = context .sql .query_map_vec( From 75a4cfae8cb6e65f2838e3211059d3640abfad5c Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Wed, 14 Jan 2026 19:06:31 +0100 Subject: [PATCH 08/13] apply suggested change to api docs. --- deltachat-ffi/deltachat.h | 2 +- deltachat-jsonrpc/src/api.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/deltachat-ffi/deltachat.h b/deltachat-ffi/deltachat.h index 6ce31fd24c..df5723a865 100644 --- a/deltachat-ffi/deltachat.h +++ b/deltachat-ffi/deltachat.h @@ -1564,7 +1564,7 @@ dc_array_t* dc_wait_next_msgs (dc_context_t* context); * Mark all messages in a chat as _noticed_. * _Noticed_ messages are no longer _fresh_ and do not count as being unseen * but are still waiting for being marked as "seen" using dc_markseen_msgs() - * (IMAP/MDNs is not done for noticed messages). + * (read receipts aren't sent for noticed messages). * * Calling this function usually results in the event #DC_EVENT_MSGS_NOTICED. * See also dc_markseen_msgs(). diff --git a/deltachat-jsonrpc/src/api.rs b/deltachat-jsonrpc/src/api.rs index 571570e55c..3a99c4ca8b 100644 --- a/deltachat-jsonrpc/src/api.rs +++ b/deltachat-jsonrpc/src/api.rs @@ -1169,7 +1169,7 @@ impl CommandApi { /// /// _Noticed_ messages are no longer _fresh_ and do not count as being unseen /// but are still waiting for being marked as "seen" using markseen_msgs() - /// (IMAP/MDNs is not done for noticed messages). + /// (read receipts aren't sent for noticed messages). /// /// Calling this function usually results in the event #DC_EVENT_MSGS_NOTICED. /// See also markseen_msgs(). @@ -1181,7 +1181,7 @@ impl CommandApi { /// Mark all messages in a chat as _noticed_. /// _Noticed_ messages are no longer _fresh_ and do not count as being unseen /// but are still waiting for being marked as "seen" using markseen_msgs() - /// (IMAP/MDNs is not done for noticed messages). + /// (read receipts aren't sent for noticed messages). /// /// Calling this function usually results in the event #DC_EVENT_MSGS_NOTICED. /// See also markseen_msgs(). From e960e1c8c512c24058cdc86e9538c401d92483fa Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Wed, 14 Jan 2026 19:26:55 +0100 Subject: [PATCH 09/13] rename TestContextManager var to "tcm" --- src/chat/chat_tests.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/chat/chat_tests.rs b/src/chat/chat_tests.rs index c6dcad240e..e006ac19f5 100644 --- a/src/chat/chat_tests.rs +++ b/src/chat/chat_tests.rs @@ -1243,11 +1243,11 @@ async fn test_unarchive_if_muted() -> Result<()> { #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_marknoticed_all_chats() -> Result<()> { - let mut t = TestContextManager::new(); - let alice = &t.alice().await; - let bob = &t.bob().await; + let mut tcm = TestContextManager::new(); + let alice = &tcm.alice().await; + let bob = &tcm.bob().await; - t.section("alice: create chats & promote them by sending a message"); + tcm.section("alice: create chats & promote them by sending a message"); let alice_chat_normal = alice .create_group_with_members("Chat (normal)", &[alice, bob]) @@ -1274,7 +1274,7 @@ async fn test_marknoticed_all_chats() -> Result<()> { .set_visibility(&alice.ctx, ChatVisibility::Archived) .await?; - t.section("bob: receive messages, accept all chats and send a reply to each messsage"); + tcm.section("bob: receive messages, accept all chats and send a reply to each messsage"); while let Some(sent_msg) = alice.pop_sent_msg_opt(Duration::default()).await { let bob_message = bob.recv_msg(&sent_msg).await; @@ -1283,7 +1283,7 @@ async fn test_marknoticed_all_chats() -> Result<()> { send_text_msg(bob, bob_chat_id, "reply".to_string()).await?; } - t.section("alice: receive replies from bob"); + tcm.section("alice: receive replies from bob"); while let Some(sent_msg) = bob.pop_sent_msg_opt(Duration::default()).await { alice.recv_msg(&sent_msg).await; } @@ -1297,10 +1297,10 @@ async fn test_marknoticed_all_chats() -> Result<()> { 1 ); - t.section("alice: mark as read"); + tcm.section("alice: mark as read"); alice.evtracker.clear_events(); marknoticed_all_chats(alice).await?; - t.section("alice: check that chats are no longer unread and that chatlist update events were received"); + tcm.section("alice: check that chats are no longer unread and that chatlist update events were received"); assert_eq!(alice_chat_normal.get_fresh_msg_cnt(alice).await?, 0); assert_eq!(alice_chat_muted.get_fresh_msg_cnt(alice).await?, 0); assert_eq!( From 3f84e244d6f167d5be1c88c9757260802892aa6e Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Wed, 14 Jan 2026 19:30:49 +0100 Subject: [PATCH 10/13] replace macro with for loop --- src/chat/chat_tests.rs | 36 +++++++++++++++--------------------- 1 file changed, 15 insertions(+), 21 deletions(-) diff --git a/src/chat/chat_tests.rs b/src/chat/chat_tests.rs index e006ac19f5..0ebf2116a3 100644 --- a/src/chat/chat_tests.rs +++ b/src/chat/chat_tests.rs @@ -1311,28 +1311,22 @@ async fn test_marknoticed_all_chats() -> Result<()> { ); let emitted_events = alice.evtracker.take_events(); - macro_rules! contains_event { - ($event: expr) => { - assert!( - emitted_events - .iter() - .find(|Event { typ, .. }| typ == &$event) - .is_some() - ); - }; + for event in &[ + EventType::ChatlistItemChanged { + chat_id: Some(alice_chat_normal), + }, + EventType::ChatlistItemChanged { + chat_id: Some(alice_chat_muted), + }, + EventType::ChatlistItemChanged { + chat_id: Some(alice_chat_archived_and_muted), + }, + EventType::ChatlistItemChanged { + chat_id: Some(DC_CHAT_ID_ARCHIVED_LINK), + }, + ] { + assert!(emitted_events.iter().any(|Event { typ, .. }| typ == event)); } - contains_event!(EventType::ChatlistItemChanged { - chat_id: Some(alice_chat_normal) - }); - contains_event!(EventType::ChatlistItemChanged { - chat_id: Some(alice_chat_muted) - }); - contains_event!(EventType::ChatlistItemChanged { - chat_id: Some(alice_chat_archived_and_muted) - }); - contains_event!(EventType::ChatlistItemChanged { - chat_id: Some(DC_CHAT_ID_ARCHIVED_LINK) - }); Ok(()) } From 03cd3bbbf7774ea49925ff7c105b1a2f71998856 Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Wed, 14 Jan 2026 19:34:53 +0100 Subject: [PATCH 11/13] ignore contact blockstate in `marknoticed_all_chats`, this makes the method also mark chats with blocked contacts as noticed. --- src/chat.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/chat.rs b/src/chat.rs index fcbcf3eb61..e07b12f953 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -3231,14 +3231,11 @@ pub async fn marknoticed_all_chats(context: &Context) -> Result<()> { .query_map_vec( "SELECT DISTINCT(c.id) FROM msgs m - LEFT JOIN contacts ct - ON m.from_id=ct.id LEFT JOIN chats c ON m.chat_id=c.id WHERE m.state=? AND m.hidden=0 AND m.chat_id>9 - AND ct.blocked=0 AND c.blocked=0;", (MessageState::InFresh,), |row| { From 12ee788e5c5a790540c6c378d649241ad5c418c8 Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Wed, 14 Jan 2026 19:36:30 +0100 Subject: [PATCH 12/13] use `INNER JOIN` instead of `LEFT JOIN` --- src/chat.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/chat.rs b/src/chat.rs index e07b12f953..41b6a23282 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -3231,7 +3231,7 @@ pub async fn marknoticed_all_chats(context: &Context) -> Result<()> { .query_map_vec( "SELECT DISTINCT(c.id) FROM msgs m - LEFT JOIN chats c + INNER JOIN chats c ON m.chat_id=c.id WHERE m.state=? AND m.hidden=0 From c94b7a113dd2a5448dad2a537d9e48c43cf106c6 Mon Sep 17 00:00:00 2001 From: Simon Laux Date: Wed, 14 Jan 2026 19:40:46 +0100 Subject: [PATCH 13/13] events are already emitted by `marknoticed_chat`, so removed them here --- src/chat.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/chat.rs b/src/chat.rs index 41b6a23282..17c9ed0220 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -3249,10 +3249,6 @@ pub async fn marknoticed_all_chats(context: &Context) -> Result<()> { marknoticed_chat(context, chat_id).await?; } - context.emit_event(EventType::MsgsNoticed(DC_CHAT_ID_ARCHIVED_LINK)); - chatlist_events::emit_chatlist_item_changed(context, DC_CHAT_ID_ARCHIVED_LINK); - context.on_archived_chats_maybe_noticed(); - Ok(()) }