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 { ReactNode, useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import tailwindTheme from "../../lib/tailwindTheme.preval";
|
import tailwindTheme from "../../lib/tailwindTheme.preval";
|
||||||
import { EditableNameValueRow } from "@/components/config/NameValueRow";
|
import { EditableNameValueRow } from "@/components/config/NameValueRow";
|
||||||
|
import { AttributesEditor } from "@/components/config/AttributesEditor";
|
||||||
|
|
||||||
export interface PlaygroundMeta {
|
export interface PlaygroundMeta {
|
||||||
name: string;
|
name: string;
|
||||||
@ -260,9 +261,23 @@ export default function Playground({
|
|||||||
editable={roomState !== ConnectionState.Connected}
|
editable={roomState !== ConnectionState.Connected}
|
||||||
/>
|
/>
|
||||||
<EditableNameValueRow
|
<EditableNameValueRow
|
||||||
name="Participant"
|
name="Participant ID"
|
||||||
value={roomState === ConnectionState.Connected ?
|
value={roomState === ConnectionState.Connected ?
|
||||||
(localParticipant?.identity || '') :
|
(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 || '')}
|
(config.settings.participant_name || '')}
|
||||||
valueColor={`${config.settings.theme_color}-500`}
|
valueColor={`${config.settings.theme_color}-500`}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
@ -270,10 +285,39 @@ export default function Playground({
|
|||||||
newSettings.participant_name = value;
|
newSettings.participant_name = value;
|
||||||
setUserSettings(newSettings);
|
setUserSettings(newSettings);
|
||||||
}}
|
}}
|
||||||
placeholder="Enter participant id"
|
placeholder="Enter participant name"
|
||||||
editable={roomState !== ConnectionState.Connected}
|
editable={roomState !== ConnectionState.Connected}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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="flex flex-col gap-2 mt-4">
|
||||||
<div className="text-xs text-gray-500 mt-2">RPC Method</div>
|
<div className="text-xs text-gray-500 mt-2">RPC Method</div>
|
||||||
<input
|
<input
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { AttributeItem } from "@/lib/types";
|
||||||
import { getCookie, setCookie } from "cookies-next";
|
import { getCookie, setCookie } from "cookies-next";
|
||||||
import jsYaml from "js-yaml";
|
import jsYaml from "js-yaml";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
@ -36,7 +37,10 @@ export type UserSettings = {
|
|||||||
ws_url: string;
|
ws_url: string;
|
||||||
token: string;
|
token: string;
|
||||||
room_name: string;
|
room_name: string;
|
||||||
|
participant_id: string;
|
||||||
participant_name: string;
|
participant_name: string;
|
||||||
|
metadata?: string;
|
||||||
|
attributes?: AttributeItem[];
|
||||||
};
|
};
|
||||||
|
|
||||||
// Fallback if NEXT_PUBLIC_APP_CONFIG is not set
|
// Fallback if NEXT_PUBLIC_APP_CONFIG is not set
|
||||||
@ -60,7 +64,10 @@ const defaultConfig: AppConfig = {
|
|||||||
ws_url: "",
|
ws_url: "",
|
||||||
token: "",
|
token: "",
|
||||||
room_name: "",
|
room_name: "",
|
||||||
|
participant_id: "",
|
||||||
participant_name: "",
|
participant_name: "",
|
||||||
|
metadata: "",
|
||||||
|
attributes: [],
|
||||||
},
|
},
|
||||||
show_qr: false,
|
show_qr: false,
|
||||||
};
|
};
|
||||||
@ -130,6 +137,7 @@ export const ConfigProvider = ({ children }: { children: React.ReactNode }) => {
|
|||||||
ws_url: "",
|
ws_url: "",
|
||||||
token: "",
|
token: "",
|
||||||
room_name: "",
|
room_name: "",
|
||||||
|
participant_id: "",
|
||||||
participant_name: "",
|
participant_name: "",
|
||||||
} as UserSettings;
|
} as UserSettings;
|
||||||
}, [appConfig]);
|
}, [appConfig]);
|
||||||
|
|||||||
@ -54,14 +54,36 @@ export const ConnectionProvider = ({
|
|||||||
throw new Error("NEXT_PUBLIC_LIVEKIT_URL is not set");
|
throw new Error("NEXT_PUBLIC_LIVEKIT_URL is not set");
|
||||||
}
|
}
|
||||||
url = process.env.NEXT_PUBLIC_LIVEKIT_URL;
|
url = process.env.NEXT_PUBLIC_LIVEKIT_URL;
|
||||||
const params = new URLSearchParams();
|
const body: Record<string, any> = {};
|
||||||
if (config.settings.room_name) {
|
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) {
|
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()
|
res.json()
|
||||||
);
|
);
|
||||||
token = accessToken;
|
token = accessToken;
|
||||||
@ -76,7 +98,9 @@ export const ConnectionProvider = ({
|
|||||||
config.settings.token,
|
config.settings.token,
|
||||||
config.settings.ws_url,
|
config.settings.ws_url,
|
||||||
config.settings.room_name,
|
config.settings.room_name,
|
||||||
config.settings.participant_name,
|
config.settings.participant_id,
|
||||||
|
config.settings.metadata,
|
||||||
|
config.settings.attributes,
|
||||||
generateToken,
|
generateToken,
|
||||||
setToastMessage,
|
setToastMessage,
|
||||||
]
|
]
|
||||||
|
|||||||
@ -13,4 +13,10 @@ export interface SessionProps {
|
|||||||
export interface TokenResult {
|
export interface TokenResult {
|
||||||
identity: string;
|
identity: string;
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AttributeItem {
|
||||||
|
id: string;
|
||||||
|
key: string;
|
||||||
|
value: string;
|
||||||
}
|
}
|
||||||
@ -19,20 +19,38 @@ export default async function handleToken(
|
|||||||
res: NextApiResponse
|
res: NextApiResponse
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
|
if (req.method !== "POST") {
|
||||||
|
res.setHeader("Allow", "POST");
|
||||||
|
res.status(405).end("Method Not Allowed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!apiKey || !apiSecret) {
|
if (!apiKey || !apiSecret) {
|
||||||
res.statusMessage = "Environment variables aren't set up correctly";
|
res.statusMessage = "Environment variables aren't set up correctly";
|
||||||
res.status(500).end();
|
res.status(500).end();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
roomName: roomNameFromBody,
|
||||||
|
participantName,
|
||||||
|
participantId: participantIdFromBody,
|
||||||
|
metadata: metadataFromBody,
|
||||||
|
attributes: attributesFromBody,
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
// Get room name from query params or generate random one
|
// 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)}`;
|
`room-${generateRandomAlphanumeric(4)}-${generateRandomAlphanumeric(4)}`;
|
||||||
|
|
||||||
// Get participant name from query params or generate random one
|
// 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)}`;
|
`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 = {
|
const grant: VideoGrant = {
|
||||||
room: roomName,
|
room: roomName,
|
||||||
roomJoin: true,
|
roomJoin: true,
|
||||||
@ -41,7 +59,7 @@ export default async function handleToken(
|
|||||||
canSubscribe: true,
|
canSubscribe: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
const token = await createToken({ identity }, grant);
|
const token = await createToken({ identity, metadata, attributes, name: participantName }, grant);
|
||||||
const result: TokenResult = {
|
const result: TokenResult = {
|
||||||
identity,
|
identity,
|
||||||
accessToken: token,
|
accessToken: token,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user