Implement Note Composer and enhance NostrFeed interactions

- Added NoteComposer.vue for composing notes, including reply functionality and mention support.
- Integrated NoteComposer into Home.vue, allowing users to publish notes and reply to existing ones.
- Enhanced NostrFeed.vue to handle reply events, improving user engagement with notes.
- Updated Home.vue to manage note publishing and reply states effectively.

These changes enhance the user experience by providing a seamless way to compose and interact with notes within the feed.

Enhance NostrFeed and Home components for improved user experience

- Added compact mode support in NostrFeed.vue to optimize display for mobile users.
- Refactored layout in NostrFeed.vue for better responsiveness and usability, including adjustments to padding and spacing.
- Updated Home.vue to integrate a collapsible filter panel and a floating action button for composing notes, enhancing accessibility and interaction.
- Implemented quick filter presets for mobile, allowing users to easily switch between content types.

These changes improve the overall functionality and user engagement within the feed, providing a more streamlined experience across devices.

Enhance NoteComposer and Home components for improved note management

- Updated NoteComposer.vue to include a close button for better user control when composing notes.
- Modified Home.vue to handle the close event from NoteComposer, allowing users to dismiss the composer easily.

These changes enhance the user experience by providing a more intuitive interface for composing and managing notes.
This commit is contained in:
padreug 2025-09-16 23:08:10 +02:00
parent 77ed913e1b
commit 2d0aadccb7
3 changed files with 634 additions and 128 deletions

View file

