frontend: Controls captions button based on admin config

Updates the captions button to respect the global admin configuration.

The button now displays a disabled state and tooltip when captions are disabled globally, preventing users from toggling them.

The UI is updated to show disabled state and a specific subtitle off icon,
reflecting whether captions are enabled at the system level.
This commit is contained in:
CSantosM 2026-01-28 16:27:30 +01:00
parent 4ac182c244
commit 3f91e281b3
8 changed files with 110 additions and 17 deletions

View File

@ -2,10 +2,20 @@
@if (showCaptionsButton()) {
@if (isMobile()) {
<!-- On mobile, the captions button will be inside a menu -->
<button id="captions-button" mat-menu-item (click)="onCaptionsClick()" [disableRipple]="true">
<button
id="captions-button"
mat-menu-item
(click)="onCaptionsClick()"
[disabled]="isCaptionsButtonDisabled()"
[disableRipple]="true"
>
<mat-icon class="material-symbols-outlined">subtitles</mat-icon>
<span class="button-text">
@if (isCaptionsButtonDisabled()) {
Live captions (disabled by admin)
} @else {
{{ areCaptionsEnabledByUser() ? 'Disable live captions' : 'Enable live captions' }}
}
</span>
</button>
} @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'
"
>
@if (isCaptionsButtonDisabled()) {
<mat-icon>subtitles_off</mat-icon>
} @else {
<mat-icon>subtitles</mat-icon>
}
</button>
}
}

View File

@ -3,3 +3,7 @@
#captions-button.active {
background-color: var(--ov-accent-action-color);
}
#captions-button.mat-mdc-button-disabled {
opacity: 0.5;
}

View File

@ -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();
}
}

View File

@ -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()
]);
}
});
}

View File

@ -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;
}
/**

View File

@ -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;

View File

@ -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<MeetRoomMemberRole | undefined>(undefined);
protected roomMemberPermissions = signal<MeetRoomMemberPermissions | undefined>(undefined);
protected appearanceConfig = signal<MeetAppearanceConfig | undefined>(undefined);
protected captionsGlobalConfig = signal<boolean>(false);
// Computed signal to derive features based on current configurations
public readonly features = computed<ApplicationFeatures>(() =>
@ -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);
}
}

View File

@ -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<void> {
try {
const config = await this.getCaptionsConfig();
this.featureConfService.setCaptionsGlobalConfig(config.enabled);
} catch (error) {
this.log.e('Error loading captions config:', error);
throw error;
}
}
}