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 4c97a3103d..3a99c4ca8b 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}; @@ -1164,10 +1164,24 @@ 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() + /// (read receipts aren't sent 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() - /// (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(). diff --git a/src/chat.rs b/src/chat.rs index 0b6444a16d..17c9ed0220 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -3222,6 +3222,36 @@ pub async fn get_chat_msgs_ex( Ok(items) } +/// 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( + "SELECT DISTINCT(c.id) + FROM msgs m + INNER JOIN chats c + ON m.chat_id=c.id + WHERE m.state=? + AND m.hidden=0 + AND m.chat_id>9 + AND c.blocked=0;", + (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?; + } + + 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<()> { diff --git a/src/chat/chat_tests.rs b/src/chat/chat_tests.rs index 1e8cff82fb..0ebf2116a3 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,96 @@ 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 tcm = TestContextManager::new(); + let alice = &tcm.alice().await; + let bob = &tcm.bob().await; + + tcm.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?; + + 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; + 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?; + } + + 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; + } + // 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 + ); + + tcm.section("alice: mark as read"); + alice.evtracker.clear_events(); + marknoticed_all_chats(alice).await?; + 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!( + alice_chat_archived_and_muted + .get_fresh_msg_cnt(alice) + .await?, + 0 + ); + + let emitted_events = alice.evtracker.take_events(); + 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)); + } + + 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.