From 2453ce276056c2f60a276fc134edfa25dd45ebb4 Mon Sep 17 00:00:00 2001 From: juancarmore Date: Wed, 4 Mar 2026 18:44:30 +0100 Subject: [PATCH] frontend: refactor captions button logic to improve state management and prevent concurrent toggles --- ...eting-toolbar-extra-buttons.component.html | 16 ++++------ ...meeting-toolbar-extra-buttons.component.ts | 18 +++++++++-- .../services/meeting-captions.service.ts | 30 ++----------------- 3 files changed, 25 insertions(+), 39 deletions(-) diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/customization/meeting-toolbar-extra-buttons/meeting-toolbar-extra-buttons.component.html b/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/customization/meeting-toolbar-extra-buttons/meeting-toolbar-extra-buttons.component.html index 5b4836da..ad8ecd41 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/customization/meeting-toolbar-extra-buttons/meeting-toolbar-extra-buttons.component.html +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/customization/meeting-toolbar-extra-buttons/meeting-toolbar-extra-buttons.component.html @@ -11,10 +11,8 @@ > subtitles - @if (captionsStatus() === 'DISABLED_WITH_WARNING') { + @if (isCaptionsButtonDisabled()) { Live captions (disabled by admin) - } @else if (isCaptionsTogglePending()) { - {{ areCaptionsEnabledByUser() ? 'Disabling live captions...' : 'Enabling live captions...' }} } @else { {{ areCaptionsEnabledByUser() ? 'Disable live captions' : 'Enable live captions' }} } @@ -30,16 +28,14 @@ [disabledInteractive]="isCaptionsButtonDisabled()" [disableRipple]="true" [matTooltip]=" - captionsStatus() === 'DISABLED_WITH_WARNING' + isCaptionsButtonDisabled() ? 'Live captions are disabled by admin' - : isCaptionsTogglePending() - ? (areCaptionsEnabledByUser() ? 'Disabling live captions...' : 'Enabling live captions...') - : areCaptionsEnabledByUser() - ? 'Disable live captions' - : 'Enable live captions' + : areCaptionsEnabledByUser() + ? 'Disable live captions' + : 'Enable live captions' " > - @if (captionsStatus() === 'DISABLED_WITH_WARNING') { + @if (isCaptionsButtonDisabled()) { subtitles_off } @else { subtitles diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/customization/meeting-toolbar-extra-buttons/meeting-toolbar-extra-buttons.component.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/customization/meeting-toolbar-extra-buttons/meeting-toolbar-extra-buttons.component.ts index 172ec35f..a5926d3b 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/customization/meeting-toolbar-extra-buttons/meeting-toolbar-extra-buttons.component.ts +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/customization/meeting-toolbar-extra-buttons/meeting-toolbar-extra-buttons.component.ts @@ -1,5 +1,5 @@ import { CommonModule } from '@angular/common'; -import { Component, computed, inject } from '@angular/core'; +import { Component, computed, inject, signal } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; import { MatMenuModule } from '@angular/material/menu'; @@ -34,8 +34,12 @@ export class MeetingToolbarExtraButtonsComponent { /** Whether to show the captions button (visible when not HIDDEN) */ showCaptionsButton = computed(() => this.meetingContextService.meetingUI().showCaptionsControls); /** Whether captions button is disabled (true when DISABLED_WITH_WARNING) */ - // TODO: Apply disabled while an enable/disable request is in flight to prevent concurrent calls isCaptionsButtonDisabled = computed(() => this.meetingContextService.meetingUI().showCaptionsControlsDisabled); + /** + * True while an enable() or disable() call is in flight. + * Use this to prevent concurrent toggle requests. + */ + isCaptionsTogglePending = signal(false); /** Whether captions are currently enabled by the user */ areCaptionsEnabledByUser = this.captionService.areCaptionsEnabledByUser; @@ -53,17 +57,27 @@ export class MeetingToolbarExtraButtonsComponent { } async onCaptionsClick(): Promise { + if (this.isCaptionsTogglePending()) { + return; + } + + this.isCaptionsTogglePending.set(true); + try { // Don't allow toggling if captions are disabled at system level if (this.isCaptionsButtonDisabled()) { this.log.w('Captions are disabled at system level (MEET_CAPTIONS_ENABLED=false)'); return; } + this.captionService.areCaptionsEnabledByUser() ? await this.captionService.disable() : await this.captionService.enable(); } catch (error) { this.log.e('Error toggling captions:', error); + } finally { + // Add a small delay before allowing another toggle to prevent rapid concurrent calls + setTimeout(() => this.isCaptionsTogglePending.set(false), 500); } } } diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/services/meeting-captions.service.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/services/meeting-captions.service.ts index bc7306c1..111be9fd 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/services/meeting-captions.service.ts +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/services/meeting-captions.service.ts @@ -1,8 +1,8 @@ import { Injectable, inject, signal } from '@angular/core'; import { ILogger, LoggerService, ParticipantService, Room, TextStreamReader } from 'openvidu-components-angular'; +import { AiAssistantService } from '../../../shared/services/ai-assistant.service'; import { Caption, CaptionsConfig } from '../models/captions.model'; import { CustomParticipantModel } from '../models/custom-participant.model'; -import { AiAssistantService } from '../../../shared/services/ai-assistant.service'; /** * Service responsible for managing live transcription captions. @@ -41,7 +41,6 @@ export class MeetingCaptionsService { private readonly _captions = signal([]); private readonly _areCaptionsEnabledByUser = signal(false); private readonly _captionsAgentId = signal(null); - private readonly _isCaptionsTogglePending = signal(false); /** * Current list of active captions @@ -51,11 +50,6 @@ export class MeetingCaptionsService { * Whether captions are enabled by the user */ readonly areCaptionsEnabledByUser = this._areCaptionsEnabledByUser.asReadonly(); - /** - * True while an enable() or disable() call is in flight. - * Use this to prevent concurrent toggle requests. - */ - readonly isCaptionsTogglePending = this._isCaptionsTogglePending.asReadonly(); // Map to track expiration timeouts private expirationTimeouts = new Map>(); @@ -102,13 +96,6 @@ export class MeetingCaptionsService { return; } - if (this._isCaptionsTogglePending()) { - this.logger.d('Captions toggle already in progress'); - return; - } - - this._isCaptionsTogglePending.set(true); - try { // Register the LiveKit transcription handler const agent = await this.aiAssistantService.createLiveCaptionsAssistant(); @@ -116,9 +103,8 @@ export class MeetingCaptionsService { this.room.registerTextStreamHandler('lk.transcription', this.handleTranscription.bind(this)); this._areCaptionsEnabledByUser.set(true); this.logger.d('Captions enabled'); - } finally { - // Add a small delay before allowing another toggle to prevent rapid concurrent calls - setTimeout(() => this._isCaptionsTogglePending.set(false), 500); + } catch (error) { + this.logger.e('Error enabling captions:', error); } } @@ -132,13 +118,6 @@ export class MeetingCaptionsService { return; } - if (this._isCaptionsTogglePending()) { - this.logger.d('Captions toggle already in progress'); - return; - } - - this._isCaptionsTogglePending.set(true); - try { const agentId = this._captionsAgentId(); @@ -155,9 +134,6 @@ export class MeetingCaptionsService { this.logger.d('Captions disabled'); } catch (error) { this.logger.e('Error disabling captions:', error); - } finally { - // Add a small delay before allowing another toggle to prevent rapid concurrent calls - setTimeout(() => this._isCaptionsTogglePending.set(false), 500); } }