Commit Graph
15 Commits
Author SHA1 Message Date
Claude 4154cae55a fix(embeds): animate GIF previews; add Mixcloud/Deezer; misc embed fixes
CI / Build & Quality Checks (push) Successful in 11m21s
CI / Trigger Desktop Build (push) Successful in 34s
GIF previews rendered but never played: Synapse's /thumbnail endpoint
flattens animated GIFs to a still first frame. GifCard and the generic OG
card now request the original via /download (no width/height) for GIFs, so
they animate. Guarded with shouldServeGifOriginal(): a matrix:image:size cap
(10 MB) keeps a huge self-hosted GIF on the frozen thumbnail, and the generic
card's eager <img> gains loading="lazy" (it was the one preview image missing
it) so originals stay off the wire until near the viewport.

Also adds Mixcloud + Deezer inline media embeds (iframe widgets via
parseMediaEmbed/MediaEmbedCard, matching the existing click-to-play pattern),
and fixes Deezer podcast links: they live at /show/<id>, not /podcast/<id>
(the latter 404s on Deezer's own oEmbed) — verified against the live API.

Reviewed by two agents; both findings (Deezer /show, GIF eager-load) fixed
and covered by tests. Desktop Tauri frame-src CSP updated separately.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 00:21:10 -04:00
jaredandClaude Opus 4.8 ef82650cf7 feat(embeds): detailed Steam store / news / app-widget embeds
Recognize store.steampowered.com content URLs and render each richly, within
the existing privacy-first facade. 2-agent reviewed (both SHIP).

- getSteamTarget / steamWidgetEmbedUrl (videoEmbed.ts, +tests): classify
  /app/{id}, /news/app/{id}/view/{gid}, /(bundle|sub|dlc)/{id}; non-content
  pages (home/search/wishlist) and other hosts fall through to the generic card.
- SteamCard now dispatches:
  - app → OG capsule header + click-to-play facade → Steam's OFFICIAL store
    widget iframe (store.steampowered.com/widget/{id}): live region-aware price,
    discount %, Buy on Steam. Nothing loads from Steam until "Show price &
    store" is pressed; gated by the inlineMediaEmbeds setting. App pages use the
    wide card so the ~646px widget has room.
  - news → rich announcement card (banner + headline + body preview + link) —
    your example URL previously fell through to the plain generic card.
  - bundle/sub/dlc → the OG store card.

Grounded in our CSP: the widget works via frame-src https: (no infra change),
images route through the homeserver (img-src excludes Steam), and there is NO
client-side Steam API call (connect-src + Steam CORS both block it) — which is
also the honest ceiling: no review scores/genres client-side, price/buy come
from the official widget.

Runtime QA still needed: the live widget iframe rendering (height/fit) can't be
verified headlessly.

Gates: tsc 0, eslint 0, prettier clean, 912 tests, build ok.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 21:41:06 -04:00
jaredandClaude Opus 4.8 f2673effe4 fix(embeds): parsing over/under-match + broken thumbnails + wide layout
Bugs found by a 3-agent audit of the inline-embed system (core posture —
sandbox, postMessage origin+source, XSS, noreferrer, oEmbed — verified sound);
fixes reviewed by 2 agents on the staged diff (both SHIP).

Parsing (videoEmbed.ts, + tests):
- Twitch/Kick/SoundCloud/Streamable reserved-path exclusion — their own utility
  pages (twitch.tv/directory, kick.com/browse, soundcloud.com/discover/…,
  streamable.com/login, bare /videos) no longer render as broken player embeds.
- SoundCloud: `/<artist>/<tab>` profile-tab listings excluded; `/<artist>/sets/<slug>`
  real sets still detected.
- Vimeo: unlisted-hash capture constrained to lowercase-hex, so a normal video's
  trailing segment (/likes, /settings, a slug) isn't captured as a bogus `h=`
  param that Vimeo then rejects.

Rendering (UrlPreviewCard.tsx, RenderMessageContent.tsx):
- Spotify/Steam/Discord/IMDb route og:image through mxcUrlToHttp like every other
  card — a raw og:image is an mxc:// URI (broken <img> on standard Synapse) or an
  off-homeserver request that defeats the click-to-play facade.
- `wide` card class now follows the RESOLVED embed (incl. the og:url short-link
  fallback), so an og:url-resolved player gets the wide layout, not a cramped one.
- Twitter host detection (isTwitter/isTwitterTweet) aligned with getTweetId —
  mobile.twitter.com and legacy /statuses/ now route to the Twitter card/embed.
- De-dupe preview URLs so a message repeating a link doesn't render sibling
  cards with identical React keys.

Gates: tsc 0, eslint 0, prettier clean, 910 tests, build ok.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 23:35:59 -04:00
jaredandClaude Opus 4.8 85ac8de5d9 style: apply prettier across fork files
check:prettier was not part of my gate routine, so formatting drift accumulated
across the session's touched files (and a few older ones). Run prettier --write
to bring the repo back to 'All matched files use Prettier code style!'.
Formatting only — no logic changes. tsc/tests/build all green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 13:52:36 -04:00
jaredandClaude Opus 4.8 fd87a27251 fix(embeds): stop building broken redd.it / i.redd.it Reddit embeds
embed.reddit.com only renders the /r/<sub>/comments/<id> path (verified against
reddit's embed widgets.js + curl: frame-ancestors * , no X-Frame-Options, so
iframing itself is fine). But getRedditPostEmbed built a bare
embed.reddit.com/comments/<id> for redd.it short links — which serves a 'not
found' page — and the i. host strip routed i.redd.it/*.jpg image links into the
same branch. Returning a (broken) URL also suppressed renderContent's og:url
fallback that would resolve the short link to its canonical /r/<sub>/... form.

Fix: getRedditPostEmbed returns null for any non-reddit.com host, so redd.it /
i.redd.it / v.redd.it fall through to the og:url fallback (working embed or a
normal preview card, never a blank 'not found' iframe). The reddit.com post path
is unchanged. Tests updated (22 pass).

Note: the live 'broken' symptom is mostly the deploy gap — live still runs the
old www.redditmedia.com embed code while the live CSP only allows
embed.reddit.com; deploying the current lotus branch (which emits
embed.reddit.com) resolves it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 20:09:05 -04:00
jaredandClaude Opus 4.8 013f113bc2 feat(embeds): add Bluesky, Loom, Kick
CI / Build & Quality Checks (push) Successful in 10m47s
CI / Trigger Desktop Build (push) Successful in 9s
From the coverage-review agent's ranked recommendations (clean iframes, ids in
the URL, one CSP host each):
- Bluesky: bsky.app/profile/{authority}/post/{rkey} → embed.bsky.app (rich, self-
  resizing like the other post embeds).
