Skip to content

Commit 93fda39

Browse files
committed
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.
1 parent ef718bb commit 93fda39

6 files changed

Lines changed: 194 additions & 51 deletions

File tree

deltachat-ffi/deltachat.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6701,6 +6701,16 @@ void dc_event_unref(dc_event_t* event);
67016701
*/
67026702
#define DC_EVENT_CALL_ENDED 2580
67036703

6704+
/**
6705+
* An incoming call was missed. Only emitted if the caller is allowed to call us. This happens when:
6706+
* - A call timed out (not accepted by us on time).
6707+
* - A call was canceled by the caller.
6708+
* - A stale call message was received, i.e. it is older than the timeout.
6709+
*
6710+
* This should trigger a UI notification.
6711+
*/
6712+
#define DC_EVENT_CALL_MISSED 2590
6713+
67046714
/**
67056715
* Transport relay added/deleted or default has changed.
67066716
* UI should update the list.

deltachat-ffi/src/lib.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -556,6 +556,7 @@ pub unsafe extern "C" fn dc_event_get_id(event: *mut dc_event_t) -> libc::c_int
556556
EventType::IncomingCallAccepted { .. } => 2560,
557557
EventType::OutgoingCallAccepted { .. } => 2570,
558558
EventType::CallEnded { .. } => 2580,
559+
EventType::CallMissed { .. } => 2590,
559560
EventType::TransportsModified => 2600,
560561
#[allow(unreachable_patterns)]
561562
#[cfg(test)]
@@ -626,6 +627,7 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
626627
| EventType::IncomingCallAccepted { msg_id, .. }
627628
| EventType::OutgoingCallAccepted { msg_id, .. }
628629
| EventType::CallEnded { msg_id, .. } => msg_id.to_u32() as libc::c_int,
630+
EventType::CallMissed { msg_id, .. } => msg_id.to_u32() as libc::c_int,
629631
EventType::ChatlistItemChanged { chat_id } => {
630632
chat_id.unwrap_or_default().to_u32() as libc::c_int
631633
}
@@ -679,6 +681,7 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
679681
| EventType::WebxdcRealtimeAdvertisementReceived { .. }
680682
| EventType::OutgoingCallAccepted { .. }
681683
| EventType::CallEnded { .. }
684+
| EventType::CallMissed { .. }
682685
| EventType::EventChannelOverflow { .. }
683686
| EventType::TransportsModified => 0,
684687
EventType::MsgsChanged { msg_id, .. }
@@ -796,7 +799,9 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
796799
let data2 = accept_call_info.to_c_string().unwrap_or_default();
797800
data2.into_raw()
798801
}
799-
EventType::CallEnded { .. } | EventType::EventChannelOverflow { .. } => ptr::null_mut(),
802+
EventType::CallEnded { .. }
803+
| EventType::CallMissed { .. }
804+
| EventType::EventChannelOverflow { .. } => ptr::null_mut(),
800805
EventType::ConfigureProgress { comment, .. } => {
801806
if let Some(comment) = comment {
802807
comment.to_c_string().unwrap_or_default().into_raw()

deltachat-jsonrpc/src/api/types/events.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -463,6 +463,14 @@ pub enum EventType {
463463
chat_id: u32,
464464
},
465465

466+
/// Call missed.
467+
CallMissed {
468+
/// ID of the info message referring to the call.
469+
msg_id: u32,
470+
/// ID of the chat which the message belongs to.
471+
chat_id: u32,
472+
},
473+
466474
/// One or more transports has changed.
467475
///
468476
/// UI should update the list.
@@ -658,6 +666,10 @@ impl From<CoreEventType> for EventType {
658666
msg_id: msg_id.to_u32(),
659667
chat_id: chat_id.to_u32(),
660668
},
669+
CoreEventType::CallMissed { msg_id, chat_id } => CallMissed {
670+
msg_id: msg_id.to_u32(),
671+
chat_id: chat_id.to_u32(),
672+
},
661673
CoreEventType::TransportsModified => TransportsModified,
662674

663675
#[allow(unreachable_patterns)]

src/calls.rs

Lines changed: 62 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -218,10 +218,11 @@ impl Context {
218218

219219
let wait = RINGING_SECONDS;
220220
let context = self.get_weak_context();
221-
task::spawn(Context::emit_end_call_if_unaccepted(
221+
task::spawn(Context::finalize_call_if_unaccepted(
222222
context,
223223
wait.try_into()?,
224224
call.id,
225+
true, // Doesn't matter for outgoing calls
225226
));
226227

227228
Ok(call.id)
@@ -314,39 +315,67 @@ impl Context {
314315
Ok(())
315316
}
316317

317-
async fn emit_end_call_if_unaccepted(
318+
async fn finalize_call_if_unaccepted(
318319
context: WeakContext,
319320
wait: u64,
320321
call_id: MsgId,
322+
can_call_me: bool,
321323
) -> Result<()> {
322324
sleep(Duration::from_secs(wait)).await;
323325
let context = context.upgrade()?;
324326
let Some(mut call) = context.load_call_by_id(call_id).await? else {
325327
warn!(
326328
context,
327-
"emit_end_call_if_unaccepted is called with {call_id} which does not refer to a call."
329+
"finalize_call_if_unaccepted is called with {call_id} which does not refer to a call."
328330
);
329331
return Ok(());
330332
};
331333
if !call.is_accepted() && !call.is_ended() {
334+
let (msg_id, chat_id) = (call_id, call.msg.chat_id);
332335
if call.is_incoming() {
333336
call.mark_as_canceled(&context).await?;
334337
let missed_call_str = stock_str::missed_call(&context);
335338
call.update_text(&context, &missed_call_str).await?;
339+
if can_call_me {
340+
context.emit_event(EventType::CallMissed { msg_id, chat_id });
341+
}
336342
} else {
337343
call.mark_as_ended(&context).await?;
338344
let canceled_call_str = stock_str::canceled_call(&context);
339345
call.update_text(&context, &canceled_call_str).await?;
340346
}
347+
if can_call_me {
348+
context.emit_event(EventType::CallEnded { msg_id, chat_id });
349+
}
341350
context.emit_msgs_changed(call.msg.chat_id, call_id);
342-
context.emit_event(EventType::CallEnded {
343-
msg_id: call.msg.id,
344-
chat_id: call.msg.chat_id,
345-
});
346351
}
347352
Ok(())
348353
}
349354

355+
async fn can_call_me(&self, from_id: ContactId) -> Result<bool> {
356+
Ok(match who_can_call_me(self).await? {
357+
WhoCanCallMe::Contacts => ChatIdBlocked::lookup_by_contact(self, from_id)
358+
.await?
359+
.is_some_and(|chat_id_blocked| {
360+
match chat_id_blocked.blocked {
361+
Blocked::Not => true,
362+
Blocked::Yes | Blocked::Request => {
363+
// Do not notify about incoming calls
364+
// from contact requests and blocked contacts.
365+
//
366+
// User can still access the call and accept it
367+
// via the chat in case of contact requests.
368+
false
369+
}
370+
}
371+
}),
372+
WhoCanCallMe::Everybody => ChatIdBlocked::lookup_by_contact(self, from_id)
373+
.await?
374+
.is_none_or(|chat_id_blocked| chat_id_blocked.blocked != Blocked::Yes),
375+
WhoCanCallMe::Nobody => false,
376+
})
377+
}
378+
350379
pub(crate) async fn handle_call_msg(
351380
&self,
352381
call_id: MsgId,
@@ -360,50 +389,33 @@ impl Context {
360389
};
361390

362391
if call.is_incoming() {
363-
if call.is_stale() {
364-
let missed_call_str = stock_str::missed_call(self);
365-
call.update_text(self, &missed_call_str).await?;
366-
self.emit_incoming_msg(call.msg.chat_id, call_id); // notify missed call
392+
let call_str = match call.is_stale() {
393+
true => stock_str::missed_call(self),
394+
false => stock_str::incoming_call(self, call.has_video_initially()),
395+
};
396+
call.update_text(self, &call_str).await?;
397+
let (msg_id, chat_id) = (call_id, call.msg.chat_id);
398+
let can_call_me = self.can_call_me(from_id).await?;
399+
if !can_call_me {
400+
} else if call.is_stale() {
401+
self.emit_event(EventType::CallMissed { msg_id, chat_id });
367402
} else {
368-
let incoming_call_str =
369-
stock_str::incoming_call(self, call.has_video_initially());
370-
call.update_text(self, &incoming_call_str).await?;
371-
self.emit_msgs_changed(call.msg.chat_id, call_id); // ringing calls are not additionally notified
372-
let can_call_me = match who_can_call_me(self).await? {
373-
WhoCanCallMe::Contacts => ChatIdBlocked::lookup_by_contact(self, from_id)
374-
.await?
375-
.is_some_and(|chat_id_blocked| {
376-
match chat_id_blocked.blocked {
377-
Blocked::Not => true,
378-
Blocked::Yes | Blocked::Request => {
379-
// Do not notify about incoming calls
380-
// from contact requests and blocked contacts.
381-
//
382-
// User can still access the call and accept it
383-
// via the chat in case of contact requests.
384-
false
385-
}
386-
}
387-
}),
388-
WhoCanCallMe::Everybody => ChatIdBlocked::lookup_by_contact(self, from_id)
389-
.await?
390-
.is_none_or(|chat_id_blocked| chat_id_blocked.blocked != Blocked::Yes),
391-
WhoCanCallMe::Nobody => false,
392-
};
393-
if can_call_me {
394-
self.emit_event(EventType::IncomingCall {
395-
msg_id: call.msg.id,
396-
chat_id: call.msg.chat_id,
397-
place_call_info: call.place_call_info.to_string(),
398-
has_video: call.has_video_initially(),
399-
});
400-
}
403+
self.emit_event(EventType::IncomingCall {
404+
msg_id,
405+
chat_id,
406+
place_call_info: call.place_call_info.to_string(),
407+
has_video: call.has_video_initially(),
408+
});
409+
}
410+
self.emit_msgs_changed(chat_id, msg_id);
411+
if !call.is_stale() {
401412
let wait = call.remaining_ring_seconds();
402413
let context = self.get_weak_context();
403-
task::spawn(Context::emit_end_call_if_unaccepted(
414+
task::spawn(Context::finalize_call_if_unaccepted(
404415
context,
405416
wait.try_into()?,
406417
call.msg.id,
418+
can_call_me,
407419
));
408420
}
409421
} else {
@@ -455,6 +467,7 @@ impl Context {
455467
return Ok(());
456468
}
457469

470+
let (msg_id, chat_id) = (call_id, call.msg.chat_id);
458471
if !call.is_accepted() {
459472
if call.is_incoming() {
460473
if from_id == ContactId::SELF {
@@ -465,6 +478,9 @@ impl Context {
465478
call.mark_as_canceled(self).await?;
466479
let missed_call_str = stock_str::missed_call(self);
467480
call.update_text(self, &missed_call_str).await?;
481+
if self.can_call_me(from_id).await? {
482+
self.emit_event(EventType::CallMissed { msg_id, chat_id });
483+
}
468484
}
469485
} else {
470486
// outgoing
@@ -482,12 +498,8 @@ impl Context {
482498
call.mark_as_ended(self).await?;
483499
call.update_text_duration(self).await?;
484500
}
485-
501+
self.emit_event(EventType::CallEnded { msg_id, chat_id });
486502
self.emit_msgs_changed(call.msg.chat_id, call_id);
487-
self.emit_event(EventType::CallEnded {
488-
msg_id: call.msg.id,
489-
chat_id: call.msg.chat_id,
490-
});
491503
}
492504
_ => {}
493505
}

src/calls/calls_tests.rs

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use crate::constants::DC_CHAT_ID_TRASH;
55
use crate::message::MessageState;
66
use crate::receive_imf::receive_imf;
77
use crate::test_utils::{TestContext, TestContextManager};
8+
use crate::tools::SystemTime;
89

910
struct CallSetup {
1011
pub alice: TestContext,
@@ -490,6 +491,9 @@ async fn test_caller_cancels_call() -> Result<()> {
490491
// Bob receives the ending message
491492
bob.recv_msg_trash(&sent3).await;
492493
assert_text(&bob, bob_call.id, "Missed call").await?;
494+
bob.evtracker
495+
.get_matching(|evt| matches!(evt, EventType::CallMissed { .. }))
496+
.await;
493497
bob.evtracker
494498
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
495499
.await;
@@ -502,6 +506,9 @@ async fn test_caller_cancels_call() -> Result<()> {
502506

503507
bob2.recv_msg_trash(&sent3).await;
504508
assert_text(&bob2, bob2_call.id, "Missed call").await?;
509+
bob2.evtracker
510+
.get_matching(|evt| matches!(evt, EventType::CallMissed { .. }))
511+
.await;
505512
bob2.evtracker
506513
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
507514
.await;
@@ -510,6 +517,95 @@ async fn test_caller_cancels_call() -> Result<()> {
510517
Ok(())
511518
}
512519

520+
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
521+
async fn test_stale_call() -> Result<()> {
522+
let mut tcm = TestContextManager::new();
523+
for accepted in [false, true] {
524+
let alice = &tcm.alice().await;
525+
let bob = &tcm.bob().await;
526+
527+
info!(bob, "Alice is accepted: {accepted}.");
528+
if accepted {
529+
bob.create_chat(alice).await;
530+
}
531+
let alice_chat = alice.create_chat(bob).await;
532+
alice
533+
.place_outgoing_call(alice_chat.id, PLACE_INFO.to_string(), true)
534+
.await?;
535+
let sent1 = alice.pop_sent_msg().await;
536+
537+
SystemTime::shift(Duration::from_secs(3600));
538+
let bob_call = bob.recv_msg(&sent1).await;
539+
let EventType::MsgsChanged { msg_id, chat_id } = bob
540+
.evtracker
541+
.get_matching(|evt| {
542+
matches!(
543+
evt,
544+
EventType::MsgsChanged { .. }
545+
| EventType::CallMissed { .. }
546+
| EventType::CallEnded { .. }
547+
)
548+
})
549+
.await
550+
else {
551+
unreachable!();
552+
};
553+
assert_eq!(chat_id, bob_call.chat_id);
554+
let msg = Message::load_from_db(bob, msg_id).await?;
555+
assert_eq!(msg.text, stock_str::messages_e2ee_info_msg(bob).await);
556+
if accepted {
557+
let EventType::CallMissed { msg_id, chat_id } = bob
558+
.evtracker
559+
.get_matching(|evt| {
560+
matches!(
561+
evt,
562+
EventType::CallMissed { .. } | EventType::CallEnded { .. }
563+
)
564+
})
565+
.await
566+
else {
567+
unreachable!();
568+
};
569+
assert_eq!(msg_id, bob_call.id);
570+
assert_eq!(chat_id, bob_call.chat_id);
571+
}
572+
let EventType::MsgsChanged { msg_id, chat_id } = bob
573+
.evtracker
574+
.get_matching(|evt| {
575+
matches!(
576+
evt,
577+
EventType::MsgsChanged { .. }
578+
| EventType::CallMissed { .. }
579+
| EventType::CallEnded { .. }
580+
)
581+
})
582+
.await
583+
else {
584+
unreachable!();
585+
};
586+
assert_eq!(msg_id, bob_call.id);
587+
assert_eq!(chat_id, bob_call.chat_id);
588+
let evt = bob
589+
.evtracker
590+
.get_matching_opt(bob, |evt| {
591+
matches!(
592+
evt,
593+
EventType::CallMissed { .. } | EventType::CallEnded { .. }
594+
)
595+
})
596+
.await;
597+
assert!(evt.is_none());
598+
assert_text(bob, bob_call.id, "Missed call").await?;
599+
assert_eq!(call_state(bob, bob_call.id).await?, CallState::Missed);
600+
601+
// Test that message summary says it is a missed call.
602+
let bob_call_msg = Message::load_from_db(bob, bob_call.id).await?;
603+
let summary = bob_call_msg.get_summary(bob, None).await?;
604+
assert_eq!(summary.text, "🎥 Missed call");
605+
}
606+
Ok(())
607+
}
608+
513609
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
514610
async fn test_is_stale_call() -> Result<()> {
515611
// a call started now is not stale

0 commit comments

Comments
 (0)