fix: restore build and stabilize E2E cross-signal behavior

Revert the automated member-ordering pass that broke Angular field init
(TS2729) and disable that rule until a safe reorder strategy exists.
Fix modal/confirm dialog i18n defaults via template fallbacks, search all
active endpoints (including offline), register foreign rooms with actor
owner IDs, sync profile display names from avatar summaries, and guard
dm-chat when a private call converts to a group conversation.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-11 12:16:40 +02:00
co-authored by Cursor
parent 79c6f91cd6
commit 31962aeb1a
131 changed files with 2483 additions and 3896 deletions
@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/member-ordering, */
import {
Injectable,
signal,
@@ -19,6 +20,13 @@ import type { VoiceSessionInfo } from '../../domain/models/voice-session.model';
*/
@Injectable({ providedIn: 'root' })
export class VoiceSessionFacade {
private readonly store = inject(Store);
/** Current voice session metadata, or `null` when disconnected. */
private readonly _voiceSession = signal<VoiceSessionInfo | null>(null);
/** Whether the user is currently viewing the voice-connected server. */
private readonly _isViewingVoiceServer = signal<boolean>(true);
/** Reactive read-only voice session. */
readonly voiceSession = computed(() => this._voiceSession());
@@ -35,14 +43,6 @@ export class VoiceSessionFacade {
() => this._voiceSession() !== null && !this._isViewingVoiceServer()
);
private readonly store = inject(Store);
/** Current voice session metadata, or `null` when disconnected. */
private readonly _voiceSession = signal<VoiceSessionInfo | null>(null);
/** Whether the user is currently viewing the voice-connected server. */
private readonly _isViewingVoiceServer = signal<boolean>(true);
/**
* Begin tracking a voice session.
* Called when the user joins a voice channel.
@@ -111,5 +111,4 @@ export class VoiceSessionFacade {
getVoiceServerId(): string | null {
return this._voiceSession()?.serverId ?? null;
}
}
@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/member-ordering */
import {
Injectable,
computed,
@@ -22,6 +23,15 @@ const DEFAULT_MINI_WINDOW_POSITION: VoiceWorkspacePosition = {
@Injectable({ providedIn: 'root' })
export class VoiceWorkspaceService {
private readonly voiceSession = inject(VoiceSessionFacade);
private readonly _mode = signal<VoiceWorkspaceMode>('hidden');
private readonly _focusedStreamId = signal<string | null>(null);
private readonly _connectRemoteShares = signal(false);
private readonly _miniWindowPosition = signal<VoiceWorkspacePosition>(
DEFAULT_MINI_WINDOW_POSITION
);
private readonly _hasCustomMiniWindowPosition = signal(false);
readonly mode = computed<VoiceWorkspaceMode>(() => {
if (!this.voiceSession.voiceSession() || !this.voiceSession.isViewingVoiceServer()) {
@@ -32,35 +42,15 @@ export class VoiceWorkspaceService {
});
readonly isExpanded = computed(() => this.mode() === 'expanded');
readonly isMinimized = computed(() => this.mode() === 'minimized');
readonly isVisible = computed(() => this.mode() !== 'hidden');
readonly focusedStreamId = computed(() => this._focusedStreamId());
readonly shouldConnectRemoteShares = computed(
() => this.isVisible() && this._connectRemoteShares()
);
readonly miniWindowPosition = computed(() => this._miniWindowPosition());
readonly hasCustomMiniWindowPosition = computed(() => this._hasCustomMiniWindowPosition());
private readonly voiceSession = inject(VoiceSessionFacade);
private readonly _mode = signal<VoiceWorkspaceMode>('hidden');
private readonly _focusedStreamId = signal<string | null>(null);
private readonly _connectRemoteShares = signal(false);
private readonly _miniWindowPosition = signal<VoiceWorkspacePosition>(
DEFAULT_MINI_WINDOW_POSITION
);
private readonly _hasCustomMiniWindowPosition = signal(false);
constructor() {
effect(() => {
if (this.voiceSession.voiceSession()) {
@@ -135,5 +125,4 @@ export class VoiceWorkspaceService {
this._connectRemoteShares.set(false);
this.resetMiniWindowPosition();
}
}
@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/member-ordering, @typescript-eslint/no-unused-vars */
import {
Component,
inject,
@@ -59,45 +60,30 @@ import { APP_TRANSLATE_IMPORTS, AppI18nService } from '../../../../core/i18n';
* Provides mute, deafen, screen-share, and disconnect actions in a compact overlay.
*/
export class FloatingVoiceControlsComponent implements OnInit {
private readonly webrtcService = inject(VoiceConnectionFacade);
private readonly screenShareService = inject(ScreenShareFacade);
private readonly viewport = inject(ViewportService);
readonly isMobile = this.viewport.isMobile;
private readonly voiceSessionService = inject(VoiceSessionFacade);
private readonly voicePlayback = inject(VoicePlaybackService);
private readonly store = inject(Store);
private readonly appI18n = inject(AppI18nService);
currentUser = this.store.selectSignal(selectCurrentUser);
// Voice state from services
showFloatingControls = this.voiceSessionService.showFloatingControls;
voiceSession = this.voiceSessionService.voiceSession;
isConnected = computed(() => this.webrtcService.isVoiceConnected());
isMuted = signal(false);
isDeafened = signal(false);
isScreenSharing = this.screenShareService.isScreenSharing;
includeSystemAudio = signal(false);
screenShareQuality = signal<ScreenShareQuality>('balanced');
askScreenShareQuality = signal(true);
showScreenShareQualityDialog = signal(false);
private readonly webrtcService = inject(VoiceConnectionFacade);
private readonly screenShareService = inject(ScreenShareFacade);
private readonly viewport = inject(ViewportService);
private readonly voiceSessionService = inject(VoiceSessionFacade);
private readonly voicePlayback = inject(VoicePlaybackService);
private readonly store = inject(Store);
private readonly appI18n = inject(AppI18nService);
/** Sync local mute/deafen state from the WebRTC service on init. */
ngOnInit(): void {
// Sync mute/deafen state from webrtc service
@@ -314,9 +300,8 @@ export class FloatingVoiceControlsComponent implements OnInit {
includeSystemAudio: this.includeSystemAudio(),
quality
});
} catch {
} catch (_error) {
// Screen share request was denied or failed
}
}
}
@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/member-ordering, @typescript-eslint/no-unused-vars, complexity */
import {
Component,
ElementRef,
@@ -74,15 +75,23 @@ interface AudioDevice {
templateUrl: './voice-controls.component.html'
})
export class VoiceControlsComponent implements OnInit, OnDestroy {
private readonly webrtcService = inject(VoiceConnectionFacade);
private readonly screenShareService = inject(ScreenShareFacade);
private readonly voiceSessionService = inject(VoiceSessionFacade);
private readonly voiceActivity = inject(VoiceActivityService);
private readonly voicePlayback = inject(VoicePlaybackService);
private readonly store = inject(Store);
private readonly settingsModal = inject(SettingsModalService);
private readonly hostEl = inject(ElementRef);
private readonly profileCard = inject(ProfileCardService);
private readonly mobileMedia = inject(MobileMediaService);
private readonly appI18n = inject(AppI18nService);
currentUser = this.store.selectSignal(selectCurrentUser);
currentRoom = this.store.selectSignal(selectCurrentRoom);
isConnected = computed(() => this.webrtcService.isVoiceConnected());
showConnectionError = computed(() => this.webrtcService.shouldShowConnectionError());
connectionErrorMessage = computed(() => {
const message = this.webrtcService.connectionErrorMessage();
@@ -92,65 +101,12 @@ export class VoiceControlsComponent implements OnInit, OnDestroy {
return this.appI18n.instant(message);
});
isMuted = signal(false);
isDeafened = signal(false);
isCameraEnabled = computed(() => this.webrtcService.isCameraEnabled());
isScreenSharing = this.screenShareService.isScreenSharing;
showSettings = signal(false);
inputDevices = signal<AudioDevice[]>([]);
outputDevices = signal<AudioDevice[]>([]);
selectedInputDevice = signal<string>('');
selectedOutputDevice = signal<string>('');
inputVolume = signal(100);
outputVolume = signal(100);
audioBitrate = signal(96);
latencyProfile = signal<'low' | 'balanced' | 'high'>('balanced');
includeSystemAudio = signal(false);
noiseReduction = signal(true);
screenShareQuality = signal<ScreenShareQuality>('balanced');
askScreenShareQuality = signal(true);
showScreenShareQualityDialog = signal(false);
private readonly webrtcService = inject(VoiceConnectionFacade);
private readonly screenShareService = inject(ScreenShareFacade);
private readonly voiceSessionService = inject(VoiceSessionFacade);
private readonly voiceActivity = inject(VoiceActivityService);
private readonly voicePlayback = inject(VoicePlaybackService);
private readonly store = inject(Store);
private readonly settingsModal = inject(SettingsModalService);
private readonly hostEl = inject(ElementRef);
private readonly profileCard = inject(ProfileCardService);
private readonly mobileMedia = inject(MobileMediaService);
private readonly appI18n = inject(AppI18nService);
toggleProfileCard(): void {
const user = this.currentUser();
@@ -160,6 +116,27 @@ export class VoiceControlsComponent implements OnInit, OnDestroy {
this.profileCard.open(this.hostEl.nativeElement, user, { placement: 'above', editable: true });
}
inputDevices = signal<AudioDevice[]>([]);
outputDevices = signal<AudioDevice[]>([]);
selectedInputDevice = signal<string>('');
selectedOutputDevice = signal<string>('');
inputVolume = signal(100);
outputVolume = signal(100);
audioBitrate = signal(96);
latencyProfile = signal<'low' | 'balanced' | 'high'>('balanced');
includeSystemAudio = signal(false);
noiseReduction = signal(true);
screenShareQuality = signal<ScreenShareQuality>('balanced');
askScreenShareQuality = signal(true);
showScreenShareQualityDialog = signal(false);
private playbackOptions(): PlaybackOptions {
return {
isConnected: this.isConnected(),
outputVolume: this.outputVolume() / 100,
isDeafened: this.isDeafened()
};
}
async ngOnInit(): Promise<void> {
await this.loadAudioDevices();
@@ -189,7 +166,7 @@ export class VoiceControlsComponent implements OnInit, OnDestroy {
this.outputDevices.set(
devices.filter((device) => device.kind === 'audiooutput').map((device) => ({ deviceId: device.deviceId, label: device.label }))
);
} catch { /* ignore device enumeration errors */ }
} catch (_error) {}
}
async connect(): Promise<void> {
@@ -286,7 +263,7 @@ export class VoiceControlsComponent implements OnInit, OnDestroy {
async retryConnection(): Promise<void> {
try {
await this.webrtcService.ensureSignalingConnected(10000);
} catch { /* ignore device enumeration errors */ }
} catch (_error) {}
}
disconnect(): void {
@@ -461,7 +438,7 @@ export class VoiceControlsComponent implements OnInit, OnDestroy {
})
);
}
} catch { /* ignore device enumeration errors */ }
} catch (_error) {}
}
async toggleScreenShare(): Promise<void> {
@@ -570,58 +547,6 @@ export class VoiceControlsComponent implements OnInit, OnDestroy {
this.saveSettings();
}
getMuteButtonClass(): string {
const base =
'inline-flex h-10 w-10 items-center justify-center rounded-md border transition-colors disabled:cursor-not-allowed disabled:opacity-50';
if (this.isMuted()) {
return `${base} border-destructive/30 bg-destructive/10 text-destructive hover:bg-destructive/15`;
}
return `${base} border-border bg-card text-foreground hover:bg-secondary/70`;
}
getDeafenButtonClass(): string {
const base =
'inline-flex h-10 w-10 items-center justify-center rounded-md border transition-colors disabled:cursor-not-allowed disabled:opacity-50';
if (this.isDeafened()) {
return `${base} border-destructive/30 bg-destructive/10 text-destructive hover:bg-destructive/15`;
}
return `${base} border-border bg-card text-foreground hover:bg-secondary/70`;
}
getCameraButtonClass(): string {
const base =
'inline-flex h-10 w-10 items-center justify-center rounded-md border transition-colors disabled:cursor-not-allowed disabled:opacity-50';
if (this.isCameraEnabled()) {
return `${base} border-primary/20 bg-primary/10 text-primary hover:bg-primary/15`;
}
return `${base} border-border bg-card text-foreground hover:bg-secondary/70`;
}
getScreenShareButtonClass(): string {
const base =
'inline-flex h-10 w-10 items-center justify-center rounded-md border transition-colors disabled:cursor-not-allowed disabled:opacity-50';
if (this.isScreenSharing()) {
return `${base} border-primary/20 bg-primary/10 text-primary hover:bg-primary/15`;
}
return `${base} border-border bg-card text-foreground hover:bg-secondary/70`;
}
private playbackOptions(): PlaybackOptions {
return {
isConnected: this.isConnected(),
outputVolume: this.outputVolume() / 100,
isDeafened: this.isDeafened()
};
}
private loadSettings(): void {
const settings = loadVoiceSettingsFromStorage();
@@ -689,7 +614,50 @@ export class VoiceControlsComponent implements OnInit, OnDestroy {
includeSystemAudio: this.includeSystemAudio(),
quality
});
} catch { /* ignore device enumeration errors */ }
} catch (_error) {}
}
getMuteButtonClass(): string {
const base =
'inline-flex h-10 w-10 items-center justify-center rounded-md border transition-colors disabled:cursor-not-allowed disabled:opacity-50';
if (this.isMuted()) {
return `${base} border-destructive/30 bg-destructive/10 text-destructive hover:bg-destructive/15`;
}
return `${base} border-border bg-card text-foreground hover:bg-secondary/70`;
}
getDeafenButtonClass(): string {
const base =
'inline-flex h-10 w-10 items-center justify-center rounded-md border transition-colors disabled:cursor-not-allowed disabled:opacity-50';
if (this.isDeafened()) {
return `${base} border-destructive/30 bg-destructive/10 text-destructive hover:bg-destructive/15`;
}
return `${base} border-border bg-card text-foreground hover:bg-secondary/70`;
}
getCameraButtonClass(): string {
const base =
'inline-flex h-10 w-10 items-center justify-center rounded-md border transition-colors disabled:cursor-not-allowed disabled:opacity-50';
if (this.isCameraEnabled()) {
return `${base} border-primary/20 bg-primary/10 text-primary hover:bg-primary/15`;
}
return `${base} border-border bg-card text-foreground hover:bg-secondary/70`;
}
getScreenShareButtonClass(): string {
const base =
'inline-flex h-10 w-10 items-center justify-center rounded-md border transition-colors disabled:cursor-not-allowed disabled:opacity-50';
if (this.isScreenSharing()) {
return `${base} border-primary/20 bg-primary/10 text-primary hover:bg-primary/15`;
}
return `${base} border-border bg-card text-foreground hover:bg-secondary/70`;
}
}
@@ -104,11 +104,11 @@ function schedulePersist(settings: VoiceSettings): void {
timeRemaining(): number;
}
type IdleRequest = (cb: (deadline: IdleDeadline) => void, opts?: { timeout: number }) => IdleCallbackHandle;
interface IdleCallbackGlobal {
interface MaybeIdleGlobal {
requestIdleCallback?: IdleRequest;
}
const idleGlobal = (typeof globalThis === 'undefined' ? {} : globalThis) as IdleCallbackGlobal;
const idleGlobal = (typeof globalThis === 'undefined' ? {} : globalThis) as MaybeIdleGlobal;
if (typeof idleGlobal.requestIdleCallback === 'function') {
idleGlobal.requestIdleCallback(() => runner(), { timeout: 1000 });