From 5dc592bfabd20b27588a9c8d7bc6fa385c100a90 Mon Sep 17 00:00:00 2001 From: Arjun <6592213+arkml@users.noreply.github.com> Date: Thu, 16 Apr 2026 00:32:34 +0530 Subject: [PATCH] improvements --- .../renderer/src/extensions/iframe-block.tsx | 135 +++++++++++++++++- apps/x/apps/renderer/src/styles/editor.css | 56 ++++++++ .../x/packages/core/src/local-sites/server.ts | 91 +++++++++++- .../core/src/local-sites/templates.ts | 1 + 4 files changed, 280 insertions(+), 3 deletions(-) diff --git a/apps/x/apps/renderer/src/extensions/iframe-block.tsx b/apps/x/apps/renderer/src/extensions/iframe-block.tsx index 38cdc432..db371872 100644 --- a/apps/x/apps/renderer/src/extensions/iframe-block.tsx +++ b/apps/x/apps/renderer/src/extensions/iframe-block.tsx @@ -2,8 +2,15 @@ import { mergeAttributes, Node } from '@tiptap/react' import { ReactNodeViewRenderer, NodeViewWrapper } from '@tiptap/react' import { ExternalLink, Globe, X } from 'lucide-react' import { blocks } from '@x/shared' +import { useEffect, useRef, useState } from 'react' +const IFRAME_HEIGHT_MESSAGE = 'rowboat:iframe-height' +const IFRAME_HEIGHT_CACHE_PREFIX = 'rowboat:iframe-height:' const DEFAULT_IFRAME_HEIGHT = 560 +const MIN_IFRAME_HEIGHT = 240 +const HEIGHT_UPDATE_THRESHOLD = 4 +const AUTO_RESIZE_SETTLE_MS = 160 +const LOAD_FALLBACK_READY_MS = 180 const DEFAULT_IFRAME_ALLOW = [ 'accelerometer', 'autoplay', @@ -29,6 +36,43 @@ function getIframeMeta(url: string): { host: string; path: string } | null { } } +function getIframeHeightCacheKey(url: string): string { + return `${IFRAME_HEIGHT_CACHE_PREFIX}${url}` +} + +function readCachedIframeHeight(url: string, fallbackHeight: number): number { + try { + const raw = window.localStorage.getItem(getIframeHeightCacheKey(url)) + if (!raw) return fallbackHeight + const parsed = Number.parseInt(raw, 10) + if (!Number.isFinite(parsed)) return fallbackHeight + return Math.max(MIN_IFRAME_HEIGHT, parsed) + } catch { + return fallbackHeight + } +} + +function writeCachedIframeHeight(url: string, height: number): void { + try { + window.localStorage.setItem(getIframeHeightCacheKey(url), String(height)) + } catch { + // ignore storage failures + } +} + +function parseIframeHeightMessage(event: MessageEvent): { height: number } | null { + const data = event.data + if (!data || typeof data !== 'object') return null + + const candidate = data as { type?: unknown; height?: unknown } + if (candidate.type !== IFRAME_HEIGHT_MESSAGE) return null + if (typeof candidate.height !== 'number' || !Number.isFinite(candidate.height)) return null + + return { + height: Math.max(MIN_IFRAME_HEIGHT, Math.ceil(candidate.height)), + } +} + function IframeBlockView({ node, deleteNode }: { node: { attrs: Record }; deleteNode: () => void }) { const raw = node.attrs.data as string let config: blocks.IframeBlock | null = null @@ -53,7 +97,75 @@ function IframeBlockView({ node, deleteNode }: { node: { attrs: Record readCachedIframeHeight(config.url, initialHeight)) + const [frameReady, setFrameReady] = useState(false) + const iframeRef = useRef(null) + const loadFallbackTimerRef = useRef(null) + const autoResizeReadyTimerRef = useRef(null) + const frameReadyRef = useRef(false) + + useEffect(() => { + setFrameHeight(readCachedIframeHeight(config.url, initialHeight)) + setFrameReady(false) + frameReadyRef.current = false + if (loadFallbackTimerRef.current !== null) { + window.clearTimeout(loadFallbackTimerRef.current) + loadFallbackTimerRef.current = null + } + if (autoResizeReadyTimerRef.current !== null) { + window.clearTimeout(autoResizeReadyTimerRef.current) + autoResizeReadyTimerRef.current = null + } + }, [config.url, initialHeight, raw]) + + useEffect(() => { + frameReadyRef.current = frameReady + }, [frameReady]) + + useEffect(() => { + const handleMessage = (event: MessageEvent) => { + const iframeWindow = iframeRef.current?.contentWindow + if (!iframeWindow || event.source !== iframeWindow) return + + const message = parseIframeHeightMessage(event) + if (!message) return + + if (loadFallbackTimerRef.current !== null) { + window.clearTimeout(loadFallbackTimerRef.current) + loadFallbackTimerRef.current = null + } + if (autoResizeReadyTimerRef.current !== null) { + window.clearTimeout(autoResizeReadyTimerRef.current) + } + writeCachedIframeHeight(config.url, message.height) + setFrameHeight((currentHeight) => ( + Math.abs(currentHeight - message.height) < HEIGHT_UPDATE_THRESHOLD ? currentHeight : message.height + )) + + if (!frameReadyRef.current) { + autoResizeReadyTimerRef.current = window.setTimeout(() => { + setFrameReady(true) + frameReadyRef.current = true + autoResizeReadyTimerRef.current = null + }, AUTO_RESIZE_SETTLE_MS) + } + } + + window.addEventListener('message', handleMessage) + return () => window.removeEventListener('message', handleMessage) + }, [config.url]) + + useEffect(() => { + return () => { + if (loadFallbackTimerRef.current !== null) { + window.clearTimeout(loadFallbackTimerRef.current) + } + if (autoResizeReadyTimerRef.current !== null) { + window.clearTimeout(autoResizeReadyTimerRef.current) + } + } + }, []) return ( @@ -91,12 +203,31 @@ function IframeBlockView({ node, deleteNode }: { node: { attrs: Record -
+
+ {!frameReady && ( +