Skip to content

Commit 89f988e

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 5f84be7 commit 89f988e

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
@@ -6778,6 +6778,16 @@ void dc_event_unref(dc_event_t* event);
67786778
*/
67796779
#define DC_EVENT_CALL_ENDED 2580
67806780

6781+
/**
6782+
* An incoming call was missed. Only emitted if the caller is allowed to call us. This happens when:
6783+
* - A call timed out (not accepted by us on time).
6784+
* - A call was canceled by the caller.
6785+
* - A stale call message was received, i.e. it is older than the timeout.
6786+
*
6787+
* This should trigger a UI notification.
6788+
*/
6789+
#define DC_EVENT_CALL_MISSED 2590
6790+
67816791
/**
67826792
* Transport relay added/deleted or default has changed.
67836793
* UI should update the list.

deltachat-ffi/src/lib.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -558,6 +558,7 @@ pub unsafe extern "C" fn dc_event_get_id(event: *mut dc_event_t) -> libc::c_int
558558
EventType::IncomingCallAccepted { .. } => 2560,
559559
EventType::OutgoingCallAccepted { .. } => 2570,
560560
EventType::CallEnded { .. } => 2580,
561+
EventType::CallMissed { .. } => 2590,
561562
EventType::TransportsModified => 2600,
562563
#[allow(unreachable_patterns)]
563564
#[cfg(test)]
@@ -628,6 +629,7 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
628629
| EventType::IncomingCallAccepted { msg_id, .. }
629630
| EventType::OutgoingCallAccepted { msg_id, .. }
630631
| EventType::CallEnded { msg_id, .. } => msg_id.to_u32() as libc::c_int,
632+
EventType::CallMissed { msg_id, .. } => msg_id.to_u32() as libc::c_int,
631633
EventType::ChatlistItemChanged { chat_id } => {
632634
chat_id.unwrap_or_default().to_u32() as libc::c_int
633635
}
@@ -682,6 +684,7 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
682684
| EventType::IncomingCallAccepted { .. }
683685
| EventType::OutgoingCallAccepted { .. }
684686
| EventType::CallEnded { .. }
687+
| EventType::CallMissed { .. }
685688
| EventType::EventChannelOverflow { .. }
686689
| EventType::TransportsModified => 0,
687690
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
@@ -461,6 +461,14 @@ pub enum EventType {
461461
chat_id: u32,
462462
},
463463

464+
/// Call missed.
465+
CallMissed {
466+
/// ID of the info message referring to the call.
467+
msg_id: u32,
468+
/// ID of the chat which the message belongs to.
469+
chat_id: u32,
470+
},
471+
464472
/// One or more transports has changed.
465473
///
466474
/// UI should update the list.
@@ -651,6 +659,10 @@ impl From<CoreEventType> for EventType {
651659
msg_id: msg_id.to_u32(),
652660
chat_id: chat_id.to_u32(),
653661
},
662+
CoreEventType::CallMissed { msg_id, chat_id } => CallMissed {
663+
msg_id: msg_id.to_u32(),
664+
chat_id: chat_id.to_u32(),
665+
},
654666
CoreEventType::TransportsModified => TransportsModified,
655667

656668
#[allow(unreachable_patterns)]

src/calls.rs

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

221221
let wait = RINGING_SECONDS;
222222
let context = self.get_weak_context();
223-
task::spawn(Context::emit_end_call_if_unaccepted(
223+
task::spawn(Context::finalize_call_if_unaccepted(
224224
context,
225225
wait.try_into()?,
226226
call.id,
227+
true, // Doesn't matter for outgoing calls
227228
));
228229

229230
Ok(call.id)
@@ -313,39 +314,67 @@ impl Context {
313314
Ok(())
314315
}
315316

316-
async fn emit_end_call_if_unaccepted(
317+
async fn finalize_call_if_unaccepted(
317318
context: WeakContext,
318319
wait: u64,
319320
call_id: MsgId,
321+
can_call_me: bool,
320322
) -> Result<()> {
321323
sleep(Duration::from_secs(wait)).await;
322324
let context = context.upgrade()?;
323325
let Some(mut call) = context.load_call_by_id(call_id).await? else {
324326
warn!(
325327
context,
326-
"emit_end_call_if_unaccepted is called with {call_id} which does not refer to a call."
328+
"finalize_call_if_unaccepted is called with {call_id} which does not refer to a call."
327329
);
328330
return Ok(());
329331
};
330332
if !call.is_accepted() && !call.is_ended() {
333+
let (msg_id, chat_id) = (call_id, call.msg.chat_id);
331334
if call.is_incoming() {
332335
call.mark_as_canceled(&context).await?;
333336
let missed_call_str = stock_str::missed_call(&context).await;
334337
call.update_text(&context, &missed_call_str).await?;
338+
if can_call_me {
339+
context.emit_event(EventType::CallMissed { msg_id, chat_id });
340+
}
335341
} else {
336342
call.mark_as_ended(&context).await?;
337343
let canceled_call_str = stock_str::canceled_call(&context).await;
338344
call.update_text(&context, &canceled_call_str).await?;
339345
}
346+
if can_call_me {
347+
context.emit_event(EventType::CallEnded { msg_id, chat_id });
348+
}
340349
context.emit_msgs_changed(call.msg.chat_id, call_id);
341-
context.emit_event(EventType::CallEnded {
342-
msg_id: call.msg.id,
343-
chat_id: call.msg.chat_id,
344-
});
345350
}
346351
Ok(())
347352
}
348353

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

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

469+
let (msg_id, chat_id) = (call_id, call.msg.chat_id);
457470
if !call.is_accepted() {
458471
if call.is_incoming() {
459472
if from_id == ContactId::SELF {
@@ -464,6 +477,9 @@ impl Context {
464477
call.mark_as_canceled(self).await?;
465478
let missed_call_str = stock_str::missed_call(self).await;
466479
call.update_text(self, &missed_call_str).await?;
480+
if self.can_call_me(from_id).await? {
481+
self.emit_event(EventType::CallMissed { msg_id, chat_id });
482+
}
467483
}
468484
} else {
469485
// outgoing
@@ -481,12 +497,8 @@ impl Context {
481497
call.mark_as_ended(self).await?;
482498
call.update_text_duration(self).await?;
483499
}
484-
500+
self.emit_event(EventType::CallEnded { msg_id, chat_id });
485501
self.emit_msgs_changed(call.msg.chat_id, call_id);
486-
self.emit_event(EventType::CallEnded {
487-
msg_id: call.msg.id,
488-
chat_id: call.msg.chat_id,
489-
});
490502
}
491503
_ => {}
492504
}

src/calls/calls_tests.rs

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use crate::config::Config;
44
use crate::constants::DC_CHAT_ID_TRASH;
55
use crate::receive_imf::receive_imf;
66
use crate::test_utils::{TestContext, TestContextManager};
7+
use crate::tools::SystemTime;
78

89
struct CallSetup {
910
pub alice: TestContext,
@@ -412,6 +413,9 @@ async fn test_caller_cancels_call() -> Result<()> {
412413
// Bob receives the ending message
413414
bob.recv_msg_trash(&sent3).await;
414415
assert_text(&bob, bob_call.id, "Missed call").await?;
416+
bob.evtracker
417+
.get_matching(|evt| matches!(evt, EventType::CallMissed { .. }))
418+
.await;
415419
bob.evtracker
416420
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
417421
.await;
@@ -424,6 +428,9 @@ async fn test_caller_cancels_call() -> Result<()> {
424428

425429
bob2.recv_msg_trash(&sent3).await;
426430
assert_text(&bob2, bob2_call.id, "Missed call").await?;
431+
bob2.evtracker
432+
.get_matching(|evt| matches!(evt, EventType::CallMissed { .. }))
433+
.await;
427434
bob2.evtracker
428435
.get_matching(|evt| matches!(evt, EventType::CallEnded { .. }))
429436
.await;
@@ -432,6 +439,95 @@ async fn test_caller_cancels_call() -> Result<()> {
432439
Ok(())
433440
}
434441

442+
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
443+
async fn test_stale_call() -> Result<()> {
444+
let mut tcm = TestContextManager::new();
445+
for accepted in [false, true] {
446+
let alice = &tcm.alice().await;
447+
let bob = &tcm.bob().await;
448+
449+
info!(bob, "Alice is accepted: {accepted}.");
450+
if accepted {
451+
bob.create_chat(alice).await;
452+
}
453+
let alice_chat = alice.create_chat(bob).await;
454+
alice
455+
.place_outgoing_call(alice_chat.id, PLACE_INFO.to_string(), true)
456+
.await?;
457+
let sent1 = alice.pop_sent_msg().await;
458+
459+
SystemTime::shift(Duration::from_secs(3600));
460+
let bob_call = bob.recv_msg(&sent1).await;
461+
let EventType::MsgsChanged { msg_id, chat_id } = bob
462+
.evtracker
463+
.get_matching(|evt| {
464+
matches!(
465+
evt,
466+
EventType::MsgsChanged { .. }
467+
| EventType::CallMissed { .. }
468+
| EventType::CallEnded { .. }
469+
)
470+
})
471+
.await
472+
else {
473+
unreachable!();
474+
};
475+
assert_eq!(chat_id, bob_call.chat_id);
476+
let msg = Message::load_from_db(bob, msg_id).await?;
477+
assert_eq!(msg.text, stock_str::messages_e2e_encrypted(bob).await);
478+
if accepted {
479+
let EventType::CallMissed { msg_id, chat_id } = bob
480+
.evtracker
481+
.get_matching(|evt| {
482+
matches!(
483+
evt,
484+
EventType::CallMissed { .. } | EventType::CallEnded { .. }
485+
)
486+
})
487+
.await
488+
else {
489+
unreachable!();
490+
};
491+
assert_eq!(msg_id, bob_call.id);
492+
assert_eq!(chat_id, bob_call.chat_id);
493+
}
494+
let EventType::MsgsChanged { msg_id, chat_id } = bob
495+
.evtracker
496+
.get_matching(|evt| {
497+
matches!(
498+
evt,
499+
EventType::MsgsChanged { .. }
500+
| EventType::CallMissed { .. }
501+
| EventType::CallEnded { .. }
502+
)
503+
})
504+
.await
505+
else {
506+
unreachable!();
507+
};
508+
assert_eq!(msg_id, bob_call.id);
509+
assert_eq!(chat_id, bob_call.chat_id);
510+
let evt = bob
511+
.evtracker
512+
.get_matching_opt(bob, |evt| {
513+
matches!(
514+
evt,
515+
EventType::CallMissed { .. } | EventType::CallEnded { .. }
516+
)
517+
})
518+
.await;
519+
assert!(evt.is_none());
520+
assert_text(bob, bob_call.id, "Missed call").await?;
521+
assert_eq!(call_state(bob, bob_call.id).await?, CallState::Missed);
522+
523+
// Test that message summary says it is a missed call.
524+
let bob_call_msg = Message::load_from_db(bob, bob_call.id).await?;
525+
let summary = bob_call_msg.get_summary(bob, None).await?;
526+
assert_eq!(summary.text, "🎥 Missed call");
527+
}
528+
Ok(())
529+
}
530+
435531
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
436532
async fn test_is_stale_call() -> Result<()> {
437533
// a call started now is not stale

0 commit comments

Comments
 (0)