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">
<mat-card class="disconnected-card">
<mat-card-header>
<mat-card-title>You have left the room</mat-card-title>
</mat-card-header>
<mat-card-content>
<p>The meeting has ended.</p>
</mat-card-content>
</mat-card>
<div class="disconnect-content" role="main" aria-labelledby="disconnect-title">
<div class="disconnect-icon-container">
<img
src="data:image/png;base64,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"
alt="Brand Logo"
class="logo-image"
/>
</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>

View File

@ -1,33 +1,153 @@
@import '../../../../../../src/assets/styles/design-tokens';
.disconnected-container {
@include ov-theme-transition;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background-color: var(--ov-meet-surface-background);
min-height: 100vh;
padding: var(--ov-meet-spacing-lg);
background: linear-gradient(135deg, var(--ov-meet-background-color) 0%, var(--ov-meet-background-secondary) 100%);
}
.disconnected-card {
width: 400px;
padding: 20px;
.disconnect-content {
@include ov-card;
max-width: 520px;
width: 100%;
text-align: center;
background-color: var(--ov-meet-surface-primary);
border-radius: var(--ov-meet-surface-radius);
padding: var(--ov-meet-spacing-xxl);
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;
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 {
font-size: 1.5em;
.disconnect-message {
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 {
font-size: 1em;
color: #555;
.disconnect-footer {
padding-top: var(--ov-meet-spacing-lg);
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 {
margin-top: 20px;
.disconnect-content {
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 { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { ActivatedRoute } from '@angular/router';
@Component({
selector: 'ov-disconnected',
standalone: true,
imports: [MatCardModule],
imports: [CommonModule, MatCardModule, MatButtonModule, MatIconModule],
templateUrl: './disconnected.component.html',
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);
}
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) {

View File

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