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

View File

@@ -6,6 +6,7 @@ struct CallSetup {
pub alice: TestContext,
pub alice2: TestContext,
pub alice_call: Message,
pub alice2_call: Message,
pub bob: TestContext,
pub bob2: TestContext,
pub bob_call: Message,
@@ -61,6 +62,7 @@ async fn setup_call() -> Result<CallSetup> {
alice,
alice2,
alice_call,
alice2_call,
bob,
bob2,
bob_call,
@@ -73,13 +75,14 @@ async fn accept_call() -> Result<CallSetup> {
alice,
alice2,
alice_call,
alice2_call,
bob,
bob2,
bob_call,
bob2_call,
} = 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())
.await?;
bob.evtracker
@@ -91,9 +94,11 @@ async fn accept_call() -> Result<CallSetup> {
assert_eq!(info.place_call_info, "place_info");
assert_eq!(info.accept_call_info, "accepted_info");
let bob_accept_msg = bob2.recv_msg(&sent2).await;
assert!(bob_accept_msg.is_info());
assert_eq!(bob_accept_msg.get_info_type(), SystemMessage::CallAccepted);
bob2.recv_msg_trash(&sent2).await;
assert_eq!(
Message::load_from_db(&bob, bob_call.id).await?.text,
"Call accepted"
);
bob2.evtracker
.get_matching(|evt| matches!(evt, EventType::IncomingCallAccepted { .. }))
.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
// Alice receives the acceptance message
let alice_accept_msg = alice.recv_msg(&sent2).await;
assert!(alice_accept_msg.is_info());
alice.recv_msg_trash(&sent2).await;
assert_eq!(
alice_accept_msg.get_info_type(),
SystemMessage::CallAccepted
Message::load_from_db(&alice, alice_call.id).await?.text,
"Call accepted"
);
alice
.evtracker
@@ -116,11 +120,10 @@ async fn accept_call() -> Result<CallSetup> {
assert_eq!(info.place_call_info, "place_info");
assert_eq!(info.accept_call_info, "accepted_info");
let alice2_accept_msg = alice2.recv_msg(&sent2).await;
assert!(alice2_accept_msg.is_info());
alice2.recv_msg_trash(&sent2).await;
assert_eq!(
alice2_accept_msg.get_info_type(),
SystemMessage::CallAccepted
Message::load_from_db(&alice2, alice2_call.id).await?.text,
"Call accepted"
);
alice2
.evtracker
@@ -131,6 +134,7 @@ async fn accept_call() -> Result<CallSetup> {
alice,
alice2,
alice_call,
alice2_call,
bob,
bob2,
bob_call,
@@ -138,9 +142,9 @@ async fn accept_call() -> Result<CallSetup> {
})
}
fn assert_is_call_ended_info_msg(msg: Message) {
assert!(msg.is_info());
assert_eq!(msg.get_info_type(), SystemMessage::CallEnded);
async fn assert_is_call_ended(t: &TestContext, call_id: MsgId) -> Result<()> {
assert_eq!(Message::load_from_db(t, call_id).await?.text, "Call ended");
Ok(())
}
#[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
let CallSetup {
alice,
alice_call,
alice2,
alice2_call,
bob,
bob2,
bob_call,
bob2_call,
..
} = accept_call().await?;
// Bob has accepted the call and also ends it
bob.end_call(bob_call.id).await?;
assert_is_call_ended(&bob, bob_call.id).await?;
bob.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
.await;
let sent3 = bob.pop_sent_msg().await;
let bob2_end_call_msg = bob2.recv_msg(&sent3).await;
assert_is_call_ended_info_msg(bob2_end_call_msg);
bob2.recv_msg_trash(&sent3).await;
assert_is_call_ended(&bob2, bob2_call.id).await?;
bob2.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
.await;
// Alice receives the ending message
let alice_end_call_msg = alice.recv_msg(&sent3).await;
assert_is_call_ended_info_msg(alice_end_call_msg);
alice.recv_msg_trash(&sent3).await;
assert_is_call_ended(&alice, alice_call.id).await?;
alice
.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
.await;
let alice2_end_call_msg = alice2.recv_msg(&sent3).await;
assert_is_call_ended_info_msg(alice2_end_call_msg);
alice2.recv_msg_trash(&sent3).await;
assert_is_call_ended(&alice2, alice2_call.id).await?;
alice2
.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
@@ -192,9 +200,11 @@ async fn test_accept_call_caller_ends() -> Result<()> {
let CallSetup {
alice,
alice2,
alice2_call,
bob,
bob2,
bob_call,
bob2_call,
..
} = accept_call().await?;
@@ -206,22 +216,22 @@ async fn test_accept_call_caller_ends() -> Result<()> {
.await;
let sent3 = alice.pop_sent_msg().await;
let alice2_end_call_msg = alice2.recv_msg(&sent3).await;
assert_is_call_ended_info_msg(alice2_end_call_msg);
alice2.recv_msg_trash(&sent3).await;
assert_is_call_ended(&alice2, alice2_call.id).await?;
alice2
.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
.await;
// Bob receives the ending message
let bob_end_call_msg = bob.recv_msg(&sent3).await;
assert_is_call_ended_info_msg(bob_end_call_msg);
bob.recv_msg_trash(&sent3).await;
assert_is_call_ended(&bob, bob_call.id).await?;
bob.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
.await;
let bob2_end_call_msg = bob2.recv_msg(&sent3).await;
assert_is_call_ended_info_msg(bob2_end_call_msg);
bob2.recv_msg_trash(&sent3).await;
assert_is_call_ended(&bob2, bob2_call.id).await?;
bob2.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
.await;
@@ -236,6 +246,7 @@ async fn test_callee_rejects_call() -> Result<()> {
bob,
bob2,
bob_call,
bob2_call,
..
} = 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 let Bob close the call window on all devices, a sync message is used instead.
bob.end_call(bob_call.id).await?;
assert_is_call_ended(&bob, bob_call.id).await?;
bob.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
.await;
sync(&bob, &bob2).await;
assert_is_call_ended(&bob2, bob2_call.id).await?;
bob2.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
.await;
@@ -262,35 +275,39 @@ async fn test_caller_cancels_call() -> Result<()> {
alice,
alice2,
alice_call,
alice2_call,
bob,
bob2,
bob_call,
bob2_call,
..
} = setup_call().await?;
// Alice changes their mind before Bob picks up
alice.end_call(alice_call.id).await?;
assert_is_call_ended(&alice, alice_call.id).await?;
alice
.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
.await;
let sent3 = alice.pop_sent_msg().await;
let alice2_call_ended_msg = alice2.recv_msg(&sent3).await;
assert_is_call_ended_info_msg(alice2_call_ended_msg);
alice2.recv_msg_trash(&sent3).await;
assert_is_call_ended(&alice2, alice2_call.id).await?;
alice2
.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
.await;
// Bob receives the ending message
let bob_call_ended_msg = bob.recv_msg(&sent3).await;
assert_is_call_ended_info_msg(bob_call_ended_msg);
bob.recv_msg_trash(&sent3).await;
assert_is_call_ended(&bob, bob_call.id).await?;
bob.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
.await;
let bob2_call_ended_msg = bob2.recv_msg(&sent3).await;
assert_is_call_ended_info_msg(bob2_call_ended_msg);
bob2.recv_msg_trash(&sent3).await;
assert_is_call_ended(&bob2, bob2_call.id).await?;
bob2.evtracker
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
.await;

View File

@@ -1153,6 +1153,11 @@ async fn decide_chat_assignment(
{
info!(context, "Chat edit/delete/iroh/sync message (TRASH).");
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 {
// Outgoing undecryptable message.
let last_time = context