diff --git a/src/app/components/url-preview/UrlPreviewCard.tsx b/src/app/components/url-preview/UrlPreviewCard.tsx index d899ddf13..7f526e6b9 100644 --- a/src/app/components/url-preview/UrlPreviewCard.tsx +++ b/src/app/components/url-preview/UrlPreviewCard.tsx @@ -19,7 +19,16 @@ import { ImageViewer } from '../image-viewer'; import { onEnterOrSpace } from '../../utils/keyboard'; import { useSetting } from '../../state/hooks/settings'; import { settingsAtom } from '../../state/settings'; -import { getTweetId, MediaEmbed, parseMediaEmbed } from '../../utils/videoEmbed'; +import { + getTikTokVideoId, + getTweetId, + isTikTokLink, + MediaEmbed, + parseMediaEmbed, + tiktokIdFromOembed, + tiktokOembedUrl, + tiktokPlayerEmbedUrl, +} from '../../utils/videoEmbed'; const linkStyles = { color: color.Success.Main }; @@ -1150,6 +1159,136 @@ function MediaEmbedCard({ ); } +// TikTok needs its own card: short "copy-link" URLs (vm.tiktok.com, tiktok.com/t/…) +// don't carry the video id, and the homeserver's link preview is bot-walled. So we +// resolve the id client-side via TikTok's CORS-enabled oEmbed API (on click, to +// keep the facade privacy model), then play the player/v1 embed. +function TikTokEmbedCard({ url, prev }: { url: string; prev: IPreviewUrlResponse }) { + const mx = useMatrixClient(); + const useAuthentication = useMediaAuthentication(); + const [inlineMediaEmbeds] = useSetting(settingsAtom, 'inlineMediaEmbeds'); + const [videoId, setVideoId] = useState(() => getTikTokVideoId(url)); + const [playing, setPlaying] = useState(false); + const [resolving, setResolving] = useState(false); + const [failed, setFailed] = useState(false); + const mediaRef = useRef(null); + + const mxcImage = prev['og:image'] as string | undefined; + const rawTitle = prev['og:title'] ?? ''; + const title = looksLikeBotWall(rawTitle) ? '' : rawTitle; + const thumbnailUrl = mxcImage + ? mxcUrlToHttp(mx, mxcImage, useAuthentication, 320, 568, 'scale', false) + : undefined; + + const start = useCallback(async () => { + if (videoId) { + setPlaying(true); + return; + } + setResolving(true); + setFailed(false); + try { + const res = await fetch(tiktokOembedUrl(url)); + const id = tiktokIdFromOembed(await res.json()); + if (id) { + setVideoId(id); + setPlaying(true); + } else { + setFailed(true); + } + } catch { + setFailed(true); + } finally { + setResolving(false); + } + }, [url, videoId]); + + const enterFullscreen = () => mediaRef.current?.requestFullscreen?.().catch(() => undefined); + + const facadeInner = ( + <> + {thumbnailUrl ? ( + {title} + ) : ( + + )} + + + {resolving ? : } + + + + ); + + return ( + +
+ {playing && videoId ? ( +