feat(embeds): fullscreen, wider cards, Instagram/Tidal/Reddit; fix TikTok/X/Twitch
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>
This commit is contained in:
@@ -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([
|
export const UrlPreviewImg = style([
|
||||||
DefaultReset,
|
DefaultReset,
|
||||||
{
|
{
|
||||||
@@ -209,6 +216,9 @@ export const EmbedMediaLandscape = style([
|
|||||||
aspectRatio: '16 / 9',
|
aspectRatio: '16 / 9',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
backgroundColor: color.Surface.Container,
|
backgroundColor: color.Surface.Container,
|
||||||
|
selectors: {
|
||||||
|
'&:fullscreen': { width: '100vw', height: '100vh', aspectRatio: 'auto' },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -216,12 +226,15 @@ export const EmbedMediaPortrait = style([
|
|||||||
DefaultReset,
|
DefaultReset,
|
||||||
{
|
{
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
width: toRem(220),
|
width: toRem(300),
|
||||||
maxWidth: '100%',
|
maxWidth: '100%',
|
||||||
aspectRatio: '9 / 16',
|
aspectRatio: '9 / 16',
|
||||||
margin: '0 auto',
|
margin: '0 auto',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
backgroundColor: color.Surface.Container,
|
backgroundColor: color.Surface.Container,
|
||||||
|
selectors: {
|
||||||
|
'&:fullscreen': { width: '100vw', height: '100vh', aspectRatio: 'auto', margin: 0 },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -430,6 +443,16 @@ export const BadgeAppleMusic = style({
|
|||||||
color: '#ffffff',
|
color: '#ffffff',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const BadgeInstagram = style({
|
||||||
|
backgroundColor: '#e1306c',
|
||||||
|
color: '#ffffff',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const BadgeTidal = style({
|
||||||
|
backgroundColor: '#000000',
|
||||||
|
color: '#ffffff',
|
||||||
|
});
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Twitch LIVE badge
|
// Twitch LIVE badge
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -991,8 +991,18 @@ const EMBED_BADGE: Record<string, { label: string; class: string }> = {
|
|||||||
spotify: { label: 'Spotify', class: previewCss.BadgeSpotify },
|
spotify: { label: 'Spotify', class: previewCss.BadgeSpotify },
|
||||||
soundcloud: { label: 'SoundCloud', class: previewCss.BadgeSoundCloud },
|
soundcloud: { label: 'SoundCloud', class: previewCss.BadgeSoundCloud },
|
||||||
applemusic: { label: 'Apple Music', class: previewCss.BadgeAppleMusic },
|
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).
|
// One media-forward tile for every embeddable provider (video / portrait / audio).
|
||||||
// Privacy-friendly facade: shows the homeserver's cached thumbnail + a play
|
// 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
|
// button; only on click does it swap in the provider iframe, so nothing loads
|
||||||
@@ -1011,12 +1021,17 @@ function MediaEmbedCard({
|
|||||||
const useAuthentication = useMediaAuthentication();
|
const useAuthentication = useMediaAuthentication();
|
||||||
const [inlineMediaEmbeds] = useSetting(settingsAtom, 'inlineMediaEmbeds');
|
const [inlineMediaEmbeds] = useSetting(settingsAtom, 'inlineMediaEmbeds');
|
||||||
const [playing, setPlaying] = useState(false);
|
const [playing, setPlaying] = useState(false);
|
||||||
|
const mediaRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const title = prev['og:title'] ?? '';
|
const rawTitle = prev['og:title'] ?? '';
|
||||||
const description = prev['og:description'] ?? '';
|
const rawDescription = prev['og:description'] ?? '';
|
||||||
const mxcImage = prev['og:image'] as string | undefined;
|
|
||||||
const portrait = embed.kind === 'portrait';
|
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
|
const thumbnailUrl = mxcImage
|
||||||
? mxcUrlToHttp(
|
? mxcUrlToHttp(
|
||||||
mx,
|
mx,
|
||||||
@@ -1035,12 +1050,12 @@ function MediaEmbedCard({
|
|||||||
? { label: 'Shorts', class: previewCss.BadgeYouTubeShorts }
|
? { label: 'Shorts', class: previewCss.BadgeYouTubeShorts }
|
||||||
: (EMBED_BADGE[embed.provider] ?? { label: embed.provider, class: '' });
|
: (EMBED_BADGE[embed.provider] ?? { label: embed.provider, class: '' });
|
||||||
|
|
||||||
const mediaClass = audio
|
const mediaClass = fixedHeight
|
||||||
? previewCss.EmbedMediaAudio
|
? previewCss.EmbedMediaAudio
|
||||||
: portrait
|
: portrait
|
||||||
? previewCss.EmbedMediaPortrait
|
? previewCss.EmbedMediaPortrait
|
||||||
: previewCss.EmbedMediaLandscape;
|
: previewCss.EmbedMediaLandscape;
|
||||||
const mediaStyle = audio ? { height: `${embed.height ?? 152}px` } : undefined;
|
const mediaStyle = fixedHeight ? { height: `${embed.height ?? 152}px` } : undefined;
|
||||||
|
|
||||||
const thumb = thumbnailUrl ? (
|
const thumb = thumbnailUrl ? (
|
||||||
<img className={previewCss.MediaThumbnailImg} src={thumbnailUrl} alt={title} loading="lazy" />
|
<img className={previewCss.MediaThumbnailImg} src={thumbnailUrl} alt={title} loading="lazy" />
|
||||||
@@ -1057,16 +1072,21 @@ function MediaEmbedCard({
|
|||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const enterFullscreen = () => {
|
||||||
|
mediaRef.current?.requestFullscreen?.().catch(() => undefined);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box direction="Column">
|
<Box direction="Column">
|
||||||
<div className={mediaClass} style={mediaStyle}>
|
<div ref={mediaRef} className={mediaClass} style={mediaStyle}>
|
||||||
{playing ? (
|
{playing ? (
|
||||||
|
// No sandbox: these are CSP-allowlisted trusted hosts, and sandboxing
|
||||||
|
// breaks player features (fullscreen, TikTok's flow, etc.).
|
||||||
<iframe
|
<iframe
|
||||||
className={previewCss.EmbedIframe}
|
className={previewCss.EmbedIframe}
|
||||||
src={embed.embedUrl}
|
src={embed.embedUrl}
|
||||||
title={title || badge.label}
|
title={title || badge.label}
|
||||||
allow="autoplay; encrypted-media; picture-in-picture; fullscreen; clipboard-write"
|
allow="autoplay; encrypted-media; picture-in-picture; fullscreen; clipboard-write"
|
||||||
sandbox="allow-scripts allow-same-origin allow-popups allow-presentation"
|
|
||||||
allowFullScreen
|
allowFullScreen
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
@@ -1094,18 +1114,25 @@ function MediaEmbedCard({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<UrlPreviewContent>
|
<UrlPreviewContent>
|
||||||
<Text
|
<Box alignItems="Center" gap="200" justifyContent="SpaceBetween">
|
||||||
style={linkStyles}
|
<Text
|
||||||
truncate
|
style={linkStyles}
|
||||||
as="a"
|
truncate
|
||||||
href={url}
|
as="a"
|
||||||
target="_blank"
|
href={url}
|
||||||
rel="noreferrer"
|
target="_blank"
|
||||||
size="T200"
|
rel="noreferrer"
|
||||||
priority="300"
|
size="T200"
|
||||||
>
|
priority="300"
|
||||||
<SiteBadge label={badge.label} colorClass={badge.class} />
|
>
|
||||||
</Text>
|
<SiteBadge label={badge.label} colorClass={badge.class} />
|
||||||
|
</Text>
|
||||||
|
{playing && video && (
|
||||||
|
<Chip variant="Secondary" radii="Pill" onClick={enterFullscreen} aria-label="Fullscreen">
|
||||||
|
<Text size="T200">⛶ Fullscreen</Text>
|
||||||
|
</Chip>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
{title && (
|
{title && (
|
||||||
<Text truncate priority="400">
|
<Text truncate priority="400">
|
||||||
<b>{title}</b>
|
<b>{title}</b>
|
||||||
@@ -1726,10 +1753,15 @@ export const UrlPreviewCard = as<'div', { url: string; ts: number }>(
|
|||||||
|
|
||||||
if (previewStatus.status === AsyncStatus.Error) return null;
|
if (previewStatus.status === AsyncStatus.Error) return null;
|
||||||
|
|
||||||
|
// Interactive embeds (players, tweets) render in a wider, responsive card so
|
||||||
|
// player chrome / tweet content isn't cramped or clipped.
|
||||||
|
const embed = parseMediaEmbed(url, window.location.hostname);
|
||||||
|
const wide = !!embed || isTwitterTweet(url);
|
||||||
|
const cardClass = wide ? previewCss.UrlPreviewWide : undefined;
|
||||||
|
|
||||||
const renderContent = (prev: IPreviewUrlResponse): React.ReactNode => {
|
const renderContent = (prev: IPreviewUrlResponse): React.ReactNode => {
|
||||||
// Embeddable media (YouTube/Vimeo/TikTok/Dailymotion/Streamable/Twitch/
|
// Embeddable media (YouTube/Vimeo/TikTok/Dailymotion/Streamable/Twitch/
|
||||||
// Spotify/SoundCloud) gets the media-forward click-to-play tile.
|
// Spotify/SoundCloud/Apple Music/Tidal/Instagram/Reddit) → click-to-play tile.
|
||||||
const embed = parseMediaEmbed(url, window.location.hostname);
|
|
||||||
if (embed) {
|
if (embed) {
|
||||||
return <MediaEmbedCard url={url} prev={prev} embed={embed} />;
|
return <MediaEmbedCard url={url} prev={prev} embed={embed} />;
|
||||||
}
|
}
|
||||||
@@ -1820,14 +1852,14 @@ export const UrlPreviewCard = as<'div', { url: string; ts: number }>(
|
|||||||
const content = renderContent(previewStatus.data);
|
const content = renderContent(previewStatus.data);
|
||||||
if (content === null) return null;
|
if (content === null) return null;
|
||||||
return (
|
return (
|
||||||
<UrlPreview {...props} ref={ref}>
|
<UrlPreview {...props} ref={ref} className={cardClass}>
|
||||||
{content}
|
{content}
|
||||||
</UrlPreview>
|
</UrlPreview>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<UrlPreview {...props} ref={ref}>
|
<UrlPreview {...props} ref={ref} className={cardClass}>
|
||||||
<Box grow="Yes" alignItems="Center" justifyContent="Center">
|
<Box grow="Yes" alignItems="Center" justifyContent="Center">
|
||||||
<Spinner variant="Secondary" size="400" />
|
<Spinner variant="Secondary" size="400" />
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ import {
|
|||||||
isSoundCloudTrack,
|
isSoundCloudTrack,
|
||||||
getAppleMusicEmbed,
|
getAppleMusicEmbed,
|
||||||
getTweetId,
|
getTweetId,
|
||||||
|
getTidalEmbed,
|
||||||
|
getInstagramEmbed,
|
||||||
|
getRedditPostEmbed,
|
||||||
buildVideoEmbedUrl,
|
buildVideoEmbedUrl,
|
||||||
spotifyEmbedHeight,
|
spotifyEmbedHeight,
|
||||||
parseMediaEmbed,
|
parseMediaEmbed,
|
||||||
@@ -136,6 +139,39 @@ test('parseMediaEmbed: Apple Music → audio', () => {
|
|||||||
assert.equal(m?.height, 175);
|
assert.equal(m?.height, 175);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Tidal: track (audio) vs video (landscape)', () => {
|
||||||
|
assert.deepEqual(getTidalEmbed('https://tidal.com/browse/track/12345'), {
|
||||||
|
kind: 'audio',
|
||||||
|
embedUrl: 'https://embed.tidal.com/tracks/12345',
|
||||||
|
height: 120,
|
||||||
|
});
|
||||||
|
assert.equal(getTidalEmbed('https://listen.tidal.com/album/999')?.embedUrl, 'https://embed.tidal.com/albums/999');
|
||||||
|
assert.equal(getTidalEmbed('https://tidal.com/video/555')?.kind, 'landscape');
|
||||||
|
assert.equal(getTidalEmbed('https://tidal.com/browse'), null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Instagram: p / reel / tv → embed path', () => {
|
||||||
|
assert.equal(getInstagramEmbed('https://www.instagram.com/p/AbC123_-/'), 'https://www.instagram.com/p/AbC123_-/embed/');
|
||||||
|
assert.equal(getInstagramEmbed('https://instagram.com/reel/XyZ/'), 'https://www.instagram.com/reel/XyZ/embed/');
|
||||||
|
assert.equal(getInstagramEmbed('https://www.instagram.com/reels/XyZ/'), 'https://www.instagram.com/reel/XyZ/embed/');
|
||||||
|
assert.equal(getInstagramEmbed('https://www.instagram.com/someuser/'), null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Reddit post embed → redditmedia', () => {
|
||||||
|
assert.equal(
|
||||||
|
getRedditPostEmbed('https://www.reddit.com/r/aww/comments/abc123/cute_cat/'),
|
||||||
|
'https://www.redditmedia.com/r/aww/comments/abc123/?ref_source=embed&ref=share&embed=true&theme=dark',
|
||||||
|
);
|
||||||
|
assert.equal(getRedditPostEmbed('https://old.reddit.com/r/aww/comments/xyz/'), 'https://www.redditmedia.com/r/aww/comments/xyz/?ref_source=embed&ref=share&embed=true&theme=dark');
|
||||||
|
assert.equal(getRedditPostEmbed('https://www.reddit.com/r/aww/'), null); // subreddit, not a post
|
||||||
|
});
|
||||||
|
|
||||||
|
test('parseMediaEmbed: Instagram/Reddit → rich, Tidal → audio', () => {
|
||||||
|
assert.equal(parseMediaEmbed('https://www.instagram.com/p/abc/', 'h')?.kind, 'rich');
|
||||||
|
assert.equal(parseMediaEmbed('https://www.reddit.com/r/x/comments/y/z/', 'h')?.provider, 'reddit');
|
||||||
|
assert.equal(parseMediaEmbed('https://tidal.com/browse/track/1', 'h')?.provider, 'tidal');
|
||||||
|
});
|
||||||
|
|
||||||
test('parseMediaEmbed: Twitch embed carries the parent host', () => {
|
test('parseMediaEmbed: Twitch embed carries the parent host', () => {
|
||||||
const chan = parseMediaEmbed('https://twitch.tv/streamer', HOST);
|
const chan = parseMediaEmbed('https://twitch.tv/streamer', HOST);
|
||||||
assert.equal(chan?.provider, 'twitch');
|
assert.equal(chan?.provider, 'twitch');
|
||||||
|
|||||||
@@ -3,9 +3,10 @@
|
|||||||
//
|
//
|
||||||
// "kind" drives how the card lays the embed out:
|
// "kind" drives how the card lays the embed out:
|
||||||
// landscape → 16:9 video portrait → 9:16 video
|
// landscape → 16:9 video portrait → 9:16 video
|
||||||
// audio → short fixed-height player (Spotify/SoundCloud)
|
// audio → short fixed-height player (Spotify/SoundCloud/Tidal/Apple Music)
|
||||||
|
// rich → tall fixed-height post embed (Instagram / Reddit)
|
||||||
|
|
||||||
export type EmbedKind = 'landscape' | 'portrait' | 'audio';
|
export type EmbedKind = 'landscape' | 'portrait' | 'audio' | 'rich';
|
||||||
|
|
||||||
export type MediaEmbed = {
|
export type MediaEmbed = {
|
||||||
provider: string;
|
provider: string;
|
||||||
@@ -201,6 +202,62 @@ export function getTweetId(url: string): string | null {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Tidal ----------------------------------------------------------------
|
||||||
|
|
||||||
|
export function getTidalEmbed(
|
||||||
|
url: string,
|
||||||
|
): { kind: 'audio' | 'landscape'; embedUrl: string; height?: number } | null {
|
||||||
|
try {
|
||||||
|
const u = new URL(url);
|
||||||
|
const h = u.hostname.replace(/^(www|listen|desktop)\./, '');
|
||||||
|
if (h !== 'tidal.com') return null;
|
||||||
|
const p = u.pathname.replace(/^\/browse/, '');
|
||||||
|
let m = p.match(/^\/track\/(\d+)/);
|
||||||
|
if (m) return { kind: 'audio', embedUrl: `https://embed.tidal.com/tracks/${m[1]}`, height: 120 };
|
||||||
|
m = p.match(/^\/album\/(\d+)/);
|
||||||
|
if (m) return { kind: 'audio', embedUrl: `https://embed.tidal.com/albums/${m[1]}`, height: 400 };
|
||||||
|
m = p.match(/^\/playlist\/([0-9a-fA-F-]+)/);
|
||||||
|
if (m)
|
||||||
|
return { kind: 'audio', embedUrl: `https://embed.tidal.com/playlists/${m[1]}`, height: 400 };
|
||||||
|
m = p.match(/^\/video\/(\d+)/);
|
||||||
|
if (m) return { kind: 'landscape', embedUrl: `https://embed.tidal.com/videos/${m[1]}` };
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Instagram ------------------------------------------------------------
|
||||||
|
|
||||||
|
export function getInstagramEmbed(url: string): string | null {
|
||||||
|
try {
|
||||||
|
const u = new URL(url);
|
||||||
|
if (u.hostname !== 'instagram.com' && u.hostname !== 'www.instagram.com') return null;
|
||||||
|
const m = u.pathname.match(/^\/(p|reel|reels|tv)\/([A-Za-z0-9_-]+)/);
|
||||||
|
if (!m) return null;
|
||||||
|
const type = m[1] === 'reels' ? 'reel' : m[1];
|
||||||
|
return `https://www.instagram.com/${type}/${m[2]}/embed/`;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Reddit (post embed via redditmedia — bypasses the homeserver's blocked
|
||||||
|
// preview fetch, which Reddit serves a bot-check "please wait" page to) ---
|
||||||
|
|
||||||
|
export function getRedditPostEmbed(url: string): string | null {
|
||||||
|
try {
|
||||||
|
const u = new URL(url);
|
||||||
|
const h = u.hostname.replace(/^(www|old|new|np|i)\./, '');
|
||||||
|
if (h !== 'reddit.com') return null;
|
||||||
|
const m = u.pathname.match(/^\/r\/([A-Za-z0-9_]+)\/comments\/([A-Za-z0-9]+)/);
|
||||||
|
if (!m) return null;
|
||||||
|
return `https://www.redditmedia.com/r/${m[1]}/comments/${m[2]}/?ref_source=embed&ref=share&embed=true&theme=dark`;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- Embed-URL builders ---------------------------------------------------
|
// --- Embed-URL builders ---------------------------------------------------
|
||||||
|
|
||||||
const enc = encodeURIComponent;
|
const enc = encodeURIComponent;
|
||||||
@@ -294,5 +351,15 @@ export function parseMediaEmbed(url: string, host: string): MediaEmbed | null {
|
|||||||
height: apple.height,
|
height: apple.height,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const tidal = getTidalEmbed(url);
|
||||||
|
if (tidal)
|
||||||
|
return { provider: 'tidal', kind: tidal.kind, embedUrl: tidal.embedUrl, height: tidal.height };
|
||||||
|
|
||||||
|
const insta = getInstagramEmbed(url);
|
||||||
|
if (insta) return { provider: 'instagram', kind: 'rich', embedUrl: insta, height: 720 };
|
||||||
|
|
||||||
|
const reddit = getRedditPostEmbed(url);
|
||||||
|
if (reddit) return { provider: 'reddit', kind: 'rich', embedUrl: reddit, height: 480 };
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user