diff --git a/CLAUDE.md b/CLAUDE.md
index 2ef2826..6fd5d02 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -10,7 +10,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
- `npm run preview` - Preview production build locally
- `npm run analyze` - Build with bundle analysis (opens visualization)
-**Electron Development**
+**Electron Development**
- `npm run electron:dev` - Run both Vite dev server and Electron concurrently
- `npm run electron:build` - Full build and package for Electron
- `npm run start` - Start Electron using Forge
@@ -26,7 +26,7 @@ This is a modular Vue 3 + TypeScript + Vite application with Electron support, f
The application uses a plugin-based modular architecture with dependency injection for service management:
**Core Modules:**
-- **Base Module** (`src/modules/base/`) - Core infrastructure (Nostr, Auth, PWA, Image Upload)
+- **Base Module** (`src/modules/base/`) - Core infrastructure (Nostr, Auth, PWA)
- **Wallet Module** (`src/modules/wallet/`) - Lightning wallet management with real-time balance updates
- **Nostr Feed Module** (`src/modules/nostr-feed/`) - Social feed functionality
- **Chat Module** (`src/modules/chat/`) - Encrypted Nostr chat
@@ -90,12 +90,6 @@ const relayHub = injectService(SERVICE_TOKENS.RELAY_HUB)
- `SERVICE_TOKENS.VISIBILITY_SERVICE` - App visibility and connection management
- `SERVICE_TOKENS.WALLET_SERVICE` - Wallet operations (send, receive, transactions)
- `SERVICE_TOKENS.WALLET_WEBSOCKET_SERVICE` - Real-time wallet balance updates via WebSocket
-- `SERVICE_TOKENS.STORAGE_SERVICE` - Local storage management
-- `SERVICE_TOKENS.TOAST_SERVICE` - Toast notification system
-- `SERVICE_TOKENS.INVOICE_SERVICE` - Lightning invoice creation and management
-- `SERVICE_TOKENS.LNBITS_API` - LNbits API client
-- `SERVICE_TOKENS.IMAGE_UPLOAD_SERVICE` - Image upload to pictrs server
-- `SERVICE_TOKENS.NOSTR_METADATA_SERVICE` - Nostr user metadata (NIP-01 kind 0)
**Core Stack:**
- Vue 3 with Composition API (`
-
-
-
-
-
-
diff --git a/src/components/ui/alert-dialog/AlertDialogAction.vue b/src/components/ui/alert-dialog/AlertDialogAction.vue
deleted file mode 100644
index 09cf6fc..0000000
--- a/src/components/ui/alert-dialog/AlertDialogAction.vue
+++ /dev/null
@@ -1,18 +0,0 @@
-
-
-
-
-
-
-
diff --git a/src/components/ui/alert-dialog/AlertDialogCancel.vue b/src/components/ui/alert-dialog/AlertDialogCancel.vue
deleted file mode 100644
index e261894..0000000
--- a/src/components/ui/alert-dialog/AlertDialogCancel.vue
+++ /dev/null
@@ -1,25 +0,0 @@
-
-
-
-
-
-
-
diff --git a/src/components/ui/alert-dialog/AlertDialogContent.vue b/src/components/ui/alert-dialog/AlertDialogContent.vue
deleted file mode 100644
index 73e4fcb..0000000
--- a/src/components/ui/alert-dialog/AlertDialogContent.vue
+++ /dev/null
@@ -1,39 +0,0 @@
-
-
-
-
-
-
-
-
-
-
diff --git a/src/components/ui/alert-dialog/AlertDialogDescription.vue b/src/components/ui/alert-dialog/AlertDialogDescription.vue
deleted file mode 100644
index b6d165e..0000000
--- a/src/components/ui/alert-dialog/AlertDialogDescription.vue
+++ /dev/null
@@ -1,23 +0,0 @@
-
-
-
-
-
-
-
diff --git a/src/components/ui/alert-dialog/AlertDialogFooter.vue b/src/components/ui/alert-dialog/AlertDialogFooter.vue
deleted file mode 100644
index c764e73..0000000
--- a/src/components/ui/alert-dialog/AlertDialogFooter.vue
+++ /dev/null
@@ -1,21 +0,0 @@
-
-
-
-
-
-
-
diff --git a/src/components/ui/alert-dialog/AlertDialogHeader.vue b/src/components/ui/alert-dialog/AlertDialogHeader.vue
deleted file mode 100644
index b5e5540..0000000
--- a/src/components/ui/alert-dialog/AlertDialogHeader.vue
+++ /dev/null
@@ -1,16 +0,0 @@
-
-
-
-
-
-
-
diff --git a/src/components/ui/alert-dialog/AlertDialogTitle.vue b/src/components/ui/alert-dialog/AlertDialogTitle.vue
deleted file mode 100644
index b829392..0000000
--- a/src/components/ui/alert-dialog/AlertDialogTitle.vue
+++ /dev/null
@@ -1,20 +0,0 @@
-
-
-
-
-
-
-
diff --git a/src/components/ui/alert-dialog/AlertDialogTrigger.vue b/src/components/ui/alert-dialog/AlertDialogTrigger.vue
deleted file mode 100644
index f104dcc..0000000
--- a/src/components/ui/alert-dialog/AlertDialogTrigger.vue
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
-
-
diff --git a/src/components/ui/alert-dialog/index.ts b/src/components/ui/alert-dialog/index.ts
deleted file mode 100644
index cf1b45d..0000000
--- a/src/components/ui/alert-dialog/index.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-export { default as AlertDialog } from "./AlertDialog.vue"
-export { default as AlertDialogAction } from "./AlertDialogAction.vue"
-export { default as AlertDialogCancel } from "./AlertDialogCancel.vue"
-export { default as AlertDialogContent } from "./AlertDialogContent.vue"
-export { default as AlertDialogDescription } from "./AlertDialogDescription.vue"
-export { default as AlertDialogFooter } from "./AlertDialogFooter.vue"
-export { default as AlertDialogHeader } from "./AlertDialogHeader.vue"
-export { default as AlertDialogTitle } from "./AlertDialogTitle.vue"
-export { default as AlertDialogTrigger } from "./AlertDialogTrigger.vue"
diff --git a/src/components/ui/button/Button.vue b/src/components/ui/button/Button.vue
index 374320b..17dc84d 100644
--- a/src/components/ui/button/Button.vue
+++ b/src/components/ui/button/Button.vue
@@ -1,25 +1,22 @@
svg]:px-3",
- "sm": "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
- "lg": "h-10 rounded-md px-6 has-[>svg]:px-4",
- "icon": "size-9",
- "icon-sm": "size-8",
- "icon-lg": "size-10",
+ default: 'h-9 px-4 py-2',
+ xs: 'h-7 rounded px-2',
+ sm: 'h-8 rounded-md px-3 text-xs',
+ lg: 'h-10 rounded-md px-8',
+ icon: 'h-9 w-9',
},
},
defaultVariants: {
- variant: "default",
- size: "default",
+ variant: 'default',
+ size: 'default',
},
},
)
diff --git a/src/composables/useModularNavigation.ts b/src/composables/useModularNavigation.ts
index 5ff695f..af47f66 100644
--- a/src/composables/useModularNavigation.ts
+++ b/src/composables/useModularNavigation.ts
@@ -61,40 +61,30 @@ export function useModularNavigation() {
// Events module items
if (appConfig.modules.events.enabled) {
- items.push({
- name: 'My Tickets',
- href: '/my-tickets',
+ items.push({
+ name: 'My Tickets',
+ href: '/my-tickets',
icon: 'Ticket',
- requiresAuth: true
+ requiresAuth: true
})
}
- // Market module items
+ // Market module items
if (appConfig.modules.market.enabled) {
- items.push({
- name: 'Market Dashboard',
- href: '/market-dashboard',
+ items.push({
+ name: 'Market Dashboard',
+ href: '/market-dashboard',
icon: 'Store',
- requiresAuth: true
- })
- }
-
- // Expenses module items
- if (appConfig.modules.expenses.enabled) {
- items.push({
- name: 'My Transactions',
- href: '/expenses/transactions',
- icon: 'Receipt',
- requiresAuth: true
+ requiresAuth: true
})
}
// Base module items (always available)
- items.push({
- name: 'Relay Hub Status',
- href: '/relay-hub-status',
+ items.push({
+ name: 'Relay Hub Status',
+ href: '/relay-hub-status',
icon: 'Activity',
- requiresAuth: true
+ requiresAuth: true
})
return items
diff --git a/src/composables/useQuickActions.ts b/src/composables/useQuickActions.ts
deleted file mode 100644
index 7046fbb..0000000
--- a/src/composables/useQuickActions.ts
+++ /dev/null
@@ -1,95 +0,0 @@
-import { computed } from 'vue'
-import { pluginManager } from '@/core/plugin-manager'
-import type { QuickAction } from '@/core/types'
-
-/**
- * Composable for dynamic quick actions based on enabled modules
- *
- * Quick actions are module-provided action buttons that appear in the floating
- * action button (FAB) menu. Each module can register its own quick actions
- * for common tasks like composing notes, sending payments, adding expenses, etc.
- *
- * @example
- * ```typescript
- * const { quickActions, getActionsByCategory } = useQuickActions()
- *
- * // Get all actions
- * const actions = quickActions.value
- *
- * // Get actions by category
- * const composeActions = getActionsByCategory('compose')
- * ```
- */
-export function useQuickActions() {
- /**
- * Get all quick actions from installed modules
- * Actions are sorted by order (lower = higher priority)
- */
- const quickActions = computed(() => {
- const actions: QuickAction[] = []
-
- // Iterate through installed modules
- const installedModules = pluginManager.getInstalledModules()
-
- for (const moduleName of installedModules) {
- const module = pluginManager.getModule(moduleName)
- if (module?.plugin.quickActions) {
- actions.push(...module.plugin.quickActions)
- }
- }
-
- // Sort by order (lower = higher priority), then by label
- return actions.sort((a, b) => {
- const orderA = a.order ?? 999
- const orderB = b.order ?? 999
- if (orderA !== orderB) {
- return orderA - orderB
- }
- return a.label.localeCompare(b.label)
- })
- })
-
- /**
- * Get actions filtered by category
- */
- const getActionsByCategory = (category: string) => {
- return computed(() => {
- return quickActions.value.filter(action => action.category === category)
- })
- }
-
- /**
- * Get a specific action by ID
- */
- const getActionById = (id: string) => {
- return computed(() => {
- return quickActions.value.find(action => action.id === id)
- })
- }
-
- /**
- * Check if any actions are available
- */
- const hasActions = computed(() => quickActions.value.length > 0)
-
- /**
- * Get all unique categories
- */
- const categories = computed(() => {
- const cats = new Set()
- quickActions.value.forEach(action => {
- if (action.category) {
- cats.add(action.category)
- }
- })
- return Array.from(cats).sort()
- })
-
- return {
- quickActions,
- getActionsByCategory,
- getActionById,
- hasActions,
- categories
- }
-}
diff --git a/src/core/di-container.ts b/src/core/di-container.ts
index 0d27524..32221f5 100644
--- a/src/core/di-container.ts
+++ b/src/core/di-container.ts
@@ -136,7 +136,6 @@ export const SERVICE_TOKENS = {
FEED_SERVICE: Symbol('feedService'),
PROFILE_SERVICE: Symbol('profileService'),
REACTION_SERVICE: Symbol('reactionService'),
- SCHEDULED_EVENT_SERVICE: Symbol('scheduledEventService'),
// Nostr metadata services
NOSTR_METADATA_SERVICE: Symbol('nostrMetadataService'),
@@ -160,9 +159,6 @@ export const SERVICE_TOKENS = {
// Image upload services
IMAGE_UPLOAD_SERVICE: Symbol('imageUploadService'),
-
- // Expenses services
- EXPENSES_API: Symbol('expensesAPI'),
} as const
// Type-safe injection helpers
diff --git a/src/core/types.ts b/src/core/types.ts
index c866653..9ce1e47 100644
--- a/src/core/types.ts
+++ b/src/core/types.ts
@@ -1,64 +1,37 @@
import type { App, Component } from 'vue'
import type { RouteRecordRaw } from 'vue-router'
-// Quick action interface for modular action buttons
-export interface QuickAction {
- /** Unique action ID */
- id: string
-
- /** Display label for the action */
- label: string
-
- /** Lucide icon name */
- icon: string
-
- /** Component to render when action is selected */
- component: Component
-
- /** Display order (lower = higher priority) */
- order?: number
-
- /** Action category (e.g., 'compose', 'wallet', 'utilities') */
- category?: string
-
- /** Whether action requires authentication */
- requiresAuth?: boolean
-}
-
// 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
-
- /** Quick actions provided by this module */
- quickActions?: QuickAction[]
}
// Module configuration for app setup
diff --git a/src/modules/base/nostr/relay-hub.ts b/src/modules/base/nostr/relay-hub.ts
index 7cdd00f..486d3f2 100644
--- a/src/modules/base/nostr/relay-hub.ts
+++ b/src/modules/base/nostr/relay-hub.ts
@@ -540,12 +540,8 @@ export class RelayHub extends BaseService {
const successful = results.filter(result => result.status === 'fulfilled').length
const total = results.length
- this.emit('eventPublished', { eventId: event.id, success: successful, total })
- // Throw error if no relays accepted the event
- if (successful === 0) {
- throw new Error(`Failed to publish event - none of the ${total} relay(s) accepted it`)
- }
+ this.emit('eventPublished', { eventId: event.id, success: successful, total })
return { success: successful, total }
}
diff --git a/src/modules/expenses/components/AccountSelector.vue b/src/modules/expenses/components/AccountSelector.vue
deleted file mode 100644
index 2d92207..0000000
--- a/src/modules/expenses/components/AccountSelector.vue
+++ /dev/null
@@ -1,270 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Loading accounts...
-
-
-
-
-
-
{{ error }}
-
-
-
-
-
-
-
-
-
-
-
-
No accounts available
-
-
-
-
-
-
-
-
-
Selected Account
-
{{ selectedAccount.name }}
-
-
-
{{ selectedAccount.account_type }}
-
-
-
-
-
diff --git a/src/modules/expenses/components/AddExpense.vue b/src/modules/expenses/components/AddExpense.vue
deleted file mode 100644
index 99f3462..0000000
--- a/src/modules/expenses/components/AddExpense.vue
+++ /dev/null
@@ -1,469 +0,0 @@
-
-
-
-
-
diff --git a/src/modules/expenses/components/admin/GrantPermissionDialog.vue b/src/modules/expenses/components/admin/GrantPermissionDialog.vue
deleted file mode 100644
index 20e7d87..0000000
--- a/src/modules/expenses/components/admin/GrantPermissionDialog.vue
+++ /dev/null
@@ -1,256 +0,0 @@
-
-
-
-
-
diff --git a/src/modules/expenses/components/admin/PermissionManager.vue b/src/modules/expenses/components/admin/PermissionManager.vue
deleted file mode 100644
index 99eac98..0000000
--- a/src/modules/expenses/components/admin/PermissionManager.vue
+++ /dev/null
@@ -1,399 +0,0 @@
-
-
-
-
-
-
-
Permission Management
-
- Manage user access to expense accounts
-
-
-
-
-
-
-
-
-
- Account Permissions
-
-
- View and manage all account permissions. Permissions on parent accounts cascade to
- children.
-
-
-
-
-
-
-
- By User
-
-
-
- By Account
-
-
-
-
-
-
-
-
-
-
-
No permissions granted yet
-
-
-
-
-
User: {{ userId }}
-
-
-
- Account
- Permission
- Granted
- Expires
- Notes
- Actions
-
-
-
-
-
- {{ getAccountName(permission.account_id) }}
-
-
-
- {{ getPermissionLabel(permission.permission_type) }}
-
-
- {{ formatDate(permission.granted_at) }}
-
- {{ permission.expires_at ? formatDate(permission.expires_at) : 'Never' }}
-
-
-
- {{ permission.notes || '-' }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
No permissions granted yet
-
-
-
-
-
Account: {{ getAccountName(accountId) }}
-
-
-
- User
- Permission
- Granted
- Expires
- Notes
- Actions
-
-
-
-
- {{ permission.user_id }}
-
-
- {{ getPermissionLabel(permission.permission_type) }}
-
-
- {{ formatDate(permission.granted_at) }}
-
- {{ permission.expires_at ? formatDate(permission.expires_at) : 'Never' }}
-
-
-
- {{ permission.notes || '-' }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Revoke Permission?
-
- Are you sure you want to revoke this permission? The user will immediately lose access.
-
-
Permission Details:
-
- User: {{ permissionToRevoke.user_id }}
-
-
- Account: {{ getAccountName(permissionToRevoke.account_id) }}
-
-
- Type: {{ getPermissionLabel(permissionToRevoke.permission_type) }}
-
-
-
-
-
- Cancel
-
- Revoke
-
-
-
-
-
-
diff --git a/src/modules/expenses/index.ts b/src/modules/expenses/index.ts
deleted file mode 100644
index 1ee1f3c..0000000
--- a/src/modules/expenses/index.ts
+++ /dev/null
@@ -1,71 +0,0 @@
-/**
- * Expenses Module
- *
- * Provides expense tracking and submission functionality
- * integrated with castle LNbits extension.
- */
-
-import type { App } from 'vue'
-import { markRaw } from 'vue'
-import type { ModulePlugin } from '@/core/types'
-import { container, SERVICE_TOKENS } from '@/core/di-container'
-import { ExpensesAPI } from './services/ExpensesAPI'
-import AddExpense from './components/AddExpense.vue'
-import TransactionsPage from './views/TransactionsPage.vue'
-
-export const expensesModule: ModulePlugin = {
- name: 'expenses',
- version: '1.0.0',
- dependencies: ['base'],
-
- routes: [
- {
- path: '/expenses/transactions',
- name: 'ExpenseTransactions',
- component: TransactionsPage,
- meta: {
- requiresAuth: true,
- title: 'My Transactions'
- }
- }
- ],
-
- quickActions: [
- {
- id: 'add-expense',
- label: 'Expense',
- icon: 'DollarSign',
- component: markRaw(AddExpense),
- category: 'finance',
- order: 10,
- requiresAuth: true
- }
- ],
-
- async install(app: App) {
- console.log('[Expenses Module] Installing...')
-
- // 1. Create and register service
- const expensesAPI = new ExpensesAPI()
- container.provide(SERVICE_TOKENS.EXPENSES_API, expensesAPI)
-
- // 2. Initialize service (wait for dependencies)
- await expensesAPI.initialize({
- waitForDependencies: true,
- maxRetries: 3,
- retryDelay: 1000
- })
-
- console.log('[Expenses Module] ExpensesAPI initialized')
-
- // 3. Register components globally (optional, for use outside quick actions)
- app.component('AddExpense', AddExpense)
-
- console.log('[Expenses Module] Installed successfully')
- }
-}
-
-export default expensesModule
-
-// Export types for use in other modules
-export type { Account, AccountNode, ExpenseEntry, ExpenseEntryRequest } from './types'
diff --git a/src/modules/expenses/services/ExpensesAPI.ts b/src/modules/expenses/services/ExpensesAPI.ts
deleted file mode 100644
index 9beaaa5..0000000
--- a/src/modules/expenses/services/ExpensesAPI.ts
+++ /dev/null
@@ -1,450 +0,0 @@
-/**
- * API service for castle extension expense operations
- */
-
-import { BaseService } from '@/core/base/BaseService'
-import type {
- Account,
- ExpenseEntryRequest,
- ExpenseEntry,
- AccountNode,
- UserInfo,
- AccountPermission,
- GrantPermissionRequest,
- TransactionListResponse
-} from '../types'
-import { appConfig } from '@/app.config'
-
-export class ExpensesAPI extends BaseService {
- protected readonly metadata = {
- name: 'ExpensesAPI',
- version: '1.0.0',
- dependencies: [] // No dependencies - wallet key is passed as parameter
- }
-
- private get config() {
- return appConfig.modules.expenses.config
- }
-
- private get baseUrl() {
- return this.config?.apiConfig?.baseUrl || ''
- }
-
- protected async onInitialize(): Promise {
- console.log('[ExpensesAPI] Initialized with config:', {
- baseUrl: this.baseUrl,
- timeout: this.config?.apiConfig?.timeout
- })
- }
-
- /**
- * Get authentication headers with provided wallet key
- */
- private getHeaders(walletKey: string): HeadersInit {
- return {
- 'Content-Type': 'application/json',
- 'X-Api-Key': walletKey
- }
- }
-
- /**
- * Get all accounts from castle
- *
- * @param walletKey - Wallet key for authentication
- * @param filterByUser - If true, only return accounts the user has permissions for
- * @param excludeVirtual - If true, exclude virtual parent accounts (default true for user views)
- */
- async getAccounts(
- walletKey: string,
- filterByUser: boolean = false,
- excludeVirtual: boolean = true
- ): Promise {
- try {
- const url = new URL(`${this.baseUrl}/castle/api/v1/accounts`)
- if (filterByUser) {
- url.searchParams.set('filter_by_user', 'true')
- }
- if (excludeVirtual) {
- url.searchParams.set('exclude_virtual', 'true')
- }
-
- const response = await fetch(url.toString(), {
- method: 'GET',
- headers: this.getHeaders(walletKey),
- signal: AbortSignal.timeout(this.config?.apiConfig?.timeout || 30000)
- })
-
- if (!response.ok) {
- throw new Error(`Failed to fetch accounts: ${response.statusText}`)
- }
-
- const accounts = await response.json()
- return accounts as Account[]
- } catch (error) {
- console.error('[ExpensesAPI] Error fetching accounts:', error)
- throw error
- }
- }
-
- /**
- * Get accounts in hierarchical tree structure
- *
- * Converts flat account list to nested tree based on colon-separated names
- * e.g., "Expenses:Groceries:Organic" becomes nested structure
- *
- * @param walletKey - Wallet key for authentication
- * @param rootAccount - Optional root account to filter by (e.g., "Expenses")
- * @param filterByUser - If true, only return accounts the user has permissions for
- * @param excludeVirtual - If true, exclude virtual parent accounts (default true for user views)
- */
- async getAccountHierarchy(
- walletKey: string,
- rootAccount?: string,
- filterByUser: boolean = false,
- excludeVirtual: boolean = true
- ): Promise {
- const accounts = await this.getAccounts(walletKey, filterByUser, excludeVirtual)
-
- // Filter by root account if specified
- let filteredAccounts = accounts
- if (rootAccount) {
- filteredAccounts = accounts.filter(
- (acc) =>
- acc.name === rootAccount || acc.name.startsWith(`${rootAccount}:`)
- )
- }
-
- // Build hierarchy
- const accountMap = new Map()
-
- // First pass: Create nodes for all accounts
- for (const account of filteredAccounts) {
- const parts = account.name.split(':')
- const level = parts.length - 1
- const parentName = parts.slice(0, -1).join(':')
-
- accountMap.set(account.name, {
- account: {
- ...account,
- level,
- parent_account: parentName || undefined,
- has_children: false
- },
- children: []
- })
- }
-
- // Second pass: Build parent-child relationships
- const rootNodes: AccountNode[] = []
-
- for (const [_name, node] of accountMap.entries()) {
- const parentName = node.account.parent_account
-
- if (parentName && accountMap.has(parentName)) {
- // Add to parent's children
- const parent = accountMap.get(parentName)!
- parent.children.push(node)
- parent.account.has_children = true
- } else {
- // Root level node
- rootNodes.push(node)
- }
- }
-
- // Sort children by name at each level
- const sortNodes = (nodes: AccountNode[]) => {
- nodes.sort((a, b) => a.account.name.localeCompare(b.account.name))
- nodes.forEach((node) => sortNodes(node.children))
- }
- sortNodes(rootNodes)
-
- return rootNodes
- }
-
- /**
- * Submit expense entry to castle
- */
- async submitExpense(walletKey: string, request: ExpenseEntryRequest): Promise {
- try {
- const response = await fetch(`${this.baseUrl}/castle/api/v1/entries/expense`, {
- method: 'POST',
- headers: this.getHeaders(walletKey),
- body: JSON.stringify(request),
- signal: AbortSignal.timeout(this.config?.apiConfig?.timeout || 30000)
- })
-
- if (!response.ok) {
- const errorData = await response.json().catch(() => ({}))
- const errorMessage =
- errorData.detail || `Failed to submit expense: ${response.statusText}`
- throw new Error(errorMessage)
- }
-
- const entry = await response.json()
- return entry as ExpenseEntry
- } catch (error) {
- console.error('[ExpensesAPI] Error submitting expense:', error)
- throw error
- }
- }
-
- /**
- * Get user's expense entries
- */
- async getUserExpenses(walletKey: string): Promise {
- try {
- const response = await fetch(`${this.baseUrl}/castle/api/v1/entries/user`, {
- method: 'GET',
- headers: this.getHeaders(walletKey),
- signal: AbortSignal.timeout(this.config?.apiConfig?.timeout || 30000)
- })
-
- if (!response.ok) {
- throw new Error(
- `Failed to fetch user expenses: ${response.statusText}`
- )
- }
-
- const entries = await response.json()
- return entries as ExpenseEntry[]
- } catch (error) {
- console.error('[ExpensesAPI] Error fetching user expenses:', error)
- throw error
- }
- }
-
- /**
- * Get user's balance with castle
- */
- async getUserBalance(walletKey: string): Promise<{ balance: number; currency: string }> {
- try {
- const response = await fetch(`${this.baseUrl}/castle/api/v1/balance`, {
- method: 'GET',
- headers: this.getHeaders(walletKey),
- signal: AbortSignal.timeout(this.config?.apiConfig?.timeout || 30000)
- })
-
- if (!response.ok) {
- throw new Error(`Failed to fetch balance: ${response.statusText}`)
- }
-
- return await response.json()
- } catch (error) {
- console.error('[ExpensesAPI] Error fetching balance:', error)
- throw error
- }
- }
-
- /**
- * Get available currencies from LNbits instance
- */
- async getCurrencies(): Promise {
- try {
- const response = await fetch(`${this.baseUrl}/api/v1/currencies`, {
- method: 'GET',
- signal: AbortSignal.timeout(this.config?.apiConfig?.timeout || 30000)
- })
-
- if (!response.ok) {
- throw new Error(`Failed to fetch currencies: ${response.statusText}`)
- }
-
- return await response.json()
- } catch (error) {
- console.error('[ExpensesAPI] Error fetching currencies:', error)
- throw error
- }
- }
-
- /**
- * Get default currency from LNbits instance
- */
- async getDefaultCurrency(): Promise {
- try {
- const response = await fetch(`${this.baseUrl}/api/v1/default-currency`, {
- method: 'GET',
- signal: AbortSignal.timeout(this.config?.apiConfig?.timeout || 30000)
- })
-
- if (!response.ok) {
- throw new Error(`Failed to fetch default currency: ${response.statusText}`)
- }
-
- const data = await response.json()
- return data.default_currency
- } catch (error) {
- console.error('[ExpensesAPI] Error fetching default currency:', error)
- throw error
- }
- }
-
- /**
- * Get user information including equity eligibility
- *
- * @param walletKey - Wallet key for authentication (invoice key)
- */
- async getUserInfo(walletKey: string): Promise {
- try {
- const response = await fetch(`${this.baseUrl}/castle/api/v1/user/info`, {
- method: 'GET',
- headers: this.getHeaders(walletKey),
- signal: AbortSignal.timeout(this.config?.apiConfig?.timeout || 30000)
- })
-
- if (!response.ok) {
- throw new Error(`Failed to fetch user info: ${response.statusText}`)
- }
-
- return await response.json()
- } catch (error) {
- console.error('[ExpensesAPI] Error fetching user info:', error)
- // Return default non-eligible user on error
- return {
- user_id: '',
- is_equity_eligible: false
- }
- }
- }
-
- /**
- * List all account permissions (admin only)
- *
- * @param adminKey - Admin key for authentication
- */
- async listPermissions(adminKey: string): Promise {
- try {
- const response = await fetch(`${this.baseUrl}/castle/api/v1/permissions`, {
- method: 'GET',
- headers: this.getHeaders(adminKey),
- signal: AbortSignal.timeout(this.config?.apiConfig?.timeout || 30000)
- })
-
- if (!response.ok) {
- throw new Error(`Failed to list permissions: ${response.statusText}`)
- }
-
- const permissions = await response.json()
- return permissions as AccountPermission[]
- } catch (error) {
- console.error('[ExpensesAPI] Error listing permissions:', error)
- throw error
- }
- }
-
- /**
- * Grant account permission to a user (admin only)
- *
- * @param adminKey - Admin key for authentication
- * @param request - Permission grant request
- */
- async grantPermission(
- adminKey: string,
- request: GrantPermissionRequest
- ): Promise {
- try {
- const response = await fetch(`${this.baseUrl}/castle/api/v1/permissions`, {
- method: 'POST',
- headers: this.getHeaders(adminKey),
- body: JSON.stringify(request),
- signal: AbortSignal.timeout(this.config?.apiConfig?.timeout || 30000)
- })
-
- if (!response.ok) {
- const errorData = await response.json().catch(() => ({}))
- const errorMessage =
- errorData.detail || `Failed to grant permission: ${response.statusText}`
- throw new Error(errorMessage)
- }
-
- const permission = await response.json()
- return permission as AccountPermission
- } catch (error) {
- console.error('[ExpensesAPI] Error granting permission:', error)
- throw error
- }
- }
-
- /**
- * Revoke account permission (admin only)
- *
- * @param adminKey - Admin key for authentication
- * @param permissionId - ID of the permission to revoke
- */
- async revokePermission(adminKey: string, permissionId: string): Promise {
- try {
- const response = await fetch(
- `${this.baseUrl}/castle/api/v1/permissions/${permissionId}`,
- {
- method: 'DELETE',
- headers: this.getHeaders(adminKey),
- signal: AbortSignal.timeout(this.config?.apiConfig?.timeout || 30000)
- }
- )
-
- if (!response.ok) {
- const errorData = await response.json().catch(() => ({}))
- const errorMessage =
- errorData.detail || `Failed to revoke permission: ${response.statusText}`
- throw new Error(errorMessage)
- }
- } catch (error) {
- console.error('[ExpensesAPI] Error revoking permission:', error)
- throw error
- }
- }
-
- /**
- * Get user's transactions from journal
- *
- * @param walletKey - Wallet key for authentication (invoice key)
- * @param options - Query options for filtering and pagination
- */
- async getUserTransactions(
- walletKey: string,
- options?: {
- limit?: number
- offset?: number
- days?: number // 15, 30, or 60 (default: 15)
- start_date?: string // ISO format: YYYY-MM-DD
- end_date?: string // ISO format: YYYY-MM-DD
- filter_user_id?: string
- filter_account_type?: string
- }
- ): Promise {
- try {
- const url = new URL(`${this.baseUrl}/castle/api/v1/entries/user`)
-
- // Add query parameters
- if (options?.limit) url.searchParams.set('limit', String(options.limit))
- if (options?.offset) url.searchParams.set('offset', String(options.offset))
-
- // Custom date range takes precedence over days
- if (options?.start_date && options?.end_date) {
- url.searchParams.set('start_date', options.start_date)
- url.searchParams.set('end_date', options.end_date)
- } else if (options?.days) {
- url.searchParams.set('days', String(options.days))
- }
-
- if (options?.filter_user_id)
- url.searchParams.set('filter_user_id', options.filter_user_id)
- if (options?.filter_account_type)
- url.searchParams.set('filter_account_type', options.filter_account_type)
-
- const response = await fetch(url.toString(), {
- method: 'GET',
- headers: this.getHeaders(walletKey),
- signal: AbortSignal.timeout(this.config?.apiConfig?.timeout || 30000)
- })
-
- if (!response.ok) {
- throw new Error(`Failed to fetch transactions: ${response.statusText}`)
- }
-
- return await response.json()
- } catch (error) {
- console.error('[ExpensesAPI] Error fetching transactions:', error)
- throw error
- }
- }
-}
diff --git a/src/modules/expenses/types/index.ts b/src/modules/expenses/types/index.ts
deleted file mode 100644
index c830a9e..0000000
--- a/src/modules/expenses/types/index.ts
+++ /dev/null
@@ -1,164 +0,0 @@
-/**
- * Types for the Expenses module
- */
-
-/**
- * Account types in the castle double-entry accounting system
- */
-export enum AccountType {
- ASSET = 'asset',
- LIABILITY = 'liability',
- EQUITY = 'equity',
- REVENUE = 'revenue',
- EXPENSE = 'expense'
-}
-
-/**
- * Account with hierarchical structure
- */
-export interface Account {
- id: string
- name: string // e.g., "Expenses:Groceries:Organic"
- account_type: AccountType
- description?: string
- user_id?: string
- // Hierarchical metadata (added by frontend)
- parent_account?: string
- level?: number
- has_children?: boolean
-}
-
-/**
- * Account with user-specific permission metadata
- * (Will be available once castle API implements permissions)
- */
-export interface AccountWithPermissions extends Account {
- user_permissions?: PermissionType[]
- inherited_from?: string
-}
-
-/**
- * Permission types for account access control
- */
-export enum PermissionType {
- READ = 'read',
- SUBMIT_EXPENSE = 'submit_expense',
- MANAGE = 'manage'
-}
-
-/**
- * Expense entry request payload
- */
-export interface ExpenseEntryRequest {
- description: string
- amount: number // Amount in the specified currency (or satoshis if currency is None)
- expense_account: string // Account name or ID
- is_equity: boolean
- user_wallet: string
- reference?: string
- currency?: string // If None, amount is in satoshis. Otherwise, fiat currency code (e.g., "EUR", "USD")
- entry_date?: string // ISO datetime string
-}
-
-/**
- * Expense entry response from castle API
- */
-export interface ExpenseEntry {
- id: string
- journal_id: string
- description: string
- amount: number
- expense_account: string
- is_equity: boolean
- user_wallet: string
- reference?: string
- currency?: string
- entry_date: string
- created_at: string
- status: 'pending' | 'approved' | 'rejected' | 'void'
-}
-
-/**
- * Hierarchical account tree node for UI rendering
- */
-export interface AccountNode {
- account: Account
- children: AccountNode[]
-}
-
-/**
- * User information including equity eligibility
- */
-export interface UserInfo {
- user_id: string
- is_equity_eligible: boolean
- equity_account_name?: string
-}
-
-/**
- * Account permission for user access control
- */
-export interface AccountPermission {
- id: string
- user_id: string
- account_id: string
- permission_type: PermissionType
- granted_at: string
- granted_by: string
- expires_at?: string
- notes?: string
-}
-
-/**
- * Grant permission request payload
- */
-export interface GrantPermissionRequest {
- user_id: string
- account_id: string
- permission_type: PermissionType
- expires_at?: string
- notes?: string
-}
-
-/**
- * Transaction entry from journal (user view)
- */
-export interface Transaction {
- id: string
- date: string
- entry_date: string
- flag?: string // *, !, #, x for cleared, pending, flagged, voided
- description: string
- payee?: string
- tags: string[]
- links: string[]
- amount: number // Amount in satoshis
- user_id?: string
- username?: string
- reference?: string
- meta?: Record
- fiat_amount?: number
- fiat_currency?: string
-}
-
-/**
- * Transaction list response with pagination
- */
-export interface TransactionListResponse {
- entries: Transaction[]
- total: number
- limit: number
- offset: number
- has_next: boolean
- has_prev: boolean
-}
-
-/**
- * Module configuration
- */
-export interface ExpensesConfig {
- apiConfig: {
- baseUrl: string
- timeout: number
- }
-}
diff --git a/src/modules/expenses/views/TransactionsPage.vue b/src/modules/expenses/views/TransactionsPage.vue
deleted file mode 100644
index de67fbc..0000000
--- a/src/modules/expenses/views/TransactionsPage.vue
+++ /dev/null
@@ -1,367 +0,0 @@
-
-
-
-
-
-
-
-
-
Transaction History
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Found {{ transactionsToDisplay.length }} matching transaction{{ transactionsToDisplay.length === 1 ? '' : 's' }}
-
-
- {{ transactions.length }} transaction{{ transactions.length === 1 ? '' : 's' }}
-
-
-
-
-
-
-
- Loading transactions...
-
-
-
-
-
-
No transactions found
-
- {{ searchResults.length > 0 ? 'Try a different search term' : 'Try selecting a different time period' }}
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ transaction.description }}
-
-
-
- {{ formatDate(transaction.date) }}
-
-
-
-
-
-
- {{ formatAmount(transaction.amount) }} sats
-
-
- {{ transaction.fiat_amount.toFixed(2) }} {{ transaction.fiat_currency }}
-
-
-
-
-
-
-
-
- Payee: {{ transaction.payee }}
-
-
-
-
- Ref: {{ transaction.reference }}
-
-
-
-
- User: {{ transaction.username }}
-
-
-
-
-
- {{ tag }}
-
-
-
-
-
- Source: {{ transaction.meta.source }}
-
-
-
-
-
-
-
-
-
-
diff --git a/src/modules/nostr-feed/components/NostrFeed.vue b/src/modules/nostr-feed/components/NostrFeed.vue
index 529b7d7..64790ef 100644
--- a/src/modules/nostr-feed/components/NostrFeed.vue
+++ b/src/modules/nostr-feed/components/NostrFeed.vue
@@ -9,16 +9,13 @@ import {
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog'
-import { Megaphone, RefreshCw, AlertCircle, ChevronLeft, ChevronRight } from 'lucide-vue-next'
+import { Megaphone, RefreshCw, AlertCircle } from 'lucide-vue-next'
import { useFeed } from '../composables/useFeed'
import { useProfiles } from '../composables/useProfiles'
import { useReactions } from '../composables/useReactions'
-import { useScheduledEvents } from '../composables/useScheduledEvents'
import ThreadedPost from './ThreadedPost.vue'
-import ScheduledEventCard from './ScheduledEventCard.vue'
import appConfig from '@/app.config'
import type { ContentFilter, FeedPost } from '../services/FeedService'
-import type { ScheduledEvent } from '../services/ScheduledEventService'
import { injectService, SERVICE_TOKENS } from '@/core/di-container'
import type { AuthService } from '@/modules/base/auth/auth-service'
import type { RelayHub } from '@/modules/base/nostr/relay-hub'
@@ -98,78 +95,6 @@ const { getDisplayName, fetchProfiles } = useProfiles()
// Use reactions service for likes/hearts
const { getEventReactions, subscribeToReactions, toggleLike } = useReactions()
-// Use scheduled events service
-const {
- getEventsForSpecificDate,
- getCompletion,
- getTaskStatus,
- claimTask,
- startTask,
- completeEvent,
- unclaimTask,
- deleteTask,
- allCompletions
-} = useScheduledEvents()
-
-// Selected date for viewing scheduled tasks (defaults to today)
-const selectedDate = ref(new Date().toISOString().split('T')[0])
-
-// Get scheduled tasks for the selected date (reactive)
-const scheduledEventsForDate = computed(() => getEventsForSpecificDate(selectedDate.value))
-
-// Navigate to previous day
-function goToPreviousDay() {
- const date = new Date(selectedDate.value)
- date.setDate(date.getDate() - 1)
- selectedDate.value = date.toISOString().split('T')[0]
-}
-
-// Navigate to next day
-function goToNextDay() {
- const date = new Date(selectedDate.value)
- date.setDate(date.getDate() + 1)
- selectedDate.value = date.toISOString().split('T')[0]
-}
-
-// Go back to today
-function goToToday() {
- selectedDate.value = new Date().toISOString().split('T')[0]
-}
-
-// Check if selected date is today
-const isToday = computed(() => {
- const today = new Date().toISOString().split('T')[0]
- return selectedDate.value === today
-})
-
-// Format date for display
-const dateDisplayText = computed(() => {
- const today = new Date().toISOString().split('T')[0]
- const yesterday = new Date()
- yesterday.setDate(yesterday.getDate() - 1)
- const yesterdayStr = yesterday.toISOString().split('T')[0]
- const tomorrow = new Date()
- tomorrow.setDate(tomorrow.getDate() + 1)
- const tomorrowStr = tomorrow.toISOString().split('T')[0]
-
- if (selectedDate.value === today) {
- return "Today's Tasks"
- } else if (selectedDate.value === yesterdayStr) {
- return "Yesterday's Tasks"
- } else if (selectedDate.value === tomorrowStr) {
- return "Tomorrow's Tasks"
- } else {
- // Format as "Tasks for Mon, Jan 15"
- const date = new Date(selectedDate.value + 'T00:00:00')
- const formatted = date.toLocaleDateString('en-US', {
- weekday: 'short',
- month: 'short',
- day: 'numeric'
- })
- return `Tasks for ${formatted}`
- }
-})
-
// Watch for new posts and fetch their profiles and reactions
watch(notes, async (newNotes) => {
if (newNotes.length > 0) {
@@ -184,38 +109,6 @@ watch(notes, async (newNotes) => {
}
}, { immediate: true })
-// Watch for scheduled events and fetch profiles for event authors and completers
-watch(scheduledEventsForDate, async (events) => {
- if (events.length > 0) {
- const pubkeys = new Set()
-
- // Add event authors
- events.forEach((event: ScheduledEvent) => {
- pubkeys.add(event.pubkey)
-
- // Add completer pubkey if event is completed
- const eventAddress = `31922:${event.pubkey}:${event.dTag}`
- const completion = getCompletion(eventAddress)
- if (completion) {
- pubkeys.add(completion.pubkey)
- }
- })
-
- // Fetch all profiles
- if (pubkeys.size > 0) {
- await fetchProfiles([...pubkeys])
- }
- }
-}, { immediate: true })
-
-// Watch for new completions and fetch profiles for completers
-watch(allCompletions, async (completions) => {
- if (completions.length > 0) {
- const pubkeys = completions.map(c => c.pubkey)
- await fetchProfiles(pubkeys)
- }
-}, { immediate: true })
-
// Check if we have admin pubkeys configured
const hasAdminPubkeys = computed(() => adminPubkeys.length > 0)
@@ -265,52 +158,6 @@ async function onToggleLike(note: FeedPost) {
}
}
-// Task action handlers
-async function onClaimTask(event: ScheduledEvent, occurrence?: string) {
- console.log('👋 NostrFeed: Claiming task:', event.title)
- try {
- await claimTask(event, '', occurrence)
- } catch (error) {
- console.error('❌ Failed to claim task:', error)
- }
-}
-
-async function onStartTask(event: ScheduledEvent, occurrence?: string) {
- console.log('▶️ NostrFeed: Starting task:', event.title)
- try {
- await startTask(event, '', occurrence)
- } catch (error) {
- console.error('❌ Failed to start task:', error)
- }
-}
-
-async function onCompleteTask(event: ScheduledEvent, occurrence?: string) {
- console.log('✅ NostrFeed: Completing task:', event.title)
- try {
- await completeEvent(event, occurrence, '')
- } catch (error) {
- console.error('❌ Failed to complete task:', error)
- }
-}
-
-async function onUnclaimTask(event: ScheduledEvent, occurrence?: string) {
- console.log('🔙 NostrFeed: Unclaiming task:', event.title)
- try {
- await unclaimTask(event, occurrence)
- } catch (error) {
- console.error('❌ Failed to unclaim task:', error)
- }
-}
-
-async function onDeleteTask(event: ScheduledEvent) {
- console.log('🗑️ NostrFeed: Deleting task:', event.title)
- try {
- await deleteTask(event)
- } catch (error) {
- console.error('❌ Failed to delete task:', error)
- }
-}
-
// Handle collapse toggle with cascading behavior
function onToggleCollapse(postId: string) {
const newCollapsed = new Set(collapsedPosts.value)
@@ -509,75 +356,20 @@ function cancelDelete() {
+
+
+
+
+ No posts yet
+
+
+ Check back later for community updates.
+
+
+
-
-
-
-
-
-
-
-
-
- 📅 {{ dateDisplayText }}
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ isToday ? 'no tasks today' : 'no tasks for this day' }}
-
-
-
-
-
-
- 💬 Posts
-
+
-
-
-
-
- No posts yet
-
-
- Check back later for community updates.
-
-
-
-
diff --git a/src/modules/nostr-feed/components/ScheduledEventCard.vue b/src/modules/nostr-feed/components/ScheduledEventCard.vue
deleted file mode 100644
index 46c188e..0000000
--- a/src/modules/nostr-feed/components/ScheduledEventCard.vue
+++ /dev/null
@@ -1,540 +0,0 @@
-
-
-
-
-
-
-
-
-
-
- {{ formattedTimeRange || formattedDate }}
-
-
-
-
- {{ event.title }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ getDisplayName(completion.pubkey) }}
-
-
-
-
- 🔄
-
-
-
-
-
-
-
-
-
-
-
-
-
- {{ formattedDate }}
-
-
-
- {{ formattedTimeRange }}
-
-
-
-
-
-
- {{ event.location }}
-
-
-
-
-
{{ event.description || event.content }}
-
-
-
-
-
- ✓ Completed by {{ getDisplayName(completion.pubkey) }}
- - {{ completion.notes }}
-
-
- 🔄 In Progress by {{ getDisplayName(completion.pubkey) }}
- - {{ completion.notes }}
-
-
- 👋 Claimed by {{ getDisplayName(completion.pubkey) }}
- - {{ completion.notes }}
-
-
-
-
-
- Posted by {{ getDisplayName(event.pubkey) }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/src/modules/nostr-feed/composables/useScheduledEvents.ts b/src/modules/nostr-feed/composables/useScheduledEvents.ts
deleted file mode 100644
index 580a26b..0000000
--- a/src/modules/nostr-feed/composables/useScheduledEvents.ts
+++ /dev/null
@@ -1,261 +0,0 @@
-import { computed } from 'vue'
-import { injectService, SERVICE_TOKENS } from '@/core/di-container'
-import type { ScheduledEventService, ScheduledEvent, EventCompletion, TaskStatus } from '../services/ScheduledEventService'
-import type { AuthService } from '@/modules/base/auth/auth-service'
-import { useToast } from '@/core/composables/useToast'
-
-/**
- * Composable for managing scheduled events in the feed
- */
-export function useScheduledEvents() {
- const scheduledEventService = injectService
(SERVICE_TOKENS.SCHEDULED_EVENT_SERVICE)
- const authService = injectService(SERVICE_TOKENS.AUTH_SERVICE)
- const toast = useToast()
-
- // Get current user's pubkey
- const currentUserPubkey = computed(() => authService?.user.value?.pubkey)
-
- /**
- * Get all scheduled events
- */
- const getScheduledEvents = (): ScheduledEvent[] => {
- if (!scheduledEventService) return []
- return scheduledEventService.getScheduledEvents()
- }
-
- /**
- * Get events for a specific date (YYYY-MM-DD)
- */
- const getEventsForDate = (date: string): ScheduledEvent[] => {
- if (!scheduledEventService) return []
- return scheduledEventService.getEventsForDate(date)
- }
-
- /**
- * Get events for a specific date (filtered by current user participation)
- * @param date - ISO date string (YYYY-MM-DD). Defaults to today.
- */
- const getEventsForSpecificDate = (date?: string): ScheduledEvent[] => {
- if (!scheduledEventService) return []
- return scheduledEventService.getEventsForSpecificDate(date, currentUserPubkey.value)
- }
-
- /**
- * Get today's scheduled events (filtered by current user participation)
- */
- const getTodaysEvents = (): ScheduledEvent[] => {
- if (!scheduledEventService) return []
- return scheduledEventService.getTodaysEvents(currentUserPubkey.value)
- }
-
- /**
- * Get completion status for an event
- */
- const getCompletion = (eventAddress: string): EventCompletion | undefined => {
- if (!scheduledEventService) return undefined
- return scheduledEventService.getCompletion(eventAddress)
- }
-
- /**
- * Check if an event is completed
- */
- const isCompleted = (eventAddress: string): boolean => {
- if (!scheduledEventService) return false
- return scheduledEventService.isCompleted(eventAddress)
- }
-
- /**
- * Get task status for an event
- */
- const getTaskStatus = (eventAddress: string, occurrence?: string): TaskStatus | null => {
- if (!scheduledEventService) return null
- return scheduledEventService.getTaskStatus(eventAddress, occurrence)
- }
-
- /**
- * Claim a task
- */
- const claimTask = async (event: ScheduledEvent, notes: string = '', occurrence?: string): Promise => {
- if (!scheduledEventService) {
- toast.error('Scheduled event service not available')
- return
- }
-
- try {
- await scheduledEventService.claimTask(event, notes, occurrence)
- toast.success('Task claimed!')
- } catch (error) {
- const message = error instanceof Error ? error.message : 'Failed to claim task'
- if (message.includes('authenticated')) {
- toast.error('Please sign in to claim tasks')
- } else {
- toast.error(message)
- }
- console.error('Failed to claim task:', error)
- }
- }
-
- /**
- * Start a task (mark as in-progress)
- */
- const startTask = async (event: ScheduledEvent, notes: string = '', occurrence?: string): Promise => {
- if (!scheduledEventService) {
- toast.error('Scheduled event service not available')
- return
- }
-
- try {
- await scheduledEventService.startTask(event, notes, occurrence)
- toast.success('Task started!')
- } catch (error) {
- const message = error instanceof Error ? error.message : 'Failed to start task'
- toast.error(message)
- console.error('Failed to start task:', error)
- }
- }
-
- /**
- * Unclaim a task (remove task status)
- */
- const unclaimTask = async (event: ScheduledEvent, occurrence?: string): Promise => {
- if (!scheduledEventService) {
- toast.error('Scheduled event service not available')
- return
- }
-
- try {
- await scheduledEventService.unclaimTask(event, occurrence)
- toast.success('Task unclaimed')
- } catch (error) {
- const message = error instanceof Error ? error.message : 'Failed to unclaim task'
- toast.error(message)
- console.error('Failed to unclaim task:', error)
- }
- }
-
- /**
- * Toggle completion status of an event (optionally for a specific occurrence)
- * DEPRECATED: Use claimTask, startTask, completeEvent, or unclaimTask instead for more granular control
- */
- const toggleComplete = async (event: ScheduledEvent, occurrence?: string, notes: string = ''): Promise => {
- console.log('🔧 useScheduledEvents: toggleComplete called for event:', event.title, 'occurrence:', occurrence)
-
- if (!scheduledEventService) {
- console.error('❌ useScheduledEvents: Scheduled event service not available')
- toast.error('Scheduled event service not available')
- return
- }
-
- try {
- const eventAddress = `31922:${event.pubkey}:${event.dTag}`
- const currentlyCompleted = scheduledEventService.isCompleted(eventAddress, occurrence)
- console.log('📊 useScheduledEvents: Current completion status:', currentlyCompleted)
-
- if (currentlyCompleted) {
- console.log('⬇️ useScheduledEvents: Unclaiming task...')
- await scheduledEventService.unclaimTask(event, occurrence)
- toast.success('Task unclaimed')
- } else {
- console.log('⬆️ useScheduledEvents: Marking as complete...')
- await scheduledEventService.completeEvent(event, notes, occurrence)
- toast.success('Task completed!')
- }
- } catch (error) {
- const message = error instanceof Error ? error.message : 'Failed to toggle completion'
-
- if (message.includes('authenticated')) {
- toast.error('Please sign in to complete tasks')
- } else if (message.includes('Not connected')) {
- toast.error('Not connected to relays')
- } else {
- toast.error(message)
- }
-
- console.error('❌ useScheduledEvents: Failed to toggle completion:', error)
- }
- }
-
- /**
- * Complete an event with optional notes
- */
- const completeEvent = async (event: ScheduledEvent, occurrence?: string, notes: string = ''): Promise => {
- if (!scheduledEventService) {
- toast.error('Scheduled event service not available')
- return
- }
-
- try {
- await scheduledEventService.completeEvent(event, notes, occurrence)
- toast.success('Task completed!')
- } catch (error) {
- const message = error instanceof Error ? error.message : 'Failed to complete task'
- toast.error(message)
- console.error('Failed to complete task:', error)
- }
- }
-
- /**
- * Get loading state
- */
- const isLoading = computed(() => {
- return scheduledEventService?.isLoading ?? false
- })
-
- /**
- * Get all scheduled events (reactive)
- */
- const allScheduledEvents = computed(() => {
- return scheduledEventService?.scheduledEvents ?? new Map()
- })
-
- /**
- * Delete a task (only author can delete)
- */
- const deleteTask = async (event: ScheduledEvent): Promise => {
- if (!scheduledEventService) {
- toast.error('Scheduled event service not available')
- return
- }
-
- try {
- await scheduledEventService.deleteTask(event)
- toast.success('Task deleted!')
- } catch (error) {
- const message = error instanceof Error ? error.message : 'Failed to delete task'
- toast.error(message)
- console.error('Failed to delete task:', error)
- }
- }
-
- /**
- * Get all completions (reactive) - returns array for better reactivity
- */
- const allCompletions = computed(() => {
- if (!scheduledEventService?.completions) return []
- return Array.from(scheduledEventService.completions.values())
- })
-
- return {
- // Methods - Getters
- getScheduledEvents,
- getEventsForDate,
- getEventsForSpecificDate,
- getTodaysEvents,
- getCompletion,
- isCompleted,
- getTaskStatus,
-
- // Methods - Actions
- claimTask,
- startTask,
- completeEvent,
- unclaimTask,
- deleteTask,
- toggleComplete, // DEPRECATED: Use specific actions instead
-
- // State
- isLoading,
- allScheduledEvents,
- allCompletions
- }
-}
diff --git a/src/modules/nostr-feed/config/content-filters.ts b/src/modules/nostr-feed/config/content-filters.ts
index 903131d..46808ee 100644
--- a/src/modules/nostr-feed/config/content-filters.ts
+++ b/src/modules/nostr-feed/config/content-filters.ts
@@ -88,14 +88,6 @@ export const CONTENT_FILTERS: Record = {
description: 'Rideshare requests, offers, and coordination',
tags: ['rideshare', 'carpool'], // NIP-12 tags
keywords: ['rideshare', 'ride share', 'carpool', '🚗', '🚶']
- },
-
- // Scheduled events (NIP-52)
- scheduledEvents: {
- id: 'scheduled-events',
- label: 'Scheduled Events',
- kinds: [31922], // NIP-52: Calendar Events
- description: 'Calendar-based tasks and scheduled activities'
}
}
@@ -118,11 +110,6 @@ export const FILTER_PRESETS: Record = {
// Rideshare only
rideshare: [
CONTENT_FILTERS.rideshare
- ],
-
- // Scheduled events only
- scheduledEvents: [
- CONTENT_FILTERS.scheduledEvents
]
}
diff --git a/src/modules/nostr-feed/index.ts b/src/modules/nostr-feed/index.ts
index c790302..9967476 100644
--- a/src/modules/nostr-feed/index.ts
+++ b/src/modules/nostr-feed/index.ts
@@ -1,15 +1,11 @@
import type { App } from 'vue'
-import { markRaw } from 'vue'
import type { ModulePlugin } from '@/core/types'
import { container, SERVICE_TOKENS } from '@/core/di-container'
import NostrFeed from './components/NostrFeed.vue'
-import NoteComposer from './components/NoteComposer.vue'
-import RideshareComposer from './components/RideshareComposer.vue'
import { useFeed } from './composables/useFeed'
import { FeedService } from './services/FeedService'
import { ProfileService } from './services/ProfileService'
import { ReactionService } from './services/ReactionService'
-import { ScheduledEventService } from './services/ScheduledEventService'
/**
* Nostr Feed Module Plugin
@@ -20,28 +16,6 @@ export const nostrFeedModule: ModulePlugin = {
version: '1.0.0',
dependencies: ['base'],
- // Register quick actions for the FAB menu
- quickActions: [
- {
- id: 'note',
- label: 'Note',
- icon: 'MessageSquare',
- component: markRaw(NoteComposer),
- category: 'compose',
- order: 1,
- requiresAuth: true
- },
- {
- id: 'rideshare',
- label: 'Rideshare',
- icon: 'Car',
- component: markRaw(RideshareComposer),
- category: 'compose',
- order: 2,
- requiresAuth: true
- }
- ],
-
async install(app: App) {
console.log('nostr-feed module: Starting installation...')
@@ -49,12 +23,10 @@ export const nostrFeedModule: ModulePlugin = {
const feedService = new FeedService()
const profileService = new ProfileService()
const reactionService = new ReactionService()
- const scheduledEventService = new ScheduledEventService()
container.provide(SERVICE_TOKENS.FEED_SERVICE, feedService)
container.provide(SERVICE_TOKENS.PROFILE_SERVICE, profileService)
container.provide(SERVICE_TOKENS.REACTION_SERVICE, reactionService)
- container.provide(SERVICE_TOKENS.SCHEDULED_EVENT_SERVICE, scheduledEventService)
console.log('nostr-feed module: Services registered in DI container')
// Initialize services
@@ -71,10 +43,6 @@ export const nostrFeedModule: ModulePlugin = {
reactionService.initialize({
waitForDependencies: true,
maxRetries: 3
- }),
- scheduledEventService.initialize({
- waitForDependencies: true,
- maxRetries: 3
})
])
console.log('nostr-feed module: Services initialized')
diff --git a/src/modules/nostr-feed/services/FeedService.ts b/src/modules/nostr-feed/services/FeedService.ts
index 2141ed7..377186b 100644
--- a/src/modules/nostr-feed/services/FeedService.ts
+++ b/src/modules/nostr-feed/services/FeedService.ts
@@ -47,7 +47,6 @@ export class FeedService extends BaseService {
protected relayHub: any = null
protected visibilityService: any = null
protected reactionService: any = null
- protected scheduledEventService: any = null
// Event ID tracking for deduplication
private seenEventIds = new Set()
@@ -73,12 +72,10 @@ export class FeedService extends BaseService {
this.relayHub = injectService(SERVICE_TOKENS.RELAY_HUB)
this.visibilityService = injectService(SERVICE_TOKENS.VISIBILITY_SERVICE)
this.reactionService = injectService(SERVICE_TOKENS.REACTION_SERVICE)
- this.scheduledEventService = injectService(SERVICE_TOKENS.SCHEDULED_EVENT_SERVICE)
console.log('FeedService: RelayHub injected:', !!this.relayHub)
console.log('FeedService: VisibilityService injected:', !!this.visibilityService)
console.log('FeedService: ReactionService injected:', !!this.reactionService)
- console.log('FeedService: ScheduledEventService injected:', !!this.scheduledEventService)
if (!this.relayHub) {
throw new Error('RelayHub service not available')
@@ -202,12 +199,6 @@ export class FeedService extends BaseService {
kinds: [5] // All deletion events (for both posts and reactions)
})
- // Add scheduled events (kind 31922) and RSVPs (kind 31925)
- filters.push({
- kinds: [31922, 31925], // Calendar events and RSVPs
- limit: 200
- })
-
console.log(`Creating feed subscription for ${config.feedType} with filters:`, filters)
// Subscribe to all events (posts, reactions, deletions) with deduplication
@@ -266,25 +257,6 @@ export class FeedService extends BaseService {
return
}
- // Route scheduled events (kind 31922) to ScheduledEventService
- if (event.kind === 31922) {
- if (this.scheduledEventService) {
- this.scheduledEventService.handleScheduledEvent(event)
- }
- return
- }
-
- // Route RSVP/completion events (kind 31925) to ScheduledEventService
- if (event.kind === 31925) {
- console.log('🔀 FeedService: Routing kind 31925 (completion) to ScheduledEventService')
- if (this.scheduledEventService) {
- this.scheduledEventService.handleCompletionEvent(event)
- } else {
- console.warn('⚠️ FeedService: ScheduledEventService not available')
- }
- return
- }
-
// Skip if event already seen (for posts only, kind 1)
if (this.seenEventIds.has(event.id)) {
return
@@ -383,28 +355,6 @@ export class FeedService extends BaseService {
return
}
- // Route to ScheduledEventService for completion/RSVP deletions (kind 31925)
- if (deletedKind === '31925') {
- console.log('🔀 FeedService: Routing kind 5 (deletion of kind 31925) to ScheduledEventService')
- if (this.scheduledEventService) {
- this.scheduledEventService.handleDeletionEvent(event)
- } else {
- console.warn('⚠️ FeedService: ScheduledEventService not available')
- }
- return
- }
-
- // Route to ScheduledEventService for scheduled event deletions (kind 31922)
- if (deletedKind === '31922') {
- console.log('🔀 FeedService: Routing kind 5 (deletion of kind 31922) to ScheduledEventService')
- if (this.scheduledEventService) {
- this.scheduledEventService.handleTaskDeletion(event)
- } else {
- console.warn('⚠️ FeedService: ScheduledEventService not available')
- }
- return
- }
-
// Handle post deletions (kind 1) in FeedService
if (deletedKind === '1' || !deletedKind) {
// Extract event IDs to delete from 'e' tags
diff --git a/src/modules/nostr-feed/services/ScheduledEventService.ts b/src/modules/nostr-feed/services/ScheduledEventService.ts
deleted file mode 100644
index d2ef6b4..0000000
--- a/src/modules/nostr-feed/services/ScheduledEventService.ts
+++ /dev/null
@@ -1,678 +0,0 @@
-import { ref, reactive } from 'vue'
-import { BaseService } from '@/core/base/BaseService'
-import { injectService, SERVICE_TOKENS } from '@/core/di-container'
-import { finalizeEvent, type EventTemplate } from 'nostr-tools'
-import type { Event as NostrEvent } from 'nostr-tools'
-
-export interface RecurrencePattern {
- frequency: 'daily' | 'weekly'
- dayOfWeek?: string // For weekly: 'monday', 'tuesday', etc.
- endDate?: string // ISO date string - when to stop recurring (optional)
-}
-
-export interface ScheduledEvent {
- id: string
- pubkey: string
- created_at: number
- dTag: string // Unique identifier from 'd' tag
- title: string
- start: string // ISO date string (YYYY-MM-DD or ISO datetime)
- end?: string
- description?: string
- location?: string
- status: string
- eventType?: string // 'task' for completable events, 'announcement' for informational
- participants?: Array<{ pubkey: string; type?: string }> // 'required', 'optional', 'organizer'
- content: string
- tags: string[][]
- recurrence?: RecurrencePattern // Optional: for recurring events
-}
-
-export type TaskStatus = 'claimed' | 'in-progress' | 'completed' | 'blocked' | 'cancelled'
-
-export interface EventCompletion {
- id: string
- eventAddress: string // "31922:pubkey:d-tag"
- occurrence?: string // ISO date string for the specific occurrence (YYYY-MM-DD)
- pubkey: string // Who claimed/completed it
- created_at: number
- taskStatus: TaskStatus
- completedAt?: number // Unix timestamp when completed
- notes: string
-}
-
-export class ScheduledEventService extends BaseService {
- protected readonly metadata = {
- name: 'ScheduledEventService',
- version: '1.0.0',
- dependencies: []
- }
-
- protected relayHub: any = null
- protected authService: any = null
-
- // Scheduled events state - indexed by event address
- private _scheduledEvents = reactive(new Map())
- private _completions = reactive(new Map())
- private _isLoading = ref(false)
-
- protected async onInitialize(): Promise {
- console.log('ScheduledEventService: Starting initialization...')
-
- this.relayHub = injectService(SERVICE_TOKENS.RELAY_HUB)
- this.authService = injectService(SERVICE_TOKENS.AUTH_SERVICE)
-
- if (!this.relayHub) {
- throw new Error('RelayHub service not available')
- }
-
- console.log('ScheduledEventService: Initialization complete')
- }
-
- /**
- * Handle incoming scheduled event (kind 31922)
- * Made public so FeedService can route kind 31922 events to this service
- */
- public handleScheduledEvent(event: NostrEvent): void {
- try {
- // Extract event data from tags
- const dTag = event.tags.find(tag => tag[0] === 'd')?.[1]
- if (!dTag) {
- console.warn('Scheduled event missing d tag:', event.id)
- return
- }
-
- const title = event.tags.find(tag => tag[0] === 'title')?.[1] || 'Untitled Event'
- const start = event.tags.find(tag => tag[0] === 'start')?.[1]
- const end = event.tags.find(tag => tag[0] === 'end')?.[1]
- const description = event.tags.find(tag => tag[0] === 'description')?.[1]
- const location = event.tags.find(tag => tag[0] === 'location')?.[1]
- const status = event.tags.find(tag => tag[0] === 'status')?.[1] || 'pending'
- const eventType = event.tags.find(tag => tag[0] === 'event-type')?.[1]
-
- // Parse participant tags: ["p", "", "", ""]
- const participantTags = event.tags.filter(tag => tag[0] === 'p')
- const participants = participantTags.map(tag => ({
- pubkey: tag[1],
- type: tag[3] // 'required', 'optional', 'organizer'
- }))
-
- // Parse recurrence tags
- const recurrenceFreq = event.tags.find(tag => tag[0] === 'recurrence')?.[1] as 'daily' | 'weekly' | undefined
- const recurrenceDayOfWeek = event.tags.find(tag => tag[0] === 'recurrence-day')?.[1]
- const recurrenceEndDate = event.tags.find(tag => tag[0] === 'recurrence-end')?.[1]
-
- let recurrence: RecurrencePattern | undefined
- if (recurrenceFreq === 'daily' || recurrenceFreq === 'weekly') {
- recurrence = {
- frequency: recurrenceFreq,
- dayOfWeek: recurrenceDayOfWeek,
- endDate: recurrenceEndDate
- }
- }
-
- if (!start) {
- console.warn('Scheduled event missing start date:', event.id)
- return
- }
-
- // Create event address: "kind:pubkey:d-tag"
- const eventAddress = `31922:${event.pubkey}:${dTag}`
-
- const scheduledEvent: ScheduledEvent = {
- id: event.id,
- pubkey: event.pubkey,
- created_at: event.created_at,
- dTag,
- title,
- start,
- end,
- description,
- location,
- status,
- eventType,
- participants: participants.length > 0 ? participants : undefined,
- content: event.content,
- tags: event.tags,
- recurrence
- }
-
- // Store or update the event (replaceable by d-tag)
- this._scheduledEvents.set(eventAddress, scheduledEvent)
-
- } catch (error) {
- console.error('Failed to handle scheduled event:', error)
- }
- }
-
- /**
- * Handle RSVP/completion event (kind 31925)
- * Made public so FeedService can route kind 31925 events to this service
- */
- public handleCompletionEvent(event: NostrEvent): void {
- console.log('🔔 ScheduledEventService: Received completion event (kind 31925)', event.id)
-
- try {
- // Find the event being responded to
- const aTag = event.tags.find(tag => tag[0] === 'a')?.[1]
- if (!aTag) {
- console.warn('Completion event missing a tag:', event.id)
- return
- }
-
- // Parse task status (new approach)
- const taskStatusTag = event.tags.find(tag => tag[0] === 'task-status')?.[1] as TaskStatus | undefined
-
- // Backward compatibility: check old 'completed' tag if task-status not present
- let taskStatus: TaskStatus
- if (taskStatusTag) {
- taskStatus = taskStatusTag
- } else {
- // Legacy support: convert old 'completed' tag to new taskStatus
- const completed = event.tags.find(tag => tag[0] === 'completed')?.[1] === 'true'
- taskStatus = completed ? 'completed' : 'claimed'
- }
-
- const completedAtTag = event.tags.find(tag => tag[0] === 'completed_at')?.[1]
- const completedAt = completedAtTag ? parseInt(completedAtTag) : undefined
- const occurrence = event.tags.find(tag => tag[0] === 'occurrence')?.[1] // ISO date string
-
- console.log('📋 Completion details:', {
- aTag,
- occurrence,
- taskStatus,
- pubkey: event.pubkey,
- eventId: event.id
- })
-
- const completion: EventCompletion = {
- id: event.id,
- eventAddress: aTag,
- occurrence,
- pubkey: event.pubkey,
- created_at: event.created_at,
- taskStatus,
- completedAt,
- notes: event.content
- }
-
- // Store completion (most recent one wins)
- // For recurring events, include occurrence in the key: "eventAddress:occurrence"
- // For non-recurring, just use eventAddress
- const completionKey = occurrence ? `${aTag}:${occurrence}` : aTag
- const existing = this._completions.get(completionKey)
- if (!existing || event.created_at > existing.created_at) {
- this._completions.set(completionKey, completion)
- console.log('✅ Stored completion for:', completionKey, '- status:', taskStatus)
- } else {
- console.log('⏭️ Skipped older completion for:', completionKey)
- }
-
- } catch (error) {
- console.error('Failed to handle completion event:', error)
- }
- }
-
- /**
- * Handle deletion event (kind 5) for completion events
- * Made public so FeedService can route deletion events to this service
- */
- public handleDeletionEvent(event: NostrEvent): void {
- console.log('🗑️ ScheduledEventService: Received deletion event (kind 5)', event.id)
-
- try {
- // Extract event IDs to delete from 'e' tags
- const eventIdsToDelete = event.tags
- ?.filter((tag: string[]) => tag[0] === 'e')
- .map((tag: string[]) => tag[1]) || []
-
- if (eventIdsToDelete.length === 0) {
- console.warn('Deletion event missing e tags:', event.id)
- return
- }
-
- console.log('🔍 Looking for completions to delete:', eventIdsToDelete)
-
- // Find and remove completions that match the deleted event IDs
- let deletedCount = 0
- for (const [completionKey, completion] of this._completions.entries()) {
- // Only delete if:
- // 1. The completion event ID matches one being deleted
- // 2. The deletion request comes from the same author (NIP-09 validation)
- if (eventIdsToDelete.includes(completion.id) && completion.pubkey === event.pubkey) {
- this._completions.delete(completionKey)
- console.log('✅ Deleted completion:', completionKey, 'event ID:', completion.id)
- deletedCount++
- }
- }
-
- console.log(`🗑️ Deleted ${deletedCount} completion(s) from deletion event`)
-
- } catch (error) {
- console.error('Failed to handle deletion event:', error)
- }
- }
-
- /**
- * Handle deletion event (kind 5) for scheduled events (kind 31922)
- * Made public so FeedService can route deletion events to this service
- */
- public handleTaskDeletion(event: NostrEvent): void {
- console.log('🗑️ ScheduledEventService: Received task deletion event (kind 5)', event.id)
-
- try {
- // Extract event addresses to delete from 'a' tags
- const eventAddressesToDelete = event.tags
- ?.filter((tag: string[]) => tag[0] === 'a')
- .map((tag: string[]) => tag[1]) || []
-
- if (eventAddressesToDelete.length === 0) {
- console.warn('Task deletion event missing a tags:', event.id)
- return
- }
-
- console.log('🔍 Looking for tasks to delete:', eventAddressesToDelete)
-
- // Find and remove tasks that match the deleted event addresses
- let deletedCount = 0
- for (const eventAddress of eventAddressesToDelete) {
- const task = this._scheduledEvents.get(eventAddress)
-
- // Only delete if:
- // 1. The task exists
- // 2. The deletion request comes from the task author (NIP-09 validation)
- if (task && task.pubkey === event.pubkey) {
- this._scheduledEvents.delete(eventAddress)
- console.log('✅ Deleted task:', eventAddress)
- deletedCount++
- } else if (task) {
- console.warn('⚠️ Deletion request not from task author:', eventAddress)
- }
- }
-
- console.log(`🗑️ Deleted ${deletedCount} task(s) from deletion event`)
-
- } catch (error) {
- console.error('Failed to handle task deletion event:', error)
- }
- }
-
- /**
- * Get all scheduled events
- */
- getScheduledEvents(): ScheduledEvent[] {
- return Array.from(this._scheduledEvents.values())
- }
-
- /**
- * Get events scheduled for a specific date (YYYY-MM-DD)
- */
- getEventsForDate(date: string): ScheduledEvent[] {
- return this.getScheduledEvents().filter(event => {
- // Simple date matching (start date)
- // For ISO datetime strings, extract just the date part
- const eventDate = event.start.split('T')[0]
- return eventDate === date
- })
- }
-
- /**
- * Check if a recurring event occurs on a specific date
- */
- private doesRecurringEventOccurOnDate(event: ScheduledEvent, targetDate: string): boolean {
- if (!event.recurrence) return false
-
- const target = new Date(targetDate)
- const eventStart = new Date(event.start.split('T')[0]) // Get date part only
-
- // Check if target date is before the event start date
- if (target < eventStart) return false
-
- // Check if target date is after the event end date (if specified)
- if (event.recurrence.endDate) {
- const endDate = new Date(event.recurrence.endDate)
- if (target > endDate) return false
- }
-
- // Check frequency-specific rules
- if (event.recurrence.frequency === 'daily') {
- // Daily events occur every day within the range
- return true
- } else if (event.recurrence.frequency === 'weekly') {
- // Weekly events occur on specific day of week
- const targetDayOfWeek = target.toLocaleDateString('en-US', { weekday: 'long' }).toLowerCase()
- const eventDayOfWeek = event.recurrence.dayOfWeek?.toLowerCase()
- return targetDayOfWeek === eventDayOfWeek
- }
-
- return false
- }
-
- /**
- * Get events for a specific date, optionally filtered by user participation
- * @param date - ISO date string (YYYY-MM-DD). Defaults to today.
- * @param userPubkey - Optional user pubkey to filter by participation
- */
- getEventsForSpecificDate(date?: string, userPubkey?: string): ScheduledEvent[] {
- const targetDate = date || new Date().toISOString().split('T')[0]
-
- // Get one-time events for the date (exclude recurring events to avoid duplicates)
- const oneTimeEvents = this.getEventsForDate(targetDate).filter(event => !event.recurrence)
-
- // Get all events and check for recurring events that occur on this date
- const allEvents = this.getScheduledEvents()
- const recurringEventsOnDate = allEvents.filter(event =>
- event.recurrence && this.doesRecurringEventOccurOnDate(event, targetDate)
- )
-
- // Combine one-time and recurring events
- let events = [...oneTimeEvents, ...recurringEventsOnDate]
-
- // Filter events based on participation (if user pubkey provided)
- if (userPubkey) {
- events = events.filter(event => {
- // If event has no participants, it's community-wide (show to everyone)
- if (!event.participants || event.participants.length === 0) return true
-
- // Otherwise, only show if user is a participant
- return event.participants.some(p => p.pubkey === userPubkey)
- })
- }
-
- // Sort by start time (ascending order)
- events.sort((a, b) => {
- // ISO datetime strings can be compared lexicographically
- return a.start.localeCompare(b.start)
- })
-
- return events
- }
-
- /**
- * Get events for today, optionally filtered by user participation
- */
- getTodaysEvents(userPubkey?: string): ScheduledEvent[] {
- return this.getEventsForSpecificDate(undefined, userPubkey)
- }
-
- /**
- * Get completion status for an event (optionally for a specific occurrence)
- */
- getCompletion(eventAddress: string, occurrence?: string): EventCompletion | undefined {
- const completionKey = occurrence ? `${eventAddress}:${occurrence}` : eventAddress
- return this._completions.get(completionKey)
- }
-
- /**
- * Check if an event is completed (optionally for a specific occurrence)
- */
- isCompleted(eventAddress: string, occurrence?: string): boolean {
- const completion = this.getCompletion(eventAddress, occurrence)
- return completion?.taskStatus === 'completed'
- }
-
- /**
- * Get task status for an event
- */
- getTaskStatus(eventAddress: string, occurrence?: string): TaskStatus | null {
- const completion = this.getCompletion(eventAddress, occurrence)
- return completion?.taskStatus || null
- }
-
- /**
- * Claim a task (mark as claimed)
- */
- async claimTask(event: ScheduledEvent, notes: string = '', occurrence?: string): Promise {
- await this.updateTaskStatus(event, 'claimed', notes, occurrence)
- }
-
- /**
- * Start a task (mark as in-progress)
- */
- async startTask(event: ScheduledEvent, notes: string = '', occurrence?: string): Promise {
- await this.updateTaskStatus(event, 'in-progress', notes, occurrence)
- }
-
- /**
- * Mark an event as complete (optionally for a specific occurrence)
- */
- async completeEvent(event: ScheduledEvent, notes: string = '', occurrence?: string): Promise {
- await this.updateTaskStatus(event, 'completed', notes, occurrence)
- }
-
- /**
- * Internal method to update task status
- */
- private async updateTaskStatus(
- event: ScheduledEvent,
- taskStatus: TaskStatus,
- notes: string = '',
- occurrence?: string
- ): Promise {
- if (!this.authService?.isAuthenticated?.value) {
- throw new Error('Must be authenticated to update task status')
- }
-
- if (!this.relayHub?.isConnected) {
- throw new Error('Not connected to relays')
- }
-
- const userPrivkey = this.authService.user.value?.prvkey
- if (!userPrivkey) {
- throw new Error('User private key not available')
- }
-
- try {
- this._isLoading.value = true
-
- const eventAddress = `31922:${event.pubkey}:${event.dTag}`
-
- // Create RSVP event with task-status tag
- const tags: string[][] = [
- ['a', eventAddress],
- ['task-status', taskStatus]
- ]
-
- // Add completed_at timestamp if task is completed
- if (taskStatus === 'completed') {
- tags.push(['completed_at', Math.floor(Date.now() / 1000).toString()])
- }
-
- // Add occurrence tag if provided (for recurring events)
- if (occurrence) {
- tags.push(['occurrence', occurrence])
- }
-
- const eventTemplate: EventTemplate = {
- kind: 31925, // Calendar Event RSVP
- content: notes,
- tags,
- created_at: Math.floor(Date.now() / 1000)
- }
-
- // Sign the event
- const privkeyBytes = this.hexToUint8Array(userPrivkey)
- const signedEvent = finalizeEvent(eventTemplate, privkeyBytes)
-
- // Publish the status update
- console.log(`📤 Publishing task status update (${taskStatus}) for:`, eventAddress)
- const result = await this.relayHub.publishEvent(signedEvent)
- console.log('✅ Task status published to', result.success, '/', result.total, 'relays')
-
- // Update local state (publishEvent throws if no relays accepted)
- console.log('🔄 Updating local state (event published successfully)')
- this.handleCompletionEvent(signedEvent)
-
- } catch (error) {
- console.error('Failed to update task status:', error)
- throw error
- } finally {
- this._isLoading.value = false
- }
- }
-
- /**
- * Unclaim/reset a task (removes task status - makes it unclaimed)
- * Note: In Nostr, we can't truly "delete" an event, but we can publish
- * a deletion request (kind 5) to ask relays to remove our RSVP
- */
- async unclaimTask(event: ScheduledEvent, occurrence?: string): Promise {
- if (!this.authService?.isAuthenticated?.value) {
- throw new Error('Must be authenticated to unclaim tasks')
- }
-
- if (!this.relayHub?.isConnected) {
- throw new Error('Not connected to relays')
- }
-
- const userPrivkey = this.authService.user.value?.prvkey
- if (!userPrivkey) {
- throw new Error('User private key not available')
- }
-
- try {
- this._isLoading.value = true
-
- const eventAddress = `31922:${event.pubkey}:${event.dTag}`
- const completionKey = occurrence ? `${eventAddress}:${occurrence}` : eventAddress
- const completion = this._completions.get(completionKey)
-
- if (!completion) {
- console.log('No completion to unclaim')
- return
- }
-
- // Create deletion event (kind 5) for the RSVP
- const deletionEvent: EventTemplate = {
- kind: 5,
- content: 'Task unclaimed',
- tags: [
- ['e', completion.id], // Reference to the RSVP event being deleted
- ['k', '31925'] // Kind of event being deleted
- ],
- created_at: Math.floor(Date.now() / 1000)
- }
-
- // Sign the event
- const privkeyBytes = this.hexToUint8Array(userPrivkey)
- const signedEvent = finalizeEvent(deletionEvent, privkeyBytes)
-
- // Publish the deletion request
- console.log('📤 Publishing deletion request for task RSVP:', completion.id)
- const result = await this.relayHub.publishEvent(signedEvent)
- console.log('✅ Deletion request published to', result.success, '/', result.total, 'relays')
-
- // Remove from local state (publishEvent throws if no relays accepted)
- this._completions.delete(completionKey)
- console.log('🗑️ Removed completion from local state:', completionKey)
-
- } catch (error) {
- console.error('Failed to unclaim task:', error)
- throw error
- } finally {
- this._isLoading.value = false
- }
- }
-
- /**
- * Delete a scheduled event (kind 31922)
- * Only the author can delete their own event
- */
- async deleteTask(event: ScheduledEvent): Promise {
- if (!this.authService?.isAuthenticated?.value) {
- throw new Error('Must be authenticated to delete tasks')
- }
-
- if (!this.relayHub?.isConnected) {
- throw new Error('Not connected to relays')
- }
-
- const userPrivkey = this.authService.user.value?.prvkey
- const userPubkey = this.authService.user.value?.pubkey
-
- if (!userPrivkey || !userPubkey) {
- throw new Error('User credentials not available')
- }
-
- // Only author can delete
- if (userPubkey !== event.pubkey) {
- throw new Error('Only the task author can delete this task')
- }
-
- try {
- this._isLoading.value = true
-
- const eventAddress = `31922:${event.pubkey}:${event.dTag}`
-
- // Create deletion event (kind 5) for the scheduled event
- const deletionEvent: EventTemplate = {
- kind: 5,
- content: 'Task deleted',
- tags: [
- ['a', eventAddress], // Reference to the parameterized replaceable event being deleted
- ['k', '31922'] // Kind of event being deleted
- ],
- created_at: Math.floor(Date.now() / 1000)
- }
-
- // Sign the event
- const privkeyBytes = this.hexToUint8Array(userPrivkey)
- const signedEvent = finalizeEvent(deletionEvent, privkeyBytes)
-
- // Publish the deletion request
- console.log('📤 Publishing deletion request for task:', eventAddress)
- const result = await this.relayHub.publishEvent(signedEvent)
- console.log('✅ Task deletion request published to', result.success, '/', result.total, 'relays')
-
- // Remove from local state (publishEvent throws if no relays accepted)
- this._scheduledEvents.delete(eventAddress)
- console.log('🗑️ Removed task from local state:', eventAddress)
-
- } catch (error) {
- console.error('Failed to delete task:', error)
- throw error
- } finally {
- this._isLoading.value = false
- }
- }
-
- /**
- * Helper function to convert hex string to Uint8Array
- */
- private hexToUint8Array(hex: string): Uint8Array {
- const bytes = new Uint8Array(hex.length / 2)
- for (let i = 0; i < hex.length; i += 2) {
- bytes[i / 2] = parseInt(hex.substr(i, 2), 16)
- }
- return bytes
- }
-
- /**
- * Get all scheduled events
- */
- get scheduledEvents(): Map {
- return this._scheduledEvents
- }
-
- /**
- * Get all completions
- */
- get completions(): Map {
- return this._completions
- }
-
- /**
- * Check if currently loading
- */
- get isLoading(): boolean {
- return this._isLoading.value
- }
-
- /**
- * Cleanup
- */
- protected async onDestroy(): Promise {
- this._scheduledEvents.clear()
- this._completions.clear()
- }
-}
diff --git a/src/pages/Home.vue b/src/pages/Home.vue
index 9b78320..23d5c4d 100644
--- a/src/pages/Home.vue
+++ b/src/pages/Home.vue
@@ -36,20 +36,24 @@