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 <noreply@anthropic.com>
This commit is contained in:
codytseng 2026-02-09 22:30:10 +08:00
parent 2b97adfdca
commit 2fbbe6ce1d
2 changed files with 30 additions and 14 deletions

View file

@ -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() {

View file

@ -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'