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;