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 435bb2e..3d49934 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
@@ -95,11 +95,11 @@
- @if (isAdmin && !isEmbeddedMode) {
+ @if (showBackButton) {
}
diff --git a/frontend/projects/shared-meet-components/src/lib/pages/video-room/video-room.component.ts b/frontend/projects/shared-meet-components/src/lib/pages/video-room/video-room.component.ts
index c42f6ec..f4b6f9e 100644
--- a/frontend/projects/shared-meet-components/src/lib/pages/video-room/video-room.component.ts
+++ b/frontend/projects/shared-meet-components/src/lib/pages/video-room/video-room.component.ts
@@ -1,5 +1,5 @@
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 { MatButtonModule, MatIconButton } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
@@ -27,6 +27,7 @@ import {
WebComponentManagerService
} from '@lib/services';
import {
+ LeftEventReason,
MeetRoom,
MeetRoomPreferences,
ParticipantRole,
@@ -71,13 +72,15 @@ import {
MatRippleModule
]
})
-export class VideoRoomComponent implements OnInit, OnDestroy {
+export class VideoRoomComponent implements OnInit {
participantForm = new FormGroup({
name: new FormControl('', [Validators.required, Validators.minLength(4)])
});
- showRoom = false;
showRecordingCard = false;
+ showBackButton = true;
+ backButtonText = 'Back';
+
room?: MeetRoom;
roomId = '';
roomSecret = '';
@@ -85,7 +88,9 @@ export class VideoRoomComponent implements OnInit, OnDestroy {
participantToken = '';
participantRole: ParticipantRole = ParticipantRole.PUBLISHER;
+ showRoom = false;
features: Signal;
+ meetingEndedByMe = false;
constructor(
protected route: ActivatedRoute,
@@ -111,92 +116,27 @@ export class VideoRoomComponent implements OnInit, OnDestroy {
this.roomId = this.roomService.getRoomId();
this.roomSecret = this.roomService.getRoomSecret();
+ await this.setBackButtonText();
await this.checkForRecordings();
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) {
- room.on(
- RoomEvent.DataReceived,
- (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.');
+ if (isStandaloneMode && !redirection && !isAdmin) {
+ // If in standalone mode, no redirection URL and not an admin, hide the back button
+ this.showBackButton = false;
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);
- }
- }
-
- 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);
- }
+ this.showBackButton = true;
+ this.backButtonText = isStandaloneMode && !redirection && isAdmin ? 'Back to Rooms' : 'Back';
}
/**
@@ -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.
*
@@ -305,25 +297,27 @@ 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);
- }
+ onTokenRequested() {
+ // Participant token must be set only when requested
+ this.participantToken = this.participantTokenService.getParticipantToken() || '';
}
- async goBack() {
- try {
- await this.navigationService.navigateTo('rooms');
- } catch (error) {
- console.error('Error navigating back to rooms:', error);
- }
+ onRoomCreated(room: Room) {
+ room.on(
+ RoomEvent.DataReceived,
+ (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);
+ }
+ }
+ );
}
onParticipantConnected(event: ParticipantModel) {
const message: WebComponentOutboundEventMessage = {
- event: WebComponentEvent.JOIN,
+ event: WebComponentEvent.JOINED,
payload: {
roomId: event.getProperties().room?.name || '',
participantName: event.name!
@@ -333,18 +327,19 @@ export class VideoRoomComponent implements OnInit, OnDestroy {
}
async onParticipantLeft(event: ParticipantLeftEvent) {
- console.warn('Participant left the room. Redirecting to:');
- const redirectURL = this.navigationService.getLeaveRedirectURL() || '/disconnected';
- const isExternalURL = /^https?:\/\//.test(redirectURL);
- const isRoomDeleted = event.reason === ParticipantLeftReason.ROOM_DELETED;
+ let leftReason = this.getReasonParamFromEvent(event.reason);
+ if (leftReason === LeftEventReason.MEETING_ENDED && this.meetingEndedByMe) {
+ leftReason = LeftEventReason.MEETING_ENDED_BY_SELF;
+ }
+ // Send LEFT or MEETING_ENDED event to the parent component
let message: WebComponentOutboundEventMessage;
-
- if (isRoomDeleted) {
+ if (event.reason === ParticipantLeftReason.ROOM_DELETED) {
message = {
event: WebComponentEvent.MEETING_ENDED,
payload: {
- roomId: event.roomName
+ roomId: event.roomName,
+ endedByMe: this.meetingEndedByMe
}
} as WebComponentOutboundEventMessage;
} else {
@@ -353,7 +348,7 @@ export class VideoRoomComponent implements OnInit, OnDestroy {
payload: {
roomId: event.roomName,
participantName: event.participantName,
- reason: event.reason
+ reason: leftReason
}
} as WebComponentOutboundEventMessage;
}
@@ -364,39 +359,63 @@ export class VideoRoomComponent implements OnInit, OnDestroy {
this.sessionStorageService.removeModeratorSecret(event.roomName);
}
- // Add disconnect reason as query parameter if redirecting to disconnect page
- let finalRedirectURL = redirectURL;
- 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);
+ // Navigate to the disconnected page with the reason
+ await this.navigationService.navigateTo('disconnected', { reason: leftReason });
}
/**
- * 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 {
- if (isRoomDeleted) {
- return 'roomDeleted';
- }
+ private getReasonParamFromEvent(reason: ParticipantLeftReason): LeftEventReason {
+ const reasonMap: Record = {
+ [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) {
- default:
- case ParticipantLeftReason.LEAVE:
- return 'disconnect';
- case ParticipantLeftReason.PARTICIPANT_REMOVED:
- return 'forceDisconnectByUser';
- case ParticipantLeftReason.SERVER_SHUTDOWN:
- return 'sessionClosedByServer';
- case ParticipantLeftReason.NETWORK_DISCONNECT:
- return 'networkDisconnect';
- case ParticipantLeftReason.SIGNAL_CLOSE:
- return 'openviduDisconnect';
- case ParticipantLeftReason.BROWSER_UNLOAD:
- return 'browserClosed';
+ async leaveMeeting() {
+ await this.openviduService.disconnectRoom();
+ }
+
+ async endMeeting() {
+ if (this.participantService.isModeratorParticipant()) {
+ const roomId = this.roomService.getRoomId();
+ this.meetingEndedByMe = true;
+ 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);
}
}