diff --git a/src/app/components/url-preview/UrlPreview.css.tsx b/src/app/components/url-preview/UrlPreview.css.tsx index 240ce8f85..89e5660c1 100644 --- a/src/app/components/url-preview/UrlPreview.css.tsx +++ b/src/app/components/url-preview/UrlPreview.css.tsx @@ -419,6 +419,56 @@ export const BadgeSteam = style({ color: '#c7d5e0', }); +// --------------------------------------------------------------------------- +// Steam card — full-width header/banner image + official store widget iframe +// --------------------------------------------------------------------------- + +export const SteamBannerWrapper = style([ + DefaultReset, + { + position: 'relative', + display: 'block', + width: '100%', + // Steam header capsules are 460×215 (~2.14:1); news banners vary but crop + // fine to the same ratio. + aspectRatio: '460 / 215', + overflow: 'hidden', + flexShrink: 0, + backgroundColor: '#0e1520', + cursor: 'pointer', + + ':hover': { + filter: 'brightness(0.9)', + }, + }, +]); + +export const SteamBannerImg = style([ + DefaultReset, + { + width: '100%', + height: '100%', + objectFit: 'cover', + objectPosition: 'center', + display: 'block', + }, +]); + +// The official Steam store widget is a compact banner (~646×190). Full-width, +// fixed height so the iframe doesn't collapse to its intrinsic size. +export const SteamWidget = style([ + DefaultReset, + { + width: '100%', + height: toRem(190), + border: 0, + display: 'block', + borderRadius: config.radii.R300, + backgroundColor: '#1b2838', + marginTop: config.space.S100, + }, +]); + export const BadgeWikipedia = style({ backgroundColor: color.SurfaceVariant.ContainerLine, color: color.SurfaceVariant.OnContainer, diff --git a/src/app/components/url-preview/UrlPreviewCard.tsx b/src/app/components/url-preview/UrlPreviewCard.tsx index ef3f10721..754668da6 100644 --- a/src/app/components/url-preview/UrlPreviewCard.tsx +++ b/src/app/components/url-preview/UrlPreviewCard.tsx @@ -34,11 +34,13 @@ import { useSetting } from '../../state/hooks/settings'; import { settingsAtom } from '../../state/settings'; import { extractEmbedHeight, + getSteamTarget, getTikTokVideoId, getTweetId, isTikTokLink, MediaEmbed, parseMediaEmbed, + steamWidgetEmbedUrl, tiktokIdFromOembed, tiktokOembedUrl, tiktokPlayerEmbedUrl, @@ -223,17 +225,6 @@ function isTwitch(url: string): boolean { } } -function isSteamApp(url: string): boolean { - try { - const { hostname, pathname } = new URL(url); - const h = hostname.replace(/^www\./, ''); - if (h !== 'store.steampowered.com') return false; - return pathname.startsWith('/app/'); - } catch { - return false; - } -} - function isWikipedia(url: string): boolean { try { const { hostname, pathname } = new URL(url); @@ -324,7 +315,7 @@ function getCardVariant(url: string): CardVariant { if (getRedditSubreddit(url) !== null) return 'reddit'; if (getSpotifyType(url) !== null) return 'spotify'; if (isTwitch(url)) return 'twitch'; - if (isSteamApp(url)) return 'steam'; + if (getSteamTarget(url)) return 'steam'; if (isWikipedia(url)) return 'wikipedia'; if (isDiscordInvite(url)) return 'discord'; if (isNpm(url)) return 'npm'; @@ -1562,7 +1553,8 @@ function SpotifyCard({ url, prev }: { url: string; prev: IPreviewUrlResponse }) ); } -function SteamCard({ url, prev }: { url: string; prev: IPreviewUrlResponse }) { +// Steam bundle/sub/dlc or other store page — OG card (no per-app widget). +function SteamStoreCard({ url, prev }: { url: string; prev: IPreviewUrlResponse }) { const mx = useMatrixClient(); const useAuthentication = useMediaAuthentication(); const title = prev['og:title'] ?? ''; @@ -1631,6 +1623,167 @@ function SteamCard({ url, prev }: { url: string; prev: IPreviewUrlResponse }) { ); } +// Steam news / announcement post — rich OG card (Steam has no official embed +// widget for announcements): banner + headline + body preview. +function SteamNewsCard({ url, prev }: { url: string; prev: IPreviewUrlResponse }) { + const mx = useMatrixClient(); + const useAuthentication = useMediaAuthentication(); + const title = prev['og:title'] ?? ''; + const description = prev['og:description'] ?? ''; + const mxcImage = prev['og:image'] as string | undefined; + const bannerUrl = mxcImage + ? mxcUrlToHttp(mx, mxcImage, useAuthentication, 460, 215, 'scale', false) + : null; + + return ( + + {bannerUrl && ( + + {title} + + )} + + + + + Announcement + + + {title && ( + + {title} + + )} + {description && ( + + {description} + + )} + + View on Steam + + + + ); +} + +// Steam store app page — OG header + a click-to-play facade that loads Steam's +// official store-widget iframe (live, region-aware price / discount / Buy on +// Steam). Nothing loads from Steam until the user presses "Show price & store". +function SteamAppCard({ + url, + prev, + appId, +}: { + url: string; + prev: IPreviewUrlResponse; + appId: string; +}) { + const mx = useMatrixClient(); + const useAuthentication = useMediaAuthentication(); + const [inlineMediaEmbeds] = useSetting(settingsAtom, 'inlineMediaEmbeds'); + const [showWidget, setShowWidget] = useState(false); + const title = prev['og:title'] ?? ''; + const description = prev['og:description'] ?? ''; + const mxcImage = prev['og:image'] as string | undefined; + const capsuleUrl = mxcImage + ? mxcUrlToHttp(mx, mxcImage, useAuthentication, 460, 215, 'scale', false) + : null; + + return ( + + {capsuleUrl && ( + + {title} + + )} + + + {title && ( + + {title} + + )} + {description && ( + + {description} + + )} + {showWidget ? ( +