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 3610b706..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 @@ -2,10 +2,20 @@ @if (showCaptionsButton()) { @if (isMobile()) { - } @else { @@ -14,10 +24,22 @@ [ngClass]="areCaptionsEnabledByUser() ? 'active' : ''" mat-icon-button (click)="onCaptionsClick()" + [disabled]="isCaptionsButtonDisabled()" + [disabledInteractive]="isCaptionsButtonDisabled()" [disableRipple]="true" - [matTooltip]="areCaptionsEnabledByUser() ? 'Disable live captions' : 'Enable live captions'" + [matTooltip]=" + isCaptionsButtonDisabled() + ? 'Live captions are disabled by admin' + : areCaptionsEnabledByUser() + ? 'Disable live captions' + : 'Enable live captions' + " > - subtitles + @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.scss b/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/customization/meeting-toolbar-extra-buttons/meeting-toolbar-extra-buttons.component.scss index 8a820dcc..f72022ed 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/customization/meeting-toolbar-extra-buttons/meeting-toolbar-extra-buttons.component.scss +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/customization/meeting-toolbar-extra-buttons/meeting-toolbar-extra-buttons.component.scss @@ -3,3 +3,7 @@ #captions-button.active { background-color: var(--ov-accent-action-color); } + +#captions-button.mat-mdc-button-disabled { + opacity: 0.5; +} 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 2b3b2929..bcaf47be 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 @@ -34,9 +34,19 @@ export class MeetingToolbarExtraButtonsComponent { protected showCopyLinkButton = computed(() => this.meetingContextService.canModerateRoom()); /** - * Whether to show the captions button + * Captions status based on room and global configuration */ - protected showCaptionsButton = computed(() => this.meetingContextService.areCaptionsAllowed()); + protected captionsStatus = computed(() => this.meetingContextService.getCaptionsStatus()); + + /** + * Whether to show the captions button (visible when not HIDDEN) + */ + protected showCaptionsButton = computed(() => this.captionsStatus() !== 'HIDDEN'); + + /** + * Whether captions button is disabled (true when DISABLED_WITH_WARNING) + */ + protected isCaptionsButtonDisabled = computed(() => this.captionsStatus() === 'DISABLED_WITH_WARNING'); /** * Whether the device is mobile (affects button style) @@ -56,6 +66,11 @@ export class MeetingToolbarExtraButtonsComponent { } onCaptionsClick(): void { + // 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() ? this.captionService.disable() : this.captionService.enable(); } } diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/pages/meeting/meeting.component.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/pages/meeting/meeting.component.ts index 8bcf2118..a3462edf 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/pages/meeting/meeting.component.ts +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/pages/meeting/meeting.component.ts @@ -120,7 +120,10 @@ export class MeetingComponent implements OnInit { if (token && this.showLobby) { // The meeting view must be shown before loading the appearance config this.showLobby = false; - await this.configService.loadRoomsAppearanceConfig(); + await Promise.all([ + this.configService.loadRoomsAppearanceConfig(), + this.configService.loadCaptionsConfig() + ]); } }); } diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/services/meeting-context.service.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/services/meeting-context.service.ts index d62a162a..7f8a38c1 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/services/meeting-context.service.ts +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/services/meeting-context.service.ts @@ -187,11 +187,11 @@ export class MeetingContextService { } /** - * Returns whether captions feature is allowed in the room - * @returns true if captions feature is allowed, false otherwise + * Returns the captions status based on room and global configuration + * @returns CaptionsStatus ('HIDDEN' | 'ENABLED' | 'DISABLED_WITH_WARNING') */ - areCaptionsAllowed(): boolean { - return this.featureConfigService.features().showCaptions; + getCaptionsStatus() { + return this.featureConfigService.features().captionsStatus; } /** diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/shared/models/app.model.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/shared/models/app.model.ts index d6d2787e..1085259e 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/shared/models/app.model.ts +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/shared/models/app.model.ts @@ -16,6 +16,11 @@ export enum Edition { PRO = 'PRO' } +/** + * Status of captions feature based on room and global configuration + */ +export type CaptionsStatus = 'HIDDEN' | 'ENABLED' | 'DISABLED_WITH_WARNING'; + /** * Interface that defines all available features in the application */ @@ -31,7 +36,7 @@ export interface ApplicationFeatures { showRecordingPanel: boolean; showChat: boolean; showBackgrounds: boolean; - showCaptions: boolean; + captionsStatus: CaptionsStatus; showParticipantList: boolean; showSettings: boolean; showFullscreen: boolean; diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/shared/services/feature-configuration.service.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/shared/services/feature-configuration.service.ts index ae930f1d..b4da83b8 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/shared/services/feature-configuration.service.ts +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/shared/services/feature-configuration.service.ts @@ -1,13 +1,14 @@ import { computed, Injectable, signal } from '@angular/core'; import { MeetAppearanceConfig, + MeetRoomCaptionsConfig, MeetRoomConfig, MeetRoomMemberPermissions, MeetRoomMemberRole, TrackSource } from '@openvidu-meet/typings'; import { LoggerService } from 'openvidu-components-angular'; -import { ApplicationFeatures } from '../models/app.model'; +import { ApplicationFeatures, CaptionsStatus } from '../models/app.model'; /** * Base configuration for default features @@ -22,7 +23,7 @@ const DEFAULT_FEATURES: ApplicationFeatures = { showRecordingPanel: true, showChat: true, showBackgrounds: true, - showCaptions: false, + captionsStatus: 'ENABLED', showParticipantList: true, showSettings: true, showFullscreen: true, @@ -52,6 +53,7 @@ export class FeatureConfigurationService { protected roomMemberRole = signal(undefined); protected roomMemberPermissions = signal(undefined); protected appearanceConfig = signal(undefined); + protected captionsGlobalConfig = signal(false); // Computed signal to derive features based on current configurations public readonly features = computed(() => @@ -59,7 +61,8 @@ export class FeatureConfigurationService { this.roomConfig(), this.roomMemberRole(), this.roomMemberPermissions(), - this.appearanceConfig() + this.appearanceConfig(), + this.captionsGlobalConfig() ) ); @@ -99,6 +102,14 @@ export class FeatureConfigurationService { this.appearanceConfig.set(config); } + /** + * Updates captions global config + */ + setCaptionsGlobalConfig(enabled: boolean): void { + this.log.d('Updating captions global config', enabled); + this.captionsGlobalConfig.set(enabled); + } + /** * Core logic to calculate features based on all configurations */ @@ -106,7 +117,8 @@ export class FeatureConfigurationService { roomConfig?: MeetRoomConfig, role?: MeetRoomMemberRole, permissions?: MeetRoomMemberPermissions, - appearanceConfig?: MeetAppearanceConfig + appearanceConfig?: MeetAppearanceConfig, + captionsGlobalEnabled: boolean = false ): ApplicationFeatures { // Start with default configuration const features: ApplicationFeatures = { ...DEFAULT_FEATURES }; @@ -116,7 +128,7 @@ export class FeatureConfigurationService { features.showRecordingPanel = roomConfig.recording.enabled; features.showChat = roomConfig.chat.enabled; features.showBackgrounds = roomConfig.virtualBackground.enabled; - features.showCaptions = roomConfig.captions?.enabled ?? false; + features.captionsStatus = this.computeCaptionsStatus(roomConfig.captions, captionsGlobalEnabled); } // Apply room member permissions (these can restrict enabled features) @@ -164,6 +176,22 @@ export class FeatureConfigurationService { return features; } + /** + * Computes the captions status based on room and global configuration + * HIDDEN: room config disabled + * ENABLED: room config enabled AND global config enabled + * DISABLED_WITH_WARNING: room config enabled BUT global config disabled + */ + protected computeCaptionsStatus( + roomCaptionsConfig: MeetRoomCaptionsConfig | undefined, + globalEnabled: boolean + ): CaptionsStatus { + if (!roomCaptionsConfig?.enabled) { + return 'HIDDEN'; + } + return globalEnabled ? 'ENABLED' : 'DISABLED_WITH_WARNING'; + } + /** * Resets all configurations to their initial values */ @@ -172,5 +200,6 @@ export class FeatureConfigurationService { this.roomMemberRole.set(undefined); this.roomMemberPermissions.set(undefined); this.appearanceConfig.set(undefined); + this.captionsGlobalConfig.set(false); } } diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/shared/services/global-config.service.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/shared/services/global-config.service.ts index 799ffa98..b092e616 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/shared/services/global-config.service.ts +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/shared/services/global-config.service.ts @@ -76,4 +76,19 @@ export class GlobalConfigService { const path = `${this.GLOBAL_CONFIG_API}/rooms/appearance`; await this.httpService.putRequest(path, { appearance: config }); } + + private async getCaptionsConfig(): Promise<{ enabled: boolean }> { + const path = `${this.GLOBAL_CONFIG_API}/captions`; + return await this.httpService.getRequest<{ enabled: boolean }>(path); + } + + async loadCaptionsConfig(): Promise { + try { + const config = await this.getCaptionsConfig(); + this.featureConfService.setCaptionsGlobalConfig(config.enabled); + } catch (error) { + this.log.e('Error loading captions config:', error); + throw error; + } + } }