diff --git a/src/app/components/url-preview/UrlPreview.css.tsx b/src/app/components/url-preview/UrlPreview.css.tsx index 480a4eb96..52d6d8b82 100644 --- a/src/app/components/url-preview/UrlPreview.css.tsx +++ b/src/app/components/url-preview/UrlPreview.css.tsx @@ -271,6 +271,17 @@ export const EmbedPlaceholder = style([ }, ]); +// Audio embeds (Spotify / SoundCloud) — full-width, short fixed height (set inline). +export const EmbedMediaAudio = style([ + DefaultReset, + { + position: 'relative', + width: '100%', + overflow: 'hidden', + backgroundColor: color.Surface.Container, + }, +]); + // --------------------------------------------------------------------------- // Shared square artwork thumbnail (Spotify, IMDb poster, Discord icon) // --------------------------------------------------------------------------- @@ -389,6 +400,21 @@ export const BadgeTikTok = style({ color: '#ffffff', }); +export const BadgeSoundCloud = style({ + backgroundColor: '#ff5500', + color: '#ffffff', +}); + +export const BadgeDailymotion = style({ + backgroundColor: '#0066dc', + color: '#ffffff', +}); + +export const BadgeStreamable = style({ + backgroundColor: '#0f90fa', + color: '#ffffff', +}); + // --------------------------------------------------------------------------- // Twitch LIVE badge // --------------------------------------------------------------------------- diff --git a/src/app/components/url-preview/UrlPreviewCard.tsx b/src/app/components/url-preview/UrlPreviewCard.tsx index 7ba148479..d1882fa4d 100644 --- a/src/app/components/url-preview/UrlPreviewCard.tsx +++ b/src/app/components/url-preview/UrlPreviewCard.tsx @@ -19,14 +19,7 @@ import { ImageViewer } from '../image-viewer'; import { onEnterOrSpace } from '../../utils/keyboard'; import { useSetting } from '../../state/hooks/settings'; import { settingsAtom } from '../../state/settings'; -import { - buildVideoEmbedUrl, - getVimeoVideoId, - getYouTubeVideoId, - getYoutubeShortsId, - isYouTubeShorts, - VideoEmbedProvider, -} from '../../utils/videoEmbed'; +import { MediaEmbed, parseMediaEmbed } from '../../utils/videoEmbed'; const linkStyles = { color: color.Success.Main }; @@ -35,10 +28,7 @@ const linkStyles = { color: color.Success.Main }; // --------------------------------------------------------------------------- type CardVariant = - | 'youtube-shorts' - | 'youtube' | 'tiktok' - | 'vimeo' | 'github' | 'twitter' | 'reddit' @@ -283,11 +273,10 @@ function isTenor(url: string): boolean { } function getCardVariant(url: string): CardVariant { - // Shorts must be detected before generic youtube - if (isYouTubeShorts(url)) return 'youtube-shorts'; - if (getYouTubeVideoId(url) !== null) return 'youtube'; + // NOTE: embeddable providers (YouTube/Vimeo/TikTok/Spotify/Twitch/…) are handled + // upstream by parseMediaEmbed + MediaEmbedCard; getCardVariant only routes the + // non-embed fallbacks (incl. TikTok short links with no resolvable id). if (isTikTok(url)) return 'tiktok'; - if (getVimeoVideoId(url) !== null) return 'vimeo'; if (isGitHubRepo(url)) return 'github'; if (isTwitter(url)) return 'twitter'; if (getRedditSubreddit(url) !== null) return 'reddit'; @@ -435,25 +424,7 @@ function SiteBadge({ label, colorClass }: { label: string; colorClass: string }) } // --------------------------------------------------------------------------- -// Card 1: YouTube Shorts -// --------------------------------------------------------------------------- - -function YouTubeShortsCard({ url, prev }: { url: string; prev: IPreviewUrlResponse }) { - return ( - - ); -} - -// --------------------------------------------------------------------------- -// Card 2: TikTok +// Card: TikTok (fallback for short vm.tiktok.com links that can't resolve an id) // --------------------------------------------------------------------------- function TikTokCard({ @@ -922,22 +893,30 @@ function RedditCard({ // in the (cookie-less) YouTube / Vimeo iframe, so nothing loads from the third // party until the user presses play. Gated by the `inlineMediaEmbeds` setting — // when off it falls back to a link that opens the video in a new tab. -function VideoEmbedCard({ +const EMBED_BADGE: Record = { + youtube: { label: 'YouTube', class: '' }, + vimeo: { label: 'Vimeo', class: previewCss.BadgeVimeo }, + tiktok: { label: 'TikTok', class: previewCss.BadgeTikTok }, + dailymotion: { label: 'Dailymotion', class: previewCss.BadgeDailymotion }, + streamable: { label: 'Streamable', class: previewCss.BadgeStreamable }, + twitch: { label: 'Twitch', class: previewCss.BadgeTwitchPurple }, + spotify: { label: 'Spotify', class: previewCss.BadgeSpotify }, + soundcloud: { label: 'SoundCloud', class: previewCss.BadgeSoundCloud }, +}; + +// 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 +// from the third party until the user presses play. Gated by `inlineMediaEmbeds` +// — when off it falls back to a link that opens the media in a new tab. +function MediaEmbedCard({ url, prev, - provider, - videoId, - portrait, - siteBadgeLabel, - siteBadgeClass, + embed, }: { url: string; prev: IPreviewUrlResponse; - provider: VideoEmbedProvider; - videoId: string; - portrait?: boolean; - siteBadgeLabel: string; - siteBadgeClass: string; + embed: MediaEmbed; }) { const mx = useMatrixClient(); const useAuthentication = useMediaAuthentication(); @@ -947,6 +926,8 @@ function VideoEmbedCard({ const title = prev['og:title'] ?? ''; const description = prev['og:description'] ?? ''; const mxcImage = prev['og:image'] as string | undefined; + const portrait = embed.kind === 'portrait'; + const audio = embed.kind === 'audio'; const thumbnailUrl = mxcImage ? mxcUrlToHttp( mx, @@ -959,7 +940,18 @@ function VideoEmbedCard({ ) : undefined; - const mediaClass = portrait ? previewCss.EmbedMediaPortrait : previewCss.EmbedMediaLandscape; + // Shorts share the youtube provider but get their own badge. + const badge = + embed.provider === 'youtube' && portrait + ? { label: 'Shorts', class: previewCss.BadgeYouTubeShorts } + : (EMBED_BADGE[embed.provider] ?? { label: embed.provider, class: '' }); + + const mediaClass = audio + ? previewCss.EmbedMediaAudio + : portrait + ? previewCss.EmbedMediaPortrait + : previewCss.EmbedMediaLandscape; + const mediaStyle = audio ? { height: `${embed.height ?? 152}px` } : undefined; const thumb = thumbnailUrl ? ( {title} @@ -978,12 +970,12 @@ function VideoEmbedCard({ return ( -
+
{playing ? (