feat: Add support for participant name, metadata, and attributes (#142)
This commit is contained in:
parent
575da78aa1
commit
9e2b7fcc61
93
src/components/config/AttributesEditor.tsx
Normal file
93
src/components/config/AttributesEditor.tsx
Normal file
@ -0,0 +1,93 @@
|
||||
import React from 'react';
|
||||
import { ConnectionState } from 'livekit-client';
|
||||
import { AttributeItem } from '@/lib/types';
|
||||
|
||||
|
||||
interface AttributesEditorProps {
|
||||
attributes: AttributeItem[];
|
||||
onAttributesChange: (attributes: AttributeItem[]) => void;
|
||||
themeColor: string;
|
||||
disabled?: boolean;
|
||||
connectionState?: ConnectionState;
|
||||
}
|
||||
|
||||
export const AttributesEditor: React.FC<AttributesEditorProps> = ({
|
||||
attributes,
|
||||
onAttributesChange,
|
||||
themeColor,
|
||||
disabled = false,
|
||||
connectionState
|
||||
}) => {
|
||||
const handleKeyChange = (id: string, newKey: string) => {
|
||||
const updatedAttributes = attributes.map(attr =>
|
||||
attr.id === id ? { ...attr, key: newKey } : attr
|
||||
);
|
||||
onAttributesChange(updatedAttributes);
|
||||
};
|
||||
|
||||
const handleValueChange = (id: string, newValue: string) => {
|
||||
const updatedAttributes = attributes.map(attr =>
|
||||
attr.id === id ? { ...attr, value: newValue } : attr
|
||||
);
|
||||
onAttributesChange(updatedAttributes);
|
||||
};
|
||||
|
||||
const handleRemoveAttribute = (id: string) => {
|
||||
const updatedAttributes = attributes.filter(attr => attr.id !== id);
|
||||
onAttributesChange(updatedAttributes);
|
||||
};
|
||||
|
||||
const handleAddAttribute = () => {
|
||||
const newId = `attr_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
const updatedAttributes = [...attributes, { id: newId, key: "", value: "" }];
|
||||
onAttributesChange(updatedAttributes);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="border border-gray-800 rounded-sm p-3 bg-gray-900/30">
|
||||
{attributes.map((attribute) => (
|
||||
<div key={attribute.id} className="flex items-center gap-2 mb-2">
|
||||
<input
|
||||
value={attribute.key}
|
||||
onChange={(e) => handleKeyChange(attribute.id, e.target.value)}
|
||||
className="flex-1 min-w-0 text-white text-sm bg-transparent border border-gray-800 rounded-sm px-3 py-1"
|
||||
placeholder="Key"
|
||||
disabled={disabled}
|
||||
/>
|
||||
<input
|
||||
value={attribute.value}
|
||||
onChange={(e) => handleValueChange(attribute.id, e.target.value)}
|
||||
className="flex-1 min-w-0 text-white text-sm bg-transparent border border-gray-800 rounded-sm px-3 py-1"
|
||||
placeholder="Value"
|
||||
disabled={disabled}
|
||||
/>
|
||||
<button
|
||||
onClick={() => handleRemoveAttribute(attribute.id)}
|
||||
className="flex-shrink-0 w-6 h-6 flex items-center justify-center text-gray-400 hover:text-white"
|
||||
disabled={disabled}
|
||||
style={{ display: disabled ? 'none' : 'flex' }}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<button
|
||||
onClick={handleAddAttribute}
|
||||
className={`text-xs py-1 px-2 rounded-sm flex items-center gap-1 ${
|
||||
disabled
|
||||
? "bg-gray-700 text-gray-500 cursor-not-allowed"
|
||||
: `bg-${themeColor}-500 hover:bg-${themeColor}-600 text-white`
|
||||
}`}
|
||||
disabled={disabled}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
Add Attribute
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -30,6 +30,7 @@ import { QRCodeSVG } from "qrcode.react";
|
||||
import { ReactNode, useCallback, useEffect, useMemo, useState } from "react";
|
||||
import tailwindTheme from "../../lib/tailwindTheme.preval";
|
||||
import { EditableNameValueRow } from "@/components/config/NameValueRow";
|
||||
import { AttributesEditor } from "@/components/config/AttributesEditor";
|
||||
|
||||
export interface PlaygroundMeta {
|
||||
name: string;
|
||||
@ -260,9 +261,23 @@ export default function Playground({
|
||||
editable={roomState !== ConnectionState.Connected}
|
||||
/>
|
||||
<EditableNameValueRow
|
||||
name="Participant"
|
||||
name="Participant ID"
|
||||
value={roomState === ConnectionState.Connected ?
|
||||
(localParticipant?.identity || '') :
|
||||
(config.settings.participant_id || '')}
|
||||
valueColor={`${config.settings.theme_color}-500`}
|
||||
onValueChange={(value) => {
|
||||
const newSettings = { ...config.settings };
|
||||
newSettings.participant_id = value;
|
||||
setUserSettings(newSettings);
|
||||
}}
|
||||
placeholder="Enter participant id"
|
||||
editable={roomState !== ConnectionState.Connected}
|
||||
/>
|
||||
<EditableNameValueRow
|
||||
name="Participant Name"
|
||||
value={roomState === ConnectionState.Connected ?
|
||||
(localParticipant?.name || '') :
|
||||
(config.settings.participant_name || '')}
|
||||
valueColor={`${config.settings.theme_color}-500`}
|
||||
onValueChange={(value) => {
|
||||
@ -270,10 +285,39 @@ export default function Playground({
|
||||
newSettings.participant_name = value;
|
||||
setUserSettings(newSettings);
|
||||
}}
|
||||
placeholder="Enter participant id"
|
||||
placeholder="Enter participant name"
|
||||
editable={roomState !== ConnectionState.Connected}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 mt-4">
|
||||
<div className="text-xs text-gray-500 mt-2">Metadata</div>
|
||||
<textarea
|
||||
value={config.settings.metadata || ""}
|
||||
onChange={(e) => {
|
||||
const newSettings = { ...config.settings };
|
||||
newSettings.metadata = e.target.value;
|
||||
setUserSettings(newSettings);
|
||||
}}
|
||||
className="w-full text-white text-sm bg-transparent border border-gray-800 rounded-sm px-3 py-2"
|
||||
placeholder="Custom metadata..."
|
||||
rows={2}
|
||||
disabled={roomState === ConnectionState.Connected}
|
||||
/>
|
||||
|
||||
<div className="text-xs text-gray-500 mt-2">Attributes</div>
|
||||
<AttributesEditor
|
||||
attributes={config.settings.attributes || []}
|
||||
onAttributesChange={(newAttributes) => {
|
||||
const newSettings = { ...config.settings };
|
||||
newSettings.attributes = newAttributes;
|
||||
setUserSettings(newSettings);
|
||||
}}
|
||||
themeColor={config.settings.theme_color}
|
||||
disabled={roomState === ConnectionState.Connected}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2 mt-4">
|
||||
<div className="text-xs text-gray-500 mt-2">RPC Method</div>
|
||||
<input
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { AttributeItem } from "@/lib/types";
|
||||
import { getCookie, setCookie } from "cookies-next";
|
||||
import jsYaml from "js-yaml";
|
||||
import { useRouter } from "next/navigation";
|
||||
@ -36,7 +37,10 @@ export type UserSettings = {
|
||||
ws_url: string;
|
||||
token: string;
|
||||
room_name: string;
|
||||
participant_id: string;
|
||||
participant_name: string;
|
||||
metadata?: string;
|
||||
attributes?: AttributeItem[];
|
||||
};
|
||||
|
||||
// Fallback if NEXT_PUBLIC_APP_CONFIG is not set
|
||||
@ -60,7 +64,10 @@ const defaultConfig: AppConfig = {
|
||||
ws_url: "",
|
||||
token: "",
|
||||
room_name: "",
|
||||
participant_id: "",
|
||||
participant_name: "",
|
||||
metadata: "",
|
||||
attributes: [],
|
||||
},
|
||||
show_qr: false,
|
||||
};
|
||||
@ -130,6 +137,7 @@ export const ConfigProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
ws_url: "",
|
||||
token: "",
|
||||
room_name: "",
|
||||
participant_id: "",
|
||||
participant_name: "",
|
||||
} as UserSettings;
|
||||
}, [appConfig]);
|
||||
|
||||
@ -54,14 +54,36 @@ export const ConnectionProvider = ({
|
||||
throw new Error("NEXT_PUBLIC_LIVEKIT_URL is not set");
|
||||
}
|
||||
url = process.env.NEXT_PUBLIC_LIVEKIT_URL;
|
||||
const params = new URLSearchParams();
|
||||
const body: Record<string, any> = {};
|
||||
if (config.settings.room_name) {
|
||||
params.append('roomName', config.settings.room_name);
|
||||
body.roomName = config.settings.room_name;
|
||||
}
|
||||
if (config.settings.participant_id) {
|
||||
body.participantId = config.settings.participant_id;
|
||||
}
|
||||
if (config.settings.participant_name) {
|
||||
params.append('participantName', config.settings.participant_name);
|
||||
body.participantName = config.settings.participant_name;
|
||||
}
|
||||
const { accessToken } = await fetch(`/api/token?${params}`).then((res) =>
|
||||
if (config.settings.metadata) {
|
||||
body.metadata = config.settings.metadata;
|
||||
}
|
||||
const attributesArray = Array.isArray(config.settings.attributes) ? config.settings.attributes : [];
|
||||
if (attributesArray?.length) {
|
||||
const attributes = attributesArray.reduce((acc, attr) => {
|
||||
if (attr.key){
|
||||
acc[attr.key] = attr.value;
|
||||
}
|
||||
return acc;
|
||||
}, {} as Record<string, string>);
|
||||
body.attributes = attributes;
|
||||
}
|
||||
const { accessToken } = await fetch(`/api/token`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
}).then((res) =>
|
||||
res.json()
|
||||
);
|
||||
token = accessToken;
|
||||
@ -76,7 +98,9 @@ export const ConnectionProvider = ({
|
||||
config.settings.token,
|
||||
config.settings.ws_url,
|
||||
config.settings.room_name,
|
||||
config.settings.participant_name,
|
||||
config.settings.participant_id,
|
||||
config.settings.metadata,
|
||||
config.settings.attributes,
|
||||
generateToken,
|
||||
setToastMessage,
|
||||
]
|
||||
|
||||
@ -13,4 +13,10 @@ export interface SessionProps {
|
||||
export interface TokenResult {
|
||||
identity: string;
|
||||
accessToken: string;
|
||||
}
|
||||
|
||||
export interface AttributeItem {
|
||||
id: string;
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
@ -19,20 +19,38 @@ export default async function handleToken(
|
||||
res: NextApiResponse
|
||||
) {
|
||||
try {
|
||||
if (req.method !== "POST") {
|
||||
res.setHeader("Allow", "POST");
|
||||
res.status(405).end("Method Not Allowed");
|
||||
return;
|
||||
}
|
||||
if (!apiKey || !apiSecret) {
|
||||
res.statusMessage = "Environment variables aren't set up correctly";
|
||||
res.status(500).end();
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
roomName: roomNameFromBody,
|
||||
participantName,
|
||||
participantId: participantIdFromBody,
|
||||
metadata: metadataFromBody,
|
||||
attributes: attributesFromBody,
|
||||
} = req.body;
|
||||
|
||||
// Get room name from query params or generate random one
|
||||
const roomName = req.query.roomName as string ||
|
||||
const roomName = roomNameFromBody as string ||
|
||||
`room-${generateRandomAlphanumeric(4)}-${generateRandomAlphanumeric(4)}`;
|
||||
|
||||
|
||||
// Get participant name from query params or generate random one
|
||||
const identity = req.query.participantName as string ||
|
||||
const identity = participantIdFromBody as string ||
|
||||
`identity-${generateRandomAlphanumeric(4)}`;
|
||||
|
||||
// Get metadata and attributes from query params
|
||||
const metadata = metadataFromBody as string | undefined;
|
||||
const attributesStr = attributesFromBody as string | undefined;
|
||||
const attributes = attributesStr || {};
|
||||
|
||||
const grant: VideoGrant = {
|
||||
room: roomName,
|
||||
roomJoin: true,
|
||||
@ -41,7 +59,7 @@ export default async function handleToken(
|
||||
canSubscribe: true,
|
||||
};
|
||||
|
||||
const token = await createToken({ identity }, grant);
|
||||
const token = await createToken({ identity, metadata, attributes, name: participantName }, grant);
|
||||
const result: TokenResult = {
|
||||
identity,
|
||||
accessToken: token,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user