chore: enforce lint across codebase and ban "maybe" in identifiers
Remove member-ordering and complexity eslint-disable comments by reordering class members and applying targeted fixes. Add metoyou/no-maybe-in-naming, type-safe WebRTC e2e harness helpers, and resolve remaining lint errors so npm run lint exits cleanly. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable @typescript-eslint/member-ordering, */
|
||||
import {
|
||||
Injectable,
|
||||
signal,
|
||||
@@ -20,13 +19,6 @@ 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());
|
||||
@@ -43,6 +35,14 @@ 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,4 +111,5 @@ export class VoiceSessionFacade {
|
||||
getVoiceServerId(): string | null {
|
||||
return this._voiceSession()?.serverId ?? null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+21
-10
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable @typescript-eslint/member-ordering */
|
||||
import {
|
||||
Injectable,
|
||||
computed,
|
||||
@@ -23,15 +22,6 @@ 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()) {
|
||||
@@ -42,15 +32,35 @@ 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()) {
|
||||
@@ -125,4 +135,5 @@ export class VoiceWorkspaceService {
|
||||
this._connectRemoteShares.set(false);
|
||||
this.resetMiniWindowPosition();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+24
-9
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable @typescript-eslint/member-ordering, @typescript-eslint/no-unused-vars */
|
||||
import {
|
||||
Component,
|
||||
inject,
|
||||
@@ -60,30 +59,45 @@ 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
|
||||
@@ -300,8 +314,9 @@ export class FloatingVoiceControlsComponent implements OnInit {
|
||||
includeSystemAudio: this.includeSystemAudio(),
|
||||
quality
|
||||
});
|
||||
} catch (_error) {
|
||||
} catch {
|
||||
// Screen share request was denied or failed
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+112
-80
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable @typescript-eslint/member-ordering, @typescript-eslint/no-unused-vars, complexity */
|
||||
import {
|
||||
Component,
|
||||
ElementRef,
|
||||
@@ -75,23 +74,15 @@ 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();
|
||||
|
||||
@@ -101,12 +92,65 @@ 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();
|
||||
|
||||
@@ -116,27 +160,6 @@ 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();
|
||||
|
||||
@@ -166,7 +189,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 (_error) {}
|
||||
} catch { /* ignore device enumeration errors */ }
|
||||
}
|
||||
|
||||
async connect(): Promise<void> {
|
||||
@@ -263,7 +286,7 @@ export class VoiceControlsComponent implements OnInit, OnDestroy {
|
||||
async retryConnection(): Promise<void> {
|
||||
try {
|
||||
await this.webrtcService.ensureSignalingConnected(10000);
|
||||
} catch (_error) {}
|
||||
} catch { /* ignore device enumeration errors */ }
|
||||
}
|
||||
|
||||
disconnect(): void {
|
||||
@@ -438,7 +461,7 @@ export class VoiceControlsComponent implements OnInit, OnDestroy {
|
||||
})
|
||||
);
|
||||
}
|
||||
} catch (_error) {}
|
||||
} catch { /* ignore device enumeration errors */ }
|
||||
}
|
||||
|
||||
async toggleScreenShare(): Promise<void> {
|
||||
@@ -547,6 +570,58 @@ 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();
|
||||
|
||||
@@ -614,50 +689,7 @@ export class VoiceControlsComponent implements OnInit, OnDestroy {
|
||||
includeSystemAudio: this.includeSystemAudio(),
|
||||
quality
|
||||
});
|
||||
} catch (_error) {}
|
||||
} catch { /* ignore device enumeration errors */ }
|
||||
}
|
||||
|
||||
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`;
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -104,11 +104,11 @@ function schedulePersist(settings: VoiceSettings): void {
|
||||
timeRemaining(): number;
|
||||
}
|
||||
type IdleRequest = (cb: (deadline: IdleDeadline) => void, opts?: { timeout: number }) => IdleCallbackHandle;
|
||||
interface MaybeIdleGlobal {
|
||||
interface IdleCallbackGlobal {
|
||||
requestIdleCallback?: IdleRequest;
|
||||
}
|
||||
|
||||
const idleGlobal = (typeof globalThis === 'undefined' ? {} : globalThis) as MaybeIdleGlobal;
|
||||
const idleGlobal = (typeof globalThis === 'undefined' ? {} : globalThis) as IdleCallbackGlobal;
|
||||
|
||||
if (typeof idleGlobal.requestIdleCallback === 'function') {
|
||||
idleGlobal.requestIdleCallback(() => runner(), { timeout: 1000 });
|
||||
|
||||
Reference in New Issue
Block a user