frontend: Refactor customization components

Moves the copy link button to a new component for additional toolbar buttons.

This allows for better organization and customization of the toolbar,
especially on mobile where space is limited. The "leave" button for
moderators is now separate.

Renamed components for better understanding and readability
This commit is contained in:
Carlos Santos 2025-11-25 13:01:10 +01:00
parent bd021c9576
commit f6abd1cb4c
29 changed files with 165 additions and 125 deletions

View File

@ -1,6 +1,7 @@
export * from './meeting-toolbar-buttons/meeting-toolbar-buttons.component';
export * from './meeting-share-link-panel/meeting-share-link-panel.component';
export * from './meeting-participant-panel-item/meeting-participant-panel-item.component';
export * from './meeting-layout/meeting-layout.component';
export * from './meeting-settings-panel/meeting-settings-panel.component';
export * from './meeting-toolbar-more-options-buttons/meeting-toolbar-more-options-buttons.component';
export * from './meeting-toolbar-leave-button/meeting-toolbar-leave-button.component';
export * from './meeting-toolbar-extra-buttons/meeting-toolbar-extra-buttons.component';
export * from './meeting-invite-panel/meeting-invite-panel.component';
export * from './meeting-participant-item/meeting-participant-item.component';
export * from './meeting-custom-layout/meeting-custom-layout.component';
export * from './meeting-settings-extensions/meeting-settings-extensions.component';
export * from './meeting-toolbar-more-options-menu/meeting-toolbar-more-options-menu.component';

View File

