frontend: implement custom participant model and role management in meeting component
This commit is contained in:
parent
b6acebfa18
commit
eb8ab3fe63
@ -0,0 +1,39 @@
|
||||
import { MeetTokenMetadata, ParticipantRole } from '@lib/typings/ce';
|
||||
import { ParticipantModel, ParticipantProperties } from 'openvidu-components-angular';
|
||||
|
||||
// Represents a participant in the application.
|
||||
export class CustomParticipantModel extends ParticipantModel {
|
||||
// Indicates the role of the participant.
|
||||
private _meetRole: ParticipantRole;
|
||||
|
||||
// Creates a new instance of CustomParticipantModel.
|
||||
constructor(props: ParticipantProperties) {
|
||||
super(props);
|
||||
const participant = props.participant;
|
||||
this._meetRole = extractParticipantRole(participant.metadata);
|
||||
}
|
||||
|
||||
// Sets the role of the participant.
|
||||
set meetRole(role: ParticipantRole) {
|
||||
this._meetRole = role;
|
||||
}
|
||||
|
||||
// Checks if the participant is a moderator.
|
||||
// Returns true if the participant's role is MODERATOR, otherwise false.
|
||||
isModerator(): boolean {
|
||||
return this._meetRole === ParticipantRole.MODERATOR;
|
||||
}
|
||||
}
|
||||
|
||||
const extractParticipantRole = (metadata: any): ParticipantRole => {
|
||||
let parsedMetadata: MeetTokenMetadata = metadata;
|
||||
try {
|
||||
parsedMetadata = JSON.parse(metadata || '{}');
|
||||
} catch (e) {
|
||||
console.warn('Failed to parse participant metadata:', e);
|
||||
}
|
||||
if (!parsedMetadata || typeof parsedMetadata !== 'object') {
|
||||
return ParticipantRole.PUBLISHER;
|
||||
}
|
||||
return parsedMetadata.selectedRole || ParticipantRole.PUBLISHER;
|
||||
};
|
||||
@ -77,21 +77,6 @@
|
||||
</mat-menu>
|
||||
</div>
|
||||
|
||||
<!-- Participant Panel Item Elements -->
|
||||
<div *ovParticipantPanelItemElements="let participant">
|
||||
<!-- Kick participant -->
|
||||
@if (!participant.isLocal) {
|
||||
<button
|
||||
mat-icon-button
|
||||
(click)="forceDisconnectParticipant(participant)"
|
||||
matTooltip="Disconnect participant"
|
||||
class="force-disconnect-btn"
|
||||
>
|
||||
<mat-icon>call_end</mat-icon>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div *ovParticipantPanelAfterLocalParticipant>
|
||||
<div class="share-meeting-link-container">
|
||||
<ov-share-meeting-link
|
||||
@ -116,6 +101,78 @@
|
||||
}
|
||||
</ng-container>
|
||||
}
|
||||
|
||||
<ng-container *ovParticipantPanelItem="let participant">
|
||||
<!-- If Meet participant is moderator -->
|
||||
@if (features().canModerateRoom) {
|
||||
<div class="participant-item-container">
|
||||
<!-- Local participant -->
|
||||
@if (participant.isLocal) {
|
||||
<ov-participant-panel-item [participant]="participant">
|
||||
<ng-container *ovParticipantPanelParticipantBadge>
|
||||
<span class="moderator-badge">
|
||||
<mat-icon matTooltip="Moderator" class="material-symbols-outlined"
|
||||
>shield_person</mat-icon
|
||||
>
|
||||
</span>
|
||||
</ng-container>
|
||||
</ov-participant-panel-item>
|
||||
} @else {
|
||||
<!-- Remote participant -->
|
||||
|
||||
<ov-participant-panel-item [participant]="participant">
|
||||
@if (participant.isModerator()) {
|
||||
<ng-container *ovParticipantPanelParticipantBadge>
|
||||
<span class="moderator-badge">
|
||||
<mat-icon matTooltip="Moderator" class="material-symbols-outlined"
|
||||
>shield_person</mat-icon
|
||||
>
|
||||
</span>
|
||||
</ng-container>
|
||||
}
|
||||
<div *ovParticipantPanelItemElements>
|
||||
<!-- Button to make moderator if not -->
|
||||
@if (!participant.isModerator()) {
|
||||
<button
|
||||
mat-icon-button
|
||||
(click)="makeModerator(participant)"
|
||||
matTooltip="Make participant moderator"
|
||||
class="make-moderator-btn"
|
||||
>
|
||||
<mat-icon class="material-symbols-outlined">add_moderator</mat-icon>
|
||||
</button>
|
||||
}
|
||||
<!-- Button to kick participant -->
|
||||
<button
|
||||
mat-icon-button
|
||||
(click)="forceDisconnectParticipant(participant)"
|
||||
matTooltip="Kick participant"
|
||||
class="force-disconnect-btn"
|
||||
>
|
||||
<mat-icon>call_end</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</ov-participant-panel-item>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<!-- If I can't moderate the room -->
|
||||
@else {
|
||||
<div class="participant-item-container">
|
||||
<ov-participant-panel-item [participant]="participant">
|
||||
@if (participant.isModerator()) {
|
||||
<ng-container *ovParticipantPanelParticipantBadge>
|
||||
<span class="moderator-badge">
|
||||
<mat-icon matTooltip="Moderator" class="material-symbols-outlined">
|
||||
shield_person
|
||||
</mat-icon>
|
||||
</span>
|
||||
</ng-container>
|
||||
}
|
||||
</ov-participant-panel-item>
|
||||
</div>
|
||||
}
|
||||
</ng-container>
|
||||
</ov-videoconference>
|
||||
} @else {
|
||||
<!-- Move this logic to prejoin meeting page -->
|
||||
|
||||
@ -279,3 +279,20 @@
|
||||
.force-disconnect-btn {
|
||||
color: var(--ov-meet-color-error);
|
||||
}
|
||||
|
||||
.make-moderator-btn {
|
||||
color: var(--ov-meet-color-warning);
|
||||
}
|
||||
|
||||
.participant-item-container {
|
||||
align-items: center;
|
||||
|
||||
::ng-deep .participant-container {
|
||||
padding: 2px 10px !important;
|
||||
}
|
||||
.moderator-badge {
|
||||
color: var(--ov-meet-color-warning);
|
||||
font-size: 24px;
|
||||
margin-right: var(--ov-meet-spacing-xs);
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,6 +13,7 @@ import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { ShareMeetingLinkComponent } from '@lib/components/share-meeting-link/share-meeting-link.component';
|
||||
import { ErrorReason } from '@lib/models';
|
||||
import { CustomParticipantModel } from '@lib/models/custom-participant.model';
|
||||
import {
|
||||
AppDataService,
|
||||
ApplicationFeatures,
|
||||
@ -35,7 +36,7 @@ import {
|
||||
WebComponentEvent,
|
||||
WebComponentOutboundEventMessage
|
||||
} from '@lib/typings/ce';
|
||||
import { MeetSignalType } from '@lib/typings/ce/event.model';
|
||||
import { MeetParticipantRoleUpdatedPayload, MeetSignalType } from '@lib/typings/ce/event.model';
|
||||
import {
|
||||
ApiDirectiveModule,
|
||||
DataPacket_Kind,
|
||||
@ -91,7 +92,8 @@ export class MeetingComponent implements OnInit {
|
||||
participantName = '';
|
||||
participantToken = '';
|
||||
participantRole: ParticipantRole = ParticipantRole.SPEAKER;
|
||||
remoteParticipants: ParticipantModel[] = [];
|
||||
localParticipant?: CustomParticipantModel;
|
||||
remoteParticipants: CustomParticipantModel[] = [];
|
||||
|
||||
showMeeting = false;
|
||||
features: Signal<ApplicationFeatures>;
|
||||
@ -258,7 +260,12 @@ export class MeetingComponent implements OnInit {
|
||||
this.componentParticipantService.remoteParticipants$
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((participants) => {
|
||||
this.remoteParticipants = participants;
|
||||
this.remoteParticipants = participants as CustomParticipantModel[];
|
||||
});
|
||||
this.componentParticipantService.localParticipant$
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((participant) => {
|
||||
this.localParticipant = participant as CustomParticipantModel;
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error accessing meeting:', error);
|
||||
@ -321,6 +328,19 @@ export class MeetingComponent implements OnInit {
|
||||
if (topic === MeetSignalType.MEET_ROOM_PREFERENCES_UPDATED) {
|
||||
const roomPreferences: MeetRoomPreferences = event.preferences;
|
||||
this.featureConfService.setRoomPreferences(roomPreferences);
|
||||
} else if (topic === MeetSignalType.MEET_PARTICIPANT_ROLE_UPDATED) {
|
||||
const { participantName, newRole, secret } = event as MeetParticipantRoleUpdatedPayload;
|
||||
|
||||
if (participantName === this.localParticipant!.name) {
|
||||
this.localParticipant!.meetRole = newRole;
|
||||
} else {
|
||||
const participant = this.remoteParticipants.find((p) => p.name === participantName);
|
||||
if (participant) {
|
||||
participant.meetRole = newRole;
|
||||
}
|
||||
}
|
||||
|
||||
// !Request for new token with the new role
|
||||
}
|
||||
}
|
||||
);
|
||||
@ -397,8 +417,17 @@ export class MeetingComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
async forceDisconnectParticipant(participant: ParticipantModel) {
|
||||
await this.meetingService.kickParticipant(this.roomId, participant.identity);
|
||||
async forceDisconnectParticipant(participant: CustomParticipantModel) {
|
||||
if (this.participantService.isModeratorParticipant()) {
|
||||
await this.meetingService.kickParticipant(this.roomId, participant.identity);
|
||||
}
|
||||
}
|
||||
|
||||
async makeModerator(participant: CustomParticipantModel) {
|
||||
if (this.participantService.isModeratorParticipant()) {
|
||||
const newRole = ParticipantRole.MODERATOR;
|
||||
await this.meetingService.changeParticipantRole(this.roomId, participant.identity, newRole);
|
||||
}
|
||||
}
|
||||
|
||||
async copyModeratorLink() {
|
||||
|
||||
@ -38,4 +38,9 @@ export class HttpService {
|
||||
statusCode: response.status
|
||||
}));
|
||||
}
|
||||
|
||||
async patchRequest<T>(path: string, body: any = {}, headers?: Record<string, string>): Promise<T> {
|
||||
const options = headers ? { headers: new HttpHeaders(headers) } : {};
|
||||
return lastValueFrom(this.http.patch<T>(path, body, options));
|
||||
}
|
||||
}
|
||||
|
||||
@ -43,4 +43,23 @@ export class MeetingService {
|
||||
await this.httpService.deleteRequest(path, headers);
|
||||
this.log.d(`Participant '${participantId}' kicked from room ${roomId}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the role of a participant in a meeting.
|
||||
*
|
||||
* @param roomId - The unique identifier of the meeting room
|
||||
* @param participantId - The unique identifier of the participant whose role is to be changed
|
||||
* @param newRole - The new role to be assigned to the participant
|
||||
*/
|
||||
async changeParticipantRole(
|
||||
roomId: string,
|
||||
participantId: string,
|
||||
newRole: string
|
||||
): Promise<void> {
|
||||
const path = `${this.MEETINGS_API}/${roomId}/participants/${participantId}`;
|
||||
const headers = this.participantService.getParticipantRoleHeader();
|
||||
const body = { role: newRole };
|
||||
await this.httpService.patchRequest(path, body, headers);
|
||||
this.log.d(`Changed role of participant '${participantId}' to '${newRole}' in room ${roomId}`);
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,11 +6,13 @@ import { provideRouter } from '@angular/router';
|
||||
import { routes } from '@app/app.routes';
|
||||
import { environment } from '@environment/environment';
|
||||
import { httpInterceptor } from '@lib/interceptors/index';
|
||||
import { CustomParticipantModel } from '@lib/models/custom-participant.model';
|
||||
import { ThemeService } from '@lib/services/theme.service';
|
||||
import { OpenViduComponentsConfig, OpenViduComponentsModule } from 'openvidu-components-angular';
|
||||
import { OpenViduComponentsConfig, OpenViduComponentsModule, ParticipantProperties } from 'openvidu-components-angular';
|
||||
|
||||
const ovComponentsconfig: OpenViduComponentsConfig = {
|
||||
production: environment.production
|
||||
production: environment.production,
|
||||
participantFactory: (props: ParticipantProperties) => new CustomParticipantModel(props)
|
||||
};
|
||||
|
||||
export const appConfig: ApplicationConfig = {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user