frontend: Enhance Disconnected Component with user-friendly messages and animations

- Updated HTML structure for improved layout and branding.
- Added disconnect reason handling in TypeScript for better user feedback.
- Enhanced SCSS styles for a more visually appealing disconnected state.
- Introduced scaleIn animation for a smoother user experience.
This commit is contained in:
Carlos Santos 2025-07-02 17:05:46 +02:00
parent fbcb70dbc2
commit 4691888309
5 changed files with 245 additions and 27 deletions

View File

@ -1,10 +1,22 @@
<div class="disconnected-container"> <div class="disconnected-container">
<mat-card class="disconnected-card"> <div class="disconnect-content" role="main" aria-labelledby="disconnect-title">
<mat-card-header> <div class="disconnect-icon-container">
<mat-card-title>You have left the room</mat-card-title> <img
</mat-card-header> src="data:image/png;base64,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"
<mat-card-content> alt="Brand Logo"
<p>The meeting has ended.</p> class="logo-image"
</mat-card-content> />
</mat-card> </div>
<div class="disconnect-message">
<h1 id="disconnect-title" class="disconnect-title">Meeting Ended</h1>
<p class="disconnect-subtitle">
{{ disconnectReason || 'You have successfully left the video conference' }}
</p>
</div>
<div class="disconnect-footer">
<p class="footer-text">Thank you for using OpenVidu Meet</p>
</div>
</div>
</div> </div>

View File

@ -1,33 +1,153 @@
@import '../../../../../../src/assets/styles/design-tokens';
.disconnected-container { .disconnected-container {
@include ov-theme-transition;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
height: 100vh; min-height: 100vh;
background-color: var(--ov-meet-surface-background); padding: var(--ov-meet-spacing-lg);
background: linear-gradient(135deg, var(--ov-meet-background-color) 0%, var(--ov-meet-background-secondary) 100%);
} }
.disconnected-card { .disconnect-content {
width: 400px; @include ov-card;
padding: 20px; max-width: 520px;
width: 100%;
text-align: center; text-align: center;
background-color: var(--ov-meet-surface-primary); padding: var(--ov-meet-spacing-xxl);
border-radius: var(--ov-meet-surface-radius); background: var(--ov-meet-surface-elevated);
box-shadow: var(--ov-meet-shadow-lg);
@include ov-mobile-down {
padding: var(--ov-meet-spacing-xl);
margin: var(--ov-meet-spacing-md);
max-width: calc(100vw - 2 * var(--ov-meet-spacing-md));
}
} }
mat-card-header { .disconnect-icon-container {
margin-bottom: var(--ov-meet-spacing-xl);
display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
.logo-image {
width: 50px;
height: auto;
object-fit: contain;
display: block;
transition: all var(--ov-meet-transition-normal);
@include ov-mobile-down {
width: 40px;
}
}
&::before {
content: '';
position: absolute;
width: 80px;
height: 80px;
background: rgba(25, 118, 210, 0.1);
border-radius: var(--ov-meet-radius-circle);
z-index: -1;
@include ov-mobile-down {
width: 64px;
height: 64px;
}
}
position: relative;
} }
mat-card-title { .disconnect-message {
font-size: 1.5em; margin-bottom: var(--ov-meet-spacing-xl);
.disconnect-title {
font-size: var(--ov-meet-font-size-xxl);
font-weight: var(--ov-meet-font-weight-semibold);
color: var(--ov-meet-text-primary);
margin: 0 0 var(--ov-meet-spacing-sm) 0;
line-height: var(--ov-meet-line-height-tight);
@include ov-mobile-down {
font-size: var(--ov-meet-font-size-xl);
}
}
.disconnect-subtitle {
font-size: var(--ov-meet-font-size-md);
color: var(--ov-meet-text-secondary);
margin: 0;
line-height: var(--ov-meet-line-height-normal);
max-width: 400px;
margin-left: auto;
margin-right: auto;
@include ov-mobile-down {
font-size: var(--ov-meet-font-size-sm);
}
}
} }
mat-card-content p { .disconnect-footer {
font-size: 1em; padding-top: var(--ov-meet-spacing-lg);
color: #555; border-top: 1px solid var(--ov-meet-border-color-light);
.footer-text {
display: flex;
align-items: center;
justify-content: center;
gap: var(--ov-meet-spacing-xs);
font-size: var(--ov-meet-font-size-sm);
color: var(--ov-meet-text-hint);
margin: 0;
font-weight: var(--ov-meet-font-weight-medium);
}
@include ov-mobile-down {
.footer-text {
font-size: var(--ov-meet-font-size-xs);
flex-direction: column;
gap: var(--ov-meet-spacing-xs);
}
}
} }
mat-card-actions { .disconnect-content {
margin-top: 20px; animation: fadeIn 0.6s ease-out;
}
.disconnect-icon-container {
animation: scaleIn 0.8s ease-out 0.2s both;
}
@media (prefers-contrast: high) {
.disconnect-footer {
border-top-width: 2px;
}
.disconnect-icon-container::before {
border: 2px solid var(--ov-meet-color-primary);
}
}
@media (prefers-reduced-motion: reduce) {
.disconnect-content,
.disconnect-icon-container {
animation: none;
}
.logo-image {
transition: none;
}
}
[data-theme='dark'] {
.disconnect-icon-container::before {
background: rgba(25, 118, 210, 0.2);
}
} }

