diff --git a/src/composables/useQuickActions.ts b/src/composables/useQuickActions.ts new file mode 100644 index 0000000..7046fbb --- /dev/null +++ b/src/composables/useQuickActions.ts @@ -0,0 +1,95 @@ +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/types.ts b/src/core/types.ts index 9ce1e47..c866653 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -1,37 +1,64 @@ 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/nostr-feed/index.ts b/src/modules/nostr-feed/index.ts index e3a7191..c790302 100644 --- a/src/modules/nostr-feed/index.ts +++ b/src/modules/nostr-feed/index.ts @@ -1,7 +1,10 @@ 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' @@ -17,6 +20,28 @@ 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...') diff --git a/src/pages/Home.vue b/src/pages/Home.vue index 23d5c4d..9b78320 100644 --- a/src/pages/Home.vue +++ b/src/pages/Home.vue @@ -36,24 +36,20 @@
- -
+ +
- - + - - -
@@ -72,39 +68,32 @@
- -
- + +
- -
+ +
-
@@ -136,32 +125,34 @@ import { ref, computed, watch } from 'vue' import { Button } from '@/components/ui/button' -import { Filter, Plus, MessageSquare, Car } from 'lucide-vue-next' +import { Filter, Plus } from 'lucide-vue-next' +import * as LucideIcons from 'lucide-vue-next' import PWAInstallPrompt from '@/components/pwa/PWAInstallPrompt.vue' import FeedFilters from '@/modules/nostr-feed/components/FeedFilters.vue' -import NoteComposer from '@/modules/nostr-feed/components/NoteComposer.vue' -import RideshareComposer from '@/modules/nostr-feed/components/RideshareComposer.vue' import NostrFeed from '@/modules/nostr-feed/components/NostrFeed.vue' import { FILTER_PRESETS } from '@/modules/nostr-feed/config/content-filters' +import { useQuickActions } from '@/composables/useQuickActions' import appConfig from '@/app.config' import type { ContentFilter } from '@/modules/nostr-feed/services/FeedService' -import type { ReplyToNote } from '@/modules/nostr-feed/components/NoteComposer.vue' +import type { QuickAction } from '@/core/types' + +// Get quick actions from modules +const { quickActions } = useQuickActions() // Get admin pubkeys from app config const adminPubkeys = appConfig.modules['nostr-feed']?.config?.adminPubkeys || [] // UI state const showFilters = ref(false) -const showComposer = ref(false) -const showComposerOptions = ref(false) -const composerType = ref<'note' | 'rideshare'>('note') +const showQuickActions = ref(false) +const activeAction = ref(null) // Feed configuration const selectedFilters = ref(FILTER_PRESETS.all) const feedKey = ref(0) // Force feed component to re-render when filters change -// Note composer state -const replyTo = ref() +// Reply state (for note composer compatibility) +const replyTo = ref() // Quick filter presets for mobile bottom bar const quickFilterPresets = { @@ -221,48 +212,46 @@ const setQuickFilter = (presetKey: string) => { } } -const onNotePublished = (noteId: string) => { - console.log('Note published:', noteId) - // Refresh the feed to show the new note - feedKey.value++ - // Clear reply state and hide composer +// Quick action methods +const toggleQuickActions = () => { + showQuickActions.value = !showQuickActions.value +} + +const openQuickAction = (action: QuickAction) => { + activeAction.value = action + showQuickActions.value = false +} + +const closeQuickAction = () => { + activeAction.value = null + replyTo.value = undefined +} + +// Event handlers for quick action components +const onActionComplete = (eventData?: any) => { + console.log('Quick action completed:', activeAction.value?.id, eventData) + // Refresh the feed to show new content + feedKey.value++ + // Close the action + activeAction.value = null replyTo.value = undefined - showComposer.value = false } const onClearReply = () => { replyTo.value = undefined - showComposer.value = false } -const onReplyToNote = (note: ReplyToNote) => { +const onReplyToNote = (note: any) => { replyTo.value = note - showComposer.value = true + // Find and open the note composer action + const noteAction = quickActions.value.find(a => a.id === 'note') + if (noteAction) { + activeAction.value = noteAction + } } -const onCloseComposer = () => { - showComposer.value = false - showComposerOptions.value = false - replyTo.value = undefined -} - -// New composer methods -const toggleComposerOptions = () => { - showComposerOptions.value = !showComposerOptions.value -} - -const openComposer = (type: 'note' | 'rideshare') => { - composerType.value = type - showComposer.value = true - showComposerOptions.value = false -} - -const onRidesharePublished = (noteId: string) => { - console.log('Rideshare post published:', noteId) - // Refresh the feed to show the new rideshare post - feedKey.value++ - // Hide composer - showComposer.value = false - showComposerOptions.value = false +// Helper to get Lucide icon component +const getIconComponent = (iconName: string) => { + return (LucideIcons as any)[iconName] || Plus }