feat: Add incoming call modal

This commit is contained in:
2026-05-17 15:26:05 +02:00
parent 9d0a4478b2
commit 8e3ccf4157
8 changed files with 339 additions and 7 deletions

View File

@@ -45,6 +45,24 @@ export class DirectCallService {
readonly sessions = computed(() => this.sessionsSignal());
readonly activeSessions = computed(() => this.sessions().filter((session) => session.status !== 'ended'));
readonly visibleActiveSessions = computed(() => this.activeSessions().filter((session) => this.hasOngoingActivity(session)));
readonly incomingCall = computed<DirectCallSession | null>(() => {
if (this.isDoNotDisturb()) {
return null;
}
const meId = this.currentUserId();
if (!meId) {
return null;
}
return [...this.activeSessions()]
.sort((left, right) => right.createdAt - left.createdAt)
.find((session) => session.status === 'ringing'
&& this.currentSession()?.callId !== session.callId
&& !session.participants[meId]?.joined
&& this.hasConnectedParticipant(session)) ?? null;
});
readonly currentSession = signal<DirectCallSession | null>(null);
readonly hasActiveCall = computed(() => this.visibleActiveSessions().length > 0);
@@ -66,6 +84,14 @@ export class DirectCallService {
this.voice.syncOutgoingVoiceRouting(peerIds);
});
effect(() => {
if (this.incomingCall() && !this.isDoNotDisturb()) {
return;
}
this.audio.stop(AppSound.Call);
});
}
sessionById(callId: string | null | undefined): DirectCallSession | null {
@@ -160,6 +186,50 @@ export class DirectCallService {
this.currentSession.set(session);
}
async answerIncomingCall(callId: string): Promise<void> {
const session = this.sessionById(callId);
if (!session || session.status === 'ended') {
return;
}
this.audio.stop(AppSound.Call);
this.currentSession.set(session);
await this.joinCall(callId);
await this.router.navigate(['/call', callId]);
}
declineIncomingCall(callId: string): void {
const session = this.sessionById(callId);
if (!session || session.status === 'ended') {
return;
}
const meId = this.currentUserId();
const nextSession = meId
? {
...this.markParticipantJoined(session, meId, false, 'ended'),
status: 'ended' as const
}
: {
...session,
status: 'ended' as const
};
this.audio.stop(AppSound.Call);
if (meId) {
this.broadcastCallEvent('leave', session);
}
this.upsertSession(nextSession);
if (this.currentSession()?.callId === callId) {
this.currentSession.set(null);
}
}
async joinCall(callId: string, notifyPeers = true): Promise<void> {
const session = this.sessionById(callId);
const me = this.requireCurrentUser();
@@ -315,13 +385,16 @@ export class DirectCallService {
if (payload.action === 'ring') {
await this.ensureCallConversation(session);
if (session.status !== 'connected') {
if (this.shouldAlertIncomingCall(session)) {
this.audio.playLoop(AppSound.Call);
} else {
this.audio.stop(AppSound.Call);
}
await this.showIncomingNotification(payload.sender.displayName, payload.callId);
if (this.shouldAlertIncomingCall(session)) {
await this.showIncomingNotification(payload.sender.displayName, payload.callId);
}
return;
}
@@ -745,6 +818,14 @@ export class DirectCallService {
}));
}
private shouldAlertIncomingCall(session: DirectCallSession): boolean {
return session.status !== 'connected' && !this.isDoNotDisturb();
}
private isDoNotDisturb(): boolean {
return this.currentUser()?.status === 'busy';
}
private async showIncomingNotification(displayName: string, callId: string): Promise<void> {
if (typeof Notification === 'undefined') {
return;