instant bookmarks (#2176)

this PR does a couple of things when creating bookmarks

- any time a url was pasted it was previously calling `fetch` on the url
to check whether the url is an image that we have cors access to. In
that case we can paste the image itself rather than a bookmark. But
that's gonna be a relatively rare use case, and the check itself seemed
to cost anywhere from 200ms to +1s which is certainly not worth it when
the fallback behaviour (create a regular bookmark) is fine. So i moved
that check behind a url pathname extension check. i.e. if the url
pathname ends with .gif, .jpg, .jpeg, .svg, or .png, then it will check
whether we can paste the image directly, otherwise it will always do a
regular bookmark.
- we create an asset-less bookmark shape on the canvas while we wait for
the asset to load if it is not already available. This means the user
gets immediate feedback that their paste succeeded, but they won't see
the actual bookmark details for a little bit.

It looks like this

![Kapture 2023-11-08 at 10 34
35](https://github.com/tldraw/tldraw/assets/1242537/89c93612-a794-419f-aa68-1efdb82bfbf2)

### Change Type

- [x] `minor` — New feature


### Release Notes

- Improves ux around pasting bookmarks
This commit is contained in:
David Sheldrick 2023-11-08 11:08:03 +00:00 committed by GitHub
parent 8dceb5ee31
commit 133b8fbdc0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 83 additions and 33 deletions

View file

@ -4,7 +4,9 @@ import {
MediaHelpers,
TLAsset,
TLAssetId,
TLBookmarkShape,
TLEmbedShape,
TLShapeId,
TLShapePartial,
TLTextShape,
TLTextShapeProps,
@ -354,6 +356,7 @@ export function registerDefaultExternalContentHandlers(
point ?? (editor.inputs.shiftKey ? editor.inputs.currentPagePoint : editor.viewportPageCenter)
const assetId: TLAssetId = AssetRecordType.createId(getHashForString(url))
const shape = createEmptyBookmarkShape(editor, url, position)
// Use an existing asset if we have one, or else else create a new one
let asset = editor.getAsset(assetId) as TLAsset
@ -370,13 +373,25 @@ export function registerDefaultExternalContentHandlers(
editor.createAssets([asset])
}
createShapesForAssets(editor, [asset], position)
editor.updateShapes([
{
...shape,
props: {
...shape.props,
assetId: asset.id,
},
},
])
})
})
}
export async function createShapesForAssets(editor: Editor, assets: TLAsset[], position: VecLike) {
if (!assets.length) return
export async function createShapesForAssets(
editor: Editor,
assets: TLAsset[],
position: VecLike
): Promise<TLShapeId[]> {
if (!assets.length) return []
const currentPoint = Vec2d.From(position)
const partials: TLShapePartial[] = []
@ -445,6 +460,14 @@ export async function createShapesForAssets(editor: Editor, assets: TLAsset[], p
// Create the shapes
editor.createShapes(partials).select(...partials.map((p) => p.id))
// Re-position shapes so that the center of the group is at the provided point
centerSelecitonAroundPoint(editor, position)
})
return partials.map((p) => p.id)
}
function centerSelecitonAroundPoint(editor: Editor, position: VecLike) {
// Re-position shapes so that the center of the group is at the provided point
const { viewportPageBounds } = editor
let { selectionPageBounds } = editor
@ -471,5 +494,29 @@ export async function createShapesForAssets(editor: Editor, assets: TLAsset[], p
if (selectionPageBounds && !viewportPageBounds.contains(selectionPageBounds)) {
editor.zoomToSelection()
}
})
}
export function createEmptyBookmarkShape(
editor: Editor,
url: string,
position: VecLike
): TLBookmarkShape {
const partial: TLShapePartial = {
id: createShapeId(),
type: 'bookmark',
x: position.x - 150,
y: position.y - 160,
opacity: 1,
props: {
assetId: null,
url,
},
}
editor.batch(() => {
editor.createShapes([partial]).select(partial.id)
centerSelecitonAroundPoint(editor, position)
})
return editor.getShape(partial.id) as TLBookmarkShape
}

View file

@ -18,12 +18,15 @@ export async function pasteUrl(
) {
// Lets see if its an image and we have CORs
try {
const resp = await fetch(url)
// skip this step if the url doesn't contain an image extension, treat it as a regular bookmark
if (new URL(url).pathname.match(/\.(png|jpe?g|gif|svg|webp)$/i)) {
const resp = await fetch(url, { method: 'HEAD' })
if (resp.headers.get('content-type')?.match(/^image\//)) {
editor.mark('paste')
pasteFiles(editor, [url])
return
}
}
} catch (err: any) {
if (err.message !== 'Failed to fetch') {
console.error(err)