diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/components/index.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/components/index.ts index 8253b4b7..31264226 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/components/index.ts +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/components/index.ts @@ -14,18 +14,11 @@ export * from './wizard-nav/wizard-nav.component'; export * from './share-meeting-link/share-meeting-link.component'; // Meeting modular components -export * from './meeting-toolbar-buttons/meeting-toolbar-buttons.component'; -export * from './meeting-participant-panel/meeting-participant-panel.component'; -export * from './meeting-share-link-panel/meeting-share-link-panel.component'; export * from './meeting-share-link-overlay/meeting-share-link-overlay.component'; export * from './meeting-lobby/meeting-lobby.component'; -export * from './meeting-layout/meeting-layout.component'; // Meeting components -export * from './meeting-toolbar-buttons/meeting-toolbar-buttons.component'; -export * from './meeting-participant-panel/meeting-participant-panel.component'; -export * from './meeting-share-link-panel/meeting-share-link-panel.component'; export * from './meeting-share-link-overlay/meeting-share-link-overlay.component'; export * from './meeting-lobby/meeting-lobby.component'; diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/components/meeting-layout/meeting-layout.component.html b/meet-ce/frontend/projects/shared-meet-components/src/lib/components/meeting-layout/meeting-layout.component.html deleted file mode 100644 index db0baf54..00000000 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/components/meeting-layout/meeting-layout.component.html +++ /dev/null @@ -1,17 +0,0 @@ - - - @if (additionalElementsInputs().showOverlay) { - - - - - } - - - - - - diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/components/meeting-lobby/meeting-lobby.component.html b/meet-ce/frontend/projects/shared-meet-components/src/lib/components/meeting-lobby/meeting-lobby.component.html index 2d5efd5e..2dcac213 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/components/meeting-lobby/meeting-lobby.component.html +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/components/meeting-lobby/meeting-lobby.component.html @@ -4,8 +4,8 @@
video_chat
-

{{ roomName }}

