import { ref, readonly } from 'vue' import type { NostrNote } from '@/lib/nostr/client' import { useRelayHub } from '@/composables/useRelayHub' import { useNostrStore } from '@/stores/nostr' import { config as globalConfig } from '@/lib/config' import { notificationManager } from '@/lib/notifications/manager' export interface NostrFeedConfig { relays?: string[] feedType?: 'all' | 'announcements' | 'events' | 'general' limit?: number includeReplies?: boolean } export function useNostrFeed(config: NostrFeedConfig = {}) { const relayHub = useRelayHub() const nostrStore = useNostrStore() // State const notes = ref([]) const isLoading = ref(false) const error = ref(null) const isConnected = ref(false) // Get admin/moderator pubkeys from centralized config const adminPubkeys = globalConfig.nostr?.adminPubkeys || [] // Track last seen note timestamp to avoid duplicate notifications let lastSeenTimestamp = Math.floor(Date.now() / 1000) // Load notes from localStorage immediately (synchronous) const loadFromStorage = () => { const storageKey = `nostr-feed-${config.feedType || 'all'}` const storedNotes = localStorage.getItem(storageKey) if (storedNotes) { try { const parsedNotes = JSON.parse(storedNotes) as NostrNote[] notes.value = parsedNotes console.log(`Loaded ${parsedNotes.length} notes from localStorage`) // Update last seen timestamp from stored notes if (notes.value.length > 0) { lastSeenTimestamp = Math.max(lastSeenTimestamp, Math.max(...notes.value.map(note => note.created_at))) } return true } catch (err) { console.warn('Failed to parse stored notes:', err) localStorage.removeItem(storageKey) } } return false } // Load notes from localStorage first, then fetch new ones const loadNotes = async (isRefresh = false) => { try { // First, try to load from localStorage immediately (only if not refreshing) if (!isRefresh) { const hasStoredData = loadFromStorage() // Only show loading if we don't have stored data if (!hasStoredData) { isLoading.value = true } } else { // For refresh, always show loading isLoading.value = true } error.value = null // Connect to Nostr using the centralized relay hub await relayHub.connect() isConnected.value = relayHub.isConnected.value if (!isConnected.value) { throw new Error('Failed to connect to Nostr relays') } // Configure fetch options based on feed type const fetchOptions: any = { limit: config.limit || 50, includeReplies: config.includeReplies || false } // Filter by authors based on feed type if (config.feedType === 'announcements') { if (adminPubkeys.length > 0) { fetchOptions.authors = adminPubkeys } else { notes.value = [] return } } // Fetch new notes using the relay hub const newNotes = await relayHub.queryEvents([ { kinds: [1], // TEXT_NOTE limit: fetchOptions.limit, authors: fetchOptions.authors } ]) // Client-side filtering for 'general' feed (exclude admin posts) let filteredNotes = newNotes if (config.feedType === 'general' && adminPubkeys.length > 0) { filteredNotes = newNotes.filter(note => !adminPubkeys.includes(note.pubkey)) } // For refresh, replace all notes. For normal load, merge with existing if (isRefresh) { notes.value = filteredNotes } else { // Merge with existing notes, avoiding duplicates const existingIds = new Set(notes.value.map(note => note.id)) const uniqueNewNotes = filteredNotes.filter(note => !existingIds.has(note.id)) if (uniqueNewNotes.length > 0) { // Add new notes to the beginning notes.value.unshift(...uniqueNewNotes) } } // Limit the array size to prevent memory issues if (notes.value.length > 100) { notes.value = notes.value.slice(0, 100) } // Save to localStorage const storageKey = `nostr-feed-${config.feedType || 'all'}` localStorage.setItem(storageKey, JSON.stringify(notes.value)) console.log(`Loaded ${notes.value.length} notes`) // Update last seen timestamp if (notes.value.length > 0) { lastSeenTimestamp = Math.max(lastSeenTimestamp, Math.max(...notes.value.map(note => note.created_at))) } } catch (err) { error.value = err instanceof Error ? err : new Error('Failed to load notes') console.error('Error loading notes:', err) } finally { isLoading.value = false } } // Real-time subscription for new notes let unsubscribe: (() => void) | null = null const subscribeToFeedUpdates = () => { try { // Subscribe to real-time notes using the relay hub unsubscribe = relayHub.subscribe({ id: `feed-${config.feedType || 'all'}`, filters: [{ kinds: [1] }], // TEXT_NOTE onEvent: (event: any) => { // Only process notes newer than last seen if (event.created_at > lastSeenTimestamp) { // Check if note should be included based on feed type const shouldInclude = shouldIncludeNote(event) if (shouldInclude) { // Add to beginning of notes array notes.value.unshift(event) // Limit the array size to prevent memory issues if (notes.value.length > 100) { notes.value = notes.value.slice(0, 100) } // Save to localStorage const storageKey = `nostr-feed-${config.feedType || 'all'}` localStorage.setItem(storageKey, JSON.stringify(notes.value)) } // Send notification if appropriate (only for admin announcements when not in announcements feed) if (config.feedType !== 'announcements' && adminPubkeys.includes(event.pubkey)) { notificationManager.notifyForNote(event, nostrStore.account?.pubkey) } // Update last seen timestamp lastSeenTimestamp = Math.max(lastSeenTimestamp, event.created_at) } } }) } catch (error) { console.error('Failed to start real-time subscription:', error) } } const shouldIncludeNote = (note: NostrNote): boolean => { if (config.feedType === 'announcements') { return adminPubkeys.length > 0 && adminPubkeys.includes(note.pubkey) } if (config.feedType === 'general' && adminPubkeys.length > 0) { return !adminPubkeys.includes(note.pubkey) } // For other feed types, include all notes return true } const connectToFeed = async () => { try { console.log('Connecting to Nostr feed...') await relayHub.connect() isConnected.value = relayHub.isConnected.value console.log('Connected to Nostr feed') } catch (err) { console.error('Error connecting to feed:', err) throw err } } const disconnectFromFeed = () => { if (unsubscribe) { unsubscribe() unsubscribe = null } isConnected.value = false } const resetFeedState = () => { notes.value = [] error.value = null isLoading.value = false isConnected.value = false lastSeenTimestamp = Math.floor(Date.now() / 1000) } const cleanup = () => { disconnectFromFeed() } // Initialize by loading from storage immediately loadFromStorage() return { // State notes: readonly(notes), isLoading: readonly(isLoading), error: readonly(error), isConnected: readonly(isConnected), // Actions loadNotes, connectToFeed, disconnectFromFeed, subscribeToFeedUpdates, resetFeedState, cleanup } }