diff --git a/src/components/Board.tsx b/src/components/Board.tsx
index 30009c3..b014085 100644
--- a/src/components/Board.tsx
+++ b/src/components/Board.tsx
@@ -4,6 +4,8 @@ import {
getHashForString,
TLBookmarkAsset,
Tldraw,
+ TLUiMenuGroup,
+ TLUiOverrides,
} from 'tldraw'
import { useParams } from 'react-router-dom'
import { ChatBoxTool } from '@/tools/ChatBoxTool'
@@ -43,16 +45,59 @@ export function Board() {
setUserName(event.target.value);
};
+ const customUiOverrides: TLUiOverrides = {
+ ...uiOverrides,
+ contextMenu: (editor, contextMenuSchema, helpers) => {
+ const defaultContextMenu = uiOverrides.contextMenu ? uiOverrides.contextMenu(editor, contextMenuSchema, helpers) : contextMenuSchema
+
+ const newContextMenu: TLUiMenuGroup[] = [
+ ...defaultContextMenu,
+ {
+ id: 'external-link',
+ type: 'group',
+ checkbox: false,
+ disabled: false,
+ readonlyOk: true,
+ children: [
+ {
+ id: 'add-external-link',
+ type: 'item',
+ readonlyOk: true,
+ label: 'Add External Link',
+ icon: 'link',
+ onSelect: () => {
+ const selectedShapes = editor.getSelectedShapes()
+ if (selectedShapes.length === 1) {
+ const shape = selectedShapes[0]
+ const externalUrl = `${window.location.origin}/board/${roomId}?shapeId=${shape.id}`
+ // Here you can implement the logic to copy the link to clipboard or show it to the user
+ console.log('External link:', externalUrl)
+ // For example, to copy to clipboard:
+ navigator.clipboard.writeText(externalUrl).then(() => {
+ editor.setToast({ id: 'external-link-copied', title: 'External link copied to clipboard' })
+ })
+ }
+ },
+ },
+ ],
+ },
+ ]
+
+ return newContextMenu
+ },
+ }
+
return (
{
editor.registerExternalAssetHandler('url', unfurlBookmarkUrl)
+ editor.setCurrentTool('hand')
}}
/>
{isChatBoxVisible && (
diff --git a/src/components/Default.tsx b/src/components/Default.tsx
index 181730a..3827ab5 100644
--- a/src/components/Default.tsx
+++ b/src/components/Default.tsx
@@ -29,7 +29,7 @@ export function Default() {
Get in touch
- I am on Twitter @OrionReedOne,
+ I am on Twitter @jeffemmett,
Mastodon @orion@hci.social and GitHub @orionreed. You can also shoot me an email me@orionreed.com
diff --git a/src/shapes/ChatBoxShapeUtil.tsx b/src/shapes/ChatBoxShapeUtil.tsx
index 36ba0a6..8d11f90 100644
--- a/src/shapes/ChatBoxShapeUtil.tsx
+++ b/src/shapes/ChatBoxShapeUtil.tsx
@@ -94,7 +94,7 @@ export const ChatBox: React.FC = ({ roomId, w, h, userNa
};
return (
-
+
{messages.map((msg) => (
@@ -114,8 +114,17 @@ export const ChatBox: React.FC = ({ roomId, w, h, userNa
onChange={(e) => setInputMessage(e.target.value)}
placeholder="Type a message..."
className="message-input"
+ style={{ touchAction: 'manipulation' }}
/>
-
+
);
diff --git a/src/shapes/EmbedShapeUtil.tsx b/src/shapes/EmbedShapeUtil.tsx
index 2dcda48..ae404d3 100644
--- a/src/shapes/EmbedShapeUtil.tsx
+++ b/src/shapes/EmbedShapeUtil.tsx
@@ -25,39 +25,113 @@ export class EmbedShape extends BaseBoxShapeUtil
{
return (
-
);
}
component(shape: IEmbedShape) {
const [inputUrl, setInputUrl] = useState(shape.props.url || '');
+ const [error, setError] = useState('');
const handleSubmit = useCallback((e: React.FormEvent) => {
e.preventDefault();
- this.editor.updateShape({ id: shape.id, type: 'Embed', props: { ...shape.props, url: inputUrl } });
+ let completedUrl = inputUrl.startsWith('http://') || inputUrl.startsWith('https://') ? inputUrl : `https://${inputUrl}`;
+
+ // Handle YouTube links
+ if (completedUrl.includes('youtube.com') || completedUrl.includes('youtu.be')) {
+ const videoId = extractYouTubeVideoId(completedUrl);
+ if (videoId) {
+ completedUrl = `https://www.youtube.com/embed/${videoId}`;
+ } else {
+ setError('Invalid YouTube URL');
+ return;
+ }
+ }
+
+ // Handle Google Docs links
+ if (completedUrl.includes('docs.google.com')) {
+ const docId = completedUrl.match(/\/d\/([a-zA-Z0-9-_]+)/)?.[1];
+ if (docId) {
+ completedUrl = `https://docs.google.com/document/d/${docId}/preview`;
+ } else {
+ setError('Invalid Google Docs URL');
+ return;
+ }
+ }
+
+ this.editor.updateShape({ id: shape.id, type: 'Embed', props: { ...shape.props, url: completedUrl } });
+
+ // Check if the URL is valid
+ const isValidUrl = completedUrl.match(/(^\w+:|^)\/\//);
+ if (!isValidUrl) {
+ setError('Invalid website URL');
+ } else {
+ setError('');
+ }
}, [inputUrl]);
+ const extractYouTubeVideoId = (url: string): string | null => {
+ const regExp = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#\&\?]*).*/;
+ const match = url.match(regExp);
+ return (match && match[2].length === 11) ? match[2] : null;
+ };
+
+ const wrapperStyle = {
+ width: `${shape.props.w}px`,
+ height: `${shape.props.h}px`,
+ padding: '15px',
+ boxShadow: '0 2px 4px rgba(0, 0, 0, 0.1)',
+ backgroundColor: '#F0F0F0',
+ borderRadius: '4px',
+ };
+
+ const contentStyle = {
+ pointerEvents: 'all' as const,
+ width: '100%',
+ height: '100%',
+ border: '1px solid #D3D3D3',
+ backgroundColor: '#FFFFFF',
+ display: 'flex',
+ justifyContent: 'center',
+ alignItems: 'center',
+ overflow: 'hidden',
+ };
+
if (!shape.props.url) {
return (
-
-
+
+
document.querySelector('input')?.focus()}>
+
+
);
}
return (
-
-
+
);
}
diff --git a/src/shapes/VideoChatShapeUtil.tsx b/src/shapes/VideoChatShapeUtil.tsx
index e66ce16..be5f7bc 100644
--- a/src/shapes/VideoChatShapeUtil.tsx
+++ b/src/shapes/VideoChatShapeUtil.tsx
@@ -14,7 +14,6 @@ export type IVideoChatShape = TLBaseShape<
>;
const WHEREBY_API_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmFwcGVhci5pbiIsImF1ZCI6Imh0dHBzOi8vYXBpLmFwcGVhci5pbi92MSIsImV4cCI6OTAwNzE5OTI1NDc0MDk5MSwiaWF0IjoxNzI5MTkzOTE3LCJvcmdhbml6YXRpb25JZCI6MjY2MDk5LCJqdGkiOiI0MzI0MmUxMC1kZmRjLTRhYmEtYjlhOS01ZjcwNTFlMTYwZjAifQ.RaxXpZKYl_dOWyoATQZrzyMR2XRh3fHf02mALQiuTTs'; // Replace with your actual API key
-// const ROOM_PREFIX = 'test'
export class VideoChatShape extends BaseBoxShapeUtil
{
static override type = 'VideoChat';
@@ -34,8 +33,6 @@ export class VideoChatShape extends BaseBoxShapeUtil {
async ensureRoomExists(shape: IVideoChatShape) {
- console.log('This is your roomUrl 1:', shape.props.roomUrl);
-
if (shape.props.roomUrl !== null) {
return;
}
@@ -45,14 +42,12 @@ export class VideoChatShape extends BaseBoxShapeUtil {
const response = await fetch(`${CORS_PROXY}https://api.whereby.dev/v1/meetings`, {
method: 'POST',
headers: {
- // 'Access-Control-Allow-Origin': 'https://jeffemmett.com/',
'Authorization': `Bearer ${WHEREBY_API_KEY}`,
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest', // Required by some CORS proxies
},
body: JSON.stringify({
isLocked: false,
- // roomNamePrefix: ROOM_PREFIX,
roomMode: 'normal',
endDate: expiryDate.toISOString(),
fields: ['hostRoomUrl'],
@@ -62,10 +57,6 @@ export class VideoChatShape extends BaseBoxShapeUtil {
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);
@@ -85,8 +76,6 @@ export class VideoChatShape extends BaseBoxShapeUtil {
roomUrl
}
})
-
-
}
component(shape: IVideoChatShape) {
@@ -107,7 +96,6 @@ export class VideoChatShape extends BaseBoxShapeUtil {
}, []);
const joinRoom = async () => {
- // this.ensureRoomExists(shape);
setError("");
setIsLoading(true);
try {
@@ -126,29 +114,53 @@ export class VideoChatShape extends BaseBoxShapeUtil {
};
return (
-
- {isLoading ? (
-
Joining room...
- ) : isInRoom && shape.props.roomUrl && typeof window !== 'undefined' ? (
-
-
-
- ) : (
-
-
- {error &&
{error}
}
-
- )}
+
+
+ {isLoading ? (
+
Joining room...
+ ) : isInRoom && shape.props.roomUrl && typeof window !== 'undefined' ? (
+
+
+
+ ) : (
+
+
+ {error &&
{error}
}
+
+ )}
+
);
}
diff --git a/worker/TldrawDurableObject.ts b/worker/TldrawDurableObject.ts
index b916883..6d99202 100644
--- a/worker/TldrawDurableObject.ts
+++ b/worker/TldrawDurableObject.ts
@@ -43,7 +43,6 @@ export class TldrawDurableObject {
private readonly ctx: DurableObjectState,
env: Environment
) {
- console.log("hello from durable object")
this.r2 = env.TLDRAW_BUCKET
ctx.blockConcurrencyWhile(async () => {