diff --git a/src/components/LiveActivitiesStrip.tsx b/src/components/LiveActivitiesStrip.tsx
index cca2e41c..c265059f 100644
--- a/src/components/LiveActivitiesStrip.tsx
+++ b/src/components/LiveActivitiesStrip.tsx
@@ -1,16 +1,19 @@
import { LIVE_ACTIVITIES_SLIDE_INTERVAL_MS } from '@/lib/live-activities'
+import { toNote } from '@/lib/link'
import { cn } from '@/lib/utils'
-import { useLiveActivitiesOptional } from '@/providers/LiveActivitiesProvider'
+import { useSmartNoteNavigation } from '@/PageManager'
+import { useLiveActivitiesOptional } from '@/providers/useLiveActivities'
import { useUserPreferencesOptional } from '@/providers/UserPreferencesProvider'
import storage from '@/services/local-storage.service'
import { ExternalLink } from 'lucide-react'
-import { useEffect, useLayoutEffect, useState } from 'react'
+import { useCallback, useEffect, useLayoutEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
type TPlacement = 'sidebar' | 'mobile'
export default function LiveActivitiesStrip({ placement }: { placement: TPlacement }) {
const { t } = useTranslation()
+ const { navigateToNote } = useSmartNoteNavigation()
const userPrefs = useUserPreferencesOptional()
const showLiveActivitiesBanner =
userPrefs?.showLiveActivitiesBanner ?? storage.getShowLiveActivitiesBanner()
@@ -45,12 +48,21 @@ export default function LiveActivitiesStrip({ placement }: { placement: TPlaceme
setSlide((s) => Math.min(s, items.length - 1))
}, [items.length])
+ // `items` can shrink without a new array identity; `slide` may then be out of range until effects run.
+ const displayIndex = items.length === 0 ? 0 : Math.min(slide, items.length - 1)
+ const itemAtSlide = items[displayIndex]
+
+ const openLiveNote = useCallback(() => {
+ const ev = itemAtSlide?.event
+ if (!ev) return
+ // Same bech32 as {@link getNoteBech32Id} + pass event so {@link navigationEventStore} / cache match the URL.
+ navigateToNote(toNote(ev), ev)
+ }, [navigateToNote, itemAtSlide])
+
if (!showLiveActivitiesBanner || items.length === 0) {
return null
}
- // `items` can shrink without a new array identity; `slide` may then be out of range until effects run.
- const displayIndex = Math.min(slide, items.length - 1)
const current = items[displayIndex]
if (!current) {
return null
@@ -69,41 +81,58 @@ export default function LiveActivitiesStrip({ placement }: { placement: TPlaceme
{t('liveActivities.heading')}
-
- {current.imageUrl ? (
-
- ) : null}
-
-
-
- {current.title}
-
-
-
- {current.summary ? (
-
{current.summary}
- ) : null}
- {current.fromFollowedHost ? (
-
{t('liveActivities.fromFollow')}
+
-
+
+
{current.title}
+ {current.summary ? (
+
{current.summary}
+ ) : null}
+ {current.fromFollowedHost ? (
+
{t('liveActivities.fromFollow')}
+ ) : null}
+
+
+ e.stopPropagation()}
+ >
+
+
+
{items.length > 1 ? (
{items.map((item, i) => (
diff --git a/src/i18n/locales/de.ts b/src/i18n/locales/de.ts
index d8965f0e..5524c823 100644
--- a/src/i18n/locales/de.ts
+++ b/src/i18n/locales/de.ts
@@ -609,6 +609,10 @@ export default {
'liveActivities.regionLabel': 'Live-Räume und Streams',
'liveActivities.fromFollow': 'Von jemandem, dem du folgst',
'liveActivities.goToSlide': 'Live-Eintrag {{n}} anzeigen',
+ 'liveActivities.viewNoteTitle':
+ 'Diese Live-Aktivität als Beitrag öffnen (Wiedergabe in der App, Links darunter)',
+ 'liveActivities.openJoinPageTitle':
+ 'Join-Seite in neuem Tab öffnen (z. B. zap.stream oder die Raum-Website)',
'liveActivities.settingsToggle': 'Banner für Live-Aktivitäten',
'liveActivities.settingsHint':
'Zeigt NIP-53-Live-Räume (Audio/Video) von deinen Relays. Aktualisierung zur Viertelstunde und nach dem ersten Session-Warm-up.',
diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts
index e8d1fe3c..724ea377 100644
--- a/src/i18n/locales/en.ts
+++ b/src/i18n/locales/en.ts
@@ -604,6 +604,8 @@ export default {
'liveActivities.regionLabel': 'Live spaces and streams',
'liveActivities.fromFollow': 'From someone you follow',
'liveActivities.goToSlide': 'Show live item {{n}}',
+ 'liveActivities.viewNoteTitle': 'Open this live activity as a note (play in app, links below)',
+ 'liveActivities.openJoinPageTitle': 'Open the join page in a new tab (e.g. zap.stream or the room site)',
'liveActivities.settingsToggle': 'Live activities banner',
'liveActivities.settingsHint':
'Shows NIP-53 live rooms (audio/video spaces) from your relays. Updates on a quarter-hour schedule and when the app finishes its initial session warm-up.',
diff --git a/src/lib/live-activities.ts b/src/lib/live-activities.ts
index 64447075..fa4051db 100644
--- a/src/lib/live-activities.ts
+++ b/src/lib/live-activities.ts
@@ -51,6 +51,8 @@ export type TLiveActivityItem = {
joinUrl: string
updatedAt: number
fromFollowedHost: boolean
+ /** Full Nostr event (for navigation cache — same payload the strip already loaded). */
+ event: Event
}
function firstTagValue(ev: Event, name: string): string | undefined {
@@ -488,7 +490,8 @@ export function parseLiveActivityEvent(
imageUrl,
joinUrl,
updatedAt: ev.created_at,
- fromFollowedHost: followSet.has(ev.pubkey)
+ fromFollowedHost: followSet.has(ev.pubkey),
+ event: ev
}
}
diff --git a/src/providers/LiveActivitiesProvider.tsx b/src/providers/LiveActivitiesProvider.tsx
index 56241be6..9003db1b 100644
--- a/src/providers/LiveActivitiesProvider.tsx
+++ b/src/providers/LiveActivitiesProvider.tsx
@@ -11,39 +11,13 @@ import logger from '@/lib/logger'
import client from '@/services/client.service'
import storage from '@/services/local-storage.service'
import { registerLiveActivitiesPrewarmCallback } from '@/services/live-activities-prewarm-bridge'
-import {
- createContext,
- useCallback,
- useContext,
- useEffect,
- useMemo,
- useRef,
- useState
-} from 'react'
+import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
+import { LiveActivitiesContext } from './live-activities-context'
import { useFavoriteRelays } from './FavoriteRelaysProvider'
import { useFollowListOptional } from './follow-list-context'
import { useNostr } from './NostrProvider'
import { useUserPreferencesOptional } from './UserPreferencesProvider'
-type TLiveActivitiesContext = {
- items: TLiveActivityItem[]
- loading: boolean
-}
-
-const LiveActivitiesContext = createContext(undefined)
-
-export function useLiveActivities(): TLiveActivitiesContext {
- const ctx = useContext(LiveActivitiesContext)
- if (!ctx) {
- throw new Error('useLiveActivities must be used within LiveActivitiesProvider')
- }
- return ctx
-}
-
-export function useLiveActivitiesOptional(): TLiveActivitiesContext | undefined {
- return useContext(LiveActivitiesContext)
-}
-
export function LiveActivitiesProvider({ children }: { children: React.ReactNode }) {
const { pubkey, relayList, isInitialized, isAccountSessionHydrating } = useNostr()
const { favoriteRelays, blockedRelays } = useFavoriteRelays()
diff --git a/src/providers/live-activities-context.ts b/src/providers/live-activities-context.ts
new file mode 100644
index 00000000..0d90907d
--- /dev/null
+++ b/src/providers/live-activities-context.ts
@@ -0,0 +1,9 @@
+import type { TLiveActivityItem } from '@/lib/live-activities'
+import { createContext } from 'react'
+
+export type LiveActivitiesContextValue = {
+ items: TLiveActivityItem[]
+ loading: boolean
+}
+
+export const LiveActivitiesContext = createContext(undefined)
diff --git a/src/providers/useLiveActivities.ts b/src/providers/useLiveActivities.ts
new file mode 100644
index 00000000..f0d89d71
--- /dev/null
+++ b/src/providers/useLiveActivities.ts
@@ -0,0 +1,14 @@
+import { useContext } from 'react'
+import { LiveActivitiesContext, type LiveActivitiesContextValue } from './live-activities-context'
+
+export function useLiveActivities(): LiveActivitiesContextValue {
+ const ctx = useContext(LiveActivitiesContext)
+ if (!ctx) {
+ throw new Error('useLiveActivities must be used within LiveActivitiesProvider')
+ }
+ return ctx
+}
+
+export function useLiveActivitiesOptional(): LiveActivitiesContextValue | undefined {
+ return useContext(LiveActivitiesContext)
+}