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.
This commit is contained in:
parent
f70bd04497
commit
8a8951c120
@ -1,14 +1,20 @@
|
||||
<ng-template #template let-participantContext="participant" let-localParticipant="localParticipant">
|
||||
@let ctx = getDisplayProperties(participantContext, localParticipant);
|
||||
<ng-template #template let-participant="participant">
|
||||
@let ctx = getDisplayProperties(participant);
|
||||
|
||||
<div class="participant-item-container">
|
||||
<ov-participant-panel-item [participant]="participantContext">
|
||||
<!-- Moderator Badge -->
|
||||
<ov-participant-panel-item [participant]="participant">
|
||||
<!-- Participant Badge -->
|
||||
<ng-container *ovParticipantPanelParticipantBadge>
|
||||
@if (ctx.showModeratorBadge) {
|
||||
<span class="moderator-badge" [attr.id]="'moderator-badge-' + participantContext.sid">
|
||||
<mat-icon [matTooltip]="'Moderator'" class="material-symbols-outlined">
|
||||
shield_person
|
||||
@if (ctx.showBadge) {
|
||||
<span
|
||||
[class]="getParticipantBadgeClass(participant)"
|
||||
[attr.id]="'participant-badge-' + participant.sid"
|
||||
>
|
||||
<mat-icon
|
||||
[matTooltip]="getParticipantBadgeTooltip(participant)"
|
||||
class="material-symbols-outlined"
|
||||
>
|
||||
{{ getParticipantBadgeIcon(participant) }}
|
||||
</mat-icon>
|
||||
</span>
|
||||
}
|
||||
@ -19,16 +25,16 @@
|
||||
<div
|
||||
*ovParticipantPanelItemElements
|
||||
class="moderation-controls"
|
||||
[attr.id]="'moderation-controls-' + participantContext.sid"
|
||||
[attr.id]="'moderation-controls-' + participant.sid"
|
||||
>
|
||||
<!-- Make Moderator Button -->
|
||||
@if (ctx.showMakeModeratorButton) {
|
||||
<button
|
||||
mat-icon-button
|
||||
(click)="onMakeModeratorClick(participantContext, localParticipant)"
|
||||
(click)="onMakeModeratorClick(participant)"
|
||||
[matTooltip]="'Make participant moderator'"
|
||||
class="make-moderator-btn"
|
||||
[attr.id]="'make-moderator-btn-' + participantContext.sid"
|
||||
[attr.id]="'make-moderator-btn-' + participant.sid"
|
||||
>
|
||||
<mat-icon class="material-symbols-outlined">add_moderator</mat-icon>
|
||||
</button>
|
||||
@ -38,10 +44,10 @@
|
||||
@if (ctx.showUnmakeModeratorButton) {
|
||||
<button
|
||||
mat-icon-button
|
||||
(click)="onUnmakeModeratorClick(participantContext, localParticipant)"
|
||||
(click)="onUnmakeModeratorClick(participant)"
|
||||
[matTooltip]="'Unmake participant moderator'"
|
||||
class="remove-moderator-btn"
|
||||
[attr.id]="'remove-moderator-btn-' + participantContext.sid"
|
||||
[attr.id]="'remove-moderator-btn-' + participant.sid"
|
||||
>
|
||||
<mat-icon class="material-symbols-outlined">remove_moderator</mat-icon>
|
||||
</button>
|
||||
@ -51,10 +57,10 @@
|
||||
@if (ctx.showKickButton) {
|
||||
<button
|
||||
mat-icon-button
|
||||
(click)="onKickParticipantClick(participantContext, localParticipant)"
|
||||
(click)="onKickParticipantClick(participant)"
|
||||
[matTooltip]="'Kick participant'"
|
||||
class="force-disconnect-btn"
|
||||
[attr.id]="'kick-participant-btn-' + participantContext.sid"
|
||||
[attr.id]="'kick-participant-btn-' + participant.sid"
|
||||
>
|
||||
<mat-icon>call_end</mat-icon>
|
||||
</button>
|
||||
|
||||
@ -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<any>;
|
||||
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
if (!localParticipant.isModerator()) return;
|
||||
async onUnmakeModeratorClick(participant: CustomParticipantModel): Promise<void> {
|
||||
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<void> {
|
||||
if (!localParticipant.isModerator()) return;
|
||||
async onKickParticipantClick(participant: CustomParticipantModel): Promise<void> {
|
||||
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);
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
@ -83,7 +83,7 @@
|
||||
<div *ovParticipantPanelItem="let participant">
|
||||
<ng-container
|
||||
[ngTemplateOutlet]="participantItemTemplate()"
|
||||
[ngTemplateOutletContext]="{ participant: participant, localParticipant: localParticipant() }"
|
||||
[ngTemplateOutletContext]="{ participant: participant }"
|
||||
>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user