frontend: refactored video-room component and renamed by meeting component

- Implemented EndMeetingComponent to handle user disconnection scenarios.
- Created SCSS styles for the EndMeetingComponent to enhance UI/UX.
- Updated MeetingComponent to manage participant interactions and room functionalities.
- Added HTML structure for meeting access and participant form.
- Integrated routing to replace DisconnectedComponent with EndMeetingComponent.
- Added unit tests for MeetingComponent to ensure functionality.
This commit is contained in:
Carlos Santos 2025-07-28 18:10:50 +02:00
parent 91c9690953
commit 1617e2b9d6
10 changed files with 135 additions and 156 deletions

View File

@ -1,22 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DisconnectedComponent } from './disconnected.component';
describe('ParticipantDisconnectedComponent', () => {
let component: DisconnectedComponent;
let fixture: ComponentFixture<DisconnectedComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [DisconnectedComponent]
}).compileComponents();
fixture = TestBed.createComponent(DisconnectedComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -6,9 +6,9 @@ export * from './console/recordings/recordings.component';
export * from './console/rooms/rooms.component';
export * from './console/rooms/room-wizard/room-wizard.component';
export * from './console/users-permissions/users-permissions.component';
export * from './disconnected/disconnected.component';
export * from './meeting/end-meeting/end-meeting.component';
export * from './error/error.component';
export * from './login/login.component';
export * from './room-recordings/room-recordings.component';
export * from './video-room/video-room.component';
export * from './meeting/meeting.component';
export * from './view-recording/view-recording.component';

View File

@ -1,4 +1,4 @@
@import '../../../../../../src/assets/styles/design-tokens';
@import '../../../../../../../src/assets/styles/design-tokens';
.disconnected-container {
@include ov-theme-transition;

View File

@ -8,13 +8,13 @@ import { AppDataService, AuthService, NavigationService, WebComponentManagerServ
import { LeftEventReason } from '@lib/typings/ce';
@Component({
selector: 'ov-disconnected',
selector: 'ov-end-meeting',
standalone: true,
imports: [CommonModule, MatCardModule, MatButtonModule, MatIconModule],
templateUrl: './disconnected.component.html',
styleUrl: './disconnected.component.scss'
templateUrl: './end-meeting.component.html',
styleUrl: './end-meeting.component.scss'
})
export class DisconnectedComponent implements OnInit {
export class EndMeetingComponent implements OnInit {
disconnectedTitle = 'You Left the Meeting';
disconnectReason = 'You have successfully left the meeting';

View File

@ -1,111 +1,4 @@
@if (!showRoom) {
<div class="ov-page-container">
<div class="room-access-container fade-in">
<!-- Header Section -->
<div class="room-header">
<mat-icon class="ov-room-icon room-icon">video_chat</mat-icon>
<div class="room-info">
<h1 class="room-title">{{ roomId }}</h1>
<p class="room-subtitle">Choose how you want to proceed</p>
</div>
</div>
<!-- Action Cards Grid -->
<div class="action-cards-grid">
<!-- Join Room Card -->
<mat-card class="action-card primary-card fade-in">
<mat-card-header class="card-header">
<mat-icon class="ov-room-icon card-icon">meeting_room</mat-icon>
<div class="card-title-group">
<mat-card-title>Join Meeting</mat-card-title>
<mat-card-subtitle>Enter the room and start connecting</mat-card-subtitle>
</div>
</mat-card-header>
<mat-card-content class="card-content">
<form [formGroup]="participantForm" (ngSubmit)="submitAccessRoom()" class="join-form">
<mat-form-field appearance="outline" class="name-field">
<mat-label>Your display name</mat-label>
<input
id="participant-name-input"
matInput
placeholder="Enter your name"
formControlName="name"
required
/>
<mat-icon matSuffix class="ov-action-icon">person</mat-icon>
@if (participantForm.get('name')?.hasError('minlength')) {
<mat-error> The name must be at least <strong>4 characters</strong> </mat-error>
}
@if (participantForm.get('name')?.hasError('required')) {
<mat-error> The name is <strong>required</strong> </mat-error>
}
@if (participantForm.get('name')?.hasError('participantExists')) {
<mat-error>
The name is already taken. <strong> Please choose another name </strong>
</mat-error>
}
</mat-form-field>
<button
mat-raised-button
color="primary"
id="participant-name-submit"
type="submit"
class="join-button"
[disabled]="participantForm.invalid"
>
<span>Join Meeting</span>
</button>
</form>
</mat-card-content>
</mat-card>
<!-- View Recordings Card -->
@if (showRecordingCard) {
<mat-card class="action-card secondary-card fade-in-delayed">
<mat-card-header class="card-header">
<mat-icon class="ov-recording-icon card-icon">video_library</mat-icon>
<div class="card-title-group">
<mat-card-title>View Recordings</mat-card-title>
<mat-card-subtitle>Browse and manage past recordings</mat-card-subtitle>
</div>
</mat-card-header>
<mat-card-content class="card-content">
<div class="recordings-info">
<p class="recordings-description">
Access previously recorded meetings from this room. You can watch, download, or
manage existing recordings.
</p>
</div>
<button
id="view-recordings-btn"
mat-stroked-button
color="accent"
(click)="goToRecordings()"
class="recordings-button"
>
<span>Browse Recordings</span>
</button>
</mat-card-content>
</mat-card>
}
</div>
<!-- Quick Actions -->
@if (showBackButton) {
<div class="quick-actions fade-in-delayed-more">
<button mat-button class="quick-action-button" (click)="goBack()">
<mat-icon>arrow_back</mat-icon>
<span>{{ backButtonText }}</span>
</button>
</div>
}
</div>
</div>
} @else {
@if (showMeeting) {
<ov-videoconference
[token]="participantToken"
[participantName]="participantName"
@ -208,4 +101,112 @@
</div>
}
</ov-videoconference>
} @else {
<!-- Move this logic to prejoin meeting page -->
<div class="ov-page-container">
<div class="room-access-container fade-in">
<!-- Header Section -->
<div class="room-header">
<mat-icon class="ov-room-icon room-icon">video_chat</mat-icon>
<div class="room-info">
<h1 class="room-title">{{ roomId }}</h1>
<p class="room-subtitle">Choose how you want to proceed</p>
</div>
</div>
<!-- Action Cards Grid -->
<div class="action-cards-grid">
<!-- Join Room Card -->
<mat-card class="action-card primary-card fade-in">
<mat-card-header class="card-header">
<mat-icon class="ov-room-icon card-icon">meeting_room</mat-icon>
<div class="card-title-group">
<mat-card-title>Join Meeting</mat-card-title>
<mat-card-subtitle>Enter the room and start connecting</mat-card-subtitle>
</div>
</mat-card-header>
<mat-card-content class="card-content">
<form [formGroup]="participantForm" (ngSubmit)="submitAccessMeeting()" class="join-form">
<mat-form-field appearance="outline" class="name-field">
<mat-label>Your display name</mat-label>
<input
id="participant-name-input"
matInput
placeholder="Enter your name"
formControlName="name"
required
/>
<mat-icon matSuffix class="ov-action-icon">person</mat-icon>
@if (participantForm.get('name')?.hasError('minlength')) {
<mat-error> The name must be at least <strong>4 characters</strong> </mat-error>
}
@if (participantForm.get('name')?.hasError('required')) {
<mat-error> The name is <strong>required</strong> </mat-error>
}
@if (participantForm.get('name')?.hasError('participantExists')) {
<mat-error>
The name is already taken. <strong> Please choose another name </strong>
</mat-error>
}
</mat-form-field>
<button
mat-raised-button
color="primary"
id="participant-name-submit"
type="submit"
class="join-button"
[disabled]="participantForm.invalid"
>
<span>Join Meeting</span>
</button>
</form>
</mat-card-content>
</mat-card>
<!-- View Recordings Card -->
@if (showRecordingCard) {
<mat-card class="action-card secondary-card fade-in-delayed">
<mat-card-header class="card-header">
<mat-icon class="ov-recording-icon card-icon">video_library</mat-icon>
<div class="card-title-group">
<mat-card-title>View Recordings</mat-card-title>
<mat-card-subtitle>Browse and manage past recordings</mat-card-subtitle>
</div>
</mat-card-header>
<mat-card-content class="card-content">
<div class="recordings-info">
<p class="recordings-description">
Access previously recorded meetings from this room. You can watch, download, or
manage existing recordings.
</p>
</div>
<button
id="view-recordings-btn"
mat-stroked-button
color="accent"
(click)="goToRecordings()"
class="recordings-button"
>
<span>Browse Recordings</span>
</button>
</mat-card-content>
</mat-card>
}
</div>
<!-- Quick Actions -->
@if (showBackButton) {
<div class="quick-actions fade-in-delayed-more">
<button mat-button class="quick-action-button" (click)="goBack()">
<mat-icon>arrow_back</mat-icon>
<span>{{ backButtonText }}</span>
</button>
</div>
}
</div>
</div>
}

View File

@ -1,18 +1,18 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { VideoRoomComponent } from './video-room.component';
import { MeetingComponent } from './meeting.component';
describe('CallComponent', () => {
let component: VideoRoomComponent;
let fixture: ComponentFixture<VideoRoomComponent>;
let component: MeetingComponent;
let fixture: ComponentFixture<MeetingComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [VideoRoomComponent]
imports: [MeetingComponent]
}).compileComponents();
});
beforeEach(() => {
fixture = TestBed.createComponent(VideoRoomComponent);
fixture = TestBed.createComponent(MeetingComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

View File

@ -51,9 +51,9 @@ import {
} from 'openvidu-components-angular';
@Component({
selector: 'app-video-room',
templateUrl: './video-room.component.html',
styleUrls: ['./video-room.component.scss'],
selector: 'app-meeting',
templateUrl: './meeting.component.html',
styleUrls: ['./meeting.component.scss'],
standalone: true,
imports: [
OpenViduComponentsUiModule,
@ -72,7 +72,7 @@ import {
MatRippleModule
]
})
export class VideoRoomComponent implements OnInit {
export class MeetingComponent implements OnInit {
participantForm = new FormGroup({
name: new FormControl('', [Validators.required, Validators.minLength(4)])
});
@ -88,7 +88,7 @@ export class VideoRoomComponent implements OnInit {
participantToken = '';
participantRole: ParticipantRole = ParticipantRole.PUBLISHER;
showRoom = false;
showMeeting = false;
features: Signal<ApplicationFeatures>;
meetingEndedByMe = false;
@ -216,11 +216,11 @@ export class VideoRoomComponent implements OnInit {
}
}
async submitAccessRoom() {
async submitAccessMeeting() {
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.');
console.warn('Participant form is invalid. Cannot access meeting.');
return;
}
@ -231,14 +231,14 @@ export class VideoRoomComponent implements OnInit {
await this.generateParticipantToken();
await this.addParticipantNameToUrl();
await this.roomService.loadPreferences(this.roomId);
this.showRoom = true;
this.showMeeting = true;
} catch (error) {
console.error('Error accessing room:', error);
console.error('Error accessing meeting:', error);
}
}
/**
* Generates a participant token for joining a video room.
* Generates a participant token for joining a meeting.
*
* @throws When participant already exists in the room (status 409)
* @returns Promise that resolves when token is generated

View File

@ -13,7 +13,7 @@ import {
import {
ConsoleComponent,
DevelopersSettingsComponent,
DisconnectedComponent,
EndMeetingComponent,
ErrorComponent,
LoginComponent,
OverviewComponent,
@ -22,7 +22,7 @@ import {
RoomsComponent,
RoomWizardComponent,
UsersPermissionsComponent,
VideoRoomComponent,
MeetingComponent,
ViewRecordingComponent
} from '@lib/pages';
@ -34,7 +34,7 @@ export const baseRoutes: Routes = [
},
{
path: 'room/:room-id',
component: VideoRoomComponent,
component: MeetingComponent,
canActivate: [
runGuardsSerially(extractRoomQueryParamsGuard, checkParticipantRoleAndAuthGuard, removeRoomSecretGuard)
]
@ -56,7 +56,7 @@ export const baseRoutes: Routes = [
component: ViewRecordingComponent,
canActivate: [checkRecordingAuthGuard]
},
{ path: 'disconnected', component: DisconnectedComponent },
{ path: 'disconnected', component: EndMeetingComponent },
{ path: 'error', component: ErrorComponent },
{
path: '',