From 29de7c3603f023804cf1320ecc14e52ee0e08dc2 Mon Sep 17 00:00:00 2001 From: bjoern Date: Fri, 22 Nov 2024 21:31:56 +0100 Subject: [PATCH] feat: webxdc notify (#6230) this PR adds support for the property `update.notify` to notify about changes in `update.info` or `update.summary`. the property can be set to an array of addresses [^1] core emits then the event `IncomingWebxdcNotify`, resulting in all UIs to display a system notification, maybe even via PUSH. for using the existing `update.info` and `update.summary`: the message is no secret and should be visible to all group members as usual, to not break the UX of having same group messages on all devices of all users - as known already from the normal messages. also, that way, there is no question what happens if user have disabled notifications as the change is presented in the chat as well doc counterpart at https://github.com/webxdc/website/pull/90 closes #6217 [^1]: addresses come in either via the payload as currently or as an explicit sender in the future - this does not affect this PR. same for translations, see discussions at #6217 and #6097 --------- Co-authored-by: adb Co-authored-by: l --- deltachat-ffi/deltachat.h | 25 ++- deltachat-ffi/src/lib.rs | 8 +- deltachat-jsonrpc/src/api/types/events.rs | 17 ++ node/constants.js | 1 + node/events.js | 1 + node/lib/constants.ts | 2 + src/debug_logging.rs | 1 + src/events/payload.rs | 12 ++ src/webxdc.rs | 230 +++++++++++++++++++--- src/webxdc/maps_integration.rs | 1 + 10 files changed, 264 insertions(+), 34 deletions(-) diff --git a/deltachat-ffi/deltachat.h b/deltachat-ffi/deltachat.h index 55235216b..ae07c3d48 100644 --- a/deltachat-ffi/deltachat.h +++ b/deltachat-ffi/deltachat.h @@ -1154,7 +1154,13 @@ uint32_t dc_send_videochat_invitation (dc_context_t* context, uint32_t chat_id); * @memberof dc_context_t * @param context The context object. * @param msg_id The ID of the message with the webxdc instance. - * @param json program-readable data, the actual payload + * @param json program-readable data, this is created in JS land as: + * - `payload`: any JS object or primitive. + * - `info`: optional informational message. Will be shown in chat and may be added as system notification. + * note that also users that are not notified explicitly get the `info` or `summary` update shown in the chat. + * - `document`: optional document name. shown eg. in title bar. + * - `summary`: optional summary. shown beside app icon. + * - `notify`: optional array of other users `selfAddr` to be notified e.g. by a sound about `info` or `summary`. * @param descr The user-visible description of JSON data, * in case of a chess game, e.g. the move. * @return 1=success, 0=error @@ -6080,12 +6086,29 @@ void dc_event_unref(dc_event_t* event); #define DC_EVENT_INCOMING_REACTION 2002 + +/** + * A webxdc wants an info message or a changed summary to be notified. + * + * @param data1 contact_id ID of the contact sending. + * @param data2 (int) msg_id + (char*) text_to_notify. + * msg_id in dc_event_get_data2_int(), referring to webxdc-info-message + * or webxdc-instance in case of summary change. + * text_to_notify in dc_event_get_data2_str(). + * string must be passed to dc_str_unref() afterwards. + */ +#define DC_EVENT_INCOMING_WEBXDC_NOTIFY 2003 + + /** * There is a fresh message. Typically, the user will show an notification * when receiving this message. * * There is no extra #DC_EVENT_MSGS_CHANGED event send together with this event. * + * If the message is a webxdc info message, + * dc_msg_get_parent() returns the webxdc instance the notification belongs to. + * * @param data1 (int) chat_id * @param data2 (int) msg_id */ diff --git a/deltachat-ffi/src/lib.rs b/deltachat-ffi/src/lib.rs index 47951a2e1..f93708411 100644 --- a/deltachat-ffi/src/lib.rs +++ b/deltachat-ffi/src/lib.rs @@ -542,6 +542,7 @@ pub unsafe extern "C" fn dc_event_get_id(event: *mut dc_event_t) -> libc::c_int EventType::MsgsChanged { .. } => 2000, EventType::ReactionsChanged { .. } => 2001, EventType::IncomingReaction { .. } => 2002, + EventType::IncomingWebxdcNotify { .. } => 2003, EventType::IncomingMsg { .. } => 2005, EventType::IncomingMsgBunch { .. } => 2006, EventType::MsgsNoticed { .. } => 2008, @@ -602,7 +603,8 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc: | EventType::ErrorSelfNotInGroup(_) | EventType::AccountsBackgroundFetchDone => 0, EventType::ChatlistChanged => 0, - EventType::IncomingReaction { contact_id, .. } => contact_id.to_u32() as libc::c_int, + EventType::IncomingReaction { contact_id, .. } + | EventType::IncomingWebxdcNotify { contact_id, .. } => contact_id.to_u32() as libc::c_int, EventType::MsgsChanged { chat_id, .. } | EventType::ReactionsChanged { chat_id, .. } | EventType::IncomingMsg { chat_id, .. } @@ -681,6 +683,7 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc: EventType::MsgsChanged { msg_id, .. } | EventType::ReactionsChanged { msg_id, .. } | EventType::IncomingReaction { msg_id, .. } + | EventType::IncomingWebxdcNotify { msg_id, .. } | EventType::IncomingMsg { msg_id, .. } | EventType::MsgDelivered { msg_id, .. } | EventType::MsgFailed { msg_id, .. } @@ -775,6 +778,9 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut .to_c_string() .unwrap_or_default() .into_raw(), + EventType::IncomingWebxdcNotify { text, .. } => { + text.to_c_string().unwrap_or_default().into_raw() + } #[allow(unreachable_patterns)] #[cfg(test)] _ => unreachable!("This is just to silence a rust_analyzer false-positive"), diff --git a/deltachat-jsonrpc/src/api/types/events.rs b/deltachat-jsonrpc/src/api/types/events.rs index 34ccdf395..f7abb7a7e 100644 --- a/deltachat-jsonrpc/src/api/types/events.rs +++ b/deltachat-jsonrpc/src/api/types/events.rs @@ -106,6 +106,14 @@ pub enum EventType { reaction: String, }, + /// Incoming webxdc info or summary update, should be notified. + #[serde(rename_all = "camelCase")] + IncomingWebxdcNotify { + contact_id: u32, + msg_id: u32, + text: String, + }, + /// There is a fresh message. Typically, the user will show an notification /// when receiving this message. /// @@ -319,6 +327,15 @@ impl From for EventType { msg_id: msg_id.to_u32(), reaction: reaction.as_str().to_string(), }, + CoreEventType::IncomingWebxdcNotify { + contact_id, + msg_id, + text, + } => IncomingWebxdcNotify { + contact_id: contact_id.to_u32(), + msg_id: msg_id.to_u32(), + text, + }, CoreEventType::IncomingMsg { chat_id, msg_id } => IncomingMsg { chat_id: chat_id.to_u32(), msg_id: msg_id.to_u32(), diff --git a/node/constants.js b/node/constants.js index d087c4543..e277e957d 100644 --- a/node/constants.js +++ b/node/constants.js @@ -52,6 +52,7 @@ module.exports = { DC_EVENT_INCOMING_MSG: 2005, DC_EVENT_INCOMING_MSG_BUNCH: 2006, DC_EVENT_INCOMING_REACTION: 2002, + DC_EVENT_INCOMING_WEBXDC_NOTIFY: 2003, DC_EVENT_INFO: 100, DC_EVENT_LOCATION_CHANGED: 2035, DC_EVENT_MSGS_CHANGED: 2000, diff --git a/node/events.js b/node/events.js index 06c929585..b75bcfd95 100644 --- a/node/events.js +++ b/node/events.js @@ -17,6 +17,7 @@ module.exports = { 2000: 'DC_EVENT_MSGS_CHANGED', 2001: 'DC_EVENT_REACTIONS_CHANGED', 2002: 'DC_EVENT_INCOMING_REACTION', + 2003: 'DC_EVENT_INCOMING_WEBXDC_NOTIFY', 2005: 'DC_EVENT_INCOMING_MSG', 2006: 'DC_EVENT_INCOMING_MSG_BUNCH', 2008: 'DC_EVENT_MSGS_NOTICED', diff --git a/node/lib/constants.ts b/node/lib/constants.ts index 6a2aaaaec..3971bdfaf 100644 --- a/node/lib/constants.ts +++ b/node/lib/constants.ts @@ -52,6 +52,7 @@ export enum C { DC_EVENT_INCOMING_MSG = 2005, DC_EVENT_INCOMING_MSG_BUNCH = 2006, DC_EVENT_INCOMING_REACTION = 2002, + DC_EVENT_INCOMING_WEBXDC_NOTIFY = 2003, DC_EVENT_INFO = 100, DC_EVENT_LOCATION_CHANGED = 2035, DC_EVENT_MSGS_CHANGED = 2000, @@ -325,6 +326,7 @@ export const EventId2EventName: { [key: number]: string } = { 2000: 'DC_EVENT_MSGS_CHANGED', 2001: 'DC_EVENT_REACTIONS_CHANGED', 2002: 'DC_EVENT_INCOMING_REACTION', + 2003: 'DC_EVENT_INCOMING_WEBXDC_NOTIFY', 2005: 'DC_EVENT_INCOMING_MSG', 2006: 'DC_EVENT_INCOMING_MSG_BUNCH', 2008: 'DC_EVENT_MSGS_NOTICED', diff --git a/src/debug_logging.rs b/src/debug_logging.rs index 766a39675..36f5623b0 100644 --- a/src/debug_logging.rs +++ b/src/debug_logging.rs @@ -63,6 +63,7 @@ pub async fn debug_logging_loop(context: &Context, events: Receiver, + + /// Array of other users `selfAddr` that should be notified about this update. + #[serde(skip_serializing_if = "Option::is_none")] + pub notify: Option>, } /// Update items as passed to the UIs. @@ -314,35 +318,14 @@ impl Context { return Ok(None); }; - if can_info_msg { - if let Some(ref info) = status_update_item.info { - if let Some(info_msg_id) = - self.get_overwritable_info_msg_id(instance, from_id).await? - { - chat::update_msg_text_and_timestamp( - self, - instance.chat_id, - info_msg_id, - info.as_str(), - timestamp, - ) - .await?; - } else { - chat::add_info_msg_with_cmd( - self, - instance.chat_id, - info.as_str(), - SystemMessage::WebxdcInfoMessage, - timestamp, - None, - Some(instance), - Some(from_id), - ) - .await?; - } - } - } - + let notify = if let Some(notify_list) = status_update_item.notify { + let self_addr = instance.get_webxdc_self_addr(self).await?; + notify_list.contains(&self_addr) + } else { + false + }; + let mut notify_msg_id = instance.id; + let mut notify_text = "".to_string(); let mut param_changed = false; let mut instance = instance.clone(); @@ -361,10 +344,42 @@ impl Context { .param .update_timestamp(Param::WebxdcSummaryTimestamp, timestamp)? { - instance - .param - .set(Param::WebxdcSummary, sanitize_bidi_characters(summary)); + let summary = sanitize_bidi_characters(summary); + instance.param.set(Param::WebxdcSummary, summary.clone()); param_changed = true; + notify_text = summary; + } + } + + if can_info_msg { + if let Some(ref info) = status_update_item.info { + if let Some(info_msg_id) = self + .get_overwritable_info_msg_id(&instance, from_id) + .await? + { + chat::update_msg_text_and_timestamp( + self, + instance.chat_id, + info_msg_id, + info.as_str(), + timestamp, + ) + .await?; + notify_msg_id = info_msg_id; + } else { + notify_msg_id = chat::add_info_msg_with_cmd( + self, + instance.chat_id, + info.as_str(), + SystemMessage::WebxdcInfoMessage, + timestamp, + None, + Some(&instance), + Some(from_id), + ) + .await?; + } + notify_text = info.to_string(); } } @@ -380,6 +395,14 @@ impl Context { }); } + if notify && !notify_text.is_empty() { + self.emit_event(EventType::IncomingWebxdcNotify { + contact_id: from_id, + msg_id: notify_msg_id, + text: notify_text, + }); + } + Ok(Some(status_update_serial)) } @@ -1437,6 +1460,7 @@ mod tests { document: None, summary: None, uid: Some("iecie2Ze".to_string()), + notify: None, }, 1640178619, true, @@ -1461,6 +1485,7 @@ mod tests { document: None, summary: None, uid: Some("iecie2Ze".to_string()), + notify: None, }, 1640178619, true, @@ -1494,6 +1519,7 @@ mod tests { document: None, summary: None, uid: None, + notify: None, }, 1640178619, true, @@ -1513,6 +1539,7 @@ mod tests { document: None, summary: None, uid: None, + notify: None, }, 1640178619, true, @@ -2903,4 +2930,143 @@ sth_for_the = "future""# Ok(()) } + + async fn has_incoming_webxdc_event( + t: &TestContext, + expected_msg: Message, + expected_text: &str, + ) -> bool { + t.evtracker + .get_matching_opt(t, |evt| { + if let EventType::IncomingWebxdcNotify { msg_id, text, .. } = evt { + *msg_id == expected_msg.id && text == expected_text + } else { + false + } + }) + .await + .is_some() + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_webxdc_notify_one() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = tcm.alice().await; + let bob = tcm.bob().await; + let fiona = tcm.fiona().await; + + let grp_id = alice + .create_group_with_members(ProtectionStatus::Unprotected, "grp", &[&bob, &fiona]) + .await; + let alice_instance = send_webxdc_instance(&alice, grp_id).await?; + let sent1 = alice.pop_sent_msg().await; + let bob_instance = bob.recv_msg(&sent1).await; + let _fiona_instance = fiona.recv_msg(&sent1).await; + + alice + .send_webxdc_status_update( + alice_instance.id, + &format!( + "{{\"payload\":7,\"info\": \"Alice moved\",\"notify\":[\"{}\"]}}", + bob_instance.get_webxdc_self_addr(&bob).await? + ), + "d", + ) + .await?; + alice.flush_status_updates().await?; + let sent2 = alice.pop_sent_msg().await; + let info_msg = alice.get_last_msg().await; + assert!(info_msg.is_info()); + assert!(!has_incoming_webxdc_event(&alice, info_msg, "Alice moved").await); + + bob.recv_msg_trash(&sent2).await; + let info_msg = bob.get_last_msg().await; + assert!(info_msg.is_info()); + assert!(has_incoming_webxdc_event(&bob, info_msg, "Alice moved").await); + + fiona.recv_msg_trash(&sent2).await; + let info_msg = fiona.get_last_msg().await; + assert!(info_msg.is_info()); + assert!(!has_incoming_webxdc_event(&fiona, info_msg, "Alice moved").await); + + Ok(()) + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_webxdc_notify_multiple() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = tcm.alice().await; + let bob = tcm.bob().await; + let fiona = tcm.fiona().await; + + let grp_id = alice + .create_group_with_members(ProtectionStatus::Unprotected, "grp", &[&bob, &fiona]) + .await; + let alice_instance = send_webxdc_instance(&alice, grp_id).await?; + let sent1 = alice.pop_sent_msg().await; + let bob_instance = bob.recv_msg(&sent1).await; + let fiona_instance = fiona.recv_msg(&sent1).await; + + alice + .send_webxdc_status_update( + alice_instance.id, + &format!( + "{{\"payload\":7,\"info\": \"moved\", \"summary\": \"ignored for notify as info is set\", \"notify\":[\"{}\",\"{}\"]}}", + bob_instance.get_webxdc_self_addr(&bob).await?, + fiona_instance.get_webxdc_self_addr(&fiona).await? + ), + "d", + ) + .await?; + alice.flush_status_updates().await?; + let sent2 = alice.pop_sent_msg().await; + let info_msg = alice.get_last_msg().await; + assert!(info_msg.is_info()); + assert!(!has_incoming_webxdc_event(&alice, info_msg, "moved").await); + + bob.recv_msg_trash(&sent2).await; + let info_msg = bob.get_last_msg().await; + assert!(info_msg.is_info()); + assert!(has_incoming_webxdc_event(&bob, info_msg, "moved").await); + + fiona.recv_msg_trash(&sent2).await; + let info_msg = fiona.get_last_msg().await; + assert!(info_msg.is_info()); + assert!(has_incoming_webxdc_event(&fiona, info_msg, "moved").await); + + Ok(()) + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_webxdc_notify_summary() -> Result<()> { + let mut tcm = TestContextManager::new(); + let alice = tcm.alice().await; + let bob = tcm.bob().await; + + let grp_id = alice + .create_group_with_members(ProtectionStatus::Unprotected, "grp", &[&bob]) + .await; + let alice_instance = send_webxdc_instance(&alice, grp_id).await?; + let sent1 = alice.pop_sent_msg().await; + let bob_instance = bob.recv_msg(&sent1).await; + + alice + .send_webxdc_status_update( + alice_instance.id, + &format!( + "{{\"payload\":7,\"summary\": \"4 moves done\",\"notify\":[\"{}\"]}}", + bob_instance.get_webxdc_self_addr(&bob).await? + ), + "d", + ) + .await?; + alice.flush_status_updates().await?; + let sent2 = alice.pop_sent_msg().await; + assert!(!has_incoming_webxdc_event(&alice, alice_instance, "4 moves done").await); + + bob.recv_msg_trash(&sent2).await; + assert!(has_incoming_webxdc_event(&bob, bob_instance, "4 moves done").await); + + Ok(()) + } } diff --git a/src/webxdc/maps_integration.rs b/src/webxdc/maps_integration.rs index 4209d8710..a69a2cf4e 100644 --- a/src/webxdc/maps_integration.rs +++ b/src/webxdc/maps_integration.rs @@ -149,6 +149,7 @@ pub(crate) async fn intercept_get_updates( document: None, summary: None, uid: None, + notify: None, }, serial: StatusUpdateSerial(location.location_id), max_serial: StatusUpdateSerial(location.location_id),