frontend: implement dynamic theme configuration and loading in meeting component

This commit is contained in:
juancarmore 2025-09-30 10:25:42 +02:00
parent e418735322
commit c499dbf6e3
3 changed files with 77 additions and 27 deletions

View File

@ -1,6 +1,6 @@
import { Clipboard } from '@angular/cdk/clipboard'; import { Clipboard } from '@angular/cdk/clipboard';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { Component, OnInit, Signal } from '@angular/core'; import { Component, effect, OnInit, Signal } from '@angular/core';
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms'; import { FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
import { MatButtonModule, MatIconButton } from '@angular/material/button'; import { MatButtonModule, MatIconButton } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card'; import { MatCardModule } from '@angular/material/card';
@ -44,10 +44,8 @@ import {
MeetSignalType MeetSignalType
} from '@lib/typings/ce/event.model'; } from '@lib/typings/ce/event.model';
import { import {
ApiDirectiveModule,
ParticipantService as ComponentParticipantService, ParticipantService as ComponentParticipantService,
DataPacket_Kind, DataPacket_Kind,
LeaveButtonDirective,
OpenViduComponentsUiModule, OpenViduComponentsUiModule,
OpenViduService, OpenViduService,
OpenViduThemeService, OpenViduThemeService,
@ -135,6 +133,21 @@ export class MeetingComponent implements OnInit {
protected configService: GlobalConfigService protected configService: GlobalConfigService
) { ) {
this.features = this.featureConfService.features; this.features = this.featureConfService.features;
// Change theme variables when custom theme is enabled
effect(async () => {
if (this.features().hasCustomTheme) {
const theme = this.features().themeConfig;
this.ovThemeService.updateThemeVariables({
'--ov-primary-action-color': theme?.primaryColor,
'--ov-secondary-action-color': theme?.secondaryColor,
'--ov-background-color': theme?.backgroundColor,
'--ov-surface-color': theme?.surfaceColor
});
} else {
this.ovThemeService.resetThemeVariables();
}
});
} }
get roomName(): string { get roomName(): string {
@ -283,23 +296,12 @@ export class MeetingComponent implements OnInit {
await this.generateParticipantToken(); await this.generateParticipantToken();
await this.addParticipantNameToUrl(); await this.addParticipantNameToUrl();
await this.roomService.loadRoomConfig(this.roomId); await this.roomService.loadRoomConfig(this.roomId);
this.showMeeting = true;
const { appearance } = await this.configService.getRoomsAppearanceConfig(); // The meeting view must be shown before loading the appearance config,
console.log('Loaded appearance config:', appearance); // as it contains theme information that might be applied immediately
if (appearance.themes.length > 0 && appearance.themes[0].enabled) { // when the meeting view is rendered
const theme = appearance.themes[0]; this.showMeeting = true;
this.ovThemeService.updateThemeVariables({ await this.configService.loadRoomsAppearanceConfig();
'--ov-primary-action-color': theme.primaryColor,
'--ov-secondary-action-color': theme.secondaryColor,
'--ov-background-color': theme.backgroundColor,
'--ov-surface-color': theme.surfaceColor
});
this.features().showThemeSelector = false;
} else {
this.ovThemeService.resetThemeVariables();
this.features().showThemeSelector = true;
}
combineLatest([ combineLatest([
this.ovComponentsParticipantService.remoteParticipants$, this.ovComponentsParticipantService.remoteParticipants$,

View File

@ -1,5 +1,6 @@
import { computed, Injectable, signal } from '@angular/core'; import { computed, Injectable, signal } from '@angular/core';
import { import {
MeetAppearanceConfig,
MeetRoomConfig, MeetRoomConfig,
ParticipantPermissions, ParticipantPermissions,
ParticipantRole, ParticipantRole,
@ -32,6 +33,15 @@ export interface ApplicationFeatures {
canModerateRoom: boolean; canModerateRoom: boolean;
canRecordRoom: boolean; canRecordRoom: boolean;
canRetrieveRecordings: boolean; canRetrieveRecordings: boolean;
// Appearance
hasCustomTheme: boolean;
themeConfig?: {
primaryColor?: string;
secondaryColor?: string;
backgroundColor?: string;
surfaceColor?: string;
};
} }
/** /**
@ -54,7 +64,10 @@ const DEFAULT_FEATURES: ApplicationFeatures = {
canModerateRoom: false, canModerateRoom: false,
canRecordRoom: false, canRecordRoom: false,
canRetrieveRecordings: false canRetrieveRecordings: false,
hasCustomTheme: false,
themeConfig: undefined
}; };
/** /**
@ -72,6 +85,7 @@ export class FeatureConfigurationService {
protected participantPermissions = signal<ParticipantPermissions | undefined>(undefined); protected participantPermissions = signal<ParticipantPermissions | undefined>(undefined);
protected participantRole = signal<ParticipantRole | undefined>(undefined); protected participantRole = signal<ParticipantRole | undefined>(undefined);
protected recordingPermissions = signal<RecordingPermissions | undefined>(undefined); protected recordingPermissions = signal<RecordingPermissions | undefined>(undefined);
protected appearanceConfig = signal<MeetAppearanceConfig | undefined>(undefined);
// Computed signal to derive features based on current configurations // Computed signal to derive features based on current configurations
public readonly features = computed<ApplicationFeatures>(() => public readonly features = computed<ApplicationFeatures>(() =>
@ -79,7 +93,8 @@ export class FeatureConfigurationService {
this.roomConfig(), this.roomConfig(),
this.participantPermissions(), this.participantPermissions(),
this.participantRole(), this.participantRole(),
this.recordingPermissions() this.recordingPermissions(),
this.appearanceConfig()
) )
); );
@ -120,10 +135,11 @@ export class FeatureConfigurationService {
} }
/** /**
* Checks if a specific feature is enabled * Updates appearance config
*/ */
isFeatureEnabled(featureName: keyof ApplicationFeatures): boolean { setAppearanceConfig(config: MeetAppearanceConfig): void {
return this.features()[featureName]; this.log.d('Updating appearance config', config);
this.appearanceConfig.set(config);
} }
/** /**
@ -133,7 +149,8 @@ export class FeatureConfigurationService {
roomConfig?: MeetRoomConfig, roomConfig?: MeetRoomConfig,
participantPerms?: ParticipantPermissions, participantPerms?: ParticipantPermissions,
role?: ParticipantRole, role?: ParticipantRole,
recordingPerms?: RecordingPermissions recordingPerms?: RecordingPermissions,
appearanceConfig?: MeetAppearanceConfig
): ApplicationFeatures { ): ApplicationFeatures {
// Start with default configuration // Start with default configuration
const features: ApplicationFeatures = { ...DEFAULT_FEATURES }; const features: ApplicationFeatures = { ...DEFAULT_FEATURES };
@ -178,6 +195,24 @@ export class FeatureConfigurationService {
features.canRetrieveRecordings = recordingPerms.canRetrieveRecordings; features.canRetrieveRecordings = recordingPerms.canRetrieveRecordings;
} }
// Apply appearance configuration
if (appearanceConfig && appearanceConfig.themes.length > 0) {
const theme = appearanceConfig.themes[0];
const hasEnabledTheme = theme.enabled;
features.hasCustomTheme = hasEnabledTheme;
features.showThemeSelector = !hasEnabledTheme;
if (hasEnabledTheme) {
features.themeConfig = {
primaryColor: theme.primaryColor,
secondaryColor: theme.secondaryColor,
backgroundColor: theme.backgroundColor,
surfaceColor: theme.surfaceColor
};
}
}
this.log.d('Calculated features', features); this.log.d('Calculated features', features);
return features; return features;
} }
@ -189,5 +224,7 @@ export class FeatureConfigurationService {
this.roomConfig.set(undefined); this.roomConfig.set(undefined);
this.participantPermissions.set(undefined); this.participantPermissions.set(undefined);
this.participantRole.set(undefined); this.participantRole.set(undefined);
this.recordingPermissions.set(undefined);
this.appearanceConfig.set(undefined);
} }
} }

View File

@ -1,5 +1,5 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { HttpService } from '@lib/services'; import { FeatureConfigurationService, HttpService } from '@lib/services';
import { AuthMode, MeetAppearanceConfig, SecurityConfig, WebhookConfig } from '@lib/typings/ce'; import { AuthMode, MeetAppearanceConfig, SecurityConfig, WebhookConfig } from '@lib/typings/ce';
import { LoggerService } from 'openvidu-components-angular'; import { LoggerService } from 'openvidu-components-angular';
@ -15,7 +15,8 @@ export class GlobalConfigService {
constructor( constructor(
protected loggerService: LoggerService, protected loggerService: LoggerService,
protected httpService: HttpService protected httpService: HttpService,
protected featureConfService: FeatureConfigurationService
) { ) {
this.log = this.loggerService.get('OpenVidu Meet - GlobalConfigService'); this.log = this.loggerService.get('OpenVidu Meet - GlobalConfigService');
} }
@ -66,6 +67,16 @@ export class GlobalConfigService {
return await this.httpService.getRequest<{ appearance: MeetAppearanceConfig }>(path); return await this.httpService.getRequest<{ appearance: MeetAppearanceConfig }>(path);
} }
async loadRoomsAppearanceConfig(): Promise<void> {
try {
const config = await this.getRoomsAppearanceConfig();
this.featureConfService.setAppearanceConfig(config.appearance);
} catch (error) {
this.log.e('Error loading rooms appearance config:', error);
throw error;
}
}
async saveRoomsAppearanceConfig(config: MeetAppearanceConfig) { async saveRoomsAppearanceConfig(config: MeetAppearanceConfig) {
const path = `${this.GLOBAL_CONFIG_API}/rooms/appearance`; const path = `${this.GLOBAL_CONFIG_API}/rooms/appearance`;
await this.httpService.putRequest(path, { appearance: config }); await this.httpService.putRequest(path, { appearance: config });