fixed a bunch of stuff
This commit is contained in:
parent
4e2103aab2
commit
4eff918bd3
|
|
@ -4,6 +4,8 @@ import {
|
||||||
getHashForString,
|
getHashForString,
|
||||||
TLBookmarkAsset,
|
TLBookmarkAsset,
|
||||||
Tldraw,
|
Tldraw,
|
||||||
|
TLUiMenuGroup,
|
||||||
|
TLUiOverrides,
|
||||||
} from 'tldraw'
|
} from 'tldraw'
|
||||||
import { useParams } from 'react-router-dom'
|
import { useParams } from 'react-router-dom'
|
||||||
import { ChatBoxTool } from '@/tools/ChatBoxTool'
|
import { ChatBoxTool } from '@/tools/ChatBoxTool'
|
||||||
|
|
@ -43,16 +45,59 @@ export function Board() {
|
||||||
setUserName(event.target.value);
|
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 (
|
return (
|
||||||
<div style={{ position: 'fixed', inset: 0 }}>
|
<div style={{ position: 'fixed', inset: 0 }}>
|
||||||
<Tldraw
|
<Tldraw
|
||||||
store={store}
|
store={store}
|
||||||
shapeUtils={shapeUtils}
|
shapeUtils={shapeUtils}
|
||||||
overrides={uiOverrides}
|
overrides={customUiOverrides}
|
||||||
components={components}
|
components={components}
|
||||||
tools={tools}
|
tools={tools}
|
||||||
onMount={(editor) => {
|
onMount={(editor) => {
|
||||||
editor.registerExternalAssetHandler('url', unfurlBookmarkUrl)
|
editor.registerExternalAssetHandler('url', unfurlBookmarkUrl)
|
||||||
|
editor.setCurrentTool('hand')
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{isChatBoxVisible && (
|
{isChatBoxVisible && (
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ export function Default() {
|
||||||
|
|
||||||
<h2>Get in touch</h2>
|
<h2>Get in touch</h2>
|
||||||
<p>
|
<p>
|
||||||
I am on Twitter <a href="https://twitter.com/OrionReedOne">@OrionReedOne</a>,
|
I am on Twitter <a href="https://twitter.com/jeffemmett">@jeffemmett</a>,
|
||||||
Mastodon <a href="https://hci.social/@orion">@orion@hci.social</a> and GitHub <a href="https://github.com/orionreed">@orionreed</a>. You can also shoot me an email <a href="mailto:me@orionreed.com">me@orionreed.com</a>
|
Mastodon <a href="https://hci.social/@orion">@orion@hci.social</a> and GitHub <a href="https://github.com/orionreed">@orionreed</a>. You can also shoot me an email <a href="mailto:me@orionreed.com">me@orionreed.com</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -94,7 +94,7 @@ export const ChatBox: React.FC<IChatBoxShape['props']> = ({ roomId, w, h, userNa
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="chat-container" style={{ pointerEvents: 'all', width: `${w}px`, height: `${h}px`, overflow: 'auto' }}>
|
<div className="chat-container" style={{ pointerEvents: 'all', width: `${w}px`, height: `${h}px`, overflow: 'auto', touchAction: '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' : ''}`}>
|
||||||
|
|
@ -114,8 +114,17 @@ export const ChatBox: React.FC<IChatBoxShape['props']> = ({ roomId, w, h, userNa
|
||||||
onChange={(e) => setInputMessage(e.target.value)}
|
onChange={(e) => setInputMessage(e.target.value)}
|
||||||
placeholder="Type a message..."
|
placeholder="Type a message..."
|
||||||
className="message-input"
|
className="message-input"
|
||||||
|
style={{ touchAction: 'manipulation' }}
|
||||||
/>
|
/>
|
||||||
<button type="submit" style={{ pointerEvents: 'all', }} onPointerDown={(e) => e.stopPropagation()} className="send-button">Send</button>
|
<button
|
||||||
|
type="submit"
|
||||||
|
style={{ pointerEvents: 'all', touchAction: 'manipulation' }}
|
||||||
|
onPointerDown={(e) => e.stopPropagation()}
|
||||||
|
onTouchStart={(e) => e.stopPropagation()}
|
||||||
|
className="send-button"
|
||||||
|
>
|
||||||
|
Send
|
||||||
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -25,39 +25,113 @@ export class EmbedShape extends BaseBoxShapeUtil<IEmbedShape> {
|
||||||
return (
|
return (
|
||||||
<g>
|
<g>
|
||||||
<rect x={0} y={0} width={shape.props.w} height={shape.props.h} />
|
<rect x={0} y={0} width={shape.props.w} height={shape.props.h} />
|
||||||
<rect x={0} y={0} width={shape.props.w} height={shape.props.h} style={{ stroke: 'black', strokeWidth: 2, fill: 'none' }} />
|
|
||||||
</g>
|
</g>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
component(shape: IEmbedShape) {
|
component(shape: IEmbedShape) {
|
||||||
const [inputUrl, setInputUrl] = useState(shape.props.url || '');
|
const [inputUrl, setInputUrl] = useState(shape.props.url || '');
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
|
||||||
const handleSubmit = useCallback((e: React.FormEvent) => {
|
const handleSubmit = useCallback((e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
this.editor.updateShape<IEmbedShape>({ 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<IEmbedShape>({ 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]);
|
}, [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) {
|
if (!shape.props.url) {
|
||||||
return (
|
return (
|
||||||
<div style={{ pointerEvents: 'all', border: '1px solid #000', borderRadius: '5px', padding: '5px' }}>
|
<div style={wrapperStyle}>
|
||||||
<form onSubmit={handleSubmit}>
|
<div style={contentStyle} onClick={() => document.querySelector('input')?.focus()}>
|
||||||
<input
|
<form onSubmit={handleSubmit} style={{ width: '100%', height: '100%', padding: '10px' }}>
|
||||||
type="text"
|
<input
|
||||||
value={inputUrl}
|
type="text"
|
||||||
onChange={(e) => setInputUrl(e.target.value)}
|
value={inputUrl}
|
||||||
placeholder="Enter URL"
|
onChange={(e) => setInputUrl(e.target.value)}
|
||||||
style={{ width: shape.props.w, height: shape.props.h }}
|
placeholder="Enter URL"
|
||||||
/>
|
style={{ width: '100%', height: '100%', border: 'none', padding: '10px' }}
|
||||||
<button type="submit" onTouchStart={handleSubmit} onClick={handleSubmit}>Load</button>
|
onKeyDown={(e) => {
|
||||||
</form>
|
if (e.key === 'Enter') {
|
||||||
|
handleSubmit(e);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{error && <div style={{ color: 'red', marginTop: '10px' }}>{error}</div>}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ pointerEvents: 'all' }}>
|
<div style={wrapperStyle}>
|
||||||
<iframe src={shape.props.url} width={shape.props.w} height={shape.props.h} />
|
<div style={contentStyle}>
|
||||||
|
<iframe
|
||||||
|
src={shape.props.url}
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
style={{ border: 'none' }}
|
||||||
|
allowFullScreen
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,6 @@ export type IVideoChatShape = TLBaseShape<
|
||||||
>;
|
>;
|
||||||
|
|
||||||
const WHEREBY_API_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmFwcGVhci5pbiIsImF1ZCI6Imh0dHBzOi8vYXBpLmFwcGVhci5pbi92MSIsImV4cCI6OTAwNzE5OTI1NDc0MDk5MSwiaWF0IjoxNzI5MTkzOTE3LCJvcmdhbml6YXRpb25JZCI6MjY2MDk5LCJqdGkiOiI0MzI0MmUxMC1kZmRjLTRhYmEtYjlhOS01ZjcwNTFlMTYwZjAifQ.RaxXpZKYl_dOWyoATQZrzyMR2XRh3fHf02mALQiuTTs'; // Replace with your actual API key
|
const WHEREBY_API_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2FjY291bnRzLmFwcGVhci5pbiIsImF1ZCI6Imh0dHBzOi8vYXBpLmFwcGVhci5pbi92MSIsImV4cCI6OTAwNzE5OTI1NDc0MDk5MSwiaWF0IjoxNzI5MTkzOTE3LCJvcmdhbml6YXRpb25JZCI6MjY2MDk5LCJqdGkiOiI0MzI0MmUxMC1kZmRjLTRhYmEtYjlhOS01ZjcwNTFlMTYwZjAifQ.RaxXpZKYl_dOWyoATQZrzyMR2XRh3fHf02mALQiuTTs'; // Replace with your actual API key
|
||||||
// const ROOM_PREFIX = 'test'
|
|
||||||
|
|
||||||
export class VideoChatShape extends BaseBoxShapeUtil<IVideoChatShape> {
|
export class VideoChatShape extends BaseBoxShapeUtil<IVideoChatShape> {
|
||||||
static override type = 'VideoChat';
|
static override type = 'VideoChat';
|
||||||
|
|
@ -34,8 +33,6 @@ export class VideoChatShape extends BaseBoxShapeUtil<IVideoChatShape> {
|
||||||
|
|
||||||
async ensureRoomExists(shape: IVideoChatShape) {
|
async ensureRoomExists(shape: IVideoChatShape) {
|
||||||
|
|
||||||
console.log('This is your roomUrl 1:', shape.props.roomUrl);
|
|
||||||
|
|
||||||
if (shape.props.roomUrl !== null) {
|
if (shape.props.roomUrl !== null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -45,14 +42,12 @@ export class VideoChatShape extends BaseBoxShapeUtil<IVideoChatShape> {
|
||||||
const response = await fetch(`${CORS_PROXY}https://api.whereby.dev/v1/meetings`, {
|
const response = await fetch(`${CORS_PROXY}https://api.whereby.dev/v1/meetings`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
// 'Access-Control-Allow-Origin': 'https://jeffemmett.com/',
|
|
||||||
'Authorization': `Bearer ${WHEREBY_API_KEY}`,
|
'Authorization': `Bearer ${WHEREBY_API_KEY}`,
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'X-Requested-With': 'XMLHttpRequest', // Required by some CORS proxies
|
'X-Requested-With': 'XMLHttpRequest', // Required by some CORS proxies
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
isLocked: false,
|
isLocked: false,
|
||||||
// roomNamePrefix: ROOM_PREFIX,
|
|
||||||
roomMode: 'normal',
|
roomMode: 'normal',
|
||||||
endDate: expiryDate.toISOString(),
|
endDate: expiryDate.toISOString(),
|
||||||
fields: ['hostRoomUrl'],
|
fields: ['hostRoomUrl'],
|
||||||
|
|
@ -62,10 +57,6 @@ export class VideoChatShape extends BaseBoxShapeUtil<IVideoChatShape> {
|
||||||
throw error;
|
throw error;
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('This is your response:', response);
|
|
||||||
|
|
||||||
console.log('This is your roomUrl 2:', shape.props.roomUrl);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorData = await response.json();
|
const errorData = await response.json();
|
||||||
console.error('Whereby API error:', errorData);
|
console.error('Whereby API error:', errorData);
|
||||||
|
|
@ -85,8 +76,6 @@ export class VideoChatShape extends BaseBoxShapeUtil<IVideoChatShape> {
|
||||||
roomUrl
|
roomUrl
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
component(shape: IVideoChatShape) {
|
component(shape: IVideoChatShape) {
|
||||||
|
|
@ -107,7 +96,6 @@ export class VideoChatShape extends BaseBoxShapeUtil<IVideoChatShape> {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const joinRoom = async () => {
|
const joinRoom = async () => {
|
||||||
// this.ensureRoomExists(shape);
|
|
||||||
setError("");
|
setError("");
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
|
|
@ -126,29 +114,53 @@ export class VideoChatShape extends BaseBoxShapeUtil<IVideoChatShape> {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4" style={{ pointerEvents: 'all', width: '100%', height: '100%' }}>
|
<div style={{
|
||||||
{isLoading ? (
|
pointerEvents: 'all',
|
||||||
<p>Joining room...</p>
|
width: `${shape.props.w}px`,
|
||||||
) : isInRoom && shape.props.roomUrl && typeof window !== 'undefined' ? (
|
height: `${shape.props.h}px`,
|
||||||
<div className="mb-4" style={{ width: '100%', height: '100%' }}>
|
position: 'absolute',
|
||||||
<whereby-embed
|
top: '10px',
|
||||||
room={shape.props.roomUrl}
|
left: '10px',
|
||||||
background="off"
|
zIndex: 9999,
|
||||||
logo="off"
|
padding: '15px', // Increased padding by 5px
|
||||||
chat="off"
|
margin: 0,
|
||||||
screenshare="on"
|
backgroundColor: '#F0F0F0', // Light gray background
|
||||||
people="on"
|
boxShadow: '0 2px 4px rgba(0, 0, 0, 0.1)', // Added drop shadow
|
||||||
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
borderRadius: '4px', // Slight border radius for softer look
|
||||||
></whereby-embed>
|
}}>
|
||||||
</div>
|
<div style={{
|
||||||
) : (
|
width: '100%',
|
||||||
<div>
|
height: '100%',
|
||||||
<button onClick={joinRoom} className="bg-blue-500 text-white px-4 py-2 rounded">
|
border: '1px solid #D3D3D3',
|
||||||
Join Room
|
backgroundColor: '#FFFFFF',
|
||||||
</button>
|
display: 'flex',
|
||||||
{error && <p className="text-red-500 mt-2">{error}</p>}
|
justifyContent: 'center',
|
||||||
</div>
|
alignItems: 'center',
|
||||||
)}
|
overflow: 'hidden',
|
||||||
|
}}>
|
||||||
|
{isLoading ? (
|
||||||
|
<p>Joining room...</p>
|
||||||
|
) : isInRoom && shape.props.roomUrl && typeof window !== 'undefined' ? (
|
||||||
|
<div className="mb-4" style={{ width: '100%', height: '100%', objectFit: 'contain' }}>
|
||||||
|
<whereby-embed
|
||||||
|
room={shape.props.roomUrl}
|
||||||
|
background="off"
|
||||||
|
logo="off"
|
||||||
|
chat="off"
|
||||||
|
screenshare="on"
|
||||||
|
people="on"
|
||||||
|
style={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
||||||
|
></whereby-embed>
|
||||||
|
</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>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,6 @@ export class TldrawDurableObject {
|
||||||
private readonly ctx: DurableObjectState,
|
private readonly ctx: DurableObjectState,
|
||||||
env: Environment
|
env: Environment
|
||||||
) {
|
) {
|
||||||
console.log("hello from durable object")
|
|
||||||
this.r2 = env.TLDRAW_BUCKET
|
this.r2 = env.TLDRAW_BUCKET
|
||||||
|
|
||||||
ctx.blockConcurrencyWhile(async () => {
|
ctx.blockConcurrencyWhile(async () => {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue