From bba3a9a734eec50e200231c7ec315926e78e8f5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5vard=20Holvik?= Date: Sat, 20 Jan 2024 00:06:00 +0100 Subject: [PATCH] Add more types to `` web component When used in React context, you are now able to get a typed ref when using the web component. The typed ref allows you to listen to and handle events with type safety, and also issue commands in a type-safe manner. --- src/lib/embed/index.ts | 61 +++++++++++++++++++++++++---- src/stories/prebuilt-ui.stories.tsx | 23 +++++++++-- 2 files changed, 73 insertions(+), 11 deletions(-) diff --git a/src/lib/embed/index.ts b/src/lib/embed/index.ts index 8f08d8d3..bfdc1cd5 100644 --- a/src/lib/embed/index.ts +++ b/src/lib/embed/index.ts @@ -1,9 +1,10 @@ import { define, ref } from "heresy"; +import { ReactHTMLElement } from "react"; import { parseRoomUrlAndSubdomain } from "../utils/roomUrl"; import { sdkVersion } from "../version"; -interface WherebyEmbedAttributes { +interface WherebyEmbedElementAttributes extends ReactHTMLElement { audio: string; avatarUrl: string; background: string; @@ -38,10 +39,48 @@ interface WherebyEmbedAttributes { video: string; virtualBackgroundUrl: string; } + +interface WherebyEmbedElementEventMap { + ready: CustomEvent; + knock: CustomEvent; + participantupdate: CustomEvent<{ count: number }>; + join: CustomEvent; + leave: CustomEvent<{ removed: boolean }>; + participant_join: CustomEvent<{ participant: { metadata: string } }>; + participant_leave: CustomEvent<{ participant: { metadata: string } }>; + microphone_toggle: CustomEvent<{ enabled: boolean }>; + camera_toggle: CustomEvent<{ enabled: boolean }>; + chat_toggle: CustomEvent<{ open: boolean }>; + pip_toggle: CustomEvent<{ open: boolean }>; + deny_device_permission: CustomEvent<{ denied: boolean }>; + screenshare_toggle: CustomEvent<{ enabled: boolean }>; + streaming_status_change: CustomEvent<{ status: string }>; + connection_status_change: CustomEvent<{ status: "stable" | "unstable" }>; +} + +interface WherebyEmbedElementCommands { + startRecording: () => void; + stopRecording: () => void; + startStreaming: () => void; + stopStreaming: () => void; + toggleCamera: (enabled?: boolean) => void; + toggleMicrophone: (enabled?: boolean) => void; + toggleScreenshare: (enabled?: boolean) => void; + toogleChat: (enabled?: boolean) => void; +} + +export interface WherebyEmbedElement extends HTMLIFrameElement, WherebyEmbedElementCommands { + addEventListener( + type: K, + listener: (this: HTMLIFrameElement, ev: (WherebyEmbedElementEventMap & HTMLElementEventMap)[K]) => void, + options?: boolean | AddEventListenerOptions | undefined + ): void; +} + declare global { namespace JSX { interface IntrinsicElements { - ["whereby-embed"]: Partial; + ["whereby-embed"]: Partial; } } } @@ -132,24 +171,32 @@ define("WherebyEmbed", { stopStreaming() { this._postCommand("stop_streaming"); }, - toggleCamera(enabled: boolean) { + toggleCamera(enabled?: boolean) { this._postCommand("toggle_camera", [enabled]); }, - toggleMicrophone(enabled: boolean) { + toggleMicrophone(enabled?: boolean) { this._postCommand("toggle_microphone", [enabled]); }, - toggleScreenshare(enabled: boolean) { + toggleScreenshare(enabled?: boolean) { this._postCommand("toggle_screenshare", [enabled]); }, - toggleChat(enabled: boolean) { + toggleChat(enabled?: boolean) { this._postCommand("toggle_chat", [enabled]); }, - onmessage({ origin, data }: { origin: string; data: { type: string; payload: string } }) { + onmessage({ + origin, + data, + }: { + origin: string; + data: { type: E; payload: WherebyEmbedElementEventMap[E] }; + }) { if (!this.roomUrl || origin !== this.roomUrl.origin) return; const { type, payload: detail } = data; + this.dispatchEvent(new CustomEvent(type, { detail })); }, + render() { const { avatarurl: avatarUrl, diff --git a/src/stories/prebuilt-ui.stories.tsx b/src/stories/prebuilt-ui.stories.tsx index 1e2dd2e5..f87223a8 100644 --- a/src/stories/prebuilt-ui.stories.tsx +++ b/src/stories/prebuilt-ui.stories.tsx @@ -1,6 +1,7 @@ import { Story } from "@storybook/react"; -import React from "react"; +import React, { useEffect, useRef, useState } from "react"; import "../lib/embed"; +import type { WherebyEmbedElement } from "../lib/embed"; interface WherebyEmbedAttributes { audio: boolean; @@ -75,8 +76,21 @@ const WherebyEmbed = ({ video, virtualBackgroundUrl, }: Partial) => { + const elmRef = useRef(null); + const [cameraEnabled, setCameraEnabled] = useState(video); + + useEffect(() => { + const element = elmRef.current; + + element?.addEventListener("camera_toggle", (e) => { + const cameraEnabled = e.detail.enabled; + setCameraEnabled(cameraEnabled); + }); + }, []); + return (

+ Camera: {cameraEnabled ? "ENABLED" : "DISABLED"}

); }; const Template: Story> = (args) => WherebyEmbed(args); -export const WherebyEmbedElement = Template.bind({}); +export const WherebyEmbedElementExample = Template.bind({}); -WherebyEmbedElement.args = { +WherebyEmbedElementExample.args = { audio: true, avatarUrl: "", background: true, @@ -127,7 +142,7 @@ WherebyEmbedElement.args = { virtualBackgroundUrl: "", }; -WherebyEmbedElement.parameters = { +WherebyEmbedElementExample.parameters = { docs: { transformSource: (src: string) => { return (src || "").replace(/>");