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>
|
</mat-menu>
|
||||||
</div>
|
</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 *ovParticipantPanelAfterLocalParticipant>
|
||||||
<div class="share-meeting-link-container">
|
<div class="share-meeting-link-container">
|
||||||
<ov-share-meeting-link
|
<ov-share-meeting-link
|
||||||
@ -116,6 +101,78 @@
|
|||||||
}
|
}
|
||||||
</ng-container>
|
</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>
|
</ov-videoconference>
|
||||||
} @else {
|
} @else {
|
||||||
<!-- Move this logic to prejoin meeting page -->
|
<!-- Move this logic to prejoin meeting page -->
|
||||||
|
|||||||
@ -279,3 +279,20 @@
|
|||||||
.force-disconnect-btn {
|
.force-disconnect-btn {
|
||||||
color: var(--ov-meet-color-error);
|
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 { ActivatedRoute } from '@angular/router';
|
||||||
import { ShareMeetingLinkComponent } from '@lib/components/share-meeting-link/share-meeting-link.component';
|
import { ShareMeetingLinkComponent } from '@lib/components/share-meeting-link/share-meeting-link.component';
|
||||||
import { ErrorReason } from '@lib/models';
|
import { ErrorReason } from '@lib/models';
|
||||||
|
import { CustomParticipantModel } from '@lib/models/custom-participant.model';
|
||||||
import {
|
import {
|
||||||
AppDataService,
|
AppDataService,
|
||||||
ApplicationFeatures,
|
ApplicationFeatures,
|
||||||
@ -35,7 +36,7 @@ import {
|
|||||||
WebComponentEvent,
|
WebComponentEvent,
|
||||||
WebComponentOutboundEventMessage
|
WebComponentOutboundEventMessage
|
||||||
} from '@lib/typings/ce';
|
} from '@lib/typings/ce';
|
||||||
import { MeetSignalType } from '@lib/typings/ce/event.model';
|
import { MeetParticipantRoleUpdatedPayload, MeetSignalType } from '@lib/typings/ce/event.model';
|
||||||
import {
|
import {
|
||||||
ApiDirectiveModule,
|
ApiDirectiveModule,
|
||||||
DataPacket_Kind,
|
DataPacket_Kind,
|
||||||
@ -91,7 +92,8 @@ export class MeetingComponent implements OnInit {
|
|||||||
participantName = '';
|
participantName = '';
|
||||||
participantToken = '';
|
participantToken = '';
|
||||||
participantRole: ParticipantRole = ParticipantRole.SPEAKER;
|
participantRole: ParticipantRole = ParticipantRole.SPEAKER;
|
||||||
remoteParticipants: ParticipantModel[] = [];
|
localParticipant?: CustomParticipantModel;
|
||||||
|
remoteParticipants: CustomParticipantModel[] = [];
|
||||||
|
|
||||||
showMeeting = false;
|
showMeeting = false;
|
||||||
features: Signal<ApplicationFeatures>;
|
features: Signal<ApplicationFeatures>;
|
||||||
@ -258,7 +260,12 @@ export class MeetingComponent implements OnInit {
|
|||||||
this.componentParticipantService.remoteParticipants$
|
this.componentParticipantService.remoteParticipants$
|
||||||
.pipe(takeUntil(this.destroy$))
|
.pipe(takeUntil(this.destroy$))
|
||||||
.subscribe((participants) => {
|
.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) {
|
} catch (error) {
|
||||||
console.error('Error accessing meeting:', error);
|
console.error('Error accessing meeting:', error);
|
||||||
@ -321,6 +328,19 @@ export class MeetingComponent implements OnInit {
|
|||||||
if (topic === MeetSignalType.MEET_ROOM_PREFERENCES_UPDATED) {
|
if (topic === MeetSignalType.MEET_ROOM_PREFERENCES_UPDATED) {
|
||||||
const roomPreferences: MeetRoomPreferences = event.preferences;
|
const roomPreferences: MeetRoomPreferences = event.preferences;
|
||||||
this.featureConfService.setRoomPreferences(roomPreferences);
|
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,9 +417,18 @@ export class MeetingComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async forceDisconnectParticipant(participant: ParticipantModel) {
|
async forceDisconnectParticipant(participant: CustomParticipantModel) {
|
||||||
|
if (this.participantService.isModeratorParticipant()) {
|
||||||
await this.meetingService.kickParticipant(this.roomId, participant.identity);
|
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() {
|
async copyModeratorLink() {
|
||||||
this.clipboard.copy(this.room!.moderatorRoomUrl);
|
this.clipboard.copy(this.room!.moderatorRoomUrl);
|
||||||
|
|||||||
@ -38,4 +38,9 @@ export class HttpService {
|
|||||||
statusCode: response.status
|
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);
|
await this.httpService.deleteRequest(path, headers);
|
||||||
this.log.d(`Participant '${participantId}' kicked from room ${roomId}`);
|
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 { routes } from '@app/app.routes';
|
||||||
import { environment } from '@environment/environment';
|
import { environment } from '@environment/environment';
|
||||||
import { httpInterceptor } from '@lib/interceptors/index';
|
import { httpInterceptor } from '@lib/interceptors/index';
|
||||||
|
import { CustomParticipantModel } from '@lib/models/custom-participant.model';
|
||||||
import { ThemeService } from '@lib/services/theme.service';
|
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 = {
|
const ovComponentsconfig: OpenViduComponentsConfig = {
|
||||||
production: environment.production
|
production: environment.production,
|
||||||
|
participantFactory: (props: ParticipantProperties) => new CustomParticipantModel(props)
|
||||||
};
|
};
|
||||||
|
|
||||||
export const appConfig: ApplicationConfig = {
|
export const appConfig: ApplicationConfig = {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user