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:
padreug 2025-11-07 16:00:07 +01:00
parent b286a0315d
commit 678ccff694
4 changed files with 219 additions and 83 deletions

View file

@ -36,24 +36,20 @@
<!-- Main Feed Area - Takes remaining height with scrolling -->
<div class="flex-1 overflow-y-auto scrollbar-thin scrollbar-thumb-border scrollbar-track-transparent">
<!-- Collapsible Composer -->
<div v-if="showComposer || replyTo" class="border-b bg-background sticky top-0 z-10">
<!-- Quick Action Component Area -->
<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="px-4 py-3 sm:px-6">
<!-- Regular Note Composer -->
<NoteComposer
v-if="composerType === 'note' || replyTo"
<!-- Dynamic Quick Action Component -->
<component
:is="activeAction?.component"
v-if="activeAction"
:reply-to="replyTo"
@note-published="onNotePublished"
@note-published="onActionComplete"
@rideshare-published="onActionComplete"
@action-complete="onActionComplete"
@clear-reply="onClearReply"
@close="onCloseComposer"
/>
<!-- Rideshare Composer -->
<RideshareComposer
v-else-if="composerType === 'rideshare'"
@rideshare-published="onRidesharePublished"
@close="onCloseComposer"
@close="closeQuickAction"
/>
</div>
</div>
@ -72,39 +68,32 @@
</div>
</div>
<!-- Floating Action Buttons for Compose -->
<div v-if="!showComposer && !replyTo" class="fixed bottom-6 right-6 z-50">
<!-- Main compose button -->
<!-- Floating Quick Action Button -->
<div v-if="!activeAction && !replyTo && quickActions.length > 0" class="fixed bottom-6 right-6 z-50">
<div class="flex flex-col items-end gap-3">
<!-- Secondary buttons (when expanded) -->
<div v-if="showComposerOptions" class="flex flex-col gap-2">
<!-- Quick Action Buttons (when expanded) -->
<div v-if="showQuickActions" class="flex flex-col gap-2">
<Button
@click="openComposer('note')"
v-for="action in quickActions"
:key="action.id"
@click="openQuickAction(action)"
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"
>
<MessageSquare class="h-4 w-4" />
<span class="text-sm font-medium">Note</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>
<component :is="getIconComponent(action.icon)" class="h-4 w-4" />
<span class="text-sm font-medium">{{ action.label }}</span>
</Button>
</div>
<!-- Main FAB -->
<Button
@click="toggleComposerOptions"
@click="toggleQuickActions"
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"
>
<Plus
class="h-6 w-6 stroke-[2.5] transition-transform duration-200"
:class="{ 'rotate-45': showComposerOptions }"
:class="{ 'rotate-45': showQuickActions }"
/>
</Button>
</div>
@ -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<QuickAction | null>(null)
// Feed configuration
const selectedFilters = ref<ContentFilter[]>(FILTER_PRESETS.all)
const feedKey = ref(0) // Force feed component to re-render when filters change
// Note composer state
const replyTo = ref<ReplyToNote | undefined>()
// Reply state (for note composer compatibility)
const replyTo = ref<any | undefined>()
// 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
}
</script>