frontend: refactor MeetingLobby and MeetingContext services to improve state management and reactivity

This commit is contained in:
juancarmore 2026-02-12 10:32:05 +01:00
parent 28bfa609d8
commit 0e77aba428
5 changed files with 184 additions and 281 deletions

View File

@ -1,5 +1,5 @@
import { CommonModule } from '@angular/common';
import { Component, computed, inject } from '@angular/core';
import { Component, inject } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
@ -8,7 +8,6 @@ import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { ShareMeetingLinkComponent } from '../../components/share-meeting-link/share-meeting-link.component';
import { MeetingLobbyService } from '../../services/meeting-lobby.service';
import { MeetingService } from '../../services/meeting.service';
/**
* Reusable component for the meeting lobby page.
@ -32,30 +31,19 @@ import { MeetingService } from '../../services/meeting.service';
})
export class MeetingLobbyComponent {
protected lobbyService = inject(MeetingLobbyService);
protected meetingService = inject(MeetingService);
protected roomName = computed(() => this.lobbyService.state().room?.roomName);
protected meetingUrl = computed(() => this.lobbyService.meetingUrl());
protected roomClosed = computed(() => this.lobbyService.state().roomClosed);
protected showRecordingCard = computed(() => this.lobbyService.state().showRecordingCard);
protected showShareLink = computed(() => {
const state = this.lobbyService.state();
const canModerate = this.lobbyService.canModerateRoom();
return !!state.room && !state.roomClosed && canModerate;
});
protected showBackButton = computed(() => this.lobbyService.state().showBackButton);
protected backButtonText = computed(() => this.lobbyService.state().backButtonText);
protected isE2EEEnabled = computed(() => this.lobbyService.state().hasRoomE2EEEnabled);
protected participantForm = computed(() => this.lobbyService.state().participantForm);
/**
* Computed signal to determine if the E2EE key input should be shown.
* When E2EE key is provided via URL query param, the control is disabled and should not be displayed.
*/
protected showE2EEKeyInput = computed(() => {
const form = this.lobbyService.state().participantForm;
const e2eeKeyControl = form.get('e2eeKey');
return this.isE2EEEnabled() && e2eeKeyControl?.enabled;
});
protected roomName = this.lobbyService.roomName;
protected roomClosed = this.lobbyService.roomClosed;
protected isE2EEEnabled = this.lobbyService.hasRoomE2EEEnabled;
protected participantForm = this.lobbyService.participantForm;
protected showE2EEKeyInput = this.lobbyService.showE2EEKeyInput;
protected showRecordingCard = this.lobbyService.showRecordingCard;
protected showShareLink = this.lobbyService.showShareLink;
protected meetingUrl = this.lobbyService.meetingUrl;
protected showBackButton = this.lobbyService.showBackButton;
protected backButtonText = this.lobbyService.backButtonText;
async onFormSubmit(): Promise<void> {
await this.lobbyService.submitAccess();

View File

@ -1,4 +1,3 @@
export * from './captions.model';
export * from './custom-participant.model';
export * from './layout.model';
export * from './lobby.model';

View File

@ -1,20 +0,0 @@
import { FormGroup } from '@angular/forms';
import { MeetRoom } from '@openvidu-meet/typings';
/**
* State interface representing the lobby phase of a meeting.
*
* IMPORTANT: This state is ONLY relevant during the lobby phase (before joining the meeting).
* Once the participant joins the meeting, MeetingContextService becomes the single source of truth.
*/
export interface LobbyState {
room?: MeetRoom;
roomId?: string;
roomClosed: boolean;
showRecordingCard: boolean;
showBackButton: boolean;
backButtonText: string;
hasRoomE2EEEnabled: boolean;
participantForm: FormGroup;
roomMemberToken?: string;
}

View File

@ -19,108 +19,65 @@ export class MeetingContextService {
private readonly viewportService = inject(ViewportService);
private readonly sessionStorageService = inject(SessionStorageService);
private readonly _meetRoom = signal<MeetRoom | undefined>(undefined);
private readonly _lkRoom = signal<Room | undefined>(undefined);
private readonly _roomId = signal<string | undefined>(undefined);
private readonly _meetRoom = signal<MeetRoom | undefined>(undefined);
private readonly _meetingUrl = signal<string>('');
private readonly _roomSecret = signal<string | undefined>(undefined);
private readonly _e2eeKey = signal<string>('');
private readonly _isE2eeKeyFromUrl = signal<boolean>(false);
private readonly _roomSecret = signal<string | undefined>(undefined);
private readonly _hasRecordings = signal<boolean>(false);
private readonly _meetingEndedBy = signal<'self' | 'other' | null>(null);
private readonly _lkRoom = signal<Room | undefined>(undefined);
private readonly _participantsVersion = signal<number>(0);
private readonly _localParticipant = signal<CustomParticipantModel | undefined>(undefined);
private readonly _remoteParticipants = signal<CustomParticipantModel[]>([]);
/**
* Readonly signal for the current room
*/
/** Readonly signal for the current room ID */
readonly roomId = this._roomId.asReadonly();
/** Readonly signal for the current room */
readonly meetRoom = this._meetRoom.asReadonly();
/**
* Readonly signal for the current room ID
*/
readonly roomId = this._roomId.asReadonly();
/**
* Readonly signal for the current lk room
*/
readonly lkRoom = this._lkRoom.asReadonly();
/**
* Readonly signal for the meeting URL
*/
/** Readonly signal for the current meeting URL */
readonly meetingUrl = this._meetingUrl.asReadonly();
/**
* Readonly signal for the E2EE key
*/
/** Readonly signal for the room secret (if any) */
readonly roomSecret = this._roomSecret.asReadonly();
/** Readonly signal for the stored E2EE key (if any) */
readonly e2eeKey = this._e2eeKey.asReadonly();
/**
* Readonly signal for whether the E2EE key came from a URL parameter
*/
/** Readonly signal for whether the E2EE key came from a URL parameter */
readonly isE2eeKeyFromUrl = this._isE2eeKeyFromUrl.asReadonly();
/**
* Readonly signal for the room secret
*/
readonly roomSecret = this._roomSecret.asReadonly();
/**
* Readonly signal for whether the room has recordings
*/
/** Readonly signal for whether the room has recordings */
readonly hasRecordings = this._hasRecordings.asReadonly();
/**
* Readonly signal for who ended the meeting ('self', 'other', or null)
*/
/** Readonly signal for who ended the meeting ('self', 'other', or null) */
readonly meetingEndedBy = this._meetingEndedBy.asReadonly();
/** Readonly signal for the current LiveKit room */
readonly lkRoom = this._lkRoom.asReadonly();
/**
* Readonly signal for participants version (increments on role changes)
* Used to trigger reactivity when participant properties change without array reference changes
*/
readonly participantsVersion = this._participantsVersion.asReadonly();
/**
* Readonly signal for the local participant
*/
/** Readonly signal for the local participant */
readonly localParticipant = this._localParticipant.asReadonly();
/**
* Readonly signal for the remote participants
*/
/** Readonly signal for the remote participants */
readonly remoteParticipants = this._remoteParticipants.asReadonly();
/**
* Computed signal that combines local and remote participants
*/
/** Computed signal that combines local and remote participants */
readonly allParticipants = computed(() => {
const local = this._localParticipant();
const remotes = this._remoteParticipants();
return local ? [local, ...remotes] : remotes;
});
/**
* Computed signal for whether the current user can moderate the room
*/
/** Computed signal for whether the current user can moderate the room */
readonly canModerateRoom = computed(() => this.roomFeatureService.features().canModerateRoom);
/**
* Computed signal for whether layout switching is allowed
*/
/** Computed signal for whether layout switching is allowed */
readonly allowLayoutSwitching = computed(() => this.roomFeatureService.features().allowLayoutSwitching);
/**
* Computed signal for captions status based on room and global configuration
*/
/** Computed signal for captions status based on room and global configuration */
readonly getCaptionsStatus = computed(() => this.roomFeatureService.features().captionsStatus);
/**
* Computed signal for whether the device is mobile
*/
readonly isMobile = computed(() => this.viewportService.isMobile());
/** Readonly signal for whether the device is mobile */
readonly isMobile = this.viewportService.isMobile;
constructor() {
// Setup automatic synchronization with ParticipantService signals
@ -145,32 +102,6 @@ export class MeetingContextService {
this.setMeetingUrl(room.accessUrl);
}
/**
* Sets the LiveKit Room instance in context
* @param room
*/
setLkRoom(room: Room) {
this._lkRoom.set(room);
}
/**
* Synchronizes participants from OpenVidu Components ParticipantService using signals.
* Effects are automatically cleaned up when the service is destroyed.
*/
private setupParticipantSynchronization(): void {
// Sync local participant signal
effect(() => {
const localParticipant = this.ovParticipantService.localParticipantSignal();
this._localParticipant.set(localParticipant as CustomParticipantModel);
});
// Sync remote participants signal
effect(() => {
const remoteParticipants = this.ovParticipantService.remoteParticipantsSignal();
this._remoteParticipants.set(remoteParticipants as CustomParticipantModel[]);
});
}
/**
* Updates the meeting URL based on room access URL
* @param accessUrl The room access URL
@ -182,6 +113,19 @@ export class MeetingContextService {
this._meetingUrl.set(meetingUrl);
}
/**
* Sets the room secret in context
* @param secret The room secret
* @param updateStorage Whether to persist in SessionStorage (default: false)
*/
setRoomSecret(secret: string, updateStorage = false): void {
if (updateStorage) {
this.sessionStorageService.setRoomSecret(secret);
}
this._roomSecret.set(secret);
}
/**
* Stores the E2EE key in context
* @param key The E2EE key
@ -204,19 +148,6 @@ export class MeetingContextService {
}
}
/**
* Sets the room secret in context
* @param secret The room secret
* @param updateStorage Whether to persist in SessionStorage (default: false)
*/
setRoomSecret(secret: string, updateStorage = false): void {
if (updateStorage) {
this.sessionStorageService.setRoomSecret(secret);
}
this._roomSecret.set(secret);
}
/**
* Updates whether the room has recordings
* @param hasRecordings True if recordings exist
@ -233,6 +164,14 @@ export class MeetingContextService {
this._meetingEndedBy.set(by);
}
/**
* Sets the LiveKit Room instance in context
* @param room
*/
setLkRoom(room: Room) {
this._lkRoom.set(room);
}
/**
* Increments the participants version counter
* Used to trigger reactivity when participant properties (like role) change
@ -241,6 +180,24 @@ export class MeetingContextService {
this._participantsVersion.update((v) => v + 1);
}
/**
* Synchronizes participants from OpenVidu Components ParticipantService using signals.
* Effects are automatically cleaned up when the service is destroyed.
*/
private setupParticipantSynchronization(): void {
// Sync local participant signal
effect(() => {
const localParticipant = this.ovParticipantService.localParticipantSignal();
this._localParticipant.set(localParticipant as CustomParticipantModel);
});
// Sync remote participants signal
effect(() => {
const remoteParticipants = this.ovParticipantService.remoteParticipantsSignal();
this._remoteParticipants.set(remoteParticipants as CustomParticipantModel[]);
});
}
/**
* Clears the meeting context
*/

View File

@ -1,7 +1,7 @@
import { computed, inject, Injectable, signal } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';
import { ActivatedRoute } from '@angular/router';
import { MeetRoomStatus } from '@openvidu-meet/typings';
import { MeetRoom, MeetRoomStatus } from '@openvidu-meet/typings';
import { LoggerService } from 'openvidu-components-angular';
import { NavigationErrorReason } from '../../../shared/models/navigation.model';
import { AppContextService } from '../../../shared/services/app-context.service';
@ -10,84 +10,122 @@ import { AuthService } from '../../auth/services/auth.service';
import { RecordingService } from '../../recordings/services/recording.service';
import { RoomMemberContextService } from '../../room-members/services/room-member-context.service';
import { RoomService } from '../../rooms/services/room.service';
import { LobbyState } from '../models/lobby.model';
import { MeetingContextService } from './meeting-context.service';
import { MeetingWebComponentManagerService } from './meeting-webcomponent-manager.service';
import { MeetingService } from './meeting.service';
/**
* Service that manages the meeting lobby phase state and operations.
*
* This service is ONLY responsible for the LOBBY PHASE - the period before a participant joins the meeting.
*
*/
@Injectable()
export class MeetingLobbyService {
/**
* Reactive signal for lobby state.
* This state is only relevant during the lobby phase.
*/
private readonly _state = signal<LobbyState>({
roomId: undefined,
roomClosed: false,
showRecordingCard: false,
showBackButton: true,
backButtonText: 'Back',
hasRoomE2EEEnabled: false,
participantForm: new FormGroup({
name: new FormControl('', [Validators.required]),
e2eeKey: new FormControl('')
}),
roomMemberToken: undefined
});
protected roomService: RoomService = inject(RoomService);
protected meetingContextService: MeetingContextService = inject(MeetingContextService);
protected meetingService: MeetingService = inject(MeetingService);
protected recordingService: RecordingService = inject(RecordingService);
protected authService: AuthService = inject(AuthService);
protected roomMemberContextService: RoomMemberContextService = inject(RoomMemberContextService);
protected navigationService: NavigationService = inject(NavigationService);
protected appCtxService: AppContextService = inject(AppContextService);
protected wcManagerService: MeetingWebComponentManagerService = inject(MeetingWebComponentManagerService);
protected roomService = inject(RoomService);
protected meetingContextService = inject(MeetingContextService);
protected meetingService = inject(MeetingService);
protected recordingService = inject(RecordingService);
protected authService = inject(AuthService);
protected roomMemberContextService = inject(RoomMemberContextService);
protected navigationService = inject(NavigationService);
protected appCtxService = inject(AppContextService);
protected wcManagerService = inject(MeetingWebComponentManagerService);
protected route = inject(ActivatedRoute);
protected loggerService = inject(LoggerService);
protected log = this.loggerService.get('OpenVidu Meet - MeetingLobbyService');
protected route: ActivatedRoute = inject(ActivatedRoute);
/**
* Readonly signal for lobby state - components can use computed() with this
* Individual signals for lobby state.
* This state is only relevant during the lobby phase - before a participant joins the meeting.
*/
readonly state = this._state.asReadonly();
private readonly _roomId = signal<string | undefined>(undefined);
private readonly _room = signal<MeetRoom | undefined>(undefined);
private readonly _showRecordingCard = signal<boolean>(false);
private readonly _showBackButton = signal<boolean>(true);
private readonly _backButtonText = signal<string>('Back');
private readonly _roomMemberToken = signal<string | undefined>(undefined);
private readonly _participantForm = signal<FormGroup>(
new FormGroup({
name: new FormControl('', [Validators.required]),
e2eeKey: new FormControl('')
})
);
/** Readonly signal for the room */
readonly room = this._room.asReadonly();
/** Readonly signal for the room ID */
readonly roomId = this._roomId.asReadonly();
/** Readonly signal for room name */
readonly roomName = computed(() => this._room()?.roomName);
/** Readonly signal for whether the room is closed */
readonly roomClosed = computed(() => this._room()?.status === MeetRoomStatus.CLOSED);
/** Readonly signal for whether E2EE is enabled in the room */
readonly hasRoomE2EEEnabled = computed(() => this._room()?.config.e2ee.enabled ?? false);
/**
* Computed signal to determine if the E2EE key input should be shown.
* When E2EE key is provided via URL query param, the control is disabled and should not be displayed.
*/
readonly showE2EEKeyInput = computed(() => {
const form = this._participantForm();
const e2eeKeyControl = form.get('e2eeKey');
return this.hasRoomE2EEEnabled() && e2eeKeyControl?.enabled;
});
/** Readonly signal for whether to show the recording card */
readonly showRecordingCard = this._showRecordingCard.asReadonly();
/** Readonly signal for whether to show the back button */
readonly showBackButton = this._showBackButton.asReadonly();
/** Readonly signal for the back button text */
readonly backButtonText = this._backButtonText.asReadonly();
/**
* Computed signal for meeting URL derived from MeetingContextService
* This ensures a single source of truth for the meeting URL
* Computed signal to determine if the share link option should be shown.
* 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();
});
/** Computed signal for meeting URL derived from MeetingContextService */
readonly meetingUrl = computed(() => this.meetingContextService.meetingUrl());
/** Readonly signal for the room member token */
readonly roomMemberToken = this._roomMemberToken.asReadonly();
/** Readonly signal for the participant form */
readonly participantForm = this._participantForm.asReadonly();
/**
* Computed signal for whether the current user can moderate the room
* Derived from MeetingContextService
* Setter for participant name
*/
readonly canModerateRoom = computed(() => this.meetingContextService.canModerateRoom());
setParticipantName(name: string): void {
this._participantForm().get('name')?.setValue(name);
}
/**
* Computed signal for participant name - optimized to avoid repeated form access
*/
readonly participantName = computed(() => {
const { valid, value } = this._state().participantForm;
const form = this._participantForm();
const { valid, value } = form;
if (!valid || !value.name?.trim()) {
return '';
}
return value.name.trim();
});
/**
* Setter for E2EE key
*/
setE2eeKey(key: string): void {
this._participantForm().get('e2eeKey')?.setValue(key);
}
/**
* Computed signal for E2EE key - optimized to avoid repeated form access
* Uses getRawValue() to get the value even when the control is disabled (e.g., when set from URL param)
*/
readonly e2eeKeyValue = computed(() => {
const form = this._state().participantForm;
const form = this._participantForm();
const rawValue = form.getRawValue();
if (!form.valid || !rawValue.e2eeKey?.trim()) {
return '';
@ -95,47 +133,6 @@ export class MeetingLobbyService {
return rawValue.e2eeKey.trim();
});
/**
* Computed signal for room member token
*/
readonly roomMemberToken = computed(() => this._state().roomMemberToken);
/**
* Computed signal for room ID
*/
readonly roomId = computed(() => this._state().roomId);
/**
* Computed signal for room secret.
* Obtained from MeetingContextService
*/
readonly roomSecret = computed(() => this.meetingContextService.roomSecret());
/**
* Computed signal for room name
*/
readonly roomName = computed(() => this._state().room?.roomName);
/**
* Computed signal for has recordings.
* Obtained from MeetingContextService
*/
readonly hasRecordings = computed(() => this.meetingContextService.hasRecordings());
/**
* Setter for participant name
*/
setParticipantName(name: string): void {
this._state().participantForm.get('name')?.setValue(name);
}
/**
* Setter for E2EE key
*/
setE2eeKey(key: string): void {
this._state().participantForm.get('e2eeKey')?.setValue(key);
}
/**
* Initializes the lobby state by fetching room data and configuring UI
*/
@ -144,7 +141,7 @@ export class MeetingLobbyService {
const roomId = this.meetingContextService.roomId();
if (!roomId) throw new Error('Room ID is not set in Meeting Context');
this._state.update((state) => ({ ...state, roomId }));
this._roomId.set(roomId);
const [room] = await Promise.all([
this.roomService.getRoom(roomId, {
@ -155,22 +152,12 @@ export class MeetingLobbyService {
this.checkForRecordings(),
this.initializeParticipantName()
]);
const roomClosed = room.status === MeetRoomStatus.CLOSED;
const hasRoomE2EEEnabled = room.config?.e2ee?.enabled || false;
this._state.update((state) => ({
...state,
room,
roomClosed,
hasRoomE2EEEnabled
}));
this._room.set(room);
this.meetingContextService.setMeetRoom(room);
if (hasRoomE2EEEnabled) {
if (this.hasRoomE2EEEnabled()) {
// If E2EE is enabled, make the e2eeKey form control required
const form = this._state().participantForm;
const form = this._participantForm();
form.get('e2eeKey')?.setValidators([Validators.required]);
const contextE2eeKey = this.meetingContextService.e2eeKey();
if (contextE2eeKey) {
@ -192,7 +179,7 @@ export class MeetingLobbyService {
* Copies the meeting speaker link to clipboard
*/
copyMeetingSpeakerLink() {
const { room } = this.state();
const room = this._room();
if (room) {
this.meetingService.copyMeetingSpeakerLink(room);
}
@ -231,27 +218,22 @@ export class MeetingLobbyService {
*/
async goToRecordings(): Promise<void> {
try {
const roomId = this._state().roomId;
const roomSecret = this.meetingContextService.roomSecret();
await this.navigationService.navigateTo(`room/${roomId}/recordings`, {
secret: roomSecret
});
const roomId = this.roomId();
await this.navigationService.navigateTo(`room/${roomId}/recordings`);
} catch (error) {
this.log.e('Error navigating to recordings:', error);
}
}
async submitAccess(): Promise<void> {
const sanitized = this.participantName().trim(); // remove leading/trailing spaces
if (!sanitized) {
const name = this.participantName();
if (!name) {
this.log.e('Participant form is invalid. Cannot access meeting.');
throw new Error('Participant form is invalid');
}
this.setParticipantName(sanitized);
// For E2EE rooms, validate passkey
const { hasRoomE2EEEnabled, roomId } = this._state();
const hasRoomE2EEEnabled = this.hasRoomE2EEEnabled();
if (hasRoomE2EEEnabled) {
const e2eeKey = this.e2eeKeyValue();
if (!e2eeKey) {
@ -262,7 +244,7 @@ export class MeetingLobbyService {
}
await this.generateRoomMemberToken();
await Promise.all([this.addParticipantNameToUrl(), this.roomService.loadRoomConfig(roomId!)]);
await Promise.all([this.addParticipantNameToUrl(), this.roomService.loadRoomConfig(this._roomId()!)]);
}
/**
@ -276,12 +258,13 @@ export class MeetingLobbyService {
// If in standalone mode without redirection and user is not authenticated,
// hide back button (user has no where to go back to)
if (isStandaloneMode && !redirection && !isAuthenticated) {
this._state.update((state) => ({ ...state, showBackButton: false }));
this._showBackButton.set(false);
return;
}
const backButtonText = isStandaloneMode && !redirection && isAuthenticated ? 'Back to Rooms' : 'Back';
this._state.update((state) => ({ ...state, showBackButton: true, backButtonText }));
this._showBackButton.set(true);
this._backButtonText.set(backButtonText);
}
/**
@ -297,11 +280,11 @@ export class MeetingLobbyService {
const canRetrieveRecordings = this.roomMemberContextService.hasPermission('canRetrieveRecordings');
if (!canRetrieveRecordings) {
this._state.update((state) => ({ ...state, showRecordingCard: false }));
this._showRecordingCard.set(false);
return;
}
const { roomId } = this._state();
const roomId = this._roomId();
if (!roomId) throw new Error('Room ID is not set in lobby state');
const { recordings } = await this.recordingService.listRecordings({
maxItems: 1,
@ -315,13 +298,10 @@ export class MeetingLobbyService {
this.meetingContextService.setHasRecordings(hasRecordings);
// Update only UI flag locally
this._state.update((state) => ({
...state,
showRecordingCard: hasRecordings
}));
this._showRecordingCard.set(hasRecordings);
} catch (error) {
this.log.e('Error checking for recordings:', error);
this._state.update((state) => ({ ...state, showRecordingCard: false }));
this._showRecordingCard.set(false);
}
}
@ -352,7 +332,7 @@ export class MeetingLobbyService {
*/
protected async generateRoomMemberToken() {
try {
const roomId = this._state().roomId;
const roomId = this._roomId();
const roomSecret = this.meetingContextService.roomSecret();
const roomMemberToken = await this.roomMemberContextService.generateToken(
roomId!,
@ -365,7 +345,7 @@ export class MeetingLobbyService {
);
const updatedName = this.roomMemberContextService.getParticipantName()!;
this.setParticipantName(updatedName);
this._state.update((state) => ({ ...state, roomMemberToken }));
this._roomMemberToken.set(roomMemberToken);
} catch (error: any) {
this.log.e('Error generating room member token for joining meeting:', error);
const message = error?.error?.message || error.message || 'Unknown error';
@ -418,18 +398,17 @@ export class MeetingLobbyService {
}
protected clearLobbyState() {
this._state.set({
roomId: undefined,
roomClosed: false,
showRecordingCard: false,
showBackButton: true,
backButtonText: 'Back',
hasRoomE2EEEnabled: false,
participantForm: new FormGroup({
this._room.set(undefined);
this._roomId.set(undefined);
this._showRecordingCard.set(false);
this._showBackButton.set(true);
this._backButtonText.set('Back');
this._roomMemberToken.set(undefined);
this._participantForm.set(
new FormGroup({
name: new FormControl('', [Validators.required]),
e2eeKey: new FormControl('')
}),
roomMemberToken: undefined
});
})
);
}
}