diff options
| author | Dhravya Shah <[email protected]> | 2024-07-28 10:58:22 -0700 |
|---|---|---|
| committer | Dhravya Shah <[email protected]> | 2024-07-28 10:58:22 -0700 |
| commit | a3f551e55768066cbb13ea1ce53a7a1e0a64d088 (patch) | |
| tree | f10eeb2b9894948c2dbdf52001ec4f842490c4ca /apps/web/lib | |
| parent | lockfile (diff) | |
| parent | Merge branch 'main' into canvas (diff) | |
| download | supermemory-a3f551e55768066cbb13ea1ce53a7a1e0a64d088.tar.xz supermemory-a3f551e55768066cbb13ea1ce53a7a1e0a64d088.zip | |
merged
Diffstat (limited to 'apps/web/lib')
| -rw-r--r-- | apps/web/lib/ExternalDroppedContent.ts | 134 | ||||
| -rw-r--r-- | apps/web/lib/createEmbeds.ts | 161 | ||||
| -rw-r--r-- | apps/web/lib/drophelpers.ts | 67 | ||||
| -rw-r--r-- | apps/web/lib/loadSnap.ts | 5 | ||||
| -rw-r--r-- | apps/web/lib/unfirlsite.ts | 53 |
5 files changed, 268 insertions, 152 deletions
diff --git a/apps/web/lib/ExternalDroppedContent.ts b/apps/web/lib/ExternalDroppedContent.ts new file mode 100644 index 00000000..1333cf20 --- /dev/null +++ b/apps/web/lib/ExternalDroppedContent.ts @@ -0,0 +1,134 @@ +import { Editor } from "tldraw"; +import createEmbedsFromUrl from "./createEmbeds"; + +function processURL(input: string): string | null { + let str = input.trim(); + if (!/^(?:f|ht)tps?:\/\//i.test(str)) { + str = "http://" + str; + } + try { + const url = new URL(str); + return url.href; + } catch { + return str.match( + /^(https?:\/\/)?(www\.)?[a-z0-9]+([-.]{1}[a-z0-9]+)*\.[a-z]{2,5}(\/.*)?$/i, + ) + ? str + : null; + } +} + +function formatContent(title: string, content: string): string { + const totalLength = title.length + content.length; + const totalHeight = Math.ceil(totalLength * (2 / 3)); + const titleLines = Math.ceil(totalHeight * (2 / 5)); + const contentLines = totalHeight - titleLines; + + const titleWithNewLines = title + "\n".repeat(titleLines); + const contentWithNewLines = content + "\n".repeat(contentLines); + + return `${titleWithNewLines.trim()}\n\n${contentWithNewLines.trim()}`; +} + +function formatTextToRatio(text: string): { height: number; width: number } { + const RATIO = 4 / 3; + const FONT_SIZE = 15; + const CHAR_WIDTH = FONT_SIZE * 0.6; + const LINE_HEIGHT = FONT_SIZE * 1.2; + const MIN_WIDTH = 200; + + let width = Math.min( + 800, + Math.max(MIN_WIDTH, Math.ceil(text.length * CHAR_WIDTH)), + ); + + width = Math.ceil(width / 4) * 4; + + const maxLineWidth = Math.floor(width / CHAR_WIDTH); + + const words = text.split(" "); + let lines: string[] = []; + let currentLine = ""; + + words.forEach((word) => { + if ((currentLine + word).length <= maxLineWidth) { + currentLine += (currentLine ? " " : "") + word; + } else { + lines.push(currentLine); + currentLine = word; + } + }); + if (currentLine) { + lines.push(currentLine); + } + + let height = Math.ceil(lines.length * LINE_HEIGHT); + + if (width / height > RATIO) { + width = Math.ceil(height * RATIO); + } else { + height = Math.ceil(width / RATIO); + } + + return { height, width }; +} + +type CardData = { + title: string; + type: string; + content: string; + text: boolean; +}; + +type DroppedData = CardData | string | { imageUrl: string }; + +export function handleExternalDroppedContent({ + droppedData, + editor, +}: { + droppedData: DroppedData; + editor: Editor; +}) { + const position = editor.inputs.shiftKey + ? editor.inputs.currentPagePoint + : editor.getViewportPageBounds().center; + + if (typeof droppedData === "string") { + const processedURL = processURL(droppedData); + if (processedURL) { + createEmbedsFromUrl({ editor, url: processedURL }); + } else { + createTextCard(editor, position, droppedData, "String Content"); + } + } else if ("imageUrl" in droppedData) { + } else { + const { content, title, type, text } = droppedData; + if (text) { + const { height, width } = formatTextToRatio(content); + editor.createShape({ + type: "text", + x: position.x - width / 2, + y: position.y - height / 2, + props: { text: formatContent(title, content) }, + }); + } else { + createTextCard(editor, position, title, type, content); + } + } +} + +function createTextCard( + editor: Editor, + position: { x: number; y: number }, + content: string, + type: string, + extraInfo: string = "", +) { + const { height, width } = formatTextToRatio(content); + editor.createShape({ + type: "Textcard", + x: position.x - width / 2, + y: position.y - height / 2, + props: { content, type, extrainfo: extraInfo, h: 200, w: 400 }, + }); +} diff --git a/apps/web/lib/createEmbeds.ts b/apps/web/lib/createEmbeds.ts index 75347d31..e3f67340 100644 --- a/apps/web/lib/createEmbeds.ts +++ b/apps/web/lib/createEmbeds.ts @@ -1,19 +1,15 @@ -// @ts-nocheck TODO: A LOT OF TS ERRORS HERE - import { AssetRecordType, Editor, TLAsset, TLAssetId, - TLBookmarkShape, TLExternalContentSource, - TLShapePartial, - Vec, VecLike, - createShapeId, getEmbedInfo, getHashForString, } from "tldraw"; +import { createEmptyBookmarkShape } from "./drophelpers"; +import { unfirlSite } from "@/app/actions/fetchers"; export default async function createEmbedsFromUrl({ url, @@ -32,7 +28,8 @@ export default async function createEmbedsFromUrl({ ? editor.inputs.currentPagePoint : editor.getViewportPageBounds().center); - if (url?.includes("x.com") || url?.includes("twitter.com")) { + const urlPattern = /https?:\/\/(x\.com|twitter\.com)\/[\w]+\/[\w]+\/[\d]+/; + if (urlPattern.test(url)) { return editor.createShape({ type: "Twittercard", x: position.x - 250, @@ -56,7 +53,6 @@ export default async function createEmbedsFromUrl({ 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) { @@ -66,20 +62,14 @@ export default async function createEmbedsFromUrl({ type: "url", url, }); - const fetchWebsite: { - title?: string; - image?: string; - description?: string; - } = await ( - await fetch(`/api/unfirlsite?website=${url}`, { - method: "POST", - }) - ).json(); + const value = await unfirlSite(url); 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.type === "bookmark") { + if (value.title) bookmarkAsset.props.title = value.title; + if (value.image) bookmarkAsset.props.image = value.image; + if (value.description) + bookmarkAsset.props.description = value.description; + } } if (!bookmarkAsset) throw Error("Could not create an asset"); asset = bookmarkAsset; @@ -105,132 +95,3 @@ export default async function createEmbedsFromUrl({ ]); }); } - -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/drophelpers.ts b/apps/web/lib/drophelpers.ts new file mode 100644 index 00000000..9094da6e --- /dev/null +++ b/apps/web/lib/drophelpers.ts @@ -0,0 +1,67 @@ +import { + Editor, + TLBookmarkShape, + TLShapePartial, + Vec, + VecLike, + createShapeId, +} from "tldraw"; + +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; +} + +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(); + } +} diff --git a/apps/web/lib/loadSnap.ts b/apps/web/lib/loadSnap.ts index bcf81eca..2d3cd24e 100644 --- a/apps/web/lib/loadSnap.ts +++ b/apps/web/lib/loadSnap.ts @@ -1,7 +1,8 @@ import { createTLStore, defaultShapeUtils, loadSnapshot } from "tldraw"; import { getCanvasData } from "../app/actions/fetchers"; -import { twitterCardUtil } from "../components/canvas/twitterCard"; -import { textCardUtil } from "../components/canvas/textCard"; +// import { twitterCardUtil } from "../components/canvas/custom_nodes/twitterCard"; +import { twitterCardUtil } from "@/components/canvas/custom_nodes/twittercard"; +import { textCardUtil } from "@/components/canvas/custom_nodes/textcard"; export async function loadRemoteSnapshot(id: string) { const snapshot = await getCanvasData(id); diff --git a/apps/web/lib/unfirlsite.ts b/apps/web/lib/unfirlsite.ts new file mode 100644 index 00000000..a7a77c0c --- /dev/null +++ b/apps/web/lib/unfirlsite.ts @@ -0,0 +1,53 @@ +import cheerio from "cheerio"; + +export async function unfurl(url: string) { + const response = await fetch(url); + if (response.status >= 400) { + throw new Error(`Error fetching url: ${response.status}`); + } + const contentType = response.headers.get("content-type"); + if (!contentType?.includes("text/html")) { + throw new Error(`Content-type not right: ${contentType}`); + } + + const content = await response.text(); + const $ = cheerio.load(content); + + const og: { [key: string]: string | undefined } = {}; + const twitter: { [key: string]: string | undefined } = {}; + // @ts-ignore trust + $("meta[property^=og:]").each( + (_, el) => (og[$(el).attr("property")!] = $(el).attr("content")), + ); + // @ts-ignore trust + $("meta[name^=twitter:]").each( + (_, el) => (twitter[$(el).attr("name")!] = $(el).attr("content")), + ); + + const title = + og["og:title"] ?? + twitter["twitter:title"] ?? + $("title").text() ?? + undefined; + const description = + og["og:description"] ?? + twitter["twitter:description"] ?? + $('meta[name="description"]').attr("content") ?? + undefined; + const image = + og["og:image:secure_url"] ?? + og["og:image"] ?? + twitter["twitter:image"] ?? + undefined; + const favicon = + $('link[rel="apple-touch-icon"]').attr("href") ?? + $('link[rel="icon"]').attr("href") ?? + undefined; + + return { + title, + description, + image, + favicon, + }; +} |