Adds dynamic quick actions via modules
Introduces a dynamic quick action system, allowing modules to register actions that appear in a floating action button menu. This provides a flexible way for modules to extend the application's functionality with common tasks like composing notes or initiating payments.
This commit is contained in:
parent
b286a0315d
commit
678ccff694
4 changed files with 219 additions and 83 deletions
95
src/composables/useQuickActions.ts
Normal file
95
src/composables/useQuickActions.ts
Normal file
|
|
@ -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<QuickAction[]>(() => {
|
||||||
|
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<string>()
|
||||||
|
quickActions.value.forEach(action => {
|
||||||
|
if (action.category) {
|
||||||
|
cats.add(action.category)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return Array.from(cats).sort()
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
quickActions,
|
||||||
|
getActionsByCategory,
|
||||||
|
getActionById,
|
||||||
|
hasActions,
|
||||||
|
categories
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,37 +1,64 @@
|
||||||
import type { App, Component } from 'vue'
|
import type { App, Component } from 'vue'
|
||||||
import type { RouteRecordRaw } from 'vue-router'
|
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
|
// Base module plugin interface
|
||||||
export interface ModulePlugin {
|
export interface ModulePlugin {
|
||||||
/** Unique module name */
|
/** Unique module name */
|
||||||
name: string
|
name: string
|
||||||
|
|
||||||
/** Module version */
|
/** Module version */
|
||||||
version: string
|
version: string
|
||||||
|
|
||||||
/** Required dependencies (other module names) */
|
/** Required dependencies (other module names) */
|
||||||
dependencies?: string[]
|
dependencies?: string[]
|
||||||
|
|
||||||
/** Module configuration */
|
/** Module configuration */
|
||||||
config?: Record<string, any>
|
config?: Record<string, any>
|
||||||
|
|
||||||
/** Install the module */
|
/** Install the module */
|
||||||
install(app: App, options?: any): Promise<void> | void
|
install(app: App, options?: any): Promise<void> | void
|
||||||
|
|
||||||
/** Uninstall the module (cleanup) */
|
/** Uninstall the module (cleanup) */
|
||||||
uninstall?(): Promise<void> | void
|
uninstall?(): Promise<void> | void
|
||||||
|
|
||||||
/** Routes provided by this module */
|
/** Routes provided by this module */
|
||||||
routes?: RouteRecordRaw[]
|
routes?: RouteRecordRaw[]
|
||||||
|
|
||||||
/** Components provided by this module */
|
/** Components provided by this module */
|
||||||
components?: Record<string, Component>
|
components?: Record<string, Component>
|
||||||
|
|
||||||
/** Services provided by this module */
|
/** Services provided by this module */
|
||||||
services?: Record<string, any>
|
services?: Record<string, any>
|
||||||
|
|
||||||
/** Composables provided by this module */
|
/** Composables provided by this module */
|
||||||
composables?: Record<string, any>
|
composables?: Record<string, any>
|
||||||
|
|
||||||
|
/** Quick actions provided by this module */
|
||||||
|
quickActions?: QuickAction[]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Module configuration for app setup
|
// Module configuration for app setup
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
import type { App } from 'vue'
|
import type { App } from 'vue'
|
||||||
|
import { markRaw } from 'vue'
|
||||||
import type { ModulePlugin } from '@/core/types'
|
import type { ModulePlugin } from '@/core/types'
|
||||||
import { container, SERVICE_TOKENS } from '@/core/di-container'
|
import { container, SERVICE_TOKENS } from '@/core/di-container'
|
||||||
import NostrFeed from './components/NostrFeed.vue'
|
import NostrFeed from './components/NostrFeed.vue'
|
||||||
|
import NoteComposer from './components/NoteComposer.vue'
|
||||||
|
import RideshareComposer from './components/RideshareComposer.vue'
|
||||||
import { useFeed } from './composables/useFeed'
|
import { useFeed } from './composables/useFeed'
|
||||||
import { FeedService } from './services/FeedService'
|
import { FeedService } from './services/FeedService'
|
||||||
import { ProfileService } from './services/ProfileService'
|
import { ProfileService } from './services/ProfileService'
|
||||||
|
|
@ -17,6 +20,28 @@ export const nostrFeedModule: ModulePlugin = {
|
||||||
version: '1.0.0',
|
version: '1.0.0',
|
||||||
dependencies: ['base'],
|
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) {
|
async install(app: App) {
|
||||||
console.log('nostr-feed module: Starting installation...')
|
console.log('nostr-feed module: Starting installation...')
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,24 +36,20 @@
|
||||||
|
|
||||||
<!-- Main Feed Area - Takes remaining height with scrolling -->
|
<!-- Main Feed Area - Takes remaining height with scrolling -->
|
||||||
<div class="flex-1 overflow-y-auto scrollbar-thin scrollbar-thumb-border scrollbar-track-transparent">
|
<div class="flex-1 overflow-y-auto scrollbar-thin scrollbar-thumb-border scrollbar-track-transparent">
|
||||||
<!-- Collapsible Composer -->
|
<!-- Quick Action Component Area -->
|
||||||
<div v-if="showComposer || replyTo" class="border-b bg-background sticky top-0 z-10">
|
<div v-if="activeAction || replyTo" class="border-b bg-background sticky top-0 z-10">
|
||||||
<div class="max-h-[70vh] overflow-y-auto scrollbar-thin scrollbar-thumb-border scrollbar-track-transparent">
|
<div class="max-h-[70vh] overflow-y-auto scrollbar-thin scrollbar-thumb-border scrollbar-track-transparent">
|
||||||
<div class="px-4 py-3 sm:px-6">
|
<div class="px-4 py-3 sm:px-6">
|
||||||
<!-- Regular Note Composer -->
|
<!-- Dynamic Quick Action Component -->
|
||||||
<NoteComposer
|
<component
|
||||||
v-if="composerType === 'note' || replyTo"
|
:is="activeAction?.component"
|
||||||
|
v-if="activeAction"
|
||||||
:reply-to="replyTo"
|
:reply-to="replyTo"
|
||||||
@note-published="onNotePublished"
|
@note-published="onActionComplete"
|
||||||
|
@rideshare-published="onActionComplete"
|
||||||
|
@action-complete="onActionComplete"
|
||||||
@clear-reply="onClearReply"
|
@clear-reply="onClearReply"
|
||||||
@close="onCloseComposer"
|
@close="closeQuickAction"
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Rideshare Composer -->
|
|
||||||
<RideshareComposer
|
|
||||||
v-else-if="composerType === 'rideshare'"
|
|
||||||
@rideshare-published="onRidesharePublished"
|
|
||||||
@close="onCloseComposer"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -72,39 +68,32 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Floating Action Buttons for Compose -->
|
<!-- Floating Quick Action Button -->
|
||||||
<div v-if="!showComposer && !replyTo" class="fixed bottom-6 right-6 z-50">
|
<div v-if="!activeAction && !replyTo && quickActions.length > 0" class="fixed bottom-6 right-6 z-50">
|
||||||
<!-- Main compose button -->
|
|
||||||
<div class="flex flex-col items-end gap-3">
|
<div class="flex flex-col items-end gap-3">
|
||||||
<!-- Secondary buttons (when expanded) -->
|
<!-- Quick Action Buttons (when expanded) -->
|
||||||
<div v-if="showComposerOptions" class="flex flex-col gap-2">
|
<div v-if="showQuickActions" class="flex flex-col gap-2">
|
||||||
<Button
|
<Button
|
||||||
@click="openComposer('note')"
|
v-for="action in quickActions"
|
||||||
|
:key="action.id"
|
||||||
|
@click="openQuickAction(action)"
|
||||||
size="lg"
|
size="lg"
|
||||||
class="h-12 px-4 rounded-full shadow-lg hover:shadow-xl transition-all gap-2 bg-card border-2 border-border hover:bg-accent text-card-foreground"
|
class="h-12 px-4 rounded-full shadow-lg hover:shadow-xl transition-all gap-2 bg-card border-2 border-border hover:bg-accent text-card-foreground"
|
||||||
>
|
>
|
||||||
<MessageSquare class="h-4 w-4" />
|
<component :is="getIconComponent(action.icon)" class="h-4 w-4" />
|
||||||
<span class="text-sm font-medium">Note</span>
|
<span class="text-sm font-medium">{{ action.label }}</span>
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
@click="openComposer('rideshare')"
|
|
||||||
size="lg"
|
|
||||||
class="h-12 px-4 rounded-full shadow-lg hover:shadow-xl transition-all gap-2 bg-card border-2 border-border hover:bg-accent text-card-foreground"
|
|
||||||
>
|
|
||||||
<Car class="h-4 w-4" />
|
|
||||||
<span class="text-sm font-medium">Rideshare</span>
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Main FAB -->
|
<!-- Main FAB -->
|
||||||
<Button
|
<Button
|
||||||
@click="toggleComposerOptions"
|
@click="toggleQuickActions"
|
||||||
size="lg"
|
size="lg"
|
||||||
class="h-14 w-14 rounded-full shadow-lg hover:shadow-xl transition-all bg-primary hover:bg-primary/90 border-2 border-primary-foreground/20 flex items-center justify-center p-0"
|
class="h-14 w-14 rounded-full shadow-lg hover:shadow-xl transition-all bg-primary hover:bg-primary/90 border-2 border-primary-foreground/20 flex items-center justify-center p-0"
|
||||||
>
|
>
|
||||||
<Plus
|
<Plus
|
||||||
class="h-6 w-6 stroke-[2.5] transition-transform duration-200"
|
class="h-6 w-6 stroke-[2.5] transition-transform duration-200"
|
||||||
:class="{ 'rotate-45': showComposerOptions }"
|
:class="{ 'rotate-45': showQuickActions }"
|
||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -136,32 +125,34 @@
|
||||||
import { ref, computed, watch } from 'vue'
|
import { ref, computed, watch } from 'vue'
|
||||||
|
|
||||||
import { Button } from '@/components/ui/button'
|
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 PWAInstallPrompt from '@/components/pwa/PWAInstallPrompt.vue'
|
||||||
import FeedFilters from '@/modules/nostr-feed/components/FeedFilters.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 NostrFeed from '@/modules/nostr-feed/components/NostrFeed.vue'
|
||||||
import { FILTER_PRESETS } from '@/modules/nostr-feed/config/content-filters'
|
import { FILTER_PRESETS } from '@/modules/nostr-feed/config/content-filters'
|
||||||
|
import { useQuickActions } from '@/composables/useQuickActions'
|
||||||
import appConfig from '@/app.config'
|
import appConfig from '@/app.config'
|
||||||
import type { ContentFilter } from '@/modules/nostr-feed/services/FeedService'
|
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
|
// Get admin pubkeys from app config
|
||||||
const adminPubkeys = appConfig.modules['nostr-feed']?.config?.adminPubkeys || []
|
const adminPubkeys = appConfig.modules['nostr-feed']?.config?.adminPubkeys || []
|
||||||
|
|
||||||
// UI state
|
// UI state
|
||||||
const showFilters = ref(false)
|
const showFilters = ref(false)
|
||||||
const showComposer = ref(false)
|
const showQuickActions = ref(false)
|
||||||
const showComposerOptions = ref(false)
|
const activeAction = ref<QuickAction | null>(null)
|
||||||
const composerType = ref<'note' | 'rideshare'>('note')
|
|
||||||
|
|
||||||
// Feed configuration
|
// Feed configuration
|
||||||
const selectedFilters = ref<ContentFilter[]>(FILTER_PRESETS.all)
|
const selectedFilters = ref<ContentFilter[]>(FILTER_PRESETS.all)
|
||||||
const feedKey = ref(0) // Force feed component to re-render when filters change
|
const feedKey = ref(0) // Force feed component to re-render when filters change
|
||||||
|
|
||||||
// Note composer state
|
// Reply state (for note composer compatibility)
|
||||||
const replyTo = ref<ReplyToNote | undefined>()
|
const replyTo = ref<any | undefined>()
|
||||||
|
|
||||||
// Quick filter presets for mobile bottom bar
|
// Quick filter presets for mobile bottom bar
|
||||||
const quickFilterPresets = {
|
const quickFilterPresets = {
|
||||||
|
|
@ -221,48 +212,46 @@ const setQuickFilter = (presetKey: string) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const onNotePublished = (noteId: string) => {
|
// Quick action methods
|
||||||
console.log('Note published:', noteId)
|
const toggleQuickActions = () => {
|
||||||
// Refresh the feed to show the new note
|
showQuickActions.value = !showQuickActions.value
|
||||||
feedKey.value++
|
}
|
||||||
// Clear reply state and hide composer
|
|
||||||
|
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
|
replyTo.value = undefined
|
||||||
showComposer.value = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const onClearReply = () => {
|
const onClearReply = () => {
|
||||||
replyTo.value = undefined
|
replyTo.value = undefined
|
||||||
showComposer.value = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const onReplyToNote = (note: ReplyToNote) => {
|
const onReplyToNote = (note: any) => {
|
||||||
replyTo.value = note
|
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 = () => {
|
// Helper to get Lucide icon component
|
||||||
showComposer.value = false
|
const getIconComponent = (iconName: string) => {
|
||||||
showComposerOptions.value = false
|
return (LucideIcons as any)[iconName] || Plus
|
||||||
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
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue