frontend: refactor VideoRoomComponent to update back button logic and improve participant leave handling

This commit is contained in:
juancarmore 2025-07-19 00:24:04 +02:00
parent b57f2d2eec
commit 3e046ce46b
2 changed files with 151 additions and 132 deletions

View File

@ -95,11 +95,11 @@
</div> </div>
<!-- Quick Actions --> <!-- Quick Actions -->
@if (isAdmin && !isEmbeddedMode) { @if (showBackButton) {
<div class="quick-actions fade-in-delayed-more"> <div class="quick-actions fade-in-delayed-more">
<button mat-button class="quick-action-button" (click)="goBack()"> <button mat-button class="quick-action-button" (click)="goBack()">
<mat-icon>arrow_back</mat-icon> <mat-icon>arrow_back</mat-icon>
<span>Back to Rooms</span> <span>{{ backButtonText }}</span>
</button> </button>
</div> </div>
} }

View File

@ -1,5 +1,5 @@
import { Clipboard } from '@angular/cdk/clipboard'; import { Clipboard } from '@angular/cdk/clipboard';
import { Component, OnDestroy, OnInit, Signal } from '@angular/core'; import { Component, 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';
@ -27,6 +27,7 @@ import {
WebComponentManagerService WebComponentManagerService
} from '@lib/services'; } from '@lib/services';
import { import {
LeftEventReason,
MeetRoom, MeetRoom,
MeetRoomPreferences, MeetRoomPreferences,
ParticipantRole, ParticipantRole,
@ -71,13 +72,15 @@ import {
MatRippleModule MatRippleModule
] ]
}) })
export class VideoRoomComponent implements OnInit, OnDestroy { export class VideoRoomComponent implements OnInit {
participantForm = new FormGroup({ participantForm = new FormGroup({
name: new FormControl('', [Validators.required, Validators.minLength(4)]) name: new FormControl('', [Validators.required, Validators.minLength(4)])
}); });
showRoom = false;
showRecordingCard = false; showRecordingCard = false;
showBackButton = true;
backButtonText = 'Back';
room?: MeetRoom; room?: MeetRoom;
roomId = ''; roomId = '';
roomSecret = ''; roomSecret = '';
@ -85,7 +88,9 @@ export class VideoRoomComponent implements OnInit, OnDestroy {
participantToken = ''; participantToken = '';
participantRole: ParticipantRole = ParticipantRole.PUBLISHER; participantRole: ParticipantRole = ParticipantRole.PUBLISHER;
showRoom = false;
features: Signal<ApplicationFeatures>; features: Signal<ApplicationFeatures>;
meetingEndedByMe = false;
constructor( constructor(
protected route: ActivatedRoute, protected route: ActivatedRoute,
@ -111,92 +116,27 @@ export class VideoRoomComponent implements OnInit, OnDestroy {
this.roomId = this.roomService.getRoomId(); this.roomId = this.roomService.getRoomId();
this.roomSecret = this.roomService.getRoomSecret(); this.roomSecret = this.roomService.getRoomSecret();
await this.setBackButtonText();
await this.checkForRecordings(); await this.checkForRecordings();
await this.initializeParticipantName(); await this.initializeParticipantName();
} }
ngOnDestroy(): void { /**
this.wcManagerService.stopCommandsListener(); * Sets the back button text based on the application mode and user role
} */
private async setBackButtonText() {
const isStandaloneMode = this.appDataService.isStandaloneMode();
const redirection = this.navigationService.getLeaveRedirectURL();
const isAdmin = await this.authService.isAdmin();
onRoomCreated(room: Room) { if (isStandaloneMode && !redirection && !isAdmin) {
room.on( // If in standalone mode, no redirection URL and not an admin, hide the back button
RoomEvent.DataReceived, this.showBackButton = false;
(payload: Uint8Array, participant?: RemoteParticipant, _?: DataPacket_Kind, topic?: string) => {
const event = JSON.parse(new TextDecoder().decode(payload));
if (topic === MeetSignalType.MEET_ROOM_PREFERENCES_UPDATED) {
const roomPreferences: MeetRoomPreferences = event.preferences;
this.featureConfService.setRoomPreferences(roomPreferences);
}
}
);
}
get isAdmin(): boolean {
return this.authService.isAdmin();
}
get isEmbeddedMode(): boolean {
return this.appDataService.isEmbeddedMode();
}
async submitAccessRoom() {
const { valid, value } = this.participantForm;
if (!valid || !value.name?.trim()) {
// If the form is invalid, do not proceed
console.warn('Participant form is invalid. Cannot access room.');
return; return;
} }
this.participantName = value.name.trim(); this.showBackButton = true;
this.participantTokenService.setParticipantName(this.participantName); this.backButtonText = isStandaloneMode && !redirection && isAdmin ? 'Back to Rooms' : 'Back';
try {
await this.generateParticipantToken();
await this.replaceUrlQueryParams();
await this.roomService.loadPreferences(this.roomId);
this.showRoom = true;
} catch (error) {
console.error('Error accessing room:', error);
}
}
onTokenRequested() {
// Participant token must be set only when requested
this.participantToken = this.participantTokenService.getParticipantToken() || '';
}
async leaveMeeting() {
await this.openviduService.disconnectRoom();
}
async endMeeting() {
if (this.participantService.isModeratorParticipant()) {
const roomId = this.roomService.getRoomId();
await this.meetingService.endMeeting(roomId);
}
}
async forceDisconnectParticipant(participant: ParticipantModel) {
await this.meetingService.kickParticipant(this.roomId, participant.identity);
}
async copyModeratorLink() {
await this.loadRoomIfAbsent();
this.clipboard.copy(this.room!.moderatorRoomUrl);
this.notificationService.showSnackbar('Moderator link copied to clipboard');
}
async copyPublisherLink() {
await this.loadRoomIfAbsent();
this.clipboard.copy(this.room!.publisherRoomUrl);
this.notificationService.showSnackbar('Publisher link copied to clipboard');
}
private async loadRoomIfAbsent() {
if (!this.room) {
this.room = await this.roomService.getRoom(this.roomId);
}
} }
/** /**
@ -243,6 +183,58 @@ export class VideoRoomComponent implements OnInit, OnDestroy {
} }
} }
async goToRecordings() {
try {
await this.navigationService.navigateTo(`room/${this.roomId}/recordings`, { secret: this.roomSecret });
} catch (error) {
console.error('Error navigating to recordings:', error);
}
}
/**
* Handles the back button click event and navigates accordingly
* If in embedded mode, it closes the WebComponentManagerService
* If in standalone mode, it navigates to the redirect URL or to the rooms page
*/
async goBack() {
if (this.appDataService.isEmbeddedMode()) {
this.wcManagerService.close();
return;
}
// Standalone mode handling
const redirectTo = this.navigationService.getLeaveRedirectURL();
if (redirectTo) {
// Navigate to the specified redirect URL
await this.navigationService.redirectTo(redirectTo);
return;
}
// Navigate to rooms page
await this.navigationService.navigateTo('/rooms');
}
async submitAccessRoom() {
const { valid, value } = this.participantForm;
if (!valid || !value.name?.trim()) {
// If the form is invalid, do not proceed
console.warn('Participant form is invalid. Cannot access room.');
return;
}
this.participantName = value.name.trim();
this.participantTokenService.setParticipantName(this.participantName);
try {
await this.generateParticipantToken();
await this.replaceUrlQueryParams();
await this.roomService.loadPreferences(this.roomId);
this.showRoom = true;
} catch (error) {
console.error('Error accessing room:', error);
}
}
/** /**
* Generates a participant token for joining a video room. * Generates a participant token for joining a video room.
* *
@ -305,25 +297,27 @@ export class VideoRoomComponent implements OnInit, OnDestroy {
}); });
} }
async goToRecordings() { onTokenRequested() {
try { // Participant token must be set only when requested
await this.navigationService.navigateTo(`room/${this.roomId}/recordings`, { secret: this.roomSecret }); this.participantToken = this.participantTokenService.getParticipantToken() || '';
} catch (error) {
console.error('Error navigating to recordings:', error);
}
} }
async goBack() { onRoomCreated(room: Room) {
try { room.on(
await this.navigationService.navigateTo('rooms'); RoomEvent.DataReceived,
} catch (error) { (payload: Uint8Array, participant?: RemoteParticipant, _?: DataPacket_Kind, topic?: string) => {
console.error('Error navigating back to rooms:', error); const event = JSON.parse(new TextDecoder().decode(payload));
if (topic === MeetSignalType.MEET_ROOM_PREFERENCES_UPDATED) {
const roomPreferences: MeetRoomPreferences = event.preferences;
this.featureConfService.setRoomPreferences(roomPreferences);
} }
} }
);
}
onParticipantConnected(event: ParticipantModel) { onParticipantConnected(event: ParticipantModel) {
const message: WebComponentOutboundEventMessage = { const message: WebComponentOutboundEventMessage = {
event: WebComponentEvent.JOIN, event: WebComponentEvent.JOINED,
payload: { payload: {
roomId: event.getProperties().room?.name || '', roomId: event.getProperties().room?.name || '',
participantName: event.name! participantName: event.name!
@ -333,18 +327,19 @@ export class VideoRoomComponent implements OnInit, OnDestroy {
} }
async onParticipantLeft(event: ParticipantLeftEvent) { async onParticipantLeft(event: ParticipantLeftEvent) {
console.warn('Participant left the room. Redirecting to:'); let leftReason = this.getReasonParamFromEvent(event.reason);
const redirectURL = this.navigationService.getLeaveRedirectURL() || '/disconnected'; if (leftReason === LeftEventReason.MEETING_ENDED && this.meetingEndedByMe) {
const isExternalURL = /^https?:\/\//.test(redirectURL); leftReason = LeftEventReason.MEETING_ENDED_BY_SELF;
const isRoomDeleted = event.reason === ParticipantLeftReason.ROOM_DELETED; }
// Send LEFT or MEETING_ENDED event to the parent component
let message: WebComponentOutboundEventMessage<WebComponentEvent.MEETING_ENDED | WebComponentEvent.LEFT>; let message: WebComponentOutboundEventMessage<WebComponentEvent.MEETING_ENDED | WebComponentEvent.LEFT>;
if (event.reason === ParticipantLeftReason.ROOM_DELETED) {
if (isRoomDeleted) {
message = { message = {
event: WebComponentEvent.MEETING_ENDED, event: WebComponentEvent.MEETING_ENDED,
payload: { payload: {
roomId: event.roomName roomId: event.roomName,
endedByMe: this.meetingEndedByMe
} }
} as WebComponentOutboundEventMessage<WebComponentEvent.MEETING_ENDED>; } as WebComponentOutboundEventMessage<WebComponentEvent.MEETING_ENDED>;
} else { } else {
@ -353,7 +348,7 @@ export class VideoRoomComponent implements OnInit, OnDestroy {
payload: { payload: {
roomId: event.roomName, roomId: event.roomName,
participantName: event.participantName, participantName: event.participantName,
reason: event.reason reason: leftReason
} }
} as WebComponentOutboundEventMessage<WebComponentEvent.LEFT>; } as WebComponentOutboundEventMessage<WebComponentEvent.LEFT>;
} }
@ -364,39 +359,63 @@ export class VideoRoomComponent implements OnInit, OnDestroy {
this.sessionStorageService.removeModeratorSecret(event.roomName); this.sessionStorageService.removeModeratorSecret(event.roomName);
} }
// Add disconnect reason as query parameter if redirecting to disconnect page // Navigate to the disconnected page with the reason
let finalRedirectURL = redirectURL; await this.navigationService.navigateTo('disconnected', { reason: leftReason });
if (!isExternalURL && (redirectURL === '/disconnected' || redirectURL.includes('/disconnected'))) {
const reasonParam = this.getReasonParamFromEvent(event.reason, isRoomDeleted);
const separator = redirectURL.includes('?') ? '&' : '?';
finalRedirectURL = `${redirectURL}${separator}reason=${encodeURIComponent(reasonParam)}`;
}
await this.navigationService.redirectTo(finalRedirectURL, isExternalURL);
} }
/** /**
* Maps ParticipantLeftReason to a query parameter value * Maps ParticipantLeftReason to LeftEventReason.
* This method translates the technical reasons for a participant leaving the room
* into user-friendly reasons that can be used in the UI or for logging purposes.
* @param reason The technical reason for the participant leaving the room.
* @returns The corresponding LeftEventReason.
*/ */
private getReasonParamFromEvent(reason: ParticipantLeftReason, isRoomDeleted: boolean): string { private getReasonParamFromEvent(reason: ParticipantLeftReason): LeftEventReason {
if (isRoomDeleted) { const reasonMap: Record<ParticipantLeftReason, LeftEventReason> = {
return 'roomDeleted'; [ParticipantLeftReason.LEAVE]: LeftEventReason.VOLUNTARY_LEAVE,
[ParticipantLeftReason.BROWSER_UNLOAD]: LeftEventReason.VOLUNTARY_LEAVE,
[ParticipantLeftReason.NETWORK_DISCONNECT]: LeftEventReason.NETWORK_DISCONNECT,
[ParticipantLeftReason.SIGNAL_CLOSE]: LeftEventReason.NETWORK_DISCONNECT,
[ParticipantLeftReason.SERVER_SHUTDOWN]: LeftEventReason.SERVER_SHUTDOWN,
[ParticipantLeftReason.PARTICIPANT_REMOVED]: LeftEventReason.PARTICIPANT_KICKED,
[ParticipantLeftReason.ROOM_DELETED]: LeftEventReason.MEETING_ENDED,
[ParticipantLeftReason.DUPLICATE_IDENTITY]: LeftEventReason.UNKNOWN,
[ParticipantLeftReason.OTHER]: LeftEventReason.UNKNOWN
};
return reasonMap[reason] ?? LeftEventReason.UNKNOWN;
} }
switch (reason) { async leaveMeeting() {
default: await this.openviduService.disconnectRoom();
case ParticipantLeftReason.LEAVE: }
return 'disconnect';
case ParticipantLeftReason.PARTICIPANT_REMOVED: async endMeeting() {
return 'forceDisconnectByUser'; if (this.participantService.isModeratorParticipant()) {
case ParticipantLeftReason.SERVER_SHUTDOWN: const roomId = this.roomService.getRoomId();
return 'sessionClosedByServer'; this.meetingEndedByMe = true;
case ParticipantLeftReason.NETWORK_DISCONNECT: await this.meetingService.endMeeting(roomId);
return 'networkDisconnect'; }
case ParticipantLeftReason.SIGNAL_CLOSE: }
return 'openviduDisconnect';
case ParticipantLeftReason.BROWSER_UNLOAD: async forceDisconnectParticipant(participant: ParticipantModel) {
return 'browserClosed'; await this.meetingService.kickParticipant(this.roomId, participant.identity);
}
async copyModeratorLink() {
await this.loadRoomIfAbsent();
this.clipboard.copy(this.room!.moderatorRoomUrl);
this.notificationService.showSnackbar('Moderator link copied to clipboard');
}
async copyPublisherLink() {
await this.loadRoomIfAbsent();
this.clipboard.copy(this.room!.publisherRoomUrl);
this.notificationService.showSnackbar('Publisher link copied to clipboard');
}
private async loadRoomIfAbsent() {
if (!this.room) {
this.room = await this.roomService.getRoom(this.roomId);
} }
} }