@ -7,13 +7,13 @@ import {
ParticipantService,
OpenViduService
} from 'openvidu-components-angular';
import { MeetingLayoutComponent } from './meeting-layout.component';
import { MeetingCustomLayoutComponent } from './meeting-custom-layout.component';
import { MeetLayoutService } from '../../../services/layout.service';
import { MeetLayoutMode } from '../../../models/layout.model';
describe('MeetingLayoutComponent', () => {
let component: MeetingLayoutComponent;
let fixture: ComponentFixture<MeetingLayoutComponent>;
let component: MeetingCustomLayoutComponent;
let fixture: ComponentFixture<MeetingCustomLayoutComponent>;
let mockLayoutService: jasmine.SpyObj<MeetLayoutService>;
let mockParticipantService: jasmine.SpyObj<ParticipantService>;
let mockOpenViduService: jasmine.SpyObj<OpenViduService>;
@ -56,7 +56,7 @@ describe('MeetingLayoutComponent', () => {
});
await TestBed.configureTestingModule({
imports: [MeetingLayoutComponent],
imports: [MeetingCustomLayoutComponent],
providers: [
{ provide: MeetLayoutService, useValue: mockLayoutService },
{ provide: ParticipantService, useValue: mockParticipantService },
@ -65,7 +65,7 @@ describe('MeetingLayoutComponent', () => {
]
}).compileComponents();
fixture = TestBed.createComponent(MeetingLayoutComponent);
fixture = TestBed.createComponent(MeetingCustomLayoutComponent);
component = fixture.componentInstance;
});

View File

@ -15,19 +15,19 @@ import { MeetingService } from '../../../services/meeting/meeting.service';
* active speakers to maximize client-side performance and scalability.
*/
@Component({
selector: 'ov-meeting-layout',
selector: 'ov-meeting-custom-layout',
imports: [OpenViduComponentsUiModule, ShareMeetingLinkComponent],
templateUrl: './meeting-layout.component.html',
styleUrl: './meeting-layout.component.scss'
templateUrl: './meeting-custom-layout.component.html',
styleUrl: './meeting-custom-layout.component.scss'
})
export class MeetingLayoutComponent {
export class MeetingCustomLayoutComponent {
private readonly loggerSrv = inject(LoggerService);
protected readonly layoutService = inject(MeetLayoutService);
protected readonly openviduService = inject(OpenViduService);
protected meetingContextService = inject(MeetingContextService);
protected meetingService = inject(MeetingService);
protected readonly destroyRef = inject(DestroyRef);
private readonly log: ILogger = this.loggerSrv.get('MeetingLayoutComponent');
private readonly log: ILogger = this.loggerSrv.get('MeetingCustomLayoutComponent');
protected readonly linkOverlayTitle = 'Start collaborating';
protected readonly linkOverlaySubtitle = 'Share this link to bring others into the meeting';
protected readonly linkOverlayTitleSize: 'sm' | 'md' | 'lg' | 'xl' = 'xl';

View File

@ -10,16 +10,16 @@ import { LoggerService } from 'openvidu-components-angular';
* inside the participants panel.
*/
@Component({
selector: 'ov-meeting-share-link-panel',
templateUrl: './meeting-share-link-panel.component.html',
styleUrls: ['./meeting-share-link-panel.component.scss'],
selector: 'ov-meeting-invite-panel',
templateUrl: './meeting-invite-panel.component.html',
styleUrls: ['./meeting-invite-panel.component.scss'],
imports: [CommonModule, ShareMeetingLinkComponent]
})
export class MeetingShareLinkPanelComponent {
export class MeetingInvitePanelComponent {
protected meetingContextService = inject(MeetingContextService);
protected meetingService = inject(MeetingService);
protected loggerService = inject(LoggerService);
protected log = this.loggerService.get('OpenVidu Meet - MeetingShareLinkPanel');
protected log = this.loggerService.get('OpenVidu Meet - MeetingInvitePanel');
/**
* Computed signal to determine if the share link should be shown

View File

@ -24,18 +24,18 @@ export interface ParticipantDisplayProperties {
* This component receives context from the template (participant, localParticipant).
*/
@Component({
selector: 'ov-meeting-participant-panel-item',
templateUrl: './meeting-participant-panel-item.component.html',
styleUrls: ['./meeting-participant-panel-item.component.scss'],
selector: 'ov-meeting-participant-item',
templateUrl: './meeting-participant-item.component.html',
styleUrls: ['./meeting-participant-item.component.scss'],
imports: [CommonModule, MatButtonModule, MatIconModule, MatTooltipModule, OpenViduComponentsUiModule]
})
export class MeetingParticipantPanelItemComponent {
export class MeetingParticipantItemComponent {
// Template reference for the component's template
@ViewChild('template', { static: true }) template!: TemplateRef<any>;
protected meetingService: MeetingService = inject(MeetingService);
protected loggerService = inject(LoggerService);
protected log = this.loggerService.get('OpenVidu Meet - ParticipantPanelItem');
protected log = this.loggerService.get('OpenVidu Meet - MeetingParticipantItem');
// Tooltips (could be made configurable in the future if needed)
protected readonly moderatorBadgeTooltip = 'Moderator';

View File

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

View File

@ -15,7 +15,7 @@ import { MeetingContextService } from '../../../services/meeting/meeting-context
* Component for additional settings in the Settings Panel.
*/
@Component({
selector: 'ov-meeting-settings-panel',
selector: 'ov-meeting-settings-extensions',
imports: [
CommonModule,
MatIconModule,
@ -26,10 +26,10 @@ import { MeetingContextService } from '../../../services/meeting/meeting-context
MatSelectModule,
FormsModule
],
templateUrl: './meeting-settings-panel.component.html',
styleUrl: './meeting-settings-panel.component.scss'
templateUrl: './meeting-settings-extensions.component.html',
styleUrl: './meeting-settings-extensions.component.scss'
})
export class MeetingSettingsPanelComponent {
export class MeetingSettingsExtensionsComponent {
private readonly layoutService = inject(MeetLayoutService);
protected readonly meetingContextService = inject(MeetingContextService);

View File

@ -0,0 +1,21 @@
<!-- Copy Link Button -->
@if (showCopyLinkButton()) {
@if (isMobile()) {
<!-- It will appear inside of a menu on mobile view -->
<button id="copy-speaker-link" mat-menu-item (click)="onCopyLinkClick()" [disableRipple]="true">
<mat-icon>link</mat-icon>
<span class="button-text">{{ copyLinkText }}</span>
</button>
} @else {
<!-- It will appear as an icon button on desktop view -->
<button
id="copy-speaker-link"
mat-icon-button
(click)="onCopyLinkClick()"
[disableRipple]="true"
[matTooltip]="copyLinkTooltip"
>
<mat-icon>link</mat-icon>
</button>
}
}

View File

@ -0,0 +1,52 @@
import { Component, inject, computed } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatMenuModule } from '@angular/material/menu';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatTooltipModule } from '@angular/material/tooltip';
import { MeetingContextService } from '../../../services/meeting/meeting-context.service';
import { MeetingService } from '../../../services/meeting/meeting.service';
import { LoggerService } from 'openvidu-components-angular';
/**
* Component for extra toolbar buttons (like copy meeting link).
* These buttons can appear inside the "More Options" menu on mobile.
*/
@Component({
selector: 'ov-meeting-toolbar-extra-buttons',
templateUrl: './meeting-toolbar-extra-buttons.component.html',
styleUrls: ['./meeting-toolbar-extra-buttons.component.scss'],
imports: [CommonModule, MatButtonModule, MatIconModule, MatMenuModule, MatTooltipModule]
})
export class MeetingToolbarExtraButtonsComponent {
protected meetingContextService = inject(MeetingContextService);
protected meetingService = inject(MeetingService);
protected loggerService = inject(LoggerService);
protected log = this.loggerService.get('OpenVidu Meet - MeetingToolbarExtraButtons');
protected readonly copyLinkTooltip = 'Copy the meeting link';
protected readonly copyLinkText = 'Copy meeting link';
/**
* Whether to show the copy link button
*/
protected showCopyLinkButton = computed(() => {
return this.meetingContextService.canModerateRoom();
});
/**
* Whether the device is mobile (affects button style)
*/
protected isMobile = computed(() => {
return this.meetingContextService.isMobile();
});
onCopyLinkClick(): void {
const room = this.meetingContextService.meetRoom();
if (!room) {
this.log.e('Cannot copy link: meeting room is undefined');
return;
}
this.meetingService.copyMeetingSpeakerLink(room);
}
}

View File

@ -1,24 +1,4 @@
<!-- Copy Link Button -->
@if (showCopyLinkButton()) {
@if (isMobile()) {
<button id="copy-speaker-link" mat-menu-item (click)="onCopyLinkClick()" [disableRipple]="true">
<mat-icon>link</mat-icon>
<span class="button-text">{{ copyLinkText }}</span>
</button>
} @else {
<button
id="copy-speaker-link"
mat-icon-button
(click)="onCopyLinkClick()"
[disableRipple]="true"
[matTooltip]="copyLinkTooltip"
>
<mat-icon>link</mat-icon>
</button>
}
}
<!-- Leave Menu -->
<!-- Leave Button for Moderators -->
@if (showLeaveMenu()) {
<button
id="leave-btn"

View File

@ -7,36 +7,27 @@ import { MatMenuModule } from '@angular/material/menu';
import { MatTooltipModule } from '@angular/material/tooltip';
import { MeetingContextService } from '../../../services/meeting/meeting-context.service';
import { MeetingService } from '../../../services/meeting/meeting.service';
import { LoggerService, OpenViduService, ViewportService } from 'openvidu-components-angular';
import { LoggerService, OpenViduService } from 'openvidu-components-angular';
/**
* Reusable component for meeting toolbar additional buttons.
* Reusable component for meeting toolbar Leave button.
*/
@Component({
selector: 'ov-meeting-toolbar-buttons',
templateUrl: './meeting-toolbar-buttons.component.html',
styleUrls: ['./meeting-toolbar-buttons.component.scss'],
selector: 'ov-meeting-toolbar-leave-button',
templateUrl: './meeting-toolbar-leave-button.component.html',
styleUrls: ['./meeting-toolbar-leave-button.component.scss'],
imports: [CommonModule, MatButtonModule, MatIconModule, MatMenuModule, MatTooltipModule, MatDividerModule]
})
export class MeetingToolbarButtonsComponent {
export class MeetingToolbarLeaveButtonComponent {
protected meetingContextService = inject(MeetingContextService);
protected meetingService = inject(MeetingService);
protected loggerService = inject(LoggerService);
protected log = this.loggerService.get('OpenVidu Meet - MeetingToolbarButtons');
protected log = this.loggerService.get('OpenVidu Meet - MeetingToolbarLeaveButtons');
protected openviduService = inject(OpenViduService);
protected readonly copyLinkTooltip = 'Copy the meeting link';
protected readonly copyLinkText = 'Copy meeting link';
protected readonly leaveMenuTooltip = 'Leave options';
protected readonly leaveOptionText = 'Leave meeting';
protected readonly endMeetingOptionText = 'End meeting for all';
/**
* Whether to show the copy link button
*/
protected showCopyLinkButton = computed(() => {
return this.meetingContextService.canModerateRoom();
});
/**
* Whether to show the leave menu with options
*/
@ -51,16 +42,6 @@ export class MeetingToolbarButtonsComponent {
return this.meetingContextService.isMobile();
});
onCopyLinkClick(): void {
const room = this.meetingContextService.meetRoom();
if (!room) {
this.log.e('Cannot copy link: meeting room is undefined');
return;
}
this.meetingService.copyMeetingSpeakerLink(room);
}
async onLeaveMeetingClick(): Promise<void> {
await this.openviduService.disconnectRoom();
}

View File

@ -1,18 +1,18 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MeetingToolbarMoreOptionsButtonsComponent } from './meeting-toolbar-more-options-buttons.component';
import { MeetingToolbarMoreOptionsMenuComponent } from './meeting-toolbar-more-options-menu.component';
describe('MeetingToolbarMoreOptionsButtonsComponent', () => {
let component: MeetingToolbarMoreOptionsButtonsComponent;
let fixture: ComponentFixture<MeetingToolbarMoreOptionsButtonsComponent>;
let component: MeetingToolbarMoreOptionsMenuComponent;
let fixture: ComponentFixture<MeetingToolbarMoreOptionsMenuComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [MeetingToolbarMoreOptionsButtonsComponent]
imports: [MeetingToolbarMoreOptionsMenuComponent]
})
.compileComponents();
fixture = TestBed.createComponent(MeetingToolbarMoreOptionsButtonsComponent);
fixture = TestBed.createComponent(MeetingToolbarMoreOptionsMenuComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

View File

@ -10,10 +10,9 @@ import { MeetingContextService } from '../../../services/meeting/meeting-context
/**
* Component for additional menu items in the toolbar's "More Options" menu.
* This component handles custom actions like opening the settings panel for grid layout changes.
* It follows the responsive pattern from openvidu-components-angular, adapting to mobile/tablet/desktop views.
*/
@Component({
selector: 'ov-meeting-toolbar-more-options-buttons',
selector: 'ov-meeting-toolbar-more-options-menu',
imports: [
CommonModule,
MatIconModule,
@ -21,10 +20,10 @@ import { MeetingContextService } from '../../../services/meeting/meeting-context
MatMenuModule,
MatTooltipModule
],
templateUrl: './meeting-toolbar-more-options-buttons.component.html',
styleUrl: './meeting-toolbar-more-options-buttons.component.scss'
templateUrl: './meeting-toolbar-more-options-menu.component.html',
styleUrl: './meeting-toolbar-more-options-menu.component.scss'
})
export class MeetingToolbarMoreOptionsButtonsComponent {
export class MeetingToolbarMoreOptionsMenuComponent {
/**
* Viewport service for responsive behavior detection
* Injected from openvidu-components-angular

View File

@ -55,37 +55,42 @@
(onRecordingStopRequested)="eventHandlerService.onRecordingStopRequested($event)"
(onViewRecordingsClicked)="onViewRecordingsClicked()"
>
<!-- Toolbar Leave Button (for moderators) -->
<ng-container *ovToolbarLeaveButton>
<ng-content select="ov-meeting-toolbar-leave-button"></ng-content>
</ng-container>
<!-- Toolbar Additional Buttons -->
<div *ovToolbarAdditionalButtons>
<ng-content select="ov-meeting-toolbar-buttons[slot='additional-buttons']"></ng-content>
<ng-content select="ov-meeting-toolbar-extra-buttons"></ng-content>
</div>
<!-- Toolbar More Options Additional Items -->
<ng-container *ovToolbarMoreOptionsAdditionalMenuItems>
<ng-content select="ov-meeting-toolbar-more-options-buttons[slot='additional-buttons']"></ng-content>
<ng-content select="ov-meeting-toolbar-more-options-menu"></ng-content>
</ng-container>
<!-- Share Link Panel After Local Participant -->
<div *ovParticipantPanelAfterLocalParticipant>
<ng-content select="ov-meeting-share-link-panel[slot='after-local-participant']"></ng-content>
<ng-content select="ov-meeting-invite-panel"></ng-content>
</div>
<!-- Participant Panel Item Template -->
<div *ovParticipantPanelItem="let participant">
<ng-container
[ngTemplateOutlet]="participantPanelItemTemplate"
[ngTemplateOutlet]="participantItemTemplate()"
[ngTemplateOutletContext]="{ participant: participant, localParticipant: localParticipant() }"
>
</ng-container>
</div>
<ng-container *ovSettingsPanelGeneralAdditionalElements>
<ng-content select="ov-meeting-settings-panel[slot='additional-general-elements']"></ng-content>
<ng-content select="ov-meeting-settings-extensions"></ng-content>
</ng-container>
<!-- Custom layout component -->
<ng-container *ovLayout>
<ng-content select="ov-meeting-layout[slot='layout']"></ng-content>
<ng-content select="ov-meeting-custom-layout"></ng-content>
</ng-container>
</ov-videoconference>
}

View File

@ -13,7 +13,7 @@ import {
ViewportService
} from 'openvidu-components-angular';
import { Subject } from 'rxjs';
import { MeetingParticipantPanelItemComponent } from '../../customization';
import { MeetingParticipantItemComponent } from '../../customization';
import {
ApplicationFeatures,
FeatureConfigurationService,
@ -44,17 +44,15 @@ import { MeetingLobbyComponent } from '../../components/meeting-lobby/meeting-lo
providers: [MeetingLobbyService, MeetingEventHandlerService]
})
export class MeetingComponent implements OnInit {
protected _participantPanelItem?: MeetingParticipantPanelItemComponent;
protected _participantItem?: MeetingParticipantItemComponent;
// Template reference for custom participant panel item
@ContentChild(MeetingParticipantPanelItemComponent)
set participantPanelItem(value: MeetingParticipantPanelItemComponent | undefined) {
@ContentChild(MeetingParticipantItemComponent)
set participantItem(value: MeetingParticipantItemComponent | undefined) {
// Store the reference to the custom participant panel item component
this._participantPanelItem = value;
}
get participantPanelItemTemplate(): TemplateRef<any> | undefined {
return this._participantPanelItem?.template;
this._participantItem = value;
}
protected participantItemTemplate = computed(() => this._participantItem?.template);
/**
* Controls whether to show lobby (true) or meeting view (false)

View File

@ -1,9 +1,9 @@
<ov-meeting>
<ov-meeting-toolbar-buttons slot="additional-buttons"></ov-meeting-toolbar-buttons>
<ov-meeting-toolbar-more-options-buttons slot="additional-buttons"></ov-meeting-toolbar-more-options-buttons>
<ov-meeting-share-link-panel slot="after-local-participant"></ov-meeting-share-link-panel>
<ov-meeting-participant-panel-item slot="participant-panel-item"></ov-meeting-participant-panel-item>
<ov-meeting-settings-panel slot="additional-general-elements"></ov-meeting-settings-panel>
<ov-meeting-layout slot="layout"></ov-meeting-layout>
<ov-meeting-toolbar-leave-button></ov-meeting-toolbar-leave-button>
<ov-meeting-toolbar-extra-buttons></ov-meeting-toolbar-extra-buttons>
<ov-meeting-toolbar-more-options-menu></ov-meeting-toolbar-more-options-menu>
<ov-meeting-invite-panel></ov-meeting-invite-panel>
<ov-meeting-participant-item></ov-meeting-participant-item>
<ov-meeting-settings-extensions></ov-meeting-settings-extensions>
<ov-meeting-custom-layout></ov-meeting-custom-layout>
</ov-meeting>

View File

@ -1,24 +1,26 @@
import { Component } from '@angular/core';
import {
MeetingComponent,
MeetingLayoutComponent,
MeetingParticipantPanelItemComponent,
MeetingShareLinkPanelComponent,
MeetingToolbarButtonsComponent,
MeetingSettingsPanelComponent,
MeetingToolbarMoreOptionsButtonsComponent
MeetingCustomLayoutComponent,
MeetingParticipantItemComponent,
MeetingInvitePanelComponent,
MeetingToolbarLeaveButtonComponent,
MeetingToolbarExtraButtonsComponent,
MeetingSettingsExtensionsComponent,
MeetingToolbarMoreOptionsMenuComponent
} from '@openvidu-meet/shared-components';
@Component({
selector: 'app-ce-ov-meeting',
imports: [
MeetingComponent,
MeetingToolbarButtonsComponent,
MeetingShareLinkPanelComponent,
MeetingParticipantPanelItemComponent,
MeetingLayoutComponent,
MeetingToolbarMoreOptionsButtonsComponent,
MeetingSettingsPanelComponent
MeetingToolbarLeaveButtonComponent,
MeetingToolbarExtraButtonsComponent,
MeetingInvitePanelComponent,
MeetingParticipantItemComponent,
MeetingCustomLayoutComponent,
MeetingToolbarMoreOptionsMenuComponent,
MeetingSettingsExtensionsComponent
],
templateUrl: './app-ce-meeting.component.html',
styleUrl: './app-ce-meeting.component.scss'