From 5c26aba22b4ae46eb92bed731792c325abd69555 Mon Sep 17 00:00:00 2001 From: Silberengel Date: Thu, 9 Apr 2026 21:30:33 +0200 Subject: [PATCH] open live-stream --- src/components/LiveActivitiesStrip.tsx | 99 +++++++++++++++--------- src/i18n/locales/de.ts | 4 + src/i18n/locales/en.ts | 2 + src/lib/live-activities.ts | 5 +- src/providers/LiveActivitiesProvider.tsx | 30 +------ src/providers/live-activities-context.ts | 9 +++ src/providers/useLiveActivities.ts | 14 ++++ 7 files changed, 99 insertions(+), 64 deletions(-) create mode 100644 src/providers/live-activities-context.ts create mode 100644 src/providers/useLiveActivities.ts 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) +}