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

View File

@ -2,6 +2,24 @@
.panel-button {
border-radius: var(--ov-toolbar-buttons-radius);
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,
@ -93,18 +111,11 @@
}
// Chat badge adjustments for menu
.mat-badge {
mat-icon {
font-size: 18px !important;
width: 18px !important;
height: 18px !important;
}
}
.mat-badge-content {
background-color: var(--ov-warn-color) !important;
::ng-deep .mat-badge-content {
background-color: var(--ov-accent-action-color) !important;
color: var(--ov-text-primary-color) !important;
font-size: 10px !important;
font-weight: 600 !important;
min-width: 14px !important;
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';
@Component({
@ -8,24 +8,38 @@ import { ViewportService } from '../../../services/viewport/viewport.service';
standalone: false
})
export class ToolbarPanelButtonsComponent {
// Inputs from toolbar
@Input() isMinimal: boolean = false;
@Input() isConnectionLost: boolean = false;
@Input() isActivitiesOpened: boolean = false;
@Input() isParticipantsOpened: boolean = false;
@Input() isChatOpened: boolean = false;
@Input() unreadMessages: number = 0;
@Input() showActivitiesPanelButton: boolean = true;
@Input() showParticipantsPanelButton: boolean = true;
@Input() showChatPanelButton: boolean = true;
@Input() recordingStatus: any;
@Input() broadcastingStatus: any;
@Input() toolbarAdditionalPanelButtonsTemplate: TemplateRef<any> | undefined;
// Signal inputs from toolbar
isMinimal = input<boolean>(false);
isConnectionLost = input<boolean>(false);
isActivitiesOpened = input<boolean>(false);
isParticipantsOpened = input<boolean>(false);
isChatOpened = input<boolean>(false);
unreadMessages = input<number>(0);
showActivitiesPanelButton = input<boolean>(true);
showParticipantsPanelButton = input<boolean>(true);
showChatPanelButton = input<boolean>(true);
recordingStatus = input<any>();
broadcastingStatus = input<any>();
toolbarAdditionalPanelButtonsTemplate = input<TemplateRef<any> | undefined>();
totalParticipants = input<number>(0);
// Outputs back to toolbar
@Output() toggleActivitiesPanel: EventEmitter<string | undefined> = new EventEmitter();
@Output() toggleParticipantsPanel: EventEmitter<void> = new EventEmitter();
@Output() toggleChatPanel: EventEmitter<void> = new EventEmitter();
// Signal outputs back to toolbar
toggleActivitiesPanel = output<string | undefined>();
toggleParticipantsPanel = output<void>();
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) {}
@ -34,20 +48,6 @@ export class ToolbarPanelButtonsComponent {
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
onToggleActivities(expand?: string) {
this.toggleActivitiesPanel.emit(expand);

View File

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

View File

@ -3,6 +3,7 @@ import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
computed,
ContentChild,
EventEmitter,
HostListener,
@ -18,6 +19,8 @@ import { DocumentService } from '../../services/document/document.service';
import { PanelService } from '../../services/panel/panel.service';
import { MatMenuTrigger } from '@angular/material/menu';
import { Room, RoomEvent } from 'livekit-client';
import { LeaveButtonDirective, ToolbarMoreOptionsAdditionalMenuItemsDirective } from '../../directives/template/internals.directive';
import {
ToolbarAdditionalButtonsDirective,
ToolbarAdditionalPanelButtonsDirective
@ -26,6 +29,7 @@ import { BroadcastingStatus, BroadcastingStatusInfo, BroadcastingStopRequestedEv
import { ChatMessage } from '../../models/chat.model';
import { ILogger } from '../../models/logger.model';
import { PanelStatusInfo, PanelType } from '../../models/panel.model';
import { ParticipantLeftEvent, ParticipantLeftReason, ParticipantModel } from '../../models/participant.model';
import {
RecordingInfo,
RecordingStartRequestedEvent,
@ -33,8 +37,10 @@ import {
RecordingStatusInfo,
RecordingStopRequestedEvent
} from '../../models/recording.model';
import { ToolbarAdditionalButtonsPosition } from '../../models/toolbar.model';
import { ActionService } from '../../services/action/action.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 { DeviceService } from '../../services/device/device.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 { TemplateManagerService, ToolbarTemplateConfiguration } from '../../services/template/template-manager.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}.
@ -405,6 +406,16 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
*/
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
private _externalAdditionalButtons?: ToolbarAdditionalButtonsDirective;
private _externalLeaveButton?: LeaveButtonDirective;
@ -624,8 +635,8 @@ export class ToolbarComponent implements OnInit, OnDestroy, AfterViewInit {
await this.openviduService.disconnectRoom(() => {
this.onParticipantLeft.emit({
roomName: this.openviduService.getRoomName(),
participantName: this.participantService.getLocalParticipant()?.name || '',
identity: this.participantService.getLocalParticipant()?.identity || '',
participantName: this.participantService.localParticipantSignal()!.name || '',
identity: this.participantService.localParticipantSignal()!.identity || '',
reason: ParticipantLeftReason.LEAVE
});
this.onRoomDisconnected.emit();