diff options
| author | Dhravya <[email protected]> | 2024-06-30 20:50:24 -0500 |
|---|---|---|
| committer | Dhravya <[email protected]> | 2024-06-30 20:50:24 -0500 |
| commit | ffd141ade4e6074ee486da7f74f31e3905807cb9 (patch) | |
| tree | 505d73b0a7c04cdec93d7f5be88c635642716c15 /apps/web/lib | |
| parent | show updates in the extension (diff) | |
| parent | Merge pull request #93 from Dhravya/editor (diff) | |
| download | supermemory-ffd141ade4e6074ee486da7f74f31e3905807cb9.tar.xz supermemory-ffd141ade4e6074ee486da7f74f31e3905807cb9.zip | |
merge conflicts
Diffstat (limited to 'apps/web/lib')
| -rw-r--r-- | apps/web/lib/context.ts | 18 | ||||
| -rw-r--r-- | apps/web/lib/createAssetUrl.ts | 94 | ||||
| -rw-r--r-- | apps/web/lib/createEmbeds.ts | 236 | ||||
| -rw-r--r-- | apps/web/lib/loadSnap.ts | 14 |
4 files changed, 362 insertions, 0 deletions
diff --git a/apps/web/lib/context.ts b/apps/web/lib/context.ts new file mode 100644 index 00000000..840c0d31 --- /dev/null +++ b/apps/web/lib/context.ts @@ -0,0 +1,18 @@ +import { createContext, useContext } from "react"; + +export interface DragContextType { + isDraggingOver: boolean; + setIsDraggingOver: React.Dispatch<React.SetStateAction<boolean>>; +} + +const DragContext = createContext<DragContextType | undefined>(undefined); + +export const useDragContext = () => { + const context = useContext(DragContext); + if (context === undefined) { + throw new Error("useAppContext must be used within an AppProvider"); + } + return context; +}; + +export default DragContext; diff --git a/apps/web/lib/createAssetUrl.ts b/apps/web/lib/createAssetUrl.ts new file mode 100644 index 00000000..05c2baea --- /dev/null +++ b/apps/web/lib/createAssetUrl.ts @@ -0,0 +1,94 @@ +import { + AssetRecordType, + TLAsset, + getHashForString, + truncateStringWithEllipsis, +} from "tldraw"; +// import { BOOKMARK_ENDPOINT } from './config' + +interface ResponseBody { + title?: string; + description?: string; + image?: string; +} + +export async function createAssetFromUrl({ + url, +}: { + type: "url"; + url: string; +}): Promise<TLAsset> { + // try { + // // First, try to get the meta data from our endpoint + // const meta = (await ( + // await fetch(BOOKMARK_ENDPOINT, { + // method: 'POST', + // headers: { + // 'Content-Type': 'application/json', + // }, + // body: JSON.stringify({ + // url, + // }), + // }) + // ).json()) as ResponseBody + + // return { + // id: AssetRecordType.createId(getHashForString(url)), + // typeName: 'asset', + // type: 'bookmark', + // props: { + // src: url, + // description: meta.description ?? '', + // image: meta.image ?? '', + // title: meta.title ?? truncateStringWithEllipsis(url, 32), + // }, + // meta: {}, + // } + // } catch (error) { + // Otherwise, fallback to fetching data from the url + + let meta: { image: string; title: string; description: string }; + + try { + const resp = await fetch(url, { method: "GET", mode: "no-cors" }); + const html = await resp.text(); + const doc = new DOMParser().parseFromString(html, "text/html"); + meta = { + image: + doc.head + .querySelector('meta[property="og:image"]') + ?.getAttribute("content") ?? "", + title: + doc.head + .querySelector('meta[property="og:title"]') + ?.getAttribute("content") ?? truncateStringWithEllipsis(url, 32), + description: + doc.head + .querySelector('meta[property="og:description"]') + ?.getAttribute("content") ?? "", + }; + } catch (error) { + console.error(error); + meta = { + image: "", + title: truncateStringWithEllipsis(url, 32), + description: "", + }; + } + + // Create the bookmark asset from the meta + return { + id: AssetRecordType.createId(getHashForString(url)), + typeName: "asset", + type: "bookmark", + props: { + src: url, + image: meta.image, + title: meta.title, + description: meta.description, + favicon: meta.image, + }, + meta: {}, + }; + // } +} diff --git a/apps/web/lib/createEmbeds.ts b/apps/web/lib/createEmbeds.ts new file mode 100644 index 00000000..b3a7fb52 --- /dev/null +++ b/apps/web/lib/createEmbeds.ts @@ -0,0 +1,236 @@ +// @ts-nocheck TODO: A LOT OF TS ERRORS HERE + +import { + AssetRecordType, + Editor, + TLAsset, + TLAssetId, + TLBookmarkShape, + TLExternalContentSource, + TLShapePartial, + Vec, + VecLike, + createShapeId, + getEmbedInfo, + getHashForString, +} from "tldraw"; + +export default async function createEmbedsFromUrl({ + url, + point, + sources, + editor, +}: { + url: string; + point?: VecLike | undefined; + sources?: TLExternalContentSource[] | undefined; + editor: Editor; +}) { + const position = + point ?? + (editor.inputs.shiftKey + ? editor.inputs.currentPagePoint + : editor.getViewportPageBounds().center); + + if (url?.includes("x.com") || url?.includes("twitter.com")) { + return editor.createShape({ + type: "Twittercard", + x: position.x - 250, + y: position.y - 150, + props: { url: url }, + }); + } + + // try to paste as an embed first + const embedInfo = getEmbedInfo(url); + + if (embedInfo) { + return editor.putExternalContent({ + type: "embed", + url: embedInfo.url, + point, + embed: embedInfo.definition, + }); + } + + 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; + let shouldAlsoCreateAsset = false; + if (!asset) { + shouldAlsoCreateAsset = true; + try { + const bookmarkAsset = await editor.getAssetForExternalContent({ + type: "url", + url, + }); + const fetchWebsite: { + title?: string; + image?: string; + description?: string; + } = await ( + await fetch(`/api/unfirlsite?website=${url}`, { + method: "POST", + }) + ).json(); + if (bookmarkAsset) { + if (fetchWebsite.title) bookmarkAsset.props.title = fetchWebsite.title; + if (fetchWebsite.image) bookmarkAsset.props.image = fetchWebsite.image; + if (fetchWebsite.description) + bookmarkAsset.props.description = fetchWebsite.description; + } + if (!bookmarkAsset) throw Error("Could not create an asset"); + asset = bookmarkAsset; + } catch (e) { + console.log(e); + return; + } + } + + editor.batch(() => { + if (shouldAlsoCreateAsset) { + editor.createAssets([asset]); + } + + editor.updateShapes([ + { + id: shape.id, + type: shape.type, + props: { + assetId: asset.id, + }, + }, + ]); + }); +} + +function isURL(str: string) { + try { + new URL(str); + return true; + } catch { + return false; + } +} + +function formatTextToRatio(text: string) { + const totalWidth = text.length; + const maxLineWidth = Math.floor(totalWidth / 10); + + const words = text.split(" "); + let lines = []; + let currentLine = ""; + + words.forEach((word) => { + if ((currentLine + word).length <= maxLineWidth) { + currentLine += (currentLine ? " " : "") + word; + } else { + lines.push(currentLine); + currentLine = word; + } + }); + if (currentLine) { + lines.push(currentLine); + } + return { height: (lines.length + 1) * 18, width: maxLineWidth * 10 }; +} + +export function handleExternalDroppedContent({ + text, + editor, +}: { + text: string; + editor: Editor; +}) { + const position = editor.inputs.shiftKey + ? editor.inputs.currentPagePoint + : editor.getViewportPageBounds().center; + + if (isURL(text)) { + createEmbedsFromUrl({ editor, url: text }); + } else { + // editor.createShape({ + // type: "text", + // x: position.x - 75, + // y: position.y - 75, + // props: { + // text: text, + // size: "s", + // textAlign: "start", + // }, + // }); + const { height, width } = formatTextToRatio(text); + editor.createShape({ + type: "Textcard", + x: position.x - width / 2, + y: position.y - height / 2, + props: { + content: text, + extrainfo: "https://chatgpt.com/c/762cd44e-1752-495b-967a-aa3c23c6024a", + w: width, + h: height, + }, + }); + } +} + +function centerSelectionAroundPoint(editor: Editor, position: VecLike) { + // Re-position shapes so that the center of the group is at the provided point + const viewportPageBounds = editor.getViewportPageBounds(); + let selectionPageBounds = editor.getSelectionPageBounds(); + + if (selectionPageBounds) { + const offset = selectionPageBounds!.center.sub(position); + + editor.updateShapes( + editor.getSelectedShapes().map((shape) => { + const localRotation = editor + .getShapeParentTransform(shape) + .decompose().rotation; + const localDelta = Vec.Rot(offset, -localRotation); + return { + id: shape.id, + type: shape.type, + x: shape.x! - localDelta.x, + y: shape.y! - localDelta.y, + }; + }), + ); + } + + // Zoom out to fit the shapes, if necessary + selectionPageBounds = editor.getSelectionPageBounds(); + 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); + centerSelectionAroundPoint(editor, position); + }); + + return editor.getShape(partial.id) as TLBookmarkShape; +} diff --git a/apps/web/lib/loadSnap.ts b/apps/web/lib/loadSnap.ts new file mode 100644 index 00000000..083603eb --- /dev/null +++ b/apps/web/lib/loadSnap.ts @@ -0,0 +1,14 @@ +import { createTLStore, defaultShapeUtils, loadSnapshot } from "tldraw"; +import { getCanvasData } from "../app/actions/fetchers"; +import { twitterCardUtil } from "../components/canvas/twitterCard"; +import { textCardUtil } from "../components/canvas/textCard"; + +export async function loadRemoteSnapshot(id: string) { + const snapshot = await getCanvasData(id); + + const newStore = createTLStore({ + shapeUtils: [...defaultShapeUtils, twitterCardUtil, textCardUtil], + }); + loadSnapshot(newStore, snapshot.snapshot); + return newStore; +} |