frontend: Decouples UI features from moderation role

Moves UI feature flags from `canModerateRoom` to granular `meetingUI` properties.
This change provides more precise control over UI elements,
allowing for customization based on specific feature flags rather than
solely relying on the user's moderation status.

Refactors components to utilize the new `meetingUI` computed signal
for determining the visibility of UI elements such as share link, layout
selector, captions controls, and leave menu. Also, the logic to enable theme
selector, start/stop recording, view recordings, join meeting and kick
participants has been included.

Updates features calculation to properly include room config
and permissions to show or hide features

This improves flexibility in managing the user interface based on a
combination of room configuration and user permissions.
This commit is contained in:
CSantosM 2026-02-18 12:57:14 +01:00
parent 583fdbe608
commit 2df238f0cc
17 changed files with 198 additions and 193 deletions

View File

@ -41,7 +41,7 @@ export class MeetingCustomLayoutComponent {
meetingUrl = this.meetingContextService.meetingUrl;
shouldShowLinkOverlay = computed(() => {
const hasNoRemotes = this.remoteParticipants().length === 0;
return this.meetingContextService.canModerateRoom() && hasNoRemotes;
return this.meetingContextService.meetingUI().showShareAccessLinks && hasNoRemotes;
});
linkOverlayConfig = {
title: 'Start collaborating',
@ -51,8 +51,8 @@ export class MeetingCustomLayoutComponent {
};
areCaptionsEnabledByUser = this.captionsService.areCaptionsEnabledByUser;
isLayoutSwitchingAllowed = this.meetingContextService.allowLayoutSwitching;
isSmartMosaicActive = computed(() => this.isLayoutSwitchingAllowed() && this.layoutService.isSmartMosaicEnabled());
private showLayoutSelector = computed(() => this.meetingContextService.meetingUI().showLayoutSelector);
isSmartMosaicActive = computed(() => this.showLayoutSelector() && this.layoutService.isSmartMosaicEnabled());
captions = this.captionsService.captions;
remoteParticipants = this.meetingContextService.remoteParticipants;
@ -188,7 +188,7 @@ export class MeetingCustomLayoutComponent {
private setupSpeakerTrackingEffect(): void {
effect(() => {
const room = this.lkRoom();
if (this.isLayoutSwitchingAllowed() && room) {
if (this.showLayoutSelector() && room) {
this.layoutService.initializeSpeakerTracking(room);
}
});

View File

@ -1,5 +1,5 @@
import { CommonModule } from '@angular/common';
import { Component, inject } from '@angular/core';
import { Component, computed, inject } from '@angular/core';
import { LoggerService } from 'openvidu-components-angular';
import { ShareMeetingLinkComponent } from '../../components/share-meeting-link/share-meeting-link.component';
import { MeetingContextService } from '../../services/meeting-context.service';
@ -21,7 +21,7 @@ export class MeetingInvitePanelComponent {
protected loggerService = inject(LoggerService);
protected log = this.loggerService.get('OpenVidu Meet - MeetingInvitePanel');
showShareLink = this.meetingContextService.canModerateRoom;
showShareLink = computed(() => this.meetingContextService.meetingUI().showShareAccessLinks);
meetingUrl = this.meetingContextService.meetingUrl;
onCopyClicked(): void {

View File

@ -1,5 +1,5 @@
<!-- Grid Layout Configuration Section -->
@if (isLayoutSwitchingAllowed()) {
@if (showLayoutSelector()) {
<div class="layout-section">
<div class="section-header">
<mat-icon class="section-icon material-symbols-outlined">browse</mat-icon>

View File

@ -1,5 +1,5 @@
import { CommonModule } from '@angular/common';
import { Component, inject } from '@angular/core';
import { Component, computed, inject } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatIconModule } from '@angular/material/icon';
@ -34,7 +34,7 @@ export class MeetingSettingsExtensionsComponent {
private readonly layoutService = inject(MeetingLayoutService);
/** Whether the layout switching feature is allowed */
isLayoutSwitchingAllowed = this.meetingContextService.allowLayoutSwitching;
showLayoutSelector = computed(() => this.meetingContextService.meetingUI().showLayoutSelector);
/** Expose LayoutMode enum to template */
LayoutMode = MeetLayoutMode;
/** Current layout mode */

View File

@ -27,16 +27,14 @@ export class MeetingToolbarExtraButtonsComponent {
protected log = this.loggerService.get('OpenVidu Meet - MeetingToolbarExtraButtons');
/** Whether to show the copy link button (only for moderators) */
showCopyLinkButton = this.meetingContextService.canModerateRoom;
showCopyLinkButton = computed(() => this.meetingContextService.meetingUI().showShareAccessLinks);
copyLinkTooltip = 'Copy the meeting link';
copyLinkText = 'Copy meeting link';
/** Captions status based on room and global configuration */
captionsStatus = this.meetingContextService.getCaptionsStatus;
/** Whether to show the captions button (visible when not HIDDEN) */
showCaptionsButton = computed(() => this.captionsStatus() !== 'HIDDEN');
showCaptionsButton = computed(() => this.meetingContextService.meetingUI().showCaptionsControls);
/** Whether captions button is disabled (true when DISABLED_WITH_WARNING) */
isCaptionsButtonDisabled = computed(() => this.captionsStatus() === 'DISABLED_WITH_WARNING');
isCaptionsButtonDisabled = computed(() => this.meetingContextService.meetingUI().showCaptionsControlsDisabled);
/** Whether captions are currently enabled by the user */
areCaptionsEnabledByUser = this.captionService.areCaptionsEnabledByUser;

View File

@ -1,5 +1,5 @@
import { CommonModule } from '@angular/common';
import { Component, inject } from '@angular/core';
import { Component, computed, inject } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatDividerModule } from '@angular/material/divider';
import { MatIconModule } from '@angular/material/icon';
@ -25,7 +25,7 @@ export class MeetingToolbarLeaveButtonComponent {
protected loggerService = inject(LoggerService);
protected log = this.loggerService.get('OpenVidu Meet - MeetingToolbarLeaveButtons');
showLeaveMenu = this.meetingContextService.canModerateRoom;
showLeaveMenu = computed(() => this.meetingContextService.meetingUI().showEndMeeting);
isMobile = this.meetingContextService.isMobile;
leaveMenuTooltip = 'Leave options';

View File

@ -1,5 +1,5 @@
<!-- Grid Layout Settings Button -->
@if (isLayoutSwitchingAllowed()) {
@if (showLayoutSelector()) {
<button
mat-menu-item
id="grid-layout-settings-btn"

View File

@ -1,5 +1,5 @@
import { CommonModule } from '@angular/common';
import { Component, inject } from '@angular/core';
import { Component, computed, inject } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatMenuModule } from '@angular/material/menu';
@ -23,7 +23,7 @@ export class MeetingToolbarMoreOptionsMenuComponent {
private panelService = inject(PanelService);
isMobileView = this.viewportService.isMobile;
isLayoutSwitchingAllowed = this.meetingContextService.allowLayoutSwitching;
showLayoutSelector = computed(() => this.meetingContextService.meetingUI().showLayoutSelector);
/**
* Opens the settings panel to allow users to change grid layout

View File

@ -18,35 +18,37 @@
[token]="roomMemberToken()!"
[prejoin]="true"
[prejoinDisplayParticipantName]="false"
[videoEnabled]="features().media.videoEnabled"
[audioEnabled]="features().media.audioEnabled"
[videoEnabled]="features().videoEnabled"
[audioEnabled]="features().audioEnabled"
[e2eeKey]="e2eeKey()"
[toolbarRoomName]="roomName()"
[toolbarCameraButton]="features().ui.showCamera"
[toolbarMicrophoneButton]="features().ui.showMicrophone"
[toolbarScreenshareButton]="features().ui.showScreenShare"
[toolbarLeaveButton]="!features().permissions.canModerateRoom"
[toolbarRecordingButton]="features().permissions.canRecordRoom"
[toolbarViewRecordingsButton]="features().permissions.canRetrieveRecordings && hasRecordings()"
[toolbarCameraButton]="features().showCamera"
[toolbarMicrophoneButton]="features().showMicrophone"
[toolbarScreenshareButton]="features().showScreenShare"
[toolbarLeaveButton]="!features().showEndMeeting"
[toolbarRecordingButton]="features().showStartStopRecording"
[toolbarViewRecordingsButton]="features().showViewRecordings && hasRecordings()"
[toolbarBroadcastingButton]="false"
[toolbarChatPanelButton]="features().ui.showChat"
[toolbarBackgroundEffectsButton]="features().ui.showBackgrounds"
[toolbarParticipantsPanelButton]="features().ui.showParticipantList"
[toolbarSettingsButton]="features().ui.showSettings"
[toolbarFullscreenButton]="features().ui.showFullscreen"
[toolbarActivitiesPanelButton]="features().ui.showRecordingPanel"
[activitiesPanelRecordingActivity]="features().ui.showRecordingPanel"
[toolbarChatPanelButton]="features().showChat"
[toolbarBackgroundEffectsButton]="features().showBackgrounds"
[toolbarParticipantsPanelButton]="features().showParticipantList"
[toolbarSettingsButton]="features().showSettings"
[toolbarFullscreenButton]="features().showFullscreen"
[toolbarActivitiesPanelButton]="features().showStartStopRecording"
[activitiesPanelRecordingActivity]="
features().showStartStopRecording || (features().showViewRecordings && hasRecordings())
"
[recordingActivityShowControls]="{
play: false,
download: false,
delete: false,
externalView: true
}"
[recordingActivityStartStopRecordingButton]="features().permissions.canRecordRoom"
[recordingActivityViewRecordingsButton]="features().permissions.canRetrieveRecordings && hasRecordings()"
[recordingActivityStartStopRecordingButton]="features().showStartStopRecording"
[recordingActivityViewRecordingsButton]="features().showViewRecordings && hasRecordings()"
[recordingActivityShowRecordingsList]="false"
[activitiesPanelBroadcastingActivity]="false"
[showThemeSelector]="features().ui.showThemeSelector"
[showThemeSelector]="features().showThemeSelector"
[showDisconnectionDialog]="false"
(onRoomCreated)="onRoomCreated($event)"
(onParticipantConnected)="onParticipantConnected($event)"

View File

@ -6,7 +6,6 @@ import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { OpenViduComponentsUiModule, OpenViduThemeMode, OpenViduThemeService, Room } from 'openvidu-components-angular';
import { Subject } from 'rxjs';
import { NotificationService } from '../../../../shared/services/notification.service';
import { RoomFeatureService } from '../../../../shared/services/room-feature.service';
import { RuntimeConfigService } from '../../../../shared/services/runtime-config.service';
import { SoundService } from '../../../../shared/services/sound.service';
import { MeetingLobbyComponent } from '../../components/meeting-lobby/meeting-lobby.component';
@ -36,7 +35,6 @@ export class MeetingComponent implements OnInit {
protected lobbyService = inject(MeetingLobbyService);
protected eventHandlerService = inject(MeetingEventHandlerService);
protected captionsService = inject(MeetingCaptionsService);
protected roomFeatureService = inject(RoomFeatureService);
protected ovThemeService = inject(OpenViduThemeService);
protected notificationService = inject(NotificationService);
protected soundService = inject(SoundService);
@ -63,8 +61,7 @@ export class MeetingComponent implements OnInit {
roomMemberToken = this.lobbyService.roomMemberToken;
e2eeKey = this.lobbyService.e2eeKeyValue;
localParticipant = this.meetingContextService.localParticipant;
features = this.roomFeatureService.features;
features = this.meetingContextService.meetingUI;
hasRecordings = this.meetingContextService.hasRecordings;
protected destroy$ = new Subject<void>();
@ -72,9 +69,10 @@ export class MeetingComponent implements OnInit {
constructor() {
// Change theme variables when custom theme is enabled
effect(() => {
const features = this.features();
if (features.appearance.hasCustomTheme) {
const theme = features.appearance.themeConfig;
const { themes } = this.meetingContextService.meetingAppearance();
const hasTheme = themes.length > 0 && themes[0].enabled;
if (hasTheme) {
const theme = themes[0];
this.ovThemeService.setTheme(theme!.baseTheme as unknown as OpenViduThemeMode);
this.ovThemeService.updateThemeVariables({
'--ov-primary-action-color': theme?.primaryColor,

View File

@ -1,6 +1,7 @@
import { computed, effect, inject, Injectable, signal } from '@angular/core';
import { MeetRoom } from 'node_modules/@openvidu-meet/typings/dist/room';
import { ParticipantService, Room, ViewportService } from 'openvidu-components-angular';
import { GlobalConfigService } from '../../../shared/services/global-config.service';
import { RoomFeatureService } from '../../../shared/services/room-feature.service';
import { SessionStorageService } from '../../../shared/services/session-storage.service';
import { CustomParticipantModel } from '../models';
@ -16,6 +17,7 @@ import { CustomParticipantModel } from '../models';
export class MeetingContextService {
private readonly ovParticipantService = inject(ParticipantService);
private readonly roomFeatureService = inject(RoomFeatureService);
private readonly globalConfigService = inject(GlobalConfigService);
private readonly viewportService = inject(ViewportService);
private readonly sessionStorageService = inject(SessionStorageService);
@ -63,12 +65,14 @@ export class MeetingContextService {
return local ? [local, ...remotes] : remotes;
});
/** Computed signal for whether the current user can moderate the room */
readonly canModerateRoom = computed(() => this.roomFeatureService.features().permissions.canModerateRoom);
/** Computed signal for whether layout switching is allowed */
readonly allowLayoutSwitching = computed(() => this.roomFeatureService.features().ui.showLayoutSelector);
/** Computed signal for captions status based on room and global configuration */
readonly getCaptionsStatus = computed(() => this.roomFeatureService.features().ui.captionsStatus);
/**
* Computed signal for meeting features
*/
readonly meetingUI = computed(() => this.roomFeatureService.features());
/**
* Computed signal for room appearance configuration from global settings
*/
readonly meetingAppearance = computed(() => this.globalConfigService.roomAppearanceConfig());
/** Readonly signal for whether the device is mobile */
readonly isMobile = this.viewportService.isMobile;

View File

@ -83,7 +83,7 @@ export class MeetingLobbyService {
* The share link is shown only if the room is not closed and the user has permissions to moderate the room
*/
readonly showShareLink = computed(() => {
return !this.roomClosed() && this.meetingContextService.canModerateRoom();
return !this.roomClosed() && this.meetingContextService.meetingUI().showShareAccessLinks;
});
/** Computed signal for meeting URL derived from MeetingContextService */
readonly meetingUrl = computed(() => this.meetingContextService.meetingUrl());

View File

@ -2,7 +2,6 @@ import { computed, Injectable, signal } from '@angular/core';
import {
MeetRoomMember,
MeetRoomMemberPermissions,
MeetRoomMemberRole,
MeetRoomMemberTokenMetadata,
MeetRoomMemberTokenOptions
} from '@openvidu-meet/typings';
@ -24,7 +23,6 @@ export class RoomMemberContextService {
private readonly _participantName = signal<string | undefined>(undefined);
private readonly _isParticipantNameFromUrl = signal<boolean>(false);
private readonly _participantIdentity = signal<string | undefined>(undefined);
private readonly _role = signal<MeetRoomMemberRole | undefined>(undefined);
private readonly _permissions = signal<MeetRoomMemberPermissions | undefined>(undefined);
private readonly _member = signal<MeetRoomMember | undefined>(undefined);
@ -36,8 +34,6 @@ export class RoomMemberContextService {
readonly isParticipantNameFromUrl = this._isParticipantNameFromUrl.asReadonly();
/** Readonly signal for the participant identity */
readonly participantIdentity = this._participantIdentity.asReadonly();
/** Readonly signal for the room member role */
readonly role = this._role.asReadonly();
/** Readonly signal for the room member permissions */
readonly permissions = this._permissions.asReadonly();
/** Readonly signal for the room member info (when memberId is set) */
@ -136,7 +132,6 @@ export class RoomMemberContextService {
this._participantIdentity.set(decodedToken.sub);
}
this._role.set(metadata.baseRole);
this._permissions.set(metadata.effectivePermissions);
// If token contains memberId, fetch and store member info
@ -163,7 +158,6 @@ export class RoomMemberContextService {
this._participantName.set(undefined);
this._isParticipantNameFromUrl.set(false);
this._participantIdentity.set(undefined);
this._role.set(undefined);
this._permissions.set(undefined);
this._member.set(undefined);
}

View File

@ -1,5 +1,3 @@
import { MeetRoomTheme } from '@openvidu-meet/typings';
export interface AppData {
mode: ApplicationMode;
edition: Edition;
@ -22,14 +20,18 @@ export enum Edition {
export type CaptionsStatus = 'HIDDEN' | 'ENABLED' | 'DISABLED_WITH_WARNING';
/**
* Sub-interfaces to group related feature flags
* Interface that defines all available features in the application
*/
export interface MediaFeatures {
export interface RoomFeatures {
/**
* Indicates if video track is enabled in the room (mutued or unmuted)
*/
videoEnabled: boolean;
/**
* Indicates if audio track is enabled in the room (muted or unmuted)
*/
audioEnabled: boolean;
}
export interface UIControls {
/**
* Indicates if camera control is shown in the UI
*/
@ -46,9 +48,9 @@ export interface UIControls {
showScreenShare: boolean;
/**
* Indicates if the recording panel is shown in the UI
* Indicates if the recording controls is shown in the UI
*/
showRecordingPanel: boolean;
showStartStopRecording: boolean;
/**
* Indicates if the chat panel is shown in the UI
@ -79,48 +81,47 @@ export interface UIControls {
*/
showThemeSelector: boolean;
/**
* Indicates if the flag for allowing smart layout is enabled. It's changed manually.
* Indicates if the flag for allowing smart layout is enabled.
*
* It's changed manually (not based on permissions or room config).
*/
showLayoutSelector: boolean;
/**
* Status of captions feature based on room and global configuration
*/
captionsStatus: CaptionsStatus;
}
// captionsStatus: CaptionsStatus;
export interface PermissionsFeatures {
/**
* Indicates if the user has permission to moderate the room
* Indicates if the captions controls (like toggle captions button) is shown in the UI
*/
canModerateRoom: boolean;
showCaptionsControls: boolean;
/**
* Indicates if the user has permission to record the room
* Indicates if the captions controls are shown but disabled in the UI, with a warning that captions are globally disabled
*/
canRecordRoom: boolean;
showCaptionsControlsDisabled: boolean;
/**
* Indicates if the user has permission to retrieve recordings of the room
* Indicates if the share access links controls is shown in the UI
*/
canRetrieveRecordings: boolean;
}
export interface AppearanceFeatures {
hasCustomTheme: boolean;
themeConfig?: MeetRoomTheme;
}
/**
* Interface that defines all available features in the application
*/
export interface RoomFeatures {
// Media capabilities
media: MediaFeatures;
// UI Controls
ui: UIControls;
// Permissions
permissions: PermissionsFeatures;
// Appearance
appearance: AppearanceFeatures;
showShareAccessLinks: boolean;
/**
* Indicates if the make moderator controls is shown in the UI
*/
showMakeModerator: boolean;
/**
* Indicates if the end meeting controls is shown in the UI
*/
showEndMeeting: boolean;
/**
* Indicates if the kick participants controls is shown in the UI
*/
showKickParticipants: boolean;
/**
* Indicates if the view recordings controls is shown in the UI
*/
showViewRecordings: boolean;
/**
* Indicates if the join meeting controls is shown in the UI
*/
showJoinMeeting: boolean;
}

View File

@ -14,7 +14,9 @@ export class GlobalConfigService {
protected log: ILogger = this.loggerService.get('OpenVidu Meet - GlobalConfigService');
private readonly _roomAppearanceConfig = signal<MeetAppearanceConfig | undefined>(undefined);
private readonly _roomAppearanceConfig = signal<MeetAppearanceConfig>({
themes: []
});
private readonly _captionsGlobalEnabled = signal<boolean>(false);
readonly roomAppearanceConfig = this._roomAppearanceConfig.asReadonly();

View File

@ -1,41 +1,36 @@
import { computed, inject, Injectable, signal } from '@angular/core';
import { MeetAppearanceConfig, MeetRoomCaptionsConfig, MeetRoomConfig, MeetRoomMemberPermissions, MeetRoomMemberRole } from '@openvidu-meet/typings';
import { MeetAppearanceConfig, MeetRoomConfig, MeetRoomMemberPermissions } from '@openvidu-meet/typings';
import { LoggerService } from 'openvidu-components-angular';
import { RoomMemberContextService } from '../../domains/room-members/services/room-member-context.service';
import { CaptionsStatus, RoomFeatures } from '../models/app.model';
import { RoomFeatures } from '../models/app.model';
import { FeatureCalculator } from '../utils/features.utils';
import { GlobalConfigService } from './global-config.service';
/**
* Base configuration for features, used as a starting point before applying room-specific and user-specific configurations
*/
const DEFAULT_FEATURES: RoomFeatures = {
media: {
videoEnabled: true,
audioEnabled: true
},
ui: {
showCamera: true,
showMicrophone: true,
showScreenShare: true,
showRecordingPanel: true,
showChat: true,
showBackgrounds: true,
showParticipantList: true,
showSettings: true,
showFullscreen: true,
showThemeSelector: true,
showLayoutSelector: true,
captionsStatus: 'ENABLED'
},
permissions: {
canModerateRoom: false,
canRecordRoom: false,
canRetrieveRecordings: false
},
appearance: {
hasCustomTheme: false,
themeConfig: undefined
}
videoEnabled: true,
audioEnabled: true,
showCamera: true,
showMicrophone: true,
showScreenShare: true,
showStartStopRecording: true,
showChat: true,
showBackgrounds: true,
showParticipantList: true,
showSettings: true,
showFullscreen: true,
showThemeSelector: true,
showLayoutSelector: true,
showCaptionsControls: true,
showCaptionsControlsDisabled: false,
showShareAccessLinks: true,
showMakeModerator: false,
showEndMeeting: false,
showKickParticipants: false,
showViewRecordings: true,
showJoinMeeting: true
};
/**
@ -52,13 +47,13 @@ export class RoomFeatureService {
// Signals to handle reactive state
protected roomConfig = signal<MeetRoomConfig | undefined>(undefined);
permissions = this.roomMemberContextService.permissions;
// Computed signal to derive features based on current configurations
public readonly features = computed<RoomFeatures>(() =>
this.calculateFeatures(
this.roomConfig(),
this.roomMemberContextService.role(),
this.roomMemberContextService.permissions(),
this.permissions(),
this.globalConfigService.roomAppearanceConfig(),
this.globalConfigService.captionsGlobalEnabled()
)
@ -96,87 +91,28 @@ export class RoomFeatureService {
*/
protected calculateFeatures(
roomConfig?: MeetRoomConfig,
role?: MeetRoomMemberRole,
permissions?: MeetRoomMemberPermissions,
appearanceConfig?: MeetAppearanceConfig,
captionsGlobalEnabled: boolean = false
): RoomFeatures {
// Start with default configuration (deep copy per group)
const features: RoomFeatures = {
media: { ...DEFAULT_FEATURES.media },
ui: { ...DEFAULT_FEATURES.ui },
permissions: { ...DEFAULT_FEATURES.permissions },
appearance: { ...DEFAULT_FEATURES.appearance }
};
const features = structuredClone(DEFAULT_FEATURES);
// Apply room configurations
if (roomConfig) {
features.ui.showRecordingPanel = roomConfig.recording.enabled;
features.ui.showChat = roomConfig.chat.enabled;
features.ui.showBackgrounds = roomConfig.virtualBackground.enabled;
features.ui.captionsStatus = this.computeCaptionsStatus(roomConfig.captions, captionsGlobalEnabled);
FeatureCalculator.applyRoomConfig(features, roomConfig, captionsGlobalEnabled);
}
// Apply room member permissions (these can restrict enabled features)
if (permissions) {
// Only restrict if the feature is already enabled
if (features.ui.showRecordingPanel) {
features.permissions.canRecordRoom = permissions.canRecord;
features.permissions.canRetrieveRecordings = permissions.canRetrieveRecordings;
}
if (features.ui.showChat) {
features.ui.showChat = permissions.canReadChat;
// TODO: Handle canWriteChat permissions
}
if (features.ui.showBackgrounds) {
features.ui.showBackgrounds = permissions.canChangeVirtualBackground;
}
// Media features
features.media.videoEnabled = permissions.canPublishVideo;
features.media.audioEnabled = permissions.canPublishAudio;
features.ui.showScreenShare = permissions.canShareScreen;
features.ui.showCamera = features.media.videoEnabled;
features.ui.showMicrophone = features.media.audioEnabled;
FeatureCalculator.applyPermissions(features, permissions);
}
// Apply role-based configurations
if (role) {
features.permissions.canModerateRoom = role === MeetRoomMemberRole.MODERATOR;
}
// Apply appearance configuration
if (appearanceConfig && appearanceConfig.themes.length > 0) {
const theme = appearanceConfig.themes[0];
const hasEnabledTheme = theme.enabled;
features.appearance.hasCustomTheme = hasEnabledTheme;
features.ui.showThemeSelector = !hasEnabledTheme;
if (hasEnabledTheme) {
features.appearance.themeConfig = theme;
}
if (appearanceConfig) {
FeatureCalculator.applyAppearanceConfig(features, appearanceConfig);
}
this.log.d('Calculated features', features);
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,
globalEnabled: boolean
): CaptionsStatus {
if (!roomCaptionsConfig.enabled) {
return 'HIDDEN';
}
return globalEnabled ? 'ENABLED' : 'DISABLED_WITH_WARNING';
}
/**
* Resets all configurations to their initial values
*/

View File

@ -0,0 +1,70 @@
import {
MeetAppearanceConfig,
MeetRoomCaptionsConfig,
MeetRoomConfig,
MeetRoomMemberPermissions
} from '@openvidu-meet/typings';
import { CaptionsStatus, RoomFeatures } from '../models/app.model';
// New helper class for feature calculation logic
export class FeatureCalculator {
static applyRoomConfig(features: RoomFeatures, roomConfig: MeetRoomConfig, captionsGlobalEnabled: boolean): void {
features.showStartStopRecording = roomConfig.recording.enabled;
features.showChat = roomConfig.chat.enabled;
features.showBackgrounds = roomConfig.virtualBackground.enabled;
const captionsStatus = this.computeCaptionsStatus(roomConfig.captions, captionsGlobalEnabled);
features.showCaptionsControls = captionsStatus !== 'HIDDEN';
features.showCaptionsControlsDisabled = captionsStatus === 'DISABLED_WITH_WARNING';
}
static applyPermissions(features: RoomFeatures, permissions: MeetRoomMemberPermissions): void {
// Recording
if (features.showStartStopRecording) {
features.showStartStopRecording = permissions.canRecord;
}
// Chat
if (features.showChat) {
features.showChat = permissions.canReadChat;
// TODO: Handle canWriteChat permissions
// features.showChatInput = permissions.canWriteChat;
}
// Backgrounds
if (features.showBackgrounds) {
features.showBackgrounds = permissions.canChangeVirtualBackground;
}
// Media features
features.videoEnabled = permissions.canPublishVideo;
features.showCamera = permissions.canPublishVideo;
features.audioEnabled = permissions.canPublishAudio;
features.showMicrophone = permissions.canPublishAudio;
features.showScreenShare = permissions.canShareScreen;
features.showShareAccessLinks = permissions.canShareAccessLinks;
features.showMakeModerator = permissions.canMakeModerator;
features.showEndMeeting = permissions.canEndMeeting;
features.showKickParticipants = permissions.canKickParticipants;
features.showViewRecordings = permissions.canRetrieveRecordings;
features.showJoinMeeting = permissions.canJoinMeeting;
}
static applyAppearanceConfig(features: RoomFeatures, appearanceConfig: MeetAppearanceConfig): void {
if (appearanceConfig?.themes.length > 0 && appearanceConfig.themes[0].enabled) {
features.showThemeSelector = false;
}
}
/**
* 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 static computeCaptionsStatus(
roomCaptionsConfig: MeetRoomCaptionsConfig,
globalEnabled: boolean
): CaptionsStatus {
if (!roomCaptionsConfig.enabled) {
return 'HIDDEN';
}
return globalEnabled ? 'ENABLED' : 'DISABLED_WITH_WARNING';
}
}