From bed1623dcb7c698cdf47b530340181fb8a30f235 Mon Sep 17 00:00:00 2001 From: bjoern Date: Thu, 4 Sep 2025 16:51:51 +0200 Subject: [PATCH] feat: use dedicated 'call' viewtype (#7174) a dedicated viewtype allows the UI to show a more advanced UI, but even when using the defaults, it has the advantage that incoming/outgoing and the date are directly visible. successor of https://github.com/chatmail/core/pull/6650 --- deltachat-ffi/deltachat.h | 10 +- deltachat-jsonrpc/src/api/types/message.rs | 9 +- src/calls.rs | 114 ++++++++++----------- src/calls/calls_tests.rs | 10 +- src/chat.rs | 6 +- src/message.rs | 6 +- src/mimefactory.rs | 16 ++- src/mimeparser.rs | 30 +++--- src/receive_imf.rs | 2 +- src/summary.rs | 12 ++- 10 files changed, 110 insertions(+), 105 deletions(-) diff --git a/deltachat-ffi/deltachat.h b/deltachat-ffi/deltachat.h index cf8ff0b90..94b932fa3 100644 --- a/deltachat-ffi/deltachat.h +++ b/deltachat-ffi/deltachat.h @@ -4620,8 +4620,6 @@ int dc_msg_is_info (const dc_msg_t* msg); * and also offer a way to fix the encryption, eg. by a button offering a QR scan * - DC_INFO_WEBXDC_INFO_MESSAGE (32) - Info-message created by webxdc app sending `update.info` * - DC_INFO_CHAT_E2EE (50) - Info-message for "Chat is end-to-end-encrypted" - * - DC_INFO_OUTGOING_CALL (60) - Info-message refers to an outgoing call - * - DC_INFO_INCOMING_CALL (65) - Info-message refers to an incoming call * * For the messages that refer to a CONTACT, * dc_msg_get_info_contact_id() returns the contact ID. @@ -4678,8 +4676,6 @@ uint32_t dc_msg_get_info_contact_id (const dc_msg_t* msg); #define DC_INFO_INVALID_UNENCRYPTED_MAIL 13 #define DC_INFO_WEBXDC_INFO_MESSAGE 32 #define DC_INFO_CHAT_E2EE 50 -#define DC_INFO_OUTGOING_CALL 60 -#define DC_INFO_INCOMING_CALL 65 /** @@ -5716,6 +5712,12 @@ int64_t dc_lot_get_timestamp (const dc_lot_t* lot); #define DC_MSG_VIDEOCHAT_INVITATION 70 +/** + * Message indicating an incoming or outgoing call. + */ +#define DC_MSG_CALL 71 + + /** * The message is a webxdc instance. * diff --git a/deltachat-jsonrpc/src/api/types/message.rs b/deltachat-jsonrpc/src/api/types/message.rs index 398a6b018..0369c43e3 100644 --- a/deltachat-jsonrpc/src/api/types/message.rs +++ b/deltachat-jsonrpc/src/api/types/message.rs @@ -324,6 +324,9 @@ pub enum MessageViewtype { /// Message is an invitation to a videochat. VideochatInvitation, + /// Message is a call. + Call, + /// Message is an webxdc instance. Webxdc, @@ -346,6 +349,7 @@ impl From for MessageViewtype { Viewtype::Video => MessageViewtype::Video, Viewtype::File => MessageViewtype::File, Viewtype::VideochatInvitation => MessageViewtype::VideochatInvitation, + Viewtype::Call => MessageViewtype::Call, Viewtype::Webxdc => MessageViewtype::Webxdc, Viewtype::Vcard => MessageViewtype::Vcard, } @@ -365,6 +369,7 @@ impl From for Viewtype { MessageViewtype::Video => Viewtype::Video, MessageViewtype::File => Viewtype::File, MessageViewtype::VideochatInvitation => Viewtype::VideochatInvitation, + MessageViewtype::Call => Viewtype::Call, MessageViewtype::Webxdc => Viewtype::Webxdc, MessageViewtype::Vcard => Viewtype::Vcard, } @@ -438,8 +443,6 @@ pub enum SystemMessageType { /// This message contains a users iroh node address. IrohNodeAddr, - OutgoingCall, - IncomingCall, CallAccepted, CallEnded, } @@ -468,8 +471,6 @@ impl From for SystemMessageType { SystemMessage::IrohNodeAddr => SystemMessageType::IrohNodeAddr, SystemMessage::SecurejoinWait => SystemMessageType::SecurejoinWait, SystemMessage::SecurejoinWaitTimeout => SystemMessageType::SecurejoinWaitTimeout, - SystemMessage::OutgoingCall => SystemMessageType::OutgoingCall, - SystemMessage::IncomingCall => SystemMessageType::IncomingCall, SystemMessage::CallAccepted => SystemMessageType::CallAccepted, SystemMessage::CallEnded => SystemMessageType::CallEnded, } diff --git a/src/calls.rs b/src/calls.rs index b59740e34..a968ccfdf 100644 --- a/src/calls.rs +++ b/src/calls.rs @@ -4,6 +4,7 @@ //! This means, the "Call ID" is a "Message ID" currently - similar to webxdc. use crate::chat::{Chat, ChatId, send_msg}; use crate::constants::Chattype; +use crate::contact::ContactId; use crate::context::Context; use crate::events::EventType; use crate::headerdef::HeaderDef; @@ -82,11 +83,10 @@ impl Context { ensure!(chat.typ == Chattype::Single && !chat.is_self_talk()); let mut call = Message { - viewtype: Viewtype::Text, + viewtype: Viewtype::Call, text: "Calling...".into(), ..Default::default() }; - call.param.set_cmd(SystemMessage::OutgoingCall); call.param.set(Param::WebrtcRoom, &place_call_info); call.id = send_msg(self, chat_id, &mut call).await?; @@ -184,60 +184,61 @@ impl Context { mime_message: &MimeMessage, call_id: MsgId, ) -> Result<()> { - match mime_message.is_system_message { - SystemMessage::IncomingCall => { - let call = self.load_call_by_id(call_id).await?; - if call.is_incoming { - if call.is_stale_call() { - call.update_text(self, "Missed call").await?; - self.emit_incoming_msg(call.msg.chat_id, call_id); - } else { - self.emit_msgs_changed(call.msg.chat_id, call_id); - self.emit_event(EventType::IncomingCall { - msg_id: call.msg.id, - place_call_info: call.place_call_info.to_string(), - }); - let wait = call.remaining_ring_seconds(); - task::spawn(Context::emit_end_call_if_unaccepted( - self.clone(), - wait.try_into()?, - call.msg.id, - )); - } + if mime_message.is_call() { + let call = self.load_call_by_id(call_id).await?; + if call.is_incoming { + if call.is_stale_call() { + call.update_text(self, "Missed call").await?; + self.emit_incoming_msg(call.msg.chat_id, call_id); } else { self.emit_msgs_changed(call.msg.chat_id, call_id); - } - } - SystemMessage::CallAccepted => { - let call = self.load_call_by_id(call_id).await?; - self.emit_msgs_changed(call.msg.chat_id, call_id); - if call.is_incoming { - self.emit_event(EventType::IncomingCallAccepted { + self.emit_event(EventType::IncomingCall { msg_id: call.msg.id, - accept_call_info: call.accept_call_info, + place_call_info: call.place_call_info.to_string(), }); - } else { - let accept_call_info = mime_message - .get_header(HeaderDef::ChatWebrtcAccepted) - .unwrap_or_default(); - call.msg - .clone() - .mark_call_as_accepted(self, accept_call_info.to_string()) - .await?; - self.emit_event(EventType::OutgoingCallAccepted { + let wait = call.remaining_ring_seconds(); + task::spawn(Context::emit_end_call_if_unaccepted( + self.clone(), + wait.try_into()?, + call.msg.id, + )); + } + } else { + self.emit_msgs_changed(call.msg.chat_id, call_id); + } + } else { + match mime_message.is_system_message { + SystemMessage::CallAccepted => { + let call = self.load_call_by_id(call_id).await?; + self.emit_msgs_changed(call.msg.chat_id, call_id); + if call.is_incoming { + self.emit_event(EventType::IncomingCallAccepted { + msg_id: call.msg.id, + accept_call_info: call.accept_call_info, + }); + } else { + let accept_call_info = mime_message + .get_header(HeaderDef::ChatWebrtcAccepted) + .unwrap_or_default(); + call.msg + .clone() + .mark_call_as_accepted(self, accept_call_info.to_string()) + .await?; + self.emit_event(EventType::OutgoingCallAccepted { + msg_id: call.msg.id, + accept_call_info: accept_call_info.to_string(), + }); + } + } + SystemMessage::CallEnded => { + let call = self.load_call_by_id(call_id).await?; + self.emit_msgs_changed(call.msg.chat_id, call_id); + self.emit_event(EventType::CallEnded { msg_id: call.msg.id, - accept_call_info: accept_call_info.to_string(), }); } + _ => {} } - SystemMessage::CallEnded => { - let call = self.load_call_by_id(call_id).await?; - self.emit_msgs_changed(call.msg.chat_id, call_id); - self.emit_event(EventType::CallEnded { - msg_id: call.msg.id, - }); - } - _ => {} } Ok(()) } @@ -255,13 +256,10 @@ impl Context { } fn load_call_by_message(&self, call: Message) -> Result { - ensure!( - call.get_info_type() == SystemMessage::IncomingCall - || call.get_info_type() == SystemMessage::OutgoingCall - ); + ensure!(call.viewtype == Viewtype::Call); Ok(CallInfo { - is_incoming: call.get_info_type() == SystemMessage::IncomingCall, + is_incoming: call.get_from_id() != ContactId::SELF, is_accepted: call.is_call_accepted()?, place_call_info: call .param @@ -284,10 +282,7 @@ impl Message { context: &Context, accept_call_info: String, ) -> Result<()> { - ensure!( - self.get_info_type() == SystemMessage::IncomingCall - || self.get_info_type() == SystemMessage::OutgoingCall - ); + ensure!(self.viewtype == Viewtype::Call); self.param.set_int(Param::Arg, 1); self.param.set(Param::WebrtcAccepted, accept_call_info); self.update_param(context).await?; @@ -295,10 +290,7 @@ impl Message { } fn is_call_accepted(&self) -> Result { - ensure!( - self.get_info_type() == SystemMessage::IncomingCall - || self.get_info_type() == SystemMessage::OutgoingCall - ); + ensure!(self.viewtype == Viewtype::Call); Ok(self.param.get_int(Param::Arg) == Some(1)) } } diff --git a/src/calls/calls_tests.rs b/src/calls/calls_tests.rs index 44a76a29d..0e5eb1418 100644 --- a/src/calls/calls_tests.rs +++ b/src/calls/calls_tests.rs @@ -33,9 +33,10 @@ async fn setup_call() -> Result { let alice_call = Message::load_from_db(&alice, sent1.sender_msg_id).await?; let alice2_call = alice2.recv_msg(&sent1).await; for (t, m) in [(&alice, &alice_call), (&alice2, &alice2_call)] { - assert!(m.is_info()); - assert_eq!(m.get_info_type(), SystemMessage::OutgoingCall); + assert!(!m.is_info()); + assert_eq!(m.viewtype, Viewtype::Call); let info = t.load_call_by_id(m.id).await?; + assert!(!info.is_incoming); assert!(!info.is_accepted); assert_eq!(info.place_call_info, "place_info"); } @@ -45,12 +46,13 @@ async fn setup_call() -> Result { let bob_call = bob.recv_msg(&sent1).await; let bob2_call = bob2.recv_msg(&sent1).await; for (t, m) in [(&bob, &bob_call), (&bob2, &bob2_call)] { - assert!(m.is_info()); - assert_eq!(m.get_info_type(), SystemMessage::IncomingCall); + assert!(!m.is_info()); + assert_eq!(m.viewtype, Viewtype::Call); t.evtracker .get_matching(|evt| matches!(evt, EventType::IncomingCall { .. })) .await; let info = t.load_call_by_id(m.id).await?; + assert!(info.is_incoming); assert!(!info.is_accepted); assert_eq!(info.place_call_info, "place_info"); } diff --git a/src/chat.rs b/src/chat.rs index f0ebdb0c6..2086e774f 100644 --- a/src/chat.rs +++ b/src/chat.rs @@ -2691,7 +2691,10 @@ impl ChatIdBlocked { } async fn prepare_msg_blob(context: &Context, msg: &mut Message) -> Result<()> { - if msg.viewtype == Viewtype::Text || msg.viewtype == Viewtype::VideochatInvitation { + if msg.viewtype == Viewtype::Text + || msg.viewtype == Viewtype::VideochatInvitation + || msg.viewtype == Viewtype::Call + { // the caller should check if the message text is empty } else if msg.viewtype.has_file() { let viewtype_orig = msg.viewtype; @@ -3167,6 +3170,7 @@ pub async fn send_edit_request(context: &Context, msg_id: MsgId, new_text: Strin original_msg.viewtype != Viewtype::VideochatInvitation, "Cannot edit videochat invitations" ); + ensure!(original_msg.viewtype != Viewtype::Call, "Cannot edit calls"); ensure!( !original_msg.text.is_empty(), // avoid complexity in UI element changes. focus is typos and rewordings "Cannot add text" diff --git a/src/message.rs b/src/message.rs index cf4b84304..4d3aef3a6 100644 --- a/src/message.rs +++ b/src/message.rs @@ -973,8 +973,6 @@ impl Message { | SystemMessage::WebxdcStatusUpdate | SystemMessage::WebxdcInfoMessage | SystemMessage::IrohNodeAddr - | SystemMessage::OutgoingCall - | SystemMessage::IncomingCall | SystemMessage::CallAccepted | SystemMessage::CallEnded | SystemMessage::Unknown => Ok(None), @@ -2280,6 +2278,9 @@ pub enum Viewtype { /// Message is an invitation to a videochat. VideochatInvitation = 70, + /// Message is an incoming or outgoing call. + Call = 71, + /// Message is an webxdc instance. Webxdc = 80, @@ -2303,6 +2304,7 @@ impl Viewtype { Viewtype::Video => true, Viewtype::File => true, Viewtype::VideochatInvitation => false, + Viewtype::Call => false, Viewtype::Webxdc => true, Viewtype::Vcard => true, } diff --git a/src/mimefactory.rs b/src/mimefactory.rs index a16420e52..c4c64c33d 100644 --- a/src/mimefactory.rs +++ b/src/mimefactory.rs @@ -3,7 +3,7 @@ use std::collections::{BTreeSet, HashSet}; use std::io::Cursor; -use anyhow::{Context as _, Result, anyhow, bail, ensure}; +use anyhow::{Context as _, Result, bail, ensure}; use base64::Engine as _; use data_encoding::BASE32_NOPAD; use deltachat_contact_tools::sanitize_bidi_characters; @@ -1533,15 +1533,6 @@ impl MimeFactory { .into(), )); } - SystemMessage::OutgoingCall => { - headers.push(( - "Chat-Content", - mail_builder::headers::raw::Raw::new("call").into(), - )); - } - SystemMessage::IncomingCall => { - return Err(anyhow!("Unexpected incoming call rendering.")); - } SystemMessage::CallAccepted => { headers.push(( "Chat-Content", @@ -1578,6 +1569,11 @@ impl MimeFactory { "Chat-Content", mail_builder::headers::raw::Raw::new("videochat-invitation").into(), )); + } else if msg.viewtype == Viewtype::Call { + headers.push(( + "Chat-Content", + mail_builder::headers::raw::Raw::new("call").into(), + )); } if msg.param.exists(Param::WebrtcRoom) { diff --git a/src/mimeparser.rs b/src/mimeparser.rs index 467a82b33..1d92a377c 100644 --- a/src/mimeparser.rs +++ b/src/mimeparser.rs @@ -217,17 +217,7 @@ pub enum SystemMessage { /// "Messages are end-to-end encrypted." ChatE2ee = 50, - /// This system message represents an outgoing call. - /// This message is visible to the user as an "info" message. - OutgoingCall = 60, - - /// This system message represents an incoming call. - /// This message is visible to the user as an "info" message. - IncomingCall = 65, - /// Message indicating that a call was accepted. - /// While the 1:1 call may be established elsewhere, - /// the message is still needed for a multidevice setup, so that other devices stop ringing. CallAccepted = 66, /// Message indicating that a call was ended. @@ -692,12 +682,6 @@ impl MimeMessage { self.is_system_message = SystemMessage::ChatProtectionDisabled; } else if value == "group-avatar-changed" { self.is_system_message = SystemMessage::GroupImageChanged; - } else if value == "call" { - self.is_system_message = if self.incoming { - SystemMessage::IncomingCall - } else { - SystemMessage::OutgoingCall - }; } else if value == "call-accepted" { self.is_system_message = SystemMessage::CallAccepted; } else if value == "call-ended" { @@ -738,6 +722,8 @@ impl MimeMessage { if let Some(room) = room { if content == "videochat-invitation" { part.typ = Viewtype::VideochatInvitation; + } else if content == "call" { + part.typ = Viewtype::Call } part.param.set(Param::WebrtcRoom, room); } else if let Some(accepted) = accepted { @@ -767,7 +753,10 @@ impl MimeMessage { | Viewtype::Vcard | Viewtype::File | Viewtype::Webxdc => true, - Viewtype::Unknown | Viewtype::Text | Viewtype::VideochatInvitation => false, + Viewtype::Unknown + | Viewtype::Text + | Viewtype::VideochatInvitation + | Viewtype::Call => false, }) { let mut parts = std::mem::take(&mut self.parts); @@ -1582,6 +1571,13 @@ impl MimeMessage { } } + /// Check if a message is a call. + pub(crate) fn is_call(&self) -> bool { + self.parts + .first() + .is_some_and(|part| part.typ == Viewtype::Call) + } + pub(crate) fn get_rfc724_mid(&self) -> Option { self.get_header(HeaderDef::MessageId) .and_then(|msgid| parse_message_id(msgid).ok()) diff --git a/src/receive_imf.rs b/src/receive_imf.rs index c228a1b18..2e1d2b603 100644 --- a/src/receive_imf.rs +++ b/src/receive_imf.rs @@ -1000,7 +1000,7 @@ pub(crate) async fn receive_imf_inner( } } - if mime_parser.is_system_message == SystemMessage::IncomingCall { + if mime_parser.is_call() { context.handle_call_msg(&mime_parser, insert_msg_id).await?; } else if received_msg.hidden { // No need to emit an event about the changed message diff --git a/src/summary.rs b/src/summary.rs index 6601e8982..7392b395e 100644 --- a/src/summary.rs +++ b/src/summary.rs @@ -97,7 +97,7 @@ impl Summary { let prefix = if msg.state == MessageState::OutDraft { Some(SummaryPrefix::Draft(stock_str::draft(context).await)) } else if msg.from_id == ContactId::SELF { - if msg.is_info() { + if msg.is_info() || msg.viewtype == Viewtype::Call { None } else { Some(SummaryPrefix::Me(stock_str::self_msg(context).await)) @@ -233,6 +233,16 @@ impl Message { type_file = self.param.get(Param::Summary1).map(|s| s.to_string()); append_text = true; } + Viewtype::Call => { + emoji = Some("📞"); + type_name = Some(if self.from_id == ContactId::SELF { + "Outgoing call".to_string() + } else { + "Incoming call".to_string() + }); + type_file = None; + append_text = false + } Viewtype::Text | Viewtype::Unknown => { emoji = None; if self.param.get_cmd() == SystemMessage::LocationOnly {