import { sendEncryptedMessage } from '@chat-app/shared/chat'; import { type CallKind, type CallSignal, fetchLivekitToken, signalTopic, } from '@chat-app/shared/rtc'; import type { RealtimeChannel } from '@supabase/supabase-js'; import { ConnectionQuality, ConnectionState, type Participant, type RemoteParticipant, type RemoteTrack, type RemoteTrackPublication, Room, RoomEvent, Track, } from 'livekit-client'; import { createContext, type ReactNode, useCallback, useContext, useEffect, useMemo, useRef, useState, } from 'react'; import { useAuth } from './AuthContext'; import { useConversationsContext } from './ConversationsContext'; import { playDeafenBeep, playEndBeep, playJoinBeep, playLeaveBeep, playMuteBeep, playUndeafenBeep, playUnmuteBeep, } from '../lib/callSounds'; import { useLiveCaptions } from '../lib/useLiveCaptions'; import { setCallWakeLock } from '../lib/wakeLock'; import { notify } from '../lib/osNotify'; import { isTauriRuntime, registerGlobalShortcutPress, registerPttShortcut, unregisterGlobalShortcut, unregisterPttShortcut, } from '../lib/globalShortcut'; import { getPttSettings, subscribePttSettings } from '../lib/pttSettings'; import { bindingToTauriShortcut, eventMatchesBinding, getVoiceHotkeys, subscribeVoiceHotkeys, type VoiceHotkeys, } from '../lib/voiceHotkeys'; import { getAudioQualityParams, getAudioSettings, subscribeAudioSettings, updateAudioSettings, } from '../lib/audioSettings'; import { applyBackgroundBlurToLocal, removeBackgroundBlurFromLocal, } from '../lib/videoBlur'; import { createCallE2EE, getCallE2EESettings, isE2EESupported, } from '../lib/callE2EE'; import { createMicPipeline, type MicPipeline } from '../lib/micPipeline'; import { getParticipantVolume, subscribeParticipantVolumes, } from '../lib/participantVolumes'; import { allPipelines, createPipeline, destroyPipeline, type RemoteAudioPipeline, setAllPipelinesSinkId, setPipelineGain, } from '../lib/remoteAudioPipelines'; import { type LoopbackTrackHandle, startLoopbackTrack, } from '../lib/loopbackAudio'; import { type SystemAudioHandle, startSystemAudioCapture, } from '../lib/screenAudio'; import { clearScreenShareVolumes, getScreenShareVolume, subscribeScreenShareVolumes, } from '../lib/screenShareVolumes'; import { startSoundboardHotkeys } from '../lib/soundboardHotkeys'; import { playEntry } from '../lib/soundboardPlayback'; import { getPrefs as getSoundboardPrefs, listSounds as listSoundboard, updatePrefs as updateSoundboardPrefs, } from '../lib/soundboardStorage'; import { type DisplaySurfaceHint, getPresetParams, getScreenShareSettings, subscribeScreenShareSettings, type ScreenSharePreset, updateScreenShareSettings, } from '../lib/screenShareSettings'; import { ensureInstallId } from '../lib/installId'; import { supabase } from '../lib/supabase'; import { cachedUserKey } from '../lib/userIdentity'; import { setWindowFullscreen } from '../lib/windowFullscreen'; export type CallState = | { kind: 'idle' } | { kind: 'outgoing'; callId: string; conversationId: string; mediaKind: CallKind; ringingSince: string; } | { kind: 'incoming'; callId: string; conversationId: string; fromUserId: string; mediaKind: CallKind; } | { kind: 'connecting'; callId: string; conversationId: string; mediaKind: CallKind; } | { kind: 'connected'; callId: string; conversationId: string; mediaKind: CallKind; startedAt: string; } | { // LiveKit dropped the signaling socket but is actively retrying. The // room + tracks stay alive — the user's mic + speakers keep working — // they just can't reach peers until we're back. Distinct from // `connecting` so the UI can show "Verbinde neu…" vs "Verbinde…". kind: 'reconnecting'; callId: string; conversationId: string; mediaKind: CallKind; startedAt: string; } | { kind: 'error'; message: string }; export interface RemoteScreenShare { track: RemoteTrack; participantId: string; participantName: string; } // Discord-style "second-call" ringer — a peer invites us while we're // already in another call. Lives alongside CallState so the active call // keeps running while we decide. Only one slot: a third caller arriving // while this is set still gets an automatic busy-reject. export interface PendingIncoming { callId: string; conversationId: string; fromUserId: string; mediaKind: CallKind; } // Visual call modes (Discord-style): grid shows all tiles equally, focus pins // one speaker with others in a strip, fullscreen is cinema mode. export type CallMode = 'grid' | 'focus' | 'fullscreen'; interface CallContextValue { state: CallState; room: Room | null; remoteParticipants: RemoteParticipant[]; isMuted: boolean; isE2EEActive: boolean; isScreenSharing: boolean; isCameraEnabled: boolean; isDeafened: boolean; /** Participants whose screen share the local user has actively clicked * "Bildschirm anschauen" on. Session-only (cleared on call end). Used to * gate both the