Merge pull request #1 from Jeff-Emmett/Video-Chat-Attempt
Video chat attempt
This commit is contained in:
commit
d81ae56de0
|
|
@ -26,7 +26,7 @@
|
||||||
<meta name="twitter:card" content="summary_large_image">
|
<meta name="twitter:card" content="summary_large_image">
|
||||||
<meta property="twitter:domain" content="jeffemmett.com">
|
<meta property="twitter:domain" content="jeffemmett.com">
|
||||||
<meta property="twitter:url" content="https://jeffemmett.com">
|
<meta property="twitter:url" content="https://jeffemmett.com">
|
||||||
<meta name="twitter:title" content="Orion Reed">
|
<meta name="twitter:title" content="Jeff Emmett">
|
||||||
<meta name="twitter:description"
|
<meta name="twitter:description"
|
||||||
content="My research doesn't investigate the intersection of computing, human-system interfaces, and emancipatory politics. I am interested in the potential of computing as a medium for thought, as a tool for collective action, and as a means of emancipation.">
|
content="My research doesn't investigate the intersection of computing, human-system interfaces, and emancipatory politics. I am interested in the potential of computing as a medium for thought, as a tool for collective action, and as a means of emancipation.">
|
||||||
<meta name="twitter:image" content="/website-embed.png">
|
<meta name="twitter:image" content="/website-embed.png">
|
||||||
|
|
|
||||||
10
package.json
10
package.json
|
|
@ -12,13 +12,13 @@
|
||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "Orion Reed",
|
"author": "Jeff Emmett",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dimforge/rapier2d": "^0.11.2",
|
"@dimforge/rapier2d": "^0.11.2",
|
||||||
"@tldraw/sync": "^2.4.6",
|
"@tldraw/sync": "^2.4.6",
|
||||||
"@tldraw/sync-core": "latest",
|
"@tldraw/sync-core": "^2.4.6",
|
||||||
"@tldraw/tlschema": "latest",
|
"@tldraw/tlschema": "^2.4.6",
|
||||||
"@types/markdown-it": "^14.1.1",
|
"@types/markdown-it": "^14.1.1",
|
||||||
"@vercel/analytics": "^1.2.2",
|
"@vercel/analytics": "^1.2.2",
|
||||||
"cloudflare-workers-unfurl": "^0.0.7",
|
"cloudflare-workers-unfurl": "^0.0.7",
|
||||||
|
|
@ -47,11 +47,11 @@
|
||||||
"eslint": "^8.38.0",
|
"eslint": "^8.38.0",
|
||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
"eslint-plugin-react-refresh": "^0.3.4",
|
"eslint-plugin-react-refresh": "^0.3.4",
|
||||||
"typescript": "^5.0.2",
|
"typescript": "^5.6.3",
|
||||||
"vite": "^5.3.3",
|
"vite": "^5.3.3",
|
||||||
"vite-plugin-static-copy": "^1.0.6",
|
"vite-plugin-static-copy": "^1.0.6",
|
||||||
"vite-plugin-top-level-await": "^1.3.1",
|
"vite-plugin-top-level-await": "^1.3.1",
|
||||||
"vite-plugin-wasm": "^3.2.2",
|
"vite-plugin-wasm": "^3.2.2",
|
||||||
"wrangler": "^3.72.3"
|
"wrangler": "^3.72.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
84
src/App.tsx
84
src/App.tsx
|
|
@ -1,37 +1,94 @@
|
||||||
import { inject } from '@vercel/analytics';
|
import { inject } from '@vercel/analytics';
|
||||||
import "tldraw/tldraw.css";
|
import "tldraw/tldraw.css";
|
||||||
import "@/css/style.css"
|
import "@/css/style.css"
|
||||||
import React, { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import ReactDOM from "react-dom/client";
|
import ReactDOM from "react-dom/client";
|
||||||
import { Default } from "@/components/Default";
|
import { Default } from "@/components/Default";
|
||||||
import { Canvas } from "@/components/Canvas";
|
import { Canvas } from "@/components/Canvas";
|
||||||
import { Toggle } from "@/components/Toggle";
|
import { Toggle } from "@/components/Toggle";
|
||||||
import { useCanvas } from "@/hooks/useCanvas"
|
import { useCanvas } from "@/hooks/useCanvas"
|
||||||
import { createShapes } from "@/utils";
|
import { createShapes } from "@/utils/utils";
|
||||||
import { BrowserRouter, Route, Routes } from 'react-router-dom';
|
import { BrowserRouter, Route, Routes } from 'react-router-dom';
|
||||||
import { Contact } from "@/components/Contact";
|
import { Contact } from "@/components/Contact";
|
||||||
import { Post } from '@/components/Post';
|
import { Post } from '@/components/Post';
|
||||||
import { Board } from './components/Board';
|
import { Board } from './components/Board';
|
||||||
import { Inbox } from './components/Inbox';
|
import { Inbox } from './components/Inbox';
|
||||||
import { Books } from './components/Books';
|
import { Books } from './components/Books';
|
||||||
|
import {
|
||||||
|
BindingUtil,
|
||||||
|
IndexKey,
|
||||||
|
TLBaseBinding,
|
||||||
|
TLBaseShape,
|
||||||
|
Tldraw,
|
||||||
|
} from 'tldraw';
|
||||||
|
import { components, uiOverrides } from './ui-overrides';
|
||||||
|
import { ChatBoxShape } from './shapes/ChatBoxShapeUtil';
|
||||||
|
import { VideoChatShape } from './shapes/VideoChatShapeUtil';
|
||||||
|
import { ChatBoxTool } from './tools/ChatBoxTool';
|
||||||
|
import { VideoChatTool } from './tools/VideoChatTool';
|
||||||
|
|
||||||
inject();
|
inject();
|
||||||
|
|
||||||
|
// The container shapes that can contain element shapes
|
||||||
|
const CONTAINER_PADDING = 24;
|
||||||
|
|
||||||
|
type ContainerShape = TLBaseShape<'element', { height: number; width: number }>;
|
||||||
|
|
||||||
|
// ... existing code for ContainerShapeUtil ...
|
||||||
|
|
||||||
|
// The element shapes that can be placed inside the container shapes
|
||||||
|
type ElementShape = TLBaseShape<'element', { color: string }>;
|
||||||
|
|
||||||
|
// ... existing code for ElementShapeUtil ...
|
||||||
|
|
||||||
|
// The binding between the element shapes and the container shapes
|
||||||
|
type LayoutBinding = TLBaseBinding<
|
||||||
|
'layout',
|
||||||
|
{
|
||||||
|
index: IndexKey;
|
||||||
|
placeholder: boolean;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
|
const customShapeUtils = [ChatBoxShape, VideoChatShape];
|
||||||
|
const customTools = [ChatBoxTool, VideoChatTool];
|
||||||
|
|
||||||
|
// [2]
|
||||||
|
export default function InteractiveShapeExample() {
|
||||||
|
return (
|
||||||
|
<div className="tldraw__editor">
|
||||||
|
<Tldraw
|
||||||
|
|
||||||
|
shapeUtils={customShapeUtils} // Use custom shape utils
|
||||||
|
tools={customTools} // Pass in the array of custom tool classes
|
||||||
|
overrides={uiOverrides}
|
||||||
|
components={components}
|
||||||
|
onMount={(editor) => {
|
||||||
|
editor.createShape({ type: 'my-interactive-shape', x: 100, y: 100 });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ... existing code ...
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById("root")!).render(<App />);
|
ReactDOM.createRoot(document.getElementById("root")!).render(<App />);
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// <React.StrictMode>
|
// <React.StrictMode>
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Home />} />
|
<Route path="/" element={<Home />} />
|
||||||
<Route path="/card/contact" element={<Contact />} />
|
<Route path="/card/contact" element={<Contact />} />
|
||||||
<Route path="/posts/:slug" element={<Post />} />
|
<Route path="/posts/:slug" element={<Post />} />
|
||||||
<Route path="/board/:slug" element={<Board />} />
|
<Route path="/board/:slug" element={<Board />} />
|
||||||
<Route path="/inbox" element={<Inbox />} />
|
<Route path="/inbox" element={<Inbox />} />
|
||||||
<Route path="/books" element={<Books />} />
|
<Route path="/books" element={<Books />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
// </React.StrictMode>
|
// </React.StrictMode>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
@ -58,5 +115,6 @@ function Home() {
|
||||||
<div style={{ zIndex: 999999 }} className={`${isCanvasEnabled && isEditorMounted ? 'transparent' : ''}`}>
|
<div style={{ zIndex: 999999 }} className={`${isCanvasEnabled && isEditorMounted ? 'transparent' : ''}`}>
|
||||||
{<Default />}
|
{<Default />}
|
||||||
</div>
|
</div>
|
||||||
{isCanvasEnabled && elementsInfo.length > 0 ? <Canvas shapes={shapes} /> : null}</>)
|
{isCanvasEnabled && elementsInfo.length > 0 ? <Canvas shapes={shapes} /> : null}</>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -2,83 +2,82 @@ import { useSync } from '@tldraw/sync'
|
||||||
import {
|
import {
|
||||||
AssetRecordType,
|
AssetRecordType,
|
||||||
getHashForString,
|
getHashForString,
|
||||||
TLAssetStore,
|
|
||||||
TLBookmarkAsset,
|
TLBookmarkAsset,
|
||||||
Tldraw,
|
Tldraw,
|
||||||
uniqueId,
|
|
||||||
} from 'tldraw'
|
} from 'tldraw'
|
||||||
import { useParams } from 'react-router-dom' // Add this import
|
import { useParams } from 'react-router-dom'
|
||||||
import { ChatBoxTool } from '@/tools/ChatBoxTool'
|
import { ChatBoxTool } from '@/tools/ChatBoxTool'
|
||||||
import { IChatBoxShape, ChatBoxShape } from '@/shapes/ChatBoxShape'
|
import { ChatBoxShape } from '@/shapes/ChatBoxShapeUtil'
|
||||||
import { multiplayerAssetStore } from '../client/multiplayerAssetStore' // Adjusted path if necessary
|
import { VideoChatTool } from '@/tools/VideoChatTool'
|
||||||
|
import { VideoChatShape } from '@/shapes/VideoChatShapeUtil'
|
||||||
|
import { multiplayerAssetStore } from '../client/multiplayerAssetStore'
|
||||||
import { customSchema } from '../../worker/TldrawDurableObject'
|
import { customSchema } from '../../worker/TldrawDurableObject'
|
||||||
import './ChatBoxStyles.css' // Add a CSS file for styles
|
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { chatBox } from '@/shapes/ChatBoxShapeUtil';
|
||||||
|
import { components, uiOverrides } from '@/ui-overrides'
|
||||||
|
|
||||||
const WORKER_URL = `https://jeffemmett-canvas.jeffemmett.workers.dev`
|
const WORKER_URL = `https://jeffemmett-canvas.jeffemmett.workers.dev`
|
||||||
|
|
||||||
const shapeUtils = [ChatBoxShape]
|
const shapeUtils = [ChatBoxShape, VideoChatShape]
|
||||||
const tools = [ChatBoxTool]
|
const tools = [ChatBoxTool, VideoChatTool]; // Array of tools
|
||||||
|
|
||||||
export function Board() {
|
export function Board() {
|
||||||
// Extract the slug from the URL
|
const { slug } = useParams<{ slug: string }>(); // Ensure this is inside the Board component
|
||||||
const { slug } = useParams<{ slug: string }>()
|
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({
|
const store = useSync({
|
||||||
// Use the dynamic roomId in the URI
|
|
||||||
uri: `${WORKER_URL}/connect/${roomId}`,
|
uri: `${WORKER_URL}/connect/${roomId}`,
|
||||||
// ...and how to handle static assets like images & videos
|
|
||||||
assets: multiplayerAssetStore,
|
assets: multiplayerAssetStore,
|
||||||
shapeUtils: shapeUtils,
|
shapeUtils: shapeUtils,
|
||||||
schema: customSchema
|
schema: customSchema,
|
||||||
})
|
});
|
||||||
|
|
||||||
|
const [isChatBoxVisible, setChatBoxVisible] = useState(false);
|
||||||
|
const [userName, setUserName] = useState('');
|
||||||
|
const [isVideoChatVisible, setVideoChatVisible] = useState(false); // Added state for video chat visibility
|
||||||
|
|
||||||
|
const handleNameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setUserName(event.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ position: 'fixed', inset: 0 }}>
|
<div style={{ position: 'fixed', inset: 0 }}>
|
||||||
<Tldraw
|
<Tldraw
|
||||||
// we can pass the connected store into the Tldraw component which will handle
|
store={store}
|
||||||
// loading states & enable multiplayer UX like cursors & a presence menu
|
|
||||||
store={store}
|
|
||||||
shapeUtils={shapeUtils}
|
shapeUtils={shapeUtils}
|
||||||
|
overrides={uiOverrides}
|
||||||
|
components={components}
|
||||||
tools={tools}
|
tools={tools}
|
||||||
onMount={(editor) => {
|
onMount={(editor) => {
|
||||||
// when the editor is ready, we need to register out bookmark unfurling service
|
|
||||||
editor.registerExternalAssetHandler('url', unfurlBookmarkUrl)
|
editor.registerExternalAssetHandler('url', unfurlBookmarkUrl)
|
||||||
editor.createShape<IChatBoxShape>({
|
|
||||||
type: 'chatBox',
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
props: {
|
|
||||||
w: 200,
|
|
||||||
h: 200,
|
|
||||||
roomId: roomId,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
{isChatBoxVisible && (
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={userName}
|
||||||
|
onChange={handleNameChange}
|
||||||
|
placeholder="Enter your name"
|
||||||
|
/>
|
||||||
|
<chatBox
|
||||||
|
userName={userName}
|
||||||
|
roomId={roomId} // Added roomId
|
||||||
|
w={200} // Set appropriate width
|
||||||
|
h={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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Assuming you have a message structure like this
|
|
||||||
interface ChatMessage {
|
|
||||||
id: string;
|
|
||||||
text: string;
|
|
||||||
isUser: boolean; // New property to identify the sender
|
|
||||||
}
|
|
||||||
|
|
||||||
// Example rendering function for messages
|
|
||||||
function renderMessage(message: ChatMessage) {
|
|
||||||
return (
|
|
||||||
<div className={message.isUser ? 'user-message' : 'other-message'}>
|
|
||||||
{message.text}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// How does our server handle bookmark unfurling?
|
// How does our server handle bookmark unfurling?
|
||||||
async function unfurlBookmarkUrl({ url }: { url: string }): Promise<TLBookmarkAsset> {
|
async function unfurlBookmarkUrl({ url }: { url: string }): Promise<TLBookmarkAsset> {
|
||||||
const asset: TLBookmarkAsset = {
|
const asset: TLBookmarkAsset = {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Editor, Tldraw, TLShape, TLUiComponents } from "tldraw";
|
import { Editor, Tldraw, TLShape, TLUiComponents } from "tldraw";
|
||||||
import { SimController } from "@/physics/PhysicsControls";
|
import { SimController } from "@/physics/PhysicsControls";
|
||||||
import { HTMLShapeUtil } from "@/shapes/HTMLShapeUtil";
|
import { HTMLShapeUtil } from "@/utils/HTMLShapeUtil";
|
||||||
|
|
||||||
const components: TLUiComponents = {
|
const components: TLUiComponents = {
|
||||||
HelpMenu: null,
|
HelpMenu: null,
|
||||||
|
|
|
||||||
|
|
@ -66,6 +66,9 @@ export class PhysicsWorld {
|
||||||
this.createGroup(shape as TLGroupShape);
|
this.createGroup(shape as TLGroupShape);
|
||||||
break;
|
break;
|
||||||
// Add cases for any new shape types here
|
// Add cases for any new shape types here
|
||||||
|
case "VideoChat":
|
||||||
|
this.createShape (shape as TLGeoShape);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,14 @@
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { BaseBoxShapeUtil, TLBaseBoxShape, TLBaseShape, TldrawBaseProps } from "tldraw";
|
import { BaseBoxShapeUtil, TLBaseShape } from "tldraw";
|
||||||
|
|
||||||
export type IChatBoxShape = TLBaseShape<
|
export type IChatBoxShape = TLBaseShape<
|
||||||
'chatBox',
|
'chatBox',
|
||||||
{
|
{
|
||||||
w: number
|
w: number
|
||||||
h: number
|
h: number
|
||||||
roomId: string
|
roomId: string
|
||||||
}
|
userName: string
|
||||||
|
}
|
||||||
>
|
>
|
||||||
|
|
||||||
export class ChatBoxShape extends BaseBoxShapeUtil<IChatBoxShape> {
|
export class ChatBoxShape extends BaseBoxShapeUtil<IChatBoxShape> {
|
||||||
|
|
@ -18,6 +19,7 @@ export class ChatBoxShape extends BaseBoxShapeUtil<IChatBoxShape> {
|
||||||
roomId: 'default-room',
|
roomId: 'default-room',
|
||||||
w: 100,
|
w: 100,
|
||||||
h: 100,
|
h: 100,
|
||||||
|
userName: '',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -27,7 +29,7 @@ export class ChatBoxShape extends BaseBoxShapeUtil<IChatBoxShape> {
|
||||||
|
|
||||||
component(shape: IChatBoxShape) {
|
component(shape: IChatBoxShape) {
|
||||||
return (
|
return (
|
||||||
<ChatBox roomId={shape.props.roomId} width={shape.props.w} height={shape.props.h} />
|
<chatBox roomId={shape.props.roomId} w={shape.props.w} h={shape.props.h} userName="" />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -39,11 +41,14 @@ interface Message {
|
||||||
timestamp: Date;
|
timestamp: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add this new component after the ChatBoxShape class
|
|
||||||
function ChatBox({ roomId, width, height }: { roomId: string, width: number, height: number }) {
|
|
||||||
|
|
||||||
|
// Update the chatBox component to accept userName
|
||||||
|
export const chatBox: React.FC<IChatBoxShape['props']> = ({ roomId, w, h, userName }) => {
|
||||||
const [messages, setMessages] = useState<Message[]>([]);
|
const [messages, setMessages] = useState<Message[]>([]);
|
||||||
const [inputMessage, setInputMessage] = useState("");
|
const [inputMessage, setInputMessage] = useState("");
|
||||||
const [username, setUsername] = useState("jeff");
|
const [username, setUsername] = useState(userName);
|
||||||
const messagesEndRef = useRef(null);
|
const messagesEndRef = useRef(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -89,12 +94,12 @@ function ChatBox({ roomId, width, height }: { roomId: string, width: number, hei
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="chat-container" style={{ pointerEvents: 'all', width: `${width}px`, height: `${height}px`, overflow: 'auto' }}>
|
<div className="chat-container" style={{ pointerEvents: 'all', width: `${w}px`, height: `${h}px`, overflow: 'auto' }}>
|
||||||
<div className="messages-container">
|
<div className="messages-container">
|
||||||
{messages.map((msg) => (
|
{messages.map((msg) => (
|
||||||
<div key={msg.id} className={`message ${msg.username === username ? 'own-message' : ''}`}>
|
<div key={msg.id} className={`message ${msg.username === username ? 'own-message' : ''}`}>
|
||||||
<div className="message-header">
|
<div className="message-header">
|
||||||
<strong>{msg.username}</strong>
|
<strong>{msg.username}</strong>
|
||||||
<span className="timestamp">{new Date(msg.timestamp).toLocaleTimeString()}</span>
|
<span className="timestamp">{new Date(msg.timestamp).toLocaleTimeString()}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="message-content">{msg.content}</div>
|
<div className="message-content">{msg.content}</div>
|
||||||
|
|
@ -110,7 +115,7 @@ function ChatBox({ roomId, width, height }: { roomId: string, width: number, hei
|
||||||
placeholder="Type a message..."
|
placeholder="Type a message..."
|
||||||
className="message-input"
|
className="message-input"
|
||||||
/>
|
/>
|
||||||
<button type="submit" style={{ pointerEvents: 'all',}} onPointerDown={(e)=>e.stopPropagation()} className="send-button">Send</button>
|
<button type="submit" style={{ pointerEvents: 'all', }} onPointerDown={(e) => e.stopPropagation()} className="send-button">Send</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -118,24 +123,24 @@ function ChatBox({ roomId, width, height }: { roomId: string, width: number, hei
|
||||||
|
|
||||||
async function sendMessageToChat(roomId: string, username: string, content: string): Promise<void> {
|
async function sendMessageToChat(roomId: string, username: string, content: string): Promise<void> {
|
||||||
const apiUrl = 'https://jeffemmett-realtimechatappwithpolling.web.val.run'; // Replace with your actual Val Town URL
|
const apiUrl = 'https://jeffemmett-realtimechatappwithpolling.web.val.run'; // Replace with your actual Val Town URL
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${apiUrl}?action=sendMessage`, {
|
const response = await fetch(`${apiUrl}?action=sendMessage`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
mode: 'no-cors',
|
mode: 'no-cors',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
roomId,
|
roomId,
|
||||||
username,
|
username,
|
||||||
content,
|
content,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await response.text();
|
const result = await response.text();
|
||||||
console.log('Message sent successfully:', result);
|
console.log('Message sent successfully:', result);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error sending message:', error);
|
console.error('Error sending message:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,181 @@
|
||||||
|
import { BaseBoxShapeUtil, TLBaseShape } from "tldraw";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
export type IVideoChatShape = TLBaseShape<
|
||||||
|
'VideoChat',
|
||||||
|
{
|
||||||
|
w: number;
|
||||||
|
h: number;
|
||||||
|
roomUrl: string | null;
|
||||||
|
userName: string;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
|
const WHEREBY_API_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmFwcGVhci5pbiIsImF1ZCI6Imh0dHBzOi8vYXBpLmFwcGVhci5pbi92MSIsImV4cCI6OTAwNzE5OTI1NDc0MDk5MSwiaWF0IjoxNzI5MTkzOTE3LCJvcmdhbml6YXRpb25JZCI6MjY2MDk5LCJqdGkiOiI0MzI0MmUxMC1kZmRjLTRhYmEtYjlhOS01ZjcwNTFlMTYwZjAifQ.RaxXpZKYl_dOWyoATQZrzyMR2XRh3fHf02mALQiuTTs'; // Replace with your actual API key
|
||||||
|
const ROOM_PREFIX = 'test'
|
||||||
|
|
||||||
|
export class VideoChatShape extends BaseBoxShapeUtil<IVideoChatShape> {
|
||||||
|
static override type = 'VideoChat';
|
||||||
|
|
||||||
|
getDefaultProps(): IVideoChatShape['props'] {
|
||||||
|
return {
|
||||||
|
roomUrl: null,
|
||||||
|
w: 640,
|
||||||
|
h: 480,
|
||||||
|
userName: ''
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
indicator(shape: IVideoChatShape) {
|
||||||
|
return <rect x={0} y={0} width={shape.props.w} height={shape.props.h} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
async ensureRoomExists(shape: IVideoChatShape) {
|
||||||
|
|
||||||
|
console.log('This is your roomUrl 1:', shape.props.roomUrl);
|
||||||
|
|
||||||
|
if (shape.props.roomUrl !== null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const expiryDate = new Date(Date.now() + 1000 * 24 * 60 * 60 * 1000)
|
||||||
|
|
||||||
|
const response = await fetch('https://api.whereby.dev/v1/meetings', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
// 'Access-Control-Allow-Origin': 'http://localhost:5173/',
|
||||||
|
'Authorization': `Bearer ${WHEREBY_API_KEY}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
isLocked: false,
|
||||||
|
roomNamePrefix: ROOM_PREFIX,
|
||||||
|
roomMode: 'normal',
|
||||||
|
endDate: expiryDate.toISOString(),
|
||||||
|
fields: ['hostRoomUrl'],
|
||||||
|
}),
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error('Failed to create meeting:', error);
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('This is your response:', response);
|
||||||
|
|
||||||
|
console.log('This is your roomUrl 2:', shape.props.roomUrl);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json();
|
||||||
|
console.error('Whereby API error:', errorData);
|
||||||
|
throw new Error(`Whereby API error: ${(errorData as any).message || 'Unknown error'}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
const roomUrl = (data as any).roomUrl;
|
||||||
|
|
||||||
|
console.log('This is your roomUrl 3:', roomUrl);
|
||||||
|
|
||||||
|
this.editor.updateShape<IVideoChatShape>({
|
||||||
|
id: shape.id,
|
||||||
|
type: 'VideoChat',
|
||||||
|
props: {
|
||||||
|
...shape.props,
|
||||||
|
roomUrl
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
component(shape: IVideoChatShape) {
|
||||||
|
const [roomUrl, setRoomUrl] = useState(""); // Added roomUrl state
|
||||||
|
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 () => {
|
||||||
|
//console.log("HI IM A CONSOLE TEST")
|
||||||
|
this.ensureRoomExists(shape);
|
||||||
|
setError("");
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
// Generate a room name based on a default slug or any logic you prefer
|
||||||
|
// const roomNamePrefix = 'default-room'; // You can modify this logic as needed
|
||||||
|
|
||||||
|
// const response = await fetch('https://cors-anywhere.herokuapp.com/https://api.whereby.dev/v1/meetings', {
|
||||||
|
const response = await fetch('https://api.whereby.dev/v1/meetings', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${WHEREBY_API_KEY}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
isLocked: false,
|
||||||
|
roomNamePrefix: ROOM_PREFIX,
|
||||||
|
roomMode: 'normal',
|
||||||
|
endDate: new Date(Date.now() + 1000 * 24 * 60 * 60 * 1000).toISOString(), // 7 days from now
|
||||||
|
fields: ['hostRoomUrl'],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData: { message?: string } = await response.json(); // Explicitly type errorData
|
||||||
|
console.error('Whereby API error:', errorData);
|
||||||
|
throw new Error(`Whereby API error: ${errorData.message || 'Unknown error'}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data: { roomUrl: string } = await response.json(); // Explicitly type the response
|
||||||
|
setRoomUrl(data.roomUrl); // Set the room URL
|
||||||
|
setIsInRoom(true);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error joining room:", e);
|
||||||
|
setError("An error occurred. Please try again.");
|
||||||
|
}
|
||||||
|
setIsLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const leaveRoom = () => {
|
||||||
|
setIsInRoom(false);
|
||||||
|
setRoomUrl(""); // Clear the room URL
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-4" style={{ pointerEvents: 'all' }}>
|
||||||
|
<h1 className="text-2xl font-bold mb-4">Whereby Video Chat Room</h1>
|
||||||
|
{isLoading ? (
|
||||||
|
<p>Joining room...</p>
|
||||||
|
) : isInRoom ? (
|
||||||
|
<div className="mb-4">
|
||||||
|
<button onClick={leaveRoom} className="bg-red-500 text-white px-4 py-2 rounded mb-4">
|
||||||
|
Leave Room
|
||||||
|
</button>
|
||||||
|
<div className="aspect-w-16 aspect-h-9">
|
||||||
|
<iframe
|
||||||
|
src={`${roomUrl}?embed&iframeSource=val.town&background=off&logo=off&chat=off&screenshare=on&people=on`}
|
||||||
|
allow="camera; microphone; fullscreen; speaker; display-capture"
|
||||||
|
className="w-full h-full"
|
||||||
|
></iframe>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div>
|
||||||
|
<button onClick={joinRoom} className="bg-blue-500 text-white px-4 py-2 rounded">
|
||||||
|
Join Room
|
||||||
|
</button>
|
||||||
|
{error && <p className="text-red-500 mt-2">{error}</p>}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<p className="mt-4 text-sm text-gray-600">
|
||||||
|
View source: <a href={import.meta.url.replace("esm.town", "val.town")} className="text-blue-500 hover:underline">Val Town</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,210 @@
|
||||||
|
{
|
||||||
|
"store": {
|
||||||
|
"document:document": {
|
||||||
|
"gridSize": 10,
|
||||||
|
"name": "",
|
||||||
|
"meta": {},
|
||||||
|
"id": "document:document",
|
||||||
|
"typeName": "document"
|
||||||
|
},
|
||||||
|
"page:page": {
|
||||||
|
"meta": {},
|
||||||
|
"id": "page:page",
|
||||||
|
"name": "Page 1",
|
||||||
|
"index": "a1",
|
||||||
|
"typeName": "page"
|
||||||
|
},
|
||||||
|
"shape:f4LKGB_8M2qsyWGpHR5Dq": {
|
||||||
|
"x": 30.9375,
|
||||||
|
"y": 69.48828125,
|
||||||
|
"rotation": 0,
|
||||||
|
"isLocked": false,
|
||||||
|
"opacity": 1,
|
||||||
|
"meta": {},
|
||||||
|
"id": "shape:f4LKGB_8M2qsyWGpHR5Dq",
|
||||||
|
"type": "container",
|
||||||
|
"parentId": "page:page",
|
||||||
|
"index": "a1",
|
||||||
|
"props": {
|
||||||
|
"width": 644,
|
||||||
|
"height": 148
|
||||||
|
},
|
||||||
|
"typeName": "shape"
|
||||||
|
},
|
||||||
|
"shape:2oThF4kJ4v31xqKN5lvq2": {
|
||||||
|
"x": 550.9375,
|
||||||
|
"y": 93.48828125,
|
||||||
|
"rotation": 0,
|
||||||
|
"isLocked": false,
|
||||||
|
"opacity": 1,
|
||||||
|
"meta": {},
|
||||||
|
"id": "shape:2oThF4kJ4v31xqKN5lvq2",
|
||||||
|
"type": "element",
|
||||||
|
"props": {
|
||||||
|
"color": "#5BCEFA"
|
||||||
|
},
|
||||||
|
"parentId": "page:page",
|
||||||
|
"index": "a2",
|
||||||
|
"typeName": "shape"
|
||||||
|
},
|
||||||
|
"shape:K2vk_VTaNh-ANaRNOAvgY": {
|
||||||
|
"x": 426.9375,
|
||||||
|
"y": 93.48828125,
|
||||||
|
"rotation": 0,
|
||||||
|
"isLocked": false,
|
||||||
|
"opacity": 1,
|
||||||
|
"meta": {},
|
||||||
|
"id": "shape:K2vk_VTaNh-ANaRNOAvgY",
|
||||||
|
"type": "element",
|
||||||
|
"props": {
|
||||||
|
"color": "#F5A9B8"
|
||||||
|
},
|
||||||
|
"parentId": "page:page",
|
||||||
|
"index": "a3",
|
||||||
|
"typeName": "shape"
|
||||||
|
},
|
||||||
|
"shape:6uouhIK7PvyIRNQHACf-d": {
|
||||||
|
"x": 302.9375,
|
||||||
|
"y": 93.48828125,
|
||||||
|
"rotation": 0,
|
||||||
|
"isLocked": false,
|
||||||
|
"opacity": 1,
|
||||||
|
"meta": {},
|
||||||
|
"id": "shape:6uouhIK7PvyIRNQHACf-d",
|
||||||
|
"type": "element",
|
||||||
|
"props": {
|
||||||
|
"color": "#FFFFFF"
|
||||||
|
},
|
||||||
|
"parentId": "page:page",
|
||||||
|
"index": "a4",
|
||||||
|
"typeName": "shape"
|
||||||
|
},
|
||||||
|
"shape:GTQq2qxkWPHEK7KMIRtsh": {
|
||||||
|
"x": 54.9375,
|
||||||
|
"y": 93.48828125,
|
||||||
|
"rotation": 0,
|
||||||
|
"isLocked": false,
|
||||||
|
"opacity": 1,
|
||||||
|
"meta": {},
|
||||||
|
"id": "shape:GTQq2qxkWPHEK7KMIRtsh",
|
||||||
|
"type": "element",
|
||||||
|
"props": {
|
||||||
|
"color": "#5BCEFA"
|
||||||
|
},
|
||||||
|
"parentId": "page:page",
|
||||||
|
"index": "a5",
|
||||||
|
"typeName": "shape"
|
||||||
|
},
|
||||||
|
"shape:05jMujN6A0sIp6zzHMpbV": {
|
||||||
|
"x": 178.9375,
|
||||||
|
"y": 93.48828125,
|
||||||
|
"rotation": 0,
|
||||||
|
"isLocked": false,
|
||||||
|
"opacity": 1,
|
||||||
|
"meta": {},
|
||||||
|
"id": "shape:05jMujN6A0sIp6zzHMpbV",
|
||||||
|
"type": "element",
|
||||||
|
"props": {
|
||||||
|
"color": "#F5A9B8"
|
||||||
|
},
|
||||||
|
"parentId": "page:page",
|
||||||
|
"index": "a6",
|
||||||
|
"typeName": "shape"
|
||||||
|
},
|
||||||
|
"binding:iOBENBUHvzD8N7mBdIM5l": {
|
||||||
|
"meta": {},
|
||||||
|
"id": "binding:iOBENBUHvzD8N7mBdIM5l",
|
||||||
|
"type": "layout",
|
||||||
|
"fromId": "shape:f4LKGB_8M2qsyWGpHR5Dq",
|
||||||
|
"toId": "shape:05jMujN6A0sIp6zzHMpbV",
|
||||||
|
"props": {
|
||||||
|
"index": "a2",
|
||||||
|
"placeholder": false
|
||||||
|
},
|
||||||
|
"typeName": "binding"
|
||||||
|
},
|
||||||
|
"binding:YTIeOALEmHJk6dczRpQmE": {
|
||||||
|
"meta": {},
|
||||||
|
"id": "binding:YTIeOALEmHJk6dczRpQmE",
|
||||||
|
"type": "layout",
|
||||||
|
"fromId": "shape:f4LKGB_8M2qsyWGpHR5Dq",
|
||||||
|
"toId": "shape:GTQq2qxkWPHEK7KMIRtsh",
|
||||||
|
"props": {
|
||||||
|
"index": "a1",
|
||||||
|
"placeholder": false
|
||||||
|
},
|
||||||
|
"typeName": "binding"
|
||||||
|
},
|
||||||
|
"binding:n4LY_pVuLfjV1qpOTZX-U": {
|
||||||
|
"meta": {},
|
||||||
|
"id": "binding:n4LY_pVuLfjV1qpOTZX-U",
|
||||||
|
"type": "layout",
|
||||||
|
"fromId": "shape:f4LKGB_8M2qsyWGpHR5Dq",
|
||||||
|
"toId": "shape:6uouhIK7PvyIRNQHACf-d",
|
||||||
|
"props": {
|
||||||
|
"index": "a3",
|
||||||
|
"placeholder": false
|
||||||
|
},
|
||||||
|
"typeName": "binding"
|
||||||
|
},
|
||||||
|
"binding:8XayRsWB_nxAH2833SYg1": {
|
||||||
|
"meta": {},
|
||||||
|
"id": "binding:8XayRsWB_nxAH2833SYg1",
|
||||||
|
"type": "layout",
|
||||||
|
"fromId": "shape:f4LKGB_8M2qsyWGpHR5Dq",
|
||||||
|
"toId": "shape:2oThF4kJ4v31xqKN5lvq2",
|
||||||
|
"props": {
|
||||||
|
"index": "a5",
|
||||||
|
"placeholder": false
|
||||||
|
},
|
||||||
|
"typeName": "binding"
|
||||||
|
},
|
||||||
|
"binding:MTYuIRiEVTn2DyVChthry": {
|
||||||
|
"meta": {},
|
||||||
|
"id": "binding:MTYuIRiEVTn2DyVChthry",
|
||||||
|
"type": "layout",
|
||||||
|
"fromId": "shape:f4LKGB_8M2qsyWGpHR5Dq",
|
||||||
|
"toId": "shape:K2vk_VTaNh-ANaRNOAvgY",
|
||||||
|
"props": {
|
||||||
|
"index": "a4",
|
||||||
|
"placeholder": false
|
||||||
|
},
|
||||||
|
"typeName": "binding"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"schema": {
|
||||||
|
"schemaVersion": 2,
|
||||||
|
"sequences": {
|
||||||
|
"com.tldraw.store": 4,
|
||||||
|
"com.tldraw.asset": 1,
|
||||||
|
"com.tldraw.camera": 1,
|
||||||
|
"com.tldraw.document": 2,
|
||||||
|
"com.tldraw.instance": 25,
|
||||||
|
"com.tldraw.instance_page_state": 5,
|
||||||
|
"com.tldraw.page": 1,
|
||||||
|
"com.tldraw.instance_presence": 5,
|
||||||
|
"com.tldraw.pointer": 1,
|
||||||
|
"com.tldraw.shape": 4,
|
||||||
|
"com.tldraw.asset.bookmark": 2,
|
||||||
|
"com.tldraw.asset.image": 4,
|
||||||
|
"com.tldraw.asset.video": 4,
|
||||||
|
"com.tldraw.shape.group": 0,
|
||||||
|
"com.tldraw.shape.text": 2,
|
||||||
|
"com.tldraw.shape.bookmark": 2,
|
||||||
|
"com.tldraw.shape.draw": 2,
|
||||||
|
"com.tldraw.shape.geo": 9,
|
||||||
|
"com.tldraw.shape.note": 7,
|
||||||
|
"com.tldraw.shape.line": 5,
|
||||||
|
"com.tldraw.shape.frame": 0,
|
||||||
|
"com.tldraw.shape.arrow": 5,
|
||||||
|
"com.tldraw.shape.highlight": 1,
|
||||||
|
"com.tldraw.shape.embed": 4,
|
||||||
|
"com.tldraw.shape.image": 3,
|
||||||
|
"com.tldraw.shape.video": 2,
|
||||||
|
"com.tldraw.shape.container": 0,
|
||||||
|
"com.tldraw.shape.element": 0,
|
||||||
|
"com.tldraw.binding.arrow": 0,
|
||||||
|
"com.tldraw.binding.layout": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
import { BaseBoxShapeTool, TLClickEvent } from 'tldraw'
|
|
||||||
export class CardShapeTool extends BaseBoxShapeTool {
|
|
||||||
static override id = 'card'
|
|
||||||
static override initial = 'idle'
|
|
||||||
override shapeType = 'card'
|
|
||||||
|
|
||||||
override onDoubleClick: TLClickEvent = (_info) => {
|
|
||||||
// you can handle events in handlers like this one;
|
|
||||||
// check the BaseBoxShapeTool source as an example
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
This file contains our custom tool. The tool is a StateNode with the `id` "card".
|
|
||||||
|
|
||||||
We get a lot of functionality for free by extending the BaseBoxShapeTool. but we can
|
|
||||||
handle events in out own way by overriding methods like onDoubleClick. For an example
|
|
||||||
of a tool with more custom functionality, check out the screenshot-tool example.
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { BaseBoxShapeTool } from "tldraw";
|
import { BaseBoxShapeTool } from "tldraw";
|
||||||
|
|
||||||
export class ChatBoxTool extends BaseBoxShapeTool {
|
export class ChatBoxTool extends BaseBoxShapeTool {
|
||||||
shapeType = 'chatBox'
|
static override id = 'chatBox'
|
||||||
override initial = 'idle'
|
shapeType = 'chatBox';
|
||||||
|
override initial = 'idle';
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { BaseBoxShapeTool } from "tldraw";
|
||||||
|
|
||||||
|
export class VideoChatTool extends BaseBoxShapeTool {
|
||||||
|
static override id = 'VideoChat'
|
||||||
|
shapeType = 'VideoChat';
|
||||||
|
override initial = 'idle';
|
||||||
|
|
||||||
|
// Additional methods for handling video chat functionality can be added here
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
import {
|
||||||
|
DefaultToolbar,
|
||||||
|
DefaultToolbarContent,
|
||||||
|
TLComponents,
|
||||||
|
TLUiOverrides,
|
||||||
|
TldrawUiMenuItem,
|
||||||
|
useIsToolSelected,
|
||||||
|
useTools,
|
||||||
|
} from 'tldraw'
|
||||||
|
|
||||||
|
export const uiOverrides: TLUiOverrides = {
|
||||||
|
tools(editor, tools) {
|
||||||
|
tools.VideoChat = {
|
||||||
|
id: 'VideoChat',
|
||||||
|
icon: 'color',
|
||||||
|
label: 'Video',
|
||||||
|
kbd: 'x',
|
||||||
|
onSelect: () => {
|
||||||
|
editor.setCurrentTool('VideoChat')
|
||||||
|
},
|
||||||
|
}
|
||||||
|
tools.chatBox = {
|
||||||
|
id: 'chatBox',
|
||||||
|
icon: 'color',
|
||||||
|
label: 'Chat',
|
||||||
|
kbd: 'x',
|
||||||
|
onSelect: () => {
|
||||||
|
editor.setCurrentTool('chatBox')
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return tools
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export const components: TLComponents = {
|
||||||
|
Toolbar: (props) => {
|
||||||
|
const tools = useTools()
|
||||||
|
const isChatBoxSelected = useIsToolSelected(tools['chatBox'])
|
||||||
|
const isVideoSelected = useIsToolSelected(tools['VideoChat'])
|
||||||
|
return (
|
||||||
|
<DefaultToolbar {...props}>
|
||||||
|
<TldrawUiMenuItem {...tools['VideoChat']} isSelected={isVideoSelected} />
|
||||||
|
<TldrawUiMenuItem {...tools['chatBox']} isSelected={isChatBoxSelected} />
|
||||||
|
<DefaultToolbarContent />
|
||||||
|
</DefaultToolbar>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { createShapePropsMigrationIds, createShapePropsMigrationSequence } from 'tldraw'
|
||||||
|
|
||||||
|
const versions = createShapePropsMigrationIds(
|
||||||
|
// this must match the shape type in the shape definition
|
||||||
|
'card',
|
||||||
|
{
|
||||||
|
AddSomeProperty: 1,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Migrations for the custom card shape (optional but very helpful)
|
||||||
|
export const cardShapeMigrations = createShapePropsMigrationSequence({
|
||||||
|
sequence: [
|
||||||
|
{
|
||||||
|
id: versions.AddSomeProperty,
|
||||||
|
up(props) {
|
||||||
|
// it is safe to mutate the props object here
|
||||||
|
props.someProperty = 'some value'
|
||||||
|
},
|
||||||
|
down(props) {
|
||||||
|
delete props.someProperty
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { DefaultColorStyle, RecordProps, T } from 'tldraw'
|
||||||
|
import { ICardShape } from './card-shape-types'
|
||||||
|
|
||||||
|
// Validation for our custom card shape's props, using one of tldraw's default styles
|
||||||
|
export const cardShapeProps: RecordProps<ICardShape> = {
|
||||||
|
w: T.number,
|
||||||
|
h: T.number,
|
||||||
|
color: DefaultColorStyle,
|
||||||
|
}
|
||||||
|
|
||||||
|
// To generate your own custom styles, check out the custom styles example.
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { TLBaseShape, TLDefaultColorStyle } from 'tldraw'
|
||||||
|
|
||||||
|
// A type for our custom card shape
|
||||||
|
export type ICardShape = TLBaseShape<
|
||||||
|
'card',
|
||||||
|
{
|
||||||
|
w: number
|
||||||
|
h: number
|
||||||
|
color: TLDefaultColorStyle
|
||||||
|
}
|
||||||
|
>
|
||||||
|
|
@ -0,0 +1,120 @@
|
||||||
|
import { BaseBoxShapeUtil, HTMLContainer, RecordProps, T, TLBaseShape } from 'tldraw'
|
||||||
|
|
||||||
|
// There's a guide at the bottom of this file!
|
||||||
|
|
||||||
|
type IMyInteractiveShape = TLBaseShape<
|
||||||
|
'my-interactive-shape',
|
||||||
|
{
|
||||||
|
w: number
|
||||||
|
h: number
|
||||||
|
checked: boolean
|
||||||
|
text: string
|
||||||
|
}
|
||||||
|
>
|
||||||
|
|
||||||
|
export class myInteractiveShape extends BaseBoxShapeUtil<IMyInteractiveShape> {
|
||||||
|
static override type = 'my-interactive-shape' as const
|
||||||
|
static override props: RecordProps<IMyInteractiveShape> = {
|
||||||
|
w: T.number,
|
||||||
|
h: T.number,
|
||||||
|
checked: T.boolean,
|
||||||
|
text: T.string,
|
||||||
|
}
|
||||||
|
|
||||||
|
getDefaultProps(): IMyInteractiveShape['props'] {
|
||||||
|
return {
|
||||||
|
w: 230,
|
||||||
|
h: 230,
|
||||||
|
checked: false,
|
||||||
|
text: '',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// [1]
|
||||||
|
component(shape: IMyInteractiveShape) {
|
||||||
|
return (
|
||||||
|
<HTMLContainer
|
||||||
|
style={{
|
||||||
|
padding: 16,
|
||||||
|
height: shape.props.h,
|
||||||
|
width: shape.props.w,
|
||||||
|
// [a] This is where we allow pointer events on our shape
|
||||||
|
pointerEvents: 'all',
|
||||||
|
backgroundColor: '#efefef',
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={shape.props.checked}
|
||||||
|
onChange={() =>
|
||||||
|
this.editor.updateShape<IMyInteractiveShape>({
|
||||||
|
id: shape.id,
|
||||||
|
type: 'my-interactive-shape',
|
||||||
|
props: { checked: !shape.props.checked },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// [b] This is where we stop event propagation
|
||||||
|
onPointerDown={(e) => e.stopPropagation()}
|
||||||
|
onTouchStart={(e) => e.stopPropagation()}
|
||||||
|
onTouchEnd={(e) => e.stopPropagation()}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Enter a todo..."
|
||||||
|
readOnly={shape.props.checked}
|
||||||
|
value={shape.props.text}
|
||||||
|
onChange={(e) =>
|
||||||
|
this.editor.updateShape<IMyInteractiveShape>({
|
||||||
|
id: shape.id,
|
||||||
|
type: 'my-interactive-shape',
|
||||||
|
props: { text: e.currentTarget.value },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// [c]
|
||||||
|
onPointerDown={(e) => {
|
||||||
|
if (!shape.props.checked) {
|
||||||
|
e.stopPropagation()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onTouchStart={(e) => {
|
||||||
|
if (!shape.props.checked) {
|
||||||
|
e.stopPropagation()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onTouchEnd={(e) => {
|
||||||
|
if (!shape.props.checked) {
|
||||||
|
e.stopPropagation()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</HTMLContainer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// [5]
|
||||||
|
indicator(shape: IMyInteractiveShape) {
|
||||||
|
return <rect width={shape.props.w} height={shape.props.h} />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
This is a custom shape, for a more in-depth look at how to create a custom shape,
|
||||||
|
see our custom shape example.
|
||||||
|
|
||||||
|
[1]
|
||||||
|
This is where we describe how our shape will render
|
||||||
|
|
||||||
|
[a] We need to set pointer-events to all so that we can interact with our shape. This CSS property is
|
||||||
|
set to "none" off by default. We need to manually opt-in to accepting pointer events by setting it to
|
||||||
|
'all' or 'auto'.
|
||||||
|
|
||||||
|
[b] We need to stop event propagation so that the editor doesn't select the shape
|
||||||
|
when we click on the checkbox. The 'canvas container' forwards events that it receives
|
||||||
|
on to the editor, so stopping propagation here prevents the event from reaching the canvas.
|
||||||
|
|
||||||
|
[c] If the shape is not checked, we stop event propagation so that the editor doesn't
|
||||||
|
select the shape when we click on the input. If the shape is checked then we allow that event to
|
||||||
|
propagate to the canvas and then get sent to the editor, triggering clicks or drags as usual.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
@ -1,67 +0,0 @@
|
||||||
import React from 'react'
|
|
||||||
import {
|
|
||||||
DefaultKeyboardShortcutsDialog,
|
|
||||||
DefaultKeyboardShortcutsDialogContent,
|
|
||||||
DefaultToolbar,
|
|
||||||
DefaultToolbarContent,
|
|
||||||
TLComponents,
|
|
||||||
TLUiOverrides,
|
|
||||||
TldrawUiMenuItem,
|
|
||||||
useIsToolSelected,
|
|
||||||
useTools,
|
|
||||||
} from 'tldraw'
|
|
||||||
|
|
||||||
// There's a guide at the bottom of this file!
|
|
||||||
|
|
||||||
export const uiOverrides: TLUiOverrides = {
|
|
||||||
tools(editor, tools) {
|
|
||||||
// Create a tool item in the ui's context.
|
|
||||||
tools.card = {
|
|
||||||
id: 'card',
|
|
||||||
icon: 'color',
|
|
||||||
label: 'Card',
|
|
||||||
kbd: 'c',
|
|
||||||
onSelect: () => {
|
|
||||||
editor.setCurrentTool('card')
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return tools
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
export const components: TLComponents = {
|
|
||||||
Toolbar: (props) => {
|
|
||||||
const tools = useTools()
|
|
||||||
const isCardSelected = useIsToolSelected(tools['card'])
|
|
||||||
return (
|
|
||||||
<DefaultToolbar {...props}>
|
|
||||||
<TldrawUiMenuItem {...tools['card']} isSelected={isCardSelected} />
|
|
||||||
<DefaultToolbarContent />
|
|
||||||
</DefaultToolbar>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
KeyboardShortcutsDialog: (props) => {
|
|
||||||
const tools = useTools()
|
|
||||||
return (
|
|
||||||
<DefaultKeyboardShortcutsDialog {...props}>
|
|
||||||
<TldrawUiMenuItem {...tools['card']} />
|
|
||||||
<DefaultKeyboardShortcutsDialogContent />
|
|
||||||
</DefaultKeyboardShortcutsDialog>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
|
|
||||||
This file contains overrides for the Tldraw UI. These overrides are used to add your custom tools to
|
|
||||||
the toolbar and the keyboard shortcuts menu.
|
|
||||||
|
|
||||||
First we have to add our new tool to the tools object in the tools override. This is where we define
|
|
||||||
all the basic information about our new tool - its icon, label, keyboard shortcut, what happens when
|
|
||||||
we select it, etc.
|
|
||||||
|
|
||||||
Then, we replace the UI components for the toolbar and keyboard shortcut dialog with our own, that
|
|
||||||
add our new tool to the existing default content. Ideally, we'd interleave our new tool into the
|
|
||||||
ideal place among the default tools, but for now we're just adding it at the start to keep things
|
|
||||||
simple.
|
|
||||||
*/
|
|
||||||
|
|
@ -10,11 +10,12 @@ import {
|
||||||
import { AutoRouter, IRequest, error } from 'itty-router'
|
import { AutoRouter, IRequest, error } from 'itty-router'
|
||||||
import throttle from 'lodash.throttle'
|
import throttle from 'lodash.throttle'
|
||||||
import { Environment } from './types'
|
import { Environment } from './types'
|
||||||
import { ChatBoxShape } from '@/shapes/ChatBoxShape'
|
import { ChatBoxShape } from '@/shapes/ChatBoxShapeUtil'
|
||||||
|
import { VideoChatShape } from '@/shapes/VideoChatShapeUtil'
|
||||||
|
|
||||||
// add custom shapes and bindings here if needed:
|
// add custom shapes and bindings here if needed:
|
||||||
export const customSchema = createTLSchema({
|
export const customSchema = createTLSchema({
|
||||||
shapes: { ...defaultShapeSchemas, chatBox: ChatBoxShape },
|
shapes: { ...defaultShapeSchemas, chatBox: ChatBoxShape, VideoChat: VideoChatShape },
|
||||||
// bindings: { ...defaultBindingSchemas },
|
// bindings: { ...defaultBindingSchemas },
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue