From 54587a46757c785cf7b6e7c0eb0bc4f96226a91f Mon Sep 17 00:00:00 2001 From: "B. Petersen" Date: Fri, 22 Nov 2024 19:19:16 +0100 Subject: [PATCH 1/4] feat: `update.href` api add `update.href` property option to update objects send via `Context::send_webxdc_status_update()`. when set together with `update.info`, UI can implement the info message as a link that is passed to the webxdc via `window.location.href`. for that purpose, UI will read the link back from `Message::get_webxdc_href()`. Practically, this allows e.g. an calendar.xdc to emits clickable update messages opening the calendar at the correct date. --- deltachat-ffi/deltachat.h | 18 +++++++ deltachat-ffi/src/lib.rs | 14 ++++++ src/debug_logging.rs | 1 + src/webxdc.rs | 86 +++++++++++++++++++++++++++++----- src/webxdc/maps_integration.rs | 1 + 5 files changed, 109 insertions(+), 11 deletions(-) diff --git a/deltachat-ffi/deltachat.h b/deltachat-ffi/deltachat.h index ae07c3d481..ee7f12c929 100644 --- a/deltachat-ffi/deltachat.h +++ b/deltachat-ffi/deltachat.h @@ -4518,6 +4518,24 @@ int dc_msg_get_info_type (const dc_msg_t* msg); #define DC_INFO_INVALID_UNENCRYPTED_MAIL 13 #define DC_INFO_WEBXDC_INFO_MESSAGE 32 + +/** + * Get link attached to an webxdc info message. + * The info message needs to be of type DC_INFO_WEBXDC_INFO_MESSAGE. + * + * Typically, this is used to set `document.location.href` in JS land. + * + * Webxdc apps can define the link by setting `update.href` when sending and update, + * see dc_send_webxdc_status_update(). + * + * @memberof dc_msg_t + * @param msg The info message object. + * Not: the webxdc instance. + * @return The link to be set to `document.location.href` in JS land. + * Returns NULL if there is no link attached to the info message and on errors. + */ +char* dc_msg_get_webxdc_href (const dc_msg_t* msg); + /** * Check if a message is still in creation. A message is in creation between * the calls to dc_prepare_msg() and dc_send_msg(). diff --git a/deltachat-ffi/src/lib.rs b/deltachat-ffi/src/lib.rs index f937084112..536c2ecd45 100644 --- a/deltachat-ffi/src/lib.rs +++ b/deltachat-ffi/src/lib.rs @@ -3687,6 +3687,20 @@ pub unsafe extern "C" fn dc_msg_get_info_type(msg: *mut dc_msg_t) -> libc::c_int ffi_msg.message.get_info_type() as libc::c_int } +#[no_mangle] +pub unsafe extern "C" fn dc_msg_get_webxdc_href(msg: *mut dc_msg_t) -> *mut libc::c_char { + if msg.is_null() { + eprintln!("ignoring careless call to dc_msg_get_webxdc_href()"); + return "".strdup(); + } + + let ffi_msg = &*msg; + match ffi_msg.message.get_webxdc_href() { + Some(str) => str.strdup(), + None => ptr::null_mut(), + } +} + #[no_mangle] pub unsafe extern "C" fn dc_msg_is_increation(msg: *mut dc_msg_t) -> libc::c_int { if msg.is_null() { diff --git a/src/debug_logging.rs b/src/debug_logging.rs index 36f5623b0e..eae36fe3d0 100644 --- a/src/debug_logging.rs +++ b/src/debug_logging.rs @@ -60,6 +60,7 @@ pub async fn debug_logging_loop(context: &Context, events: Receiver, + /// Optional link the info message will point to. + /// Used to set `window.location.href` in JS land. + #[serde(skip_serializing_if = "Option::is_none")] + pub href: Option, + /// The new name of the editing document. /// This is not needed if the webxdc doesn't edit documents. #[serde(skip_serializing_if = "Option::is_none")] @@ -353,19 +358,22 @@ impl Context { if can_info_msg { if let Some(ref info) = status_update_item.info { - if let Some(info_msg_id) = self + let 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; + + if info_msg_id.is_some() && !status_update_item.href.is_some() { + if let Some(info_msg_id) = info_msg_id { + 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, @@ -380,6 +388,12 @@ impl Context { .await?; } notify_text = info.to_string(); + + if let Some(href) = status_update_item.href { + let mut notify_msg = Message::load_from_db(self, notify_msg_id).await?; + notify_msg.param.set(Param::Arg, href); + notify_msg.update_param(self).await?; + } } } @@ -944,6 +958,18 @@ impl Message { let hash = Sha256::digest(data.as_bytes()); Ok(format!("{:x}", hash)) } + + /// Get link attached to an info message. + /// + /// The info message needs to be of type SystemMessage::WebxdcInfoMessage. + /// Typically, this is used to start the corresponding + // with `window.location.href` set in JS land. + pub fn get_webxdc_href(&self) -> Option { + let Some(href) = self.param.get(Param::Arg) else { + return None; + }; + Some(href.to_string()) + } } #[cfg(test)] @@ -1457,6 +1483,7 @@ mod tests { StatusUpdateItem { payload: json!({"foo": "bar"}), info: None, + href: None, document: None, summary: None, uid: Some("iecie2Ze".to_string()), @@ -1482,6 +1509,7 @@ mod tests { StatusUpdateItem { payload: json!({"nothing": "this should be ignored"}), info: None, + href: None, document: None, summary: None, uid: Some("iecie2Ze".to_string()), @@ -1516,6 +1544,7 @@ mod tests { StatusUpdateItem { payload: json!({"foo2": "bar2"}), info: None, + href: None, document: None, summary: None, uid: None, @@ -1536,6 +1565,7 @@ mod tests { StatusUpdateItem { payload: Value::Bool(true), info: None, + href: None, document: None, summary: None, uid: None, @@ -3069,4 +3099,38 @@ sth_for_the = "future""# Ok(()) } + + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] + async fn test_webxdc_href() -> 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 instance = send_webxdc_instance(&alice, grp_id).await?; + let sent1 = alice.pop_sent_msg().await; + + alice + .send_webxdc_status_update( + instance.id, + r##"{"payload": "my deeplink data", "info": "my move!", "href": "#foobar"}"##, + "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_eq!(info_msg.get_webxdc_href(), Some("#foobar".to_string())); + + bob.recv_msg(&sent1).await; + bob.recv_msg_trash(&sent2).await; + let info_msg = bob.get_last_msg().await; + assert!(info_msg.is_info()); + assert_eq!(info_msg.get_webxdc_href(), Some("#foobar".to_string())); + + Ok(()) + } } diff --git a/src/webxdc/maps_integration.rs b/src/webxdc/maps_integration.rs index a69a2cf4e9..e444a7cbf4 100644 --- a/src/webxdc/maps_integration.rs +++ b/src/webxdc/maps_integration.rs @@ -146,6 +146,7 @@ pub(crate) async fn intercept_get_updates( item: StatusUpdateItem { payload: serde_json::to_value(location_item)?, info: None, + href: None, document: None, summary: None, uid: None, From 8349d1c75ff0ca4c643985f4a11cf736323b1144 Mon Sep 17 00:00:00 2001 From: "B. Petersen" Date: Fri, 22 Nov 2024 22:29:52 +0100 Subject: [PATCH 2/4] simplify cffi --- deltachat-ffi/src/lib.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/deltachat-ffi/src/lib.rs b/deltachat-ffi/src/lib.rs index 536c2ecd45..6730dff27d 100644 --- a/deltachat-ffi/src/lib.rs +++ b/deltachat-ffi/src/lib.rs @@ -3695,10 +3695,7 @@ pub unsafe extern "C" fn dc_msg_get_webxdc_href(msg: *mut dc_msg_t) -> *mut libc } let ffi_msg = &*msg; - match ffi_msg.message.get_webxdc_href() { - Some(str) => str.strdup(), - None => ptr::null_mut(), - } + ffi_msg.message.get_webxdc_href().strdup() } #[no_mangle] From 2bbf081b43da590034933771b2c06c0200e1d20a Mon Sep 17 00:00:00 2001 From: "B. Petersen" Date: Fri, 22 Nov 2024 22:30:29 +0100 Subject: [PATCH 3/4] fix comment --- src/webxdc.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/webxdc.rs b/src/webxdc.rs index 2173c970eb..e9d2b69267 100644 --- a/src/webxdc.rs +++ b/src/webxdc.rs @@ -962,8 +962,8 @@ impl Message { /// Get link attached to an info message. /// /// The info message needs to be of type SystemMessage::WebxdcInfoMessage. - /// Typically, this is used to start the corresponding - // with `window.location.href` set in JS land. + /// Typically, this is used to start the corresponding webxdc app + /// with `window.location.href` set in JS land. pub fn get_webxdc_href(&self) -> Option { let Some(href) = self.param.get(Param::Arg) else { return None; From f021ea2faffad4888397081b69394e984c866df5 Mon Sep 17 00:00:00 2001 From: "B. Petersen" Date: Fri, 22 Nov 2024 22:39:57 +0100 Subject: [PATCH 4/4] make clippy happy --- src/webxdc.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/webxdc.rs b/src/webxdc.rs index e9d2b69267..8749898f27 100644 --- a/src/webxdc.rs +++ b/src/webxdc.rs @@ -362,7 +362,7 @@ impl Context { .get_overwritable_info_msg_id(&instance, from_id) .await?; - if info_msg_id.is_some() && !status_update_item.href.is_some() { + if info_msg_id.is_some() && status_update_item.href.is_none() { if let Some(info_msg_id) = info_msg_id { chat::update_msg_text_and_timestamp( self, @@ -965,10 +965,7 @@ impl Message { /// Typically, this is used to start the corresponding webxdc app /// with `window.location.href` set in JS land. pub fn get_webxdc_href(&self) -> Option { - let Some(href) = self.param.get(Param::Arg) else { - return None; - }; - Some(href.to_string()) + self.param.get(Param::Arg).map(|href| href.to_string()) } }