@ -1,32 +1,92 @@
<template>
<div class="w-full px-4 sm:px-6 lg:px-8 xl:px-12 2xl:px-16 py-8 space-y-6">
<div class="flex flex-col h-screen bg-background">
<PWAInstallPrompt auto-show />
<!-- TODO: Implement push notifications properly - currently commenting out admin notifications dialog -->
<!-- <NotificationPermission auto-show /> -->
<!-- Feed Filter Controls -->
<Card>
<CardHeader>
<CardTitle class="flex items-center gap-2">
<Filter class="h-5 w-5" />
Content Filters
</CardTitle>
<CardDescription>
Choose what types of content you want to see in your feed
</CardDescription>
</CardHeader>
<CardContent>
<FeedFilters v-model="selectedFilters" :admin-pubkeys="adminPubkeys" />
</CardContent>
</Card>
<!-- Compact Header with Filters Toggle (Mobile) -->
<div class="sticky top-0 z-40 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 border-b">
<div class="flex items-center justify-between px-4 py-2 sm:px-6">
<h1 class="text-lg font-semibold">Feed</h1>
<div class="flex items-center gap-2">
<!-- Active Filter Indicator -->
<div class="flex items-center gap-1 text-xs text-muted-foreground">
<span v-if="activeFilterCount > 0">{{ activeFilterCount }} filters</span>
<span v-else>All content</span>
</div>
<!-- Filter Toggle Button -->
<Button
variant="ghost"
size="sm"
@click="showFilters = !showFilters"
class="h-8 w-8 p-0"
>
<Filter class="h-4 w-4" />
</Button>
</div>
</div>
<!-- Feed Content -->
<NostrFeed
:feed-type="feedType"
:content-filters="selectedFilters"
:admin-pubkeys="adminPubkeys"
:key="feedKey"
/>
<!-- Collapsible Filter Panel -->
<div v-if="showFilters" class="border-t bg-background/95 backdrop-blur">
<div class="px-4 py-3 sm:px-6">
<FeedFilters v-model="selectedFilters" :admin-pubkeys="adminPubkeys" />
</div>
</div>
</div>
<!-- Main Feed Area - Takes remaining height -->
<div class="flex-1 overflow-hidden">
<!-- Collapsible Note Composer -->
<div v-if="showComposer || replyTo" class="border-b bg-background">
<div class="px-4 py-3 sm:px-6">
<NoteComposer
:reply-to="replyTo"
@note-published="onNotePublished"
@clear-reply="onClearReply"
@close="onCloseComposer"
/>
</div>
</div>
<!-- Feed Content - Full height scroll -->
<div class="h-full">
<NostrFeed
:feed-type="feedType"
:content-filters="selectedFilters"
:admin-pubkeys="adminPubkeys"
:key="feedKey"
:compact-mode="true"
@reply-to-note="onReplyToNote"
/>
</div>
</div>
<!-- Floating Action Button for Compose -->
<div v-if="!showComposer && !replyTo" class="fixed bottom-6 right-6 z-50">
<Button
@click="showComposer = true"
size="lg"
class="h-14 w-14 rounded-full shadow-lg hover:shadow-xl transition-shadow"
>
<Plus class="h-6 w-6" />
</Button>
</div>
<!-- Quick Filters Bar (Mobile) -->
<div class="md:hidden sticky bottom-0 z-30 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 border-t">
<div class="flex overflow-x-auto px-4 py-2 gap-2 scrollbar-hide">
<Button
v-for="(preset, key) in quickFilterPresets"
:key="key"
:variant="isPresetActive(key) ? 'default' : 'outline'"
size="sm"
@click="setQuickFilter(key)"
class="whitespace-nowrap"
>
{{ preset.label }}
</Button>
</div>
</div>
</div>
</template>
@ -37,20 +97,50 @@
// import NotificationPermission from '@/components/notifications/NotificationPermission.vue'
import { ref, computed, watch } from 'vue'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Filter } from 'lucide-vue-next'
import { Filter, Plus } 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 NostrFeed from '@/modules/nostr-feed/components/NostrFeed.vue'
import { FILTER_PRESETS } from '@/modules/nostr-feed/config/content-filters'
import appConfig from '@/app.config'
import type { ContentFilter } from '@/modules/nostr-feed/services/FeedService'
import type { ReplyToNote } from '@/modules/nostr-feed/components/NoteComposer.vue'
// Get admin pubkeys from app config
const adminPubkeys = appConfig.modules['nostr-feed']?.config?.adminPubkeys || []
// UI state
const showFilters = ref(false)
const showComposer = ref(false)
// 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>()
// Quick filter presets for mobile bottom bar
const quickFilterPresets = {
all: { label: 'All', filters: FILTER_PRESETS.all },
announcements: { label: 'News', filters: FILTER_PRESETS.announcements },
marketplace: { label: 'Market', filters: FILTER_PRESETS.marketplace },
social: { label: 'Social', filters: FILTER_PRESETS.social },
events: { label: 'Events', filters: FILTER_PRESETS.events }
}
// Computed properties
const activeFilterCount = computed(() => selectedFilters.value.length)
const isPresetActive = (presetKey: string) => {
const preset = quickFilterPresets[presetKey as keyof typeof quickFilterPresets]
if (!preset) return false
return preset.filters.length === selectedFilters.value.length &&
preset.filters.every(pf => selectedFilters.value.some(sf => sf.id === pf.id))
}
// Determine feed type based on selected filters
const feedType = computed(() => {
if (selectedFilters.value.length === 0) return 'all'
@ -70,4 +160,37 @@ const feedType = computed(() => {
watch(selectedFilters, () => {
feedKey.value++
}, { deep: true })
// Handle note composer events
// Methods
const setQuickFilter = (presetKey: string) => {
const preset = quickFilterPresets[presetKey as keyof typeof quickFilterPresets]
if (preset) {
selectedFilters.value = preset.filters
}
}
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
replyTo.value = undefined
showComposer.value = false
}
const onClearReply = () => {
replyTo.value = undefined
showComposer.value = false
}
const onReplyToNote = (note: ReplyToNote) => {
replyTo.value = note
showComposer.value = true
}
const onCloseComposer = () => {
showComposer.value = false
replyTo.value = undefined
}
</script>