import React, { MouseEvent, useEffect, useState } from 'react'; import { Box, Spinner, Text, as } from 'folds'; import * as css from './TikTokEmbed.css'; import { openExternalUrl } from '../../utils/tauri'; /** * Regex patterns to match TikTok URLs */ const TIKTOK_URL_PATTERNS = [ // Standard tiktok.com/@username/video/VIDEO_ID /(?:https?:\/\/)?(?:www\.)?tiktok\.com\/@[\w.-]+\/video\/(\d+)/i, // Short vm.tiktok.com/VIDEO_CODE /(?:https?:\/\/)?(?:www\.)?vm\.tiktok\.com\/([\w]+)/i, // Short vt.tiktok.com/VIDEO_CODE /(?:https?:\/\/)?(?:www\.)?vt\.tiktok\.com\/([\w]+)/i, // Mobile m.tiktok.com /(?:https?:\/\/)?(?:www\.)?m\.tiktok\.com\/v\/(\d+)/i, ]; /** * TikTok oEmbed API base */ const TIKTOK_OEMBED_API = 'https://www.tiktok.com/oembed'; /** * Click handler for external links - uses Tauri shell on desktop/mobile */ const handleExternalLinkClick = (e: MouseEvent) => { const { href } = e.currentTarget; if (href && (href.startsWith('http://') || href.startsWith('https://'))) { e.preventDefault(); openExternalUrl(href); } }; /** * Checks if a URL is a TikTok URL * @param url The URL to check * @returns True if the URL is a TikTok URL */ export function isTikTokUrl(url: string): boolean { return TIKTOK_URL_PATTERNS.some((pattern) => pattern.test(url)); } /** * Fetches TikTok video metadata using oEmbed API * @param url The TikTok video URL * @returns Video metadata including title, author, and thumbnail */ async function fetchTikTokOEmbed(url: string): Promise<{ title: string; authorName: string; authorUrl: string; thumbnailUrl: string; embedHtml: string; } | null> { try { const oembedUrl = `${TIKTOK_OEMBED_API}?url=${encodeURIComponent(url)}`; const response = await fetch(oembedUrl); if (!response.ok) return null; const data = await response.json(); return { title: data.title || 'TikTok Video', authorName: data.author_name || 'Unknown', authorUrl: data.author_url || url, thumbnailUrl: data.thumbnail_url || '', embedHtml: data.html || '', }; } catch { return null; } } type TikTokEmbedState = | { status: 'loading' } | { status: 'loaded'; title: string; authorName: string; authorUrl: string; thumbnailUrl: string; embedHtml: string } | { status: 'error' }; type TikTokEmbedProps = { /** The original TikTok URL */ url: string; }; /** * TikTok embed component that shows video preview with metadata */ export const TikTokEmbed = as<'div', TikTokEmbedProps>(({ url, ...props }, ref) => { const [state, setState] = useState({ status: 'loading' }); const [showEmbed, setShowEmbed] = useState(false); // Fetch video info on mount useEffect(() => { let cancelled = false; fetchTikTokOEmbed(url).then((info) => { if (cancelled) return; if (info) { setState({ status: 'loaded', title: info.title, authorName: info.authorName, authorUrl: info.authorUrl, thumbnailUrl: info.thumbnailUrl, embedHtml: info.embedHtml, }); } else { setState({ status: 'error' }); } }); return () => { cancelled = true; }; }, [url]); // Loading state if (state.status === 'loading') { return (
TikTok - Loading...
); } // Error state - show basic link if (state.status === 'error') { return (
TikTok Video
); } // Loaded state - show thumbnail or embed if (!showEmbed) { return (
{state.title} {state.authorName}
); } // Show embed (using dangerouslySetInnerHTML for TikTok's oEmbed HTML) return (
{state.title}
); });