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, BlockLayout } 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 (
{/*
*/}
{!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 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 (
);
};