From 8a8951c120921d6f810b3dbf1ff973ecc30299fa Mon Sep 17 00:00:00 2001 From: juancarmore Date: Fri, 6 Mar 2026 11:44:39 +0100 Subject: [PATCH] frontend: update participant moderation controls and badge handling in meeting component Refactors participant moderation and badge display logic Unifies participant badge handling to support multiple roles (owner, admin, moderator) and updates control visibility based on user permissions. Simplifies template context, centralizes moderation action checks, and refines role change logic for better maintainability and scalability of participant controls. --- .../meeting-participant-item.component.html | 36 +++-- .../meeting-participant-item.component.ts | 139 ++++++++++++------ .../models/custom-participant.model.ts | 61 +++++--- .../pages/meeting/meeting.component.html | 2 +- .../pages/meeting/meeting.component.ts | 1 - 5 files changed, 158 insertions(+), 81 deletions(-) diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/customization/meeting-participant-item/meeting-participant-item.component.html b/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/customization/meeting-participant-item/meeting-participant-item.component.html index 6a315e6d..a394232a 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/customization/meeting-participant-item/meeting-participant-item.component.html +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/customization/meeting-participant-item/meeting-participant-item.component.html @@ -1,14 +1,20 @@ - - @let ctx = getDisplayProperties(participantContext, localParticipant); + + @let ctx = getDisplayProperties(participant);
- - + + - @if (ctx.showModeratorBadge) { - - - shield_person + @if (ctx.showBadge) { + + + {{ getParticipantBadgeIcon(participant) }} } @@ -19,16 +25,16 @@
@if (ctx.showMakeModeratorButton) { @@ -38,10 +44,10 @@ @if (ctx.showUnmakeModeratorButton) { @@ -51,10 +57,10 @@ @if (ctx.showKickButton) { diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/customization/meeting-participant-item/meeting-participant-item.component.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/customization/meeting-participant-item/meeting-participant-item.component.ts index d0ea28b9..7de72486 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/customization/meeting-participant-item/meeting-participant-item.component.ts +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/customization/meeting-participant-item/meeting-participant-item.component.ts @@ -3,8 +3,9 @@ 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 { MeetRoomMemberRole } from '@openvidu-meet/typings'; +import { MeetParticipantModerationAction, MeetRoomMemberUIBadge } from '@openvidu-meet/typings'; import { LoggerService, OpenViduComponentsUiModule } from 'openvidu-components-angular'; +import { RoomMemberContextService } from '../../../room-members/services/room-member-context.service'; import { CustomParticipantModel, ParticipantDisplayProperties } from '../../models/custom-participant.model'; import { MeetingService } from '../../services/meeting.service'; @@ -23,39 +24,101 @@ export class MeetingParticipantItemComponent { @ViewChild('template', { static: true }) template!: TemplateRef; protected meetingService = inject(MeetingService); + protected roomMemberContextService = inject(RoomMemberContextService); protected loggerService = inject(LoggerService); protected log = this.loggerService.get('OpenVidu Meet - MeetingParticipantItem'); /** * Get or compute display properties for a participant */ - 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 + getDisplayProperties(participant: CustomParticipantModel): ParticipantDisplayProperties { + const hasBadge = participant.hasBadge(); + const displayProperties: ParticipantDisplayProperties = { + showBadge: hasBadge, + showModerationControls: false, + showMakeModeratorButton: false, + showUnmakeModeratorButton: false, + showKickButton: false }; + + // Moderation controls are only shown for remote participants + // Skip if participant is local (current user) + if (participant.isLocal) { + return displayProperties; + } + + const canMakeModerator = this.roomMemberContextService.hasPermission('canMakeModerator'); + const canKickParticipants = this.roomMemberContextService.hasPermission('canKickParticipants'); + + // If user doesn't have any moderation permissions, no need to compute further + if (!canMakeModerator && !canKickParticipants) { + return displayProperties; + } + + const isPromotedModerator = participant.isPromotedModerator(); + if (isPromotedModerator) { + // Show unmake moderator and/or kick participant buttons if user has correct permission and + // this participant is a promoted moderator (not an original moderator, who cannot be kicked or unmade moderator) + displayProperties.showUnmakeModeratorButton = canMakeModerator; + displayProperties.showKickButton = canKickParticipants; + } else { + // Show make moderator and/or kick participant buttons if user has correct permission + // and this participant doesn't have any badge (is not owner/admin/moderator) + displayProperties.showMakeModeratorButton = canMakeModerator && !hasBadge; + displayProperties.showKickButton = canKickParticipants && !hasBadge; + } + + // Show moderation controls container if any of the buttons should be shown + displayProperties.showModerationControls = + displayProperties.showMakeModeratorButton || + displayProperties.showUnmakeModeratorButton || + displayProperties.showKickButton; + return displayProperties; } - async onMakeModeratorClick( - participantContext: CustomParticipantModel, - localParticipant: CustomParticipantModel - ): Promise { - if (!localParticipant.isModerator()) return; + getParticipantBadgeIcon(participant: CustomParticipantModel): string { + switch (participant.getBadge()) { + case MeetRoomMemberUIBadge.OWNER: + return 'workspace_premium'; + case MeetRoomMemberUIBadge.ADMIN: + return 'admin_panel_settings'; + case MeetRoomMemberUIBadge.MODERATOR: + return 'shield_person'; + default: + return ''; + } + } - const roomId = localParticipant.roomName; + getParticipantBadgeTooltip(participant: CustomParticipantModel): string { + switch (participant.getBadge()) { + case MeetRoomMemberUIBadge.OWNER: + return 'Owner'; + case MeetRoomMemberUIBadge.ADMIN: + return 'Admin'; + case MeetRoomMemberUIBadge.MODERATOR: + return 'Moderator'; + default: + return ''; + } + } + + getParticipantBadgeClass(participant: CustomParticipantModel): string { + switch (participant.getBadge()) { + case MeetRoomMemberUIBadge.OWNER: + return 'owner-badge'; + case MeetRoomMemberUIBadge.ADMIN: + return 'admin-badge'; + case MeetRoomMemberUIBadge.MODERATOR: + return 'moderator-badge'; + default: + return ''; + } + } + + async onMakeModeratorClick(participant: CustomParticipantModel): Promise { + if (!this.roomMemberContextService.hasPermission('canMakeModerator')) return; + + const roomId = participant.roomName; if (!roomId) { this.log.e('Cannot change participant role: local participant room name is undefined'); return; @@ -64,8 +127,8 @@ export class MeetingParticipantItemComponent { try { await this.meetingService.changeParticipantRole( roomId, - participantContext.identity, - MeetRoomMemberRole.MODERATOR + participant.identity, + MeetParticipantModerationAction.UPGRADE ); this.log.d('Moderator assigned successfully'); } catch (error) { @@ -73,13 +136,10 @@ export class MeetingParticipantItemComponent { } } - async onUnmakeModeratorClick( - participantContext: CustomParticipantModel, - localParticipant: CustomParticipantModel - ): Promise { - if (!localParticipant.isModerator()) return; + async onUnmakeModeratorClick(participant: CustomParticipantModel): Promise { + if (!this.roomMemberContextService.hasPermission('canMakeModerator')) return; - const roomId = localParticipant.roomName; + const roomId = participant.roomName; if (!roomId) { this.log.e('Cannot change participant role: local participant room name is undefined'); return; @@ -88,8 +148,8 @@ export class MeetingParticipantItemComponent { try { await this.meetingService.changeParticipantRole( roomId, - participantContext.identity, - MeetRoomMemberRole.SPEAKER + participant.identity, + MeetParticipantModerationAction.DOWNGRADE ); this.log.d('Moderator unassigned successfully'); } catch (error) { @@ -97,20 +157,17 @@ export class MeetingParticipantItemComponent { } } - async onKickParticipantClick( - participantContext: CustomParticipantModel, - localParticipant: CustomParticipantModel - ): Promise { - if (!localParticipant.isModerator()) return; + async onKickParticipantClick(participant: CustomParticipantModel): Promise { + if (!this.roomMemberContextService.hasPermission('canKickParticipants')) return; - const roomId = localParticipant.roomName; + const roomId = participant.roomName; if (!roomId) { this.log.e('Cannot kick participant: local participant room name is undefined'); return; } try { - await this.meetingService.kickParticipant(roomId, participantContext.identity); + await this.meetingService.kickParticipant(roomId, participant.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/domains/meeting/models/custom-participant.model.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/models/custom-participant.model.ts index 818efbc9..76c3331a 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/models/custom-participant.model.ts +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/models/custom-participant.model.ts @@ -1,11 +1,11 @@ -import { MeetRoomMemberRole, MeetRoomMemberTokenMetadata } from '@openvidu-meet/typings'; +import { MeetRoomMemberTokenMetadata, MeetRoomMemberUIBadge } from '@openvidu-meet/typings'; import { ParticipantModel, ParticipantProperties } from 'openvidu-components-angular'; /** * Interface for computed participant display properties */ export interface ParticipantDisplayProperties { - showModeratorBadge: boolean; + showBadge: boolean; showModerationControls: boolean; showMakeModeratorButton: boolean; showUnmakeModeratorButton: boolean; @@ -14,49 +14,64 @@ export interface ParticipantDisplayProperties { // Represents a participant in the application. export class CustomParticipantModel extends ParticipantModel { - // Indicates the original role of the participant. - private _meetOriginalRole: MeetRoomMemberRole; - // Indicates the current role of the participant. - private _meetRole: MeetRoomMemberRole; + private _meetBadge = MeetRoomMemberUIBadge.OTHER; + private _isPromotedModerator = false; constructor(props: ParticipantProperties) { super(props); - const participant = props.participant; - this._meetOriginalRole = extractParticipantRole(participant.metadata); - this._meetRole = this._meetOriginalRole; + this.updateModerationMetadata(props.participant.metadata); } - set meetRole(role: MeetRoomMemberRole) { - this._meetRole = role; + set meetBadge(badge: MeetRoomMemberUIBadge) { + this._meetBadge = badge; + } + + set promotedModerator(isPromoted: boolean) { + this._isPromotedModerator = isPromoted; + } + + private updateModerationMetadata(metadata: unknown): void { + const parsedMetadata = parseParticipantMetadata(metadata); + this._meetBadge = parsedMetadata?.badge || MeetRoomMemberUIBadge.OTHER; + this._isPromotedModerator = Boolean(parsedMetadata?.isPromotedModerator); } /** - * Checks if the current role of the participant is moderator. - * @returns True if the current role is moderator, false otherwise. + * Gets the participant's badge. + * @returns The MeetRoomMemberUIBadge representing the participant's badge. */ - isModerator(): boolean { - return this._meetRole === MeetRoomMemberRole.MODERATOR; + getBadge(): MeetRoomMemberUIBadge { + return this._meetBadge; } /** - * Checks if the original role of the participant is moderator. - * @returns True if the original role is moderator, false otherwise. + * Checks if the participant has a badge other than OTHER. + * @returns True if the participant has a badge, false otherwise. */ - isOriginalModerator(): boolean { - return this._meetOriginalRole === MeetRoomMemberRole.MODERATOR; + hasBadge(): boolean { + return this._meetBadge !== MeetRoomMemberUIBadge.OTHER; + } + + /** + * Checks if the participant is a promoted moderator (not an original moderator). + * @returns True if the participant is a promoted moderator, false otherwise. + */ + isPromotedModerator(): boolean { + return this._isPromotedModerator; } } -const extractParticipantRole = (metadata: any): MeetRoomMemberRole => { +const parseParticipantMetadata = (metadata: unknown): MeetRoomMemberTokenMetadata | undefined => { let parsedMetadata: MeetRoomMemberTokenMetadata | undefined; try { - parsedMetadata = JSON.parse(metadata || '{}'); + parsedMetadata = JSON.parse((metadata as string) || '{}'); } catch (e) { console.warn('Failed to parse participant metadata:', e); } if (!parsedMetadata || typeof parsedMetadata !== 'object') { - return MeetRoomMemberRole.SPEAKER; + return undefined; } - return parsedMetadata.baseRole || MeetRoomMemberRole.SPEAKER; + + return parsedMetadata; }; diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/pages/meeting/meeting.component.html b/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/pages/meeting/meeting.component.html index 0f2d0578..9cdd7e75 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/pages/meeting/meeting.component.html +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/pages/meeting/meeting.component.html @@ -83,7 +83,7 @@
diff --git a/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/pages/meeting/meeting.component.ts b/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/pages/meeting/meeting.component.ts index 991d2c86..b0bd2717 100644 --- a/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/pages/meeting/meeting.component.ts +++ b/meet-ce/frontend/projects/shared-meet-components/src/lib/domains/meeting/pages/meeting/meeting.component.ts @@ -60,7 +60,6 @@ export class MeetingComponent implements OnInit { roomName = this.lobbyService.roomName; roomMemberToken = this.lobbyService.roomMemberToken; e2eeKey = this.lobbyService.e2eeKeyValue; - localParticipant = this.meetingContextService.localParticipant; features = this.meetingContextService.meetingUI; hasRecordings = this.meetingContextService.hasRecordings;