- Introduce a new composable, useRelayHub, to manage all Nostr WebSocket connections, enhancing connection stability and performance. - Update existing components and composables to utilize the Relay Hub for connecting, publishing events, and subscribing to updates, streamlining the overall architecture. - Add a RelayHubStatus component to display connection status and health metrics, improving user feedback on the connection state. - Implement a RelayHubDemo page to showcase the functionality of the Relay Hub, including connection tests and subscription management. - Ensure proper error handling and logging throughout the integration process to facilitate debugging and user experience.
254 lines
No EOL
7.9 KiB
TypeScript
254 lines
No EOL
7.9 KiB
TypeScript
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<NostrNote[]>([])
|
|
const isLoading = ref(false)
|
|
const error = ref<Error | null>(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
|
|
}
|
|
}
|