feat(call): add sound effects and incoming call notification UI

- Implemented sound management for call events including mute, unmute, deafen, undeafen, call joined, call depart, and dialing sounds.
- Created a new CallSounds class to handle sound playback and state management.
- Added IncomingCallNotification component to display incoming call alerts with caller information and action buttons.
- Styled the incoming call notification using vanilla-extract CSS for animations and layout.
- Integrated sound playback for incoming calls with a 15-second threshold for dialing sound.
This commit is contained in:
2026-02-05 03:24:18 +11:00
parent cd912025f1
commit 1a452f52ca
27 changed files with 1888 additions and 179 deletions

View File

@@ -0,0 +1,157 @@
/* eslint-disable no-console */
import MicMuteSound from '../../../../public/sound/Mic_Mute.ogg';
import MicUnmuteSound from '../../../../public/sound/Mic_Unmuted.ogg';
import SpeakersDeafenSound from '../../../../public/sound/Speakers_Deafen.ogg';
import SpeakersUndeafenSound from '../../../../public/sound/Speakers_Undeafen.ogg';
import CallJoinedSound from '../../../../public/sound/Call_Joined.ogg';
import CallDepartSound from '../../../../public/sound/Call_Depart.ogg';
import CallDialingSound from '../../../../public/sound/Call_Dialing.ogg';
/**
* Sound types that can be played during a call
*/
export enum CallSoundType {
MicMute = 'mic_mute',
MicUnmute = 'mic_unmute',
Deafen = 'deafen',
Undeafen = 'undeafen',
CallJoined = 'call_joined',
CallDepart = 'call_depart',
CallDialing = 'call_dialing',
}
/**
* Maps sound types to their audio sources
*/
const SOUND_SOURCES: Record<CallSoundType, string> = {
[CallSoundType.MicMute]: MicMuteSound,
[CallSoundType.MicUnmute]: MicUnmuteSound,
[CallSoundType.Deafen]: SpeakersDeafenSound,
[CallSoundType.Undeafen]: SpeakersUndeafenSound,
[CallSoundType.CallJoined]: CallJoinedSound,
[CallSoundType.CallDepart]: CallDepartSound,
[CallSoundType.CallDialing]: CallDialingSound,
};
/**
* Manages call-related sound effects.
* Handles playing local sounds and coordinating with remote participants.
*/
export class CallSounds {
private audioElements: Map<CallSoundType, HTMLAudioElement> = new Map();
private dialingLoopElement: HTMLAudioElement | null = null;
private isDeafened = false;
constructor() {
this.preloadSounds();
}
/**
* Preloads all call sounds for instant playback
*/
private preloadSounds(): void {
Object.entries(SOUND_SOURCES).forEach(([type, src]) => {
const audio = new Audio(src);
audio.preload = 'auto';
this.audioElements.set(type as CallSoundType, audio);
});
}
/**
* Plays a call sound locally
* @param soundType - The type of sound to play
* @param loop - Whether to loop the sound (e.g., for dialing)
*/
playSound(soundType: CallSoundType, loop = false): void {
// Don't play sounds if deafened (except deafen/undeafen sounds for feedback)
if (this.isDeafened &&
soundType !== CallSoundType.Deafen &&
soundType !== CallSoundType.Undeafen) {
return;
}
const audio = this.audioElements.get(soundType);
if (!audio) {
console.warn(`Sound not found: ${soundType}`);
return;
}
// Stop any currently playing instance and reset
audio.currentTime = 0;
audio.loop = loop;
// Track looping dialing sound separately
if (soundType === CallSoundType.CallDialing && loop) {
this.dialingLoopElement = audio;
}
audio.play().catch((err) => {
console.warn(`Failed to play sound ${soundType}:`, err);
});
}
/**
* Stops the dialing loop sound if playing
*/
stopDialingLoop(): void {
if (this.dialingLoopElement) {
this.dialingLoopElement.pause();
this.dialingLoopElement.currentTime = 0;
this.dialingLoopElement.loop = false;
this.dialingLoopElement = null;
}
}
/**
* Sets the deafened state - affects whether sounds are played
* @param deafened - Whether the user is deafened
*/
setDeafened(deafened: boolean): void {
this.isDeafened = deafened;
}
/**
* Gets whether sounds are currently deafened
*/
getIsDeafened(): boolean {
return this.isDeafened;
}
/**
* Cleans up audio resources
*/
dispose(): void {
this.stopDialingLoop();
this.audioElements.forEach((audio) => {
audio.pause();
});
this.audioElements.clear();
}
}
/**
* Singleton instance of CallSounds for app-wide use
*/
let callSoundsInstance: CallSounds | null = null;
/**
* Gets or creates the CallSounds singleton instance
*/
export function getCallSounds(): CallSounds {
if (!callSoundsInstance) {
callSoundsInstance = new CallSounds();
}
return callSoundsInstance;
}
/**
* Disposes the CallSounds singleton
*/
export function disposeCallSounds(): void {
if (callSoundsInstance) {
callSoundsInstance.dispose();
callSoundsInstance = null;
}
}