Mobile-audit batch 1. All fixes reuse cinny's own responsive primitives and are
mobile-gated so desktop is unchanged.
- Message tables: wrap <table> in an overflow-x container so a wide table scrolls
instead of overflowing the message column / page body.
- Composer toolbar: let the before|editable|after row and the toolbar wrap on
phones (@media <=750px) instead of squeezing the editable to zero and pushing
Send off-screen.
- In-call control bar: collapse to the compact/stacked layout on a mobile
viewport (ScreenSize.Mobile) too, not just when the bar's own container is
<500px — fixes the 500-750px band where the control row overflowed.
- URL-preview card: base width toRem(400) -> min(25rem, 92vw) so a single card
fits a narrow phone (still exactly 400px on desktop).
- Explore card grid: drop to one column at <=750px (was a fixed 3-col grid).
Two review passes: desktop behavior provably unchanged (all gated by @media /
ScreenSize.Mobile; the table wrapper only contains previously-overflowing tables).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The inline video embed container (EmbedMediaLandscape/Portrait) used CSS
aspect-ratio for its 16:9 / 9:16 box. An absolutely-positioned *replaced* element
— the player <iframe> (position:absolute; inset:0; width/height:100%) — collapses
to its ~200px intrinsic size inside an aspect-ratio box rather than filling it, so
after pressing play the YouTube/Vimeo player rendered at ~202x114 in the top-left of
the (correct) 600x340 facade box, leaving a wide gray gap. The click-to-play
facade uses an <img>, which doesn't hit this, so the pre-play preview looked fine.
Switch the video containers to the padding-top percentage hack (56.25% / 177.78%),
which derives a definite height from the definite width so the absolutely-positioned
iframe fills it reliably. Fullscreen override sets padding-top:0 + height:100vh.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
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>
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>
- Widen the interactive-embed card to min(38rem, 94vw) so the Tidal player (and
other audio/video embeds) has enough width.
- Hide the title/description caption while a video embed is playing, so the text
below it stops squeezing the player small (Twitch).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
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>
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>
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>
YouTube Shorts: portrait 9:16 thumbnail, red Shorts badge, channel parse
TikTok: portrait thumbnail, @user extract, caption parse (3 OG formats),
hashtag chips, dark ♫ placeholder fallback
Twitter/X: tweet text parse from all og:title formats, media image when
og:image:width>=300, profile vs tweet URL distinction, 𝕏 SVG badge
Twitch: live/clip/VOD detection, pulsing LIVE badge with CSS keyframes,
game extraction from og:description, channel from URL
Reddit: r/subreddit badge, u/author + upvote + comment count parsed from
og:description, post thumbnail 80x60, redd.it short URL support
Shared PortraitThumbnail (80x142) reused by TikTok + Shorts.
All brand hex colors in CSS file only, never in TSX.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
P2-7: Domain-specific URL preview cards — YouTube shows mqdefault.jpg
thumbnail with ▶ play overlay + title; GitHub shows inline SVG icon +
owner/repo parsed from og:title + star/language info from og:description;
generic cards get a Google favicon when og:image is absent; empty cards
(no title or description) are suppressed entirely
P2-9: Live local time in user profiles — useLocalTime(timezone, hour12)
uses Intl.DateTimeFormat with the user's m.tz IANA zone; updates every
60s; shows clock icon + formatted time + timezone abbreviation (EST/JST
etc.) in dim text; respects existing hour24Clock setting
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>