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
This commit is contained in:
bjoern
2025-09-04 16:51:51 +02:00
committed by GitHub
parent d4704977bc
commit bed1623dcb
10 changed files with 110 additions and 105 deletions

View File

@@ -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<CallInfo> {
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<bool> {
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))
}
}

View File

@@ -33,9 +33,10 @@ async fn setup_call() -> Result<CallSetup> {
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<CallSetup> {
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");
}

View File

@@ -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"

View File

@@ -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,
}

View File

@@ -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) {

View File

@@ -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<String> {
self.get_header(HeaderDef::MessageId)
.and_then(|msgid| parse_message_id(msgid).ok())

View File

@@ -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

View File

@@ -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 {