diff --git a/frontend/projects/shared-meet-components/src/lib/pages/video-room/video-room.component.html b/frontend/projects/shared-meet-components/src/lib/pages/video-room/video-room.component.html index f621b0f..6668f5b 100644 --- a/frontend/projects/shared-meet-components/src/lib/pages/video-room/video-room.component.html +++ b/frontend/projects/shared-meet-components/src/lib/pages/video-room/video-room.component.html @@ -52,17 +52,17 @@ ; constructor( + protected route: ActivatedRoute, protected navigationService: NavigationService, protected participantTokenService: ParticipantTokenService, protected recManagerService: RecordingManagerService, - protected route: ActivatedRoute, protected authService: AuthService, protected ctxService: ContextService, protected roomService: RoomService, protected wcManagerService: WebComponentManagerService, - protected sessionStorageService: SessionStorageService - ) {} + protected sessionStorageService: SessionStorageService, + protected featureConfService: FeatureConfigurationService + ) { + this.featureConfService.features$.subscribe((features) => { + console.log('!!!!!!Feature flags updated:', features); + }); + this.features$ = this.featureConfService.features$; + } async ngOnInit() { this.roomId = this.ctxService.getRoomId(); @@ -120,7 +135,7 @@ export class VideoRoomComponent implements OnInit, OnDestroy { try { await this.generateParticipantToken(); await this.replaceUrlQueryParams(); - await this.loadRoomPreferences(); + await this.roomService.loadPreferences(this.roomId); this.showRoom = true; } catch (error) { console.error('Error accessing room:', error); @@ -281,27 +296,6 @@ export class VideoRoomComponent implements OnInit, OnDestroy { } } - /** - * Loads the room preferences from the global preferences service and assigns them to the component. - * - * This method fetches the room preferences asynchronously and updates the component's properties - * based on the fetched preferences. It also updates the UI flags to show or hide certain features - * like chat, recording, and activity panel based on the preferences. - * - * @returns {Promise} A promise that resolves when the room preferences have been loaded and applied. - */ - private async loadRoomPreferences() { - try { - this.roomPreferences = await this.roomService.getRoomPreferences(); - } catch (error) { - console.error('Error loading room preferences:', error); - } - - this.featureFlags.showChat = this.roomPreferences.chatPreferences.enabled; - this.featureFlags.showRecording = this.roomPreferences.recordingPreferences.enabled; - this.featureFlags.showBackgrounds = this.roomPreferences.virtualBackgroundPreferences.enabled; - } - /** * Configures the feature flags based on participant permissions. */ diff --git a/frontend/projects/shared-meet-components/src/lib/services/context/context.service.ts b/frontend/projects/shared-meet-components/src/lib/services/context/context.service.ts index ff64f0a..0989011 100644 --- a/frontend/projects/shared-meet-components/src/lib/services/context/context.service.ts +++ b/frontend/projects/shared-meet-components/src/lib/services/context/context.service.ts @@ -2,7 +2,13 @@ import { Injectable } from '@angular/core'; import { jwtDecode } from 'jwt-decode'; import { ApplicationMode, ContextData, Edition } from '../../models/context.model'; import { LoggerService } from 'openvidu-components-angular'; -import { AuthMode, HttpService, ParticipantRole } from 'projects/shared-meet-components/src/public-api'; +import { + AuthMode, + HttpService, + OpenViduMeetPermissions, + ParticipantRole +} from 'projects/shared-meet-components/src/public-api'; +import { FeatureConfigurationService } from '../feature-configuration/feature-configuration.service'; @Injectable({ providedIn: 'root' @@ -36,14 +42,15 @@ export class ContextService { leaveRedirectUrl: '' }; - private log; + protected log; /** * Initializes a new instance of the ContextService class. */ constructor( - private loggerService: LoggerService, - private httpService: HttpService + protected loggerService: LoggerService, + protected featureConfService: FeatureConfigurationService, + protected httpService: HttpService ) { this.log = this.loggerService.get('OpenVidu Meet - ContextService'); } @@ -126,24 +133,26 @@ export class ContextService { setParticipantTokenAndUpdateContext(token: string): void { try { const decodedToken = this.getValidDecodedToken(token); - this.context.participantToken = token; - this.context.participantPermissions = decodedToken.metadata.permissions; - this.context.participantRole = decodedToken.metadata.role; - - // Update feature configuration based on the new token - // this.updateFeatureConfiguration(); + this.setParticipantToken(token); + this.setParticipantPermissions(decodedToken.metadata.permissions); + this.setParticipantRole(decodedToken.metadata.role); } catch (error: any) { this.log.e('Error setting token in context', error); throw new Error('Error setting token', error); } } + setParticipantToken(token: string): void { + this.context.participantToken = token; + } + getParticipantToken(): string { return this.context.participantToken; } setParticipantRole(participantRole: ParticipantRole): void { this.context.participantRole = participantRole; + this.featureConfService.setParticipantRole(participantRole); } getParticipantRole(): ParticipantRole { @@ -154,6 +163,11 @@ export class ContextService { return this.context.participantRole === ParticipantRole.MODERATOR; } + setParticipantPermissions(permissions: OpenViduMeetPermissions): void { + this.context.participantPermissions = permissions; + this.featureConfService.setParticipantPermissions(permissions); + } + getParticipantPermissions() { return this.context.participantPermissions; } @@ -162,6 +176,7 @@ export class ContextService { try { const decodedToken = this.getValidDecodedToken(token); this.context.recordingPermissions = decodedToken.metadata.recordingPermissions; + this.featureConfService.setRecordingPermissions(decodedToken.metadata.recordingPermissions); } catch (error: any) { this.log.e('Error setting recording token in context', error); throw new Error('Error setting recording token', error); diff --git a/frontend/projects/shared-meet-components/src/lib/services/feature-configuration/feature-configuration.service.spec.ts b/frontend/projects/shared-meet-components/src/lib/services/feature-configuration/feature-configuration.service.spec.ts new file mode 100644 index 0000000..e5af4fc --- /dev/null +++ b/frontend/projects/shared-meet-components/src/lib/services/feature-configuration/feature-configuration.service.spec.ts @@ -0,0 +1,15 @@ +import { TestBed } from '@angular/core/testing'; +import { FeatureConfigurationService } from './feature-configuration.service'; + +describe('FeatureConfigurationService', () => { + let service: FeatureConfigurationService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(FeatureConfigurationService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/frontend/projects/shared-meet-components/src/lib/services/feature-configuration/feature-configuration.service.ts b/frontend/projects/shared-meet-components/src/lib/services/feature-configuration/feature-configuration.service.ts new file mode 100644 index 0000000..b797ca3 --- /dev/null +++ b/frontend/projects/shared-meet-components/src/lib/services/feature-configuration/feature-configuration.service.ts @@ -0,0 +1,249 @@ +import { Injectable } from '@angular/core'; +import { BehaviorSubject, Observable, combineLatest, map } from 'rxjs'; +import { LoggerService } from 'openvidu-components-angular'; +import { + MeetRoomPreferences, + GlobalPreferences, + OpenViduMeetPermissions, + ParticipantRole, + RecordingPermissions +} from '@lib/typings/ce'; +import { GlobalPreferencesService } from '../global-preferences/global-preferences.service'; + +/** + * Interface that defines all available features in the application + */ +export interface ApplicationFeatures { + // Video Room Features + videoEnabled: boolean; + audioEnabled: boolean; + showMicrophone: boolean; + showCamera: boolean; + showScreenShare: boolean; + showPrejoin: boolean; + + // Communication Features + showChat: boolean; + showRecording: boolean; + showBackgrounds: boolean; + + // UI Features + showParticipantList: boolean; + showSettings: boolean; + showFullscreen: boolean; + + // Admin Features + canModerateRoom: boolean; + canManageRecordings: boolean; + canAccessConsole: boolean; + + // Recording Features + canDeleteRecordings: boolean; + canRetrieveRecordings: boolean; +} + +/** + * Base configuration for default features + */ +const DEFAULT_FEATURES: ApplicationFeatures = { + videoEnabled: true, + audioEnabled: true, + showMicrophone: true, + showCamera: true, + showScreenShare: true, + showPrejoin: true, + showChat: true, + showRecording: true, + showBackgrounds: true, + showParticipantList: true, + showSettings: true, + showFullscreen: true, + canModerateRoom: false, + canManageRecordings: false, + canAccessConsole: false, + canDeleteRecordings: false, + canRetrieveRecordings: false +}; + +/** + * Centralized service to manage feature configuration + * based on global preferences, room preferences, and participant permissions + */ +@Injectable({ + providedIn: 'root' +}) +export class FeatureConfigurationService { + protected log; + + // Subjects to handle reactive state + protected globalPreferencesSubject = new BehaviorSubject(null); + protected roomPreferencesSubject = new BehaviorSubject(null); + protected participantPermissionsSubject = new BehaviorSubject(null); + protected recordingPermissionsSubject = new BehaviorSubject(null); + protected participantRoleSubject = new BehaviorSubject(null); + + // Observable that combines all configurations + public readonly features$: Observable; + + constructor( + protected loggerService: LoggerService, + protected globalPreferencesService: GlobalPreferencesService + ) { + this.log = this.loggerService.get('OpenVidu Meet - FeatureConfigurationService'); + + // Configure the combined observable + this.features$ = combineLatest([ + this.globalPreferencesSubject.asObservable(), + this.roomPreferencesSubject.asObservable(), + this.participantPermissionsSubject.asObservable(), + this.recordingPermissionsSubject.asObservable(), + this.participantRoleSubject.asObservable() + ]).pipe( + map(([globalPrefs, roomPrefs, participantPerms, recordingPerms, role]) => + this.calculateFeatures(globalPrefs, roomPrefs, participantPerms, recordingPerms, role) + ) + ); + } + + /** + * Updates global preferences + */ + setGlobalPreferences(preferences: GlobalPreferences | null): void { + this.log.d('Updating global preferences', preferences); + this.globalPreferencesSubject.next(preferences); + } + + /** + * Updates room preferences + */ + setRoomPreferences(preferences: MeetRoomPreferences | null): void { + this.log.d('Updating room preferences', preferences); + this.roomPreferencesSubject.next(preferences); + } + + /** + * Updates participant permissions + */ + setParticipantPermissions(permissions: OpenViduMeetPermissions | null): void { + this.log.d('Updating participant permissions', permissions); + this.participantPermissionsSubject.next(permissions); + } + + /** + * Updates recording permissions + */ + setRecordingPermissions(permissions: RecordingPermissions | null): void { + this.log.d('Updating recording permissions', permissions); + this.recordingPermissionsSubject.next(permissions); + } + + /** + * Updates participant role + */ + setParticipantRole(role: ParticipantRole | null): void { + this.log.d('Updating participant role', role); + this.participantRoleSubject.next(role); + } + + /** + * Gets the current feature configuration synchronously + */ + getCurrentFeatures(): ApplicationFeatures { + return this.calculateFeatures( + this.globalPreferencesSubject.value, + this.roomPreferencesSubject.value, + this.participantPermissionsSubject.value, + this.recordingPermissionsSubject.value, + this.participantRoleSubject.value + ); + } + + /** + * Checks if a specific feature is enabled + */ + isFeatureEnabled(featureName: keyof ApplicationFeatures): boolean { + return this.getCurrentFeatures()[featureName]; + } + + /** + * Core logic to calculate features based on all configurations + */ + protected calculateFeatures( + globalPrefs: GlobalPreferences | null, + roomPrefs: MeetRoomPreferences | null, + participantPerms: OpenViduMeetPermissions | null, + recordingPerms: RecordingPermissions | null, + role: ParticipantRole | null + ): ApplicationFeatures { + // Start with default configuration + const features: ApplicationFeatures = { ...DEFAULT_FEATURES }; + + // Apply global preferences restrictions + if (globalPrefs) { + } + + // Apply room configurations + if (roomPrefs) { + features.showChat = roomPrefs.chatPreferences.enabled; + features.showRecording = roomPrefs.recordingPreferences.enabled && role === ParticipantRole.MODERATOR; + features.showBackgrounds = roomPrefs.virtualBackgroundPreferences.enabled; + } + + // Apply participant permissions (these can restrict enabled features) + if (participantPerms) { + // Only restrict if the feature is already enabled + if (features.showChat) { + features.showChat = participantPerms.canChat; + } + if (features.showRecording) { + features.showRecording = participantPerms.canRecord; + } + if (features.showBackgrounds) { + features.showBackgrounds = participantPerms.canChangeVirtualBackground; + } + if (features.showScreenShare) { + features.showScreenShare = participantPerms.canPublishScreen; + } + } + + if (recordingPerms) { + // Apply recording permissions + features.canDeleteRecordings = recordingPerms.canDeleteRecordings; + features.canRetrieveRecordings = recordingPerms.canRetrieveRecordings; + } + + // Apply role-based configurations + if (role) { + features.canModerateRoom = role === ParticipantRole.MODERATOR; + features.canManageRecordings = role === ParticipantRole.MODERATOR; + features.canAccessConsole = role === ParticipantRole.MODERATOR; + } + + this.log.d('Calculated features', features); + return features; + } + + /** + * Loads initial preferences from services + */ + async initializeConfiguration(): Promise { + try { + this.log.d('Initializing feature configuration'); + + // Load global preferences if available + // (this will depend on your GlobalPreferencesService implementation) + } catch (error) { + this.log.e('Error initializing feature configuration', error); + } + } + + /** + * Resets all configurations to their initial values + */ + reset(): void { + this.globalPreferencesSubject.next(null); + this.roomPreferencesSubject.next(null); + this.participantPermissionsSubject.next(null); + this.participantRoleSubject.next(null); + } +} diff --git a/frontend/projects/shared-meet-components/src/lib/services/room/room.service.ts b/frontend/projects/shared-meet-components/src/lib/services/room/room.service.ts index 4a78b40..7be51e9 100644 --- a/frontend/projects/shared-meet-components/src/lib/services/room/room.service.ts +++ b/frontend/projects/shared-meet-components/src/lib/services/room/room.service.ts @@ -3,6 +3,7 @@ import { MeetRoomPreferences } from '@lib/typings/ce'; import { LoggerService } from 'openvidu-components-angular'; import { HttpService } from '../http/http.service'; import { MeetRoom, MeetRoomOptions } from 'projects/shared-meet-components/src/lib/typings/ce/room'; +import { FeatureConfigurationService } from '../feature-configuration/feature-configuration.service'; @Injectable({ providedIn: 'root' @@ -12,7 +13,8 @@ export class RoomService { protected roomPreferences: MeetRoomPreferences | undefined; constructor( protected loggerService: LoggerService, - protected httpService: HttpService + protected httpService: HttpService, + protected featureConfService: FeatureConfigurationService ) { this.log = this.loggerService.get('OpenVidu Meet - RoomService'); } @@ -39,14 +41,23 @@ export class RoomService { return this.httpService.getRoom(roomId); } - async getRoomPreferences(): Promise { - if (!this.roomPreferences) { - this.log.d('Room preferences not found, fetching from server'); - // Fetch the room preferences from the server - this.roomPreferences = await this.httpService.getRoomPreferences(); + async loadPreferences(roomId: string, forceUpdate: boolean = false): Promise { + if (this.roomPreferences && !forceUpdate) { + this.log.d('Returning cached room preferences'); + return this.roomPreferences; } - return this.roomPreferences; + this.log.d('Fetching room preferences from server'); + try { + const room = await this.getRoom(roomId); + this.roomPreferences = room.preferences! as MeetRoomPreferences; + this.featureConfService.setRoomPreferences(this.roomPreferences); + console.log('Room preferences loaded:', this.roomPreferences); + return this.roomPreferences; + } catch (error) { + this.log.e('Error loading room preferences', error); + throw new Error('Failed to load room preferences'); + } } /**