- @if (isE2EEEnabled) { +

{{ roomName() }}

+ @if (isE2EEEnabled()) { lock This meeting is end-to-end encrypted @@ -17,22 +17,22 @@
- + - {{ roomClosed ? 'lock' : 'meeting_room' }} + {{ roomClosed() ? 'lock' : 'meeting_room' }}
- {{ roomClosed ? 'Room Closed' : 'Join Meeting' }} + {{ roomClosed() ? 'Room Closed' : 'Join Meeting' }} {{ - roomClosed + roomClosed() ? 'This room is not available for meetings' : 'Enter the room and start connecting' }}
- - @if (!roomClosed) { -
+ + @if (!roomClosed()) { + Your display name @@ -44,13 +44,13 @@ required /> person - @if (participantForm.get('name')?.hasError('required')) { + @if (participantForm().get('name')?.hasError('required')) { The name is required } - @if (isE2EEEnabled) { + @if (isE2EEEnabled()) { Encryption Key - vpn_key - @if (participantForm.get('e2eeKey')?.hasError('required')) { - The encryption key is required - } + vpn_key + @if (participantForm().get('e2eeKey')?.hasError('required')) { + The encryption key is required + } This room requires an encryption key to join } @@ -75,7 +75,7 @@ id="participant-name-submit" type="submit" class="join-button" - [disabled]="!participantForm.valid" + [disabled]="!participantForm().valid" > Join Meeting @@ -93,7 +93,7 @@ - @if (showRecordingCard) { + @if (showRecordingCard()) { video_library @@ -126,16 +126,16 @@
- @if (!roomClosed && showShareLink) { - + @if (!roomClosed() && showShareLink()) { + } - @if (showBackButton) { + @if (showBackButton()) {
} diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/components/meeting-lobby/meeting-lobby.component.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/components/meeting-lobby/meeting-lobby.component.ts index 5b7a7a98..f4f37e37 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/components/meeting-lobby/meeting-lobby.component.ts +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/components/meeting-lobby/meeting-lobby.component.ts @@ -1,12 +1,14 @@ import { CommonModule } from '@angular/common'; -import { Component, EventEmitter, Input, Output } from '@angular/core'; -import { FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { Component, computed, inject } from '@angular/core'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatIconModule } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; import { ShareMeetingLinkComponent } from '../../components'; +import { MeetingLobbyService } from '../../services/meeting/meeting-lobby.service'; +import { MeetingService } from '../../services/meeting/meeting.service'; /** * Reusable component for the meeting lobby page. @@ -29,121 +31,36 @@ import { ShareMeetingLinkComponent } from '../../components'; ] }) export class MeetingLobbyComponent { - /** - * The room name to display - */ - @Input({ required: true }) roomName = ''; + protected lobbyService = inject(MeetingLobbyService); + protected meetingService = inject(MeetingService); - /** - * The meeting URL to share - */ - @Input() meetingUrl = ''; + protected roomName = computed(() => this.lobbyService.state().room?.roomName); + protected meetingUrl = computed(() => this.lobbyService.meetingUrl()); + protected roomClosed = computed(() => this.lobbyService.state().roomClosed); + protected showRecordingCard = computed(() => this.lobbyService.state().showRecordingCard); + protected showShareLink = computed(() => { + const state = this.lobbyService.state(); + const canModerate = this.lobbyService.canModerateRoom(); + return !!state.room && !state.roomClosed && canModerate; + }); + protected showBackButton = computed(() => this.lobbyService.state().showBackButton); + protected backButtonText = computed(() => this.lobbyService.state().backButtonText); + protected isE2EEEnabled = computed(() => this.lobbyService.state().hasRoomE2EEEnabled); + protected participantForm = computed(() => this.lobbyService.state().participantForm); - /** - * Whether the room is closed - */ - @Input() roomClosed = false; - - /** - * Whether to show the recording card - */ - @Input() showRecordingCard = false; - - /** - * Whether to show the share meeting link component - */ - @Input() showShareLink = false; - - /** - * Whether to show the back button - */ - @Input() showBackButton = false; - - /** - * Back button text - */ - @Input() backButtonText = 'Back'; - - /** - * Whether E2EE is enabled for the meeting - */ - @Input() isE2EEEnabled = false; - - /** - * The participant form group - */ - @Input({ required: true }) participantForm!: FormGroup; - - /** - * Emitted when the form is submitted - */ - @Output() formSubmitted = new EventEmitter(); - - /** - * Emitted when the view recordings button is clicked - */ - @Output() viewRecordingsClicked = new EventEmitter(); - - /** - * Emitted when the back button is clicked - */ - @Output() backClicked = new EventEmitter(); - - /** - * Emitted when the copy link button is clicked - */ - @Output() copyLinkClicked = new EventEmitter(); - - /** - * Alternative to @Output: Function to call when form is submitted - * When using NgComponentOutlet, use this instead of the @Output above - */ - @Input() formSubmittedFn?: () => void; - - /** - * Alternative to @Output: Function to call when view recordings is clicked - */ - @Input() viewRecordingsClickedFn?: () => void; - - /** - * Alternative to @Output: Function to call when back button is clicked - */ - @Input() backClickedFn?: () => void; - - /** - * Alternative to @Output: Function to call when copy link is clicked - */ - @Input() copyLinkClickedFn?: () => void; - - onFormSubmit(): void { - if (this.formSubmittedFn) { - this.formSubmittedFn(); - } else { - this.formSubmitted.emit(); - } + async onFormSubmit(): Promise { + await this.lobbyService.submitAccess(); } - onViewRecordingsClick(): void { - if (this.viewRecordingsClickedFn) { - this.viewRecordingsClickedFn(); - } else { - this.viewRecordingsClicked.emit(); - } + async onViewRecordingsClick(): Promise { + await this.lobbyService.goToRecordings(); } - onBackClick(): void { - if (this.backClickedFn) { - this.backClickedFn(); - } else { - this.backClicked.emit(); - } + async onBackClick(): Promise { + await this.lobbyService.goBack(); } onCopyLinkClick(): void { - if (this.copyLinkClickedFn) { - this.copyLinkClickedFn(); - } else { - this.copyLinkClicked.emit(); - } + this.lobbyService.copyMeetingSpeakerLink(); } } diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/components/meeting-participant-panel/meeting-participant-panel.component.html b/meet-ce/frontend/projects/shared-meet-components/src/lib/components/meeting-participant-panel/meeting-participant-panel.component.html deleted file mode 100644 index a68aaaa3..00000000 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/components/meeting-participant-panel/meeting-participant-panel.component.html +++ /dev/null @@ -1,62 +0,0 @@ -
- - - - @if (showModeratorBadge) { - - - shield_person - - - } - - - - @if (showModerationControls) { -
- - @if (showMakeModerator) { - - } - - - @if (showUnmakeModerator) { - - } - - - @if (showKickButton) { - - } -
- } -
-
diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/components/meeting-participant-panel/meeting-participant-panel.component.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/components/meeting-participant-panel/meeting-participant-panel.component.ts deleted file mode 100644 index e0bdfe36..00000000 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/components/meeting-participant-panel/meeting-participant-panel.component.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { CommonModule } from '@angular/common'; -import { Component, EventEmitter, Input, Output } from '@angular/core'; -import { MatButtonModule } from '@angular/material/button'; -import { MatIconModule } from '@angular/material/icon'; -import { MatTooltipModule } from '@angular/material/tooltip'; -import { OpenViduComponentsUiModule } from 'openvidu-components-angular'; - -/** - * Reusable component for displaying participant panel items with moderation controls. - * This component is agnostic and configurable via inputs. - */ -@Component({ - selector: 'ov-meeting-participant-panel', - templateUrl: './meeting-participant-panel.component.html', - styleUrls: ['./meeting-participant-panel.component.scss'], - imports: [CommonModule, MatButtonModule, MatIconModule, MatTooltipModule, OpenViduComponentsUiModule] -}) -export class MeetingParticipantPanelComponent { - /** - * The participant to display - */ - @Input({ required: true }) participant: any; - - /** - * All participants in the meeting (used for determining moderation controls) - */ - @Input() allParticipants: any[] = []; - - /** - * Whether to show the moderator badge - */ - @Input() showModeratorBadge = false; - - /** - * Whether to show moderation controls (make/unmake moderator, kick) - */ - @Input() showModerationControls = false; - - /** - * Whether to show the "make moderator" button - */ - @Input() showMakeModerator = false; - - /** - * Whether to show the "unmake moderator" button - */ - @Input() showUnmakeModerator = false; - - /** - * Whether to show the "kick participant" button - */ - @Input() showKickButton = false; - - /** - * Moderator badge tooltip text - */ - @Input() moderatorBadgeTooltip = 'Moderator'; - - /** - * Make moderator button tooltip text - */ - @Input() makeModeratorTooltip = 'Make participant moderator'; - - /** - * Unmake moderator button tooltip text - */ - @Input() unmakeModeratorTooltip = 'Unmake participant moderator'; - - /** - * Kick participant button tooltip text - */ - @Input() kickParticipantTooltip = 'Kick participant'; - - /** - * Emitted when the make moderator button is clicked - */ - @Output() makeModeratorClicked = new EventEmitter(); - - /** - * Emitted when the unmake moderator button is clicked - */ - @Output() unmakeModeratorClicked = new EventEmitter(); - - /** - * Emitted when the kick participant button is clicked - */ - @Output() kickParticipantClicked = new EventEmitter(); - - /** - * Alternative to @Output: Function to call when make moderator is clicked - * When using NgComponentOutlet, use this instead of the @Output above - */ - @Input() makeModeratorClickedFn?: () => void; - - /** - * Alternative to @Output: Function to call when unmake moderator is clicked - */ - @Input() unmakeModeratorClickedFn?: () => void; - - /** - * Alternative to @Output: Function to call when kick participant is clicked - */ - @Input() kickParticipantClickedFn?: () => void; - - onMakeModeratorClick(): void { - if (this.makeModeratorClickedFn) { - this.makeModeratorClickedFn(); - } else { - this.makeModeratorClicked.emit(this.participant); - } - } - - onUnmakeModeratorClick(): void { - if (this.unmakeModeratorClickedFn) { - this.unmakeModeratorClickedFn(); - } else { - this.unmakeModeratorClicked.emit(this.participant); - } - } - - onKickParticipantClick(): void { - if (this.kickParticipantClickedFn) { - this.kickParticipantClickedFn(); - } else { - this.kickParticipantClicked.emit(this.participant); - } - } -} diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/components/meeting-share-link-panel/meeting-share-link-panel.component.html b/meet-ce/frontend/projects/shared-meet-components/src/lib/components/meeting-share-link-panel/meeting-share-link-panel.component.html deleted file mode 100644 index 6835466a..00000000 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/components/meeting-share-link-panel/meeting-share-link-panel.component.html +++ /dev/null @@ -1,5 +0,0 @@ -@if (showShareLink) { - -} diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/components/meeting-share-link-panel/meeting-share-link-panel.component.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/components/meeting-share-link-panel/meeting-share-link-panel.component.ts deleted file mode 100644 index f235bb96..00000000 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/components/meeting-share-link-panel/meeting-share-link-panel.component.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { CommonModule } from '@angular/common'; -import { Component, EventEmitter, Input, Output } from '@angular/core'; -import { ShareMeetingLinkComponent } from '../share-meeting-link/share-meeting-link.component'; - -/** - * Reusable component for displaying the share meeting link panel - * 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'], - imports: [CommonModule, ShareMeetingLinkComponent] -}) -export class MeetingShareLinkPanelComponent { - /** - * Controls whether the share link panel should be shown - */ - @Input() showShareLink = true; - - /** - * The meeting URL to share - */ - @Input({ required: true }) meetingUrl = ''; - - /** - * Emitted when the copy button is clicked - */ - @Output() copyClicked = new EventEmitter(); - - /** - * Alternative to @Output: Function to call when copy button is clicked - * When using NgComponentOutlet, use this instead of the @Output above - */ - @Input() copyClickedFn?: () => void; - - onCopyClicked(): void { - if (this.copyClickedFn) { - this.copyClickedFn(); - } else { - this.copyClicked.emit(); - } - } -} diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/components/meeting-toolbar-buttons/meeting-toolbar-buttons.component.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/components/meeting-toolbar-buttons/meeting-toolbar-buttons.component.ts deleted file mode 100644 index 2faa1f2f..00000000 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/components/meeting-toolbar-buttons/meeting-toolbar-buttons.component.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { Component, EventEmitter, Input, Output } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { MatButtonModule } from '@angular/material/button'; -import { MatDividerModule } from '@angular/material/divider'; -import { MatIconModule } from '@angular/material/icon'; -import { MatMenuModule } from '@angular/material/menu'; -import { MatTooltipModule } from '@angular/material/tooltip'; - -/** - * Reusable component for meeting toolbar additional buttons. - * This component is agnostic and can be configured via inputs. - */ -@Component({ - selector: 'ov-meeting-toolbar-buttons', - templateUrl: './meeting-toolbar-buttons.component.html', - styleUrls: ['./meeting-toolbar-buttons.component.scss'], - imports: [CommonModule, MatButtonModule, MatIconModule, MatMenuModule, MatTooltipModule, MatDividerModule] -}) -export class MeetingToolbarButtonsComponent { - /** - * Whether to show the copy link button - */ - @Input() showCopyLinkButton = false; - - /** - * Whether to show the leave menu with options - */ - @Input() showLeaveMenu = false; - - /** - * Whether the device is mobile (affects button style) - */ - @Input() isMobile = false; - - /** - * Copy link button tooltip text - */ - @Input() copyLinkTooltip = 'Copy the meeting link'; - - /** - * Copy link button text (for mobile) - */ - @Input() copyLinkText = 'Copy meeting link'; - - /** - * Leave menu tooltip text - */ - @Input() leaveMenuTooltip = 'Leave options'; - - /** - * Leave option text - */ - @Input() leaveOptionText = 'Leave meeting'; - - /** - * End meeting option text - */ - @Input() endMeetingOptionText = 'End meeting for all'; - - /** - * Emitted when the copy link button is clicked - */ - @Output() copyLinkClicked = new EventEmitter(); - - /** - * Emitted when the leave meeting option is clicked - */ - @Output() leaveMeetingClicked = new EventEmitter(); - - /** - * Emitted when the end meeting option is clicked - */ - @Output() endMeetingClicked = new EventEmitter(); - - /** - * Alternative to @Output: Function to call when copy link button is clicked - * When using NgComponentOutlet, use this instead of the @Output above - */ - @Input() copyLinkClickedFn?: () => void; - - /** - * Alternative to @Output: Function to call when leave meeting is clicked - * When using NgComponentOutlet, use this instead of the @Output above - */ - @Input() leaveMeetingClickedFn?: () => Promise; - - /** - * Alternative to @Output: Function to call when end meeting is clicked - * When using NgComponentOutlet, use this instead of the @Output above - */ - @Input() endMeetingClickedFn?: () => Promise; - - onCopyLinkClick(): void { - if (this.copyLinkClickedFn) { - this.copyLinkClickedFn(); - } else { - this.copyLinkClicked.emit(); - } - } - - async onLeaveMeetingClick(): Promise { - if (this.leaveMeetingClickedFn) { - await this.leaveMeetingClickedFn(); - } else { - this.leaveMeetingClicked.emit(); - } - } - - async onEndMeetingClick(): Promise { - if (this.endMeetingClickedFn) { - await this.endMeetingClickedFn(); - } else { - this.endMeetingClicked.emit(); - } - } -} diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/customization/components/index.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/customization/components/index.ts new file mode 100644 index 00000000..ac592add --- /dev/null +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/customization/components/index.ts @@ -0,0 +1,4 @@ +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'; diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/customization/components/meeting-layout/meeting-layout.component.html b/meet-ce/frontend/projects/shared-meet-components/src/lib/customization/components/meeting-layout/meeting-layout.component.html new file mode 100644 index 00000000..ed89606a --- /dev/null +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/customization/components/meeting-layout/meeting-layout.component.html @@ -0,0 +1,23 @@ +@if (meetingContextService.lkRoom()) { + + @if (showMeetingLinkOverlay()) { + + + + } + + + + + +} diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/components/meeting-layout/meeting-layout.component.scss b/meet-ce/frontend/projects/shared-meet-components/src/lib/customization/components/meeting-layout/meeting-layout.component.scss similarity index 68% rename from meet-ce/frontend/projects/shared-meet-components/src/lib/components/meeting-layout/meeting-layout.component.scss rename to meet-ce/frontend/projects/shared-meet-components/src/lib/customization/components/meeting-layout/meeting-layout.component.scss index 56a2df80..2974f02c 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/components/meeting-layout/meeting-layout.component.scss +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/customization/components/meeting-layout/meeting-layout.component.scss @@ -1,3 +1,6 @@ +@use '../../../../../../../src/assets/styles/design-tokens'; + + .remote-participant { height: -webkit-fill-available; height: -moz-available; @@ -76,3 +79,38 @@ z-index: 999 !important; cursor: move; } + + + +.main-share-meeting-link-container { + background-color: var(--ov-surface-color); // Use ov-components variable + display: flex; + align-items: center; + justify-content: center; + border-radius: var(--ov-meet-radius-md); + width: 100%; + height: 100%; + position: absolute; + top: 0; + left: 0; + pointer-events: none; + z-index: 1; + + .main-share-meeting-link { + pointer-events: all; + max-width: 100%; + } +} + +.fade-in-delayed-more { + animation: fadeIn 0.5s ease-in 0.9s both; +} + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/components/meeting-layout/meeting-layout.component.spec.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/customization/components/meeting-layout/meeting-layout.component.spec.ts similarity index 98% rename from meet-ce/frontend/projects/shared-meet-components/src/lib/components/meeting-layout/meeting-layout.component.spec.ts rename to meet-ce/frontend/projects/shared-meet-components/src/lib/customization/components/meeting-layout/meeting-layout.component.spec.ts index 9e326fcf..115b32f3 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/components/meeting-layout/meeting-layout.component.spec.ts +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/customization/components/meeting-layout/meeting-layout.component.spec.ts @@ -8,8 +8,8 @@ import { OpenViduService } from 'openvidu-components-angular'; import { MeetingLayoutComponent } from './meeting-layout.component'; -import { MeetLayoutService } from '../../services/layout.service'; -import { MeetLayoutMode } from '../../models/layout.model'; +import { MeetLayoutService } from '../../../services/layout.service'; +import { MeetLayoutMode } from '../../../models/layout.model'; describe('MeetingLayoutComponent', () => { let component: MeetingLayoutComponent; diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/components/meeting-layout/meeting-layout.component.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/customization/components/meeting-layout/meeting-layout.component.ts similarity index 76% rename from meet-ce/frontend/projects/shared-meet-components/src/lib/components/meeting-layout/meeting-layout.component.ts rename to meet-ce/frontend/projects/shared-meet-components/src/lib/customization/components/meeting-layout/meeting-layout.component.ts index 6f0a86c6..f479b4c4 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/components/meeting-layout/meeting-layout.component.ts +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/customization/components/meeting-layout/meeting-layout.component.ts @@ -1,18 +1,13 @@ -import { Component, signal, computed, effect, inject, DestroyRef, input, untracked, Type } from '@angular/core'; -import { NgComponentOutlet } from '@angular/common'; +import { Component, signal, computed, effect, inject, DestroyRef, input, untracked } from '@angular/core'; import { toSignal } from '@angular/core/rxjs-interop'; import { Participant } from 'livekit-client'; -import { - ParticipantModel, - LoggerService, - ParticipantService, - OpenViduService, - ILogger, - OpenViduComponentsUiModule -} from 'openvidu-components-angular'; -import { MeetLayoutMode } from '../../models/layout.model'; -import { MeetLayoutService } from '../../services/layout.service'; -import { MEETING_COMPONENTS_TOKEN, MeetingComponentsPlugins } from '../../customization'; +import { LoggerService, OpenViduService, ILogger, OpenViduComponentsUiModule } from 'openvidu-components-angular'; +import { MeetLayoutMode } from '../../../models/layout.model'; +import { CustomParticipantModel } from '../../../models'; +import { MeetLayoutService } from '../../../services/layout.service'; +import { MeetingContextService } from '../../../services/meeting/meeting-context.service'; +import { ShareMeetingLinkComponent } from '../../../components/share-meeting-link/share-meeting-link.component'; +import { MeetingService } from '../../../services/meeting/meeting.service'; /** * MeetingLayoutComponent - Intelligent layout component for scalable video conferencing @@ -22,43 +17,40 @@ import { MEETING_COMPONENTS_TOKEN, MeetingComponentsPlugins } from '../../custom */ @Component({ selector: 'ov-meeting-layout', - imports: [OpenViduComponentsUiModule, NgComponentOutlet], + imports: [OpenViduComponentsUiModule, ShareMeetingLinkComponent], templateUrl: './meeting-layout.component.html', styleUrl: './meeting-layout.component.scss' }) export class MeetingLayoutComponent { - plugins: MeetingComponentsPlugins = inject(MEETING_COMPONENTS_TOKEN, { optional: true }) || {}; - private readonly loggerSrv = inject(LoggerService); private readonly layoutService = inject(MeetLayoutService); - private readonly participantService = inject(ParticipantService); - private readonly openviduService = inject(OpenViduService); + protected readonly openviduService = inject(OpenViduService); + protected meetingContextService = inject(MeetingContextService); + protected meetingService = inject(MeetingService); private readonly destroyRef = inject(DestroyRef); private readonly log: ILogger = this.loggerSrv.get('MeetingLayoutComponent'); + 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'; + protected readonly linkOverlayTitleWeight: 'normal' | 'bold' = 'bold'; /** * Maximum number of active remote speakers to show in the layout when the last speakers layout is enabled. - * Higher values provide more context but may impact performance on lower-end devices. - * @default 4 */ readonly maxRemoteSpeakers = input(4); - /** - * Optional component to render additional elements in the layout (e.g., share link overlay) - * This allows plugins to inject custom UI elements into the layout. - */ - readonly additionalElementsComponent = input | undefined>(undefined); - - /** - * Inputs to pass to the additional elements component - */ - readonly additionalElementsInputs = input(undefined); - - // Reactive state with Signals - private readonly remoteParticipants = toSignal(this.participantService.remoteParticipants$, { - initialValue: [] as ParticipantModel[] + protected meetingUrl = computed(() => { + return this.meetingContextService.meetingUrl(); }); + protected showMeetingLinkOverlay = computed(() => { + const remoteParticipants = this.meetingContextService.remoteParticipants(); + return this.meetingContextService.canModerateRoom() && remoteParticipants.length === 0; + }); + + // Reactive state with Signals - now using MeetingContextService + private readonly remoteParticipants = computed(() => this.meetingContextService.remoteParticipants()); + private readonly layoutMode = toSignal(this.layoutService.layoutMode$, { initialValue: MeetLayoutMode.LAST_SPEAKERS }); @@ -103,7 +95,7 @@ export class MeetingLayoutComponent { // Filter active speakers that still exist in remote participants const validActiveSpeakers = activeSpeakersOrder .map((identity) => participantsMap.get(identity)) - .filter((p): p is ParticipantModel => p !== undefined) + .filter((p): p is CustomParticipantModel => p !== undefined) .slice(-maxSpeakers); // Take last N speakers (most recent) // If we have fewer active speakers than max, fill with additional participants @@ -120,8 +112,12 @@ export class MeetingLayoutComponent { }); constructor() { - // Setup active speakers listener - this.setupActiveSpeakersListener(); + effect(() => { + const lkRoom = this.meetingContextService.lkRoom(); + if (lkRoom) { + this.setupActiveSpeakersListener(); + } + }); // Effect to log layout mode changes (development only) effect(() => { @@ -157,17 +153,30 @@ export class MeetingLayoutComponent { }); } + protected onCopyMeetingLinkClicked(): void { + const room = this.meetingContextService.meetRoom(); + if (!room) { + this.log.e('Cannot copy link: meeting room is undefined'); + return; + } + + this.meetingService.copyMeetingSpeakerLink(room); + } + /** * Sets up the listener for active speakers changes from LiveKit * Uses efficient Set operations and early returns for performance */ private setupActiveSpeakersListener(): void { const room = this.openviduService.getRoom(); + if (!room) { + this.log.e('Cannot setup active speakers listener: room is undefined'); + return; + } // Register cleanup on component destroy this.destroyRef.onDestroy(() => { room.off('activeSpeakersChanged', this.handleActiveSpeakersChanged); - this.log.d('Active speakers listener cleaned up'); }); room.on('activeSpeakersChanged', this.handleActiveSpeakersChanged); diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/customization/components/meeting-participant-panel-item/meeting-participant-panel-item.component.html b/meet-ce/frontend/projects/shared-meet-components/src/lib/customization/components/meeting-participant-panel-item/meeting-participant-panel-item.component.html new file mode 100644 index 00000000..f0d46bda --- /dev/null +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/customization/components/meeting-participant-panel-item/meeting-participant-panel-item.component.html @@ -0,0 +1,67 @@ + + @let ctx = getDisplayProperties(participantContext, localParticipant); + +
+ + + + @if (ctx.showModeratorBadge) { + + + shield_person + + + } + + + + @if (ctx.showModerationControls) { +
+ + @if (ctx.showMakeModeratorButton) { + + } + + + @if (ctx.showUnmakeModeratorButton) { + + } + + + @if (ctx.showKickButton) { + + } +
+ } +
+
+
+ diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/components/meeting-participant-panel/meeting-participant-panel.component.scss b/meet-ce/frontend/projects/shared-meet-components/src/lib/customization/components/meeting-participant-panel-item/meeting-participant-panel-item.component.scss similarity index 86% rename from meet-ce/frontend/projects/shared-meet-components/src/lib/components/meeting-participant-panel/meeting-participant-panel.component.scss rename to meet-ce/frontend/projects/shared-meet-components/src/lib/customization/components/meeting-participant-panel-item/meeting-participant-panel-item.component.scss index 3773a541..5577b076 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/components/meeting-participant-panel/meeting-participant-panel.component.scss +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/customization/components/meeting-participant-panel-item/meeting-participant-panel-item.component.scss @@ -1,4 +1,4 @@ -@use '../../../../../../src/assets/styles/design-tokens'; +@use '../../../../../../../src/assets/styles/design-tokens'; .participant-item-container { width: 100%; diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/customization/components/meeting-participant-panel-item/meeting-participant-panel-item.component.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/customization/components/meeting-participant-panel-item/meeting-participant-panel-item.component.ts new file mode 100644 index 00000000..8cb4d2c3 --- /dev/null +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/customization/components/meeting-participant-panel-item/meeting-participant-panel-item.component.ts @@ -0,0 +1,139 @@ +import { CommonModule } from '@angular/common'; +import { Component, TemplateRef, ViewChild, inject } from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; +import { MatIconModule } from '@angular/material/icon'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { LoggerService, OpenViduComponentsUiModule } from 'openvidu-components-angular'; +import { CustomParticipantModel } from '../../../models'; +import { MeetingService } from '../../../services/meeting/meeting.service'; +import { MeetRoomMemberRole } from '@openvidu-meet/typings'; + +/** + * Interface for computed participant display properties + */ +export interface ParticipantDisplayProperties { + showModeratorBadge: boolean; + showModerationControls: boolean; + showMakeModeratorButton: boolean; + showUnmakeModeratorButton: boolean; + showKickButton: boolean; +} + +/** + * Reusable component for displaying participant panel items with moderation controls. + * 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'], + imports: [CommonModule, MatButtonModule, MatIconModule, MatTooltipModule, OpenViduComponentsUiModule] +}) +export class MeetingParticipantPanelItemComponent { + // Template reference for the component's template + @ViewChild('template', { static: true }) template!: TemplateRef; + + protected meetingService: MeetingService = inject(MeetingService); + protected loggerService = inject(LoggerService); + protected log = this.loggerService.get('OpenVidu Meet - ParticipantPanelItem'); + + // Tooltips (could be made configurable in the future if needed) + protected readonly moderatorBadgeTooltip = 'Moderator'; + protected readonly makeModeratorTooltip = 'Make participant moderator'; + protected readonly unmakeModeratorTooltip = 'Unmake participant moderator'; + protected readonly kickParticipantTooltip = 'Kick participant'; + + /** + * Get or compute display properties for a participant + */ + protected getDisplayProperties( + participant: CustomParticipantModel, + localParticipant: CustomParticipantModel + ): ParticipantDisplayProperties { + // Compute all display properties once + const isLocalModerator = localParticipant.isModerator(); + const isParticipantLocal = participant.isLocal; + const isParticipantModerator = participant.isModerator(); + const isParticipantOriginalModerator = participant.isOriginalModerator(); + + return { + showModeratorBadge: isParticipantModerator, + showModerationControls: isLocalModerator && !isParticipantLocal, + showMakeModeratorButton: isLocalModerator && !isParticipantLocal && !isParticipantModerator, + showUnmakeModeratorButton: + isLocalModerator && !isParticipantLocal && isParticipantModerator && !isParticipantOriginalModerator, + showKickButton: isLocalModerator && !isParticipantLocal && !isParticipantOriginalModerator + }; + } + + async onMakeModeratorClick( + participantContext: CustomParticipantModel, + localParticipant: CustomParticipantModel + ): Promise { + if (!localParticipant.isModerator()) return; + + const roomId = localParticipant.roomName; + + if (!roomId) { + this.log.e('Cannot change participant role: local participant room name is undefined'); + return; + } + + try { + await this.meetingService.changeParticipantRole( + roomId, + participantContext.identity, + MeetRoomMemberRole.MODERATOR + ); + this.log.d('Moderator assigned successfully'); + } catch (error) { + this.log.e('Error assigning moderator:', error); + } + } + + async onUnmakeModeratorClick( + participantContext: CustomParticipantModel, + localParticipant: CustomParticipantModel + ): Promise { + if (!localParticipant.isModerator()) return; + + const roomId = localParticipant.roomName; + + if (!roomId) { + this.log.e('Cannot change participant role: local participant room name is undefined'); + return; + } + + try { + await this.meetingService.changeParticipantRole( + roomId, + participantContext.identity, + MeetRoomMemberRole.SPEAKER + ); + this.log.d('Moderator unassigned successfully'); + } catch (error) { + this.log.e('Error unassigning moderator:', error); + } + } + + async onKickParticipantClick( + participantContext: CustomParticipantModel, + localParticipant: CustomParticipantModel + ): Promise { + if (!localParticipant.isModerator()) return; + + const roomId = localParticipant.roomName; + + if (!roomId) { + this.log.e('Cannot change participant role: local participant room name is undefined'); + return; + } + + try { + await this.meetingService.kickParticipant(roomId, participantContext.identity); + this.log.d('Participant kicked successfully'); + } catch (error) { + this.log.e('Error kicking participant:', error); + } + } +} diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/customization/components/meeting-share-link-panel/meeting-share-link-panel.component.html b/meet-ce/frontend/projects/shared-meet-components/src/lib/customization/components/meeting-share-link-panel/meeting-share-link-panel.component.html new file mode 100644 index 00000000..a03b0554 --- /dev/null +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/customization/components/meeting-share-link-panel/meeting-share-link-panel.component.html @@ -0,0 +1,5 @@ +@if (showShareLink()) { + +} diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/components/meeting-share-link-panel/meeting-share-link-panel.component.scss b/meet-ce/frontend/projects/shared-meet-components/src/lib/customization/components/meeting-share-link-panel/meeting-share-link-panel.component.scss similarity index 100% rename from meet-ce/frontend/projects/shared-meet-components/src/lib/components/meeting-share-link-panel/meeting-share-link-panel.component.scss rename to meet-ce/frontend/projects/shared-meet-components/src/lib/customization/components/meeting-share-link-panel/meeting-share-link-panel.component.scss diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/customization/components/meeting-share-link-panel/meeting-share-link-panel.component.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/customization/components/meeting-share-link-panel/meeting-share-link-panel.component.ts new file mode 100644 index 00000000..9b6a2335 --- /dev/null +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/customization/components/meeting-share-link-panel/meeting-share-link-panel.component.ts @@ -0,0 +1,47 @@ +import { CommonModule } from '@angular/common'; +import { Component, computed, inject } from '@angular/core'; +import { ShareMeetingLinkComponent } from '../../../components/share-meeting-link/share-meeting-link.component'; +import { MeetingContextService } from '../../../services/meeting/meeting-context.service'; +import { MeetingService } from '../../../services/meeting/meeting.service'; +import { LoggerService } from 'openvidu-components-angular'; + +/** + * Reusable component for displaying the share meeting link panel + * 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'], + imports: [CommonModule, ShareMeetingLinkComponent] +}) +export class MeetingShareLinkPanelComponent { + protected meetingContextService = inject(MeetingContextService); + protected meetingService = inject(MeetingService); + protected loggerService = inject(LoggerService); + protected log = this.loggerService.get('OpenVidu Meet - MeetingShareLinkPanel'); + + /** + * 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(); + }); + + onCopyClicked(): void { + const room = this.meetingContextService.meetRoom(); + if (!room) { + this.log.e('Cannot copy link: meeting room is undefined'); + return; + } + + this.meetingService.copyMeetingSpeakerLink(room); + } +} diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/components/meeting-toolbar-buttons/meeting-toolbar-buttons.component.html b/meet-ce/frontend/projects/shared-meet-components/src/lib/customization/components/meeting-toolbar-buttons/meeting-toolbar-buttons.component.html similarity index 90% rename from meet-ce/frontend/projects/shared-meet-components/src/lib/components/meeting-toolbar-buttons/meeting-toolbar-buttons.component.html rename to meet-ce/frontend/projects/shared-meet-components/src/lib/customization/components/meeting-toolbar-buttons/meeting-toolbar-buttons.component.html index 975af447..801ef20a 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/components/meeting-toolbar-buttons/meeting-toolbar-buttons.component.html +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/customization/components/meeting-toolbar-buttons/meeting-toolbar-buttons.component.html @@ -1,6 +1,6 @@ -@if (showCopyLinkButton) { - @if (isMobile) { +@if (showCopyLinkButton()) { + @if (isMobile()) { diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/components/meeting-toolbar-buttons/meeting-toolbar-buttons.component.scss b/meet-ce/frontend/projects/shared-meet-components/src/lib/customization/components/meeting-toolbar-buttons/meeting-toolbar-buttons.component.scss similarity index 85% rename from meet-ce/frontend/projects/shared-meet-components/src/lib/components/meeting-toolbar-buttons/meeting-toolbar-buttons.component.scss rename to meet-ce/frontend/projects/shared-meet-components/src/lib/customization/components/meeting-toolbar-buttons/meeting-toolbar-buttons.component.scss index a75cd2b1..8401a707 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/components/meeting-toolbar-buttons/meeting-toolbar-buttons.component.scss +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/customization/components/meeting-toolbar-buttons/meeting-toolbar-buttons.component.scss @@ -1,4 +1,4 @@ -@use '../../../../../../src/assets/styles/design-tokens'; +@use '../../../../../../../src/assets/styles/design-tokens'; .button-text { margin-left: 8px; diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/customization/components/meeting-toolbar-buttons/meeting-toolbar-buttons.component.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/customization/components/meeting-toolbar-buttons/meeting-toolbar-buttons.component.ts new file mode 100644 index 00000000..f44f3ac3 --- /dev/null +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/customization/components/meeting-toolbar-buttons/meeting-toolbar-buttons.component.ts @@ -0,0 +1,77 @@ +import { Component, inject, computed } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { MatButtonModule } from '@angular/material/button'; +import { MatDividerModule } from '@angular/material/divider'; +import { MatIconModule } from '@angular/material/icon'; +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'; + +/** + * Reusable component for meeting toolbar additional buttons. + */ +@Component({ + selector: 'ov-meeting-toolbar-buttons', + templateUrl: './meeting-toolbar-buttons.component.html', + styleUrls: ['./meeting-toolbar-buttons.component.scss'], + imports: [CommonModule, MatButtonModule, MatIconModule, MatMenuModule, MatTooltipModule, MatDividerModule] +}) +export class MeetingToolbarButtonsComponent { + protected meetingContextService = inject(MeetingContextService); + protected meetingService = inject(MeetingService); + protected loggerService = inject(LoggerService); + protected log = this.loggerService.get('OpenVidu Meet - MeetingToolbarButtons'); + 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 + */ + protected showLeaveMenu = 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); + } + + async onLeaveMeetingClick(): Promise { + await this.openviduService.disconnectRoom(); + } + + async onEndMeetingClick(): Promise { + const roomId = this.meetingContextService.roomId(); + if (!roomId) { + this.log.e('Cannot end meeting: room ID is undefined'); + return; + } + + await this.meetingService.endMeeting(roomId); + } +} diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/customization/index.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/customization/index.ts index a025182a..358c3326 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/customization/index.ts +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/customization/index.ts @@ -3,3 +3,5 @@ */ export * from './components/meeting-components-plugins.token'; export * from './handlers/meeting-action-handler'; + +export * from './components/index'; diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/guards/check-room-edit.guard.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/guards/check-room-edit.guard.ts index ae785548..7e437cf3 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/guards/check-room-edit.guard.ts +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/guards/check-room-edit.guard.ts @@ -7,7 +7,7 @@ import { RoomService } from '../services/room.service'; * Guard that prevents editing a room when there's an active meeting. * Redirects to /rooms if the room has an active meeting. */ -export const checkRoomEditGuard: CanActivateFn = async (route) => { +export const checkEditableRoomGuard: CanActivateFn = async (route) => { const roomService = inject(RoomService); const router = inject(Router); diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/guards/extract-query-params.guard.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/guards/extract-query-params.guard.ts index 4890d00b..b46df3fc 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/guards/extract-query-params.guard.ts +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/guards/extract-query-params.guard.ts @@ -2,11 +2,12 @@ import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, CanActivateFn } from '@angular/router'; import { WebComponentProperty } from '@openvidu-meet/typings'; import { ErrorReason } from '../models'; -import { AppDataService, NavigationService, RoomMemberService, RoomService, SessionStorageService } from '../services'; +import { AppDataService, MeetingContextService, NavigationService, RoomMemberService, RoomService, SessionStorageService } from '../services'; export const extractRoomQueryParamsGuard: CanActivateFn = (route: ActivatedRouteSnapshot) => { const navigationService = inject(NavigationService); const roomService = inject(RoomService); + const meetingContextService = inject(MeetingContextService); const roomMemberService = inject(RoomMemberService); const sessionStorageService = inject(SessionStorageService); @@ -33,7 +34,7 @@ export const extractRoomQueryParamsGuard: CanActivateFn = (route: ActivatedRoute roomService.setRoomSecret(secret); if (e2eeKey) { - roomService.setE2EEKey(e2eeKey); + meetingContextService.setE2eeKey(e2eeKey); } if (participantName) { diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/guards/validate-access.guard.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/guards/validate-access.guard.ts index d5340ad3..401cb969 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/guards/validate-access.guard.ts +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/guards/validate-access.guard.ts @@ -1,7 +1,7 @@ import { inject } from '@angular/core'; import { ActivatedRouteSnapshot, CanActivateFn, RouterStateSnapshot } from '@angular/router'; import { ErrorReason } from '../models'; -import { NavigationService, RecordingService, RoomMemberService, RoomService } from '../services'; +import { MeetingContextService, NavigationService, RecordingService, RoomMemberService } from '../services'; /** * Guard to validate access to a room by generating a room member token. @@ -31,12 +31,20 @@ export const validateRoomRecordingsAccessGuard: CanActivateFn = async ( * @returns True if access is granted, or UrlTree for redirection */ const validateRoomAccessInternal = async (pageUrl: string, validateRecordingPermissions = false) => { - const roomService = inject(RoomService); const roomMemberService = inject(RoomMemberService); const navigationService = inject(NavigationService); + const meetingContextService = inject(MeetingContextService); - const roomId = roomService.getRoomId(); - const secret = roomService.getRoomSecret(); + const roomId = meetingContextService.roomId(); + if (!roomId) { + console.error('Cannot validate room access: room ID is undefined'); + return navigationService.redirectToErrorPage(ErrorReason.INVALID_ROOM); + } + const secret = meetingContextService.roomSecret(); + if (!secret) { + console.error('Cannot validate room access: room secret is undefined'); + return navigationService.redirectToErrorPage(ErrorReason.MISSING_ROOM_SECRET); + } try { await roomMemberService.generateToken(roomId, { diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/interceptors/http.interceptor.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/interceptors/http.interceptor.ts index 2db7053f..cbbc5ce4 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/interceptors/http.interceptor.ts +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/interceptors/http.interceptor.ts @@ -2,7 +2,7 @@ import { HttpErrorResponse, HttpEvent, HttpHandlerFn, HttpInterceptorFn, HttpReq import { inject } from '@angular/core'; import { Router } from '@angular/router'; import { catchError, from, Observable, switchMap } from 'rxjs'; -import { AuthService, RoomMemberService, RoomService, TokenStorageService } from '../services'; +import { AuthService, MeetingContextService, RoomMemberService, RoomService, TokenStorageService } from '../services'; /** * Adds all necessary authorization headers to the request based on available tokens @@ -34,7 +34,7 @@ const addAuthHeadersIfNeeded = ( export const httpInterceptor: HttpInterceptorFn = (req: HttpRequest, next: HttpHandlerFn) => { const router: Router = inject(Router); const authService: AuthService = inject(AuthService); - const roomService = inject(RoomService); + const meetingContextService = inject(MeetingContextService); const roomMemberService = inject(RoomMemberService); const tokenStorageService = inject(TokenStorageService); @@ -72,8 +72,10 @@ export const httpInterceptor: HttpInterceptorFn = (req: HttpRequest, ne const refreshRoomMemberToken = (firstError: HttpErrorResponse): Observable> => { console.log('Refreshing room member token...'); - const roomId = roomService.getRoomId(); - const secret = roomService.getRoomSecret(); + const roomId = meetingContextService.roomId(); + if (!roomId) throw new Error('Cannot refresh room member token: room ID is undefined'); + const secret = meetingContextService.roomSecret(); + if (!secret) throw new Error('Cannot refresh room member token: room secret is undefined'); const participantName = roomMemberService.getParticipantName(); const participantIdentity = roomMemberService.getParticipantIdentity(); const grantJoinMeetingPermission = !!participantIdentity; // Grant join permission if identity is set diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/models/lobby.model.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/models/lobby.model.ts index d9098324..e791e986 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/models/lobby.model.ts +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/models/lobby.model.ts @@ -2,18 +2,19 @@ import { FormGroup } from '@angular/forms'; import { MeetRoom } from '@openvidu-meet/typings'; /** - * State interface representing the lobby state of a meeting + * State interface representing the lobby phase of a meeting. + * + * IMPORTANT: This state is ONLY relevant during the lobby phase (before joining the meeting). + * Once the participant joins the meeting, MeetingContextService becomes the single source of truth. */ export interface LobbyState { room?: MeetRoom; - roomId: string; - roomSecret: string; + roomId?: string; roomClosed: boolean; - hasRecordings: boolean; showRecordingCard: boolean; showBackButton: boolean; backButtonText: string; - isE2EEEnabled: boolean; + hasRoomE2EEEnabled: boolean; participantForm: FormGroup; - roomMemberToken: string; + roomMemberToken?: string; } diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/pages/meeting/meeting.component.html b/meet-ce/frontend/projects/shared-meet-components/src/lib/pages/meeting/meeting.component.html index d0daa6db..280228ec 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/pages/meeting/meeting.component.html +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/pages/meeting/meeting.component.html @@ -1,8 +1,8 @@ -@if (showPrejoin) { - - @if (prejoinReady && plugins.lobby) { - - } @else if (!prejoinReady) { +@if (showLobby) { + + @if (isLobbyReady) { + + } @else if (!isLobbyReady) {

Preparing your meeting...

@@ -10,24 +10,24 @@ } @else {
error_outline -

Unable to load the pre-join screen. Please try reloading the page.

+

Unable to load the lobby. Please try reloading the page.

} } @else { - - @if (plugins.toolbar?.additionalButtons) { - - - - } + +
+ +
- - @if (plugins.toolbar?.leaveButton) { - - - - } + +
+ +
- - @if (plugins.participantPanel?.afterLocalParticipant) { - - + +
+ - } +
- + - + - - - @if (plugins.participantPanel?.item) { - - - - }
} diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/pages/meeting/meeting.component.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/pages/meeting/meeting.component.ts index 69ff6ef8..c71c99d3 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/pages/meeting/meeting.component.ts +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/pages/meeting/meeting.component.ts @@ -1,12 +1,9 @@ -import { Clipboard } from '@angular/cdk/clipboard'; -import { CommonModule, NgComponentOutlet } from '@angular/common'; -import { Component, computed, effect, inject, OnInit, Signal, signal } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { Component, computed, ContentChild, effect, inject, OnInit, Signal, signal, TemplateRef } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { MatIconModule } from '@angular/material/icon'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; -import { MeetRoom, MeetRoomMemberRole } from '@openvidu-meet/typings'; import { - ParticipantService as ComponentParticipantService, OpenViduComponentsUiModule, OpenViduService, OpenViduThemeMode, @@ -15,22 +12,21 @@ import { Track, ViewportService } from 'openvidu-components-angular'; -import { combineLatest, Subject, takeUntil } from 'rxjs'; -import { MEETING_ACTION_HANDLER_TOKEN, MEETING_COMPONENTS_TOKEN, MeetingComponentsPlugins } from '../../customization'; -import { CustomParticipantModel } from '../../models'; -import { LobbyState } from '../../models/lobby.model'; +import { Subject } from 'rxjs'; +import { MeetingParticipantPanelItemComponent } from '../../customization'; import { ApplicationFeatures, FeatureConfigurationService, GlobalConfigService, + MeetingContextService, MeetingEventHandlerService, MeetingLobbyService, - MeetingPluginManagerService, MeetingService, NotificationService, RoomMemberService, WebComponentManagerService } from '../../services'; +import { MeetingLobbyComponent } from '../../components/meeting-lobby/meeting-lobby.component'; @Component({ selector: 'ov-meeting', @@ -41,53 +37,67 @@ import { CommonModule, FormsModule, ReactiveFormsModule, - NgComponentOutlet, MatIconModule, - MatProgressSpinnerModule + MatProgressSpinnerModule, + MeetingLobbyComponent ], - providers: [MeetingLobbyService, MeetingPluginManagerService, MeetingEventHandlerService] + providers: [MeetingLobbyService, MeetingEventHandlerService] }) export class MeetingComponent implements OnInit { - lobbyState?: LobbyState; - protected localParticipant = signal(undefined); + protected _participantPanelItem?: MeetingParticipantPanelItemComponent; - // Reactive signal for remote participants to trigger computed updates - protected remoteParticipants = signal([]); + // Template reference for custom participant panel item + @ContentChild(MeetingParticipantPanelItemComponent) + set participantPanelItem(value: MeetingParticipantPanelItemComponent | undefined) { + // Store the reference to the custom participant panel item component + this._participantPanelItem = value; + } + get participantPanelItemTemplate(): TemplateRef | undefined { + return this._participantPanelItem?.template; + } - // Signal to track participant updates (role changes, etc.) that don't change array references - protected participantsVersion = signal(0); - - showPrejoin = true; - prejoinReady = false; - features: Signal; - - // Injected plugins - plugins: MeetingComponentsPlugins; + /** + * Controls whether to show lobby (true) or meeting view (false) + */ + showLobby = true; + isLobbyReady = false; + protected features: Signal; protected meetingService = inject(MeetingService); protected participantService = inject(RoomMemberService); protected featureConfService = inject(FeatureConfigurationService); protected wcManagerService = inject(WebComponentManagerService); protected openviduService = inject(OpenViduService); - protected ovComponentsParticipantService = inject(ComponentParticipantService); protected viewportService = inject(ViewportService); protected ovThemeService = inject(OpenViduThemeService); protected configService = inject(GlobalConfigService); - protected clipboard = inject(Clipboard); protected notificationService = inject(NotificationService); protected lobbyService = inject(MeetingLobbyService); - protected pluginManager = inject(MeetingPluginManagerService); - - // Public for direct template binding (uses arrow functions to preserve 'this' context) - public eventHandler = inject(MeetingEventHandlerService); - - // Injected action handler (optional - falls back to default implementation) - protected actionHandler = inject(MEETING_ACTION_HANDLER_TOKEN, { optional: true }); + protected meetingContextService = inject(MeetingContextService); + protected eventHandlerService = inject(MeetingEventHandlerService); 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.featureConfService.features; - this.plugins = inject(MEETING_COMPONENTS_TOKEN, { optional: true }) || {}; // Change theme variables when custom theme is enabled effect(() => { @@ -105,142 +115,23 @@ export class MeetingComponent implements OnInit { this.ovThemeService.resetThemeVariables(); } }); - } - // Computed signals for plugin inputs - protected toolbarAdditionalButtonsInputs = computed(() => - this.pluginManager.getToolbarAdditionalButtonsInputs(this.features().canModerateRoom, this.isMobile, () => - this.handleCopySpeakerLink() - ) - ); - - protected toolbarLeaveButtonInputs = computed(() => - this.pluginManager.getToolbarLeaveButtonInputs( - this.features().canModerateRoom, - this.isMobile, - () => this.openviduService.disconnectRoom(), - () => this.endMeeting() - ) - ); - - protected participantPanelAfterLocalInputs = computed(() => - this.pluginManager.getParticipantPanelAfterLocalInputs( - this.features().canModerateRoom, - `${this.hostname}/room/${this.roomId}`, - () => this.handleCopySpeakerLink() - ) - ); - - /** - * Inputs for custom layout component (CE or PRO) - * Includes additionalElementsComponent if provided via plugin - */ - protected layoutInputs = computed(() => { - const showOverlay = this.onlyModeratorIsPresent; - const meetingUrl = `${this.hostname}/room/${this.roomId}`; - const onCopyLinkFn = () => this.handleCopySpeakerLink(); - const additionalElementsComponent = this.plugins.layoutAdditionalElements; - return this.pluginManager.getLayoutInputs(showOverlay, meetingUrl, onCopyLinkFn, additionalElementsComponent); - }); - - protected lobbyInputs = computed(() => { - if (!this.lobbyState) return {}; - return this.pluginManager.getLobbyInputs( - this.lobbyState, - this.hostname, - this.features().canModerateRoom, - () => this.submitAccessMeeting(), - () => this.lobbyService.goToRecordings(), - () => this.lobbyService.goBack(), - () => this.handleCopySpeakerLink() - ); - }); - - protected participantPanelItemInputsMap = computed(() => { - const local = this.localParticipant(); - const remotes = this.remoteParticipants(); - // Force reactivity by reading participantsVersion signal - this.participantsVersion(); - const allParticipants: CustomParticipantModel[] = local ? [local, ...remotes] : remotes; - - const inputsMap = new Map(); - for (const participant of allParticipants) { - const inputs = this.pluginManager.getParticipantPanelItemInputs( - participant, - allParticipants, - (p) => this.handleMakeModerator(p), - (p) => this.handleUnmakeModerator(p), - (p) => this.handleKickParticipant(p) - ); - inputsMap.set(participant.identity, inputs); - } - - return inputsMap; - }); - - get participantName(): string { - return this.lobbyService.participantName; - } - get e2eeKey(): string { - return this.lobbyService.e2eeKey; - } - - get roomMemberToken(): string { - return this.lobbyState!.roomMemberToken; - } - - get room(): MeetRoom | undefined { - return this.lobbyState?.room; - } - - get roomName(): string { - return this.lobbyState?.room?.roomName || 'Room'; - } - - get roomId(): string { - return this.lobbyState?.roomId || ''; - } - - get roomSecret(): string { - return this.lobbyState?.roomSecret || ''; - } - - set roomSecret(value: string) { - if (this.lobbyState) { - this.lobbyState.roomSecret = value; - } - } - - get onlyModeratorIsPresent(): boolean { - return this.features().canModerateRoom && !this.hasRemoteParticipants; - } - - get hasRemoteParticipants(): boolean { - return this.remoteParticipants().length > 0; - } - - get hasRecordings(): boolean { - return this.lobbyState?.hasRecordings || false; - } - - set hasRecordings(value: boolean) { - if (this.lobbyState) { - this.lobbyState.hasRecordings = value; - } - } - - get hostname(): string { - return window.location.origin.replace('http://', '').replace('https://', ''); - } - - get isMobile(): boolean { - return this.viewportService.isMobile(); + // Observe lobby state changes reactively + // When roomMemberToken is set, transition from lobby to meeting + effect(async () => { + const token = this.roomMemberToken(); + if (token && this.showLobby) { + // The meeting view must be shown before loading the appearance config + this.showLobby = false; + await this.configService.loadRoomsAppearanceConfig(); + } + }); } async ngOnInit() { try { - this.lobbyState = await this.lobbyService.initialize(); - this.prejoinReady = true; + await this.lobbyService.initialize(); + this.isLobbyReady = true; } catch (error) { console.error('Error initializing lobby state:', error); this.notificationService.showDialog({ @@ -255,79 +146,58 @@ export class MeetingComponent implements OnInit { ngOnDestroy() { this.destroy$.next(); this.destroy$.complete(); + + // Clear meeting context when component is destroyed + this.meetingContextService.clearContext(); } - async submitAccessMeeting() { - try { - await this.lobbyService.submitAccess(); + // 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); + // } + // } - // The meeting view must be shown before loading the appearance config, - // as it contains theme information that might be applied immediately - // when the meeting view is rendered - this.showPrejoin = false; - await this.configService.loadRoomsAppearanceConfig(); + 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 - combineLatest([ - this.ovComponentsParticipantService.remoteParticipants$, - this.ovComponentsParticipantService.localParticipant$ - ]) - .pipe(takeUntil(this.destroy$)) - .subscribe(([participants, local]) => { - this.remoteParticipants.set(participants as CustomParticipantModel[]); - this.localParticipant.set(local as CustomParticipantModel); + // Store LiveKit room in context + this.meetingContextService.setLkRoom(lkRoom); - // Update action handler context if provided - if (this.actionHandler) { - this.actionHandler.roomId = this.roomId; - this.actionHandler.roomSecret = this.roomSecret; - this.actionHandler.localParticipant = this.localParticipant(); - } - - this.updateVideoPinState(); - }); - } catch (error) { - console.error('Error accessing meeting:', error); - } + // Setup LK room event listeners + this.eventHandlerService.setupRoomListeners(lkRoom); } - onRoomCreated(room: Room) { - this.eventHandler.setupRoomListeners(room, { - roomId: this.roomId, - roomSecret: this.roomSecret, - participantName: this.participantName, - localParticipant: () => this.localParticipant(), - remoteParticipants: () => this.remoteParticipants(), - onHasRecordingsChanged: (hasRecordings) => { - this.hasRecordings = hasRecordings; - }, - onRoomSecretChanged: (secret) => { - this.roomSecret = secret; - }, - onParticipantRoleUpdated: () => { - // Increment version to trigger reactivity in participant panel items - this.participantsVersion.update((v) => v + 1); - } - }); - } + // async leaveMeeting() { + // await this.openviduService.disconnectRoom(); + // } - async leaveMeeting() { - await this.openviduService.disconnectRoom(); - } + // async endMeeting() { + // if (!this.participantService.isModerator()) return; - async endMeeting() { - if (!this.participantService.isModerator()) return; + // this.meetingContextService.setMeetingEndedBy('self'); - this.eventHandler.setMeetingEndedByMe(true); - - try { - await this.meetingService.endMeeting(this.roomId); - } catch (error) { - console.error('Error ending meeting:', error); - } - } + // try { + // await this.meetingService.endMeeting(this.roomId()!); + // } catch (error) { + // console.error('Error ending meeting:', error); + // } + // } async onViewRecordingsClicked() { - window.open(`/room/${this.roomId}/recordings?secret=${this.roomSecret}`, '_blank'); + window.open(`/room/${this.roomId()}/recordings?secret=${this.roomSecret()}`, '_blank'); } /** @@ -335,107 +205,17 @@ export class MeetingComponent implements OnInit { * remote participants and local screen sharing state. */ protected updateVideoPinState(): void { - if (!this.localParticipant) return; + const localParticipant = this.localParticipant(); + if (!localParticipant) return; - const isSharing = this.localParticipant()?.isScreenShareEnabled; + const isSharing = localParticipant.isScreenShareEnabled; - if (this.hasRemoteParticipants && isSharing) { + if (this.hasRemoteParticipants() && isSharing) { // Pin the local screen share to appear bigger - this.localParticipant()?.setVideoPinnedBySource(Track.Source.ScreenShare, true); + localParticipant.setVideoPinnedBySource(Track.Source.ScreenShare, true); } else { // Unpin everything if no remote participants or not sharing - this.localParticipant()?.setAllVideoPinned(false); - } - } - - /** - * Event handler wrappers - delegates to actionHandler if provided, otherwise uses default implementation - */ - protected async handleKickParticipant(participant: CustomParticipantModel) { - if (this.actionHandler) { - await this.actionHandler.kickParticipant(participant); - } else { - // Default implementation - if (!this.participantService.isModerator()) return; - - try { - await this.meetingService.kickParticipant(this.roomId, participant.identity); - console.log('Participant kicked successfully'); - } catch (error) { - console.error('Error kicking participant:', error); - } - } - } - - protected async handleMakeModerator(participant: CustomParticipantModel) { - if (this.actionHandler) { - await this.actionHandler.makeModerator(participant); - } else { - // Default implementation - if (!this.participantService.isModerator()) return; - - try { - await this.meetingService.changeParticipantRole( - this.roomId, - participant.identity, - MeetRoomMemberRole.MODERATOR - ); - console.log('Moderator assigned successfully'); - } catch (error) { - console.error('Error assigning moderator:', error); - } - } - } - - protected async handleUnmakeModerator(participant: CustomParticipantModel) { - if (this.actionHandler) { - await this.actionHandler.unmakeModerator(participant); - } else { - // Default implementation - if (!this.participantService.isModerator()) return; - - try { - await this.meetingService.changeParticipantRole( - this.roomId, - participant.identity, - MeetRoomMemberRole.SPEAKER - ); - console.log('Moderator unassigned successfully'); - } catch (error) { - console.error('Error unassigning moderator:', error); - } - } - } - - // private async handleCopyModeratorLink() { - // if (this.actionHandler) { - // await this.actionHandler.copyModeratorLink(); - // } else { - // // Default implementation - // try { - // this.clipboard.copy(this.room!.moderatorUrl); - // this.notificationService.showSnackbar('Moderator link copied to clipboard'); - - // console.log('Moderator link copied to clipboard'); - // } catch (error) { - // console.error('Failed to copy moderator link:', error); - // } - // } - // } - - protected async handleCopySpeakerLink() { - if (this.actionHandler) { - await this.actionHandler.copySpeakerLink(); - } else { - // Default implementation - try { - const speakerLink = this.room!.speakerUrl; - this.clipboard.copy(speakerLink); - this.notificationService.showSnackbar('Speaker link copied to clipboard'); - console.log('Speaker link copied to clipboard'); - } catch (error) { - console.error('Failed to copy speaker link:', error); - } + localParticipant.setAllVideoPinned(false); } } } diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/pages/room-recordings/room-recordings.component.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/pages/room-recordings/room-recordings.component.ts index 90b90e46..3f18294c 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/pages/room-recordings/room-recordings.component.ts +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/pages/room-recordings/room-recordings.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit, signal } from '@angular/core'; +import { Component, inject, OnInit, signal } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; @@ -8,11 +8,11 @@ import { MeetRecordingFilters, MeetRecordingInfo } from '@openvidu-meet/typings' import { ILogger, LoggerService } from 'openvidu-components-angular'; import { RecordingListsComponent, RecordingTableAction } from '../../components'; import { + MeetingContextService, NavigationService, NotificationService, RecordingService, - RoomMemberService, - RoomService + RoomMemberService } from '../../services'; @Component({ @@ -38,15 +38,15 @@ export class RoomRecordingsComponent implements OnInit { protected log: ILogger; - constructor( - protected loggerService: LoggerService, - protected recordingService: RecordingService, - protected roomService: RoomService, - protected roomMemberService: RoomMemberService, - protected notificationService: NotificationService, - protected navigationService: NavigationService, - protected route: ActivatedRoute - ) { + protected readonly loggerService = inject(LoggerService); + protected readonly recordingService = inject(RecordingService); + protected readonly roomMemberService = inject(RoomMemberService); + protected readonly notificationService = inject(NotificationService); + protected readonly navigationService = inject(NavigationService); + protected readonly meetingContextService = inject(MeetingContextService); + protected readonly route = inject(ActivatedRoute); + + constructor() { this.log = this.loggerService.get('OpenVidu Meet - RoomRecordingsComponent'); } @@ -76,8 +76,10 @@ export class RoomRecordingsComponent implements OnInit { async goBackToRoom() { try { + const roomSecret = this.meetingContextService.roomSecret(); + if (!roomSecret) throw new Error('Cannot navigate back to room: room secret is undefined'); await this.navigationService.navigateTo(`/room/${this.roomId}`, { - secret: this.roomService.getRoomSecret() + secret: roomSecret }); } catch (error) { this.log.e('Error navigating back to room:', error); diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/routes/base-routes.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/routes/base-routes.ts index 3484b975..4b360325 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/routes/base-routes.ts +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/routes/base-routes.ts @@ -1,7 +1,7 @@ import { Routes } from '@angular/router'; import { WebComponentProperty } from '@openvidu-meet/typings'; import { - checkRoomEditGuard, + checkEditableRoomGuard, checkUserAuthenticatedGuard, checkUserNotAuthenticatedGuard, extractRecordingQueryParamsGuard, @@ -89,7 +89,7 @@ export const baseRoutes: Routes = [ { path: 'rooms/:roomId/edit', component: RoomWizardComponent, - canActivate: [checkRoomEditGuard] + canActivate: [checkEditableRoomGuard] }, { path: 'recordings', diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/services/index.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/services/index.ts index 10883b32..1952af1f 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/services/index.ts +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/services/index.ts @@ -7,8 +7,8 @@ export * from './room.service'; export * from './room-member.service'; export * from './meeting/meeting.service'; export * from './meeting/meeting-lobby.service'; -export * from './meeting/meeting-plugin-manager.service'; export * from './meeting/meeting-event-handler.service'; +export * from './meeting/meeting-context.service'; export * from './feature-configuration.service'; export * from './recording.service'; export * from './webcomponent-manager.service'; diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/services/meeting/meeting-context.service.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/services/meeting/meeting-context.service.ts new file mode 100644 index 00000000..52237baf --- /dev/null +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/services/meeting/meeting-context.service.ts @@ -0,0 +1,234 @@ +import { Injectable, signal, computed, inject, DestroyRef } from '@angular/core'; +import { MeetRoom } from 'node_modules/@openvidu-meet/typings/dist/room'; +import { Room, ParticipantService, ViewportService } from 'openvidu-components-angular'; +import { CustomParticipantModel } from '../../models'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { FeatureConfigurationService } from '../feature-configuration.service'; + +/** + * Central service for managing meeting context and state during the MEETING PHASE. + * + * This service is the SINGLE SOURCE OF TRUTH for all meeting-related state once a participant has joined. + */ +@Injectable({ + providedIn: 'root' +}) +export class MeetingContextService { + private readonly ovParticipantService = inject(ParticipantService); + private readonly featureConfigService = inject(FeatureConfigurationService); + private readonly viewportService = inject(ViewportService); + private readonly destroyRef = inject(DestroyRef); + private isSubscribed = false; + private readonly _meetRoom = signal(undefined); + private readonly _lkRoom = signal(undefined); + private readonly _roomId = signal(undefined); + private readonly _meetingUrl = signal(''); + private readonly _e2eeKey = signal(''); + private readonly _roomSecret = signal(undefined); + private readonly _hasRecordings = signal(false); + private readonly _meetingEndedBy = signal<'self' | 'other' | null>(null); + private readonly _participantsVersion = signal(0); + private readonly _localParticipant = signal(undefined); + private readonly _remoteParticipants = signal([]); + + /** + * Readonly signal for the current room + */ + readonly meetRoom = this._meetRoom.asReadonly(); + + /** + * Readonly signal for the current room ID + */ + readonly roomId = this._roomId.asReadonly(); + + /** + * Readonly signal for the current lk room + */ + readonly lkRoom = this._lkRoom.asReadonly(); + + /** + * Readonly signal for the meeting URL + */ + readonly meetingUrl = this._meetingUrl.asReadonly(); + + /** + * Readonly signal for the E2EE key + */ + readonly e2eeKey = this._e2eeKey.asReadonly(); + + /** + * Readonly signal for the room secret + */ + readonly roomSecret = this._roomSecret.asReadonly(); + + /** + * Readonly signal for whether the room has recordings + */ + readonly hasRecordings = this._hasRecordings.asReadonly(); + + /** + * Readonly signal for who ended the meeting ('self', 'other', or null) + */ + readonly meetingEndedBy = this._meetingEndedBy.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 + */ + readonly remoteParticipants = this._remoteParticipants.asReadonly(); + + /** + * Computed signal that combines local and remote participants + */ + readonly allParticipants = computed(() => { + const local = this._localParticipant(); + const remotes = this._remoteParticipants(); + return local ? [local, ...remotes] : remotes; + }); + + /** + * Computed signal for whether the current user can moderate the room + * Derived from FeatureConfigurationService + */ + readonly canModerateRoom = computed(() => this.featureConfigService.features().canModerateRoom); + + /** + * Computed signal for whether the device is mobile + * Derived from ViewportService for responsive UI + */ + readonly isMobile = computed(() => this.viewportService.isMobile()); + + /** + * Sets the room ID in context + * @param roomId The room ID + */ + setRoomId(roomId: string): void { + this._roomId.set(roomId); + } + + /** + * Sets the meeting context with meet room information + * @param room The room object + */ + setMeetRoom(room: MeetRoom): void { + this._meetRoom.set(room); + this.setRoomId(room.roomId); + this.setMeetingUrl(room.roomId); + } + + /** + * Sets the LiveKit Room instance in context + * @param room + */ + setLkRoom(room: Room) { + this._lkRoom.set(room); + // Subscribe to participants only once when lkRoom is set + if (!this.isSubscribed) { + this.subscribeToParticipants(); + this.isSubscribed = true; + } + } + + /** + * Subscribes to local and remote participants from the OpenVidu Components ParticipantService + */ + protected subscribeToParticipants(): void { + this.ovParticipantService.localParticipant$ + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe((participant) => { + this._localParticipant.set(participant as CustomParticipantModel); + }); + + this.ovParticipantService.remoteParticipants$ + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe((participants) => { + this._remoteParticipants.set(participants as CustomParticipantModel[]); + }); + } + + /** + * Updates the meeting URL based on room ID + * @param roomId The room ID + */ + private setMeetingUrl(roomId: string): void { + const hostname = window.location.origin.replace('http://', '').replace('https://', ''); + const meetingUrl = roomId ? `${hostname}/room/${roomId}` : ''; + this._meetingUrl.set(meetingUrl); + } + + /** + * Stores the E2EE key in context + * @param key The E2EE key + */ + setE2eeKey(key: string): void { + this._e2eeKey.set(key); + } + + /** + * Returns whether E2EE is enabled (has a key set) + * @returns true if E2EE is enabled, false otherwise + */ + isE2eeEnabled(): boolean { + return this._e2eeKey().length > 0; + } + + /** + * Sets the room secret in context + * @param secret The room secret + */ + setRoomSecret(secret: string): void { + this._roomSecret.set(secret); + } + + /** + * Updates whether the room has recordings + * @param hasRecordings True if recordings exist + */ + setHasRecordings(hasRecordings: boolean): void { + this._hasRecordings.set(hasRecordings); + } + + /** + * Sets who ended the meeting + * @param by 'self' if ended by this user, 'other' if ended by someone else + */ + setMeetingEndedBy(by: 'self' | 'other' | null): void { + this._meetingEndedBy.set(by); + } + + /** + * Increments the participants version counter + * Used to trigger reactivity when participant properties (like role) change + */ + incrementParticipantsVersion(): void { + this._participantsVersion.update((v) => v + 1); + } + + /** + * Clears the meeting context + */ + clearContext(): void { + this._meetRoom.set(undefined); + this._lkRoom.set(undefined); + this._roomId.set(undefined); + this._meetingUrl.set(''); + this._e2eeKey.set(''); + this._roomSecret.set(undefined); + this._hasRecordings.set(false); + this._meetingEndedBy.set(null); + this._participantsVersion.set(0); + this._localParticipant.set(undefined); + this._remoteParticipants.set([]); + this.isSubscribed = false; + } +} diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/services/meeting/meeting-event-handler.service.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/services/meeting/meeting-event-handler.service.ts index c7a1ac18..75111fa2 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/services/meeting/meeting-event-handler.service.ts +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/services/meeting/meeting-event-handler.service.ts @@ -21,10 +21,10 @@ import { import { CustomParticipantModel } from '../../models'; import { FeatureConfigurationService, + MeetingContextService, NavigationService, RecordingService, RoomMemberService, - RoomService, SessionStorageService, TokenStorageService, WebComponentManagerService @@ -33,39 +33,20 @@ import { /** * Service that handles all LiveKit/OpenVidu room events. * - * This service encapsulates all event handling logic previously in MeetingComponent, - * providing a clean separation of concerns and making the component more maintainable. - * - * Responsibilities: - * - Setup and manage room event listeners - * - Handle data received events (recording stopped, config updates, role changes) - * - Handle participant lifecycle events (connected, left) - * - Handle recording events (start, stop) - * - Map technical reasons to user-friendly reasons - * - Manage meeting ended state - * - Navigate to disconnected page with appropriate reason - * - * Benefits: - * - Reduces MeetingComponent size by ~200 lines - * - All event logic in one place (easier to test and maintain) - * - Clear API for event handling - * - Reusable across different components if needed + * This service encapsulates all event handling logic and updates the MeetingContextService + * as the single source of truth for meeting state. */ @Injectable() export class MeetingEventHandlerService { - // Injected services + protected meetingContext = inject(MeetingContextService); protected featureConfService = inject(FeatureConfigurationService); protected recordingService = inject(RecordingService); protected roomMemberService = inject(RoomMemberService); - protected roomService = inject(RoomService); protected sessionStorageService = inject(SessionStorageService); protected tokenStorageService = inject(TokenStorageService); protected wcManagerService = inject(WebComponentManagerService); protected navigationService = inject(NavigationService); - // Internal state - private meetingEndedByMe = false; - // ============================================ // PUBLIC METHODS - Room Event Handlers // ============================================ @@ -75,21 +56,16 @@ export class MeetingEventHandlerService { * This is the main entry point for room event handling. * * @param room The LiveKit Room instance - * @param context Context object containing all necessary data and callbacks */ - setupRoomListeners( - room: Room, - context: { - roomId: string; - roomSecret: string; - participantName: string; - localParticipant: () => CustomParticipantModel | undefined; - remoteParticipants: () => CustomParticipantModel[]; - onHasRecordingsChanged: (hasRecordings: boolean) => void; - onRoomSecretChanged: (secret: string) => void; - onParticipantRoleUpdated?: () => void; - } - ): void { + 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) => { @@ -109,24 +85,17 @@ export class MeetingEventHandlerService { switch (topic) { case 'recordingStopped': - // Notify that recordings are now available - context.onHasRecordingsChanged(true); + // Update hasRecordings in MeetingContextService + this.meetingContext.setHasRecordings(true); break; case MeetSignalType.MEET_ROOM_CONFIG_UPDATED: - await this.handleRoomConfigUpdated(event, context.roomId, context.roomSecret); + // Room cannot be updated if a meeting is ongoing + // await this.handleRoomConfigUpdated(event); break; case MeetSignalType.MEET_PARTICIPANT_ROLE_UPDATED: - await this.handleParticipantRoleUpdated( - event, - context.roomId, - context.participantName, - context.localParticipant, - context.remoteParticipants, - context.onRoomSecretChanged, - context.onParticipantRoleUpdated - ); + await this.handleParticipantRoleUpdated(event); break; } } catch (error) { @@ -170,7 +139,8 @@ export class MeetingEventHandlerService { let leftReason = this.mapLeftReason(event.reason); // If meeting was ended by this user, update reason - if (leftReason === LeftEventReason.MEETING_ENDED && this.meetingEndedByMe) { + const meetingEndedBy = this.meetingContext.meetingEndedBy(); + if (leftReason === LeftEventReason.MEETING_ENDED && meetingEndedBy === 'self') { leftReason = LeftEventReason.MEETING_ENDED_BY_SELF; } @@ -236,16 +206,6 @@ export class MeetingEventHandlerService { } }; - /** - * Sets the "meeting ended by me" flag. - * This is used to differentiate between meeting ended by this user vs ended by someone else. - * - * @param value True if this user ended the meeting - */ - setMeetingEndedByMe(value: boolean): void { - this.meetingEndedByMe = value; - } - // ============================================ // PRIVATE METHODS - Event Handlers // ============================================ @@ -253,12 +213,9 @@ export class MeetingEventHandlerService { /** * 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, - roomId: string, - roomSecret: string - ): Promise { + private async handleRoomConfigUpdated(event: MeetRoomConfigUpdatedPayload): Promise { const { config } = event; // Update feature configuration @@ -267,8 +224,16 @@ export class MeetingEventHandlerService { // 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, @@ -284,26 +249,21 @@ export class MeetingEventHandlerService { /** * Handles participant role updated event. * Updates local or remote participant role and refreshes room member token if needed. + * Obtains all necessary data from MeetingContextService. */ - private async handleParticipantRoleUpdated( - event: MeetParticipantRoleUpdatedPayload, - roomId: string, - participantName: string, - localParticipant: () => CustomParticipantModel | undefined, - remoteParticipants: () => CustomParticipantModel[], - onRoomSecretChanged: (secret: string) => void, - onParticipantRoleUpdated?: () => void - ): Promise { + private async handleParticipantRoleUpdated(event: MeetParticipantRoleUpdatedPayload): Promise { const { participantIdentity, newRole, secret } = event; - const local = localParticipant(); + const roomId = this.meetingContext.roomId(); + const local = this.meetingContext.localParticipant(); + const participantName = this.roomMemberService.getParticipantName(); // Check if the role update is for the local participant if (local && participantIdentity === local.identity) { - if (!secret) return; + if (!secret || !roomId) return; - // Update room secret - onRoomSecretChanged(secret); - this.roomService.setRoomSecret(secret, false); + // Update room secret in context + this.meetingContext.setRoomSecret(secret); + this.sessionStorageService.setRoomSecret(secret); try { // Refresh participant token with new role @@ -318,19 +278,20 @@ export class MeetingEventHandlerService { local.meetRole = newRole; console.log(`You have been assigned the role of ${newRole}`); - // Notify component that participant role was updated - onParticipantRoleUpdated?.(); + // Increment version to trigger reactivity + this.meetingContext.incrementParticipantsVersion(); } catch (error) { console.error('Error refreshing room member token:', error); } } else { // Update remote participant role - const participant = remoteParticipants().find((p) => p.identity === participantIdentity); + const remoteParticipants = this.meetingContext.remoteParticipants(); + const participant = remoteParticipants.find((p) => p.identity === participantIdentity); if (participant) { participant.meetRole = newRole; - // Notify component that participant role was updated - onParticipantRoleUpdated?.(); + // Increment version to trigger reactivity + this.meetingContext.incrementParticipantsVersion(); } } } diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/services/meeting/meeting-lobby.service.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/services/meeting/meeting-lobby.service.ts index 439a26f6..ec2720ef 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/services/meeting/meeting-lobby.service.ts +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/services/meeting/meeting-lobby.service.ts @@ -1,10 +1,12 @@ -import { inject, Injectable } from '@angular/core'; +import { computed, inject, Injectable, signal } from '@angular/core'; import { FormControl, FormGroup, Validators } from '@angular/forms'; import { ActivatedRoute } from '@angular/router'; import { MeetRoomStatus } from '@openvidu-meet/typings'; import { AppDataService, AuthService, + MeetingContextService, + MeetingService, NavigationService, RecordingService, RoomMemberService, @@ -13,104 +15,182 @@ import { } from '..'; import { ErrorReason } from '../../models'; import { LobbyState } from '../../models/lobby.model'; +import { LoggerService } from 'openvidu-components-angular'; /** - * Service that manages the meeting lobby state and operations. + * Service that manages the meeting lobby phase state and operations. * - * Responsibilities: - * - Initialize and maintain lobby state - * - Validate participant information - * - Check for recordings availability - * - Handle navigation (back button, recordings) + * This service is ONLY responsible for the LOBBY PHASE - the period before a participant joins the meeting. * - * This service coordinates multiple domain services to provide - * a simplified interface for the MeetingComponent. */ @Injectable() export class MeetingLobbyService { - private state: LobbyState = { - roomId: '', - roomSecret: '', + /** + * Reactive signal for lobby state. + * This state is only relevant during the lobby phase. + */ + private readonly _state = signal({ + roomId: undefined, roomClosed: false, - hasRecordings: false, showRecordingCard: false, showBackButton: true, backButtonText: 'Back', - isE2EEEnabled: false, + hasRoomE2EEEnabled: false, participantForm: new FormGroup({ name: new FormControl('', [Validators.required]), e2eeKey: new FormControl('') }), - roomMemberToken: '' - }; + roomMemberToken: undefined + }); protected roomService: RoomService = inject(RoomService); + protected meetingContextService: MeetingContextService = inject(MeetingContextService); + protected meetingService: MeetingService = inject(MeetingService); protected recordingService: RecordingService = inject(RecordingService); protected authService: AuthService = inject(AuthService); protected roomMemberService: RoomMemberService = inject(RoomMemberService); protected navigationService: NavigationService = inject(NavigationService); protected appDataService: AppDataService = inject(AppDataService); protected wcManagerService: WebComponentManagerService = inject(WebComponentManagerService); + protected loggerService = inject(LoggerService); + protected log = this.loggerService.get('OpenVidu Meet - MeetingLobbyService'); protected route: ActivatedRoute = inject(ActivatedRoute); /** - * Gets the current lobby state + * Readonly signal for lobby state - components can use computed() with this */ - get lobbyState(): LobbyState { - return this.state; - } + readonly state = this._state.asReadonly(); - set participantName(name: string) { - this.state.participantForm.get('name')?.setValue(name); - } + /** + * Computed signal for meeting URL derived from MeetingContextService + * This ensures a single source of truth for the meeting URL + */ + readonly meetingUrl = computed(() => this.meetingContextService.meetingUrl()); - get participantName(): string { - const { valid, value } = this.state.participantForm; + /** + * Computed signal for whether the current user can moderate the room + * Derived from MeetingContextService + */ + readonly canModerateRoom = computed(() => this.meetingContextService.canModerateRoom()); + + /** + * Computed signal for participant name - optimized to avoid repeated form access + */ + readonly participantName = computed(() => { + const { valid, value } = this._state().participantForm; if (!valid || !value.name?.trim()) { return ''; } return value.name.trim(); - } + }); - set e2eeKey(key: string) { - this.state.participantForm.get('e2eeKey')?.setValue(key); - } - - get e2eeKey(): string { - const { valid, value } = this.state.participantForm; + /** + * Computed signal for E2EE key - optimized to avoid repeated form access + */ + readonly e2eeKeyValue = computed(() => { + const { valid, value } = this._state().participantForm; if (!valid || !value.e2eeKey?.trim()) { return ''; } return value.e2eeKey.trim(); + }); + + /** + * Computed signal for room member token + */ + readonly roomMemberToken = computed(() => this._state().roomMemberToken); + + /** + * Computed signal for room ID + */ + readonly roomId = computed(() => this._state().roomId); + + /** + * Computed signal for room secret. + * Obtained from MeetingContextService + */ + readonly roomSecret = computed(() => this.meetingContextService.roomSecret()); + + /** + * Computed signal for room name + */ + readonly roomName = computed(() => this._state().room?.roomName); + + /** + * Computed signal for has recordings. + * Obtained from MeetingContextService + */ + readonly hasRecordings = computed(() => this.meetingContextService.hasRecordings()); + + /** + * Setter for participant name + */ + setParticipantName(name: string): void { + this._state().participantForm.get('name')?.setValue(name); + } + + /** + * Setter for E2EE key + */ + setE2eeKey(key: string): void { + this._state().participantForm.get('e2eeKey')?.setValue(key); } /** * Initializes the lobby state by fetching room data and configuring UI */ - async initialize(): Promise { - this.state.roomId = this.roomService.getRoomId(); - this.state.roomSecret = this.roomService.getRoomSecret(); - this.state.room = await this.roomService.getRoom(this.state.roomId); - this.state.roomClosed = this.state.room.status === MeetRoomStatus.CLOSED; - this.state.isE2EEEnabled = this.state.room.config.e2ee.enabled; + async initialize(): Promise { + try { + const roomId = this.meetingContextService.roomId(); + if (!roomId) throw new Error('Room ID is not set in Meeting Context'); - // If E2EE is enabled, require e2eeKey - if (this.state.isE2EEEnabled) { - this.state.participantForm.get('e2eeKey')?.setValidators([Validators.required]); - this.e2eeKey = this.roomService.getE2EEKey(); + this._state.update((state) => ({ ...state, roomId })); - if (this.e2eeKey) { - // when e2eeKey is already set (e.g., from URL or webcomponent), populate and disable field - this.state.participantForm.get('e2eeKey')?.disable(); + const [room] = await Promise.all([ + this.roomService.getRoom(roomId), + this.setBackButtonText(), + this.checkForRecordings(), + this.initializeParticipantName() + ]); + + const roomClosed = room.status === MeetRoomStatus.CLOSED; + const hasRoomE2EEEnabled = room.config?.e2ee?.enabled || false; + + this._state.update((state) => ({ + ...state, + room, + roomClosed, + hasRoomE2EEEnabled + })); + + this.meetingContextService.setMeetRoom(room); + + if (hasRoomE2EEEnabled) { + // If E2EE is enabled, make the e2eeKey form control required + const form = this._state().participantForm; + form.get('e2eeKey')?.setValidators([Validators.required]); + const contextE2eeKey = this.meetingContextService.e2eeKey(); + if (contextE2eeKey) { + this.setE2eeKey(contextE2eeKey); + // fill the e2eeKey form control if already set in context (e.g., from URL param) + form.get('e2eeKey')?.disable(); + } + form.get('e2eeKey')?.updateValueAndValidity(); } - this.state.participantForm.get('e2eeKey')?.updateValueAndValidity(); + } catch (error) { + this.clearLobbyState(); + throw error; } + } - await this.setBackButtonText(); - await this.checkForRecordings(); - await this.initializeParticipantName(); - - return this.state; + /** + * Copies the meeting speaker link to clipboard + */ + copyMeetingSpeakerLink() { + const { room } = this.state(); + if (room) { + this.meetingService.copyMeetingSpeakerLink(room); + } } /** @@ -137,7 +217,7 @@ export class MeetingLobbyService { await this.navigationService.navigateTo('/rooms'); } } catch (error) { - console.error('Error handling back navigation:', error); + this.log.e('Error handling back navigation:', error); } } @@ -146,36 +226,43 @@ export class MeetingLobbyService { */ async goToRecordings(): Promise { try { - await this.navigationService.navigateTo(`room/${this.state.roomId}/recordings`, { - secret: this.state.roomSecret + const roomId = this._state().roomId; + const roomSecret = this.meetingContextService.roomSecret(); + await this.navigationService.navigateTo(`room/${roomId}/recordings`, { + secret: roomSecret }); } catch (error) { - console.error('Error navigating to recordings:', error); + this.log.e('Error navigating to recordings:', error); } } async submitAccess(): Promise { - const sanitized = this.participantName.trim(); // remove leading/trailing spaces + const sanitized = this.participantName().trim(); // remove leading/trailing spaces if (!sanitized) { - console.error('Participant form is invalid. Cannot access meeting.'); + this.log.e('Participant form is invalid. Cannot access meeting.'); throw new Error('Participant form is invalid'); } - this.participantName = sanitized; + this.setParticipantName(sanitized); // For E2EE rooms, validate passkey - if (this.state.isE2EEEnabled && !this.e2eeKey) { - console.warn('E2EE key is required for encrypted rooms.'); - return; + const { hasRoomE2EEEnabled, roomId } = this._state(); + if (hasRoomE2EEEnabled) { + const e2eeKey = this.e2eeKeyValue(); + if (!e2eeKey) { + this.log.w('E2EE key is required for encrypted rooms.'); + return; + } + this.meetingContextService.setE2eeKey(e2eeKey); } - await this.generateRoomMemberToken(); - await this.addParticipantNameToUrl(); - await this.roomService.loadRoomConfig(this.state.roomId); + await Promise.all([ + this.generateRoomMemberToken(), + this.addParticipantNameToUrl(), + this.roomService.loadRoomConfig(roomId!) + ]); } - // Protected helper methods - /** * Sets the back button text based on the application mode and user role */ @@ -185,12 +272,12 @@ export class MeetingLobbyService { const isAdmin = await this.authService.isAdmin(); if (isStandaloneMode && !redirection && !isAdmin) { - this.state.showBackButton = false; + this._state.update((state) => ({ ...state, showBackButton: false })); return; } - this.state.showBackButton = true; - this.state.backButtonText = isStandaloneMode && !redirection && isAdmin ? 'Back to Rooms' : 'Back'; + const backButtonText = isStandaloneMode && !redirection && isAdmin ? 'Back to Rooms' : 'Back'; + this._state.update((state) => ({ ...state, showBackButton: true, backButtonText })); } /** @@ -199,28 +286,38 @@ export class MeetingLobbyService { * If the user does not have sufficient permissions to list recordings, * the recordings card will be hidden (`showRecordingCard` will be set to `false`). * - * If recordings exist, sets `showRecordingCard` to `true`; otherwise, to `false`. + * If recordings exist, stores in MeetingContextService and shows recording card UI. */ protected async checkForRecordings(): Promise { try { const canRetrieveRecordings = this.roomMemberService.canRetrieveRecordings(); if (!canRetrieveRecordings) { - this.state.showRecordingCard = false; + this._state.update((state) => ({ ...state, showRecordingCard: false })); return; } + const { roomId } = this._state(); + if (!roomId) throw new Error('Room ID is not set in lobby state'); const { recordings } = await this.recordingService.listRecordings({ maxItems: 1, - roomId: this.state.roomId, + roomId, fields: 'recordingId' }); - this.state.hasRecordings = recordings.length > 0; - this.state.showRecordingCard = this.state.hasRecordings; + const hasRecordings = recordings.length > 0; + + // Store in MeetingContextService (Single Source of Truth) + this.meetingContextService.setHasRecordings(hasRecordings); + + // Update only UI flag locally + this._state.update((state) => ({ + ...state, + showRecordingCard: hasRecordings + })); } catch (error) { - console.error('Error checking for recordings:', error); - this.state.showRecordingCard = false; + this.log.e('Error checking for recordings:', error); + this._state.update((state) => ({ ...state, showRecordingCard: false })); } } @@ -240,7 +337,7 @@ export class MeetingLobbyService { const participantName = currentParticipantName || username; if (participantName) { - this.participantName = participantName; + this.setParticipantName(participantName); } } @@ -251,18 +348,22 @@ export class MeetingLobbyService { */ protected async generateRoomMemberToken() { try { - this.state.roomMemberToken = await this.roomMemberService.generateToken( - this.state.roomId, + const roomId = this._state().roomId; + const roomSecret = this.meetingContextService.roomSecret(); + const roomMemberToken = await this.roomMemberService.generateToken( + roomId!, { - secret: this.state.roomSecret, + secret: roomSecret!, grantJoinMeetingPermission: true, - participantName: this.participantName + participantName: this.participantName() }, - this.e2eeKey + this.e2eeKeyValue() ); - this.participantName = this.roomMemberService.getParticipantName()!; + const updatedName = this.roomMemberService.getParticipantName()!; + this.setParticipantName(updatedName); + this._state.update((state) => ({ ...state, roomMemberToken })); } catch (error: any) { - console.error('Error generating room member token:', error); + this.log.e('Error generating room member token:', error); switch (error.status) { case 400: // Invalid secret @@ -289,7 +390,23 @@ export class MeetingLobbyService { */ protected async addParticipantNameToUrl() { await this.navigationService.updateQueryParamsFromUrl(this.route.snapshot.queryParams, { - 'participant-name': this.participantName + 'participant-name': this.participantName() + }); + } + + protected clearLobbyState() { + this._state.set({ + roomId: undefined, + roomClosed: false, + showRecordingCard: false, + showBackButton: true, + backButtonText: 'Back', + hasRoomE2EEEnabled: false, + participantForm: new FormGroup({ + name: new FormControl('', [Validators.required]), + e2eeKey: new FormControl('') + }), + roomMemberToken: undefined }); } } diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/services/meeting/meeting-plugin-manager.service.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/services/meeting/meeting-plugin-manager.service.ts deleted file mode 100644 index 6640fd77..00000000 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/services/meeting/meeting-plugin-manager.service.ts +++ /dev/null @@ -1,203 +0,0 @@ -import { Inject, Injectable, Optional } from '@angular/core'; -import { MEETING_ACTION_HANDLER_TOKEN, MeetingActionHandler, ParticipantControls } from '../../customization'; -import { CustomParticipantModel, LobbyState } from '../../models'; -import { RoomMemberService } from '../room-member.service'; - -/** - * Service that manages plugin inputs and configurations for the MeetingComponent. - * - * Responsibilities: - * - Prepare input objects for toolbar plugins - * - Prepare input objects for participant panel plugins - * - Prepare input objects for layout plugins - * - Prepare input objects for lobby plugin - * - Calculate participant control visibility based on roles and permissions - * - * This service acts as a bridge between the MeetingComponent and the plugin components, - * encapsulating the logic for determining what inputs each plugin should receive. - */ -@Injectable() -export class MeetingPluginManagerService { - constructor( - private roomMemberService: RoomMemberService, - @Optional() @Inject(MEETING_ACTION_HANDLER_TOKEN) private actionHandler?: MeetingActionHandler - ) {} - - /** - * Prepares inputs for the toolbar additional buttons plugin - */ - getToolbarAdditionalButtonsInputs(canModerateRoom: boolean, isMobile: boolean, onCopyLink: () => void) { - return { - showCopyLinkButton: canModerateRoom, - showLeaveMenu: false, - isMobile, - copyLinkClickedFn: onCopyLink - }; - } - - /** - * Prepares inputs for the toolbar leave button plugin - */ - getToolbarLeaveButtonInputs( - canModerateRoom: boolean, - isMobile: boolean, - onLeave: () => Promise, - onEnd: () => Promise - ) { - return { - showCopyLinkButton: false, - showLeaveMenu: canModerateRoom, - isMobile, - leaveMeetingClickedFn: onLeave, - endMeetingClickedFn: onEnd - }; - } - - /** - * Prepares inputs for the participant panel "after local participant" plugin - */ - getParticipantPanelAfterLocalInputs(canModerateRoom: boolean, meetingUrl: string, onCopyLink: () => void) { - return { - showShareLink: canModerateRoom, - meetingUrl, - copyClickedFn: onCopyLink - }; - } - - /** - * Prepares inputs for the layout component (CE or PRO) - * - * This method prepares all inputs needed by the layout component including: - * - Additional elements component to be rendered inside the layout - * - Inputs object to pass to the additional elements component - */ - getLayoutInputs( - showOverlay: boolean, - meetingUrl: string, - onCopyLink: () => void, - additionalElementsComponent?: any - ) { - return { - additionalElementsComponent, - additionalElementsInputs: { - showOverlay, - meetingUrl, - copyClickedFn: onCopyLink - } - }; - } - - /** - * Prepares inputs for the participant panel item plugin - */ - getParticipantPanelItemInputs( - participant: CustomParticipantModel, - allParticipants: CustomParticipantModel[], - onMakeModerator: (p: CustomParticipantModel) => void, - onUnmakeModerator: (p: CustomParticipantModel) => void, - onKick: (p: CustomParticipantModel) => void - ) { - const controls = this.getParticipantControls(participant); - - return { - participant, - allParticipants, - showModeratorBadge: controls.showModeratorBadge, - showModerationControls: controls.showModerationControls, - showMakeModerator: controls.showMakeModerator, - showUnmakeModerator: controls.showUnmakeModerator, - showKickButton: controls.showKickButton, - makeModeratorClickedFn: () => onMakeModerator(participant), - unmakeModeratorClickedFn: () => onUnmakeModerator(participant), - kickParticipantClickedFn: () => onKick(participant) - }; - } - - /** - * Prepares inputs for the lobby plugin - */ - getLobbyInputs( - lobbyState: LobbyState, - hostname: string, - canModerateRoom: boolean, - onFormSubmit: () => void, - onViewRecordings: () => void, - onBack: () => void, - onCopyLink: () => void - ) { - const { - room, - roomId, - roomClosed, - showRecordingCard, - showBackButton, - backButtonText, - isE2EEEnabled, - participantForm - } = lobbyState; - const meetingUrl = `${hostname}/room/${roomId}`; - const showShareLink = !roomClosed && canModerateRoom; - - return { - roomName: room?.roomName || 'Room', - meetingUrl, - roomClosed, - showRecordingCard, - showShareLink, - showBackButton, - backButtonText, - isE2EEEnabled, - participantForm, - formSubmittedFn: onFormSubmit, - viewRecordingsClickedFn: onViewRecordings, - backClickedFn: onBack, - copyLinkClickedFn: onCopyLink - }; - } - - /** - * Gets participant controls based on action handler or default logic - */ - private getParticipantControls(participant: CustomParticipantModel): ParticipantControls { - if (this.actionHandler) { - return this.actionHandler.getParticipantControls(participant); - } - - // Default implementation - return this.getDefaultParticipantControls(participant); - } - - /** - * Default implementation for calculating participant control visibility. - * - * Rules: - * - Only moderators can see moderation controls - * - Local participant never sees controls on themselves - * - A moderator who was promoted (not original) cannot remove the moderator role from original moderators - * - A moderator who was promoted (not original) cannot kick original moderators - * - The moderator badge is shown based on the current role, not original role - */ - protected getDefaultParticipantControls(participant: CustomParticipantModel): ParticipantControls { - const isCurrentUser = participant.isLocal; - const currentUserIsModerator = this.roomMemberService.isModerator(); - const participantIsModerator = participant.isModerator(); - const participantIsOriginalModerator = participant.isOriginalModerator(); - - // Calculate if current moderator can revoke the moderator role from the target participant - // Only allow if target is not an original moderator - const canRevokeModeratorRole = - currentUserIsModerator && !isCurrentUser && participantIsModerator && !participantIsOriginalModerator; - - // Calculate if current moderator can kick the target participant - // Only allow if target is not an original moderator - const canKickParticipant = currentUserIsModerator && !isCurrentUser && !participantIsOriginalModerator; - - return { - showModeratorBadge: participantIsModerator, - showModerationControls: currentUserIsModerator && !isCurrentUser, - showMakeModerator: currentUserIsModerator && !isCurrentUser && !participantIsModerator, - showUnmakeModerator: canRevokeModeratorRole, - showKickButton: canKickParticipant - }; - } -} diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/services/meeting/meeting.service.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/services/meeting/meeting.service.ts index 0fddd420..b117d4c1 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/services/meeting/meeting.service.ts +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/services/meeting/meeting.service.ts @@ -1,20 +1,30 @@ -import { Injectable } from '@angular/core'; +import { inject, Injectable } from '@angular/core'; +import { Clipboard } from '@angular/cdk/clipboard'; import { LoggerService } from 'openvidu-components-angular'; -import { HttpService } from '..'; +import { HttpService } from '../http.service'; +import { MeetRoom } from 'node_modules/@openvidu-meet/typings/dist/room'; +import { NotificationService } from '../notification.service'; @Injectable({ providedIn: 'root' }) export class MeetingService { protected readonly MEETINGS_API = `${HttpService.INTERNAL_API_PATH_PREFIX}/meetings`; + protected loggerService: LoggerService = inject(LoggerService); + protected notificationService = inject(NotificationService); - protected log; + protected httpService: HttpService = inject(HttpService); + protected clipboard = inject(Clipboard); - constructor( - protected loggerService: LoggerService, - protected httpService: HttpService - ) { - this.log = this.loggerService.get('OpenVidu Meet - MeetingService'); + protected log = this.loggerService.get('OpenVidu Meet - MeetingService'); + + /** + * Copies the meeting speaker link to the clipboard. + */ + copyMeetingSpeakerLink(room: MeetRoom): void { + const speakerLink = room.speakerUrl; + this.clipboard.copy(speakerLink); + this.notificationService.showSnackbar('Speaker link copied to clipboard'); } /** diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/services/room.service.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/services/room.service.ts index 526e24dc..467dca42 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/services/room.service.ts +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/services/room.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@angular/core'; +import { inject, Injectable } from '@angular/core'; import { MeetRoom, MeetRoomConfig, @@ -11,8 +11,19 @@ import { MeetRoomStatus } from '@openvidu-meet/typings'; import { LoggerService } from 'openvidu-components-angular'; -import { FeatureConfigurationService, HttpService, SessionStorageService } from '../services'; +import { FeatureConfigurationService, HttpService, MeetingContextService, SessionStorageService } from '../services'; +/** + * RoomService - Persistence Layer for Room Data + * + * This service acts as a PERSISTENCE LAYER for room-related data and CRUD operations. + * + * Responsibilities: + * - Persist room data (roomId, roomSecret) in SessionStorage for page refresh/reload + * - Automatically sync persisted data to MeetingContextService (Single Source of Truth) + * - Provide HTTP API methods for room CRUD operations + * - Load and update room configuration + */ @Injectable({ providedIn: 'root' }) @@ -23,8 +34,8 @@ export class RoomService { protected roomId: string = ''; protected roomSecret: string = ''; protected e2eeKey: string = ''; - protected log; + protected meetingContext = inject(MeetingContextService); constructor( protected loggerService: LoggerService, @@ -35,32 +46,47 @@ export class RoomService { this.log = this.loggerService.get('OpenVidu Meet - RoomService'); } + /** + * Stores the room ID in memory and automatically syncs to MeetingContextService. + * This ensures persistence and reactivity across the application. + * + * @param roomId - The room identifier to store + */ setRoomId(roomId: string) { this.roomId = roomId; + // Auto-sync to MeetingContextService (Single Source of Truth for runtime) + this.meetingContext.setRoomId(roomId); } - getRoomId(): string { - return this.roomId; + /** + * Loads persisted room state from internal storage to MeetingContextService. + * Should be called during application initialization to restore state after page reload. + * + * This method transfers data from RoomService → MeetingContextService, + * making it available as reactive signals throughout the application. + */ + loadPersistedStateToContext(): void { + if (this.roomId) { + this.meetingContext.setRoomId(this.roomId); + } + if (this.roomSecret) { + this.meetingContext.setRoomSecret(this.roomSecret); + } } + /** + * Stores the room secret in memory, session storage, and automatically syncs to MeetingContextService. + * + * @param secret - The room secret to store + * @param updateStorage - Whether to persist in SessionStorage (default: true) + */ setRoomSecret(secret: string, updateStorage = true) { this.roomSecret = secret; if (updateStorage) { this.sessionStorageService.setRoomSecret(secret); } - } - - getRoomSecret(): string { - return this.roomSecret; - } - - setE2EEKey(e2eeKey: string) { - this.e2eeKey = e2eeKey; - this.sessionStorageService.setE2EEKey(e2eeKey); - } - - getE2EEKey(): string { - return this.e2eeKey; + // Auto-sync to MeetingContextService (Single Source of Truth for runtime) + this.meetingContext.setRoomSecret(secret); } /** @@ -211,7 +237,7 @@ export class RoomService { try { const config = await this.getRoomConfig(roomId); this.featureConfService.setRoomConfig(config); - console.log('Room config loaded:', config); + this.log.d('Room config loaded:', config); } catch (error) { this.log.e('Error loading room config', error); throw new Error('Failed to load room config'); diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/services/webcomponent-manager.service.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/services/webcomponent-manager.service.ts index abfd95bf..0f7af349 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/services/webcomponent-manager.service.ts +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/services/webcomponent-manager.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@angular/core'; +import { inject, Injectable } from '@angular/core'; import { WebComponentCommand, WebComponentEvent, @@ -6,7 +6,7 @@ import { WebComponentOutboundEventMessage } from '@openvidu-meet/typings'; import { LoggerService, OpenViduService } from 'openvidu-components-angular'; -import { MeetingService, RoomMemberService, RoomService } from '../services'; +import { MeetingContextService, MeetingService, RoomMemberService } from '../services'; /** * Service to manage the commands from OpenVidu Meet WebComponent/Iframe. @@ -23,14 +23,13 @@ export class WebComponentManagerService { protected boundHandleMessage: (event: MessageEvent) => Promise; protected log; + protected readonly meetingContextService = inject(MeetingContextService); + protected readonly roomMemberService = inject(RoomMemberService); + protected readonly openviduService = inject(OpenViduService); + protected readonly meetingService = inject(MeetingService); + protected readonly loggerService = inject(LoggerService); - constructor( - protected loggerService: LoggerService, - protected roomMemberService: RoomMemberService, - protected openviduService: OpenViduService, - protected roomService: RoomService, - protected meetingService: MeetingService - ) { + constructor() { this.log = this.loggerService.get('OpenVidu Meet - WebComponentManagerService'); this.boundHandleMessage = this.handleMessage.bind(this); } @@ -122,7 +121,8 @@ export class WebComponentManagerService { try { this.log.d('Ending meeting...'); - const roomId = this.roomService.getRoomId(); + const roomId = this.meetingContextService.roomId(); + if (!roomId) throw new Error('Room ID is undefined while trying to end meeting'); await this.meetingService.endMeeting(roomId); } catch (error) { this.log.e('Error ending meeting:', error); @@ -148,7 +148,8 @@ export class WebComponentManagerService { try { this.log.d(`Kicking participant '${participantIdentity}' from the meeting...`); - const roomId = this.roomService.getRoomId(); + const roomId = this.meetingContextService.roomId(); + if (!roomId) throw new Error('Room ID is undefined while trying to kick participant'); await this.meetingService.kickParticipant(roomId, participantIdentity); } catch (error) { this.log.e(`Error kicking participant '${participantIdentity}':`, error); diff --git a/meet-ce/frontend/src/app/app.routes.ts b/meet-ce/frontend/src/app/app.routes.ts index dae811ec..7eee2888 100644 --- a/meet-ce/frontend/src/app/app.routes.ts +++ b/meet-ce/frontend/src/app/app.routes.ts @@ -1,14 +1,13 @@ import { Routes } from '@angular/router'; -import { baseRoutes, MeetingComponent } from '@openvidu-meet/shared-components'; -import { MEETING_CE_PROVIDERS } from './customization'; +import { baseRoutes } from '@openvidu-meet/shared-components'; +import { AppCeMeetingComponent } from './customization/pages/app-ce-meeting/app-ce-meeting.component'; /** * CE routes configure the plugin system using library components. - * The library's MeetingComponent uses NgComponentOutlet to render plugins dynamically. + * The library's MeetingComponent uses content projection to allow customization */ const routes = baseRoutes; const meetingRoute = routes.find((route) => route.path === 'room/:room-id')!; -meetingRoute.component = MeetingComponent; -meetingRoute.providers = MEETING_CE_PROVIDERS; +meetingRoute.component = AppCeMeetingComponent; export const ceRoutes: Routes = routes; diff --git a/meet-ce/frontend/src/app/customization/meeting-ce.providers.ts b/meet-ce/frontend/src/app/customization/meeting-ce.providers.ts deleted file mode 100644 index f6d57693..00000000 --- a/meet-ce/frontend/src/app/customization/meeting-ce.providers.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { Provider } from '@angular/core'; -import { - MEETING_COMPONENTS_TOKEN, - MeetingLobbyComponent, - MeetingParticipantPanelComponent, - MeetingShareLinkOverlayComponent, - MeetingShareLinkPanelComponent, - MeetingToolbarButtonsComponent -} from '@openvidu-meet/shared-components'; - -/** - * CE Meeting Providers - * - * Configures the plugin system using library components directly. - * No wrappers needed - library components receive @Input properties directly through NgComponentOutlet. - * - * The library's MeetingComponent: - * - Uses NgComponentOutlet to render plugins dynamically - * - Prepares inputs via helper methods (getToolbarAdditionalButtonsInputs, etc.) - * - Passes these inputs to plugins via [ngComponentOutletInputs] - * - * CE uses library components as plugins without any customization. - * PRO will later define its own custom components to override CE behavior. - */ -export const MEETING_CE_PROVIDERS: Provider[] = [ - { - provide: MEETING_COMPONENTS_TOKEN, - useValue: { - toolbar: { - additionalButtons: MeetingToolbarButtonsComponent, - leaveButton: MeetingToolbarButtonsComponent - }, - participantPanel: { - item: MeetingParticipantPanelComponent, - afterLocalParticipant: MeetingShareLinkPanelComponent - }, - layoutAdditionalElements: MeetingShareLinkOverlayComponent, - lobby: MeetingLobbyComponent - } - } - // { - // provide: MEETING_ACTION_HANDLER, - // useValue: { - // copySpeakerLink: () => { - // console.log('Copy speaker link clicked'); - // } - // } - // } -]; diff --git a/meet-ce/frontend/src/app/customization/pages/app-ce-meeting/app-ce-meeting.component.html b/meet-ce/frontend/src/app/customization/pages/app-ce-meeting/app-ce-meeting.component.html new file mode 100644 index 00000000..dea98532 --- /dev/null +++ b/meet-ce/frontend/src/app/customization/pages/app-ce-meeting/app-ce-meeting.component.html @@ -0,0 +1,6 @@ + + + + + + diff --git a/meet-ce/frontend/src/app/customization/pages/app-ce-meeting/app-ce-meeting.component.scss b/meet-ce/frontend/src/app/customization/pages/app-ce-meeting/app-ce-meeting.component.scss new file mode 100644 index 00000000..e69de29b diff --git a/meet-ce/frontend/src/app/customization/pages/app-ce-meeting/app-ce-meeting.component.spec.ts b/meet-ce/frontend/src/app/customization/pages/app-ce-meeting/app-ce-meeting.component.spec.ts new file mode 100644 index 00000000..05ccb953 --- /dev/null +++ b/meet-ce/frontend/src/app/customization/pages/app-ce-meeting/app-ce-meeting.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { AppCeMeetingComponent } from './app-ce-meeting.component'; + +describe('AppCeMeetingComponent', () => { + let component: AppCeMeetingComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [AppCeMeetingComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(AppCeMeetingComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/meet-ce/frontend/src/app/customization/pages/app-ce-meeting/app-ce-meeting.component.ts b/meet-ce/frontend/src/app/customization/pages/app-ce-meeting/app-ce-meeting.component.ts new file mode 100644 index 00000000..bf49e764 --- /dev/null +++ b/meet-ce/frontend/src/app/customization/pages/app-ce-meeting/app-ce-meeting.component.ts @@ -0,0 +1,22 @@ +import { Component } from '@angular/core'; +import { + MeetingComponent, + MeetingLayoutComponent, + MeetingParticipantPanelItemComponent, + MeetingShareLinkPanelComponent, + MeetingToolbarButtonsComponent +} from '@openvidu-meet/shared-components'; + +@Component({ + selector: 'app-ce-ov-meeting', + imports: [ + MeetingComponent, + MeetingToolbarButtonsComponent, + MeetingShareLinkPanelComponent, + MeetingParticipantPanelItemComponent, + MeetingLayoutComponent + ], + templateUrl: './app-ce-meeting.component.html', + styleUrl: './app-ce-meeting.component.scss' +}) +export class AppCeMeetingComponent {}