import { useEntitySetContext } from "components/EntitySetProvider"; import { generateKeyBetween } from "fractional-indexing"; import { useCallback, useEffect, useState } from "react"; import { useEntity, useReplicache } from "src/replicache"; import { useUIState } from "src/useUIState"; import { BlockProps } from "./Block"; import { v7 } from "uuid"; import { useSmoker } from "components/Toast"; import { Separator } from "components/Layout"; import { Input } from "components/Input"; import { isUrl } from "src/utils/isURL"; import { elementId } from "src/utils/elementId"; import { focusBlock } from "src/utils/focusBlock"; import { useDrag } from "src/hooks/useDrag"; import { BlockEmbedSmall } from "components/Icons/BlockEmbedSmall"; import { CheckTiny } from "components/Icons/CheckTiny"; import { DotLoader } from "components/utils/DotLoader"; import { LinkPreviewBody, LinkPreviewMetadataResult, } from "app/api/link_previews/route"; export const EmbedBlock = (props: BlockProps & { preview?: boolean }) => { let { permissions } = useEntitySetContext(); let { rep } = useReplicache(); let url = useEntity(props.entityID, "embed/url"); let isCanvasBlock = props.pageType === "canvas"; let isSelected = useUIState((s) => s.selectedBlocks.find((b) => b.value === props.entityID), ); let height = useEntity(props.entityID, "embed/height")?.data.value || 360; let heightOnDragEnd = useCallback( (dragPosition: { x: number; y: number }) => { rep?.mutate.assertFact({ entity: props.entityID, attribute: "embed/height", data: { type: "number", value: height + dragPosition.y, }, }); }, [props, rep, height], ); let heightHandle = useDrag({ onDragEnd: heightOnDragEnd }); useEffect(() => { if (props.preview) return; let input = document.getElementById(elementId.block(props.entityID).input); if (isSelected) { input?.focus(); } else input?.blur(); }, [isSelected, props.entityID, props.preview]); if (!url) { if (!permissions.write) return null; return ( ); } if (props.preview) return null; return (
{/* the iframe! can also add 'allow' and 'referrerpolicy' attributes later if needed */} {/*
{url?.data.value}
*/} {!props.preview && permissions.write && ( <>
)}
); }; // TODO: maybe extract into a component… // would just have to branch for the mutations (addLinkBlock or addEmbedBlock) const BlockLinkInput = (props: BlockProps) => { let isSelected = useUIState((s) => s.selectedBlocks.find((b) => b.value === props.entityID), ); let isLocked = useEntity(props.entityID, "block/is-locked")?.data.value; let entity_set = useEntitySetContext(); let [linkValue, setLinkValue] = useState(""); let [loading, setLoading] = useState(false); let { rep } = useReplicache(); let submit = async () => { let entity = props.entityID; if (!entity) { entity = v7(); await rep?.mutate.addBlock({ permission_set: entity_set.set, factID: v7(), parent: props.parent, type: "card", position: generateKeyBetween(props.position, props.nextPosition), newEntityID: entity, }); } let link = linkValue; if (!linkValue.startsWith("http")) link = `https://${linkValue}`; if (!rep) return; // Try to get embed URL from iframely, fallback to direct URL setLoading(true); try { let res = await fetch("/api/link_previews", { headers: { "Content-Type": "application/json" }, method: "POST", body: JSON.stringify({ url: link, type: "meta" } as LinkPreviewBody), }); let embedUrl = link; let embedHeight = 360; if (res.status === 200) { let data = await (res.json() as LinkPreviewMetadataResult); if (data.success && data.data.links?.player?.[0]) { let embed = data.data.links.player[0]; embedUrl = embed.href; embedHeight = embed.media?.height || 300; } } await rep.mutate.assertFact([ { entity: entity, attribute: "embed/url", data: { type: "string", value: embedUrl, }, }, { entity: entity, attribute: "embed/height", data: { type: "number", value: embedHeight, }, }, ]); } catch { // On any error, fallback to using the URL directly await rep.mutate.assertFact([ { entity: entity, attribute: "embed/url", data: { type: "string", value: link, }, }, ]); } finally { setLoading(false); } }; let smoker = useSmoker(); return (
{ e.preventDefault(); if (loading) return; let rect = document .getElementById("embed-block-submit") ?.getBoundingClientRect(); if (!linkValue || linkValue === "") { smoker({ error: true, text: "no url!", position: { x: rect ? rect.left + 12 : 0, y: rect ? rect.top : 0 }, }); return; } if (!isUrl(linkValue)) { smoker({ error: true, text: "invalid url!", position: { x: rect ? rect.left + 12 : 0, y: rect ? rect.top : 0, }, }); return; } submit(); }} >
setLinkValue(e.target.value)} />
); };