frontend: Decouples room feature service from global config

Removes direct dependency of the room feature service on the global config service and room member contexts.

The global configs and room member data are now observed through signals, ensuring reactive updates and decoupling of concerns.

This change allows for a more streamlined and testable architecture.
This commit is contained in:
CSantosM 2026-02-17 16:56:39 +01:00
parent 9c187be2b8
commit bfe97395d0
4 changed files with 47 additions and 76 deletions

View File

@ -5,7 +5,6 @@ import { MatIconModule } from '@angular/material/icon';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { OpenViduComponentsUiModule, OpenViduThemeMode, OpenViduThemeService, Room } from 'openvidu-components-angular';
import { Subject } from 'rxjs';
import { GlobalConfigService } from '../../../../shared/services/global-config.service';
import { NotificationService } from '../../../../shared/services/notification.service';
import { RoomFeatureService } from '../../../../shared/services/room-feature.service';
import { RuntimeConfigService } from '../../../../shared/services/runtime-config.service';
@ -37,7 +36,6 @@ export class MeetingComponent implements OnInit {
protected lobbyService = inject(MeetingLobbyService);
protected eventHandlerService = inject(MeetingEventHandlerService);
protected captionsService = inject(MeetingCaptionsService);
protected configService = inject(GlobalConfigService);
protected roomFeatureService = inject(RoomFeatureService);
protected ovThemeService = inject(OpenViduThemeService);
protected notificationService = inject(NotificationService);
@ -95,12 +93,7 @@ export class MeetingComponent implements OnInit {
effect(async () => {
const token = this.roomMemberToken();
if (token && this.showLobby) {
// The meeting view must be shown before loading the appearance config
this.showLobby = false;
await Promise.all([
this.configService.loadRoomsAppearanceConfig(),
this.configService.loadCaptionsConfig()
]);
}
});
}

View File

@ -2,11 +2,11 @@ import { computed, Injectable, signal } from '@angular/core';
import {
MeetRoomMember,
MeetRoomMemberPermissions,
MeetRoomMemberRole,
MeetRoomMemberTokenMetadata,
MeetRoomMemberTokenOptions
} from '@openvidu-meet/typings';
import { E2eeService, LoggerService } from 'openvidu-components-angular';
import { RoomFeatureService } from '../../../shared/services/room-feature.service';
import { TokenStorageService } from '../../../shared/services/token-storage.service';
import { decodeToken } from '../../../shared/utils/token.utils';
import { RoomMemberService } from './room-member.service';
@ -24,6 +24,7 @@ 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);
@ -35,6 +36,8 @@ 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) */
@ -47,7 +50,6 @@ export class RoomMemberContextService {
constructor(
protected loggerService: LoggerService,
protected roomMemberService: RoomMemberService,
protected roomFeatureService: RoomFeatureService,
protected tokenStorageService: TokenStorageService,
protected e2eeService: E2eeService
) {
@ -134,6 +136,7 @@ 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
@ -146,9 +149,6 @@ export class RoomMemberContextService {
}
}
// Update feature configuration
this.roomFeatureService.setRoomMemberRole(metadata.baseRole);
this.roomFeatureService.setRoomMemberPermissions(metadata.effectivePermissions);
} catch (error) {
this.log.e('Error decoding room member token:', error);
throw new Error('Invalid room member token');
@ -163,6 +163,7 @@ 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,8 +1,7 @@
import { inject, Injectable } from '@angular/core';
import { inject, Injectable, signal } from '@angular/core';
import { MeetAppearanceConfig, SecurityConfig, WebhookConfig } from '@openvidu-meet/typings';
import { ILogger, LoggerService } from 'openvidu-components-angular';
import { HttpService } from './http.service';
import { RoomFeatureService } from './room-feature.service';
@Injectable({
providedIn: 'root'
@ -12,10 +11,15 @@ export class GlobalConfigService {
protected loggerService: LoggerService = inject(LoggerService);
protected httpService: HttpService = inject(HttpService);
protected roomFeatureService: RoomFeatureService = inject(RoomFeatureService);
protected log: ILogger = this.loggerService.get('OpenVidu Meet - GlobalConfigService');
private readonly _roomAppearanceConfig = signal<MeetAppearanceConfig | undefined>(undefined);
private readonly _captionsGlobalEnabled = signal<boolean>(false);
readonly roomAppearanceConfig = this._roomAppearanceConfig.asReadonly();
readonly captionsGlobalEnabled = this._captionsGlobalEnabled.asReadonly();
constructor() {}
async getSecurityConfig(): Promise<SecurityConfig> {
@ -50,14 +54,24 @@ export class GlobalConfigService {
async loadRoomsAppearanceConfig(): Promise<void> {
try {
const config = await this.getRoomsAppearanceConfig();
this.roomFeatureService.setAppearanceConfig(config.appearance);
const { appearance } = await this.getRoomsAppearanceConfig();
this._roomAppearanceConfig.set(appearance);
} catch (error) {
this.log.e('Error loading rooms appearance config:', error);
throw error;
}
}
async loadCaptionsConfig(): Promise<void> {
try {
const { enabled } = await this.getCaptionsConfig();
this._captionsGlobalEnabled.set(enabled);
} catch (error) {
this.log.e('Error loading captions config:', error);
throw error;
}
}
async saveRoomsAppearanceConfig(config: MeetAppearanceConfig) {
const path = `${this.GLOBAL_CONFIG_API}/rooms/appearance`;
await this.httpService.putRequest(path, { appearance: config });
@ -67,14 +81,4 @@ export class GlobalConfigService {
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.roomFeatureService.setCaptionsGlobalConfig(config.enabled);
} catch (error) {
this.log.e('Error loading captions config:', error);
throw error;
}
}
}

View File

@ -1,13 +1,9 @@
import { computed, Injectable, signal } from '@angular/core';
import {
MeetAppearanceConfig,
MeetRoomCaptionsConfig,
MeetRoomConfig,
MeetRoomMemberPermissions,
MeetRoomMemberRole
} from '@openvidu-meet/typings';
import { computed, inject, Injectable, signal } from '@angular/core';
import { MeetAppearanceConfig, MeetRoomCaptionsConfig, MeetRoomConfig, MeetRoomMemberPermissions, MeetRoomMemberRole } 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 { GlobalConfigService } from './global-config.service';
/**
* Base configuration for features, used as a starting point before applying room-specific and user-specific configurations
@ -51,27 +47,26 @@ const DEFAULT_FEATURES: RoomFeatures = {
})
export class RoomFeatureService {
protected log;
protected globalConfigService = inject(GlobalConfigService);
protected roomMemberContextService = inject(RoomMemberContextService);
// Signals to handle reactive state
protected roomConfig = signal<MeetRoomConfig | undefined>(undefined);
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<RoomFeatures>(() =>
this.calculateFeatures(
this.roomConfig(),
this.roomMemberRole(),
this.roomMemberPermissions(),
this.appearanceConfig(),
this.captionsGlobalConfig()
this.roomMemberContextService.role(),
this.roomMemberContextService.permissions(),
this.globalConfigService.roomAppearanceConfig(),
this.globalConfigService.captionsGlobalEnabled()
)
);
constructor(protected loggerService: LoggerService) {
this.log = this.loggerService.get('OpenVidu Meet - RoomFeatureService');
void this.loadGlobalFeatureConfigs();
}
/**
@ -82,36 +77,18 @@ export class RoomFeatureService {
this.roomConfig.set(config);
}
/**
* Updates room member role
*/
setRoomMemberRole(role: MeetRoomMemberRole): void {
this.log.d('Updating room member role', role);
this.roomMemberRole.set(role);
}
protected async loadGlobalFeatureConfigs(): Promise<void> {
const [appearanceResult, captionsResult] = await Promise.allSettled([
this.globalConfigService.loadRoomsAppearanceConfig(),
this.globalConfigService.loadCaptionsConfig()
]);
/**
* Updates room member permissions
*/
setRoomMemberPermissions(permissions: MeetRoomMemberPermissions): void {
this.log.d('Updating room member permissions', permissions);
this.roomMemberPermissions.set(permissions);
}
/**
* Updates appearance config
*/
setAppearanceConfig(config: MeetAppearanceConfig): void {
this.log.d('Updating appearance config', config);
this.appearanceConfig.set(config);
}
/**
* Updates captions global config
*/
setCaptionsGlobalConfig(enabled: boolean): void {
this.log.d('Updating captions global config', enabled);
this.captionsGlobalConfig.set(enabled);
if (appearanceResult.status === 'rejected') {
this.log.e('Could not load room appearance config for features:', appearanceResult.reason);
}
if (captionsResult.status === 'rejected') {
this.log.e('Could not load captions config for features:', captionsResult.reason);
}
}
/**
@ -205,9 +182,5 @@ export class RoomFeatureService {
*/
reset(): void {
this.roomConfig.set(undefined);
this.roomMemberRole.set(undefined);
this.roomMemberPermissions.set(undefined);
this.appearanceConfig.set(undefined);
this.captionsGlobalConfig.set(false);
}
}