From 2fbbe6ce1d6eef97d7930f14ded4890b15693500 Mon Sep 17 00:00:00 2001 From: codytseng Date: Mon, 9 Feb 2026 22:30:10 +0800 Subject: [PATCH] fix: allow multiple YouTube embeds to render in a single note Two issues prevented multiple YouTube URLs from rendering: 1. YOUTUBE_URL_REGEX and X_URL_REGEX had the global flag, causing stateful lastIndex to skip matches on alternating calls. 2. Each YouTube Player overwrote window.onYouTubeIframeAPIReady, so only the last mounted player received the callback. Replace with a shared callback queue via ensureYTApi(). Co-Authored-By: Claude Opus 4.6 --- .../YoutubeEmbeddedPlayer/Player.tsx | 40 +++++++++++++------ src/constants.ts | 4 +- 2 files changed, 30 insertions(+), 14 deletions(-) diff --git a/src/components/YoutubeEmbeddedPlayer/Player.tsx b/src/components/YoutubeEmbeddedPlayer/Player.tsx index c1bd2cc..3c6590a 100644 --- a/src/components/YoutubeEmbeddedPlayer/Player.tsx +++ b/src/components/YoutubeEmbeddedPlayer/Player.tsx @@ -4,6 +4,30 @@ import mediaManager from '@/services/media-manager.service' import { YouTubePlayer } from '@/types/youtube' import { memo, useEffect, useRef, useState } from 'react' +let ytApiReady = false +const ytApiCallbacks: (() => void)[] = [] + +function ensureYTApi(callback: () => void) { + if (ytApiReady && window.YT?.Player) { + callback() + return + } + + ytApiCallbacks.push(callback) + + if (!document.querySelector('script[src="https://www.youtube.com/iframe_api"]')) { + const script = document.createElement('script') + script.src = 'https://www.youtube.com/iframe_api' + document.body.appendChild(script) + + window.onYouTubeIframeAPIReady = () => { + ytApiReady = true + ytApiCallbacks.forEach((cb) => cb()) + ytApiCallbacks.length = 0 + } + } +} + interface PlayerProps { videoId: string isShort: boolean @@ -25,19 +49,11 @@ const Player = memo(({ videoId, isShort, className }: PlayerProps) => { if (!videoId || !containerRef.current) return - if (!window.YT) { - const script = document.createElement('script') - script.src = 'https://www.youtube.com/iframe_api' - document.body.appendChild(script) - - window.onYouTubeIframeAPIReady = () => { - if (!unmountedRef.current) { - initPlayer() - } + ensureYTApi(() => { + if (!unmountedRef.current) { + initPlayer() } - } else { - initPlayer() - } + }) let checkMutedInterval: NodeJS.Timeout | null = null function initPlayer() { diff --git a/src/constants.ts b/src/constants.ts index 6a0784e..e4e948e 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -142,9 +142,9 @@ export const LN_INVOICE_REGEX = /(ln(?:bc|tb|bcrt))([0-9]+[munp]?)?1([02-9ac-hj- export const EMOJI_REGEX = /[\u{1F600}-\u{1F64F}]|[\u{1F300}-\u{1F5FF}]|[\u{1F680}-\u{1F6FF}]|[\u{1F1E0}-\u{1F1FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]|[\u{1F900}-\u{1F9FF}]|[\u{1FA70}-\u{1FAFF}]|[\u{1F004}]|[\u{1F0CF}]|[\u{1F18E}]|[\u{3030}]|[\u{2B50}]|[\u{2B55}]|[\u{2934}-\u{2935}]|[\u{2B05}-\u{2B07}]|[\u{2B1B}-\u{2B1C}]|[\u{3297}]|[\u{3299}]|[\u{303D}]|[\u{00A9}]|[\u{00AE}]|[\u{2122}]|[\u{23E9}-\u{23EF}]|[\u{23F0}]|[\u{23F3}]|[\u{FE00}-\u{FE0F}]|[\u{200D}]/gu export const YOUTUBE_URL_REGEX = - /https?:\/\/(?:(?:www|m)\.)?(?:youtube\.com\/(?:watch\?[^#\s]*|embed\/[\w-]+|shorts\/[\w-]+|live\/[\w-]+)|youtu\.be\/[\w-]+)(?:\?[^#\s]*)?(?:#[^\s]*)?/gi + /https?:\/\/(?:(?:www|m)\.)?(?:youtube\.com\/(?:watch\?[^#\s]*|embed\/[\w-]+|shorts\/[\w-]+|live\/[\w-]+)|youtu\.be\/[\w-]+)(?:\?[^#\s]*)?(?:#[^\s]*)?/i export const X_URL_REGEX = - /https?:\/\/(?:www\.)?(twitter\.com|x\.com)\/(?:#!\/)?(\w+)\/status(?:es)?\/(\d+)(?:[?#].*)?/gi + /https?:\/\/(?:www\.)?(twitter\.com|x\.com)\/(?:#!\/)?(\w+)\/status(?:es)?\/(\d+)(?:[?#].*)?/i export const JUMBLE_PUBKEY = 'f4eb8e62add1340b9cadcd9861e669b2e907cea534e0f7f3ac974c11c758a51a' export const CODY_PUBKEY = '8125b911ed0e94dbe3008a0be48cfe5cd0c0b05923cfff917ae7e87da8400883'