ov-components: refactor toolbar panel buttons to use Angular signals for state management and enhance badge styles

master
Carlos Santos 2025-12-22 19:42:49 +01:00
parent 13cfff1aad
commit 703403f182
5 changed files with 113 additions and 82 deletions

View File

@ -1,12 +1,12 @@
<!-- Responsive container: show individual buttons on larger screens, collapsed menu on smaller screens --> <!-- Responsive container: show individual buttons on larger screens, collapsed menu on smaller screens -->
<ng-container *ngIf="shouldShowCollapsed; else uncollapsedButtons"> <ng-container *ngIf="shouldShowCollapsed; else uncollapsedButtons">
<div class="collapsed-menu-container" *ngIf="visibleButtonsCount > 0"> <div class="collapsed-menu-container" *ngIf="visibleButtonsCount() > 0">
<button <button
mat-icon-button mat-icon-button
class="fab-menu-trigger" class="fab-menu-trigger"
[matMenuTriggerFor]="panelsMenu" [matMenuTriggerFor]="panelsMenu"
[disabled]="isConnectionLost" [disabled]="isConnectionLost()"
[class.active-btn]="isAnyPanelOpened" [class.active-btn]="isAnyPanelOpened()"
matTooltip="{{ 'TOOLBAR.PANELS' | translate }}" matTooltip="{{ 'TOOLBAR.PANELS' | translate }}"
#menuTrigger="matMenuTrigger" #menuTrigger="matMenuTrigger"
> >
@ -19,41 +19,43 @@
<button <button
mat-menu-item mat-menu-item
class="panel-menu-item" class="panel-menu-item"
*ngIf="!isMinimal && showActivitiesPanelButton" *ngIf="!isMinimal() && showActivitiesPanelButton()"
(click)="onToggleActivities()" (click)="onToggleActivities()"
[disabled]="isConnectionLost" [disabled]="isConnectionLost()"
[class.active-menu-item]="isActivitiesOpened" [class.active-menu-item]="isActivitiesOpened()"
> >
<mat-icon>category</mat-icon> <mat-icon>category</mat-icon>
</button> </button>
<!-- Participants menu item --> <!-- Participants menu item -->
<button <button
*ngIf="!isMinimal() && showParticipantsPanelButton()"
mat-menu-item mat-menu-item
[matBadge]="totalParticipants()"
[matBadgeHidden]="totalParticipants() === 0"
matBadgePosition="above before"
class="panel-menu-item" class="panel-menu-item"
*ngIf="!isMinimal && showParticipantsPanelButton"
(click)="onToggleParticipants()" (click)="onToggleParticipants()"
[disabled]="isConnectionLost" [disabled]="isConnectionLost()"
[class.active-menu-item]="isParticipantsOpened" [class.active-menu-item]="isParticipantsOpened()"
> >
<mat-icon>people</mat-icon> <mat-icon aria-hidden="false"> people</mat-icon>
</button> </button>
<!-- Chat menu item --> <!-- Chat menu item -->
<button <button
mat-menu-item mat-menu-item
class="panel-menu-item" class="panel-menu-item"
*ngIf="!isMinimal && showChatPanelButton" *ngIf="!isMinimal() && showChatPanelButton()"
(click)="onToggleChat()" (click)="onToggleChat()"
[disabled]="isConnectionLost" [disabled]="isConnectionLost()"
[class.active-menu-item]="isChatOpened" [class.active-menu-item]="isChatOpened()"
> >
<mat-icon <mat-icon
matBadge="{{ unreadMessages }}" [matBadge]="unreadMessages()"
[matBadgeHidden]="unreadMessages === 0" [matBadgeHidden]="unreadMessages() === 0"
matBadgePosition="above before" matBadgePosition="above before"
matBadgeSize="small" matBadgeSize="small"
matBadgeColor="accent"
aria-hidden="false" aria-hidden="false"
> >
chat chat
@ -61,8 +63,8 @@
</button> </button>
<!-- External additional panel buttons in menu --> <!-- External additional panel buttons in menu -->
<ng-container *ngIf="toolbarAdditionalPanelButtonsTemplate"> <ng-container *ngIf="toolbarAdditionalPanelButtonsTemplate()">
<ng-container *ngTemplateOutlet="toolbarAdditionalPanelButtonsTemplate"></ng-container> <ng-container *ngTemplateOutlet="toolbarAdditionalPanelButtonsTemplate()"></ng-container>
</ng-container> </ng-container>
</mat-menu> </mat-menu>
</div> </div>
@ -75,11 +77,11 @@
mat-icon-button mat-icon-button
id="activities-panel-btn" id="activities-panel-btn"
class="panel-button" class="panel-button"
*ngIf="!isMinimal && showActivitiesPanelButton" *ngIf="!isMinimal() && showActivitiesPanelButton()"
matTooltip="{{ 'TOOLBAR.ACTIVITIES' | translate }}" matTooltip="{{ 'TOOLBAR.ACTIVITIES' | translate }}"
(click)="onToggleActivities()" (click)="onToggleActivities()"
[disabled]="isConnectionLost" [disabled]="isConnectionLost()"
[class.active-btn]="isActivitiesOpened" [class.active-btn]="isActivitiesOpened()"
> >
<mat-icon>category</mat-icon> <mat-icon>category</mat-icon>
</button> </button>
@ -89,13 +91,20 @@
mat-icon-button mat-icon-button
class="panel-button" class="panel-button"
id="participants-panel-btn" id="participants-panel-btn"
*ngIf="!isMinimal && showParticipantsPanelButton" *ngIf="!isMinimal() && showParticipantsPanelButton()"
matTooltip="{{ 'TOOLBAR.PARTICIPANTS' | translate }}" matTooltip="{{ 'TOOLBAR.PARTICIPANTS' | translate }}"
(click)="onToggleParticipants()" (click)="onToggleParticipants()"
[disabled]="isConnectionLost" [disabled]="isConnectionLost()"
[class.active-btn]="isParticipantsOpened" [class.active-btn]="isParticipantsOpened()"
> >
<mat-icon>people</mat-icon> <mat-icon
[matBadge]="totalParticipants()"
[matBadgeHidden]="totalParticipants() === 0"
matBadgePosition="above before"
matBadgeSize="small"
aria-hidden="false"
>people</mat-icon
>
</button> </button>
<!-- Default chat button --> <!-- Default chat button -->
@ -103,18 +112,17 @@
mat-icon-button mat-icon-button
class="panel-button" class="panel-button"
id="chat-panel-btn" id="chat-panel-btn"
*ngIf="!isMinimal && showChatPanelButton" *ngIf="!isMinimal() && showChatPanelButton()"
matTooltip="{{ 'TOOLBAR.CHAT' | translate }}" matTooltip="{{ 'TOOLBAR.CHAT' | translate }}"
(click)="onToggleChat()" (click)="onToggleChat()"
[disabled]="isConnectionLost" [disabled]="isConnectionLost()"
[class.active-btn]="isChatOpened" [class.active-btn]="isChatOpened()"
> >
<mat-icon <mat-icon
matBadge="{{ unreadMessages }}" [matBadge]="unreadMessages()"
[matBadgeHidden]="unreadMessages === 0" [matBadgeHidden]="unreadMessages() === 0"
matBadgePosition="above before" matBadgePosition="above before"
matBadgeSize="small" matBadgeSize="small"
matBadgeColor="accent"
aria-hidden="false" aria-hidden="false"
> >
chat chat
@ -122,7 +130,7 @@
</button> </button>
<!-- External additional panel buttons --> <!-- External additional panel buttons -->
<ng-container *ngIf="toolbarAdditionalPanelButtonsTemplate"> <ng-container *ngIf="toolbarAdditionalPanelButtonsTemplate()">
<ng-container *ngTemplateOutlet="toolbarAdditionalPanelButtonsTemplate"></ng-container> <ng-container *ngTemplateOutlet="toolbarAdditionalPanelButtonsTemplate()"></ng-container>
</ng-container> </ng-container>
</ng-template> </ng-template>

