feat(embeds): fallback for hung embeds and deleted X posts (#200)
CI / Build & Quality Checks (push) Successful in 3m24s
CI / Docker image build & smoke test (push) Skipped
CI / Secret scan (gitleaks) (push) Successful in 10s
CI / Trigger Desktop Build (push) Successful in 9s
CI / Playwright smoke (e2e) (push) Successful in 9m57s
CI / Build & Quality Checks (push) Successful in 3m24s
CI / Docker image build & smoke test (push) Skipped
CI / Secret scan (gitleaks) (push) Successful in 10s
CI / Trigger Desktop Build (push) Successful in 9s
CI / Playwright smoke (e2e) (push) Successful in 9m57s
Probed how real providers fail before picking signals: - X renders an EMPTY frame for a deleted/private/suspended post and says so only via postMessage `twttr.private.no_results`. The post embed now swaps to "This post isn't available…" with an "Open on X" link. - A hung frame never fires `load`. After 20 s every player (media, rich posts, TikTok, Steam widget, X) overlays "This embed is taking too long to load" with Retry (remounts the iframe) and "Open on <site>". A late `load` clears it. - Instagram and Bluesky show their own "removed / not found" page, and a refused request still fires `load` (browser error page), so neither needs or can use a guess. A missing height message is not treated as failure. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
1b5e6a37f5
commit
ba5b1ffe7d
@@ -168,6 +168,33 @@ export function extractEmbedHeight(data: unknown): number | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// How long an embed iframe may take to fire `load` before the card offers
|
||||
// Retry / "Open on …". Only a hang trips it: a frame whose request was refused,
|
||||
// or whose provider shows its own error page, still fires `load`.
|
||||
export const EMBED_LOAD_TIMEOUT_MS = 20_000;
|
||||
|
||||
/**
|
||||
* X's post embed (platform.twitter.com/embed/Tweet.html) renders an empty frame
|
||||
* for a deleted, private or suspended post and reports it only via this
|
||||
* postMessage: `{"twttr.embed": {"method": "twttr.private.no_results"}}`.
|
||||
* `data` may be the raw string or the parsed object.
|
||||
*/
|
||||
export function isTwitterNoResults(data: unknown): boolean {
|
||||
let d = data;
|
||||
if (typeof d === 'string') {
|
||||
try {
|
||||
d = JSON.parse(d);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (!d || typeof d !== 'object') return false;
|
||||
const tw = (d as { 'twttr.embed'?: unknown })['twttr.embed'];
|
||||
if (!tw) return false;
|
||||
const calls = (Array.isArray(tw) ? tw : [tw]) as Array<{ method?: string }>;
|
||||
return calls.some((c) => c?.method === 'twttr.private.no_results');
|
||||
}
|
||||
|
||||
export function tiktokPlayerEmbedUrl(id: string): string {
|
||||
// Pure 9:16 video player. music_info/description default OFF (they'd switch
|
||||
// TikTok to a wide "video + info panel" layout); controls/progress/play/volume/
|
||||
|
||||
Reference in New Issue
Block a user