Squash merge nostrfeed-localStorage into ario
This commit is contained in:
parent
5e6bc32f02
commit
5ceb12ca3b
5 changed files with 432 additions and 121 deletions
248
src/composables/useNostrFeed.ts
Normal file
248
src/composables/useNostrFeed.ts
Normal file
|
|
@ -0,0 +1,248 @@
|
|||
import { ref, readonly } from 'vue'
|
||||
import type { NostrNote } from '@/lib/nostr/client'
|
||||
import { useNostr } from '@/composables/useNostr'
|
||||
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 { getClient } = useNostr(config.relays ? { relays: config.relays } : undefined)
|
||||
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
|
||||
const client = getClient()
|
||||
await client.connect()
|
||||
isConnected.value = client.isConnected
|
||||
|
||||
if (!isConnected.value) {
|
||||
throw new Error('Failed to connect to Nostr relays')
|
||||
}
|
||||
|
||||
// Configure fetch options based on feed type
|
||||
const fetchOptions: Parameters<typeof client.fetchNotes>[0] = {
|
||||
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
|
||||
const newNotes = await client.fetchNotes(fetchOptions)
|
||||
|
||||
// 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 {
|
||||
const client = getClient()
|
||||
|
||||
// Subscribe to real-time notes
|
||||
unsubscribe = client.subscribeToNotes((newNote) => {
|
||||
// Only process notes newer than last seen
|
||||
if (newNote.created_at > lastSeenTimestamp) {
|
||||
// Check if note should be included based on feed type
|
||||
const shouldInclude = shouldIncludeNote(newNote)
|
||||
if (shouldInclude) {
|
||||
// Add to beginning of notes array
|
||||
notes.value.unshift(newNote)
|
||||
|
||||
// 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(newNote.pubkey)) {
|
||||
notificationManager.notifyForNote(newNote, nostrStore.account?.pubkey)
|
||||
}
|
||||
|
||||
// Update last seen timestamp
|
||||
lastSeenTimestamp = Math.max(lastSeenTimestamp, newNote.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...')
|
||||
const client = getClient()
|
||||
await client.connect()
|
||||
isConnected.value = client.isConnected
|
||||
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
|
||||
}
|
||||
}
|
||||
85
src/composables/useNostrFeedPreloader.ts
Normal file
85
src/composables/useNostrFeedPreloader.ts
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
import { ref, computed } from 'vue'
|
||||
import { useNostrFeed, type NostrFeedConfig } from './useNostrFeed'
|
||||
|
||||
export function useNostrFeedPreloader(config: NostrFeedConfig = {}) {
|
||||
const feed = useNostrFeed(config)
|
||||
|
||||
// Preload state
|
||||
const isPreloading = ref(false)
|
||||
const isPreloaded = ref(false)
|
||||
const preloadError = ref<Error | null>(null)
|
||||
|
||||
// Check if feed data is available in localStorage
|
||||
const hasStoredData = computed(() => {
|
||||
const storageKey = `nostr-feed-${config.feedType || 'all'}`
|
||||
const storedData = localStorage.getItem(storageKey)
|
||||
return storedData !== null
|
||||
})
|
||||
|
||||
// Check if we should show loading (only if no cached data and actually loading)
|
||||
const shouldShowLoading = computed(() => {
|
||||
return (feed.isLoading.value || isPreloading.value) && feed.notes.value.length === 0
|
||||
})
|
||||
|
||||
// Preload feed data
|
||||
const preloadFeed = async () => {
|
||||
if (isPreloaded.value || isPreloading.value) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
isPreloading.value = true
|
||||
preloadError.value = null
|
||||
|
||||
console.log('Preloading Nostr feed...')
|
||||
|
||||
// Connect and load notes
|
||||
await feed.connectToFeed()
|
||||
await feed.loadNotes()
|
||||
|
||||
// Subscribe to updates
|
||||
feed.subscribeToFeedUpdates()
|
||||
|
||||
isPreloaded.value = true
|
||||
console.log('Nostr feed preloaded successfully')
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to preload Nostr feed:', error)
|
||||
preloadError.value = error instanceof Error ? error : new Error('Failed to preload feed')
|
||||
} finally {
|
||||
isPreloading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Reset preload state
|
||||
const resetPreload = () => {
|
||||
isPreloaded.value = false
|
||||
isPreloading.value = false
|
||||
preloadError.value = null
|
||||
feed.resetFeedState()
|
||||
}
|
||||
|
||||
// Auto-preload if we have stored data
|
||||
const autoPreload = async () => {
|
||||
if (hasStoredData.value && !isPreloaded.value && !isPreloading.value) {
|
||||
await preloadFeed()
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
// State
|
||||
isPreloading: computed(() => isPreloading.value),
|
||||
isPreloaded: computed(() => isPreloaded.value),
|
||||
preloadError: computed(() => preloadError.value),
|
||||
hasStoredData,
|
||||
shouldShowLoading,
|
||||
|
||||
// Actions
|
||||
preloadFeed,
|
||||
resetPreload,
|
||||
autoPreload,
|
||||
|
||||
// Expose feed methods
|
||||
...feed
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue