Add collapsible components and feed filter functionality
- Introduced Collapsible, CollapsibleContent, and CollapsibleTrigger components for improved UI interactions. - Added FeedFilters component to allow users to customize content visibility in the NostrFeed. - Updated NostrFeed and Home components to integrate new filtering capabilities, enhancing user experience with customizable content display. - Implemented content filter logic in FeedService to support dynamic filtering based on user selections. These changes enhance the modularity and interactivity of the feed system, providing users with greater control over the content they see.
This commit is contained in:
parent
a5e6c301e1
commit
e90c4992da
10 changed files with 574 additions and 24 deletions
214
src/modules/nostr-feed/components/FeedFilters.vue
Normal file
214
src/modules/nostr-feed/components/FeedFilters.vue
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
<template>
|
||||
<div class="space-y-4">
|
||||
<!-- Quick Presets -->
|
||||
<div class="space-y-2">
|
||||
<h3 class="text-sm font-medium">Quick Filters</h3>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<Button
|
||||
v-for="preset in presets"
|
||||
:key="preset.id"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
:class="selectedPreset === preset.id ? 'bg-primary text-primary-foreground' : ''"
|
||||
@click="selectPreset(preset.id)"
|
||||
>
|
||||
{{ preset.label }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Custom Filter Selection -->
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-sm font-medium">Content Types</h3>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
@click="showCustom = !showCustom"
|
||||
>
|
||||
{{ showCustom ? 'Hide' : 'Customize' }}
|
||||
<ChevronDown :class="showCustom ? 'rotate-180' : ''" class="ml-1 h-3 w-3 transition-transform" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Collapsible v-model:open="showCustom">
|
||||
<CollapsibleContent class="space-y-3">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
<div
|
||||
v-for="filter in availableFilters"
|
||||
:key="filter.id"
|
||||
class="flex items-start space-x-3 p-3 border rounded-lg hover:bg-accent/50 transition-colors"
|
||||
>
|
||||
<Checkbox
|
||||
:id="filter.id"
|
||||
:checked="isFilterSelected(filter.id)"
|
||||
@update:checked="toggleFilter(filter)"
|
||||
/>
|
||||
<div class="flex-1 min-w-0">
|
||||
<Label
|
||||
:for="filter.id"
|
||||
class="text-sm font-medium cursor-pointer"
|
||||
>
|
||||
{{ filter.label }}
|
||||
</Label>
|
||||
<p class="text-xs text-muted-foreground mt-1">
|
||||
{{ filter.description }}
|
||||
</p>
|
||||
<div class="flex items-center gap-2 mt-1">
|
||||
<Badge variant="secondary" class="text-xs">
|
||||
Kind{{ filter.kinds.length > 1 ? 's' : '' }}: {{ filter.kinds.join(', ') }}
|
||||
</Badge>
|
||||
<Badge v-if="filter.requiresAuth" variant="outline" class="text-xs">
|
||||
Auth Required
|
||||
</Badge>
|
||||
<Badge
|
||||
v-if="filter.filterByAuthor === 'admin'"
|
||||
variant="default"
|
||||
class="text-xs"
|
||||
>
|
||||
Admin Only
|
||||
</Badge>
|
||||
<Badge
|
||||
v-if="filter.filterByAuthor === 'exclude-admin'"
|
||||
variant="outline"
|
||||
class="text-xs"
|
||||
>
|
||||
Community
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Apply Custom Filters Button -->
|
||||
<div class="flex justify-end pt-2">
|
||||
<Button @click="applyCustomFilters" :disabled="selectedFilters.length === 0">
|
||||
Apply Custom Filters ({{ selectedFilters.length }})
|
||||
</Button>
|
||||
</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</div>
|
||||
|
||||
<!-- Active Filters Display -->
|
||||
<div v-if="activeFilters.length > 0" class="space-y-2">
|
||||
<h3 class="text-sm font-medium">Active Filters</h3>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<Badge
|
||||
v-for="filter in activeFilters"
|
||||
:key="filter.id"
|
||||
variant="default"
|
||||
class="text-xs"
|
||||
>
|
||||
{{ filter.label }}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="ml-1 h-3 w-3 p-0"
|
||||
@click="removeFilter(filter)"
|
||||
>
|
||||
<X class="h-2 w-2" />
|
||||
</Button>
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import { Collapsible, CollapsibleContent } from '@/components/ui/collapsible'
|
||||
import { ChevronDown, X } from 'lucide-vue-next'
|
||||
import { FILTER_PRESETS, getAllContentFilters } from '../config/content-filters'
|
||||
import type { ContentFilter } from '../services/FeedService'
|
||||
|
||||
interface Props {
|
||||
modelValue: ContentFilter[]
|
||||
adminPubkeys?: string[]
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
(e: 'update:modelValue', value: ContentFilter[]): void
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
// UI state
|
||||
const showCustom = ref(false)
|
||||
const selectedPreset = ref<string>('all')
|
||||
const selectedFilters = ref<ContentFilter[]>([])
|
||||
|
||||
// Available filters (excluding auth-required ones if no auth)
|
||||
const availableFilters = computed(() => {
|
||||
return getAllContentFilters().filter(() => {
|
||||
// For now, include all filters - auth check can be added later
|
||||
return true
|
||||
})
|
||||
})
|
||||
|
||||
// Quick presets for common use cases
|
||||
const presets = computed(() => [
|
||||
{ id: 'all', label: 'All Content' },
|
||||
{ id: 'announcements', label: 'Announcements' },
|
||||
{ id: 'community', label: 'Community' },
|
||||
{ id: 'marketplace', label: 'Marketplace' },
|
||||
{ id: 'social', label: 'Social' },
|
||||
{ id: 'events', label: 'Events' },
|
||||
{ id: 'content', label: 'Articles' }
|
||||
])
|
||||
|
||||
// Current active filters
|
||||
const activeFilters = computed(() => props.modelValue)
|
||||
|
||||
// Check if a filter is selected
|
||||
function isFilterSelected(filterId: string): boolean {
|
||||
return selectedFilters.value.some(f => f.id === filterId)
|
||||
}
|
||||
|
||||
// Toggle individual filter
|
||||
function toggleFilter(filter: ContentFilter): void {
|
||||
const index = selectedFilters.value.findIndex(f => f.id === filter.id)
|
||||
if (index >= 0) {
|
||||
selectedFilters.value.splice(index, 1)
|
||||
} else {
|
||||
selectedFilters.value.push(filter)
|
||||
}
|
||||
}
|
||||
|
||||
// Select a preset
|
||||
function selectPreset(presetId: string): void {
|
||||
selectedPreset.value = presetId
|
||||
const presetFilters = FILTER_PRESETS[presetId] || []
|
||||
emit('update:modelValue', presetFilters)
|
||||
showCustom.value = false
|
||||
}
|
||||
|
||||
// Apply custom filters
|
||||
function applyCustomFilters(): void {
|
||||
selectedPreset.value = 'custom'
|
||||
emit('update:modelValue', [...selectedFilters.value])
|
||||
showCustom.value = false
|
||||
}
|
||||
|
||||
// Remove a filter
|
||||
function removeFilter(filter: ContentFilter): void {
|
||||
const newFilters = activeFilters.value.filter(f => f.id !== filter.id)
|
||||
emit('update:modelValue', newFilters)
|
||||
|
||||
// Update selectedFilters for custom mode
|
||||
const index = selectedFilters.value.findIndex(f => f.id === filter.id)
|
||||
if (index >= 0) {
|
||||
selectedFilters.value.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize selected filters when component mounts
|
||||
watch(() => props.modelValue, (newFilters) => {
|
||||
selectedFilters.value = [...newFilters]
|
||||
}, { immediate: true })
|
||||
</script>
|
||||
|
|
@ -8,20 +8,24 @@ import { formatDistanceToNow } from 'date-fns'
|
|||
import { Megaphone, RefreshCw, AlertCircle } from 'lucide-vue-next'
|
||||
import { useFeed } from '../composables/useFeed'
|
||||
import appConfig from '@/app.config'
|
||||
import type { ContentFilter } from '../services/FeedService'
|
||||
|
||||
const props = defineProps<{
|
||||
relays?: string[]
|
||||
feedType?: 'all' | 'announcements' | 'events' | 'general'
|
||||
feedType?: 'all' | 'announcements' | 'events' | 'general' | 'custom'
|
||||
contentFilters?: ContentFilter[]
|
||||
adminPubkeys?: string[]
|
||||
}>()
|
||||
|
||||
// Get admin/moderator pubkeys from app config
|
||||
const adminPubkeys = appConfig.modules['nostr-feed'].config.adminPubkeys
|
||||
// Get admin/moderator pubkeys from props or app config
|
||||
const adminPubkeys = props.adminPubkeys || appConfig.modules['nostr-feed']?.config?.adminPubkeys || []
|
||||
|
||||
// Use centralized feed service - this handles all subscription management and deduplication
|
||||
const { posts: notes, isLoading, error, refreshFeed } = useFeed({
|
||||
feedType: props.feedType || 'all',
|
||||
maxPosts: 100,
|
||||
adminPubkeys
|
||||
adminPubkeys,
|
||||
contentFilters: props.contentFilters
|
||||
})
|
||||
|
||||
// Check if we have admin pubkeys configured
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue