video chat attempt

This commit is contained in:
Jeff Emmett 2024-09-04 17:52:58 +02:00
parent 836d37df76
commit 8ce8dec8f7
7 changed files with 238 additions and 33 deletions

View File

@ -10,42 +10,99 @@ import {
import { useParams } from 'react-router-dom' // Add this import
import { ChatBoxTool } from '@/tools/ChatBoxTool'
import { IChatBoxShape, ChatBoxShape } from '@/shapes/ChatBoxShape'
import { VideoChatTool } from '@/tools/VideoChatTool'
import { IVideoChatShape, VideoChatShape } from '@/shapes/VideoChatShape'
import { multiplayerAssetStore } from '../client/multiplayerAssetStore' // Adjusted path if necessary
import { customSchema } from '../../worker/TldrawDurableObject'
import './ChatBoxStyles.css' // Add a CSS file for styles
import React, { useState, useEffect, useRef } from 'react'; // Ensure useRef is imported
import { ChatBox } from '@/shapes/ChatBoxShape'; // Add this import
import { VideoChat } from '@/shapes/VideoChatShape';
const WORKER_URL = `https://jeffemmett-canvas.jeffemmett.workers.dev`
const shapeUtils = [ChatBoxShape]
const tools = [ChatBoxTool]
const shapeUtils = [ChatBoxShape, VideoChatShape]
const tools = [ChatBoxTool, VideoChatTool]; // Array of tools
// Function to register tools
const registerTools = (store: any, registeredToolsRef: React.MutableRefObject<Set<string>>) => {
const typedStore = store as unknown as {
registerTool: (tool: any) => void;
hasTool: (id: string) => boolean;
};
tools.forEach(tool => {
if (!registeredToolsRef.current.has(tool.id) && typedStore.hasTool && !typedStore.hasTool(tool.id)) {
typedStore.registerTool(tool); // Register the tool
registeredToolsRef.current.add(tool.id); // Mark this tool as registered
}
});
};
export function Board() {
// Extract the slug from the URL
const { slug } = useParams<{ slug: string }>()
const { slug } = useParams<{ slug: string }>(); // Ensure this is inside the Board component
const roomId = slug || 'default-room'; // Declare roomId here
// Use the slug as the roomId, or fallback to 'default-room' if not provided
const roomId = slug || 'default-room'
// Create a store connected to multiplayer.
const store = useSync({
// Use the dynamic roomId in the URI
uri: `${WORKER_URL}/connect/${roomId}`,
// ...and how to handle static assets like images & videos
assets: multiplayerAssetStore,
shapeUtils: shapeUtils,
schema: customSchema
})
});
const [isChatBoxVisible, setChatBoxVisible] = useState(false);
const [userName, setUserName] = useState('');
const [isVideoChatVisible, setVideoChatVisible] = useState(false); // Added state for video chat visibility
const registeredToolsRef = useRef(new Set<string>()); // Ref to track registered tools
// Call the function to register tools only once
useEffect(() => {
registerTools(store, registeredToolsRef);
}, [store]); // Ensure this effect runs when the store changes
// const videoChatTool = {
// id: 'videoChatTool', // Ensure this ID is unique
// // ... other properties of the tool
// };
//const typedStore = store as unknown as {
// registerTool: (tool: any) => void;
// hasTool: (id: string) => boolean; // Ensure hasTool is included
//};
//if (typedStore.hasTool && !typedStore.hasTool(videoChatTool.id)) { // Check if the tool ID is unique
// typedStore.registerTool(videoChatTool); // Register the video chat tool
//} else {
// console.error(`Tool with id "${videoChatTool.id}" is already registered. Cannot register again.`); // Log an error
//}
// useEffect(() => {
// tools.forEach(tool => {
// const typedStore = store as unknown as {
// registerTool: (tool: any) => void;
// hasTool: (id: string) => boolean; // Ensure hasTool is included
// };
// if (typedStore.hasTool && !typedStore.hasTool(tool.id)) { // Check if hasTool exists
// typedStore.registerTool(tool); // Use the typed store
// } else {
// console.warn(`Tool with id "${tool.id}" is already registered. Cannot register again.`); // Log an error
// }
// });
// }, [store]); // Run this effect when the store changes
const handleNameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setUserName(event.target.value);
};
return (
<div style={{ position: 'fixed', inset: 0 }}>
<Tldraw
// we can pass the connected store into the Tldraw component which will handle
// loading states & enable multiplayer UX like cursors & a presence menu
store={store}
shapeUtils={shapeUtils}
tools={tools}
onMount={(editor) => {
// when the editor is ready, we need to register out bookmark unfurling service
editor.registerExternalAssetHandler('url', unfurlBookmarkUrl)
editor.createShape<IChatBoxShape>({
type: 'chatBox',
@ -56,9 +113,43 @@ export function Board() {
h: 200,
roomId: roomId,
},
})
});
//if (isVideoChatVisible) {
//editor.registerExternalAssetHandler('url', unfurlBookmarkUrl)
editor.createShape<IVideoChatShape>({
type: 'videoChat',
x: 300, // Adjust position as needed
y: 0,
props: {
roomUrl: 'https://whereby.com/default-room', // Default room URL
w: 640,
h: 480,
},
});
//}
}}
/>
{isChatBoxVisible && (
<div>
<input
type="text"
value={userName}
onChange={handleNameChange}
placeholder="Enter your name"
/>
<ChatBox
userName={userName}
roomId={roomId} // Added roomId
width={200} // Set appropriate width
height={200} // Set appropriate height
/>
</div>
)}
{isVideoChatVisible && ( // Render the button to join video chat
<button onClick={() => setVideoChatVisible(false)} className="bg-green-500 text-white px-4 py-2 rounded">
Join Video Call
</button>
)}
</div>
)
}

View File

@ -66,6 +66,9 @@ export class PhysicsWorld {
this.createGroup(shape as TLGroupShape);
break;
// Add cases for any new shape types here
case "videoChat":
this.createShape (shape as TLGeoShape);
break;
}
}
}

View File

@ -7,6 +7,7 @@ export type IChatBoxShape = TLBaseShape<
w: number
h: number
roomId: string
userName: string
}
>
@ -18,6 +19,7 @@ export class ChatBoxShape extends BaseBoxShapeUtil<IChatBoxShape> {
roomId: 'default-room',
w: 100,
h: 100,
userName: '',
}
}
@ -27,7 +29,7 @@ export class ChatBoxShape extends BaseBoxShapeUtil<IChatBoxShape> {
component(shape: IChatBoxShape) {
return (
<ChatBox roomId={shape.props.roomId} width={shape.props.w} height={shape.props.h} />
<ChatBox roomId={shape.props.roomId} width={shape.props.w} height={shape.props.h} userName="" />
)
}
}
@ -39,11 +41,18 @@ interface Message {
timestamp: Date;
}
// Add this new component after the ChatBoxShape class
function ChatBox({ roomId, width, height }: { roomId: string, width: number, height: number }) {
interface ChatBoxProps {
roomId: string;
width: number;
height: number;
userName: string; // Add this line
}
// Update the ChatBox component to accept userName
export const ChatBox: React.FC<ChatBoxProps> = ({ roomId, width, height, userName }) => {
const [messages, setMessages] = useState<Message[]>([]);
const [inputMessage, setInputMessage] = useState("");
const [username, setUsername] = useState("jeff");
const [username, setUsername] = useState(userName);
const messagesEndRef = useRef(null);
useEffect(() => {

View File

@ -0,0 +1,93 @@
import { BaseBoxShapeUtil, TLBaseShape } from "tldraw";
import React, { useEffect, useState } from "react"; // Updated import for React
export type IVideoChatShape = TLBaseShape<
'videoChat',
{
w: number;
h: number;
roomUrl: string; // Changed from roomId to roomUrl for Whereby
userName: string;
}
>;
export class VideoChatShape extends BaseBoxShapeUtil <IVideoChatShape> {
static override type = 'videoChat';
getDefaultProps(): IVideoChatShape['props'] {
return {
roomUrl: 'https://whereby.com/default-room', // Default Whereby room URL
w: 640,
h: 480,
userName: ''
};
}
indicator(shape: IVideoChatShape) {
return <rect x={0} y={0} width={shape.props.w} height={shape.props.h} />;
}
component(shape: IVideoChatShape) {
return <VideoChat roomUrl={shape.props.roomUrl} />;
}
}
interface VideoChatProps {
roomUrl: string;
// Remove width and height as they are not used
// width: number;
// height: number;
// Remove userName as it is not used
// userName: string;
}
// VideoChat component using Whereby
export const VideoChat: React.FC<VideoChatProps> = () => { // Removed roomUrl from props
// Remove unused destructured props
// const [roomUrl, setRoomUrl] = useState(initialRoomUrl); // Use initialRoomUrl to avoid duplicate identifier
// const [roomUrl, setRoomUrl] = useState(roomId); // Use roomId instead
const [isInRoom, setIsInRoom] = useState(false);
const [error, setError] = useState("");
const [isLoading, setIsLoading] = useState(false);
// Automatically show the button on load
useEffect(() => {
joinRoom();
}, []);
const joinRoom = async () => {
setError("");
setIsLoading(true);
try {
const response = await fetch('/api/get-or-create-room', { method: 'GET' });
const data: { roomUrl?: string; error?: string } = await response.json(); // Explicitly type 'data'
if (data.roomUrl) {
// setRoomUrl(data.roomUrl); // Remove this line
setIsInRoom(true);
} else {
setError(data.error || "Failed to join room. Please try again.");
}
} catch (e) {
console.error("Error joining room:", e);
setError("An error occurred. Please try again.");
}
setIsLoading(false);
};
const leaveRoom = () => {
setIsInRoom(false);
// setRoomUrl(""); // Remove this line
};
return (
<div>
{!isInRoom && ( // Show button if not in room
<button onClick={() => setIsInRoom(true)} className="bg-green-500 text-white px-4 py-2 rounded">
Join Video Chat
</button>
)}
{/* Render Video Chat UI here when isInRoom is true */}
{isInRoom && <div>Video Chat UI goes here</div>}
</div>
);
}

View File

@ -1,6 +1,6 @@
import { BaseBoxShapeTool } from "tldraw";
export class ChatBoxTool extends BaseBoxShapeTool {
shapeType = 'chatBox'
override initial = 'idle'
shapeType = 'chatBox';
override initial = 'idle';
}

View File

@ -0,0 +1,8 @@
import { BaseBoxShapeTool } from "tldraw";
export class VideoChatTool extends BaseBoxShapeTool {
shapeType = 'videoChat';
override initial = 'idle';
// Additional methods for handling video chat functionality can be added here
}

View File

@ -11,10 +11,11 @@ import { AutoRouter, IRequest, error } from 'itty-router'
import throttle from 'lodash.throttle'
import { Environment } from './types'
import { ChatBoxShape } from '@/shapes/ChatBoxShape'
import { VideoChatShape } from '@/shapes/VideoChatShape'
// add custom shapes and bindings here if needed:
export const customSchema = createTLSchema({
shapes: { ...defaultShapeSchemas, chatBox: ChatBoxShape },
shapes: { ...defaultShapeSchemas, chatBox: ChatBoxShape, videoChat: VideoChatShape },
// bindings: { ...defaultBindingSchemas },
})