frontend: Improve webcomponent event and command types

This commit is contained in:
Carlos Santos 2025-05-07 16:31:55 +02:00
parent e75b21fa49
commit 326ee174c6
9 changed files with 194 additions and 69 deletions

View File

@ -25,7 +25,8 @@ import {
RoomService, RoomService,
SessionStorageService SessionStorageService
} from '../../services'; } from '../../services';
import { OpenViduMeetMessage, WebComponentEventType } from 'webcomponent/src/types/message.type'; import { OutboundEventMessage } from 'webcomponent/src/models/message.type';
import { WebComponentEvent } from 'webcomponent/src/models/event.model';
@Component({ @Component({
selector: 'app-video-room', selector: 'app-video-room',
@ -114,11 +115,11 @@ export class VideoRoomComponent implements OnInit, OnDestroy {
} }
onParticipantConnected(event: ParticipantModel) { onParticipantConnected(event: ParticipantModel) {
const message: OpenViduMeetMessage = { const message: OutboundEventMessage = {
eventType: WebComponentEventType.LOCAL_PARTICIPANT_CONNECTED, event: WebComponentEvent.JOIN,
payload: { payload: {
roomId: event.getProperties().room?.name, roomId: event.getProperties().room?.name || '',
participantName: event.name participantName: event.name!
} }
}; };
this.wcManagerService.sendMessageToParent(message); this.wcManagerService.sendMessageToParent(message);
@ -129,8 +130,8 @@ export class VideoRoomComponent implements OnInit, OnDestroy {
const redirectURL = this.ctxService.getLeaveRedirectURL() || '/disconnected'; const redirectURL = this.ctxService.getLeaveRedirectURL() || '/disconnected';
const isExternalURL = /^https?:\/\//.test(redirectURL); const isExternalURL = /^https?:\/\//.test(redirectURL);
const message: OpenViduMeetMessage = { const message: OutboundEventMessage = {
eventType: WebComponentEventType.LOCAL_PARTICIPANT_LEFT, event: WebComponentEvent.LEFT,
payload: { payload: {
roomId: event.roomName, roomId: event.roomName,
participantName: event.participantName participantName: event.participantName

View File

@ -8,12 +8,8 @@ import {
OpenViduService, OpenViduService,
LoggerService LoggerService
} from 'projects/shared-meet-components/src/public-api'; } from 'projects/shared-meet-components/src/public-api';
import { import { WebComponentCommand } from 'webcomponent/src/models/command.model';
OpenViduMeetMessage, import { OutboundEventMessage, InboundCommandMessage } from 'webcomponent/src/models/message.type';
ParentMessage,
WebComponentActionType,
WebComponentEventType
} from 'webcomponent/src/types/message.type';
/** /**
* Service to manage the commands from OpenVidu Meet WebComponent/Iframe. * Service to manage the commands from OpenVidu Meet WebComponent/Iframe.
@ -43,12 +39,12 @@ export class WebComponentManagerService {
this.isListenerStarted = true; this.isListenerStarted = true;
// Listen for messages from the iframe // Listen for messages from the iframe
window.addEventListener('message', async (event) => { window.addEventListener('message', async (event) => {
const message: ParentMessage = event.data; const message: InboundCommandMessage = event.data;
const parentDomain = this.contextService.getParentDomain(); const parentDomain = this.contextService.getParentDomain();
const { action, payload } = message; const { command, payload } = message;
if (!parentDomain) { if (!parentDomain) {
if (action === WebComponentActionType.INITIALIZE) { if (command === WebComponentCommand.INITIALIZE) {
if (!payload || !('domain' in payload)) { if (!payload || !('domain' in payload)) {
console.error('Parent domain not provided in message payload'); console.error('Parent domain not provided in message payload');
return; return;
@ -66,19 +62,19 @@ export class WebComponentManagerService {
console.debug('Message received from parent:', event.data); console.debug('Message received from parent:', event.data);
// TODO: reject if room is not connected // TODO: reject if room is not connected
switch (action) { switch (command) {
case WebComponentActionType.END_MEETING: case WebComponentCommand.END_MEETING:
// Moderator only // Moderator only
if (this.contextService.isModeratorParticipant()) { if (this.contextService.isModeratorParticipant()) {
const roomId = this.contextService.getRoomId(); const roomId = this.contextService.getRoomId();
await this.httpService.endMeeting(roomId); await this.httpService.endMeeting(roomId);
} }
break; break;
case WebComponentActionType.TOGGLE_CHAT: // case WebComponentCommand.TOGGLE_CHAT:
// Toggle chat // Toggle chat
this.panelService.togglePanel(PanelType.CHAT); // this.panelService.togglePanel(PanelType.CHAT);
break; // break;
case WebComponentActionType.LEAVE_ROOM: case WebComponentCommand.LEAVE_ROOM:
// Leave room. // Leave room.
await this.openviduService.disconnectRoom(); await this.openviduService.disconnectRoom();
break; break;
@ -94,7 +90,7 @@ export class WebComponentManagerService {
window.removeEventListener('message', this.startCommandsListener); window.removeEventListener('message', this.startCommandsListener);
} }
sendMessageToParent(event: OpenViduMeetMessage /*| RoomDisconnectedEvent*/) { sendMessageToParent(event: OutboundEventMessage) {
if (!this.contextService.isEmbeddedMode()) return; if (!this.contextService.isEmbeddedMode()) return;
this.log.d('Sending message to parent :', event); this.log.d('Sending message to parent :', event);
const origin = this.contextService.getParentDomain(); const origin = this.contextService.getParentDomain();

View File

@ -1,4 +1,4 @@
import { ParentMessage } from '../types/message.type'; import { InboundCommandMessage } from '../models/message.type';
/** /**
* Handles sending messages to the iframe. * Handles sending messages to the iframe.
@ -12,7 +12,7 @@ export class CommandsManager {
this.allowedOrigin = allowedOrigin; this.allowedOrigin = allowedOrigin;
} }
public sendMessage(message: ParentMessage, targetOrigin?: string): void { public sendMessage(message: InboundCommandMessage, targetOrigin?: string): void {
targetOrigin = targetOrigin || this.allowedOrigin; targetOrigin = targetOrigin || this.allowedOrigin;
this.iframe.contentWindow?.postMessage(message, targetOrigin); this.iframe.contentWindow?.postMessage(message, targetOrigin);
} }

View File

@ -1,4 +1,4 @@
import { OpenViduMeetMessage } from '../types/message.type'; import { OutboundEventMessage } from '../models/message.type';
export class EventsManager { export class EventsManager {
private element: HTMLElement; private element: HTMLElement;
@ -12,9 +12,9 @@ export class EventsManager {
} }
private handleMessage(event: MessageEvent) { private handleMessage(event: MessageEvent) {
const message: OpenViduMeetMessage = event.data; const message: OutboundEventMessage = event.data;
// Validate message origin (security measure) // Validate message origin (security measure)
if (!message || !message.eventType) { if (!message || !message.event) {
// console.warn('Invalid message:', message); // console.warn('Invalid message:', message);
return; return;
} }
@ -22,8 +22,8 @@ export class EventsManager {
this.dispatchEvent(message); this.dispatchEvent(message);
} }
private dispatchEvent(message: OpenViduMeetMessage) { private dispatchEvent(message: OutboundEventMessage) {
const event = new CustomEvent(message.eventType, { const event = new CustomEvent(message.event, {
detail: message.payload, detail: message.payload,
bubbles: true, bubbles: true,
composed: true composed: true

View File

@ -1,4 +1,5 @@
import { ParentMessage, WebComponentActionType } from '../types/message.type'; import { WebComponentCommand } from '../models/command.model';
import { InboundCommandMessage } from '../models/message.type';
import { CommandsManager } from './CommandsManager'; import { CommandsManager } from './CommandsManager';
import { EventsManager } from './EventsManager'; import { EventsManager } from './EventsManager';
@ -68,8 +69,8 @@ export class OpenViduMeet extends HTMLElement {
this.shadowRoot?.appendChild(style); this.shadowRoot?.appendChild(style);
this.shadowRoot?.appendChild(this.iframe); this.shadowRoot?.appendChild(this.iframe);
this.iframe.onload = () => { this.iframe.onload = () => {
const message: ParentMessage = { const message: InboundCommandMessage = {
action: WebComponentActionType.INITIALIZE, command: WebComponentCommand.INITIALIZE,
payload: { domain: window.location.origin } payload: { domain: window.location.origin }
}; };
this.commandsManager.sendMessage(message); this.commandsManager.sendMessage(message);
@ -101,17 +102,17 @@ export class OpenViduMeet extends HTMLElement {
// Public methods // Public methods
public endMeeting() { public endMeeting() {
const message: ParentMessage = { action: WebComponentActionType.END_MEETING }; const message: InboundCommandMessage = { command: WebComponentCommand.END_MEETING };
this.commandsManager.sendMessage(message); this.commandsManager.sendMessage(message);
} }
public leaveRoom() { public leaveRoom() {
const message: ParentMessage = { action: WebComponentActionType.LEAVE_ROOM }; const message: InboundCommandMessage = { command: WebComponentCommand.LEAVE_ROOM };
this.commandsManager.sendMessage(message); this.commandsManager.sendMessage(message);
} }
public toggleChat() { // public toggleChat() {
const message: ParentMessage = { action: WebComponentActionType.TOGGLE_CHAT }; // const message: ParentMessage = { action: WebComponentActionType.TOGGLE_CHAT };
this.commandsManager.sendMessage(message); // this.commandsManager.sendMessage(message);
} // }
} }

View File

@ -0,0 +1,49 @@
/**
* All available commands that can be sent to the WebComponent.
*/
export enum WebComponentCommand {
/**
* Initializes the WebComponent with the given configuration.
* @private
*/
INITIALIZE = 'INITIALIZE',
/**
* Ends the current meeting for all participants.
* This command is only available for the moderator.
*/
END_MEETING = 'END_MEETING',
/**
* Disconnects the local participant from the current room.
*/
LEAVE_ROOM = 'LEAVE_ROOM'
// TOGGLE_CHAT = 'TOGGLE_CHAT'
}
/**
* Type definitions for command payloads.
* Each property corresponds to a command in {@link WebComponentCommand}.
* @category Communication
*/
export interface CommandPayloads {
/**
* Payload for the INITIALIZE command.
* @private
*/
[WebComponentCommand.INITIALIZE]: {
domain: string;
};
[WebComponentCommand.END_MEETING]: void;
[WebComponentCommand.LEAVE_ROOM]: void;
// [WebComponentCommand.TOGGLE_CHAT]: void;
}
/**
* Gets the type-safe payload for a specific command.
* This type allows TypeScript to infer the correct payload type based on the command.
* @category Type Helpers
* @private
*/
export type CommandPayloadFor<T extends WebComponentCommand> = T extends keyof CommandPayloads
? CommandPayloads[T]
: never;

View File

@ -0,0 +1,45 @@
/**
* All available events that can be emitted by the WebComponent.
* @category Communication
*/
export enum WebComponentEvent {
/**
* Event emitted when the local participant joins the room.
*/
JOIN = 'JOIN',
/**
* Event emitted when the local participant leaves the room.
*/
LEFT = 'LEFT',
/**
* Event emitted when a moderator ends the meeting.
*/
MEETING_ENDED = 'MEETING_ENDED'
}
/**
* Type definitions for event payloads.
* Each property corresponds to an event in {@link WebComponentEvent}.
* @category Communication
*/
export interface EventPayloads {
[WebComponentEvent.JOIN]: {
roomId: string;
participantName: string;
};
[WebComponentEvent.LEFT]: {
roomId: string;
participantName: string;
};
[WebComponentEvent.MEETING_ENDED]: {
roomId: string;
};
}
/**
* Gets the type-safe payload for a specific event.
* This type allows TypeScript to infer the correct payload type based on the event.
* @category Type Helpers
* @private
*/
export type EventPayloadFor<T extends WebComponentEvent> = T extends keyof EventPayloads ? EventPayloads[T] : never;

View File

@ -0,0 +1,62 @@
import { CommandPayloadFor, WebComponentCommand } from './command.model';
import { EventPayloadFor, WebComponentEvent } from './event.model';
/**
* Represents all possible messages exchanged between the host application and WebComponent.
* @category Communication
*/
export type WebComponentMessage = InboundCommandMessage<WebComponentCommand> | OutboundEventMessage<WebComponentEvent>;
/**
* Message sent from the host application to the WebComponent.
* Contains a command with an optional type-safe payload.
* @category Communication
*/
export interface InboundCommandMessage<T extends WebComponentCommand = WebComponentCommand> {
/** The command to execute in the WebComponent */
command: T;
/** Optional payload with additional data for the command */
payload?: CommandPayloadFor<T>;
}
/**
* Message sent from the WebComponent to the host application.
* Contains an event type with an optional type-safe payload.
* @category Communication
*/
export interface OutboundEventMessage<T extends WebComponentEvent = WebComponentEvent> {
/** The type of event being emitted */
event: T;
/** Optional payload with additional data about the event */
payload?: EventPayloadFor<T>;
}
/**
* Helper function to create a properly typed command message.
* @param command The command to send
* @param payload The payload for the command
* @returns A properly formatted command message
* @category Utilities
* @private
*/
export function createCommandMessage<T extends WebComponentCommand>(
command: T,
payload?: CommandPayloadFor<T>
): InboundCommandMessage<T> {
return { command, payload };
}
/**
* Helper function to create a properly typed event message.
* @param event The event type
* @param payload The payload for the event
* @returns A properly formatted event message
* @category Utilities
* @private
*/
export function createEventMessage<T extends WebComponentEvent>(
event: T,
payload?: EventPayloadFor<T>
): OutboundEventMessage<T> {
return { event, payload };
}

View File

@ -1,29 +0,0 @@
export type WebComponentMessage = ParentMessage | OpenViduMeetMessage;
/**
* Message sent from the parent to the OpenViduMeet component.
*/
export interface ParentMessage {
action: WebComponentActionType;
payload?: Record<string, any>;
}
/**
* Message sent from the OpenViduMeet component to the parent.
*/
export interface OpenViduMeetMessage {
eventType: WebComponentEventType;
payload?: Record<string, unknown>;
}
export enum WebComponentActionType {
INITIALIZE = 'initialize',
END_MEETING = 'endMeeting',
LEAVE_ROOM = 'leaveRoom',
TOGGLE_CHAT = 'toggleChat'
}
export enum WebComponentEventType {
LOCAL_PARTICIPANT_CONNECTED = 'join',
LOCAL_PARTICIPANT_LEFT = 'left'
}