diff --git a/draft/webxdc-dev-reference.md b/draft/webxdc-dev-reference.md index c49d552c5..926552a8b 100644 --- a/draft/webxdc-dev-reference.md +++ b/draft/webxdc-dev-reference.md @@ -28,7 +28,11 @@ Webxdc apps are usually shared in a chat and run independently on each peer. To get a shared state, the peers use `sendUpdate()` to send updates to each other. - `update`: an object with the following fields: - `update.payload`: any javascript primitive, array or object. + - `update.payload`: any javascript primitive, array or object. + - `update.info`: optional, short, informational message that will be added to the chat, + eg. "Alice voted" or "Bob scored 123 in MyGame"; + usually only one line of text is shown, + use this option sparingly to not spam the chat. - `descr`: short, human-readable description what this update is about. this is shown eg. as a fallback text in an email program. diff --git a/src/webxdc.rs b/src/webxdc.rs index 7bf81176f..9cfb43b20 100644 --- a/src/webxdc.rs +++ b/src/webxdc.rs @@ -2,7 +2,7 @@ use crate::constants::Viewtype; use crate::context::Context; -use crate::dc_tools::dc_open_file_std; +use crate::dc_tools::{dc_create_smeared_timestamp, dc_open_file_std}; use crate::message::{Message, MessageState, MsgId}; use crate::mimeparser::SystemMessage; use crate::param::Param; @@ -88,6 +88,9 @@ struct StatusUpdates { #[derive(Debug, Serialize, Deserialize)] pub(crate) struct StatusUpdateItem { payload: Value, + + #[serde(skip_serializing_if = "Option::is_none")] + info: Option, } impl Context { @@ -119,6 +122,7 @@ impl Context { &self, instance_msg_id: MsgId, update_str: &str, + timestamp: i64, ) -> Result { let update_str = update_str.trim(); if update_str.is_empty() { @@ -131,9 +135,19 @@ impl Context { } else { // TODO: this fallback (legacy `PAYLOAD`) should be deleted soon, together with the test below let payload: Value = serde_json::from_str(update_str)?; // checks if input data are valid json - StatusUpdateItem { payload } + StatusUpdateItem { + payload, + info: None, + } }; + if status_update_item.info.is_some() { + let mut instance = Message::load_from_db(self, instance_msg_id).await?; + if let Some(ref info) = status_update_item.info { + chat::add_info_msg(self, instance.chat_id, info.as_str(), timestamp).await?; + } + } + let rowid = self .sql .insert( @@ -170,7 +184,11 @@ impl Context { } let status_update_id = self - .create_status_update_record(instance_msg_id, update_str) + .create_status_update_record( + instance_msg_id, + update_str, + dc_create_smeared_timestamp(self).await, + ) .await?; match instance.state { MessageState::Undefined | MessageState::OutPreparing | MessageState::OutDraft => { @@ -227,11 +245,11 @@ impl Context { /// the array is parsed using serde, the single payloads are used as is. pub(crate) async fn receive_status_update(&self, msg_id: MsgId, json: &str) -> Result<()> { let msg = Message::load_from_db(self, msg_id).await?; - let instance = if msg.viewtype == Viewtype::Webxdc { - msg + let (timestamp, instance) = if msg.viewtype == Viewtype::Webxdc { + (msg.timestamp_sort, msg) } else if let Some(parent) = msg.parent(self).await? { if parent.viewtype == Viewtype::Webxdc { - parent + (msg.timestamp_sort, parent) } else { bail!("receive_status_update: message is not the child of a webxdc message.") } @@ -241,8 +259,12 @@ impl Context { let updates: StatusUpdates = serde_json::from_str(json)?; for update_item in updates.updates { - self.create_status_update_record(instance.id, &*serde_json::to_string(&update_item)?) - .await?; + self.create_status_update_record( + instance.id, + &*serde_json::to_string(&update_item)?, + timestamp, + ) + .await?; } Ok(()) @@ -562,7 +584,11 @@ mod tests { assert_eq!(t.get_webxdc_status_updates(instance.id, None).await?, "[]"); let id = t - .create_status_update_record(instance.id, "\n\n{\"payload\": {\"foo\":\"bar\"}}\n") + .create_status_update_record( + instance.id, + "\n\n{\"payload\": {\"foo\":\"bar\"}}\n", + 1640178619, + ) .await?; assert_eq!( t.get_webxdc_status_updates(instance.id, Some(id)).await?, @@ -570,11 +596,11 @@ mod tests { ); assert!(t - .create_status_update_record(instance.id, "\n\n\n") + .create_status_update_record(instance.id, "\n\n\n", 1640178619) .await .is_err()); assert!(t - .create_status_update_record(instance.id, "bad json") + .create_status_update_record(instance.id, "bad json", 1640178619) .await .is_err()); assert_eq!( @@ -587,13 +613,17 @@ mod tests { ); let id = t - .create_status_update_record(instance.id, r#"{"payload" : { "foo2":"bar2"}}"#) + .create_status_update_record( + instance.id, + r#"{"payload" : { "foo2":"bar2"}}"#, + 1640178619, + ) .await?; assert_eq!( t.get_webxdc_status_updates(instance.id, Some(id)).await?, r#"[{"payload":{"foo2":"bar2"}}]"# ); - t.create_status_update_record(instance.id, r#"{"payload":true}"#) + t.create_status_update_record(instance.id, r#"{"payload":true}"#, 1640178619) .await?; assert_eq!( t.get_webxdc_status_updates(instance.id, None).await?, @@ -606,6 +636,7 @@ mod tests { .create_status_update_record( instance.id, r#"{"payload" : 1, "sender": "that is not used"}"#, + 1640178619, ) .await?; assert_eq!( @@ -615,7 +646,7 @@ mod tests { // TODO: legacy `PAYLOAD` support should be deleted soon let id = t - .create_status_update_record(instance.id, r#"{"foo" : 1}"#) + .create_status_update_record(instance.id, r#"{"foo" : 1}"#, 1640178619) .await?; assert_eq!( t.get_webxdc_status_updates(instance.id, Some(id)).await?, @@ -1088,4 +1119,77 @@ sth_for_the = "future""# Ok(()) } + + #[async_std::test] + async fn test_webxdc_info_msg() -> Result<()> { + let alice = TestContext::new_alice().await; + let bob = TestContext::new_bob().await; + + // Alice sends update with an info message + let alice_chat = alice.create_chat(&bob).await; + let alice_instance = send_webxdc_instance(&alice, alice_chat.id).await?; + let sent1 = &alice.pop_sent_msg().await; + assert_eq!(alice_chat.id.get_msg_cnt(&alice).await?, 1); + + alice + .send_webxdc_status_update( + alice_instance.id, + r#"{"info":"this appears in-chat", "payload":"sth. else"}"#, + "descr text", + ) + .await?; + let sent2 = &alice.pop_sent_msg().await; + assert_eq!(alice_chat.id.get_msg_cnt(&alice).await?, 2); + let info_msg = alice.get_last_msg().await; + assert!(info_msg.is_info()); + assert_eq!( + info_msg.get_text(), + Some("this appears in-chat".to_string()) + ); + assert_eq!( + alice + .get_webxdc_status_updates(alice_instance.id, None) + .await?, + r#"[{"payload":"sth. else","info":"this appears in-chat"}]"# + ); + + // Bob receives all messages + bob.recv_msg(sent1).await; + let bob_instance = bob.get_last_msg().await; + let bob_chat_id = bob_instance.chat_id; + bob.recv_msg(sent2).await; + assert_eq!(bob_chat_id.get_msg_cnt(&bob).await?, 2); + let info_msg = bob.get_last_msg().await; + assert!(info_msg.is_info()); + assert_eq!( + info_msg.get_text(), + Some("this appears in-chat".to_string()) + ); + assert_eq!( + bob.get_webxdc_status_updates(bob_instance.id, None).await?, + r#"[{"payload":"sth. else","info":"this appears in-chat"}]"# + ); + + // Alice has a second device and also receives the info message there + let alice2 = TestContext::new_alice().await; + alice2.recv_msg(sent1).await; + let alice2_instance = alice2.get_last_msg().await; + let alice2_chat_id = alice2_instance.chat_id; + alice2.recv_msg(sent2).await; + assert_eq!(alice2_chat_id.get_msg_cnt(&alice2).await?, 2); + let info_msg = alice2.get_last_msg().await; + assert!(info_msg.is_info()); + assert_eq!( + info_msg.get_text(), + Some("this appears in-chat".to_string()) + ); + assert_eq!( + alice2 + .get_webxdc_status_updates(alice2_instance.id, None) + .await?, + r#"[{"payload":"sth. else","info":"this appears in-chat"}]"# + ); + + Ok(()) + } }