a tool for shared writing and social publishing

use iframely for urls in embed block editor

+65 -16
+65 -16
components/Blocks/EmbedBlock.tsx
··· 10 10 import { Input } from "components/Input"; 11 11 import { isUrl } from "src/utils/isURL"; 12 12 import { elementId } from "src/utils/elementId"; 13 - import { deleteBlock } from "./DeleteBlock"; 14 13 import { focusBlock } from "src/utils/focusBlock"; 15 14 import { useDrag } from "src/hooks/useDrag"; 16 15 import { BlockEmbedSmall } from "components/Icons/BlockEmbedSmall"; 17 16 import { CheckTiny } from "components/Icons/CheckTiny"; 17 + import { DotLoader } from "components/utils/DotLoader"; 18 + import { 19 + LinkPreviewBody, 20 + LinkPreviewMetadataResult, 21 + } from "app/api/link_previews/route"; 18 22 19 23 export const EmbedBlock = (props: BlockProps & { preview?: boolean }) => { 20 24 let { permissions } = useEntitySetContext(); ··· 132 136 133 137 let entity_set = useEntitySetContext(); 134 138 let [linkValue, setLinkValue] = useState(""); 139 + let [loading, setLoading] = useState(false); 135 140 let { rep } = useReplicache(); 136 141 let submit = async () => { 137 142 let entity = props.entityID; ··· 149 154 } 150 155 let link = linkValue; 151 156 if (!linkValue.startsWith("http")) link = `https://${linkValue}`; 152 - // these mutations = simpler subset of addLinkBlock 153 157 if (!rep) return; 154 - await rep.mutate.assertFact({ 155 - entity: entity, 156 - attribute: "block/type", 157 - data: { type: "block-type-union", value: "embed" }, 158 - }); 159 - await rep?.mutate.assertFact({ 160 - entity: entity, 161 - attribute: "embed/url", 162 - data: { 163 - type: "string", 164 - value: link, 165 - }, 166 - }); 158 + 159 + // Try to get embed URL from iframely, fallback to direct URL 160 + setLoading(true); 161 + try { 162 + let res = await fetch("/api/link_previews", { 163 + headers: { "Content-Type": "application/json" }, 164 + method: "POST", 165 + body: JSON.stringify({ url: link, type: "meta" } as LinkPreviewBody), 166 + }); 167 + 168 + let embedUrl = link; 169 + let embedHeight = 360; 170 + 171 + if (res.status === 200) { 172 + let data = await (res.json() as LinkPreviewMetadataResult); 173 + if (data.success && data.data.links?.player?.[0]) { 174 + let embed = data.data.links.player[0]; 175 + embedUrl = embed.href; 176 + embedHeight = embed.media?.height || 300; 177 + } 178 + } 179 + 180 + await rep.mutate.assertFact([ 181 + { 182 + entity: entity, 183 + attribute: "embed/url", 184 + data: { 185 + type: "string", 186 + value: embedUrl, 187 + }, 188 + }, 189 + { 190 + entity: entity, 191 + attribute: "embed/height", 192 + data: { 193 + type: "number", 194 + value: embedHeight, 195 + }, 196 + }, 197 + ]); 198 + } catch { 199 + // On any error, fallback to using the URL directly 200 + await rep.mutate.assertFact([ 201 + { 202 + entity: entity, 203 + attribute: "embed/url", 204 + data: { 205 + type: "string", 206 + value: link, 207 + }, 208 + }, 209 + ]); 210 + } finally { 211 + setLoading(false); 212 + } 167 213 }; 168 214 let smoker = useSmoker(); 169 215 ··· 171 217 <form 172 218 onSubmit={(e) => { 173 219 e.preventDefault(); 220 + if (loading) return; 174 221 let rect = document 175 222 .getElementById("embed-block-submit") 176 223 ?.getBoundingClientRect(); ··· 212 259 <button 213 260 type="submit" 214 261 id="embed-block-submit" 262 + disabled={loading} 215 263 className={`p-1 ${isSelected && !isLocked ? "text-accent-contrast" : "text-border"}`} 216 264 onMouseDown={(e) => { 217 265 e.preventDefault(); 266 + if (loading) return; 218 267 if (!linkValue || linkValue === "") { 219 268 smoker({ 220 269 error: true, ··· 234 283 submit(); 235 284 }} 236 285 > 237 - <CheckTiny /> 286 + {loading ? <DotLoader /> : <CheckTiny />} 238 287 </button> 239 288 </div> 240 289 </form>