From cacb4b7827b333ad7938d2297b4875b97b3b04cc Mon Sep 17 00:00:00 2001 From: Steve Ruiz Date: Wed, 18 May 2022 21:46:24 +0100 Subject: [PATCH] [improvement] copy assets when copying to multiplayer room (#694) --- apps/www/components/Editor.tsx | 4 ++ apps/www/components/MultiplayerEditor.tsx | 3 +- apps/www/hooks/useMultiplayerAssets.ts | 34 ++++++++++++++- apps/www/hooks/useUploadAssets.ts | 36 ++++++++++++++++ apps/www/pages/api/create.ts | 8 ++-- packages/tldraw/src/Tldraw.tsx | 5 +++ .../MultiplayerMenu/MultiplayerMenu.tsx | 43 +++++++++++++++---- packages/tldraw/src/state/TldrawApp.ts | 4 ++ 8 files changed, 122 insertions(+), 15 deletions(-) create mode 100644 apps/www/hooks/useUploadAssets.ts diff --git a/apps/www/components/Editor.tsx b/apps/www/components/Editor.tsx index 3285802ae..538f7ee6a 100644 --- a/apps/www/components/Editor.tsx +++ b/apps/www/components/Editor.tsx @@ -1,5 +1,6 @@ import { Tldraw, TldrawApp, TldrawProps, useFileSystem } from '@tldraw/tldraw' import { useAccountHandlers } from 'hooks/useAccountHandlers' +import { useUploadAssets } from 'hooks/useUploadAssets' import React, { FC } from 'react' import * as gtag from 'utils/gtag' @@ -35,6 +36,8 @@ const Editor: FC> = ({ const { onSignIn, onSignOut } = useAccountHandlers() + const { onAssetUpload } = useUploadAssets() + return (
> = ({ showSponsorLink={!isSponsor} onSignIn={isSponsor ? undefined : onSignIn} onSignOut={isUser ? onSignOut : undefined} + onAssetUpload={onAssetUpload} {...fileSystemEvents} {...rest} /> diff --git a/apps/www/components/MultiplayerEditor.tsx b/apps/www/components/MultiplayerEditor.tsx index 864f23452..00ab94348 100644 --- a/apps/www/components/MultiplayerEditor.tsx +++ b/apps/www/components/MultiplayerEditor.tsx @@ -42,7 +42,7 @@ function Editor({ roomId, isUser, isSponsor }: Props) { const fileSystemEvents = useFileSystem() const { onSignIn, onSignOut } = useAccountHandlers() const { error, ...events } = useMultiplayerState(roomId) - const { onAssetCreate, onAssetDelete } = useMultiplayerAssets() + const { onAssetCreate, onAssetUpload, onAssetDelete } = useMultiplayerAssets() if (error) return Error: {error.message} @@ -57,6 +57,7 @@ function Editor({ roomId, isUser, isSponsor }: Props) { onSignOut={isUser ? onSignOut : undefined} onAssetCreate={onAssetCreate} onAssetDelete={onAssetDelete} + onAssetUpload={onAssetUpload} {...fileSystemEvents} {...events} /> diff --git a/apps/www/hooks/useMultiplayerAssets.ts b/apps/www/hooks/useMultiplayerAssets.ts index 68bea16d7..7fb61e76b 100644 --- a/apps/www/hooks/useMultiplayerAssets.ts +++ b/apps/www/hooks/useMultiplayerAssets.ts @@ -1,4 +1,4 @@ -import { TldrawApp } from '@tldraw/tldraw' +import { TDAsset, TldrawApp } from '@tldraw/tldraw' import { useCallback } from 'react' export function useMultiplayerAssets() { @@ -32,10 +32,40 @@ export function useMultiplayerAssets() { [] ) + const onAssetUpload = useCallback( + // Send the asset to our upload enpoint, which in turn will send it to AWS and + // respond with the URL of the uploaded file. + async (app: TldrawApp, id: string, asset: TDAsset): Promise => { + const filename = encodeURIComponent(asset.id) + + const fileType = encodeURIComponent(asset.type) + + const res = await fetch(`/api/upload?file=${filename}&fileType=${fileType}`) + + const { url, fields } = await res.json() + + const formData = new FormData() + + Object.entries({ ...fields, asset }).forEach(([key, value]) => { + formData.append(key, value as any) + }) + + const upload = await fetch(url, { + method: 'POST', + body: formData, + }) + + if (!upload.ok) return false + + return url + '/' + filename + }, + [] + ) + const onAssetDelete = useCallback(async (app: TldrawApp, id: string): Promise => { // noop return true }, []) - return { onAssetCreate, onAssetDelete } + return { onAssetCreate, onAssetUpload, onAssetDelete } } diff --git a/apps/www/hooks/useUploadAssets.ts b/apps/www/hooks/useUploadAssets.ts new file mode 100644 index 000000000..89d3aecdc --- /dev/null +++ b/apps/www/hooks/useUploadAssets.ts @@ -0,0 +1,36 @@ +import { TDAsset, TldrawApp } from '@tldraw/tldraw' +import { useCallback } from 'react' + +export function useUploadAssets() { + const onAssetUpload = useCallback( + // Send the asset to our upload enpoint, which in turn will send it to AWS and + // respond with the URL of the uploaded file. + async (app: TldrawApp, id: string, asset: TDAsset): Promise => { + const filename = encodeURIComponent(asset.id) + + const fileType = encodeURIComponent(asset.type) + + const res = await fetch(`/api/upload?file=${filename}&fileType=${fileType}`) + + const { url, fields } = await res.json() + + const formData = new FormData() + + Object.entries({ ...fields, asset }).forEach(([key, value]) => { + formData.append(key, value as any) + }) + + const upload = await fetch(url, { + method: 'POST', + body: formData, + }) + + if (!upload.ok) return false + + return url + '/' + filename + }, + [] + ) + + return { onAssetUpload } +} diff --git a/apps/www/pages/api/create.ts b/apps/www/pages/api/create.ts index 8eccd723b..2c3f994f1 100644 --- a/apps/www/pages/api/create.ts +++ b/apps/www/pages/api/create.ts @@ -51,7 +51,7 @@ export default async function CreateMultiplayerRoom(req: NextApiRequest, res: Ne // }, // }).then((d) => d.json()) - //POST + // POST const result = await fetch(`https://liveblocks.net/api/v1/room/${roomId}/storage`, { method: 'POST', mode: 'cors', @@ -64,10 +64,12 @@ export default async function CreateMultiplayerRoom(req: NextApiRequest, res: Ne }) if (result.status === 200) { - res.send({ status: 'success', roomId }) + res.send({ status: 'success', message: result.statusText, roomId }) + } else { + res.send({ status: 'error', message: result.statusText }) } } catch (e) { - res.send({ status: 'error' }) + res.send({ status: 'error', message: e.message }) // noop } } diff --git a/packages/tldraw/src/Tldraw.tsx b/packages/tldraw/src/Tldraw.tsx index 456c0db19..f1b71f6f4 100644 --- a/packages/tldraw/src/Tldraw.tsx +++ b/packages/tldraw/src/Tldraw.tsx @@ -128,6 +128,7 @@ export function Tldraw({ onChangePage, onAssetCreate, onAssetDelete, + onAssetUpload, onExport, }: TldrawProps) { const [sId, setSId] = React.useState(id) @@ -153,6 +154,7 @@ export function Tldraw({ onChangePage, onAssetDelete, onAssetCreate, + onAssetUpload, }) return app }) @@ -179,6 +181,7 @@ export function Tldraw({ onChangePage, onAssetDelete, onAssetCreate, + onAssetUpload, onExport, }) @@ -243,6 +246,7 @@ export function Tldraw({ onChangePage, onAssetDelete, onAssetCreate, + onAssetUpload, onExport, } }, [ @@ -264,6 +268,7 @@ export function Tldraw({ onChangePage, onAssetDelete, onAssetCreate, + onAssetUpload, onExport, ]) diff --git a/packages/tldraw/src/components/TopPanel/MultiplayerMenu/MultiplayerMenu.tsx b/packages/tldraw/src/components/TopPanel/MultiplayerMenu/MultiplayerMenu.tsx index 304f9ed73..9ca190ae2 100644 --- a/packages/tldraw/src/components/TopPanel/MultiplayerMenu/MultiplayerMenu.tsx +++ b/packages/tldraw/src/components/TopPanel/MultiplayerMenu/MultiplayerMenu.tsx @@ -43,10 +43,25 @@ export const MultiplayerMenu = React.memo(function MultiplayerMenu() { }, []) const handleCopyToMultiplayerRoom = React.useCallback(async () => { + const nextDocument = { ...app.document } + + // TODO: Upload images to server + if (app.callbacks.onAssetUpload) { + for (const id in nextDocument.assets) { + const asset = nextDocument.assets[id] + const newSrc = await app.callbacks.onAssetUpload(app, id, asset) + if (newSrc) { + asset.src = newSrc + } else { + asset.src = '' + } + } + } + const body = JSON.stringify({ roomId: Utils.uniqueId(), pageId: app.currentPageId, - document: app.document, + document: nextDocument, }) const myHeaders = new Headers({ @@ -54,16 +69,26 @@ export const MultiplayerMenu = React.memo(function MultiplayerMenu() { 'Content-Type': 'application/json', }) - const res = await fetch(`/api/create`, { - headers: myHeaders, - method: 'POST', - mode: 'no-cors', - body, - }).then((res) => res.json()) + app.setIsLoading(true) - if (res?.roomId) { - window.location.href = `/r/${res.roomId}` + try { + const res = await fetch(`/api/create`, { + headers: myHeaders, + method: 'POST', + mode: 'no-cors', + body, + }).then((res) => res.json()) + + if (res?.roomId) { + window.location.href = `/r/${res.roomId}` + } else { + TLDR.warn(res.message) + } + } catch (e: any) { + TLDR.warn(e.message) } + + app.setIsLoading(false) }, []) return ( diff --git a/packages/tldraw/src/state/TldrawApp.ts b/packages/tldraw/src/state/TldrawApp.ts index 7effee32f..eeb1e7f95 100644 --- a/packages/tldraw/src/state/TldrawApp.ts +++ b/packages/tldraw/src/state/TldrawApp.ts @@ -162,6 +162,10 @@ export interface TDCallbacks { * (optional) A callback to run when an asset will be created. Should return the value for the image/video's `src` property. */ onAssetCreate?: (app: TldrawApp, file: File, id: string) => Promise + /** + * (optional) A callback to run when an asset will be uploaded. Should return the value for the image/video's `src` property. + */ + onAssetUpload?: (app: TldrawApp, id: string, asset: TDAsset) => Promise /** * (optional) A callback to run when the user exports their page or selection. */