From c499dbf6e34ba9c4249fb32e2b57974e7613e9ed Mon Sep 17 00:00:00 2001 From: juancarmore Date: Tue, 30 Sep 2025 10:25:42 +0200 Subject: [PATCH] frontend: implement dynamic theme configuration and loading in meeting component --- .../lib/pages/meeting/meeting.component.ts | 40 ++++++++------- .../services/feature-configuration.service.ts | 49 ++++++++++++++++--- .../src/lib/services/global-config.service.ts | 15 +++++- 3 files changed, 77 insertions(+), 27 deletions(-) diff --git a/frontend/projects/shared-meet-components/src/lib/pages/meeting/meeting.component.ts b/frontend/projects/shared-meet-components/src/lib/pages/meeting/meeting.component.ts index cee1f4c..ad87652 100644 --- a/frontend/projects/shared-meet-components/src/lib/pages/meeting/meeting.component.ts +++ b/frontend/projects/shared-meet-components/src/lib/pages/meeting/meeting.component.ts @@ -1,6 +1,6 @@ import { Clipboard } from '@angular/cdk/clipboard'; 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 { MatButtonModule, MatIconButton } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; @@ -44,10 +44,8 @@ import { MeetSignalType } from '@lib/typings/ce/event.model'; import { - ApiDirectiveModule, ParticipantService as ComponentParticipantService, DataPacket_Kind, - LeaveButtonDirective, OpenViduComponentsUiModule, OpenViduService, OpenViduThemeService, @@ -135,6 +133,21 @@ export class MeetingComponent implements OnInit { protected configService: GlobalConfigService ) { 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 { @@ -283,23 +296,12 @@ export class MeetingComponent implements OnInit { await this.generateParticipantToken(); await this.addParticipantNameToUrl(); await this.roomService.loadRoomConfig(this.roomId); - this.showMeeting = true; - const { appearance } = await this.configService.getRoomsAppearanceConfig(); - console.log('Loaded appearance config:', appearance); - if (appearance.themes.length > 0 && appearance.themes[0].enabled) { - const theme = appearance.themes[0]; - 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 - }); - this.features().showThemeSelector = false; - } else { - this.ovThemeService.resetThemeVariables(); - this.features().showThemeSelector = true; - } + // The meeting view must be shown before loading the appearance config, + // as it contains theme information that might be applied immediately + // when the meeting view is rendered + this.showMeeting = true; + await this.configService.loadRoomsAppearanceConfig(); combineLatest([ this.ovComponentsParticipantService.remoteParticipants$, diff --git a/frontend/projects/shared-meet-components/src/lib/services/feature-configuration.service.ts b/frontend/projects/shared-meet-components/src/lib/services/feature-configuration.service.ts index e5227bb..14342cf 100644 --- a/frontend/projects/shared-meet-components/src/lib/services/feature-configuration.service.ts +++ b/frontend/projects/shared-meet-components/src/lib/services/feature-configuration.service.ts @@ -1,5 +1,6 @@ import { computed, Injectable, signal } from '@angular/core'; import { + MeetAppearanceConfig, MeetRoomConfig, ParticipantPermissions, ParticipantRole, @@ -32,6 +33,15 @@ export interface ApplicationFeatures { canModerateRoom: boolean; canRecordRoom: 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, canRecordRoom: false, - canRetrieveRecordings: false + canRetrieveRecordings: false, + + hasCustomTheme: false, + themeConfig: undefined }; /** @@ -72,6 +85,7 @@ export class FeatureConfigurationService { protected participantPermissions = signal(undefined); protected participantRole = signal(undefined); protected recordingPermissions = signal(undefined); + protected appearanceConfig = signal(undefined); // Computed signal to derive features based on current configurations public readonly features = computed(() => @@ -79,7 +93,8 @@ export class FeatureConfigurationService { this.roomConfig(), this.participantPermissions(), 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 { - return this.features()[featureName]; + setAppearanceConfig(config: MeetAppearanceConfig): void { + this.log.d('Updating appearance config', config); + this.appearanceConfig.set(config); } /** @@ -133,7 +149,8 @@ export class FeatureConfigurationService { roomConfig?: MeetRoomConfig, participantPerms?: ParticipantPermissions, role?: ParticipantRole, - recordingPerms?: RecordingPermissions + recordingPerms?: RecordingPermissions, + appearanceConfig?: MeetAppearanceConfig ): ApplicationFeatures { // Start with default configuration const features: ApplicationFeatures = { ...DEFAULT_FEATURES }; @@ -178,6 +195,24 @@ export class FeatureConfigurationService { 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); return features; } @@ -189,5 +224,7 @@ export class FeatureConfigurationService { this.roomConfig.set(undefined); this.participantPermissions.set(undefined); this.participantRole.set(undefined); + this.recordingPermissions.set(undefined); + this.appearanceConfig.set(undefined); } } diff --git a/frontend/projects/shared-meet-components/src/lib/services/global-config.service.ts b/frontend/projects/shared-meet-components/src/lib/services/global-config.service.ts index 932d57d..745ca0f 100644 --- a/frontend/projects/shared-meet-components/src/lib/services/global-config.service.ts +++ b/frontend/projects/shared-meet-components/src/lib/services/global-config.service.ts @@ -1,5 +1,5 @@ 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 { LoggerService } from 'openvidu-components-angular'; @@ -15,7 +15,8 @@ export class GlobalConfigService { constructor( protected loggerService: LoggerService, - protected httpService: HttpService + protected httpService: HttpService, + protected featureConfService: FeatureConfigurationService ) { this.log = this.loggerService.get('OpenVidu Meet - GlobalConfigService'); } @@ -66,6 +67,16 @@ export class GlobalConfigService { return await this.httpService.getRequest<{ appearance: MeetAppearanceConfig }>(path); } + async loadRoomsAppearanceConfig(): Promise { + 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) { const path = `${this.GLOBAL_CONFIG_API}/rooms/appearance`; await this.httpService.putRequest(path, { appearance: config });