Compare commits
2 Commits
b16263b481
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 47979718eb | |||
| 862307ee7d |
@@ -262,7 +262,7 @@ export function MImage({ content, renderImageContent, outlined }: MImageProps) {
|
|||||||
<AttachmentBox
|
<AttachmentBox
|
||||||
style={{
|
style={{
|
||||||
width: toRem(width),
|
width: toRem(width),
|
||||||
height: toRem(height),
|
aspectRatio: `${width} / ${height}`,
|
||||||
['--media-h' as string]: `${height}px`,
|
['--media-h' as string]: `${height}px`,
|
||||||
}}
|
}}
|
||||||
data-paarrot-media-height={height}
|
data-paarrot-media-height={height}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const Attachment = recipe({
|
|||||||
color: color.SurfaceVariant.OnContainer,
|
color: color.SurfaceVariant.OnContainer,
|
||||||
borderRadius: config.radii.R400,
|
borderRadius: config.radii.R400,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
maxWidth: toRem(400),
|
maxWidth: `min(100%, ${toRem(400)})`,
|
||||||
},
|
},
|
||||||
variants: {
|
variants: {
|
||||||
outlined: {
|
outlined: {
|
||||||
@@ -33,7 +33,7 @@ export const AttachmentHeader = style({
|
|||||||
export const AttachmentBox = style([
|
export const AttachmentBox = style([
|
||||||
DefaultReset,
|
DefaultReset,
|
||||||
{
|
{
|
||||||
maxWidth: toRem(400),
|
maxWidth: `min(100%, ${toRem(400)})`,
|
||||||
maxHeight: toRem(400),
|
maxHeight: toRem(400),
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -112,5 +112,7 @@ export const TikTokEmbedContainer = style([
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
backgroundColor: '#000',
|
backgroundColor: '#000',
|
||||||
|
border: 'none',
|
||||||
|
display: 'block',
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -42,6 +42,12 @@ export function isTikTokUrl(url: string): boolean {
|
|||||||
return TIKTOK_URL_PATTERNS.some((pattern) => pattern.test(url));
|
return TIKTOK_URL_PATTERNS.some((pattern) => pattern.test(url));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function extractTikTokVideoId(url: string, embedHtml = ''): string | null {
|
||||||
|
const urlMatch = url.match(/\/video\/(\d+)/i) ?? url.match(/\/v\/(\d+)/i);
|
||||||
|
const embedMatch = embedHtml.match(/data-video-id=["'](\d+)["']/i);
|
||||||
|
return urlMatch?.[1] ?? embedMatch?.[1] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches TikTok video metadata using oEmbed API
|
* Fetches TikTok video metadata using oEmbed API
|
||||||
* @param url The TikTok video URL
|
* @param url The TikTok video URL
|
||||||
@@ -53,6 +59,7 @@ async function fetchTikTokOEmbed(url: string): Promise<{
|
|||||||
authorUrl: string;
|
authorUrl: string;
|
||||||
thumbnailUrl: string;
|
thumbnailUrl: string;
|
||||||
embedHtml: string;
|
embedHtml: string;
|
||||||
|
videoId: string | null;
|
||||||
} | null> {
|
} | null> {
|
||||||
try {
|
try {
|
||||||
const oembedUrl = `${TIKTOK_OEMBED_API}?url=${encodeURIComponent(url)}`;
|
const oembedUrl = `${TIKTOK_OEMBED_API}?url=${encodeURIComponent(url)}`;
|
||||||
@@ -67,6 +74,7 @@ async function fetchTikTokOEmbed(url: string): Promise<{
|
|||||||
authorUrl: data.author_url || url,
|
authorUrl: data.author_url || url,
|
||||||
thumbnailUrl: data.thumbnail_url || '',
|
thumbnailUrl: data.thumbnail_url || '',
|
||||||
embedHtml: data.html || '',
|
embedHtml: data.html || '',
|
||||||
|
videoId: extractTikTokVideoId(url, data.html || ''),
|
||||||
};
|
};
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
@@ -75,7 +83,15 @@ async function fetchTikTokOEmbed(url: string): Promise<{
|
|||||||
|
|
||||||
type TikTokEmbedState =
|
type TikTokEmbedState =
|
||||||
| { status: 'loading' }
|
| { status: 'loading' }
|
||||||
| { status: 'loaded'; title: string; authorName: string; authorUrl: string; thumbnailUrl: string; embedHtml: string }
|
| {
|
||||||
|
status: 'loaded';
|
||||||
|
title: string;
|
||||||
|
authorName: string;
|
||||||
|
authorUrl: string;
|
||||||
|
thumbnailUrl: string;
|
||||||
|
embedHtml: string;
|
||||||
|
videoId: string | null;
|
||||||
|
}
|
||||||
| { status: 'error' };
|
| { status: 'error' };
|
||||||
|
|
||||||
type TikTokEmbedProps = {
|
type TikTokEmbedProps = {
|
||||||
@@ -88,7 +104,7 @@ type TikTokEmbedProps = {
|
|||||||
*/
|
*/
|
||||||
export const TikTokEmbed = as<'div', TikTokEmbedProps>(({ url, ...props }, ref) => {
|
export const TikTokEmbed = as<'div', TikTokEmbedProps>(({ url, ...props }, ref) => {
|
||||||
const [state, setState] = useState<TikTokEmbedState>({ status: 'loading' });
|
const [state, setState] = useState<TikTokEmbedState>({ status: 'loading' });
|
||||||
const [showEmbed, setShowEmbed] = useState(false);
|
const [showPlayer, setShowPlayer] = useState(false);
|
||||||
|
|
||||||
// Fetch video info on mount
|
// Fetch video info on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -105,6 +121,7 @@ export const TikTokEmbed = as<'div', TikTokEmbedProps>(({ url, ...props }, ref)
|
|||||||
authorUrl: info.authorUrl,
|
authorUrl: info.authorUrl,
|
||||||
thumbnailUrl: info.thumbnailUrl,
|
thumbnailUrl: info.thumbnailUrl,
|
||||||
embedHtml: info.embedHtml,
|
embedHtml: info.embedHtml,
|
||||||
|
videoId: info.videoId,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setState({ status: 'error' });
|
setState({ status: 'error' });
|
||||||
@@ -169,7 +186,7 @@ export const TikTokEmbed = as<'div', TikTokEmbedProps>(({ url, ...props }, ref)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Loaded state - show thumbnail or embed
|
// Loaded state - show thumbnail or embed
|
||||||
if (!showEmbed) {
|
if (!showPlayer) {
|
||||||
return (
|
return (
|
||||||
<Box shrink="No" className={css.TikTokEmbed} direction="Column" {...props} ref={ref}>
|
<Box shrink="No" className={css.TikTokEmbed} direction="Column" {...props} ref={ref}>
|
||||||
<div className={css.TikTokEmbedHeader}>
|
<div className={css.TikTokEmbedHeader}>
|
||||||
@@ -203,23 +220,27 @@ export const TikTokEmbed = as<'div', TikTokEmbedProps>(({ url, ...props }, ref)
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={css.TikTokThumbnailButton}
|
className={css.TikTokThumbnailButton}
|
||||||
onClick={() => setShowEmbed(true)}
|
onClick={() => setShowPlayer(true)}
|
||||||
aria-label="Load TikTok video"
|
aria-label="Play TikTok video"
|
||||||
>
|
>
|
||||||
{state.thumbnailUrl && (
|
{state.thumbnailUrl ? (
|
||||||
<img
|
<img
|
||||||
src={state.thumbnailUrl}
|
src={state.thumbnailUrl}
|
||||||
alt={state.title}
|
alt={state.title}
|
||||||
className={css.TikTokThumbnail}
|
className={css.TikTokThumbnail}
|
||||||
crossOrigin="anonymous"
|
crossOrigin="anonymous"
|
||||||
/>
|
/>
|
||||||
)}
|
) : null}
|
||||||
</button>
|
</button>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show embed (using dangerouslySetInnerHTML for TikTok's oEmbed HTML)
|
const playerUrl = state.videoId
|
||||||
|
? `https://www.tiktok.com/player/v1/${state.videoId}?autoplay=1`
|
||||||
|
: null;
|
||||||
|
|
||||||
|
// Use TikTok's documented player instead of injecting the oEmbed script.
|
||||||
return (
|
return (
|
||||||
<Box shrink="No" className={css.TikTokEmbed} direction="Column" {...props} ref={ref}>
|
<Box shrink="No" className={css.TikTokEmbed} direction="Column" {...props} ref={ref}>
|
||||||
<div className={css.TikTokEmbedHeader}>
|
<div className={css.TikTokEmbedHeader}>
|
||||||
@@ -236,10 +257,20 @@ export const TikTokEmbed = as<'div', TikTokEmbedProps>(({ url, ...props }, ref)
|
|||||||
{state.title}
|
{state.title}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
|
{playerUrl ? (
|
||||||
|
<iframe
|
||||||
|
className={css.TikTokEmbedContainer}
|
||||||
|
src={playerUrl}
|
||||||
|
title="TikTok video player"
|
||||||
|
allow="autoplay; encrypted-media; fullscreen; picture-in-picture"
|
||||||
|
allowFullScreen
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
<div
|
<div
|
||||||
className={css.TikTokEmbedContainer}
|
className={css.TikTokEmbedContainer}
|
||||||
dangerouslySetInnerHTML={{ __html: state.embedHtml }}
|
dangerouslySetInnerHTML={{ __html: state.embedHtml }}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user