View File

@ -2,6 +2,24 @@
.panel-button { .panel-button {
border-radius: var(--ov-toolbar-buttons-radius); border-radius: var(--ov-toolbar-buttons-radius);
color: var(--ov-text-primary-color); color: var(--ov-text-primary-color);
// Custom badge styles for participants and chat buttons
::ng-deep .mat-badge-content {
background-color: var(--ov-accent-action-color) !important;
color: var(--ov-text-primary-color) !important;
font-weight: 600;
font-size: 10px;
min-width: 16px;
height: 16px;
line-height: 16px;
}
::ng-deep .mat-badge-small .mat-badge-content {
font-size: 9px;
min-width: 14px;
height: 14px;
line-height: 14px;
}
} }
.active-btn, .active-btn,
@ -93,18 +111,11 @@
} }
// Chat badge adjustments for menu // Chat badge adjustments for menu
.mat-badge { ::ng-deep .mat-badge-content {
mat-icon { background-color: var(--ov-accent-action-color) !important;
font-size: 18px !important;
width: 18px !important;
height: 18px !important;
}
}
.mat-badge-content {
background-color: var(--ov-warn-color) !important;
color: var(--ov-text-primary-color) !important; color: var(--ov-text-primary-color) !important;
font-size: 10px !important; font-size: 10px !important;
font-weight: 600 !important;
min-width: 14px !important; min-width: 14px !important;
height: 14px !important; height: 14px !important;
line-height: 14px !important; line-height: 14px !important;

View File

@ -1,4 +1,4 @@
import { Component, EventEmitter, Input, Output, TemplateRef } from '@angular/core'; import { Component, TemplateRef, computed, input, output } from '@angular/core';
import { ViewportService } from '../../../services/viewport/viewport.service'; import { ViewportService } from '../../../services/viewport/viewport.service';
@Component({ @Component({
@ -8,24 +8,38 @@ import { ViewportService } from '../../../services/viewport/viewport.service';
standalone: false standalone: false
}) })
export class ToolbarPanelButtonsComponent { export class ToolbarPanelButtonsComponent {
// Inputs from toolbar // Signal inputs from toolbar
@Input() isMinimal: boolean = false; isMinimal = input<boolean>(false);
@Input() isConnectionLost: boolean = false; isConnectionLost = input<boolean>(false);
@Input() isActivitiesOpened: boolean = false; isActivitiesOpened = input<boolean>(false);
@Input() isParticipantsOpened: boolean = false; isParticipantsOpened = input<boolean>(false);
@Input() isChatOpened: boolean = false; isChatOpened = input<boolean>(false);
@Input() unreadMessages: number = 0; unreadMessages = input<number>(0);
@Input() showActivitiesPanelButton: boolean = true; showActivitiesPanelButton = input<boolean>(true);
@Input() showParticipantsPanelButton: boolean = true; showParticipantsPanelButton = input<boolean>(true);
@Input() showChatPanelButton: boolean = true; showChatPanelButton = input<boolean>(true);
@Input() recordingStatus: any; recordingStatus = input<any>();
@Input() broadcastingStatus: any; broadcastingStatus = input<any>();
@Input() toolbarAdditionalPanelButtonsTemplate: TemplateRef<any> | undefined; toolbarAdditionalPanelButtonsTemplate = input<TemplateRef<any> | undefined>();
totalParticipants = input<number>(0);
// Outputs back to toolbar // Signal outputs back to toolbar
@Output() toggleActivitiesPanel: EventEmitter<string | undefined> = new EventEmitter(); toggleActivitiesPanel = output<string | undefined>();
@Output() toggleParticipantsPanel: EventEmitter<void> = new EventEmitter(); toggleParticipantsPanel = output<void>();
@Output() toggleChatPanel: EventEmitter<void> = new EventEmitter(); toggleChatPanel = output<void>();
// Computed signals
visibleButtonsCount = computed(() => {
let count = 0;
if (!this.isMinimal() && this.showActivitiesPanelButton()) count++;
if (!this.isMinimal() && this.showParticipantsPanelButton()) count++;
if (!this.isMinimal() && this.showChatPanelButton()) count++;
return count;
});
isAnyPanelOpened = computed(() => {
return this.isActivitiesOpened() || this.isParticipantsOpened() || this.isChatOpened();
});
constructor(public viewportService: ViewportService) {} constructor(public viewportService: ViewportService) {}
@ -34,20 +48,6 @@ export class ToolbarPanelButtonsComponent {
return this.viewportService.isMobileView() return this.viewportService.isMobileView()
} }
// Get count of visible buttons
get visibleButtonsCount(): number {
let count = 0;
if (!this.isMinimal && this.showActivitiesPanelButton) count++;
if (!this.isMinimal && this.showParticipantsPanelButton) count++;
if (!this.isMinimal && this.showChatPanelButton) count++;
return count;
}
// Check if any panel is currently opened
get isAnyPanelOpened(): boolean {
return this.isActivitiesOpened || this.isParticipantsOpened || this.isChatOpened;
}
// Local methods that emit events // Local methods that emit events
onToggleActivities(expand?: string) { onToggleActivities(expand?: string) {
this.toggleActivitiesPanel.emit(expand); this.toggleActivitiesPanel.emit(expand);

View File

@ -104,6 +104,7 @@
[showChatPanelButton]="showChatPanelButton" [showChatPanelButton]="showChatPanelButton"
[recordingStatus]="recordingStatus" [recordingStatus]="recordingStatus"
[broadcastingStatus]="broadcastingStatus" [broadcastingStatus]="broadcastingStatus"
[totalParticipants]="totalParticipants()"
[toolbarAdditionalPanelButtonsTemplate]="toolbarAdditionalPanelButtonsTemplate" [toolbarAdditionalPanelButtonsTemplate]="toolbarAdditionalPanelButtonsTemplate"
(toggleActivitiesPanel)="toggleActivitiesPanel($event)" (toggleActivitiesPanel)="toggleActivitiesPanel($event)"
(toggleParticipantsPanel)="toggleParticipantsPanel()" (toggleParticipantsPanel)="toggleParticipantsPanel()"

View File

@ -3,6 +3,7 @@ import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
computed,
ContentChild, ContentChild,
EventEmitter, EventEmitter,
HostListener, HostListener,
@ -18,6 +19,8 @@ import { DocumentService } from '../../services/document/document.service';
import { PanelService } from '../../services/panel/panel.service'; import { PanelService } from '../../services/panel/panel.service';
import { MatMenuTrigger } from '@angular/material/menu'; import { MatMenuTrigger } from '@angular/material/menu';
import { Room, RoomEvent } from 'livekit-client';
import { LeaveButtonDirective, ToolbarMoreOptionsAdditionalMenuItemsDirective } from '../../directives/template/internals.directive';
import { import {
ToolbarAdditionalButtonsDirective, ToolbarAdditionalButtonsDirective,
ToolbarAdditionalPanelButtonsDirective ToolbarAdditionalPanelButtonsDirective
@ -26,6 +29,7 @@ import { BroadcastingStatus, BroadcastingStatusInfo, BroadcastingStopRequestedEv
import { ChatMessage } from '../../models/chat.model'; import { ChatMessage } from '../../models/chat.model';
import { ILogger } from '../../models/logger.model'; import { ILogger } from '../../models/logger.model';
import { PanelStatusInfo, PanelType } from '../../models/panel.model'; import { PanelStatusInfo, PanelType } from '../../models/panel.model';
import { ParticipantLeftEvent, ParticipantLeftReason, ParticipantModel } from '../../models/participant.model';
import { import {
RecordingInfo, RecordingInfo,
RecordingStartRequestedEvent, RecordingStartRequestedEvent,
@ -33,8 +37,10 @@ import {
RecordingStatusInfo, RecordingStatusInfo,
RecordingStopRequestedEvent RecordingStopRequestedEvent
} from '../../models/recording.model'; } from '../../models/recording.model';
import { ToolbarAdditionalButtonsPosition } from '../../models/toolbar.model';
import { ActionService } from '../../services/action/action.service'; import { ActionService } from '../../services/action/action.service';
import { BroadcastingService } from '../../services/broadcasting/broadcasting.service'; import { BroadcastingService } from '../../services/broadcasting/broadcasting.service';
import { CdkOverlayService } from '../../services/cdk-overlay/cdk-overlay.service';
import { OpenViduComponentsConfigService } from '../../services/config/directive-config.service'; import { OpenViduComponentsConfigService } from '../../services/config/directive-config.service';
import { DeviceService } from '../../services/device/device.service'; import { DeviceService } from '../../services/device/device.service';
import { LayoutService } from '../../services/layout/layout.service'; import { LayoutService } from '../../services/layout/layout.service';
@ -46,11 +52,6 @@ import { RecordingService } from '../../services/recording/recording.service';
import { StorageService } from '../../services/storage/storage.service'; import { StorageService } from '../../services/storage/storage.service';
import { TemplateManagerService, ToolbarTemplateConfiguration } from '../../services/template/template-manager.service'; import { TemplateManagerService, ToolbarTemplateConfiguration } from '../../services/template/template-manager.service';
import { TranslateService } from '../../services/translate/translate.service'; import { TranslateService } from '../../services/translate/translate.service';
import { CdkOverlayService } from '../../services/cdk-overlay/cdk-overlay.service';
import { ParticipantLeftEvent, ParticipantLeftReason, ParticipantModel } from '../../models/participant.model';
import { Room, RoomEvent } from 'livekit-client';
import { ToolbarAdditionalButtonsPosition } from '../../models/toolbar.model';
import { LeaveButtonDirective, ToolbarMoreOptionsAdditionalMenuItemsDirective } from '../../directives/template/internals.directive';
/** /**
* The **ToolbarComponent** is hosted inside of the {@link VideoconferenceComponent}. * The **ToolbarComponent** is hosted inside of the {@link VideoconferenceComponent}.
@ -405,6 +406,16 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
*/ */
templateConfig: ToolbarTemplateConfiguration = {}; templateConfig: ToolbarTemplateConfiguration = {};
/**
* @internal
* Computed signal for total participants count (local + remote)
*/
totalParticipants = computed(() => {
const local = this.participantService.localParticipantSignal();
const remotes = this.participantService.remoteParticipantsSignal();
return (local ? 1 : 0) + remotes.length;
});
// Store directive references for template setup // Store directive references for template setup
private _externalAdditionalButtons?: ToolbarAdditionalButtonsDirective; private _externalAdditionalButtons?: ToolbarAdditionalButtonsDirective;
private _externalLeaveButton?: LeaveButtonDirective; private _externalLeaveButton?: LeaveButtonDirective;
@ -624,8 +635,8 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
await this.openviduService.disconnectRoom(() => { await this.openviduService.disconnectRoom(() => {
this.onParticipantLeft.emit({ this.onParticipantLeft.emit({
roomName: this.openviduService.getRoomName(), roomName: this.openviduService.getRoomName(),
participantName: this.participantService.getLocalParticipant()?.name || '', participantName: this.participantService.localParticipantSignal()!.name || '',
identity: this.participantService.getLocalParticipant()?.identity || '', identity: this.participantService.localParticipantSignal()!.identity || '',
reason: ParticipantLeftReason.LEAVE reason: ParticipantLeftReason.LEAVE
}); });
this.onRoomDisconnected.emit(); this.onRoomDisconnected.emit();