diff --git a/src/components/Content/index.tsx b/src/components/Content/index.tsx
index 6a5caa1e..cad523b2 100644
--- a/src/components/Content/index.tsx
+++ b/src/components/Content/index.tsx
@@ -26,7 +26,7 @@ import ImageGallery from '../ImageGallery'
import MediaPlayer from '../MediaPlayer'
import SpotifyEmbeddedPlayer from '../SpotifyEmbeddedPlayer'
import YoutubeEmbeddedPlayer from '../YoutubeEmbeddedPlayer'
-import ZapStreamEmbeddedPlayer from '../ZapStreamEmbeddedPlayer'
+import ZapStreamLiveEventEmbed from '../ZapStreamLiveEventEmbed'
import WebPreview from '../WebPreview'
import { toNote } from '@/lib/link'
import { YOUTUBE_URL_REGEX } from '@/constants'
@@ -459,11 +459,12 @@ export default function Content({
))}
{zapstreamUrlsFromTags.map((url) => (
-
))}
@@ -617,11 +618,12 @@ export default function Content({
}
if (node.type === 'zapstream') {
return (
-
)
}
diff --git a/src/components/Embedded/HttpNostrAwareUrl.tsx b/src/components/Embedded/HttpNostrAwareUrl.tsx
index 25eb0381..095f77e4 100644
--- a/src/components/Embedded/HttpNostrAwareUrl.tsx
+++ b/src/components/Embedded/HttpNostrAwareUrl.tsx
@@ -14,6 +14,10 @@ import { EmbeddedMention } from './EmbeddedMention'
import { EmbeddedNormalUrl } from './EmbeddedNormalUrl'
import { EmbeddedNote } from './EmbeddedNote'
import WebPreview from '@/components/WebPreview'
+import YoutubeEmbeddedPlayer from '@/components/YoutubeEmbeddedPlayer'
+import ZapStreamLiveEventEmbed from '@/components/ZapStreamLiveEventEmbed'
+import { isEmbeddableYoutubeUrl } from '@/lib/youtube-url'
+import { isZapStreamWatchUrl } from '@/lib/zap-stream-url'
type RenderMode = 'note-content' | 'article'
@@ -41,6 +45,27 @@ export function HttpNostrAwareUrl({
const cleaned = cleanUrl(url) || url
+ if (isZapStreamWatchUrl(cleaned)) {
+ return (
+
+ )
+ }
+
+ if (isEmbeddableYoutubeUrl(cleaned)) {
+ return (
+
+ )
+ }
+
if (sameOriginTarget) {
if (sameOriginTarget.kind === 'event') {
return (
diff --git a/src/components/Note/MarkdownArticle/MarkdownArticle.tsx b/src/components/Note/MarkdownArticle/MarkdownArticle.tsx
index dbee909a..65b3bc93 100644
--- a/src/components/Note/MarkdownArticle/MarkdownArticle.tsx
+++ b/src/components/Note/MarkdownArticle/MarkdownArticle.tsx
@@ -5,7 +5,7 @@ import Wikilink from '@/components/UniversalContent/Wikilink'
import { BookstrContent } from '@/components/Bookstr'
import WebPreview from '@/components/WebPreview'
import SpotifyEmbeddedPlayer from '@/components/SpotifyEmbeddedPlayer'
-import ZapStreamEmbeddedPlayer from '@/components/ZapStreamEmbeddedPlayer'
+import ZapStreamLiveEventEmbed from '@/components/ZapStreamLiveEventEmbed'
import YoutubeEmbeddedPlayer from '@/components/YoutubeEmbeddedPlayer'
import { getLongFormArticleMetadataFromEvent } from '@/lib/event-metadata'
import { toNoteList } from '@/lib/link'
@@ -2239,7 +2239,12 @@ function parseMarkdownContentLegacy(
const { url } = pattern.data
parts.push(
-
+
)
} else if (pattern.type === 'relay-url') {
@@ -3417,7 +3422,12 @@ function parseMarkdownContentMarked(
if (isZapStreamUrl(cleaned)) {
return (
-
+
)
}
@@ -3577,7 +3587,12 @@ function parseMarkdownContentMarked(
if (isZapStreamUrl(cleaned)) {
return (
-
+
)
}
@@ -5379,7 +5394,12 @@ export default function MarkdownArticle({
{leftoverTagZapStreamUrls.map((url) => (
-
+
))}
diff --git a/src/components/ZapStreamEmbeddedPlayer/index.tsx b/src/components/ZapStreamEmbeddedPlayer/index.tsx
deleted file mode 100644
index ac9de1ab..00000000
--- a/src/components/ZapStreamEmbeddedPlayer/index.tsx
+++ /dev/null
@@ -1,56 +0,0 @@
-import { canonicalZapStreamWatchUrl } from '@/lib/zap-stream-url'
-import { cn } from '@/lib/utils'
-import { useContentPolicyOptional } from '@/providers/ContentPolicyProvider'
-import { useLayoutEffect, useMemo, useState } from 'react'
-import ExternalLink from '../ExternalLink'
-import LazyMediaTapPlaceholder from '../MediaPlayer/LazyMediaTapPlaceholder'
-
-export default function ZapStreamEmbeddedPlayer({
- url,
- className,
- mustLoad = false
-}: {
- url: string
- className?: string
- mustLoad?: boolean
-}) {
- const contentPolicy = useContentPolicyOptional()
- const autoLoadMedia = contentPolicy?.autoLoadMedia ?? true
- const [userClickedLoad, setUserClickedLoad] = useState(false)
- const embedSrc = useMemo(() => canonicalZapStreamWatchUrl(url), [url])
- const showEmbed = mustLoad || autoLoadMedia || userClickedLoad
-
- useLayoutEffect(() => {
- if (!autoLoadMedia) setUserClickedLoad(false)
- }, [autoLoadMedia])
-
- if (!embedSrc) {
- return
- }
-
- if (!mustLoad && !showEmbed) {
- return (
- setUserClickedLoad(true)}
- className={cn('aspect-video max-h-[min(70vh,28rem)]', className)}
- />
- )
- }
-
- return (
-
- )
-}
diff --git a/src/components/ZapStreamLiveEventEmbed/index.tsx b/src/components/ZapStreamLiveEventEmbed/index.tsx
new file mode 100644
index 00000000..ced00ba6
--- /dev/null
+++ b/src/components/ZapStreamLiveEventEmbed/index.tsx
@@ -0,0 +1,31 @@
+import { EmbeddedNote } from '@/components/Embedded/EmbeddedNote'
+import ExternalLink from '@/components/ExternalLink'
+import { naddrFromZapStreamWatchUrl } from '@/lib/zap-stream-url'
+import { cn } from '@/lib/utils'
+import type { Event } from 'nostr-tools'
+
+/** zap.stream `/naddr1…` → fetch kind 30311 and render as embedded note (LiveEvent), not a full-site iframe. */
+export default function ZapStreamLiveEventEmbed({
+ url,
+ className,
+ containingEvent,
+ showFull
+}: {
+ url: string
+ className?: string
+ containingEvent?: Event
+ showFull?: boolean
+}) {
+ const naddr = naddrFromZapStreamWatchUrl(url)
+ if (!naddr) {
+ return
+ }
+ return (
+
+ )
+}
diff --git a/src/lib/youtube-url.ts b/src/lib/youtube-url.ts
index 04db1a38..1b08ccda 100644
--- a/src/lib/youtube-url.ts
+++ b/src/lib/youtube-url.ts
@@ -48,3 +48,8 @@ export function parseYoutubeUrl(url: string): { videoId: string | null; isShort:
return { videoId: null, isShort: false }
}
}
+
+/** True when the in-app YouTube player can embed this URL (watch, Shorts, live, youtu.be, embed). */
+export function isEmbeddableYoutubeUrl(url: string): boolean {
+ return parseYoutubeUrl(url).videoId != null
+}
diff --git a/src/lib/zap-stream-url.test.ts b/src/lib/zap-stream-url.test.ts
index 76672419..4d16f709 100644
--- a/src/lib/zap-stream-url.test.ts
+++ b/src/lib/zap-stream-url.test.ts
@@ -1,5 +1,9 @@
import { describe, expect, it } from 'vitest'
-import { canonicalZapStreamWatchUrl, isZapStreamWatchUrl } from './zap-stream-url'
+import {
+ canonicalZapStreamWatchUrl,
+ isZapStreamWatchUrl,
+ naddrFromZapStreamWatchUrl
+} from './zap-stream-url'
const SAMPLE =
'https://zap.stream/naddr1qqjrqcmzv3nr2des95ergdp3956xzdf595uxzefk94nxzefnxgukycnzxyexyqgewaehxw309aex2mrp0yh8xmn0wf6zuum0vd5kzmp0qgsv73dxhgfk8tt76gf6q788zrfyz9dwwgwfk3aar6l5gk82a76v9fgrqsqqqan84z6qnu'
@@ -32,4 +36,11 @@ describe('zap-stream-url', () => {
expect(isZapStreamWatchUrl(SAMPLE)).toBe(true)
expect(isZapStreamWatchUrl('https://zap.stream/')).toBe(false)
})
+
+ it('naddrFromZapStreamWatchUrl returns the naddr segment', () => {
+ expect(naddrFromZapStreamWatchUrl(SAMPLE)).toBe(
+ 'naddr1qqjrqcmzv3nr2des95ergdp3956xzdf595uxzefk94nxzefnxgukycnzxyexyqgewaehxw309aex2mrp0yh8xmn0wf6zuum0vd5kzmp0qgsv73dxhgfk8tt76gf6q788zrfyz9dwwgwfk3aar6l5gk82a76v9fgrqsqqqan84z6qnu'
+ )
+ expect(naddrFromZapStreamWatchUrl('https://zap.stream/')).toBeNull()
+ })
})
diff --git a/src/lib/zap-stream-url.ts b/src/lib/zap-stream-url.ts
index 0ef8fe0c..57c51113 100644
--- a/src/lib/zap-stream-url.ts
+++ b/src/lib/zap-stream-url.ts
@@ -26,3 +26,16 @@ export function canonicalZapStreamWatchUrl(raw: string): string | null {
export function isZapStreamWatchUrl(url: string): boolean {
return canonicalZapStreamWatchUrl(url) != null
}
+
+/** NIP-19 `naddr1…` path segment from a zap.stream watch URL (embed as kind 30311 / LiveEvent). */
+export function naddrFromZapStreamWatchUrl(raw: string): string | null {
+ const canon = canonicalZapStreamWatchUrl(raw)
+ if (!canon) return null
+ try {
+ const seg = new URL(canon).pathname.split('/').filter(Boolean)[0]
+ if (seg?.startsWith('naddr1')) return seg
+ return null
+ } catch {
+ return null
+ }
+}