diff --git a/deltachat-ffi/deltachat.h b/deltachat-ffi/deltachat.h index d7218c79f..965c968b0 100644 --- a/deltachat-ffi/deltachat.h +++ b/deltachat-ffi/deltachat.h @@ -6031,6 +6031,21 @@ void dc_event_unref(dc_event_t* event); #define DC_EVENT_REACTIONS_CHANGED 2001 +/** + * A reaction to one's own sent message received. + * Typically, the UI will show a notification for that. + * + * In addition to this event, DC_EVENT_REACTIONS_CHANGED is emitted. + * + * @param data1 (int) contact_id ID of the contact sending this reaction. + * @param data2 (int) msg_id + (char*) reaction. + * ID of the message for which a reaction was received in dc_event_get_data2_int(), + * and the reaction as dc_event_get_data2_str(). + * string must be passed to dc_str_unref() afterwards. + */ +#define DC_EVENT_INCOMING_REACTION 2002 + + /** * There is a fresh message. Typically, the user will show an notification * when receiving this message. diff --git a/deltachat-ffi/src/lib.rs b/deltachat-ffi/src/lib.rs index 72ff1c6af..6559ab663 100644 --- a/deltachat-ffi/src/lib.rs +++ b/deltachat-ffi/src/lib.rs @@ -541,6 +541,7 @@ pub unsafe extern "C" fn dc_event_get_id(event: *mut dc_event_t) -> libc::c_int EventType::ErrorSelfNotInGroup(_) => 410, EventType::MsgsChanged { .. } => 2000, EventType::ReactionsChanged { .. } => 2001, + EventType::IncomingReaction { .. } => 2002, EventType::IncomingMsg { .. } => 2005, EventType::IncomingMsgBunch { .. } => 2006, EventType::MsgsNoticed { .. } => 2008, @@ -601,6 +602,7 @@ 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::MsgsChanged { chat_id, .. } | EventType::ReactionsChanged { chat_id, .. } | EventType::IncomingMsg { chat_id, .. } @@ -678,6 +680,7 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc: | EventType::EventChannelOverflow { .. } => 0, EventType::MsgsChanged { msg_id, .. } | EventType::ReactionsChanged { msg_id, .. } + | EventType::IncomingReaction { msg_id, .. } | EventType::IncomingMsg { msg_id, .. } | EventType::MsgDelivered { msg_id, .. } | EventType::MsgFailed { msg_id, .. } @@ -767,6 +770,11 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut libc::memcpy(ptr, data.as_ptr() as *mut libc::c_void, data.len()); ptr as *mut libc::c_char } + EventType::IncomingReaction { reaction, .. } => reaction + .as_str() + .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 b282bad89..fab754202 100644 --- a/deltachat-jsonrpc/src/api/types/events.rs +++ b/deltachat-jsonrpc/src/api/types/events.rs @@ -98,6 +98,14 @@ pub enum EventType { contact_id: u32, }, + /// Incoming reaction, should be notified. + #[serde(rename_all = "camelCase")] + IncomingReaction { + contact_id: u32, + msg_id: u32, + reaction: String, + }, + /// There is a fresh message. Typically, the user will show an notification /// when receiving this message. /// @@ -302,6 +310,15 @@ impl From for EventType { msg_id: msg_id.to_u32(), contact_id: contact_id.to_u32(), }, + CoreEventType::IncomingReaction { + contact_id, + msg_id, + reaction, + } => IncomingReaction { + contact_id: contact_id.to_u32(), + msg_id: msg_id.to_u32(), + reaction: reaction.as_str().to_string(), + }, 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 36ac5a556..ea5343179 100644 --- a/node/constants.js +++ b/node/constants.js @@ -50,6 +50,7 @@ module.exports = { DC_EVENT_IMEX_PROGRESS: 2051, DC_EVENT_INCOMING_MSG: 2005, DC_EVENT_INCOMING_MSG_BUNCH: 2006, + DC_EVENT_INCOMING_REACTION: 2002, 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 de02aa7f5..06c929585 100644 --- a/node/events.js +++ b/node/events.js @@ -16,6 +16,7 @@ module.exports = { 410: 'DC_EVENT_ERROR_SELF_NOT_IN_GROUP', 2000: 'DC_EVENT_MSGS_CHANGED', 2001: 'DC_EVENT_REACTIONS_CHANGED', + 2002: 'DC_EVENT_INCOMING_REACTION', 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 93cf45814..ec0734ccc 100644 --- a/node/lib/constants.ts +++ b/node/lib/constants.ts @@ -50,6 +50,7 @@ export enum C { DC_EVENT_IMEX_PROGRESS = 2051, DC_EVENT_INCOMING_MSG = 2005, DC_EVENT_INCOMING_MSG_BUNCH = 2006, + DC_EVENT_INCOMING_REACTION = 2002, DC_EVENT_INFO = 100, DC_EVENT_LOCATION_CHANGED = 2035, DC_EVENT_MSGS_CHANGED = 2000, @@ -322,6 +323,7 @@ export const EventId2EventName: { [key: number]: string } = { 410: 'DC_EVENT_ERROR_SELF_NOT_IN_GROUP', 2000: 'DC_EVENT_MSGS_CHANGED', 2001: 'DC_EVENT_REACTIONS_CHANGED', + 2002: 'DC_EVENT_INCOMING_REACTION', 2005: 'DC_EVENT_INCOMING_MSG', 2006: 'DC_EVENT_INCOMING_MSG_BUNCH', 2008: 'DC_EVENT_MSGS_NOTICED', diff --git a/src/events/payload.rs b/src/events/payload.rs index 69302a9a9..394fd76a7 100644 --- a/src/events/payload.rs +++ b/src/events/payload.rs @@ -8,6 +8,7 @@ use crate::config::Config; use crate::contact::ContactId; use crate::ephemeral::Timer as EphemeralTimer; use crate::message::MsgId; +use crate::reaction::Reaction; use crate::webxdc::StatusUpdateSerial; /// Event payload. @@ -94,6 +95,18 @@ pub enum EventType { contact_id: ContactId, }, + /// Reactions for the message changed. + IncomingReaction { + /// ID of the contact whose reaction set is changed. + contact_id: ContactId, + + /// ID of the message for which reactions were changed. + msg_id: MsgId, + + /// The reaction. + reaction: Reaction, + }, + /// There is a fresh message. Typically, the user will show an notification /// when receiving this message. /// diff --git a/src/reaction.rs b/src/reaction.rs index a5d382582..b621e914a 100644 --- a/src/reaction.rs +++ b/src/reaction.rs @@ -19,6 +19,7 @@ use std::collections::BTreeMap; use std::fmt; use anyhow::Result; +use serde::{Deserialize, Serialize}; use crate::chat::{send_msg, Chat, ChatId}; use crate::chatlist_events; @@ -31,7 +32,7 @@ use crate::param::Param; /// A single reaction consisting of multiple emoji sequences. /// /// It is guaranteed to have all emojis sorted and deduplicated inside. -#[derive(Debug, Default, Clone)] +#[derive(Debug, Default, Clone, Deserialize, Eq, PartialEq, Serialize)] pub struct Reaction { /// Canonical representation of reaction as a string of space-separated emojis. reaction: String, @@ -173,7 +174,7 @@ async fn set_msg_id_reaction( chat_id: ChatId, contact_id: ContactId, timestamp: i64, - reaction: Reaction, + reaction: &Reaction, ) -> Result<()> { if reaction.is_empty() { // Simply remove the record instead of setting it to empty string. @@ -244,7 +245,7 @@ pub async fn send_reaction(context: &Context, msg_id: MsgId, reaction: &str) -> msg.chat_id, ContactId::SELF, reaction_msg.timestamp_sort, - reaction, + &reaction, ) .await?; Ok(reaction_msg_id) @@ -275,16 +276,28 @@ pub(crate) async fn set_msg_reaction( contact_id: ContactId, timestamp: i64, reaction: Reaction, + is_incoming_fresh: bool, ) -> Result<()> { if let Some((msg_id, _)) = rfc724_mid_exists(context, in_reply_to).await? { - set_msg_id_reaction(context, msg_id, chat_id, contact_id, timestamp, reaction).await + set_msg_id_reaction(context, msg_id, chat_id, contact_id, timestamp, &reaction).await?; + + if is_incoming_fresh + && !reaction.is_empty() + && msg_id.get_state(context).await?.is_outgoing() + { + context.emit_event(EventType::IncomingReaction { + contact_id, + msg_id, + reaction, + }); + } } else { info!( context, "Can't assign reaction to unknown message with Message-ID {}", in_reply_to ); - Ok(()) } + Ok(()) } /// Get our own reaction for a given message. @@ -563,6 +576,38 @@ Here's my footer -- bob@example.net" Ok(()) } + async fn expect_incoming_reactions_event( + t: &TestContext, + expected_msg_id: MsgId, + expected_contact_id: ContactId, + expected_reaction: &str, + ) -> Result<()> { + let event = t + .evtracker + .get_matching(|evt| matches!(evt, EventType::IncomingReaction { .. })) + .await; + match event { + EventType::IncomingReaction { + msg_id, + contact_id, + reaction, + } => { + assert_eq!(msg_id, expected_msg_id); + assert_eq!(contact_id, expected_contact_id); + assert_eq!(reaction, Reaction::from(expected_reaction)); + } + _ => unreachable!(), + } + Ok(()) + } + + async fn has_incoming_reactions_event(t: &TestContext) -> bool { + t.evtracker + .get_matching_opt(t, |evt| matches!(evt, EventType::IncomingReaction { .. })) + .await + .is_some() + } + #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_send_reaction() -> Result<()> { let alice = TestContext::new_alice().await; @@ -593,6 +638,7 @@ Here's my footer -- bob@example.net" send_reaction(&bob, bob_msg.id, "👍").await.unwrap(); expect_reactions_changed_event(&bob, bob_msg.chat_id, bob_msg.id, ContactId::SELF).await?; + assert!(!has_incoming_reactions_event(&bob).await); assert_eq!(get_chat_msgs(&bob, bob_msg.chat_id).await?.len(), 2); let bob_reaction_msg = bob.pop_sent_msg().await; @@ -610,6 +656,7 @@ Here's my footer -- bob@example.net" assert_eq!(bob_reaction.as_str(), "👍"); expect_reactions_changed_event(&alice, chat_alice.id, alice_msg.sender_msg_id, *bob_id) .await?; + expect_incoming_reactions_event(&alice, alice_msg.sender_msg_id, *bob_id, "👍").await?; // Alice reacts to own message. send_reaction(&alice, alice_msg.sender_msg_id, "👍 😀") @@ -650,6 +697,7 @@ Here's my footer -- bob@example.net" send_reaction(&bob, bob_msg1.id, "👍").await?; let bob_send_reaction = bob.pop_sent_msg().await; alice.recv_msg_trash(&bob_send_reaction).await; + assert!(has_incoming_reactions_event(&alice).await); let chatlist = Chatlist::try_load(&bob, 0, None, None).await?; let summary = chatlist.get_summary(&bob, 0, None).await?; @@ -665,6 +713,7 @@ Here's my footer -- bob@example.net" send_reaction(&alice, alice_msg1.sender_msg_id, "🍿").await?; let alice_send_reaction = alice.pop_sent_msg().await; bob.recv_msg_opt(&alice_send_reaction).await; + assert!(!has_incoming_reactions_event(&bob).await); assert_summary(&alice, "You reacted 🍿 to \"Party?\"").await; assert_summary(&bob, "ALICE reacted 🍿 to \"Party?\"").await; diff --git a/src/receive_imf.rs b/src/receive_imf.rs index 25afe854a..1aa4c3381 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -1473,6 +1473,7 @@ async fn add_parts( for part in &mime_parser.parts { if part.is_reaction { let reaction_str = simplify::remove_footers(part.msg.as_str()); + let is_incoming_fresh = mime_parser.incoming && !seen && !fetching_existing_messages; set_msg_reaction( context, mime_in_reply_to, @@ -1480,6 +1481,7 @@ async fn add_parts( from_id, sort_timestamp, Reaction::from(reaction_str.as_str()), + is_incoming_fresh, ) .await?; } diff --git a/src/test_utils.rs b/src/test_utils.rs index bb9aaf139..507529889 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -1106,6 +1106,24 @@ impl EventTracker { .expect("timeout waiting for event match") } + /// Consumes emitted events returning the first matching one if any. + pub async fn get_matching_opt bool>( + &self, + ctx: &Context, + event_matcher: F, + ) -> Option { + ctx.emit_event(EventType::Test); + loop { + let event = self.recv().await.unwrap(); + if event_matcher(&event.typ) { + return Some(event.typ); + } + if let EventType::Test = event.typ { + return None; + } + } + } + /// Consumes events looking for an [`EventType::Info`] with substring matching. pub async fn get_info_contains(&self, s: &str) -> EventType { self.get_matching(|evt| match evt {