From d1f8da34c5ada0a5c26f43f4e4b60e8a894a50e1 Mon Sep 17 00:00:00 2001 From: WofWca Date: Mon, 6 Jul 2026 22:22:27 +0400 Subject: [PATCH] feat: a way to add a device message as `InNoticed` This is useful for adding the same device message on multiple accounts, which is what we do at least on Desktop currently: https://github.com/deltachat/deltachat-desktop/blob/c50896f1d529b09ea584f8fccca3f5fca7fe6ed7/packages/frontend/src/deviceMessages.ts#L35-L44. The problem on desktop is that we then have to `rpc.marknoticedChat()` so that the user isn't forced to read the same message on all their accounts: https://github.com/deltachat/deltachat-desktop/issues/3934. However, that, in turn, is now causing problems in our notification clearing logic: marking a chat as noticed is supposed to remove the per-account "N messages in M chats" notification, but we only want to do that when the user actually reads a message on the account and, whereas we do this `marknoticedChat()` for device messages on app startup for all accounts. See the TODO in https://github.com/deltachat/deltachat-desktop/pull/6154. --- deltachat-jsonrpc/src/api.rs | 21 +++++++++---- deltachat-jsonrpc/src/api/types/message.rs | 36 ++++++++++++++++++++++ src/chat.rs | 16 +++++++++- src/chat/chat_tests.rs | 3 ++ 4 files changed, 69 insertions(+), 7 deletions(-) diff --git a/deltachat-jsonrpc/src/api.rs b/deltachat-jsonrpc/src/api.rs index bf1b29ced..9dc18f751 100644 --- a/deltachat-jsonrpc/src/api.rs +++ b/deltachat-jsonrpc/src/api.rs @@ -53,7 +53,7 @@ use types::chat::FullChat; use types::contact::{ContactObject, VcardContact}; use types::events::Event; use types::http::HttpResponse; -use types::message::{MessageData, MessageObject, MessageReadReceipt}; +use types::message::{DeviceMessageData, MessageData, MessageObject, MessageReadReceipt}; use types::notify_state::JsonrpcNotifyState; use types::provider_info::ProviderInfo; use types::reactions::JsonrpcReactions; @@ -1229,18 +1229,27 @@ impl CommandApi { &self, account_id: u32, label: String, - msg: Option, + msg: Option, ) -> Result> { let ctx = self.get_context(account_id).await?; if let Some(msg) = msg { - let mut message = msg.create_message(&ctx).await?; - let message_id = - deltachat::chat::add_device_msg(&ctx, Some(&label), Some(&mut message)).await?; + let mut message = msg.message_data.create_message(&ctx).await?; + if let Some(state) = msg.message_state { + deltachat::chat::set_device_msg_state(&mut message, state.into()); + } + let message_id = deltachat::chat::add_device_msg_with_importance( + &ctx, + Some(&label), + Some(&mut message), + false, + ) + .await?; if !message_id.is_unset() { return Ok(Some(message_id.to_u32())); } } else { - deltachat::chat::add_device_msg(&ctx, Some(&label), None).await?; + deltachat::chat::add_device_msg_with_importance(&ctx, Some(&label), None, false) + .await?; } Ok(None) } diff --git a/deltachat-jsonrpc/src/api/types/message.rs b/deltachat-jsonrpc/src/api/types/message.rs index 2b37fb507..06fd6f1ae 100644 --- a/deltachat-jsonrpc/src/api/types/message.rs +++ b/deltachat-jsonrpc/src/api/types/message.rs @@ -661,6 +661,42 @@ impl MessageData { } } +#[derive(Deserialize, Serialize, TypeDef, schemars::JsonSchema)] +#[repr(u32)] +pub enum DeviceMessageState { + // Variants that are not really wanted for device messages are omitted, + // to avoid misuse. + + // Undefined, + InFresh, + InNoticed, + // InSeen, + // OutDraft, + // OutPending, + // OutFailed, + // OutDelivered, + // OutMdnRcvd, +} + +impl From for deltachat::message::MessageState { + fn from(message_state: DeviceMessageState) -> Self { + use deltachat::message::MessageState; + match message_state { + DeviceMessageState::InFresh => MessageState::InFresh, + DeviceMessageState::InNoticed => MessageState::InNoticed, + } + } +} + +#[derive(Deserialize, Serialize, TypeDef, schemars::JsonSchema)] +#[serde(rename_all = "camelCase")] +pub struct DeviceMessageData { + #[serde(flatten)] + pub message_data: MessageData, + #[serde(default)] + pub message_state: Option, +} + #[derive(Serialize, TypeDef, schemars::JsonSchema)] #[serde(rename_all = "camelCase")] pub struct MessageReadReceipt { diff --git a/src/chat.rs b/src/chat.rs index 7c2a855a6..7b655d963 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -4810,10 +4810,20 @@ pub(crate) async fn get_chat_id_by_grpid( .await } +/// Intended to be used only for messages passed to +/// [`add_device_msg_with_importance`]. +pub fn set_device_msg_state(msg: &mut Message, state: MessageState) { + msg.state = state; +} + /// Adds a message to device chat. /// /// Optional `label` can be provided to ensure that message is added only once. /// If `important` is true, a notification will be sent. +/// +/// If message state is [`MessageState::Undefined`], +/// the message will be added to the DB as [`MessageState::InFresh`]. +/// To set message state, you may utilize [`set_device_msg_state`]. #[expect(clippy::arithmetic_side_effects)] pub async fn add_device_msg_with_importance( context: &Context, @@ -4850,7 +4860,11 @@ pub async fn add_device_msg_with_importance( msg.timestamp_sort = last_msg_time + 1; } prepare_msg_blob(context, msg).await?; - let state = MessageState::InFresh; + let state = if msg.state == MessageState::Undefined { + MessageState::InFresh + } else { + msg.state + }; let row_id = context .sql .insert( diff --git a/src/chat/chat_tests.rs b/src/chat/chat_tests.rs index a27b70670..516fb4026 100644 --- a/src/chat/chat_tests.rs +++ b/src/chat/chat_tests.rs @@ -854,6 +854,7 @@ async fn test_add_device_msg_unlabelled() { // add two device-messages let mut msg1 = Message::new_text("first message".to_string()); + msg1.state = MessageState::InNoticed; let msg1_id = add_device_msg(&t, None, Some(&mut msg1)).await; assert!(msg1_id.is_ok()); @@ -869,12 +870,14 @@ async fn test_add_device_msg_unlabelled() { assert_eq!(msg1.text, "first message"); assert_eq!(msg1.from_id, ContactId::DEVICE); assert_eq!(msg1.to_id, ContactId::SELF); + assert_eq!(msg1.get_state(), MessageState::InNoticed); assert!(!msg1.is_info()); let msg2 = message::Message::load_from_db(&t, msg2_id.unwrap()).await; assert!(msg2.is_ok()); let msg2 = msg2.unwrap(); assert_eq!(msg2.text, "second message"); + assert_eq!(msg2.get_state(), MessageState::InFresh); // check device chat assert_eq!(msg2.chat_id.get_msg_cnt(&t).await.unwrap(), 2);