diff --git a/src/app/components/url-preview/UrlPreview.css.tsx b/src/app/components/url-preview/UrlPreview.css.tsx index 5c2bea788..3e29e20f8 100644 --- a/src/app/components/url-preview/UrlPreview.css.tsx +++ b/src/app/components/url-preview/UrlPreview.css.tsx @@ -14,6 +14,13 @@ export const UrlPreview = style([ }, ]); +// Wider, responsive card for interactive embeds (video players, tweets, posts) +// so the player chrome / tweet content isn't cramped or clipped. Defined after +// UrlPreview so its width wins the cascade. +export const UrlPreviewWide = style({ + width: 'min(34rem, 92vw)', +}); + export const UrlPreviewImg = style([ DefaultReset, { @@ -209,6 +216,9 @@ export const EmbedMediaLandscape = style([ aspectRatio: '16 / 9', overflow: 'hidden', backgroundColor: color.Surface.Container, + selectors: { + '&:fullscreen': { width: '100vw', height: '100vh', aspectRatio: 'auto' }, + }, }, ]); @@ -216,12 +226,15 @@ export const EmbedMediaPortrait = style([ DefaultReset, { position: 'relative', - width: toRem(220), + width: toRem(300), maxWidth: '100%', aspectRatio: '9 / 16', margin: '0 auto', overflow: 'hidden', backgroundColor: color.Surface.Container, + selectors: { + '&:fullscreen': { width: '100vw', height: '100vh', aspectRatio: 'auto', margin: 0 }, + }, }, ]); @@ -430,6 +443,16 @@ export const BadgeAppleMusic = style({ color: '#ffffff', }); +export const BadgeInstagram = style({ + backgroundColor: '#e1306c', + color: '#ffffff', +}); + +export const BadgeTidal = style({ + backgroundColor: '#000000', + color: '#ffffff', +}); + // --------------------------------------------------------------------------- // Twitch LIVE badge // --------------------------------------------------------------------------- diff --git a/src/app/components/url-preview/UrlPreviewCard.tsx b/src/app/components/url-preview/UrlPreviewCard.tsx index e122f9c09..bee4311e6 100644 --- a/src/app/components/url-preview/UrlPreviewCard.tsx +++ b/src/app/components/url-preview/UrlPreviewCard.tsx @@ -991,8 +991,18 @@ const EMBED_BADGE: Record = { spotify: { label: 'Spotify', class: previewCss.BadgeSpotify }, soundcloud: { label: 'SoundCloud', class: previewCss.BadgeSoundCloud }, applemusic: { label: 'Apple Music', class: previewCss.BadgeAppleMusic }, + tidal: { label: 'TIDAL', class: previewCss.BadgeTidal }, + instagram: { label: 'Instagram', class: previewCss.BadgeInstagram }, + reddit: { label: 'Reddit', class: previewCss.BadgeReddit }, }; +// The homeserver preview for some sites (notably Reddit) comes back as a bot-check +// "please wait for verification" page; don't surface that as the caption. +const looksLikeBotWall = (s: string): boolean => + /please wait|are you a (human|robot)|verifying|verification|just a moment|attention required/i.test( + s, + ); + // One media-forward tile for every embeddable provider (video / portrait / audio). // Privacy-friendly facade: shows the homeserver's cached thumbnail + a play // button; only on click does it swap in the provider iframe, so nothing loads @@ -1011,12 +1021,17 @@ function MediaEmbedCard({ const useAuthentication = useMediaAuthentication(); const [inlineMediaEmbeds] = useSetting(settingsAtom, 'inlineMediaEmbeds'); const [playing, setPlaying] = useState(false); + const mediaRef = useRef(null); - const title = prev['og:title'] ?? ''; - const description = prev['og:description'] ?? ''; - const mxcImage = prev['og:image'] as string | undefined; + const rawTitle = prev['og:title'] ?? ''; + const rawDescription = prev['og:description'] ?? ''; const portrait = embed.kind === 'portrait'; - const audio = embed.kind === 'audio'; + const video = embed.kind === 'landscape' || embed.kind === 'portrait'; + const fixedHeight = embed.kind === 'audio' || embed.kind === 'rich'; + // 'rich' providers (esp. Reddit) can carry a bot-wall title — suppress it. + const title = looksLikeBotWall(rawTitle) ? '' : rawTitle; + const description = looksLikeBotWall(rawDescription) ? '' : rawDescription; + const mxcImage = prev['og:image'] as string | undefined; const thumbnailUrl = mxcImage ? mxcUrlToHttp( mx, @@ -1035,12 +1050,12 @@ function MediaEmbedCard({ ? { label: 'Shorts', class: previewCss.BadgeYouTubeShorts } : (EMBED_BADGE[embed.provider] ?? { label: embed.provider, class: '' }); - const mediaClass = audio + const mediaClass = fixedHeight ? previewCss.EmbedMediaAudio : portrait ? previewCss.EmbedMediaPortrait : previewCss.EmbedMediaLandscape; - const mediaStyle = audio ? { height: `${embed.height ?? 152}px` } : undefined; + const mediaStyle = fixedHeight ? { height: `${embed.height ?? 152}px` } : undefined; const thumb = thumbnailUrl ? ( {title} @@ -1057,16 +1072,21 @@ function MediaEmbedCard({ ); + const enterFullscreen = () => { + mediaRef.current?.requestFullscreen?.().catch(() => undefined); + }; + return ( -
+
{playing ? ( + // No sandbox: these are CSP-allowlisted trusted hosts, and sandboxing + // breaks player features (fullscreen, TikTok's flow, etc.).