diff --git a/src/app.config.ts b/src/app.config.ts
new file mode 100644
index 0000000..1280fff
--- /dev/null
+++ b/src/app.config.ts
@@ -0,0 +1,71 @@
+import type { AppConfig } from './core/types'
+
+export const appConfig: AppConfig = {
+ modules: {
+ base: {
+ name: 'base',
+ enabled: true,
+ lazy: false,
+ config: {
+ nostr: {
+ relays: JSON.parse(import.meta.env.VITE_NOSTR_RELAYS || '["wss://relay.damus.io", "wss://nos.lol"]')
+ },
+ auth: {
+ sessionTimeout: 24 * 60 * 60 * 1000, // 24 hours
+ },
+ pwa: {
+ autoPrompt: true
+ }
+ }
+ },
+ 'nostr-feed': {
+ name: 'nostr-feed',
+ enabled: true,
+ lazy: false,
+ config: {
+ refreshInterval: 30000, // 30 seconds
+ maxPosts: 100,
+ adminPubkeys: JSON.parse(import.meta.env.VITE_ADMIN_PUBKEYS || '[]'),
+ feedTypes: ['announcements', 'general']
+ }
+ },
+ market: {
+ name: 'market',
+ enabled: true,
+ lazy: false,
+ config: {
+ defaultCurrency: 'sats',
+ paymentTimeout: 300000, // 5 minutes
+ maxOrderHistory: 50
+ }
+ },
+ chat: {
+ name: 'chat',
+ enabled: true,
+ lazy: true, // Load on demand
+ config: {
+ maxMessages: 500,
+ autoScroll: true,
+ showTimestamps: true
+ }
+ },
+ events: {
+ name: 'events',
+ enabled: true,
+ lazy: false,
+ config: {
+ ticketValidationEndpoint: '/api/tickets/validate',
+ maxTicketsPerUser: 10
+ }
+ }
+ },
+
+ features: {
+ pwa: true,
+ pushNotifications: true,
+ electronApp: false,
+ developmentMode: import.meta.env.DEV
+ }
+}
+
+export default appConfig
\ No newline at end of file
diff --git a/src/app.ts b/src/app.ts
new file mode 100644
index 0000000..e16bfe2
--- /dev/null
+++ b/src/app.ts
@@ -0,0 +1,157 @@
+import { createApp } from 'vue'
+import { createRouter, createWebHistory } from 'vue-router'
+import { createPinia } from 'pinia'
+// Core plugin system
+import { pluginManager } from './core/plugin-manager'
+import { eventBus } from './core/event-bus'
+import { container, SERVICE_TOKENS } from './core/di-container'
+
+// App configuration
+import appConfig from './app.config'
+
+// Base modules
+import baseModule from './modules/base'
+import nostrFeedModule from './modules/nostr-feed'
+
+// Root component
+import App from './App.vue'
+
+// Styles
+import './assets/index.css'
+
+// Use existing i18n setup
+import { i18n } from './i18n'
+
+/**
+ * Initialize and start the modular application
+ */
+export async function createAppInstance() {
+ console.log('🚀 Starting modular application...')
+
+ // Create Vue app
+ const app = createApp(App)
+
+ // Create router
+ const router = createRouter({
+ history: createWebHistory(),
+ routes: [
+ // Default route - will be populated by modules
+ {
+ path: '/',
+ name: 'home',
+ component: () => import('./pages/Home.vue'),
+ meta: { requiresAuth: true }
+ },
+ {
+ path: '/login',
+ name: 'login',
+ component: () => import('./pages/LoginDemo.vue'),
+ meta: { requiresAuth: false }
+ }
+ ]
+ })
+
+ // Use existing i18n setup
+
+ // Create Pinia store
+ const pinia = createPinia()
+
+ // Install core plugins
+ app.use(router)
+ app.use(pinia)
+ app.use(i18n)
+
+ // Initialize plugin manager
+ pluginManager.init(app, router)
+
+ // Register modules based on configuration
+ const moduleRegistrations = []
+
+ // Register base module first (required)
+ if (appConfig.modules.base.enabled) {
+ moduleRegistrations.push(
+ pluginManager.register(baseModule, appConfig.modules.base)
+ )
+ }
+
+ // Register nostr-feed module
+ if (appConfig.modules['nostr-feed'].enabled) {
+ moduleRegistrations.push(
+ pluginManager.register(nostrFeedModule, appConfig.modules['nostr-feed'])
+ )
+ }
+
+ // TODO: Register other modules as they're converted
+ // - market module
+ // - chat module
+ // - events module
+
+ // Wait for all modules to register
+ await Promise.all(moduleRegistrations)
+
+ // Install all enabled modules
+ await pluginManager.installAll()
+
+ // Set up auth guard
+ router.beforeEach(async (to, _from, next) => {
+ const authService = container.inject(SERVICE_TOKENS.AUTH_SERVICE) as any
+
+ if (to.meta.requiresAuth && authService && !authService.isAuthenticated?.value) {
+ next('/login')
+ } else if (to.path === '/login' && authService && authService.isAuthenticated?.value) {
+ next('/')
+ } else {
+ next()
+ }
+ })
+
+ // Global error handling
+ app.config.errorHandler = (err, _vm, info) => {
+ console.error('Global error:', err, info)
+ eventBus.emit('app:error', { error: err, info }, 'app')
+ }
+
+ // Development helpers
+ if (appConfig.features.developmentMode) {
+ // Expose debugging helpers globally
+ ;(window as any).__pluginManager = pluginManager
+ ;(window as any).__eventBus = eventBus
+ ;(window as any).__container = container
+
+ console.log('🔧 Development mode enabled')
+ console.log('Available globals: __pluginManager, __eventBus, __container')
+ }
+
+ console.log('✅ Application initialized successfully')
+
+ return { app, router }
+}
+
+/**
+ * Start the application
+ */
+export async function startApp() {
+ try {
+ const { app } = await createAppInstance()
+
+ // Mount the app
+ app.mount('#app')
+
+ console.log('🎉 Application started!')
+
+ // Emit app started event
+ eventBus.emit('app:started', {}, 'app')
+
+ } catch (error) {
+ console.error('💥 Failed to start application:', error)
+
+ // Show error to user
+ document.getElementById('app')!.innerHTML = `
+
+
Application Failed to Start
+
${error instanceof Error ? error.message : 'Unknown error'}
+
Please refresh the page or contact support.
+
+ `
+ }
+}
\ No newline at end of file
diff --git a/src/core/di-container.ts b/src/core/di-container.ts
new file mode 100644
index 0000000..c28d2d1
--- /dev/null
+++ b/src/core/di-container.ts
@@ -0,0 +1,137 @@
+import type { DIContainer, ServiceToken } from './types'
+
+interface ServiceRegistration {
+ service: any
+ scope: 'singleton' | 'transient'
+ instance?: any
+}
+
+/**
+ * Dependency Injection Container
+ * Manages service registration and injection across modules
+ */
+export class DIContainerImpl implements DIContainer {
+ private services = new Map()
+
+ /**
+ * Register a service in the container
+ */
+ provide(token: ServiceToken, service: T, scope: 'singleton' | 'transient' = 'singleton'): void {
+ this.services.set(token, {
+ service,
+ scope,
+ instance: scope === 'singleton' ? service : undefined
+ })
+ }
+
+ /**
+ * Inject a service from the container
+ */
+ inject(token: ServiceToken): T | undefined {
+ const registration = this.services.get(token)
+
+ if (!registration) {
+ return undefined
+ }
+
+ if (registration.scope === 'singleton') {
+ return registration.instance as T
+ }
+
+ // For transient services, create new instance
+ // Note: This assumes the service is a constructor function
+ if (typeof registration.service === 'function') {
+ try {
+ return new registration.service() as T
+ } catch (error) {
+ console.error(`Error creating transient service for token ${String(token)}:`, error)
+ return undefined
+ }
+ }
+
+ return registration.service as T
+ }
+
+ /**
+ * Remove a service from the container
+ */
+ remove(token: ServiceToken): boolean {
+ return this.services.delete(token)
+ }
+
+ /**
+ * Clear all services
+ */
+ clear(): void {
+ this.services.clear()
+ }
+
+ /**
+ * Get all registered service tokens
+ */
+ getRegisteredTokens(): ServiceToken[] {
+ return Array.from(this.services.keys())
+ }
+
+ /**
+ * Check if a service is registered
+ */
+ has(token: ServiceToken): boolean {
+ return this.services.has(token)
+ }
+
+ /**
+ * Get service registration info
+ */
+ getServiceInfo(token: ServiceToken): { scope: string; hasInstance: boolean } | undefined {
+ const registration = this.services.get(token)
+ if (!registration) {
+ return undefined
+ }
+
+ return {
+ scope: registration.scope,
+ hasInstance: registration.instance !== undefined
+ }
+ }
+}
+
+// Global DI container instance
+export const container = new DIContainerImpl()
+
+// Service token constants
+export const SERVICE_TOKENS = {
+ // Core services
+ EVENT_BUS: Symbol('eventBus'),
+ ROUTER: Symbol('router'),
+
+ // Nostr services
+ RELAY_HUB: Symbol('relayHub'),
+ NOSTR_CLIENT_HUB: Symbol('nostrClientHub'),
+
+ // Auth services
+ AUTH_SERVICE: Symbol('authService'),
+
+ // Market services
+ MARKET_STORE: Symbol('marketStore'),
+ PAYMENT_MONITOR: Symbol('paymentMonitor'),
+
+ // Chat services
+ CHAT_SERVICE: Symbol('chatService'),
+
+ // Events services
+ EVENTS_SERVICE: Symbol('eventsService'),
+} as const
+
+// Type-safe injection helpers
+export function injectService(token: ServiceToken): T {
+ const service = container.inject(token)
+ if (!service) {
+ throw new Error(`Service not found for token: ${String(token)}`)
+ }
+ return service
+}
+
+export function tryInjectService(token: ServiceToken): T | undefined {
+ return container.inject(token)
+}
\ No newline at end of file
diff --git a/src/core/event-bus.ts b/src/core/event-bus.ts
new file mode 100644
index 0000000..c4b456a
--- /dev/null
+++ b/src/core/event-bus.ts
@@ -0,0 +1,124 @@
+import type { ModuleEvent, ModuleEventHandler } from './types'
+
+/**
+ * Global event bus for inter-module communication
+ * Provides loose coupling between modules via event-driven architecture
+ */
+export class ModuleEventBus {
+ private listeners = new Map>()
+ private eventHistory: ModuleEvent[] = []
+ private maxHistorySize = 1000
+
+ /**
+ * Emit an event to all registered listeners
+ */
+ emit(type: string, data: any, source = 'unknown'): void {
+ const event: ModuleEvent = {
+ type,
+ source,
+ data,
+ timestamp: Date.now()
+ }
+
+ // Store in history
+ this.eventHistory.push(event)
+ if (this.eventHistory.length > this.maxHistorySize) {
+ this.eventHistory.shift()
+ }
+
+ // Notify listeners
+ const listeners = this.listeners.get(type)
+ if (listeners) {
+ listeners.forEach(handler => {
+ try {
+ handler(event)
+ } catch (error) {
+ console.error(`Error in event handler for ${type}:`, error)
+ }
+ })
+ }
+
+ // Also notify wildcard listeners
+ const wildcardListeners = this.listeners.get('*')
+ if (wildcardListeners) {
+ wildcardListeners.forEach(handler => {
+ try {
+ handler(event)
+ } catch (error) {
+ console.error(`Error in wildcard event handler:`, error)
+ }
+ })
+ }
+ }
+
+ /**
+ * Register an event listener
+ */
+ on(type: string, handler: ModuleEventHandler): () => void {
+ if (!this.listeners.has(type)) {
+ this.listeners.set(type, new Set())
+ }
+
+ this.listeners.get(type)!.add(handler)
+
+ // Return unsubscribe function
+ return () => this.off(type, handler)
+ }
+
+ /**
+ * Remove an event listener
+ */
+ off(type: string, handler: ModuleEventHandler): void {
+ const listeners = this.listeners.get(type)
+ if (listeners) {
+ listeners.delete(handler)
+ if (listeners.size === 0) {
+ this.listeners.delete(type)
+ }
+ }
+ }
+
+ /**
+ * Remove all listeners for a given event type
+ */
+ removeAllListeners(type: string): void {
+ this.listeners.delete(type)
+ }
+
+ /**
+ * Clear all listeners
+ */
+ clear(): void {
+ this.listeners.clear()
+ this.eventHistory = []
+ }
+
+ /**
+ * Get recent events (for debugging/monitoring)
+ */
+ getEventHistory(count = 50): ModuleEvent[] {
+ return this.eventHistory.slice(-count)
+ }
+
+ /**
+ * Get all registered event types
+ */
+ getEventTypes(): string[] {
+ return Array.from(this.listeners.keys())
+ }
+
+ /**
+ * Get listener count for an event type
+ */
+ getListenerCount(type: string): number {
+ return this.listeners.get(type)?.size || 0
+ }
+}
+
+// Global event bus instance
+export const eventBus = new ModuleEventBus()
+
+// Convenience functions
+export const emit = eventBus.emit.bind(eventBus)
+export const on = eventBus.on.bind(eventBus)
+export const off = eventBus.off.bind(eventBus)
\ No newline at end of file
diff --git a/src/core/plugin-manager.ts b/src/core/plugin-manager.ts
new file mode 100644
index 0000000..cb54421
--- /dev/null
+++ b/src/core/plugin-manager.ts
@@ -0,0 +1,329 @@
+import type { App } from 'vue'
+import type { Router } from 'vue-router'
+import type { ModulePlugin, ModuleConfig, ModuleRegistration } from './types'
+import { eventBus } from './event-bus'
+import { container } from './di-container'
+
+/**
+ * Plugin Manager
+ * Handles module registration, dependency resolution, and lifecycle management
+ */
+export class PluginManager {
+ private modules = new Map()
+ private app: App | null = null
+ private router: Router | null = null
+ private installOrder: string[] = []
+
+ /**
+ * Initialize the plugin manager
+ */
+ init(app: App, router: Router): void {
+ this.app = app
+ this.router = router
+
+ // Register core services
+ container.provide('app', app)
+ container.provide('router', router)
+ container.provide('eventBus', eventBus)
+
+ console.log('🔧 Plugin Manager initialized')
+ }
+
+ /**
+ * Register a module plugin
+ */
+ async register(plugin: ModulePlugin, config: ModuleConfig): Promise {
+ if (this.modules.has(plugin.name)) {
+ throw new Error(`Module ${plugin.name} is already registered`)
+ }
+
+ // Validate dependencies
+ const missingDeps = this.validateDependencies(plugin)
+ if (missingDeps.length > 0) {
+ throw new Error(`Module ${plugin.name} has missing dependencies: ${missingDeps.join(', ')}`)
+ }
+
+ // Register the module
+ const registration: ModuleRegistration = {
+ plugin,
+ config,
+ installed: false
+ }
+
+ this.modules.set(plugin.name, registration)
+ console.log(`📦 Registered module: ${plugin.name} v${plugin.version}`)
+
+ // Auto-install if enabled and not lazy
+ if (config.enabled && !config.lazy) {
+ await this.install(plugin.name)
+ }
+
+ eventBus.emit('module:registered', { name: plugin.name, config }, 'plugin-manager')
+ }
+
+ /**
+ * Install a module
+ */
+ async install(moduleName: string): Promise {
+ const registration = this.modules.get(moduleName)
+ if (!registration) {
+ throw new Error(`Module ${moduleName} is not registered`)
+ }
+
+ if (registration.installed) {
+ console.warn(`Module ${moduleName} is already installed`)
+ return
+ }
+
+ const { plugin, config } = registration
+
+ // Install dependencies first
+ if (plugin.dependencies) {
+ for (const dep of plugin.dependencies) {
+ const depRegistration = this.modules.get(dep)
+ if (!depRegistration) {
+ throw new Error(`Dependency ${dep} is not registered`)
+ }
+ if (!depRegistration.installed) {
+ await this.install(dep)
+ }
+ }
+ }
+
+ try {
+ // Install the module
+ if (!this.app) {
+ throw new Error('Plugin manager not initialized')
+ }
+
+ await plugin.install(this.app, config.config)
+
+ // Register routes if provided
+ if (plugin.routes && this.router) {
+ for (const route of plugin.routes) {
+ this.router.addRoute(route)
+ }
+ }
+
+ // Register services in DI container
+ if (plugin.services) {
+ for (const [name, service] of Object.entries(plugin.services)) {
+ container.provide(Symbol(name), service)
+ }
+ }
+
+ // Mark as installed
+ registration.installed = true
+ registration.installTime = Date.now()
+ this.installOrder.push(moduleName)
+
+ console.log(`✅ Installed module: ${moduleName}`)
+ eventBus.emit('module:installed', { name: moduleName, plugin, config }, 'plugin-manager')
+
+ } catch (error) {
+ console.error(`❌ Failed to install module ${moduleName}:`, error)
+ eventBus.emit('module:install-failed', { name: moduleName, error }, 'plugin-manager')
+ throw error
+ }
+ }
+
+ /**
+ * Uninstall a module
+ */
+ async uninstall(moduleName: string): Promise {
+ const registration = this.modules.get(moduleName)
+ if (!registration) {
+ throw new Error(`Module ${moduleName} is not registered`)
+ }
+
+ if (!registration.installed) {
+ console.warn(`Module ${moduleName} is not installed`)
+ return
+ }
+
+ // Check for dependents
+ const dependents = this.findDependents(moduleName)
+ if (dependents.length > 0) {
+ throw new Error(`Cannot uninstall ${moduleName}: required by ${dependents.join(', ')}`)
+ }
+
+ try {
+ // Call module's uninstall hook
+ if (registration.plugin.uninstall) {
+ await registration.plugin.uninstall()
+ }
+
+ // Remove routes
+ if (registration.plugin.routes && this.router) {
+ // Note: Vue Router doesn't have removeRoute, so we'd need to track and rebuild
+ // For now, we'll just log this limitation
+ console.warn(`Routes from ${moduleName} cannot be removed (Vue Router limitation)`)
+ }
+
+ // Remove services from DI container
+ if (registration.plugin.services) {
+ for (const name of Object.keys(registration.plugin.services)) {
+ container.remove(Symbol(name))
+ }
+ }
+
+ // Mark as uninstalled
+ registration.installed = false
+ registration.installTime = undefined
+
+ const orderIndex = this.installOrder.indexOf(moduleName)
+ if (orderIndex !== -1) {
+ this.installOrder.splice(orderIndex, 1)
+ }
+
+ console.log(`🗑️ Uninstalled module: ${moduleName}`)
+ eventBus.emit('module:uninstalled', { name: moduleName }, 'plugin-manager')
+
+ } catch (error) {
+ console.error(`❌ Failed to uninstall module ${moduleName}:`, error)
+ eventBus.emit('module:uninstall-failed', { name: moduleName, error }, 'plugin-manager')
+ throw error
+ }
+ }
+
+ /**
+ * Get module registration info
+ */
+ getModule(name: string): ModuleRegistration | undefined {
+ return this.modules.get(name)
+ }
+
+ /**
+ * Get all registered modules
+ */
+ getModules(): Map {
+ return new Map(this.modules)
+ }
+
+ /**
+ * Get installed modules in installation order
+ */
+ getInstalledModules(): string[] {
+ return [...this.installOrder]
+ }
+
+ /**
+ * Check if a module is installed
+ */
+ isInstalled(name: string): boolean {
+ return this.modules.get(name)?.installed || false
+ }
+
+ /**
+ * Get module status
+ */
+ getStatus(): {
+ registered: number
+ installed: number
+ failed: number
+ modules: Array<{ name: string; version: string; installed: boolean; dependencies?: string[] }>
+ } {
+ const modules = Array.from(this.modules.values())
+
+ return {
+ registered: modules.length,
+ installed: modules.filter(m => m.installed).length,
+ failed: modules.filter(m => !m.installed && m.config.enabled).length,
+ modules: modules.map(({ plugin, installed }) => ({
+ name: plugin.name,
+ version: plugin.version,
+ installed,
+ dependencies: plugin.dependencies
+ }))
+ }
+ }
+
+ /**
+ * Validate module dependencies
+ */
+ private validateDependencies(plugin: ModulePlugin): string[] {
+ if (!plugin.dependencies) {
+ return []
+ }
+
+ return plugin.dependencies.filter(dep => !this.modules.has(dep))
+ }
+
+ /**
+ * Find modules that depend on the given module
+ */
+ private findDependents(moduleName: string): string[] {
+ const dependents: string[] = []
+
+ for (const [name, registration] of this.modules) {
+ if (registration.installed &&
+ registration.plugin.dependencies?.includes(moduleName)) {
+ dependents.push(name)
+ }
+ }
+
+ return dependents
+ }
+
+ /**
+ * Install all enabled modules
+ */
+ async installAll(): Promise {
+ const enabledModules = Array.from(this.modules.entries())
+ .filter(([_, reg]) => reg.config.enabled && !reg.config.lazy)
+ .map(([name, _]) => name)
+
+ // Sort by dependencies to ensure correct installation order
+ const sortedModules = this.topologicalSort(enabledModules)
+
+ for (const moduleName of sortedModules) {
+ if (!this.isInstalled(moduleName)) {
+ await this.install(moduleName)
+ }
+ }
+ }
+
+ /**
+ * Topological sort for dependency resolution
+ */
+ private topologicalSort(moduleNames: string[]): string[] {
+ const visited = new Set()
+ const visiting = new Set()
+ const result: string[] = []
+
+ const visit = (name: string) => {
+ if (visiting.has(name)) {
+ throw new Error(`Circular dependency detected involving module: ${name}`)
+ }
+ if (visited.has(name)) {
+ return
+ }
+
+ visiting.add(name)
+
+ const registration = this.modules.get(name)
+ if (registration?.plugin.dependencies) {
+ for (const dep of registration.plugin.dependencies) {
+ if (moduleNames.includes(dep)) {
+ visit(dep)
+ }
+ }
+ }
+
+ visiting.delete(name)
+ visited.add(name)
+ result.push(name)
+ }
+
+ for (const name of moduleNames) {
+ if (!visited.has(name)) {
+ visit(name)
+ }
+ }
+
+ return result
+ }
+}
+
+// Global plugin manager instance
+export const pluginManager = new PluginManager()
\ No newline at end of file
diff --git a/src/core/types.ts b/src/core/types.ts
new file mode 100644
index 0000000..9ce1e47
--- /dev/null
+++ b/src/core/types.ts
@@ -0,0 +1,93 @@
+import type { App, Component } from 'vue'
+import type { RouteRecordRaw } from 'vue-router'
+
+// Base module plugin interface
+export interface ModulePlugin {
+ /** Unique module name */
+ name: string
+
+ /** Module version */
+ version: string
+
+ /** Required dependencies (other module names) */
+ dependencies?: string[]
+
+ /** Module configuration */
+ config?: Record
+
+ /** Install the module */
+ install(app: App, options?: any): Promise | void
+
+ /** Uninstall the module (cleanup) */
+ uninstall?(): Promise | void
+
+ /** Routes provided by this module */
+ routes?: RouteRecordRaw[]
+
+ /** Components provided by this module */
+ components?: Record
+
+ /** Services provided by this module */
+ services?: Record
+
+ /** Composables provided by this module */
+ composables?: Record
+}
+
+// Module configuration for app setup
+export interface ModuleConfig {
+ /** Module name */
+ name: string
+
+ /** Whether module is enabled */
+ enabled: boolean
+
+ /** Module-specific configuration */
+ config?: Record
+
+ /** Load module dynamically */
+ lazy?: boolean
+}
+
+// App configuration
+export interface AppConfig {
+ /** Configured modules */
+ modules: Record
+
+ /** Global app features */
+ features: {
+ pwa: boolean
+ pushNotifications: boolean
+ electronApp: boolean
+ developmentMode: boolean
+ }
+}
+
+// Module registration info
+export interface ModuleRegistration {
+ plugin: ModulePlugin
+ config: ModuleConfig
+ installed: boolean
+ installTime?: number
+}
+
+// Event system for inter-module communication
+export interface ModuleEvent {
+ type: string
+ source: string
+ data: any
+ timestamp: number
+}
+
+export type ModuleEventHandler = (event: ModuleEvent) => void
+
+// Service injection tokens
+export type ServiceToken = string | symbol
+
+// Dependency injection container interface
+export interface DIContainer {
+ provide(token: ServiceToken, service: T, scope?: 'singleton' | 'transient'): void
+ inject(token: ServiceToken): T | undefined
+ remove(token: ServiceToken): boolean
+ clear(): void
+}
\ No newline at end of file
diff --git a/src/main.ts b/src/main.ts
index 2302ee1..ee1eff7 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -1,19 +1,8 @@
-import { createApp } from 'vue'
-import { createPinia } from 'pinia'
-import App from './App.vue'
-import router from './router'
-import { i18n } from './i18n'
-import './assets/index.css'
+// New modular application entry point
+import { startApp } from './app'
import { registerSW } from 'virtual:pwa-register'
import 'vue-sonner/style.css'
-const app = createApp(App)
-const pinia = createPinia()
-
-app.use(router)
-app.use(i18n)
-app.use(pinia)
-
// Simple periodic service worker updates
const intervalMS = 60 * 60 * 1000 // 1 hour
registerSW({
@@ -27,4 +16,5 @@ registerSW({
}
})
-app.mount('#app')
+// Start the modular application
+startApp()
diff --git a/src/modules/base/auth/auth-service.ts b/src/modules/base/auth/auth-service.ts
new file mode 100644
index 0000000..11b8d61
--- /dev/null
+++ b/src/modules/base/auth/auth-service.ts
@@ -0,0 +1,80 @@
+// Copy the existing auth logic into a service class
+import { ref } from 'vue'
+import { eventBus } from '@/core/event-bus'
+
+export class AuthService {
+ public isAuthenticated = ref(false)
+ public user = ref(null)
+ public isLoading = ref(false)
+
+ async initialize(): Promise {
+ console.log('🔑 Initializing auth service...')
+
+ // Check for existing auth state
+ this.checkAuth()
+
+ if (this.isAuthenticated.value) {
+ eventBus.emit('auth:login', { user: this.user.value }, 'auth-service')
+ }
+ }
+
+ checkAuth(): boolean {
+ // Implement your existing auth check logic here
+ // For now, we'll use a simple localStorage check
+ const authData = localStorage.getItem('auth')
+ if (authData) {
+ try {
+ const parsed = JSON.parse(authData)
+ this.isAuthenticated.value = true
+ this.user.value = parsed
+ return true
+ } catch (error) {
+ console.error('Invalid auth data in localStorage:', error)
+ this.logout()
+ }
+ }
+
+ this.isAuthenticated.value = false
+ this.user.value = null
+ return false
+ }
+
+ async login(credentials: any): Promise {
+ this.isLoading.value = true
+
+ try {
+ // Implement your login logic here
+ // For demo purposes, we'll accept any credentials
+ this.user.value = credentials
+ this.isAuthenticated.value = true
+
+ // Store auth state
+ localStorage.setItem('auth', JSON.stringify(credentials))
+
+ eventBus.emit('auth:login', { user: credentials }, 'auth-service')
+
+ } catch (error) {
+ console.error('Login failed:', error)
+ eventBus.emit('auth:login-failed', { error }, 'auth-service')
+ throw error
+ } finally {
+ this.isLoading.value = false
+ }
+ }
+
+ logout(): void {
+ this.user.value = null
+ this.isAuthenticated.value = false
+ localStorage.removeItem('auth')
+
+ eventBus.emit('auth:logout', {}, 'auth-service')
+ }
+
+ async refresh(): Promise {
+ // Implement token refresh logic if needed
+ console.log('Refreshing auth token...')
+ }
+}
+
+// Export singleton instance
+export const auth = new AuthService()
\ No newline at end of file
diff --git a/src/modules/base/index.ts b/src/modules/base/index.ts
new file mode 100644
index 0000000..abf431e
--- /dev/null
+++ b/src/modules/base/index.ts
@@ -0,0 +1,65 @@
+import type { App } from 'vue'
+import type { ModulePlugin } from '@/core/types'
+import { container, SERVICE_TOKENS } from '@/core/di-container'
+import { relayHub } from './nostr/relay-hub'
+import { nostrclientHub } from './nostr/nostrclient-hub'
+
+// Import auth services
+import { auth } from './auth/auth-service'
+
+// Import PWA services
+import { pwaService } from './pwa/pwa-service'
+
+/**
+ * Base Module Plugin
+ * Provides core infrastructure: Nostr, Auth, PWA, and UI components
+ */
+export const baseModule: ModulePlugin = {
+ name: 'base',
+ version: '1.0.0',
+
+ async install(_app: App, options?: any) {
+ console.log('🔧 Installing base module...')
+
+ // Register core Nostr services
+ container.provide(SERVICE_TOKENS.RELAY_HUB, relayHub)
+ container.provide(SERVICE_TOKENS.NOSTR_CLIENT_HUB, nostrclientHub)
+
+ // Register auth service
+ container.provide(SERVICE_TOKENS.AUTH_SERVICE, auth)
+
+ // Register PWA service
+ container.provide('pwaService', pwaService)
+
+ // Initialize core services
+ await relayHub.initialize(options?.nostr?.relays || [])
+ await auth.initialize()
+
+ console.log('✅ Base module installed successfully')
+ },
+
+ async uninstall() {
+ console.log('🗑️ Uninstalling base module...')
+
+ // Cleanup Nostr connections
+ relayHub.disconnect()
+ nostrclientHub.disconnect?.()
+
+ console.log('✅ Base module uninstalled')
+ },
+
+ services: {
+ relayHub,
+ nostrclientHub,
+ auth,
+ pwaService
+ },
+
+ // No routes - base module is pure infrastructure
+ routes: [],
+
+ // No UI components at module level - they'll be imported as needed
+ components: {}
+}
+
+export default baseModule
\ No newline at end of file
diff --git a/src/modules/base/nostr/index.ts b/src/modules/base/nostr/index.ts
new file mode 100644
index 0000000..2cd4bdf
--- /dev/null
+++ b/src/modules/base/nostr/index.ts
@@ -0,0 +1,16 @@
+// Re-export Nostr infrastructure from base module
+export { RelayHub } from './relay-hub'
+export { NostrclientHub } from './nostrclient-hub'
+export { relayHub } from './relay-hub'
+export { nostrclientHub } from './nostrclient-hub'
+
+// Re-export types
+export type {
+ RelayConfig,
+ SubscriptionConfig,
+ RelayStatus
+} from './relay-hub'
+
+export type {
+ NostrclientConfig
+} from './nostrclient-hub'
\ No newline at end of file
diff --git a/src/modules/base/nostr/nostrclient-hub.ts b/src/modules/base/nostr/nostrclient-hub.ts
new file mode 100644
index 0000000..0db1f6a
--- /dev/null
+++ b/src/modules/base/nostr/nostrclient-hub.ts
@@ -0,0 +1,362 @@
+import type { Filter, Event } from 'nostr-tools'
+
+// Simple EventEmitter for browser compatibility
+class EventEmitter {
+ private events: { [key: string]: Function[] } = {}
+
+ on(event: string, listener: Function) {
+ if (!this.events[event]) {
+ this.events[event] = []
+ }
+ this.events[event].push(listener)
+ }
+
+ emit(event: string, ...args: any[]) {
+ if (this.events[event]) {
+ this.events[event].forEach(listener => listener(...args))
+ }
+ }
+
+ removeAllListeners(event?: string) {
+ if (event) {
+ delete this.events[event]
+ } else {
+ this.events = {}
+ }
+ }
+}
+
+export interface NostrclientConfig {
+ url: string
+ privateKey?: string // For private WebSocket endpoint
+}
+
+export interface SubscriptionConfig {
+ id: string
+ filters: Filter[]
+ onEvent?: (event: Event) => void
+ onEose?: () => void
+ onClose?: () => void
+}
+
+export interface RelayStatus {
+ url: string
+ connected: boolean
+ lastSeen: number
+ error?: string
+}
+
+export class NostrclientHub extends EventEmitter {
+ private ws: WebSocket | null = null
+ private config: NostrclientConfig
+ private subscriptions: Map = new Map()
+ private reconnectInterval?: number
+ private reconnectAttempts = 0
+ private readonly maxReconnectAttempts = 5
+ private readonly reconnectDelay = 5000
+
+ // Connection state
+ private _isConnected = false
+ private _isConnecting = false
+
+ constructor(config: NostrclientConfig) {
+ super()
+ this.config = config
+ }
+
+ get isConnected(): boolean {
+ return this._isConnected
+ }
+
+ get isConnecting(): boolean {
+ return this._isConnecting
+ }
+
+ get totalSubscriptionCount(): number {
+ return this.subscriptions.size
+ }
+
+ get subscriptionDetails(): Array<{ id: string; filters: Filter[] }> {
+ return Array.from(this.subscriptions.values()).map(sub => ({
+ id: sub.id,
+ filters: sub.filters
+ }))
+ }
+
+ /**
+ * Initialize and connect to nostrclient WebSocket
+ */
+ async initialize(): Promise {
+ console.log('🔧 NostrclientHub: Initializing connection to', this.config.url)
+ await this.connect()
+ }
+
+ /**
+ * Connect to the nostrclient WebSocket
+ */
+ async connect(): Promise {
+ if (this._isConnecting || this._isConnected) {
+ return
+ }
+
+ this._isConnecting = true
+ this.reconnectAttempts++
+
+ try {
+ console.log('🔧 NostrclientHub: Connecting to nostrclient WebSocket')
+
+ // Determine WebSocket endpoint
+ const wsUrl = this.config.privateKey
+ ? `${this.config.url}/${this.config.privateKey}` // Private endpoint
+ : `${this.config.url}/relay` // Public endpoint
+
+ this.ws = new WebSocket(wsUrl)
+
+ this.ws.onopen = () => {
+ console.log('🔧 NostrclientHub: WebSocket connected')
+ this._isConnected = true
+ this._isConnecting = false
+ this.reconnectAttempts = 0
+ this.emit('connected')
+
+ // Resubscribe to existing subscriptions
+ this.resubscribeAll()
+ }
+
+ this.ws.onmessage = (event) => {
+ this.handleMessage(event.data)
+ }
+
+ this.ws.onclose = (event) => {
+ console.log('🔧 NostrclientHub: WebSocket closed:', event.code, event.reason)
+ this._isConnected = false
+ this._isConnecting = false
+ this.emit('disconnected', event)
+
+ // Schedule reconnection
+ if (this.reconnectAttempts < this.maxReconnectAttempts) {
+ this.scheduleReconnect()
+ } else {
+ this.emit('maxReconnectionAttemptsReached')
+ }
+ }
+
+ this.ws.onerror = (error) => {
+ console.error('🔧 NostrclientHub: WebSocket error:', error)
+ this.emit('error', error)
+ }
+
+ } catch (error) {
+ this._isConnecting = false
+ console.error('🔧 NostrclientHub: Connection failed:', error)
+ this.emit('connectionError', error)
+
+ if (this.reconnectAttempts < this.maxReconnectAttempts) {
+ this.scheduleReconnect()
+ }
+ }
+ }
+
+ /**
+ * Disconnect from the WebSocket
+ */
+ disconnect(): void {
+ if (this.reconnectInterval) {
+ clearTimeout(this.reconnectInterval)
+ this.reconnectInterval = undefined
+ }
+
+ if (this.ws) {
+ this.ws.close()
+ this.ws = null
+ }
+
+ this._isConnected = false
+ this._isConnecting = false
+ this.subscriptions.clear()
+ this.emit('disconnected')
+ }
+
+ /**
+ * Subscribe to events
+ */
+ subscribe(config: SubscriptionConfig): () => void {
+ if (!this._isConnected) {
+ throw new Error('Not connected to nostrclient')
+ }
+
+ // Store subscription
+ this.subscriptions.set(config.id, config)
+
+ // Send REQ message
+ const reqMessage = JSON.stringify([
+ 'REQ',
+ config.id,
+ ...config.filters
+ ])
+
+ this.ws?.send(reqMessage)
+ console.log('🔧 NostrclientHub: Subscribed to', config.id)
+
+ // Return unsubscribe function
+ return () => {
+ this.unsubscribe(config.id)
+ }
+ }
+
+ /**
+ * Unsubscribe from events
+ */
+ unsubscribe(subscriptionId: string): void {
+ if (!this._isConnected) {
+ return
+ }
+
+ // Send CLOSE message
+ const closeMessage = JSON.stringify(['CLOSE', subscriptionId])
+ this.ws?.send(closeMessage)
+
+ // Remove from subscriptions
+ this.subscriptions.delete(subscriptionId)
+ console.log('🔧 NostrclientHub: Unsubscribed from', subscriptionId)
+ }
+
+ /**
+ * Publish an event
+ */
+ async publishEvent(event: Event): Promise {
+ if (!this._isConnected) {
+ throw new Error('Not connected to nostrclient')
+ }
+
+ const eventMessage = JSON.stringify(['EVENT', event])
+ this.ws?.send(eventMessage)
+
+ console.log('🔧 NostrclientHub: Published event', event.id)
+ this.emit('eventPublished', { eventId: event.id })
+ }
+
+ /**
+ * Query events (one-time fetch)
+ */
+ async queryEvents(filters: Filter[]): Promise {
+ return new Promise((resolve, reject) => {
+ if (!this._isConnected) {
+ reject(new Error('Not connected to nostrclient'))
+ return
+ }
+
+ const queryId = `query-${Date.now()}`
+ const events: Event[] = []
+ let eoseReceived = false
+
+ // Create temporary subscription for query
+ const tempSubscription = this.subscribe({
+ id: queryId,
+ filters,
+ onEvent: (event) => {
+ events.push(event)
+ },
+ onEose: () => {
+ eoseReceived = true
+ this.unsubscribe(queryId)
+ resolve(events)
+ },
+ onClose: () => {
+ if (!eoseReceived) {
+ reject(new Error('Query subscription closed unexpectedly'))
+ }
+ }
+ })
+
+ // Timeout after 30 seconds
+ setTimeout(() => {
+ if (!eoseReceived) {
+ tempSubscription()
+ reject(new Error('Query timeout'))
+ }
+ }, 30000)
+ })
+ }
+
+ /**
+ * Handle incoming WebSocket messages
+ */
+ private handleMessage(data: string): void {
+ try {
+ const message = JSON.parse(data)
+
+ if (Array.isArray(message) && message.length >= 2) {
+ const [type, subscriptionId, ...rest] = message
+
+ switch (type) {
+ case 'EVENT':
+ const event = rest[0] as Event
+ const subscription = this.subscriptions.get(subscriptionId)
+ if (subscription?.onEvent) {
+ subscription.onEvent(event)
+ }
+ this.emit('event', { subscriptionId, event })
+ break
+
+ case 'EOSE':
+ const eoseSubscription = this.subscriptions.get(subscriptionId)
+ if (eoseSubscription?.onEose) {
+ eoseSubscription.onEose()
+ }
+ this.emit('eose', { subscriptionId })
+ break
+
+ case 'NOTICE':
+ console.log('🔧 NostrclientHub: Notice from relay:', rest[0])
+ this.emit('notice', { message: rest[0] })
+ break
+
+ default:
+ console.log('🔧 NostrclientHub: Unknown message type:', type)
+ }
+ }
+ } catch (error) {
+ console.error('🔧 NostrclientHub: Failed to parse message:', error)
+ }
+ }
+
+ /**
+ * Resubscribe to all existing subscriptions after reconnection
+ */
+ private resubscribeAll(): void {
+ for (const [id, config] of this.subscriptions) {
+ const reqMessage = JSON.stringify([
+ 'REQ',
+ id,
+ ...config.filters
+ ])
+ this.ws?.send(reqMessage)
+ }
+ console.log('🔧 NostrclientHub: Resubscribed to', this.subscriptions.size, 'subscriptions')
+ }
+
+ /**
+ * Schedule automatic reconnection
+ */
+ private scheduleReconnect(): void {
+ if (this.reconnectInterval) {
+ clearTimeout(this.reconnectInterval)
+ }
+
+ const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1)
+ console.log(`🔧 NostrclientHub: Scheduling reconnection in ${delay}ms`)
+
+ this.reconnectInterval = setTimeout(async () => {
+ await this.connect()
+ }, delay) as unknown as number
+ }
+}
+
+// Export singleton instance
+export const nostrclientHub = new NostrclientHub({
+ url: import.meta.env.VITE_NOSTRCLIENT_URL || 'wss://localhost:5000/nostrclient/api/v1'
+})
+
+// Ensure global export
+;(globalThis as any).nostrclientHub = nostrclientHub
diff --git a/src/modules/base/nostr/relay-hub.ts b/src/modules/base/nostr/relay-hub.ts
new file mode 100644
index 0000000..162911e
--- /dev/null
+++ b/src/modules/base/nostr/relay-hub.ts
@@ -0,0 +1,524 @@
+import { SimplePool, type Filter, type Event, type Relay } from 'nostr-tools'
+
+// Simple EventEmitter implementation for browser compatibility
+class EventEmitter {
+ private events: { [key: string]: Function[] } = {}
+
+ on(event: string, listener: Function): void {
+ if (!this.events[event]) {
+ this.events[event] = []
+ }
+ this.events[event].push(listener)
+ }
+
+ off(event: string, listener: Function): void {
+ if (!this.events[event]) return
+ const index = this.events[event].indexOf(listener)
+ if (index > -1) {
+ this.events[event].splice(index, 1)
+ }
+ }
+
+ emit(event: string, ...args: any[]): void {
+ if (!this.events[event]) return
+ this.events[event].forEach(listener => listener(...args))
+ }
+
+ removeAllListeners(event?: string): void {
+ if (event) {
+ delete this.events[event]
+ } else {
+ this.events = {}
+ }
+ }
+}
+
+export interface RelayConfig {
+ url: string
+ read: boolean
+ write: boolean
+ priority?: number // Lower number = higher priority
+}
+
+export interface SubscriptionConfig {
+ id: string
+ filters: Filter[]
+ relays?: string[] // If not specified, uses all connected relays
+ onEvent?: (event: Event) => void
+ onEose?: () => void
+ onClose?: () => void
+}
+
+export interface RelayStatus {
+ url: string
+ connected: boolean
+ lastSeen: number
+ error?: string
+ latency?: number
+}
+
+export class RelayHub extends EventEmitter {
+ private pool: SimplePool
+ private relayConfigs: Map = new Map()
+ private connectedRelays: Map = new Map()
+ private subscriptions: Map = new Map()
+ public isInitialized = false
+ private reconnectInterval?: number
+ private healthCheckInterval?: number
+ private mobileVisibilityHandler?: () => void
+
+ // Connection state
+ private _isConnected = false
+ private _connectionAttempts = 0
+ private readonly maxReconnectAttempts = 5
+ private readonly reconnectDelay = 5000 // 5 seconds
+ private readonly healthCheckIntervalMs = 30000 // 30 seconds
+
+ constructor() {
+ super()
+ this.pool = new SimplePool()
+ this.setupMobileVisibilityHandling()
+ }
+
+ get isConnected(): boolean {
+ return this._isConnected
+ }
+
+ get connectedRelayCount(): number {
+ // Return the actual size of connectedRelays map
+ return this.connectedRelays.size
+ }
+
+ get totalRelayCount(): number {
+ return this.relayConfigs.size
+ }
+
+ get totalSubscriptionCount(): number {
+ return this.subscriptions.size
+ }
+
+ get subscriptionDetails(): Array<{ id: string; filters: any[]; relays?: string[] }> {
+ return Array.from(this.subscriptions.entries()).map(([id, subscription]) => {
+ // Try to extract subscription details if available
+ return {
+ id,
+ filters: subscription.filters || [],
+ relays: subscription.relays || []
+ }
+ })
+ }
+
+ get relayStatuses(): RelayStatus[] {
+ return Array.from(this.relayConfigs.values()).map(config => {
+ const relay = this.connectedRelays.get(config.url)
+ return {
+ url: config.url,
+ connected: !!relay,
+ lastSeen: relay ? Date.now() : 0,
+ error: relay ? undefined : 'Not connected',
+ latency: relay ? 0 : undefined // TODO: Implement actual latency measurement
+ }
+ })
+ }
+
+ /**
+ * Initialize the relay hub with relay configurations
+ */
+ async initialize(relayUrls: string[]): Promise {
+ if (this.isInitialized) {
+ console.warn('RelayHub already initialized')
+ return
+ }
+
+ console.log('🔧 RelayHub: Initializing with URLs:', relayUrls)
+
+ // Convert URLs to relay configs
+ this.relayConfigs.clear()
+ relayUrls.forEach((url, index) => {
+ this.relayConfigs.set(url, {
+ url,
+ read: true,
+ write: true,
+ priority: index
+ })
+ })
+
+ console.log('🔧 RelayHub: Relay configs created:', Array.from(this.relayConfigs.values()))
+
+ // Start connection management
+ console.log('🔧 RelayHub: Starting connection...')
+ await this.connect()
+ this.startHealthCheck()
+ this.isInitialized = true
+ console.log('🔧 RelayHub: Initialization complete')
+ }
+
+ /**
+ * Connect to all configured relays
+ */
+ async connect(): Promise {
+ if (this.relayConfigs.size === 0) {
+ throw new Error('No relay configurations found. Call initialize() first.')
+ }
+
+ console.log('🔧 RelayHub: Connecting to', this.relayConfigs.size, 'relays')
+
+ try {
+ this._connectionAttempts++
+ console.log('🔧 RelayHub: Connection attempt', this._connectionAttempts)
+
+ // Connect to relays in priority order
+ const sortedRelays = Array.from(this.relayConfigs.values())
+ .sort((a, b) => (a.priority || 0) - (b.priority || 0))
+
+ console.log('🔧 RelayHub: Attempting connections to:', sortedRelays.map(r => r.url))
+
+ const connectionPromises = sortedRelays.map(async (config) => {
+ try {
+ console.log('🔧 RelayHub: Connecting to relay:', config.url)
+ const relay = await this.pool.ensureRelay(config.url)
+ this.connectedRelays.set(config.url, relay)
+ console.log('🔧 RelayHub: Successfully connected to:', config.url)
+
+ return { url: config.url, success: true }
+ } catch (error) {
+ console.error(`🔧 RelayHub: Failed to connect to relay ${config.url}:`, error)
+ return { url: config.url, success: false, error }
+ }
+ })
+
+ const results = await Promise.allSettled(connectionPromises)
+ const successfulConnections = results.filter(
+ result => result.status === 'fulfilled' && result.value.success
+ )
+
+ console.log('🔧 RelayHub: Connection results:', {
+ total: results.length,
+ successful: successfulConnections.length,
+ failed: results.length - successfulConnections.length
+ })
+
+ if (successfulConnections.length > 0) {
+ this._isConnected = true
+ this._connectionAttempts = 0
+ console.log('🔧 RelayHub: Connection successful, connected to', successfulConnections.length, 'relays')
+ this.emit('connected', successfulConnections.length)
+
+ } else {
+ console.error('🔧 RelayHub: Failed to connect to any relay')
+ throw new Error('Failed to connect to any relay')
+ }
+ } catch (error) {
+ this._isConnected = false
+ console.error('🔧 RelayHub: Connection failed with error:', error)
+ this.emit('connectionError', error)
+
+ // Schedule reconnection if we haven't exceeded max attempts
+ if (this._connectionAttempts < this.maxReconnectAttempts) {
+ console.log('🔧 RelayHub: Scheduling reconnection attempt', this._connectionAttempts + 1)
+ this.scheduleReconnect()
+ } else {
+ this.emit('maxReconnectionAttemptsReached')
+ console.error('🔧 RelayHub: Max reconnection attempts reached')
+ }
+ }
+ }
+
+ /**
+ * Disconnect from all relays
+ */
+ disconnect(): void {
+
+
+ // Clear intervals
+ if (this.reconnectInterval) {
+ clearTimeout(this.reconnectInterval)
+ this.reconnectInterval = undefined
+ }
+
+ if (this.healthCheckInterval) {
+ clearInterval(this.healthCheckInterval)
+ this.healthCheckInterval = undefined
+ }
+
+ // Close all subscriptions
+ this.subscriptions.forEach(sub => sub.close())
+ this.subscriptions.clear()
+
+ // Close all relay connections
+ this.pool.close(Array.from(this.relayConfigs.keys()))
+ this.connectedRelays.clear()
+
+ this._isConnected = false
+ this.emit('disconnected')
+ }
+
+ /**
+ * Subscribe to events from relays
+ */
+ subscribe(config: SubscriptionConfig): () => void {
+ if (!this.isInitialized) {
+ throw new Error('RelayHub not initialized. Call initialize() first.')
+ }
+
+ if (!this._isConnected) {
+ throw new Error('Not connected to any relays')
+ }
+
+ // Determine which relays to use
+ const targetRelays = config.relays || Array.from(this.connectedRelays.keys())
+ const availableRelays = targetRelays.filter(url => this.connectedRelays.has(url))
+
+ if (availableRelays.length === 0) {
+ throw new Error('No available relays for subscription')
+ }
+
+
+
+ // Create subscription using the pool
+ const subscription = this.pool.subscribeMany(availableRelays, config.filters, {
+ onevent: (event: Event) => {
+ config.onEvent?.(event)
+ this.emit('event', { subscriptionId: config.id, event, relay: 'unknown' })
+ },
+ oneose: () => {
+ config.onEose?.()
+ this.emit('eose', { subscriptionId: config.id })
+ }
+ })
+
+ // Store subscription for cleanup
+ this.subscriptions.set(config.id, subscription)
+
+ // Emit subscription created event
+ this.emit('subscriptionCreated', { id: config.id, count: this.subscriptions.size })
+
+ // Return unsubscribe function
+ return () => {
+ this.unsubscribe(config.id)
+ }
+ }
+
+ /**
+ * Unsubscribe from a specific subscription
+ */
+ unsubscribe(subscriptionId: string): void {
+ const subscription = this.subscriptions.get(subscriptionId)
+ if (subscription) {
+ subscription.close()
+ this.subscriptions.delete(subscriptionId)
+
+
+ // Emit subscription removed event
+ this.emit('subscriptionRemoved', { id: subscriptionId, count: this.subscriptions.size })
+ }
+ }
+
+ /**
+ * Publish an event to all connected relays
+ */
+ async publishEvent(event: Event): Promise<{ success: number; total: number }> {
+ if (!this._isConnected) {
+ throw new Error('Not connected to any relays')
+ }
+
+ const relayUrls = Array.from(this.connectedRelays.keys())
+ const results = await Promise.allSettled(
+ relayUrls.map(relay => this.pool.publish([relay], event))
+ )
+
+ const successful = results.filter(result => result.status === 'fulfilled').length
+ const total = results.length
+
+
+ this.emit('eventPublished', { eventId: event.id, success: successful, total })
+
+ return { success: successful, total }
+ }
+
+ /**
+ * Query events from relays (one-time fetch)
+ */
+ async queryEvents(filters: Filter[], relays?: string[]): Promise {
+ if (!this._isConnected) {
+ throw new Error('Not connected to any relays')
+ }
+
+ const targetRelays = relays || Array.from(this.connectedRelays.keys())
+ const availableRelays = targetRelays.filter(url => this.connectedRelays.has(url))
+
+ if (availableRelays.length === 0) {
+ throw new Error('No available relays for query')
+ }
+
+ try {
+ // Query each filter separately and combine results
+ const allEvents: Event[] = []
+ for (const filter of filters) {
+ const events = await this.pool.querySync(availableRelays, filter)
+ allEvents.push(...events)
+ }
+
+ return allEvents
+ } catch (error) {
+ console.error('Query failed:', error)
+ throw error
+ }
+ }
+
+ /**
+ * Get a specific relay instance
+ */
+ getRelay(url: string): Relay | undefined {
+ return this.connectedRelays.get(url)
+ }
+
+ /**
+ * Check if a specific relay is connected
+ */
+ isRelayConnected(url: string): boolean {
+ return this.connectedRelays.has(url)
+ }
+
+ /**
+ * Force reconnection to all relays
+ */
+ async reconnect(): Promise {
+
+ this.disconnect()
+ await this.connect()
+ }
+
+ /**
+ * Schedule automatic reconnection
+ */
+ private scheduleReconnect(): void {
+ if (this.reconnectInterval) {
+ clearTimeout(this.reconnectInterval)
+ }
+
+ this.reconnectInterval = setTimeout(async () => {
+
+ await this.connect()
+ }, this.reconnectDelay) as unknown as number
+ }
+
+ /**
+ * Start health check monitoring
+ */
+ private startHealthCheck(): void {
+ if (this.healthCheckInterval) {
+ clearInterval(this.healthCheckInterval)
+ }
+
+ this.healthCheckInterval = setInterval(() => {
+ this.performHealthCheck()
+ }, this.healthCheckIntervalMs) as unknown as number
+ }
+
+ /**
+ * Perform health check on all relays
+ */
+ private async performHealthCheck(): Promise {
+ if (!this._isConnected) return
+
+
+ const disconnectedRelays: string[] = []
+
+ // Check each relay connection
+ for (const [url] of this.connectedRelays) {
+ try {
+ // Try to send a ping or check connection status
+ // For now, we'll just check if the relay is still in our connected relays map
+ if (!this.connectedRelays.has(url)) {
+ disconnectedRelays.push(url)
+ }
+ } catch (error) {
+ console.warn(`Health check failed for relay ${url}:`, error)
+ disconnectedRelays.push(url)
+ }
+ }
+
+ // Remove disconnected relays
+ disconnectedRelays.forEach(url => {
+ this.connectedRelays.delete(url)
+
+ })
+
+ // Update connection status
+ if (this.connectedRelays.size === 0) {
+ this._isConnected = false
+ this.emit('allRelaysDisconnected')
+ console.warn('All relays disconnected, attempting reconnection...')
+ await this.connect()
+ } else if (this.connectedRelays.size < this.relayConfigs.size) {
+ this.emit('partialDisconnection', {
+ connected: this.connectedRelays.size,
+ total: this.relayConfigs.size
+ })
+ }
+ }
+
+ /**
+ * Setup mobile visibility handling for better WebSocket management
+ */
+ private setupMobileVisibilityHandling(): void {
+ // Handle page visibility changes (mobile app backgrounding)
+ if (typeof document !== 'undefined') {
+ this.mobileVisibilityHandler = () => {
+ if (document.hidden) {
+
+ // Keep connections alive but reduce activity
+ } else {
+ console.log('Page visible, resuming normal WebSocket activity')
+ // Resume normal activity and check connections
+ this.performHealthCheck()
+ }
+ }
+
+ document.addEventListener('visibilitychange', this.mobileVisibilityHandler)
+ }
+
+ // Handle online/offline events
+ if (typeof window !== 'undefined') {
+ window.addEventListener('online', () => {
+
+ this.performHealthCheck()
+ })
+
+ window.addEventListener('offline', () => {
+ console.log('Network offline, marking as disconnected...')
+ this._isConnected = false
+ this.emit('networkOffline')
+ })
+ }
+ }
+
+ /**
+ * Cleanup resources
+ */
+ destroy(): void {
+
+
+ // Remove event listeners
+ if (this.mobileVisibilityHandler && typeof document !== 'undefined') {
+ document.removeEventListener('visibilitychange', this.mobileVisibilityHandler)
+ }
+
+ if (typeof window !== 'undefined') {
+ window.removeEventListener('online', () => {})
+ window.removeEventListener('offline', () => {})
+ }
+
+ // Disconnect and cleanup
+ this.disconnect()
+ this.removeAllListeners()
+ this.isInitialized = false
+ }
+}
+
+// Export singleton instance
+export const relayHub = new RelayHub()
+
+// Ensure global export
+;(globalThis as any).relayHub = relayHub
diff --git a/src/modules/base/pwa/pwa-service.ts b/src/modules/base/pwa/pwa-service.ts
new file mode 100644
index 0000000..c727b9e
--- /dev/null
+++ b/src/modules/base/pwa/pwa-service.ts
@@ -0,0 +1,45 @@
+// PWA service for base module
+export class PWAService {
+ private deferredPrompt: any = null
+
+ async initialize(): Promise {
+ console.log('📱 Initializing PWA service...')
+
+ // Listen for beforeinstallprompt event
+ window.addEventListener('beforeinstallprompt', (e) => {
+ console.log('PWA install prompt available')
+ e.preventDefault()
+ this.deferredPrompt = e
+ })
+
+ // Listen for app installed event
+ window.addEventListener('appinstalled', () => {
+ console.log('PWA was installed')
+ this.deferredPrompt = null
+ })
+ }
+
+ canInstall(): boolean {
+ return this.deferredPrompt !== null
+ }
+
+ async install(): Promise {
+ if (!this.deferredPrompt) {
+ return false
+ }
+
+ this.deferredPrompt.prompt()
+ const result = await this.deferredPrompt.userChoice
+
+ if (result.outcome === 'accepted') {
+ console.log('User accepted PWA install')
+ } else {
+ console.log('User dismissed PWA install')
+ }
+
+ this.deferredPrompt = null
+ return result.outcome === 'accepted'
+ }
+}
+
+export const pwaService = new PWAService()
\ No newline at end of file
diff --git a/src/modules/nostr-feed/components/NostrFeed.vue b/src/modules/nostr-feed/components/NostrFeed.vue
new file mode 100644
index 0000000..1723445
--- /dev/null
+++ b/src/modules/nostr-feed/components/NostrFeed.vue
@@ -0,0 +1,336 @@
+
+
+
+
+
+
+
+
+
+ {{ feedTitle }}
+ {{ feedDescription }}
+
+
+
+
+
+
+
+
+
+
+
+
Not connected to relays
+
+
+
+
+
+
+
+ Loading announcements...
+
+
+
+
+
+
+
+
Failed to load announcements
+
+
{{ error.message }}
+
+
+
+
+
+
+
+ No admin pubkeys configured
+
+
+ Community announcements will appear here once admin pubkeys are configured.
+
+
+
+
+
+
+
+ No announcements yet
+
+
+ Check back later for community updates and announcements.
+
+
+
+
+
+
+
+
+
+
+
+ Admin
+
+
+ Reply
+
+
+ {{ formatDistanceToNow(note.created_at * 1000, { addSuffix: true }) }}
+
+
+
+
+
+
+ {{ note.content }}
+
+
+
+
+
+ Mentions:
+
+ {{ mention.slice(0, 8) }}...
+
+
+ +{{ note.mentions.length - 3 }} more
+
+
+
+
+
+
+
+
+
diff --git a/src/modules/nostr-feed/composables/useFeed.ts b/src/modules/nostr-feed/composables/useFeed.ts
new file mode 100644
index 0000000..91259cd
--- /dev/null
+++ b/src/modules/nostr-feed/composables/useFeed.ts
@@ -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(SERVICE_TOKENS.RELAY_HUB)
+ const posts = ref([])
+ const isLoading = ref(false)
+ const error = ref(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
+ }
+}
\ No newline at end of file
diff --git a/src/modules/nostr-feed/index.ts b/src/modules/nostr-feed/index.ts
new file mode 100644
index 0000000..95f8958
--- /dev/null
+++ b/src/modules/nostr-feed/index.ts
@@ -0,0 +1,47 @@
+import type { App } from 'vue'
+import type { ModulePlugin } from '@/core/types'
+import type { RouteRecordRaw } from 'vue-router'
+import NostrFeed from './components/NostrFeed.vue'
+import { useFeed } from './composables/useFeed'
+
+/**
+ * Nostr Feed Module Plugin
+ * Provides social feed functionality with admin announcements support
+ */
+export const nostrFeedModule: ModulePlugin = {
+ name: 'nostr-feed',
+ version: '1.0.0',
+ dependencies: ['base'],
+
+ async install(app: App, _options?: any) {
+ console.log('📰 Installing nostr-feed module...')
+
+ // Register global components
+ app.component('NostrFeed', NostrFeed)
+
+ // Module-specific initialization
+ console.log('✅ Nostr-feed module installed successfully')
+ },
+
+ async uninstall() {
+ console.log('🗑️ Uninstalling nostr-feed module...')
+ // Cleanup if needed
+ console.log('✅ Nostr-feed module uninstalled')
+ },
+
+ // Routes - currently none, but feed could have its own page
+ routes: [] as RouteRecordRaw[],
+
+ components: {
+ NostrFeed
+ },
+
+ composables: {
+ useFeed
+ },
+
+ // Services that other modules can use
+ services: {}
+}
+
+export default nostrFeedModule
\ No newline at end of file