diff --git a/src/components/Note/MarkdownArticle/MarkdownArticle.tsx b/src/components/Note/MarkdownArticle/MarkdownArticle.tsx index d213009..7cccddb 100644 --- a/src/components/Note/MarkdownArticle/MarkdownArticle.tsx +++ b/src/components/Note/MarkdownArticle/MarkdownArticle.tsx @@ -3,13 +3,14 @@ import Image from '@/components/Image' import MediaPlayer from '@/components/MediaPlayer' import Wikilink from '@/components/UniversalContent/Wikilink' import WebPreview from '@/components/WebPreview' +import YoutubeEmbeddedPlayer from '@/components/YoutubeEmbeddedPlayer' import { getLongFormArticleMetadataFromEvent } from '@/lib/event-metadata' import { toNoteList } from '@/lib/link' import { useMediaExtraction } from '@/hooks' import { cleanUrl, isImage, isMedia, isVideo, isAudio, isWebsocketUrl } from '@/lib/url' import { getImetaInfosFromEvent } from '@/lib/event' import { Event, kinds } from 'nostr-tools' -import { ExtendedKind, WS_URL_REGEX } from '@/constants' +import { ExtendedKind, WS_URL_REGEX, YOUTUBE_URL_REGEX } from '@/constants' import React, { useMemo, useState, useCallback } from 'react' import { createPortal } from 'react-dom' import Lightbox from 'yet-another-react-lightbox' @@ -27,6 +28,17 @@ function truncateLinkText(text: string, maxLength: number = 200): string { return text.substring(0, maxLength) + '...' } +/** + * Check if a URL is a YouTube URL + */ +function isYouTubeUrl(url: string): boolean { + // Create a new regex instance to avoid state issues with global regex + // Keep the 'i' flag for case-insensitivity but remove 'g' to avoid state issues + const flags = YOUTUBE_URL_REGEX.flags.replace('g', '') + const regex = new RegExp(YOUTUBE_URL_REGEX.source, flags) + return regex.test(url) +} + /** * Parse markdown content and render with post-processing for nostr: links and hashtags * Post-processes: @@ -86,14 +98,37 @@ function parseMarkdownContent( } }) + // YouTube URLs - not in markdown links + const youtubeUrlMatches = Array.from(content.matchAll(YOUTUBE_URL_REGEX)) + youtubeUrlMatches.forEach(match => { + if (match.index !== undefined) { + const url = match[0] + // Only add if not already covered by a markdown link/image + const isInMarkdown = patterns.some(p => + (p.type === 'markdown-link' || p.type === 'markdown-image') && + match.index! >= p.index && + match.index! < p.end + ) + // Only process if not in markdown link + if (!isInMarkdown && isYouTubeUrl(url)) { + patterns.push({ + index: match.index, + end: match.index + match[0].length, + type: 'youtube-url', + data: { url } + }) + } + } + }) + // Relay URLs (wss:// or ws://) - not in markdown links const relayUrlMatches = Array.from(content.matchAll(WS_URL_REGEX)) relayUrlMatches.forEach(match => { if (match.index !== undefined) { const url = match[0] - // Only add if not already covered by a markdown link/image + // Only add if not already covered by a markdown link/image or YouTube URL const isInMarkdown = patterns.some(p => - (p.type === 'markdown-link' || p.type === 'markdown-image') && + (p.type === 'markdown-link' || p.type === 'markdown-image' || p.type === 'youtube-url') && match.index! >= p.index && match.index! < p.end ) @@ -109,14 +144,14 @@ function parseMarkdownContent( } }) - // Nostr addresses (nostr:npub1..., nostr:note1..., etc.) - not in markdown links or relay URLs + // Nostr addresses (nostr:npub1..., nostr:note1..., etc.) - not in markdown links, relay URLs, or YouTube URLs const nostrRegex = /nostr:(npub1[a-z0-9]{58}|nprofile1[a-z0-9]+|note1[a-z0-9]{58}|nevent1[a-z0-9]+|naddr1[a-z0-9]+)/g const nostrMatches = Array.from(content.matchAll(nostrRegex)) nostrMatches.forEach(match => { if (match.index !== undefined) { - // Only add if not already covered by a markdown link/image or relay URL + // Only add if not already covered by a markdown link/image, relay URL, or YouTube URL const isInOther = patterns.some(p => - (p.type === 'markdown-link' || p.type === 'markdown-image' || p.type === 'relay-url') && + (p.type === 'markdown-link' || p.type === 'markdown-image' || p.type === 'relay-url' || p.type === 'youtube-url') && match.index! >= p.index && match.index! < p.end ) @@ -479,6 +514,17 @@ function parseMarkdownContent( {displayText} ) + } else if (isYouTubeUrl(url)) { + // Render YouTube URL as embedded player + parts.push( +