- Loom: loom.com/share|embed/{id} → www.loom.com/embed/{id} (16:9).
- Kick: kick.com/{channel} → player.kick.com/{channel} (live channels only; VODs/
  clips have no clean embed and fall back to a link).

Parsers unit-tested. CSP frame-src gains embed.bsky.app / www.loom.com /
player.kick.com (desktop + live web, both updated). Needs live verification once
deployed since the embeds themselves can't be exercised from here.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 00:50:39 -04:00
jaredandClaude Opus 4.8 a52c9e12a4 fix(embeds): quality pass — close button, focus, a11y, perf, SoundCloud revert
From the quality-review agents:
- Revert on.soundcloud.com support: the w.soundcloud widget doesn't follow the
  redirect (needs an oEmbed resolve, deferred).
- Add a Close button to playing video/TikTok embeds and a Collapse button to the
  expanded X post — playback was previously one-way (only escapable by scrolling).
- focus-visible ring on the embed facade (folds resets outline:none, leaving
  keyboard users with no indicator).
- Only subscribe to resize postMessages while the iframe is mounted (was attaching
  a global listener per Instagram/Reddit facade before play).
- TikTok oEmbed fetch now uses AbortController (abort on unmount) + aria-busy /
  'Loading…' label on the resolving spinner.
- Decorative facade thumbnails use alt="" (parent already names them); drop the
  dangling-colon aria-labels when there's no title.
- Cap URL previews at 6 per message so a link-dump can't spawn dozens of fetches.

Tests 728. No CSP change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 00:45:51 -04:00
jaredandClaude Opus 4.8 a28c305835 feat(embeds): TikTok URL trim, SoundCloud on. + redd.it short links
- TikTok player URL trimmed to ?autoplay=1&rel=0 (the control params were all
  default-on no-ops).
- SoundCloud on.soundcloud.com share short links now embed (widget follows the
  redirect via w.soundcloud.com).
- redd.it short links now embed via embed.reddit.com/comments/<id>/ (the redirect
  target is reddit.com/comments/<id>, no subreddit needed).

