diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/customization/meeting-captions/meeting-captions.component.html b/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/customization/meeting-captions/meeting-captions.component.html index 0257acb4..2fcfdfc0 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/customization/meeting-captions/meeting-captions.component.html +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/customization/meeting-captions/meeting-captions.component.html @@ -1,6 +1,6 @@
- @for (caption of captions(); track trackByCaption($index, caption)) { + @for (caption of captions(); track caption.id) {
([]); // Track animation state for each caption - protected readonly captionAnimationState = signal>(new Map()); + captionAnimationState = signal>(new Map()); // ViewChildren to access caption text containers for auto-scroll @ViewChildren('captionTextContainer') @@ -77,17 +77,6 @@ export class MeetingCaptionsComponent { return classes.join(' '); } - /** - * Tracks captions by their ID for optimal Angular rendering. - * - * @param index Item index - * @param caption Caption item - * @returns Unique identifier - */ - protected trackByCaption(index: number, caption: Caption): string { - return caption.id; - } - /** * Scrolls all caption text containers to the bottom to show the most recent text. * Called automatically when captions are updated. diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/customization/meeting-custom-layout/meeting-custom-layout.component.html b/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/customization/meeting-custom-layout/meeting-custom-layout.component.html index 362b125f..65eb2bbb 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/customization/meeting-custom-layout/meeting-custom-layout.component.html +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/customization/meeting-custom-layout/meeting-custom-layout.component.html @@ -1,4 +1,4 @@ -@if (meetingContextService.lkRoom()) { +@if (lkRoom()) {
{ + const hasNoRemotes = this.remoteParticipants().length === 0; + return this.meetingContextService.canModerateRoom() && hasNoRemotes; + }); + linkOverlayConfig = { title: 'Start collaborating', subtitle: 'Share this link to bring others into the meeting', titleSize: 'xl' as const, titleWeight: 'bold' as const }; - protected readonly meetingUrl = computed(() => this.meetingContextService.meetingUrl()); - protected readonly remoteParticipants = computed(() => this.meetingContextService.remoteParticipants()); - protected readonly shouldShowLinkOverlay = computed(() => { - const hasNoRemotes = this.meetingContextService.remoteParticipants().length === 0; - return this.meetingContextService.canModerateRoom() && hasNoRemotes; - }); - - protected readonly areCaptionsEnabledByUser = computed(() => this.captionsService.areCaptionsEnabledByUser()); - - protected readonly captions = computed(() => this.captionsService.captions()); - - protected readonly isLayoutSwitchingAllowed = this.meetingContextService.allowLayoutSwitching; - - private displayedParticipantIds: string[] = []; - private audioElements = new Map(); - private proxyCache = new WeakMap(); + areCaptionsEnabledByUser = this.captionsService.areCaptionsEnabledByUser; + isLayoutSwitchingAllowed = this.meetingContextService.allowLayoutSwitching; + isSmartMosaicActive = computed(() => this.isLayoutSwitchingAllowed() && this.layoutService.isSmartMosaicEnabled()); + captions = this.captionsService.captions; + remoteParticipants = this.meetingContextService.remoteParticipants; private _visibleRemoteParticipants = signal([]); - readonly visibleRemoteParticipants = this._visibleRemoteParticipants.asReadonly(); + visibleRemoteParticipants = this._visibleRemoteParticipants.asReadonly(); - protected readonly hiddenParticipantsCount = computed(() => { + hiddenParticipantsCount = computed(() => { const total = this.remoteParticipants().length; const visible = this.visibleRemoteParticipants().length; return Math.max(0, total - visible); }); - - protected readonly hiddenParticipantNames = computed(() => { + hiddenParticipantNames = computed(() => { const visibleIds = new Set(this.visibleRemoteParticipants().map((p) => p.identity)); return this.remoteParticipants() .filter((p) => !visibleIds.has(p.identity)) @@ -80,7 +75,7 @@ export class MeetingCustomLayoutComponent { * Indicates whether to show the hidden participants indicator in the top bar * when in smart mosaic mode. */ - protected readonly showTopBarHiddenParticipantsIndicator = computed(() => { + showTopBarHiddenParticipantsIndicator = computed(() => { const localParticipant = this.meetingContextService.localParticipant()!; const hasPinnedParticipant = localParticipant.isPinned || this.remoteParticipants().some((p) => (p as CustomParticipantModel).isPinned); @@ -90,6 +85,10 @@ export class MeetingCustomLayoutComponent { return showTopBar; }); + private displayedParticipantIds: string[] = []; + private audioElements = new Map(); + private proxyCache = new WeakMap(); + constructor() { this.setupSpeakerTrackingEffect(); this.setupParticipantCleanupEffect(); @@ -106,17 +105,13 @@ export class MeetingCustomLayoutComponent { this.meetingService.copyMeetingSpeakerLink(room); } - protected isSmartMosaicActive(): boolean { - return this.isLayoutSwitchingAllowed() && this.layoutService.isSmartMosaicEnabled(); - } - protected toggleParticipantsPanel(): void { this.panelService.togglePanel(PanelType.PARTICIPANTS); } private setupVisibleParticipantsUpdate(): void { effect(() => { - const allRemotes = this.meetingContextService.remoteParticipants(); + const allRemotes = this.remoteParticipants(); if (!this.isSmartMosaicActive()) { this._visibleRemoteParticipants.set(allRemotes); @@ -164,7 +159,6 @@ export class MeetingCustomLayoutComponent { * @param targetIds Set of participant IDs that should be displayed. * @param availableIds Set of participant IDs that are currently available for display. */ - private syncDisplayedParticipantsWithTarget(targetIds: Set, availableIds: Set): void { this.displayedParticipantIds = this.displayedParticipantIds.filter((id) => availableIds.has(id)); @@ -193,7 +187,7 @@ export class MeetingCustomLayoutComponent { private setupSpeakerTrackingEffect(): void { effect(() => { - const room = this.meetingContextService.lkRoom(); + const room = this.lkRoom(); if (this.isLayoutSwitchingAllowed() && room) { this.layoutService.initializeSpeakerTracking(room); } diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/customization/meeting-invite-panel/meeting-invite-panel.component.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/customization/meeting-invite-panel/meeting-invite-panel.component.ts index 858871f3..bebbe888 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/customization/meeting-invite-panel/meeting-invite-panel.component.ts +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/customization/meeting-invite-panel/meeting-invite-panel.component.ts @@ -1,5 +1,5 @@ import { CommonModule } from '@angular/common'; -import { Component, computed, inject } from '@angular/core'; +import { Component, inject } from '@angular/core'; import { LoggerService } from 'openvidu-components-angular'; import { ShareMeetingLinkComponent } from '../../components/share-meeting-link/share-meeting-link.component'; import { MeetingContextService } from '../../services/meeting-context.service'; @@ -21,19 +21,8 @@ export class MeetingInvitePanelComponent { protected loggerService = inject(LoggerService); protected log = this.loggerService.get('OpenVidu Meet - MeetingInvitePanel'); - /** - * Computed signal to determine if the share link should be shown - */ - protected showShareLink = computed(() => { - return this.meetingContextService.canModerateRoom(); - }); - - /** - * Computed signal for the meeting URL from context - */ - protected meetingUrl = computed(() => { - return this.meetingContextService.meetingUrl(); - }); + showShareLink = this.meetingContextService.canModerateRoom; + meetingUrl = this.meetingContextService.meetingUrl; onCopyClicked(): void { const room = this.meetingContextService.meetRoom(); diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/customization/meeting-participant-item/meeting-participant-item.component.html b/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/customization/meeting-participant-item/meeting-participant-item.component.html index 948f449b..6a315e6d 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/customization/meeting-participant-item/meeting-participant-item.component.html +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/customization/meeting-participant-item/meeting-participant-item.component.html @@ -64,4 +64,3 @@
- diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/customization/meeting-participant-item/meeting-participant-item.component.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/customization/meeting-participant-item/meeting-participant-item.component.ts index b6a9741f..d0ea28b9 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/customization/meeting-participant-item/meeting-participant-item.component.ts +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/customization/meeting-participant-item/meeting-participant-item.component.ts @@ -22,14 +22,14 @@ export class MeetingParticipantItemComponent { // Template reference for the component's template @ViewChild('template', { static: true }) template!: TemplateRef; - protected meetingService: MeetingService = inject(MeetingService); + protected meetingService = inject(MeetingService); protected loggerService = inject(LoggerService); protected log = this.loggerService.get('OpenVidu Meet - MeetingParticipantItem'); /** * Get or compute display properties for a participant */ - protected getDisplayProperties( + getDisplayProperties( participant: CustomParticipantModel, localParticipant: CustomParticipantModel ): ParticipantDisplayProperties { @@ -56,7 +56,6 @@ export class MeetingParticipantItemComponent { if (!localParticipant.isModerator()) return; const roomId = localParticipant.roomName; - if (!roomId) { this.log.e('Cannot change participant role: local participant room name is undefined'); return; @@ -81,7 +80,6 @@ export class MeetingParticipantItemComponent { if (!localParticipant.isModerator()) return; const roomId = localParticipant.roomName; - if (!roomId) { this.log.e('Cannot change participant role: local participant room name is undefined'); return; @@ -106,9 +104,8 @@ export class MeetingParticipantItemComponent { if (!localParticipant.isModerator()) return; const roomId = localParticipant.roomName; - if (!roomId) { - this.log.e('Cannot change participant role: local participant room name is undefined'); + this.log.e('Cannot kick participant: local participant room name is undefined'); return; } diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/customization/meeting-settings-extensions/meeting-settings-extensions.component.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/customization/meeting-settings-extensions/meeting-settings-extensions.component.ts index 41dc8717..979eaaf7 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/customization/meeting-settings-extensions/meeting-settings-extensions.component.ts +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/customization/meeting-settings-extensions/meeting-settings-extensions.component.ts @@ -1,5 +1,5 @@ import { CommonModule } from '@angular/common'; -import { Component, computed, inject } from '@angular/core'; +import { Component, inject } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatIconModule } from '@angular/material/icon'; @@ -30,43 +30,24 @@ import { MeetingLayoutService } from '../../services/meeting-layout.service'; styleUrl: './meeting-settings-extensions.component.scss' }) export class MeetingSettingsExtensionsComponent { - private readonly layoutService = inject(MeetingLayoutService); protected readonly meetingContextService = inject(MeetingContextService); + private readonly layoutService = inject(MeetingLayoutService); - /** - * Expose LayoutMode enum to template - */ - readonly LayoutMode = MeetLayoutMode; + /** Whether the layout switching feature is allowed */ + isLayoutSwitchingAllowed = this.meetingContextService.allowLayoutSwitching; + /** Expose LayoutMode enum to template */ + LayoutMode = MeetLayoutMode; + /** Current layout mode */ + layoutMode = this.layoutService.layoutMode; + /** Whether Smart Mosaic layout is enabled */ + isSmartMode = this.layoutService.isSmartMosaicEnabled; - /** - * Whether the layout switching feature is allowed - */ - protected readonly isLayoutSwitchingAllowed = this.meetingContextService.allowLayoutSwitching; - - /** - * Current layout mode - */ - protected readonly layoutMode = computed(() => this.layoutService.layoutMode()); - - /** - * Current participant count - */ - protected readonly participantCount = computed(() => this.layoutService.maxRemoteSpeakers()); - - /** - * Minimum number of participants that can be shown when Smart Mosaic layout is enabled - */ - protected readonly minParticipants = this.layoutService.MIN_REMOTE_SPEAKERS; - - /** - * Maximum number of participants that can be shown - */ - protected readonly maxParticipants = this.layoutService.MAX_REMOTE_SPEAKERS_LIMIT; - - /** - * Computed property to check if Smart Mosaic mode is active - */ - readonly isSmartMode = this.layoutService.isSmartMosaicEnabled; + /** Minimum number of participants that can be shown when Smart Mosaic layout is enabled */ + minParticipants = this.layoutService.MIN_REMOTE_SPEAKERS; + /** Maximum number of participants that can be shown */ + maxParticipants = this.layoutService.MAX_REMOTE_SPEAKERS_LIMIT; + /** Current participant count */ + participantCount = this.layoutService.maxRemoteSpeakers; /** * Handler for layout mode change diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/customization/meeting-toolbar-extra-buttons/meeting-toolbar-extra-buttons.component.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/customization/meeting-toolbar-extra-buttons/meeting-toolbar-extra-buttons.component.ts index bcaf47be..980f5aa3 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/customization/meeting-toolbar-extra-buttons/meeting-toolbar-extra-buttons.component.ts +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/customization/meeting-toolbar-extra-buttons/meeting-toolbar-extra-buttons.component.ts @@ -22,38 +22,26 @@ import { MeetingService } from '../../services/meeting.service'; export class MeetingToolbarExtraButtonsComponent { protected meetingContextService = inject(MeetingContextService); protected meetingService = inject(MeetingService); - protected loggerService = inject(LoggerService); protected captionService = inject(MeetingCaptionsService); + 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(() => this.meetingContextService.canModerateRoom()); + /** Whether to show the copy link button (only for moderators) */ + showCopyLinkButton = this.meetingContextService.canModerateRoom; + copyLinkTooltip = 'Copy the meeting link'; + copyLinkText = 'Copy meeting link'; - /** - * Captions status based on room and global configuration - */ - protected captionsStatus = computed(() => this.meetingContextService.getCaptionsStatus()); + /** Captions status based on room and global configuration */ + captionsStatus = this.meetingContextService.getCaptionsStatus; + /** Whether to show the captions button (visible when not HIDDEN) */ + showCaptionsButton = computed(() => this.captionsStatus() !== 'HIDDEN'); + /** Whether captions button is disabled (true when DISABLED_WITH_WARNING) */ + isCaptionsButtonDisabled = computed(() => this.captionsStatus() === 'DISABLED_WITH_WARNING'); + /** Whether captions are currently enabled by the user */ + areCaptionsEnabledByUser = this.captionService.areCaptionsEnabledByUser; - /** - * Whether to show the captions button (visible when not HIDDEN) - */ - protected showCaptionsButton = computed(() => this.captionsStatus() !== 'HIDDEN'); - - /** - * Whether captions button is disabled (true when DISABLED_WITH_WARNING) - */ - protected isCaptionsButtonDisabled = computed(() => this.captionsStatus() === 'DISABLED_WITH_WARNING'); - - /** - * Whether the device is mobile (affects button style) - */ - protected isMobile = computed(() => this.meetingContextService.isMobile()); - - protected areCaptionsEnabledByUser = computed(() => this.captionService.areCaptionsEnabledByUser()); + /** Whether the device is mobile (affects button style) */ + isMobile = this.meetingContextService.isMobile; onCopyLinkClick(): void { const room = this.meetingContextService.meetRoom(); @@ -71,6 +59,6 @@ export class MeetingToolbarExtraButtonsComponent { this.log.w('Captions are disabled at system level (MEET_CAPTIONS_ENABLED=false)'); return; } - this.captionService.areCaptionsEnabledByUser() ? this.captionService.disable() : this.captionService.enable(); + this.areCaptionsEnabledByUser() ? this.captionService.disable() : this.captionService.enable(); } } diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/customization/meeting-toolbar-leave-button/meeting-toolbar-leave-button.component.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/customization/meeting-toolbar-leave-button/meeting-toolbar-leave-button.component.ts index 8b0695aa..f4d33d0b 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/customization/meeting-toolbar-leave-button/meeting-toolbar-leave-button.component.ts +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/customization/meeting-toolbar-leave-button/meeting-toolbar-leave-button.component.ts @@ -1,5 +1,5 @@ import { CommonModule } from '@angular/common'; -import { Component, computed, inject } from '@angular/core'; +import { Component, inject } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { MatDividerModule } from '@angular/material/divider'; import { MatIconModule } from '@angular/material/icon'; @@ -21,26 +21,16 @@ import { MeetingService } from '../../services/meeting.service'; export class MeetingToolbarLeaveButtonComponent { protected meetingContextService = inject(MeetingContextService); protected meetingService = inject(MeetingService); + protected openviduService = inject(OpenViduService); protected loggerService = inject(LoggerService); protected log = this.loggerService.get('OpenVidu Meet - MeetingToolbarLeaveButtons'); - protected openviduService = inject(OpenViduService); - protected readonly leaveMenuTooltip = 'Leave options'; - protected readonly leaveOptionText = 'Leave meeting'; - protected readonly endMeetingOptionText = 'End meeting for all'; - /** - * Whether to show the leave menu with options - */ - protected showLeaveMenu = computed(() => { - return this.meetingContextService.canModerateRoom(); - }); + showLeaveMenu = this.meetingContextService.canModerateRoom; + isMobile = this.meetingContextService.isMobile; - /** - * Whether the device is mobile (affects button style) - */ - protected isMobile = computed(() => { - return this.meetingContextService.isMobile(); - }); + leaveMenuTooltip = 'Leave options'; + leaveOptionText = 'Leave meeting'; + endMeetingOptionText = 'End meeting for all'; async onLeaveMeetingClick(): Promise { await this.openviduService.disconnectRoom(); diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/customization/meeting-toolbar-more-options-menu/meeting-toolbar-more-options-menu.component.spec.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/customization/meeting-toolbar-more-options-menu/meeting-toolbar-more-options-menu.component.spec.ts deleted file mode 100644 index 383d71a4..00000000 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/customization/meeting-toolbar-more-options-menu/meeting-toolbar-more-options-menu.component.spec.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { MeetingToolbarMoreOptionsMenuComponent } from './meeting-toolbar-more-options-menu.component'; - -describe('MeetingToolbarMoreOptionsButtonsComponent', () => { - let component: MeetingToolbarMoreOptionsMenuComponent; - let fixture: ComponentFixture; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [MeetingToolbarMoreOptionsMenuComponent] - }) - .compileComponents(); - - fixture = TestBed.createComponent(MeetingToolbarMoreOptionsMenuComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/customization/meeting-toolbar-more-options-menu/meeting-toolbar-more-options-menu.component.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/customization/meeting-toolbar-more-options-menu/meeting-toolbar-more-options-menu.component.ts index 07df3f47..f6e3ea13 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/customization/meeting-toolbar-more-options-menu/meeting-toolbar-more-options-menu.component.ts +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/customization/meeting-toolbar-more-options-menu/meeting-toolbar-more-options-menu.component.ts @@ -1,5 +1,5 @@ import { CommonModule } from '@angular/common'; -import { Component, computed, inject } from '@angular/core'; +import { Component, inject } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; import { MatMenuModule } from '@angular/material/menu'; @@ -12,52 +12,23 @@ import { MeetingContextService } from '../../services/meeting-context.service'; * This component handles custom actions like opening the settings panel for grid layout changes. */ @Component({ - selector: 'ov-meeting-toolbar-more-options-menu', - imports: [ - CommonModule, - MatIconModule, - MatButtonModule, - MatMenuModule, - MatTooltipModule - ], - templateUrl: './meeting-toolbar-more-options-menu.component.html', - styleUrl: './meeting-toolbar-more-options-menu.component.scss' + selector: 'ov-meeting-toolbar-more-options-menu', + imports: [CommonModule, MatIconModule, MatButtonModule, MatMenuModule, MatTooltipModule], + templateUrl: './meeting-toolbar-more-options-menu.component.html', + styleUrl: './meeting-toolbar-more-options-menu.component.scss' }) export class MeetingToolbarMoreOptionsMenuComponent { - /** - * Viewport service for responsive behavior detection - * Injected from openvidu-components-angular - */ - private viewportService = inject(ViewportService); + private meetingContextService = inject(MeetingContextService); + private viewportService = inject(ViewportService); + private panelService = inject(PanelService); - /** - * Panel service for opening/closing panels - * Injected from openvidu-components-angular - */ - private panelService = inject(PanelService); + isMobileView = this.viewportService.isMobile; + isLayoutSwitchingAllowed = this.meetingContextService.allowLayoutSwitching; - /** - * Meeting context service for feature flags - */ - private meetingContextService = inject(MeetingContextService); - - /** - * Computed properties for responsive button behavior - * These follow the same pattern as toolbar-media-buttons component - */ - readonly isMobileView = computed(() => this.viewportService.isMobile()); - readonly isTabletView = computed(() => this.viewportService.isTablet()); - readonly isDesktopView = computed(() => this.viewportService.isDesktop()); - - /** - * Whether the layout switching feature is allowed - */ - readonly isLayoutSwitchingAllowed = computed(() => this.meetingContextService.allowLayoutSwitching()); - - /** - * Opens the settings panel to allow users to change grid layout - */ - onOpenSettings(): void { - this.panelService.togglePanel(PanelType.SETTINGS); - } + /** + * Opens the settings panel to allow users to change grid layout + */ + onOpenSettings(): void { + this.panelService.togglePanel(PanelType.SETTINGS); + } } diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/pages/meeting/meeting.component.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/pages/meeting/meeting.component.ts index 56303dd6..171ba8cf 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/pages/meeting/meeting.component.ts +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/pages/meeting/meeting.component.ts @@ -1,23 +1,15 @@ import { CommonModule } from '@angular/common'; -import { Component, computed, ContentChild, effect, inject, OnInit, signal, Signal } from '@angular/core'; +import { Component, computed, ContentChild, effect, inject, OnInit, signal } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { MatIconModule } from '@angular/material/icon'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; -import { - OpenViduComponentsUiModule, - OpenViduThemeMode, - OpenViduThemeService, - Room, - Track -} from 'openvidu-components-angular'; +import { OpenViduComponentsUiModule, OpenViduThemeMode, OpenViduThemeService, Room } from 'openvidu-components-angular'; import { Subject } from 'rxjs'; -import { RoomFeatures } from '../../../../shared/models/app.model'; import { GlobalConfigService } from '../../../../shared/services/global-config.service'; import { NotificationService } from '../../../../shared/services/notification.service'; import { RoomFeatureService } from '../../../../shared/services/room-feature.service'; import { RuntimeConfigService } from '../../../../shared/services/runtime-config.service'; import { SoundService } from '../../../../shared/services/sound.service'; -import { RoomMemberContextService } from '../../../room-members/services/room-member-context.service'; import { MeetingLobbyComponent } from '../../components/meeting-lobby/meeting-lobby.component'; import { MeetingParticipantItemComponent } from '../../customization/meeting-participant-item/meeting-participant-item.component'; import { MeetingCaptionsService } from '../../services/meeting-captions.service'; @@ -41,7 +33,16 @@ import { MeetingLobbyService } from '../../services/meeting-lobby.service'; providers: [MeetingLobbyService, MeetingEventHandlerService, SoundService] }) export class MeetingComponent implements OnInit { - protected _participantItem?: MeetingParticipantItemComponent; + protected meetingContextService = inject(MeetingContextService); + protected lobbyService = inject(MeetingLobbyService); + protected eventHandlerService = inject(MeetingEventHandlerService); + protected captionsService = inject(MeetingCaptionsService); + protected configService = inject(GlobalConfigService); + protected roomFeatureService = inject(RoomFeatureService); + protected ovThemeService = inject(OpenViduThemeService); + protected notificationService = inject(NotificationService); + protected soundService = inject(SoundService); + protected runtimeConfigService = inject(RuntimeConfigService); // Template reference for custom participant panel item @ContentChild(MeetingParticipantItemComponent) @@ -49,55 +50,28 @@ export class MeetingComponent implements OnInit { // Store the reference to the custom participant panel item component this._participantItem = value; } + protected _participantItem?: MeetingParticipantItemComponent; protected participantItemTemplate = computed(() => this._participantItem?.template); - /** - * Controls whether to show lobby (true) or meeting view (false) - */ + /** Controls whether to show lobby (true) or meeting view (false) */ showLobby = true; isLobbyReady = false; - /** - * Controls whether to show the videoconference component - */ - protected isMeetingLeft = signal(false); + /** Controls whether to show the videoconference component */ + isMeetingLeft = signal(false); + + // Signals for meeting context data + roomName = this.lobbyService.roomName; + roomMemberToken = this.lobbyService.roomMemberToken; + e2eeKey = this.lobbyService.e2eeKeyValue; + localParticipant = this.meetingContextService.localParticipant; + + features = this.roomFeatureService.features; + hasRecordings = this.meetingContextService.hasRecordings; - protected features: Signal; - protected roomMemberContextService = inject(RoomMemberContextService); - protected roomFeatureService = inject(RoomFeatureService); - protected ovThemeService = inject(OpenViduThemeService); - protected configService = inject(GlobalConfigService); - protected notificationService = inject(NotificationService); - protected lobbyService = inject(MeetingLobbyService); - protected meetingContextService = inject(MeetingContextService); - protected eventHandlerService = inject(MeetingEventHandlerService); - protected captionsService = inject(MeetingCaptionsService); - protected soundService = inject(SoundService); - protected runtimeConfigService = inject(RuntimeConfigService); protected destroy$ = new Subject(); - // === LOBBY PHASE COMPUTED SIGNALS (when showLobby = true) === - protected participantName = computed(() => this.lobbyService.participantName()); - protected e2eeKey = computed(() => this.lobbyService.e2eeKeyValue()); - protected roomName = computed(() => this.lobbyService.roomName()); - protected roomMemberToken = computed(() => this.lobbyService.roomMemberToken()); - - // === MEETING PHASE COMPUTED SIGNALS (when showLobby = false) === - // These read from MeetingContextService (Single Source of Truth during meeting) - protected localParticipant = computed(() => this.meetingContextService.localParticipant()); - protected remoteParticipants = computed(() => this.meetingContextService.remoteParticipants()); - protected hasRemoteParticipants = computed(() => this.remoteParticipants().length > 0); - protected participantsVersion = computed(() => this.meetingContextService.participantsVersion()); - - // === SHARED COMPUTED SIGNALS (used in both phases) === - // Both lobby and meeting need these, so we read from MeetingContextService (Single Source of Truth) - protected roomId = computed(() => this.meetingContextService.roomId()); - protected roomSecret = computed(() => this.meetingContextService.roomSecret()); - protected hasRecordings = computed(() => this.meetingContextService.hasRecordings()); - constructor() { - this.features = this.roomFeatureService.features; - // Change theme variables when custom theme is enabled effect(() => { if (this.features().hasCustomTheme) { @@ -156,28 +130,8 @@ export class MeetingComponent implements OnInit { this.captionsService.destroy(); } - // async onRoomConnected() { - // try { - // // Suscribirse solo para actualizar el estado de video pin - // // Los participantes se actualizan automáticamente en MeetingContextService - // combineLatest([ - // this.ovComponentsParticipantService.remoteParticipants$, - // this.ovComponentsParticipantService.localParticipant$ - // ]) - // .pipe(takeUntil(this.destroy$)) - // .subscribe(() => { - // this.updateVideoPinState(); - // }); - // } catch (error) { - // console.error('Error accessing meeting:', error); - // } - // } - onRoomCreated(lkRoom: Room) { // At this point, user has joined the meeting and MeetingContextService becomes the Single Source of Truth - // MeetingContextService has been updated during lobby initialization with roomId, roomSecret, hasRecordings - // All subsequent updates (hasRecordings, roomSecret, participants) go to MeetingContextService - // Store LiveKit room in context this.meetingContextService.setLkRoom(lkRoom); @@ -193,29 +147,15 @@ export class MeetingComponent implements OnInit { this.eventHandlerService.setupRoomListeners(lkRoom); } - // async leaveMeeting() { - // await this.openviduService.disconnectRoom(); - // } - - // async endMeeting() { - // if (!this.participantService.isModerator()) return; - - // this.meetingContextService.setMeetingEndedBy('self'); - - // try { - // await this.meetingService.endMeeting(this.roomId()!); - // } catch (error) { - // console.error('Error ending meeting:', error); - // } - // } - async onViewRecordingsClicked() { const basePath = this.runtimeConfigService.basePath; const basePathForUrl = basePath.endsWith('/') ? basePath.slice(0, -1) : basePath; - let recordingsUrl = `${basePathForUrl}/room/${this.roomId()}/recordings`; + + const roomId = this.meetingContextService.roomId(); + let recordingsUrl = `${basePathForUrl}/room/${roomId}/recordings`; // Append room secret as query param if it exists - const secret = this.roomSecret(); + const secret = this.meetingContextService.roomSecret(); if (secret) { recordingsUrl += `?secret=${secret}`; } @@ -232,27 +172,8 @@ export class MeetingComponent implements OnInit { /** * Handles the participant left event and hides the videoconference component */ - protected onParticipantLeft(event: any): void { + onParticipantLeft(event: any): void { this.isMeetingLeft.set(true); this.eventHandlerService.onParticipantLeft(event); } - - /** - * Centralized logic for managing video pinning based on - * remote participants and local screen sharing state. - */ - protected updateVideoPinState(): void { - const localParticipant = this.localParticipant(); - if (!localParticipant) return; - - const isSharing = localParticipant.isScreenShareEnabled; - - if (this.hasRemoteParticipants() && isSharing) { - // Pin the local screen share to appear bigger - localParticipant.setVideoPinnedBySource(Track.Source.ScreenShare, true); - } else { - // Unpin everything if no remote participants or not sharing - localParticipant.setAllVideoPinned(false); - } - } } diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/services/meeting-captions.service.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/services/meeting-captions.service.ts index 5094763b..b104041a 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/services/meeting-captions.service.ts +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/services/meeting-captions.service.ts @@ -18,9 +18,9 @@ import { CustomParticipantModel } from '../models/custom-participant.model'; providedIn: 'root' }) export class MeetingCaptionsService { + private readonly participantService = inject(ParticipantService); private readonly loggerService = inject(LoggerService); private readonly logger: ILogger; - private readonly participantService = inject(ParticipantService); // Configuration with defaults private readonly defaultConfig: Required = { diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/services/meeting-context.service.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/services/meeting-context.service.ts index 5f9e3c83..d671f913 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/services/meeting-context.service.ts +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/services/meeting-context.service.ts @@ -28,7 +28,6 @@ export class MeetingContextService { private readonly _hasRecordings = signal(false); private readonly _meetingEndedBy = signal<'self' | 'other' | null>(null); private readonly _lkRoom = signal(undefined); - private readonly _participantsVersion = signal(0); private readonly _localParticipant = signal(undefined); private readonly _remoteParticipants = signal([]); @@ -53,11 +52,6 @@ export class MeetingContextService { /** Readonly signal for the current LiveKit room */ readonly lkRoom = this._lkRoom.asReadonly(); - /** - * Readonly signal for participants version (increments on role changes) - * Used to trigger reactivity when participant properties change without array reference changes - */ - readonly participantsVersion = this._participantsVersion.asReadonly(); /** Readonly signal for the local participant */ readonly localParticipant = this._localParticipant.asReadonly(); /** Readonly signal for the remote participants */ @@ -172,14 +166,6 @@ export class MeetingContextService { this._lkRoom.set(room); } - /** - * Increments the participants version counter - * Used to trigger reactivity when participant properties (like role) change - */ - incrementParticipantsVersion(): void { - this._participantsVersion.update((v) => v + 1); - } - /** * Synchronizes participants from OpenVidu Components ParticipantService using signals. * Effects are automatically cleaned up when the service is destroyed. @@ -211,7 +197,6 @@ export class MeetingContextService { this._roomSecret.set(undefined); this._hasRecordings.set(false); this._meetingEndedBy.set(null); - this._participantsVersion.set(0); this._localParticipant.set(undefined); this._remoteParticipants.set([]); } diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/services/meeting-event-handler.service.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/services/meeting-event-handler.service.ts index 6a970f7a..4086318a 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/services/meeting-event-handler.service.ts +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/services/meeting-event-handler.service.ts @@ -59,23 +59,11 @@ export class MeetingEventHandlerService { * @param room The LiveKit Room instance */ setupRoomListeners(room: Room): void { - this.setupDataReceivedListener(room); - } - - /** - * Sets up the DataReceived event listener for handling room signals - * @param room The LiveKit Room instance - */ - private setupDataReceivedListener(room: Room): void { room.on( RoomEvent.DataReceived, async (payload: Uint8Array, _participant?: RemoteParticipant, _kind?: DataPacket_Kind, topic?: string) => { // Only process topics that this handler is responsible for - const relevantTopics = [ - 'recordingStopped', - MeetSignalType.MEET_ROOM_CONFIG_UPDATED, - MeetSignalType.MEET_PARTICIPANT_ROLE_UPDATED - ]; + const relevantTopics = ['recordingStopped', MeetSignalType.MEET_PARTICIPANT_ROLE_UPDATED]; if (!topic || !relevantTopics.includes(topic)) { return; @@ -90,14 +78,10 @@ export class MeetingEventHandlerService { this.meetingContext.setHasRecordings(true); break; - case MeetSignalType.MEET_ROOM_CONFIG_UPDATED: - // Room cannot be updated if a meeting is ongoing - // await this.handleRoomConfigUpdated(event); - break; - case MeetSignalType.MEET_PARTICIPANT_ROLE_UPDATED: - await this.handleParticipantRoleUpdated(event); - this.showParticipantRoleUpdatedNotification(event); + const roleUpdateEvent = event as MeetParticipantRoleUpdatedPayload; + await this.handleParticipantRoleUpdated(roleUpdateEvent); + this.showParticipantRoleUpdatedNotification(roleUpdateEvent.newRole); break; } } catch (error) { @@ -111,8 +95,6 @@ export class MeetingEventHandlerService { * Handles participant connected event. * Sends JOINED event to parent window (for web component integration). * - * Arrow function ensures correct 'this' binding when called from template. - * * @param event Participant model from OpenVidu */ onParticipantConnected = (event: ParticipantModel): void => { @@ -130,19 +112,17 @@ export class MeetingEventHandlerService { * Handles participant left event. * - Maps technical reason to user-friendly reason * - Sends LEFT event to parent window - * - Cleans up session storage (secrets, tokens) + * - Clears participant identity and token from RoomMemberContextService * - Navigates to disconnected page * - * Arrow function ensures correct 'this' binding when called from template. - * * @param event Participant left event from OpenVidu */ onParticipantLeft = async (event: ParticipantLeftEvent): Promise => { let leftReason = this.mapLeftReason(event.reason); - // If meeting was ended by this user, update reason - const meetingEndedBy = this.meetingContext.meetingEndedBy(); - if (leftReason === LeftEventReason.MEETING_ENDED && meetingEndedBy === 'self') { + // If meeting was ended by local user, update reason + const meetingEndedBySelf = this.meetingContext.meetingEndedBy() === 'self'; + if (leftReason === LeftEventReason.MEETING_ENDED && meetingEndedBySelf) { leftReason = LeftEventReason.MEETING_ENDED_BY_SELF; } @@ -167,8 +147,6 @@ export class MeetingEventHandlerService { /** * Handles recording start request event. * - * Arrow function ensures correct 'this' binding when called from template. - * * @param event Recording start requested event from OpenVidu */ onRecordingStartRequested = async (event: RecordingStartRequestedEvent): Promise => { @@ -189,8 +167,6 @@ export class MeetingEventHandlerService { /** * Handles recording stop request event. * - * Arrow function ensures correct 'this' binding when called from template. - * * @param event Recording stop requested event from OpenVidu */ onRecordingStopRequested = async (event: RecordingStopRequestedEvent): Promise => { @@ -205,42 +181,6 @@ export class MeetingEventHandlerService { // PRIVATE METHODS - Event Handlers // ============================================ - /** - * Handles room config updated event. - * Updates feature config and refreshes room member token if needed. - * Obtains roomId and roomSecret from MeetingContextService. - */ - // private async handleRoomConfigUpdated(event: MeetRoomConfigUpdatedPayload): Promise { - // const { config } = event; - - // // Update feature configuration - // this.featureConfService.setRoomConfig(config); - - // // Refresh room member token if recording is enabled - // if (config.recording.enabled) { - // try { - // const roomId = this.meetingContext.roomId(); - // const roomSecret = this.meetingContext.roomSecret(); - // const participantName = this.roomMemberService.getParticipantName(); - // const participantIdentity = this.roomMemberService.getParticipantIdentity(); - - // if (!roomId || !roomSecret) { - // console.error('Room ID or secret not available for token refresh'); - // return; - // } - - // await this.roomMemberService.generateToken(roomId, { - // secret: roomSecret, - // grantJoinMeetingPermission: true, - // participantName, - // participantIdentity - // }); - // } catch (error) { - // console.error('Error refreshing room member token:', error); - // } - // } - // } - /** * Handles participant role updated event. * Updates local or remote participant role and refreshes room member token if needed. @@ -256,9 +196,8 @@ export class MeetingEventHandlerService { if (local && participantIdentity === local.identity) { if (!secret || !roomId) return; - // Update room secret in context + // Update room secret in context (without updating session storage) this.meetingContext.setRoomSecret(secret); - this.sessionStorageService.setRoomSecret(secret); try { // Refresh participant token with new role @@ -272,9 +211,6 @@ export class MeetingEventHandlerService { // Update local participant role local.meetRole = newRole; console.log(`You have been assigned the role of ${newRole}`); - - // Increment version to trigger reactivity - this.meetingContext.incrementParticipantsVersion(); } catch (error) { console.error('Error refreshing room member token:', error); } @@ -284,15 +220,11 @@ export class MeetingEventHandlerService { const participant = remoteParticipants.find((p) => p.identity === participantIdentity); if (participant) { participant.meetRole = newRole; - - // Increment version to trigger reactivity - this.meetingContext.incrementParticipantsVersion(); } } } - private showParticipantRoleUpdatedNotification(event: MeetParticipantRoleUpdatedPayload): void { - const { newRole } = event as MeetParticipantRoleUpdatedPayload; + private showParticipantRoleUpdatedNotification(newRole: MeetRoomMemberRole): void { this.notificationService.showSnackbar(`You have been assigned the role of ${newRole.toUpperCase()}`); newRole === MeetRoomMemberRole.MODERATOR ? this.soundService.playParticipantRoleUpgradedSound() diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/services/meeting-webcomponent-manager.service.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/services/meeting-webcomponent-manager.service.ts index 0d3fa3f7..242c73fc 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/services/meeting-webcomponent-manager.service.ts +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/services/meeting-webcomponent-manager.service.ts @@ -20,20 +20,19 @@ import { MeetingService } from './meeting.service'; providedIn: 'root' }) export class MeetingWebComponentManagerService { + protected meetingService = inject(MeetingService); + protected meetingContextService = inject(MeetingContextService); + protected roomMemberContextService = inject(RoomMemberContextService); + protected appCtxService = inject(AppContextService); + protected openviduService = inject(OpenViduService); + protected loggerService = inject(LoggerService); + protected log = this.loggerService.get('OpenVidu Meet - WebComponentManagerService'); + protected isInitialized = false; protected parentDomain: string = ''; protected boundHandleMessage: (event: MessageEvent) => Promise; - protected log; - protected readonly meetingContextService = inject(MeetingContextService); - protected readonly roomMemberContextService = inject(RoomMemberContextService); - protected readonly openviduService = inject(OpenViduService); - protected readonly meetingService = inject(MeetingService); - protected readonly loggerService = inject(LoggerService); - protected readonly appCtxService = inject(AppContextService); - constructor() { - this.log = this.loggerService.get('OpenVidu Meet - WebComponentManagerService'); this.boundHandleMessage = this.handleMessage.bind(this); effect(() => { if (this.appCtxService.isEmbeddedMode()) { @@ -95,6 +94,7 @@ export class MeetingWebComponentManagerService { const message: WebComponentInboundCommandMessage = event.data; const { command, payload } = message; + // If parent domain is not set, only accept INITIALIZE command to set the parent domain if (!this.parentDomain) { if (command === WebComponentCommand.INITIALIZE) { if (!payload || !('domain' in payload)) { @@ -107,6 +107,7 @@ export class MeetingWebComponentManagerService { return; } + // For security, only accept messages from the parent domain if (event.origin !== this.parentDomain) { console.warn(`Untrusted origin: ${event.origin}`); return; diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/shared/services/app-context.service.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/shared/services/app-context.service.ts index cf4cc7c3..b9dcc270 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/shared/services/app-context.service.ts +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/shared/services/app-context.service.ts @@ -13,16 +13,12 @@ export class AppContextService { private readonly _edition: WritableSignal = signal(Edition.CE); private readonly _version: WritableSignal = signal(''); - readonly mode = computed(() => this._mode()); - readonly edition = computed(() => this._edition()); - readonly version = computed(() => this._version()); + readonly mode = this._mode.asReadonly(); + readonly edition = this._edition.asReadonly(); + readonly version = this._version.asReadonly(); + readonly isEmbeddedMode = computed(() => this._mode() === ApplicationMode.EMBEDDED); readonly isStandaloneMode = computed(() => this._mode() === ApplicationMode.STANDALONE); - readonly appData = computed(() => ({ - mode: this._mode(), - edition: this._edition(), - version: this._version() - })); constructor() { this.detectMode();