Implement modular architecture with core services and Nostr integration

- Introduce a modular application structure with a new app configuration file to manage module settings and features.
- Implement a dependency injection container for service management across modules.
- Create a plugin manager to handle module registration, installation, and lifecycle management.
- Develop a global event bus for inter-module communication, enhancing loose coupling between components.
- Add core modules including base functionalities, Nostr feed, and PWA services, with support for dynamic loading and configuration.
- Establish a Nostr client hub for managing WebSocket connections and event handling.
- Enhance user experience with a responsive Nostr feed component, integrating admin announcements and community posts.
- Refactor existing components to align with the new modular architecture, improving maintainability and scalability.
This commit is contained in:
padreug 2025-09-04 23:43:33 +02:00
parent 2d8215a35e
commit 519a9003d4
16 changed files with 2520 additions and 14 deletions

View file

@ -0,0 +1,130 @@
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { injectService, SERVICE_TOKENS } from '@/core/di-container'
import { eventBus } from '@/core/event-bus'
import type { Event as NostrEvent, Filter } from 'nostr-tools'
export interface FeedConfig {
feedType: 'announcements' | 'general' | 'mentions'
maxPosts?: number
refreshInterval?: number
adminPubkeys?: string[]
}
export function useFeed(config: FeedConfig) {
const relayHub = injectService<any>(SERVICE_TOKENS.RELAY_HUB)
const posts = ref<NostrEvent[]>([])
const isLoading = ref(false)
const error = ref<string | null>(null)
let refreshTimer: number | null = null
let unsubscribe: (() => void) | null = null
const filteredPosts = computed(() => {
let filtered = posts.value
// Filter by feed type
if (config.feedType === 'announcements' && config.adminPubkeys) {
filtered = filtered.filter(post => config.adminPubkeys!.includes(post.pubkey))
}
// Sort by created timestamp (newest first)
filtered = filtered.sort((a, b) => b.created_at - a.created_at)
// Limit posts
if (config.maxPosts) {
filtered = filtered.slice(0, config.maxPosts)
}
return filtered
})
const loadFeed = async () => {
if (!relayHub) {
error.value = 'RelayHub not available'
return
}
isLoading.value = true
error.value = null
try {
// Create filter based on feed type
const filter: Filter = {
kinds: [1], // Text notes
limit: config.maxPosts || 50
}
if (config.feedType === 'announcements' && config.adminPubkeys) {
filter.authors = config.adminPubkeys
}
// Subscribe to events
await relayHub.subscribe('feed-subscription', [filter], {
onEvent: (event: NostrEvent) => {
// Add new event if not already present
if (!posts.value.some(p => p.id === event.id)) {
posts.value = [event, ...posts.value]
// Emit event for other modules
eventBus.emit('nostr-feed:new-post', { event, feedType: config.feedType }, 'nostr-feed')
}
},
onEose: () => {
console.log('Feed subscription end of stored events')
isLoading.value = false
},
onClose: () => {
console.log('Feed subscription closed')
}
})
unsubscribe = () => {
relayHub.unsubscribe('feed-subscription')
}
} catch (err) {
console.error('Failed to load feed:', err)
error.value = err instanceof Error ? err.message : 'Failed to load feed'
isLoading.value = false
}
}
const refreshFeed = () => {
posts.value = []
loadFeed()
}
const startAutoRefresh = () => {
if (config.refreshInterval && config.refreshInterval > 0) {
refreshTimer = setInterval(refreshFeed, config.refreshInterval) as unknown as number
}
}
const stopAutoRefresh = () => {
if (refreshTimer) {
clearInterval(refreshTimer)
refreshTimer = null
}
}
// Lifecycle
onMounted(() => {
loadFeed()
startAutoRefresh()
})
onUnmounted(() => {
stopAutoRefresh()
if (unsubscribe) {
unsubscribe()
}
})
return {
posts: filteredPosts,
isLoading,
error,
refreshFeed,
loadFeed
}
}