frontend: refactor VideoRoomComponent to update back button logic and improve participant leave handling
This commit is contained in:
parent
b57f2d2eec
commit
3e046ce46b
@ -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>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user