mirror of
https://github.com/chatmail/core.git
synced 2026-04-23 00:16:34 +03:00
feat: Add EventType::CallMissed and emit it for missed calls (#7840)
Before, only `CallEnded` was emitted for missed calls, or, if a call arrives already being stale, `IncomingMsg`. Now: - `CallMissed` is emitted in addition to `CallEnded`. - `IncomingMsg` is replaced with `CallMissed` for stale calls. Having only one event type for missed calls should simplify handling them in the apps. This doesn't emit `CallMissed` for those who aren't allowed to call us. Also, don't emit `CallEnded` if the caller isn't allowed to call us and the call wasn't accepted, as there's no previous `IncomingCall` event in this case.
This commit is contained in:
@@ -6701,6 +6701,16 @@ void dc_event_unref(dc_event_t* event);
|
||||
*/
|
||||
#define DC_EVENT_CALL_ENDED 2580
|
||||
|
||||
/**
|
||||
* An incoming call was missed. Only emitted if the caller is allowed to call us. This happens when:
|
||||
* - A call timed out (not accepted by us on time).
|
||||
* - A call was canceled by the caller.
|
||||
* - A stale call message was received, i.e. it is older than the timeout.
|
||||
*
|
||||
* This should trigger a UI notification.
|
||||
*/
|
||||
#define DC_EVENT_CALL_MISSED 2590
|
||||
|
||||
/**
|
||||
* Transport relay added/deleted or default has changed.
|
||||
* UI should update the list.
|
||||
|
||||
@@ -556,6 +556,7 @@ pub unsafe extern "C" fn dc_event_get_id(event: *mut dc_event_t) -> libc::c_int
|
||||
EventType::IncomingCallAccepted { .. } => 2560,
|
||||
EventType::OutgoingCallAccepted { .. } => 2570,
|
||||
EventType::CallEnded { .. } => 2580,
|
||||
EventType::CallMissed { .. } => 2590,
|
||||
EventType::TransportsModified => 2600,
|
||||
#[allow(unreachable_patterns)]
|
||||
#[cfg(test)]
|
||||
@@ -626,6 +627,7 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
|
||||
| EventType::IncomingCallAccepted { msg_id, .. }
|
||||
| EventType::OutgoingCallAccepted { msg_id, .. }
|
||||
| EventType::CallEnded { msg_id, .. } => msg_id.to_u32() as libc::c_int,
|
||||
EventType::CallMissed { msg_id, .. } => msg_id.to_u32() as libc::c_int,
|
||||
EventType::ChatlistItemChanged { chat_id } => {
|
||||
chat_id.unwrap_or_default().to_u32() as libc::c_int
|
||||
}
|
||||
@@ -679,6 +681,7 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
|
||||
| EventType::WebxdcRealtimeAdvertisementReceived { .. }
|
||||
| EventType::OutgoingCallAccepted { .. }
|
||||
| EventType::CallEnded { .. }
|
||||
| EventType::CallMissed { .. }
|
||||
| EventType::EventChannelOverflow { .. }
|
||||
| EventType::TransportsModified => 0,
|
||||
EventType::MsgsChanged { msg_id, .. }
|
||||
@@ -796,7 +799,9 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
|
||||
let data2 = accept_call_info.to_c_string().unwrap_or_default();
|
||||
data2.into_raw()
|
||||
}
|
||||
EventType::CallEnded { .. } | EventType::EventChannelOverflow { .. } => ptr::null_mut(),
|
||||
EventType::CallEnded { .. }
|
||||
| EventType::CallMissed { .. }
|
||||
| EventType::EventChannelOverflow { .. } => ptr::null_mut(),
|
||||
EventType::ConfigureProgress { comment, .. } => {
|
||||
if let Some(comment) = comment {
|
||||
comment.to_c_string().unwrap_or_default().into_raw()
|
||||
|
||||
@@ -463,6 +463,14 @@ pub enum EventType {
|
||||
chat_id: u32,
|
||||
},
|
||||
|
||||
/// Call missed.
|
||||
CallMissed {
|
||||
/// ID of the info message referring to the call.
|
||||
msg_id: u32,
|
||||
/// ID of the chat which the message belongs to.
|
||||
chat_id: u32,
|
||||
},
|
||||
|
||||
/// One or more transports has changed.
|
||||
///
|
||||
/// UI should update the list.
|
||||
@@ -658,6 +666,10 @@ impl From<CoreEventType> for EventType {
|
||||
msg_id: msg_id.to_u32(),
|
||||
chat_id: chat_id.to_u32(),
|
||||
},
|
||||
CoreEventType::CallMissed { msg_id, chat_id } => CallMissed {
|
||||
msg_id: msg_id.to_u32(),
|
||||
chat_id: chat_id.to_u32(),
|
||||
},
|
||||
CoreEventType::TransportsModified => TransportsModified,
|
||||
|
||||
#[allow(unreachable_patterns)]
|
||||
|
||||
112
src/calls.rs
112
src/calls.rs
@@ -218,10 +218,11 @@ impl Context {
|
||||
|
||||
let wait = RINGING_SECONDS;
|
||||
let context = self.get_weak_context();
|
||||
task::spawn(Context::emit_end_call_if_unaccepted(
|
||||
task::spawn(Context::finalize_call_if_unaccepted(
|
||||
context,
|
||||
wait.try_into()?,
|
||||
call.id,
|
||||
true, // Doesn't matter for outgoing calls
|
||||
));
|
||||
|
||||
Ok(call.id)
|
||||
@@ -314,39 +315,67 @@ impl Context {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn emit_end_call_if_unaccepted(
|
||||
async fn finalize_call_if_unaccepted(
|
||||
context: WeakContext,
|
||||
wait: u64,
|
||||
call_id: MsgId,
|
||||
can_call_me: bool,
|
||||
) -> Result<()> {
|
||||
sleep(Duration::from_secs(wait)).await;
|
||||
let context = context.upgrade()?;
|
||||
let Some(mut call) = context.load_call_by_id(call_id).await? else {
|
||||
warn!(
|
||||
context,
|
||||
"emit_end_call_if_unaccepted is called with {call_id} which does not refer to a call."
|
||||
"finalize_call_if_unaccepted is called with {call_id} which does not refer to a call."
|
||||
);
|
||||
return Ok(());
|
||||
};
|
||||
if !call.is_accepted() && !call.is_ended() {
|
||||
let (msg_id, chat_id) = (call_id, call.msg.chat_id);
|
||||
if call.is_incoming() {
|
||||
call.mark_as_canceled(&context).await?;
|
||||
let missed_call_str = stock_str::missed_call(&context);
|
||||
call.update_text(&context, &missed_call_str).await?;
|
||||
if can_call_me {
|
||||
context.emit_event(EventType::CallMissed { msg_id, chat_id });
|
||||
}
|
||||
} else {
|
||||
call.mark_as_ended(&context).await?;
|
||||
let canceled_call_str = stock_str::canceled_call(&context);
|
||||
call.update_text(&context, &canceled_call_str).await?;
|
||||
}
|
||||
if can_call_me {
|
||||
context.emit_event(EventType::CallEnded { msg_id, chat_id });
|
||||
}
|
||||
context.emit_msgs_changed(call.msg.chat_id, call_id);
|
||||
context.emit_event(EventType::CallEnded {
|
||||
msg_id: call.msg.id,
|
||||
chat_id: call.msg.chat_id,
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn can_call_me(&self, from_id: ContactId) -> Result<bool> {
|
||||
Ok(match who_can_call_me(self).await? {
|
||||
WhoCanCallMe::Contacts => ChatIdBlocked::lookup_by_contact(self, from_id)
|
||||
.await?
|
||||
.is_some_and(|chat_id_blocked| {
|
||||
match chat_id_blocked.blocked {
|
||||
Blocked::Not => true,
|
||||
Blocked::Yes | Blocked::Request => {
|
||||
// Do not notify about incoming calls
|
||||
// from contact requests and blocked contacts.
|
||||
//
|
||||
// User can still access the call and accept it
|
||||
// via the chat in case of contact requests.
|
||||
false
|
||||
}
|
||||
}
|
||||
}),
|
||||
WhoCanCallMe::Everybody => ChatIdBlocked::lookup_by_contact(self, from_id)
|
||||
.await?
|
||||
.is_none_or(|chat_id_blocked| chat_id_blocked.blocked != Blocked::Yes),
|
||||
WhoCanCallMe::Nobody => false,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) async fn handle_call_msg(
|
||||
&self,
|
||||
call_id: MsgId,
|
||||
@@ -360,50 +389,33 @@ impl Context {
|
||||
};
|
||||
|
||||
if call.is_incoming() {
|
||||
if call.is_stale() {
|
||||
let missed_call_str = stock_str::missed_call(self);
|
||||
call.update_text(self, &missed_call_str).await?;
|
||||
self.emit_incoming_msg(call.msg.chat_id, call_id); // notify missed call
|
||||
let call_str = match call.is_stale() {
|
||||
true => stock_str::missed_call(self),
|
||||
false => stock_str::incoming_call(self, call.has_video_initially()),
|
||||
};
|
||||
call.update_text(self, &call_str).await?;
|
||||
let (msg_id, chat_id) = (call_id, call.msg.chat_id);
|
||||
let can_call_me = self.can_call_me(from_id).await?;
|
||||
if !can_call_me {
|
||||
} else if call.is_stale() {
|
||||
self.emit_event(EventType::CallMissed { msg_id, chat_id });
|
||||
} else {
|
||||
let incoming_call_str =
|
||||
stock_str::incoming_call(self, call.has_video_initially());
|
||||
call.update_text(self, &incoming_call_str).await?;
|
||||
self.emit_msgs_changed(call.msg.chat_id, call_id); // ringing calls are not additionally notified
|
||||
let can_call_me = match who_can_call_me(self).await? {
|
||||
WhoCanCallMe::Contacts => ChatIdBlocked::lookup_by_contact(self, from_id)
|
||||
.await?
|
||||
.is_some_and(|chat_id_blocked| {
|
||||
match chat_id_blocked.blocked {
|
||||
Blocked::Not => true,
|
||||
Blocked::Yes | Blocked::Request => {
|
||||
// Do not notify about incoming calls
|
||||
// from contact requests and blocked contacts.
|
||||
//
|
||||
// User can still access the call and accept it
|
||||
// via the chat in case of contact requests.
|
||||
false
|
||||
}
|
||||
}
|
||||
}),
|
||||
WhoCanCallMe::Everybody => ChatIdBlocked::lookup_by_contact(self, from_id)
|
||||
.await?
|
||||
.is_none_or(|chat_id_blocked| chat_id_blocked.blocked != Blocked::Yes),
|
||||
WhoCanCallMe::Nobody => false,
|
||||
};
|
||||
if can_call_me {
|
||||
self.emit_event(EventType::IncomingCall {
|
||||
msg_id: call.msg.id,
|
||||
chat_id: call.msg.chat_id,
|
||||
place_call_info: call.place_call_info.to_string(),
|
||||
has_video: call.has_video_initially(),
|
||||
});
|
||||
}
|
||||
self.emit_event(EventType::IncomingCall {
|
||||
msg_id,
|
||||
chat_id,
|
||||
place_call_info: call.place_call_info.to_string(),
|
||||
has_video: call.has_video_initially(),
|
||||
});
|
||||
}
|
||||
self.emit_msgs_changed(chat_id, msg_id);
|
||||
if !call.is_stale() {
|
||||
let wait = call.remaining_ring_seconds();
|
||||
let context = self.get_weak_context();
|
||||
task::spawn(Context::emit_end_call_if_unaccepted(
|
||||
task::spawn(Context::finalize_call_if_unaccepted(
|
||||
context,
|
||||
wait.try_into()?,
|
||||
call.msg.id,
|
||||
can_call_me,
|
||||
));
|
||||
}
|
||||
} else {
|
||||
@@ -455,6 +467,7 @@ impl Context {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let (msg_id, chat_id) = (call_id, call.msg.chat_id);
|
||||
if !call.is_accepted() {
|
||||
if call.is_incoming() {
|
||||
if from_id == ContactId::SELF {
|
||||
@@ -465,6 +478,9 @@ impl Context {
|
||||
call.mark_as_canceled(self).await?;
|
||||
let missed_call_str = stock_str::missed_call(self);
|
||||
call.update_text(self, &missed_call_str).await?;
|
||||
if self.can_call_me(from_id).await? {
|
||||
self.emit_event(EventType::CallMissed { msg_id, chat_id });
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// outgoing
|
||||
@@ -482,12 +498,8 @@ impl Context {
|
||||
call.mark_as_ended(self).await?;
|
||||
call.update_text_duration(self).await?;
|
||||
}
|
||||
|
||||
self.emit_event(EventType::CallEnded { msg_id, chat_id });
|
||||
self.emit_msgs_changed(call.msg.chat_id, call_id);
|
||||
self.emit_event(EventType::CallEnded {
|
||||
msg_id: call.msg.id,
|
||||
chat_id: call.msg.chat_id,
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ use crate::constants::DC_CHAT_ID_TRASH;
|
||||
use crate::message::MessageState;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::test_utils::{TestContext, TestContextManager};
|
||||
use crate::tools::SystemTime;
|
||||
|
||||
struct CallSetup {
|
||||
pub alice: TestContext,
|
||||
@@ -490,6 +491,9 @@ async fn test_caller_cancels_call() -> Result<()> {
|
||||
// Bob receives the ending message
|
||||
bob.recv_msg_trash(&sent3).await;
|
||||
assert_text(&bob, bob_call.id, "Missed call").await?;
|
||||
bob.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::CallMissed { .. }))
|
||||
.await;
|
||||
bob.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
|
||||
.await;
|
||||
@@ -502,6 +506,9 @@ async fn test_caller_cancels_call() -> Result<()> {
|
||||
|
||||
bob2.recv_msg_trash(&sent3).await;
|
||||
assert_text(&bob2, bob2_call.id, "Missed call").await?;
|
||||
bob2.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::CallMissed { .. }))
|
||||
.await;
|
||||
bob2.evtracker
|
||||
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
|
||||
.await;
|
||||
@@ -510,6 +517,95 @@ async fn test_caller_cancels_call() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_stale_call() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
for accepted in [false, true] {
|
||||
let alice = &tcm.alice().await;
|
||||
let bob = &tcm.bob().await;
|
||||
|
||||
info!(bob, "Alice is accepted: {accepted}.");
|
||||
if accepted {
|
||||
bob.create_chat(alice).await;
|
||||
}
|
||||
let alice_chat = alice.create_chat(bob).await;
|
||||
alice
|
||||
.place_outgoing_call(alice_chat.id, PLACE_INFO.to_string(), true)
|
||||
.await?;
|
||||
let sent1 = alice.pop_sent_msg().await;
|
||||
|
||||
SystemTime::shift(Duration::from_secs(3600));
|
||||
let bob_call = bob.recv_msg(&sent1).await;
|
||||
let EventType::MsgsChanged { msg_id, chat_id } = bob
|
||||
.evtracker
|
||||
.get_matching(|evt| {
|
||||
matches!(
|
||||
evt,
|
||||
EventType::MsgsChanged { .. }
|
||||
| EventType::CallMissed { .. }
|
||||
| EventType::CallEnded { .. }
|
||||
)
|
||||
})
|
||||
.await
|
||||
else {
|
||||
unreachable!();
|
||||
};
|
||||
assert_eq!(chat_id, bob_call.chat_id);
|
||||
let msg = Message::load_from_db(bob, msg_id).await?;
|
||||
assert_eq!(msg.text, stock_str::messages_e2ee_info_msg(bob));
|
||||
if accepted {
|
||||
let EventType::CallMissed { msg_id, chat_id } = bob
|
||||
.evtracker
|
||||
.get_matching(|evt| {
|
||||
matches!(
|
||||
evt,
|
||||
EventType::CallMissed { .. } | EventType::CallEnded { .. }
|
||||
)
|
||||
})
|
||||
.await
|
||||
else {
|
||||
unreachable!();
|
||||
};
|
||||
assert_eq!(msg_id, bob_call.id);
|
||||
assert_eq!(chat_id, bob_call.chat_id);
|
||||
}
|
||||
let EventType::MsgsChanged { msg_id, chat_id } = bob
|
||||
.evtracker
|
||||
.get_matching(|evt| {
|
||||
matches!(
|
||||
evt,
|
||||
EventType::MsgsChanged { .. }
|
||||
| EventType::CallMissed { .. }
|
||||
| EventType::CallEnded { .. }
|
||||
)
|
||||
})
|
||||
.await
|
||||
else {
|
||||
unreachable!();
|
||||
};
|
||||
assert_eq!(msg_id, bob_call.id);
|
||||
assert_eq!(chat_id, bob_call.chat_id);
|
||||
let evt = bob
|
||||
.evtracker
|
||||
.get_matching_opt(bob, |evt| {
|
||||
matches!(
|
||||
evt,
|
||||
EventType::CallMissed { .. } | EventType::CallEnded { .. }
|
||||
)
|
||||
})
|
||||
.await;
|
||||
assert!(evt.is_none());
|
||||
assert_text(bob, bob_call.id, "Missed call").await?;
|
||||
assert_eq!(call_state(bob, bob_call.id).await?, CallState::Missed);
|
||||
|
||||
// Test that message summary says it is a missed call.
|
||||
let bob_call_msg = Message::load_from_db(bob, bob_call.id).await?;
|
||||
let summary = bob_call_msg.get_summary(bob, None).await?;
|
||||
assert_eq!(summary.text, "🎥 Missed call");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_is_stale_call() -> Result<()> {
|
||||
// a call started now is not stale
|
||||
|
||||
@@ -419,6 +419,14 @@ pub enum EventType {
|
||||
chat_id: ChatId,
|
||||
},
|
||||
|
||||
/// Call missed.
|
||||
CallMissed {
|
||||
/// ID of the message referring to the call.
|
||||
msg_id: MsgId,
|
||||
/// ID of the chat which the message belongs to.
|
||||
chat_id: ChatId,
|
||||
},
|
||||
|
||||
/// One or more transports has changed or another transport is primary now.
|
||||
///
|
||||
/// UI should update the list.
|
||||
|
||||
Reference in New Issue
Block a user