diff --git a/frontend/projects/shared-meet-components/src/lib/models/custom-participant.model.ts b/frontend/projects/shared-meet-components/src/lib/models/custom-participant.model.ts
new file mode 100644
index 0000000..4ac9665
--- /dev/null
+++ b/frontend/projects/shared-meet-components/src/lib/models/custom-participant.model.ts
@@ -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;
+};
diff --git a/frontend/projects/shared-meet-components/src/lib/pages/meeting/meeting.component.html b/frontend/projects/shared-meet-components/src/lib/pages/meeting/meeting.component.html
index 70dd790..63ea4f5 100644
--- a/frontend/projects/shared-meet-components/src/lib/pages/meeting/meeting.component.html
+++ b/frontend/projects/shared-meet-components/src/lib/pages/meeting/meeting.component.html
@@ -77,21 +77,6 @@
-
-
-
- @if (!participant.isLocal) {
-
- }
-
-
}
+
+
+
+ @if (features().canModerateRoom) {
+
+
+ @if (participant.isLocal) {
+
+
+
+ shield_person
+
+
+
+ } @else {
+
+
+
+ @if (participant.isModerator()) {
+
+
+ shield_person
+
+
+ }
+
+
+ @if (!participant.isModerator()) {
+
+ }
+
+
+
+
+ }
+
+ }
+
+ @else {
+
+
+ @if (participant.isModerator()) {
+
+
+
+ shield_person
+
+
+
+ }
+
+
+ }
+
} @else {
diff --git a/frontend/projects/shared-meet-components/src/lib/pages/meeting/meeting.component.scss b/frontend/projects/shared-meet-components/src/lib/pages/meeting/meeting.component.scss
index 7e08b54..5a9f271 100644
--- a/frontend/projects/shared-meet-components/src/lib/pages/meeting/meeting.component.scss
+++ b/frontend/projects/shared-meet-components/src/lib/pages/meeting/meeting.component.scss
@@ -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);
+ }
+}
diff --git a/frontend/projects/shared-meet-components/src/lib/pages/meeting/meeting.component.ts b/frontend/projects/shared-meet-components/src/lib/pages/meeting/meeting.component.ts
index ff56a3c..30d09ef 100644
--- a/frontend/projects/shared-meet-components/src/lib/pages/meeting/meeting.component.ts
+++ b/frontend/projects/shared-meet-components/src/lib/pages/meeting/meeting.component.ts
@@ -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;
@@ -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() {
diff --git a/frontend/projects/shared-meet-components/src/lib/services/http.service.ts b/frontend/projects/shared-meet-components/src/lib/services/http.service.ts
index bc12c99..f6d514a 100644
--- a/frontend/projects/shared-meet-components/src/lib/services/http.service.ts
+++ b/frontend/projects/shared-meet-components/src/lib/services/http.service.ts
@@ -38,4 +38,9 @@ export class HttpService {
statusCode: response.status
}));
}
+
+ async patchRequest(path: string, body: any = {}, headers?: Record): Promise {
+ const options = headers ? { headers: new HttpHeaders(headers) } : {};
+ return lastValueFrom(this.http.patch(path, body, options));
+ }
}
diff --git a/frontend/projects/shared-meet-components/src/lib/services/meeting.service.ts b/frontend/projects/shared-meet-components/src/lib/services/meeting.service.ts
index ad04894..fedf35f 100644
--- a/frontend/projects/shared-meet-components/src/lib/services/meeting.service.ts
+++ b/frontend/projects/shared-meet-components/src/lib/services/meeting.service.ts
@@ -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 {
+ 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}`);
+ }
}
diff --git a/frontend/src/app/app.config.ts b/frontend/src/app/app.config.ts
index 15c3433..3633583 100644
--- a/frontend/src/app/app.config.ts
+++ b/frontend/src/app/app.config.ts
@@ -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 = {