View File

@ -1,11 +1,53 @@
import { Component } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatCardModule } from '@angular/material/card'; import { MatCardModule } from '@angular/material/card';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { ActivatedRoute } from '@angular/router';
@Component({ @Component({
selector: 'ov-disconnected', selector: 'ov-disconnected',
standalone: true, standalone: true,
imports: [MatCardModule], imports: [CommonModule, MatCardModule, MatButtonModule, MatIconModule],
templateUrl: './disconnected.component.html', templateUrl: './disconnected.component.html',
styleUrl: './disconnected.component.scss' styleUrl: './disconnected.component.scss'
}) })
export class DisconnectedComponent {} export class DisconnectedComponent implements OnInit {
disconnectReason?: string;
constructor(private route: ActivatedRoute) {}
ngOnInit(): void {
// Get disconnect reason from query parameters
this.getDisconnectReasonFromQueryParams();
}
/**
* Retrieves the disconnect reason from URL query parameters
*/
private getDisconnectReasonFromQueryParams(): void {
const reason = this.route.snapshot.queryParams['reason'];
if (reason) {
// Map technical reasons to user-friendly messages
this.disconnectReason = this.mapReasonToUserMessage(reason);
}
}
/**
* Maps technical disconnect reasons to user-friendly messages
*/
private mapReasonToUserMessage(reason: string): string {
const reasonMap: { [key: string]: string } = {
disconnect: 'You have successfully disconnected from the meeting',
forceDisconnectByUser: 'You were removed from the meeting by meeting host',
forceDisconnectByServer: 'Your connection was terminated by the server',
sessionClosedByServer: 'The meeting was ended by the host',
networkDisconnect: 'Connection lost due to network connectivity issues',
openviduDisconnect: 'The meeting ended due to technical difficulties',
roomDeleted: 'The meeting room has been deleted',
browserClosed: 'The meeting ended when your browser was closed'
};
return reasonMap[reason] || reasonMap['disconnect'];
}
}

View File

@ -267,7 +267,40 @@ export class VideoRoomComponent implements OnInit, OnDestroy {
this.sessionStorageService.removeModeratorSecret(event.roomName); this.sessionStorageService.removeModeratorSecret(event.roomName);
} }
await this.navigationService.redirectTo(redirectURL, isExternalURL); // 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);
}
/**
* Maps ParticipantLeftReason to a query parameter value
*/
private getReasonParamFromEvent(reason: ParticipantLeftReason, isRoomDeleted: boolean): string {
if (isRoomDeleted) {
return 'roomDeleted';
}
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 onRecordingStartRequested(event: RecordingStartRequestedEvent) { async onRecordingStartRequested(event: RecordingStartRequestedEvent) {

View File

@ -91,6 +91,17 @@
} }
} }
@keyframes scaleIn {
from {
opacity: 0;
transform: scale(0.9);
}
to {
opacity: 1;
transform: scale(1);
}
}
// Transition classes // Transition classes
.fade-in { .fade-in {
animation: fadeIn 0.6s ease-out forwards; animation: fadeIn 0.6s ease-out forwards;