No CSP change (hosts already allowlisted). Vimeo event/ondemand deferred — the
embed format/host couldn't be verified from a fake id and would need a CSP
change; leaving it for the review agents to research. Tests 21.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 00:32:09 -04:00
jaredandClaude Opus 4.8 3694a3612d fix(embeds): second-pass review — Reddit auto-height, sandbox, Vimeo/Shorts gaps
From the second review pass (agents inspected providers' live embed scripts):
- FIX Reddit auto-height (was broken): embed.reddit.com posts
  { type:'resize.embed', data:<height> } — height is under 'data', not 'height',
  so it never resized and clipped taller posts (scrolling=no). Add that shape.
- Align TweetEmbed sandbox with EMBED_SANDBOX (adds allow-popups-to-escape-sandbox)
  so links/login popups opened from inside a tweet aren't crippled.
- Vimeo: resolve channel/group/album video forms (vimeo.com/channels/{n}/{id} etc.),
  not just paths starting with the id.
- Mobile Shorts: m.youtube.com/shorts/{id} now renders portrait 9:16, not landscape.
- Move extractEmbedHeight into videoEmbed.ts and unit-test all three resize shapes
  (Instagram MEASURE / Reddit resize.embed / Twitter twttr.private.resize).

Agents confirmed everything else current & robust (Dailymotion geo host, Tidal
gridify, sandbox tokens are a safe superset, X still on platform.twitter.com,
550px cap). Tests 21 in this suite.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 00:08:51 -04:00
jaredandClaude Opus 4.8 4de2d233ef feat(embeds): apply review findings + fix TikTok portrait padding
From the review-agent audit:
- Dailymotion: move off the Sept-2024-deprecated /embed/video path to
  geo.dailymotion.com/player.html.
- Reddit: point at embed.reddit.com (www.redditmedia.com now 301s there).
- Vimeo: parse the unlisted hash (vimeo.com/{id}/{hash}) and pass &h=…, add dnt=1.
- Tidal: layout=gridify + ~275px height for albums/playlists (fixes narrow player).
- YouTube/Shorts: playsinline=1 (iOS keeps playback inline); parse /live/ +
  music.youtube.com.
- Apple Music: /music-video/ renders 16:9 instead of a fixed audio height.
- Re-add a minimal sandbox to all media iframes (omits allow-top-navigation →
  blocks phishing redirects) — defense-in-depth atop the CSP frame-src allowlist.
- Self-resize Instagram + Reddit post embeds via a shared useIframeAutoHeight hook
  (also now covers the Tweet embed; matches platform.x.com origin too); drop the
  fixed 720/480 heights. Cap tweet/post columns at ~550px, centered.

Also from user feedback: TikTok portrait player dropped music_info/description,
which forced TikTok's wide 'video + info panel' layout and left empty space
beside the video — now a clean 9:16 player that fills the box.

Tests 726 pass. CSP frame-src gains embed.reddit.com (separate desktop commit).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 23:56:47 -04:00
jaredandClaude Opus 4.8 b247a0447c fix(embeds): resolve TikTok short links via oEmbed + og:url fallback
CI / Build & Quality Checks (push) Successful in 10m51s
CI / Trigger Desktop Build (push) Successful in 9s
TikTok 'copy-link' share URLs (vm.tiktok.com, tiktok.com/t/…) carry no video id
and the homeserver's link preview is bot-walled (generic 'TikTok - Make Your
Day', no og:url/og:image), so they fell through to the static fallback card with
no play button.

New TikTokEmbedCard resolves the id client-side via TikTok's CORS-enabled oEmbed
API on click (keeps the facade privacy model), then plays the player/v1 embed
(portrait, autoplay + full controls + our fullscreen button). Canonical
/video/<id> links skip the lookup. Also added a general og:url fallback so other
short/redirect links resolve to their canonical form when the raw URL doesn't.

Web CSP connect-src gains www.tiktok.com for the oEmbed fetch (desktop already
allows https:). Tests 19/19.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 23:03:44 -04:00
jaredandClaude Opus 4.8 fbaa921f83 feat(embeds): fullscreen, wider cards, Instagram/Tidal/Reddit; fix TikTok/X/Twitch
CI / Build & Quality Checks (push) Successful in 10m35s
CI / Trigger Desktop Build (push) Successful in 8s
Feedback fixes + new platforms:
- Fullscreen: add a universal 'Fullscreen' button on video embeds (requests
  fullscreen on the media container) so Shorts/TikTok/etc. can go fullscreen
  regardless of each player's own controls. Portrait media enlarged (220->300).
- Drop the iframe sandbox (CSP-allowlisted trusted hosts; sandbox was breaking
  player features and likely TikTok).
- Wider, responsive embed/tweet cards (min(34rem,92vw)) so Twitch player chrome
  isn't cramped and X posts stop getting clipped.
- Instagram (p/reel/tv), Tidal (track/album/playlist/video), and Reddit posts
  now embed. Reddit uses redditmedia.com to bypass the homeserver's blocked
  preview (Reddit serves it a bot-check 'please wait for verification' page);
  a bot-wall title filter keeps that garbage out of any caption.

New pure parsers unit-tested (videoEmbed.test.ts, 17 cases). Desktop + live web
CSP frame-src updated for instagram.com, embed.tidal.com, redditmedia.com.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 20:55:47 -04:00
jaredandClaude Opus 4.8 791b1cb5ea feat(embeds): interactive X/Twitter post embed + Apple Music player
CI / Build & Quality Checks (push) Successful in 10m39s
CI / Trigger Desktop Build (push) Successful in 7s
X/Twitter: the static OG card was a snapshot — no video, no galleries/threads.
Keep it as the (reliable) facade and add a 'View post' button that loads the
official platform.twitter.com interactive embed on click: playable video/GIF,
image galleries, quote tweets. The embed self-sizes via a scoped postMessage
resize listener (matched to our iframe + the platform.twitter.com origin). Nothing
loads from X until the user clicks, and the link still works if X blocks the frame.

Apple Music: music.apple.com album/playlist/song links now play inline via the
embed.music.apple.com player (compact for a single song, tall for collections),
through the existing MediaEmbedCard audio path.

Pure parsers/builders unit-tested (13 cases). Desktop CSP frame-src adds
platform.twitter.com + embed.music.apple.com (separate commit).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 20:14:53 -04:00
jaredandClaude Opus 4.8 6c903fe3e5 feat(embeds): add TikTok, Twitch, Dailymotion, Streamable, Spotify, SoundCloud players
Generalize the inline-embed system: parseMediaEmbed() resolves any supported URL
to an embed spec (provider + kind + embed URL), and a single MediaEmbedCard
renders the media-forward click-to-play facade for all of them — landscape 16:9
(YouTube, Vimeo, Dailymotion, Streamable, Twitch), portrait 9:16 (Shorts, TikTok),
and short fixed-height audio players (Spotify, SoundCloud). Same privacy facade as
before: nothing loads from the third party until the user presses play, gated by
the 'Inline Media Players' setting.

Twitch embeds pass the current page hostname as the required parent param.
Non-embeddable fallbacks (e.g. vm.tiktok.com short links, non-media tweets) keep
their existing rich OG cards; X/Twitter intentionally keeps its rich tweet card.

All parsers/builders are pure + unit-tested (videoEmbed.test.ts, 10 cases).
Desktop Tauri CSP frame-src updated for the new hosts (separate commit).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 20:01:05 -04:00
jaredandClaude Opus 4.8 93f307cd63 feat(embeds): inline YouTube/Vimeo players + media-forward video tiles
CI / Build & Quality Checks (push) Successful in 11m17s
CI / Trigger Desktop Build (push) Successful in 10s
Video link tiles (YouTube, Shorts, Vimeo) now play in place instead of only
opening a browser tab. Adds a media-forward 16:9 (9:16 for Shorts) tile with a
privacy-friendly click-to-play facade: the homeserver's cached og:image thumbnail
+ a play button, and only on click does it swap in the cookie-less
youtube-nocookie / player.vimeo iframe — so nothing loads from Google/Vimeo until
the user presses play. Gated by a new 'Inline Media Players' setting (default on);
when off it falls back to a link that opens the video in a new tab.

Also sources YouTube thumbnails from the homeserver og:image instead of
img.youtube.com, which fixes the existing broken YouTube thumbnails on the web
build (nginx img-src has no YouTube host) and removes the pre-click Google request.

Pure URL parsing + embed-URL building moved to utils/videoEmbed.ts (unit-tested).

Note: the desktop app's Tauri CSP frame-src must allow the video hosts (separate
commit in cinny-desktop).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 19:48:12 -04:00