feat: hide call status change messages (#7175)

this PR uses the initial "call messages" (that has a separate viewtype
since #7174) to show all call status.

this is what most other messengers are doing as well. additional "info
messages" after a call are no longer needed.

on the wire, as we cannot pickpack on visible info messages, we use
hidden messages, similar to eg. webxdc status updates.

in future PR, it is planned to allow getting call state as a json, so
that UI can render nicely. it is then decided if we want to translate
the strings in the core.

<img width="320" alt="IMG_0150"
src="https://github.com/user-attachments/assets/41ee3fa3-8be4-42c3-8dd9-d20f49881650"
/>

successor of https://github.com/chatmail/core/pull/6650
This commit is contained in:
bjoern
2025-09-05 08:52:15 +02:00
committed by GitHub
parent 53a3e51920
commit b6ab13f1de
3 changed files with 69 additions and 35 deletions

View File

@@ -114,6 +114,7 @@ impl Context {
chat.id.accept(self).await?; chat.id.accept(self).await?;
} }
call.update_text(self, "Call accepted").await?;
call.msg call.msg
.mark_call_as_accepted(self, accept_call_info.to_string()) .mark_call_as_accepted(self, accept_call_info.to_string())
.await?; .await?;
@@ -125,6 +126,7 @@ impl Context {
..Default::default() ..Default::default()
}; };
msg.param.set_cmd(SystemMessage::CallAccepted); msg.param.set_cmd(SystemMessage::CallAccepted);
msg.hidden = true;
msg.param msg.param
.set(Param::WebrtcAccepted, accept_call_info.to_string()); .set(Param::WebrtcAccepted, accept_call_info.to_string());
msg.set_quote(self, Some(&call.msg)).await?; msg.set_quote(self, Some(&call.msg)).await?;
@@ -133,6 +135,7 @@ impl Context {
msg_id: call.msg.id, msg_id: call.msg.id,
accept_call_info, accept_call_info,
}); });
self.emit_msgs_changed(call.msg.chat_id, call_id);
Ok(()) Ok(())
} }
@@ -140,6 +143,8 @@ impl Context {
pub async fn end_call(&self, call_id: MsgId) -> Result<()> { pub async fn end_call(&self, call_id: MsgId) -> Result<()> {
let call: CallInfo = self.load_call_by_id(call_id).await?; let call: CallInfo = self.load_call_by_id(call_id).await?;
call.update_text(self, "Call ended").await?;
if call.is_accepted || !call.is_incoming { if call.is_accepted || !call.is_incoming {
let mut msg = Message { let mut msg = Message {
viewtype: Viewtype::Text, viewtype: Viewtype::Text,
@@ -147,6 +152,7 @@ impl Context {
..Default::default() ..Default::default()
}; };
msg.param.set_cmd(SystemMessage::CallEnded); msg.param.set_cmd(SystemMessage::CallEnded);
msg.hidden = true;
msg.set_quote(self, Some(&call.msg)).await?; msg.set_quote(self, Some(&call.msg)).await?;
msg.id = send_msg(self, call.msg.chat_id, &mut msg).await?; msg.id = send_msg(self, call.msg.chat_id, &mut msg).await?;
} else if call.is_incoming { } else if call.is_incoming {
@@ -161,6 +167,7 @@ impl Context {
self.emit_event(EventType::CallEnded { self.emit_event(EventType::CallEnded {
msg_id: call.msg.id, msg_id: call.msg.id,
}); });
self.emit_msgs_changed(call.msg.chat_id, call_id);
Ok(()) Ok(())
} }
@@ -189,9 +196,9 @@ impl Context {
if call.is_incoming { if call.is_incoming {
if call.is_stale_call() { if call.is_stale_call() {
call.update_text(self, "Missed call").await?; call.update_text(self, "Missed call").await?;
self.emit_incoming_msg(call.msg.chat_id, call_id); self.emit_incoming_msg(call.msg.chat_id, call_id); // notify missed call
} else { } else {
self.emit_msgs_changed(call.msg.chat_id, call_id); self.emit_msgs_changed(call.msg.chat_id, call_id); // ringing calls are not additionally notified
self.emit_event(EventType::IncomingCall { self.emit_event(EventType::IncomingCall {
msg_id: call.msg.id, msg_id: call.msg.id,
place_call_info: call.place_call_info.to_string(), place_call_info: call.place_call_info.to_string(),
@@ -210,6 +217,7 @@ impl Context {
match mime_message.is_system_message { match mime_message.is_system_message {
SystemMessage::CallAccepted => { SystemMessage::CallAccepted => {
let call = self.load_call_by_id(call_id).await?; let call = self.load_call_by_id(call_id).await?;
call.update_text(self, "Call accepted").await?;
self.emit_msgs_changed(call.msg.chat_id, call_id); self.emit_msgs_changed(call.msg.chat_id, call_id);
if call.is_incoming { if call.is_incoming {
self.emit_event(EventType::IncomingCallAccepted { self.emit_event(EventType::IncomingCallAccepted {
@@ -232,6 +240,7 @@ impl Context {
} }
SystemMessage::CallEnded => { SystemMessage::CallEnded => {
let call = self.load_call_by_id(call_id).await?; let call = self.load_call_by_id(call_id).await?;
call.update_text(self, "Call ended").await?;
self.emit_msgs_changed(call.msg.chat_id, call_id); self.emit_msgs_changed(call.msg.chat_id, call_id);
self.emit_event(EventType::CallEnded { self.emit_event(EventType::CallEnded {
msg_id: call.msg.id, msg_id: call.msg.id,
@@ -245,7 +254,10 @@ impl Context {
pub(crate) async fn sync_call_rejection(&self, rfc724_mid: &str) -> Result<()> { pub(crate) async fn sync_call_rejection(&self, rfc724_mid: &str) -> Result<()> {
if let Some((msg_id, _)) = rfc724_mid_exists(self, rfc724_mid).await? { if let Some((msg_id, _)) = rfc724_mid_exists(self, rfc724_mid).await? {
let call = self.load_call_by_id(msg_id).await?;
call.update_text(self, "Call ended").await?;
self.emit_event(EventType::CallEnded { msg_id }); self.emit_event(EventType::CallEnded { msg_id });
self.emit_msgs_changed(call.msg.chat_id, msg_id);
} }
Ok(()) Ok(())
} }

View File

@@ -6,6 +6,7 @@ struct CallSetup {
pub alice: TestContext, pub alice: TestContext,
pub alice2: TestContext, pub alice2: TestContext,
pub alice_call: Message, pub alice_call: Message,
pub alice2_call: Message,
pub bob: TestContext, pub bob: TestContext,
pub bob2: TestContext, pub bob2: TestContext,
pub bob_call: Message, pub bob_call: Message,
@@ -61,6 +62,7 @@ async fn setup_call() -> Result<CallSetup> {
alice, alice,
alice2, alice2,
alice_call, alice_call,
alice2_call,
bob, bob,
bob2, bob2,
bob_call, bob_call,
@@ -73,13 +75,14 @@ async fn accept_call() -> Result<CallSetup> {
alice, alice,
alice2, alice2,
alice_call, alice_call,
alice2_call,
bob, bob,
bob2, bob2,
bob_call, bob_call,
bob2_call, bob2_call,
} = setup_call().await?; } = setup_call().await?;
// Bob accepts the incoming call, this does not add an additional message to the chat // Bob accepts the incoming call
bob.accept_incoming_call(bob_call.id, "accepted_info".to_string()) bob.accept_incoming_call(bob_call.id, "accepted_info".to_string())
.await?; .await?;
bob.evtracker bob.evtracker
@@ -91,9 +94,11 @@ async fn accept_call() -> Result<CallSetup> {
assert_eq!(info.place_call_info, "place_info"); assert_eq!(info.place_call_info, "place_info");
assert_eq!(info.accept_call_info, "accepted_info"); assert_eq!(info.accept_call_info, "accepted_info");
let bob_accept_msg = bob2.recv_msg(&sent2).await; bob2.recv_msg_trash(&sent2).await;
assert!(bob_accept_msg.is_info()); assert_eq!(
assert_eq!(bob_accept_msg.get_info_type(), SystemMessage::CallAccepted); Message::load_from_db(&bob, bob_call.id).await?.text,
"Call accepted"
);
bob2.evtracker bob2.evtracker
.get_matching(|evt| matches!(evt, EventType::IncomingCallAccepted { .. })) .get_matching(|evt| matches!(evt, EventType::IncomingCallAccepted { .. }))
.await; .await;
@@ -101,11 +106,10 @@ async fn accept_call() -> Result<CallSetup> {
assert!(!info.is_accepted); // "accepted" is only true on the device that does the call assert!(!info.is_accepted); // "accepted" is only true on the device that does the call
// Alice receives the acceptance message // Alice receives the acceptance message
let alice_accept_msg = alice.recv_msg(&sent2).await; alice.recv_msg_trash(&sent2).await;
assert!(alice_accept_msg.is_info());
assert_eq!( assert_eq!(
alice_accept_msg.get_info_type(), Message::load_from_db(&alice, alice_call.id).await?.text,
SystemMessage::CallAccepted "Call accepted"
); );
alice alice
.evtracker .evtracker
@@ -116,11 +120,10 @@ async fn accept_call() -> Result<CallSetup> {
assert_eq!(info.place_call_info, "place_info"); assert_eq!(info.place_call_info, "place_info");
assert_eq!(info.accept_call_info, "accepted_info"); assert_eq!(info.accept_call_info, "accepted_info");
let alice2_accept_msg = alice2.recv_msg(&sent2).await; alice2.recv_msg_trash(&sent2).await;
assert!(alice2_accept_msg.is_info());
assert_eq!( assert_eq!(
alice2_accept_msg.get_info_type(), Message::load_from_db(&alice2, alice2_call.id).await?.text,
SystemMessage::CallAccepted "Call accepted"
); );
alice2 alice2
.evtracker .evtracker
@@ -131,6 +134,7 @@ async fn accept_call() -> Result<CallSetup> {
alice, alice,
alice2, alice2,
alice_call, alice_call,
alice2_call,
bob, bob,
bob2, bob2,
bob_call, bob_call,
@@ -138,9 +142,9 @@ async fn accept_call() -> Result<CallSetup> {
}) })
} }
fn assert_is_call_ended_info_msg(msg: Message) { async fn assert_is_call_ended(t: &TestContext, call_id: MsgId) -> Result<()> {
assert!(msg.is_info()); assert_eq!(Message::load_from_db(t, call_id).await?.text, "Call ended");
assert_eq!(msg.get_info_type(), SystemMessage::CallEnded); Ok(())
} }
#[tokio::test(flavor = "multi_thread", worker_threads = 2)] #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -148,36 +152,40 @@ async fn test_accept_call_callee_ends() -> Result<()> {
// Alice calls Bob, Bob accepts // Alice calls Bob, Bob accepts
let CallSetup { let CallSetup {
alice, alice,
alice_call,
alice2, alice2,
alice2_call,
bob, bob,
bob2, bob2,
bob_call, bob_call,
bob2_call,
.. ..
} = accept_call().await?; } = accept_call().await?;
// Bob has accepted the call and also ends it // Bob has accepted the call and also ends it
bob.end_call(bob_call.id).await?; bob.end_call(bob_call.id).await?;
assert_is_call_ended(&bob, bob_call.id).await?;
bob.evtracker bob.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. })) .get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
.await; .await;
let sent3 = bob.pop_sent_msg().await; let sent3 = bob.pop_sent_msg().await;
let bob2_end_call_msg = bob2.recv_msg(&sent3).await; bob2.recv_msg_trash(&sent3).await;
assert_is_call_ended_info_msg(bob2_end_call_msg); assert_is_call_ended(&bob2, bob2_call.id).await?;
bob2.evtracker bob2.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. })) .get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
.await; .await;
// Alice receives the ending message // Alice receives the ending message
let alice_end_call_msg = alice.recv_msg(&sent3).await; alice.recv_msg_trash(&sent3).await;
assert_is_call_ended_info_msg(alice_end_call_msg); assert_is_call_ended(&alice, alice_call.id).await?;
alice alice
.evtracker .evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. })) .get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
.await; .await;
let alice2_end_call_msg = alice2.recv_msg(&sent3).await; alice2.recv_msg_trash(&sent3).await;
assert_is_call_ended_info_msg(alice2_end_call_msg); assert_is_call_ended(&alice2, alice2_call.id).await?;
alice2 alice2
.evtracker .evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. })) .get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
@@ -192,9 +200,11 @@ async fn test_accept_call_caller_ends() -> Result<()> {
let CallSetup { let CallSetup {
alice, alice,
alice2, alice2,
alice2_call,
bob, bob,
bob2, bob2,
bob_call, bob_call,
bob2_call,
.. ..
} = accept_call().await?; } = accept_call().await?;
@@ -206,22 +216,22 @@ async fn test_accept_call_caller_ends() -> Result<()> {
.await; .await;
let sent3 = alice.pop_sent_msg().await; let sent3 = alice.pop_sent_msg().await;
let alice2_end_call_msg = alice2.recv_msg(&sent3).await; alice2.recv_msg_trash(&sent3).await;
assert_is_call_ended_info_msg(alice2_end_call_msg); assert_is_call_ended(&alice2, alice2_call.id).await?;
alice2 alice2
.evtracker .evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. })) .get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
.await; .await;
// Bob receives the ending message // Bob receives the ending message
let bob_end_call_msg = bob.recv_msg(&sent3).await; bob.recv_msg_trash(&sent3).await;
assert_is_call_ended_info_msg(bob_end_call_msg); assert_is_call_ended(&bob, bob_call.id).await?;
bob.evtracker bob.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. })) .get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
.await; .await;
let bob2_end_call_msg = bob2.recv_msg(&sent3).await; bob2.recv_msg_trash(&sent3).await;
assert_is_call_ended_info_msg(bob2_end_call_msg); assert_is_call_ended(&bob2, bob2_call.id).await?;
bob2.evtracker bob2.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. })) .get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
.await; .await;
@@ -236,6 +246,7 @@ async fn test_callee_rejects_call() -> Result<()> {
bob, bob,
bob2, bob2,
bob_call, bob_call,
bob2_call,
.. ..
} = setup_call().await?; } = setup_call().await?;
@@ -243,11 +254,13 @@ async fn test_callee_rejects_call() -> Result<()> {
// To protect Bob's privacy, no message is sent to Alice (who will time out). // To protect Bob's privacy, no message is sent to Alice (who will time out).
// To let Bob close the call window on all devices, a sync message is used instead. // To let Bob close the call window on all devices, a sync message is used instead.
bob.end_call(bob_call.id).await?; bob.end_call(bob_call.id).await?;
assert_is_call_ended(&bob, bob_call.id).await?;
bob.evtracker bob.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. })) .get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
.await; .await;
sync(&bob, &bob2).await; sync(&bob, &bob2).await;
assert_is_call_ended(&bob2, bob2_call.id).await?;
bob2.evtracker bob2.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. })) .get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
.await; .await;
@@ -262,35 +275,39 @@ async fn test_caller_cancels_call() -> Result<()> {
alice, alice,
alice2, alice2,
alice_call, alice_call,
alice2_call,
bob, bob,
bob2, bob2,
bob_call,
bob2_call,
.. ..
} = setup_call().await?; } = setup_call().await?;
// Alice changes their mind before Bob picks up // Alice changes their mind before Bob picks up
alice.end_call(alice_call.id).await?; alice.end_call(alice_call.id).await?;
assert_is_call_ended(&alice, alice_call.id).await?;
alice alice
.evtracker .evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. })) .get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
.await; .await;
let sent3 = alice.pop_sent_msg().await; let sent3 = alice.pop_sent_msg().await;
let alice2_call_ended_msg = alice2.recv_msg(&sent3).await; alice2.recv_msg_trash(&sent3).await;
assert_is_call_ended_info_msg(alice2_call_ended_msg); assert_is_call_ended(&alice2, alice2_call.id).await?;
alice2 alice2
.evtracker .evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. })) .get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
.await; .await;
// Bob receives the ending message // Bob receives the ending message
let bob_call_ended_msg = bob.recv_msg(&sent3).await; bob.recv_msg_trash(&sent3).await;
assert_is_call_ended_info_msg(bob_call_ended_msg); assert_is_call_ended(&bob, bob_call.id).await?;
bob.evtracker bob.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. })) .get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
.await; .await;
let bob2_call_ended_msg = bob2.recv_msg(&sent3).await; bob2.recv_msg_trash(&sent3).await;
assert_is_call_ended_info_msg(bob2_call_ended_msg); assert_is_call_ended(&bob2, bob2_call.id).await?;
bob2.evtracker bob2.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. })) .get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
.await; .await;

View File

@@ -1153,6 +1153,11 @@ async fn decide_chat_assignment(
{ {
info!(context, "Chat edit/delete/iroh/sync message (TRASH)."); info!(context, "Chat edit/delete/iroh/sync message (TRASH).");
true true
} else if mime_parser.is_system_message == SystemMessage::CallAccepted
|| mime_parser.is_system_message == SystemMessage::CallEnded
{
info!(context, "Call state changed (TRASH).");
true
} else if mime_parser.decrypting_failed && !mime_parser.incoming { } else if mime_parser.decrypting_failed && !mime_parser.incoming {
// Outgoing undecryptable message. // Outgoing undecryptable message.
let last_time = context let last_time = context