Add Rideshare functionality to NostrFeed module
- Introduced a new RideshareComposer component for creating rideshare posts, allowing users to specify ride details such as type, locations, date, time, and contact methods. - Enhanced NostrFeed.vue to display rideshare badges for relevant posts, improving visibility and categorization of rideshare content. - Updated Home.vue to integrate the RideshareComposer, providing users with an option to compose rideshare requests or offers alongside regular notes. These changes enhance the user experience by facilitating rideshare interactions within the NostrFeed module, promoting community engagement.
This commit is contained in:
parent
667a7c2d89
commit
dfd3ddd112
3 changed files with 564 additions and 10 deletions
|
|
@ -92,6 +92,44 @@ function isAdminPost(pubkey: string): boolean {
|
|||
return adminPubkeys.includes(pubkey)
|
||||
}
|
||||
|
||||
// Check if a post is a rideshare post
|
||||
function isRidesharePost(note: any): boolean {
|
||||
// Check for rideshare tags
|
||||
const hasTags = note.tags?.some((tag: string[]) =>
|
||||
tag[0] === 't' && ['rideshare', 'carpool'].includes(tag[1])
|
||||
) || false
|
||||
|
||||
// Check for rideshare-specific custom tags
|
||||
const hasRideshareTypeTags = note.tags?.some((tag: string[]) =>
|
||||
tag[0] === 'rideshare_type' && ['offering', 'seeking'].includes(tag[1])
|
||||
) || false
|
||||
|
||||
// Check content for rideshare keywords (fallback)
|
||||
const hasRideshareContent = note.content && (
|
||||
note.content.includes('🚗 OFFERING RIDE') ||
|
||||
note.content.includes('🚶 SEEKING RIDE') ||
|
||||
note.content.includes('#rideshare') ||
|
||||
note.content.includes('#carpool')
|
||||
)
|
||||
|
||||
return hasTags || hasRideshareTypeTags || hasRideshareContent
|
||||
}
|
||||
|
||||
// Get rideshare type from post
|
||||
function getRideshareType(note: any): string | null {
|
||||
// Check custom tags first
|
||||
const typeTag = note.tags?.find((tag: string[]) => tag[0] === 'rideshare_type')
|
||||
if (typeTag) {
|
||||
return typeTag[1] === 'offering' ? 'Offering Ride' : 'Seeking Ride'
|
||||
}
|
||||
|
||||
// Fallback to content analysis
|
||||
if (note.content?.includes('🚗 OFFERING RIDE')) return 'Offering Ride'
|
||||
if (note.content?.includes('🚶 SEEKING RIDE')) return 'Seeking Ride'
|
||||
|
||||
return 'Rideshare'
|
||||
}
|
||||
|
||||
// Get market product data for market events
|
||||
function getMarketProductData(note: any) {
|
||||
if (note.kind === 30018) {
|
||||
|
|
@ -281,6 +319,13 @@ async function onToggleLike(note: any) {
|
|||
>
|
||||
{{ getMarketEventType({ kind: note.kind }) }}
|
||||
</Badge>
|
||||
<Badge
|
||||
v-if="isRidesharePost(note)"
|
||||
variant="secondary"
|
||||
class="text-xs px-1.5 py-0.5 bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200"
|
||||
>
|
||||
🚗 {{ getRideshareType(note) }}
|
||||
</Badge>
|
||||
<span class="text-sm font-medium">{{ getDisplayName(note.pubkey) }}</span>
|
||||
</div>
|
||||
<span class="text-xs text-muted-foreground">
|
||||
|
|
|
|||
447
src/modules/nostr-feed/components/RideshareComposer.vue
Normal file
447
src/modules/nostr-feed/components/RideshareComposer.vue
Normal file
|
|
@ -0,0 +1,447 @@
|
|||
<template>
|
||||
<Card class="w-full">
|
||||
<CardHeader>
|
||||
<div class="flex items-center justify-between">
|
||||
<CardTitle class="flex items-center gap-2">
|
||||
<Car class="h-5 w-5" />
|
||||
Create Rideshare Post
|
||||
</CardTitle>
|
||||
<Button variant="ghost" size="sm" @click="emit('close')" class="h-8 w-8 p-0">
|
||||
<X class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<CardDescription>
|
||||
Share or find rides with the community
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent>
|
||||
<form @submit="onSubmit" class="space-y-4">
|
||||
<!-- Rideshare Type -->
|
||||
<FormField v-slot="{ componentField }" name="type">
|
||||
<FormItem>
|
||||
<FormLabel>Type *</FormLabel>
|
||||
<FormControl>
|
||||
<Select v-bind="componentField">
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select rideshare type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="offering">Offering a ride</SelectItem>
|
||||
<SelectItem value="seeking">Looking for a ride</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Are you offering a ride or looking for one?
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<!-- From Location -->
|
||||
<FormField v-slot="{ componentField }" name="fromLocation">
|
||||
<FormItem>
|
||||
<FormLabel>From *</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Pickup location"
|
||||
v-bind="componentField"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Starting location or pickup point
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<!-- To Location -->
|
||||
<FormField v-slot="{ componentField }" name="toLocation">
|
||||
<FormItem>
|
||||
<FormLabel>To *</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Destination"
|
||||
v-bind="componentField"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Destination or drop-off point
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<!-- Date and Time -->
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<FormField v-slot="{ componentField }" name="date">
|
||||
<FormItem>
|
||||
<FormLabel>Date *</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="date"
|
||||
v-bind="componentField"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<FormField v-slot="{ componentField }" name="time">
|
||||
<FormItem>
|
||||
<FormLabel>Time *</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="time"
|
||||
v-bind="componentField"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
</div>
|
||||
|
||||
<!-- Available Seats (for offering) -->
|
||||
<FormField v-slot="{ componentField }" name="seats">
|
||||
<FormItem v-show="watchedType === 'offering'">
|
||||
<FormLabel>Available Seats</FormLabel>
|
||||
<FormControl>
|
||||
<Select v-bind="componentField">
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Number of seats" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="1">1 seat</SelectItem>
|
||||
<SelectItem value="2">2 seats</SelectItem>
|
||||
<SelectItem value="3">3 seats</SelectItem>
|
||||
<SelectItem value="4">4 seats</SelectItem>
|
||||
<SelectItem value="5">5+ seats</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
How many passengers can you take?
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<!-- Price/Cost -->
|
||||
<FormField v-slot="{ componentField }" name="price">
|
||||
<FormItem>
|
||||
<FormLabel>{{ watchedType === 'offering' ? 'Price per person' : 'Willing to pay' }}</FormLabel>
|
||||
<FormControl>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm text-muted-foreground">$</span>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="0.00"
|
||||
step="0.01"
|
||||
min="0"
|
||||
v-bind="componentField"
|
||||
/>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{{ watchedType === 'offering' ? 'Cost per passenger (optional)' : 'Maximum you\'re willing to pay (optional)' }}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<!-- Additional Details -->
|
||||
<FormField v-slot="{ componentField }" name="details">
|
||||
<FormItem>
|
||||
<FormLabel>Additional Details</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
placeholder="Any additional information, requirements, or preferences..."
|
||||
class="min-h-[80px] resize-y"
|
||||
v-bind="componentField"
|
||||
:maxlength="200"
|
||||
/>
|
||||
</FormControl>
|
||||
<div class="flex items-center justify-between">
|
||||
<FormDescription>
|
||||
Extra details, preferences, or contact info
|
||||
</FormDescription>
|
||||
<span class="text-xs text-muted-foreground">
|
||||
{{ detailsCharacterCount }}/200
|
||||
</span>
|
||||
</div>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<!-- Contact Method -->
|
||||
<FormField v-slot="{ componentField }" name="contactMethod">
|
||||
<FormItem>
|
||||
<FormLabel>Contact Method</FormLabel>
|
||||
<FormControl>
|
||||
<Select v-bind="componentField">
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="How should people contact you?" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="nostr-dm">Nostr DM</SelectItem>
|
||||
<SelectItem value="replies">Replies to this post</SelectItem>
|
||||
<SelectItem value="other">Other (specify in details)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Preferred way for people to reach you
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
</FormField>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex justify-end pt-4 border-t">
|
||||
<div class="flex gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
@click="emit('close')"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
:disabled="isPublishing || !isFormValid"
|
||||
class="gap-2"
|
||||
>
|
||||
<Send class="h-4 w-4" />
|
||||
{{ isPublishing ? 'Publishing...' : 'Publish Rideshare' }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { useForm } from 'vee-validate'
|
||||
import { toTypedSchema } from '@vee-validate/zod'
|
||||
import * as z from 'zod'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import {
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from '@/components/ui/form'
|
||||
import { Car, Send, X } from 'lucide-vue-next'
|
||||
import { injectService, SERVICE_TOKENS } from '@/core/di-container'
|
||||
import { useToast } from '@/core/composables/useToast'
|
||||
import { finalizeEvent, type EventTemplate } from 'nostr-tools'
|
||||
|
||||
// Emits
|
||||
interface Emits {
|
||||
(e: 'close'): void
|
||||
(e: 'rideshare-published', noteId: string): void
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
// Services
|
||||
const authService = injectService(SERVICE_TOKENS.AUTH_SERVICE)
|
||||
const relayHub = injectService(SERVICE_TOKENS.RELAY_HUB)
|
||||
const toast = useToast()
|
||||
|
||||
// Form schema
|
||||
const rideshareSchema = toTypedSchema(z.object({
|
||||
type: z.enum(['offering', 'seeking'], {
|
||||
required_error: "Please select rideshare type"
|
||||
}),
|
||||
fromLocation: z.string().min(1, "From location is required").max(100, "Location too long"),
|
||||
toLocation: z.string().min(1, "To location is required").max(100, "Location too long"),
|
||||
date: z.string().min(1, "Date is required").refine(
|
||||
(date) => new Date(date) >= new Date(new Date().toDateString()),
|
||||
"Date cannot be in the past"
|
||||
),
|
||||
time: z.string().min(1, "Time is required"),
|
||||
seats: z.string().optional(),
|
||||
price: z.string().optional(),
|
||||
details: z.string().max(200, "Details too long").optional(),
|
||||
contactMethod: z.enum(['nostr-dm', 'replies', 'other']).optional(),
|
||||
}))
|
||||
|
||||
// Form setup
|
||||
const form = useForm({
|
||||
validationSchema: rideshareSchema,
|
||||
initialValues: {
|
||||
type: '',
|
||||
fromLocation: '',
|
||||
toLocation: '',
|
||||
date: '',
|
||||
time: '',
|
||||
seats: '',
|
||||
price: '',
|
||||
details: '',
|
||||
contactMethod: 'nostr-dm'
|
||||
}
|
||||
})
|
||||
|
||||
const { values, meta, setFieldValue, resetForm } = form
|
||||
const isFormValid = computed(() => meta.value.valid)
|
||||
|
||||
// State
|
||||
const isPublishing = ref(false)
|
||||
|
||||
// Computed properties
|
||||
const watchedType = computed(() => values.type)
|
||||
const detailsCharacterCount = computed(() => (values.details || '').length)
|
||||
|
||||
// Helper function to convert hex string to Uint8Array
|
||||
const hexToUint8Array = (hex: string): Uint8Array => {
|
||||
const bytes = new Uint8Array(hex.length / 2)
|
||||
for (let i = 0; i < hex.length; i += 2) {
|
||||
bytes[i / 2] = parseInt(hex.substr(i, 2), 16)
|
||||
}
|
||||
return bytes
|
||||
}
|
||||
|
||||
// Generate rideshare post content
|
||||
const generateRideshareContent = (values: any): string => {
|
||||
const type = values.type === 'offering' ? '🚗 OFFERING RIDE' : '🚶 SEEKING RIDE'
|
||||
const dateTime = `${values.date} at ${values.time}`
|
||||
|
||||
let content = `${type}\n\n`
|
||||
content += `📍 From: ${values.fromLocation}\n`
|
||||
content += `🎯 To: ${values.toLocation}\n`
|
||||
content += `📅 When: ${dateTime}\n`
|
||||
|
||||
if (values.type === 'offering' && values.seats) {
|
||||
content += `👥 Seats available: ${values.seats}\n`
|
||||
}
|
||||
|
||||
if (values.price) {
|
||||
const priceLabel = values.type === 'offering' ? 'Price per person' : 'Willing to pay'
|
||||
content += `💰 ${priceLabel}: $${values.price}\n`
|
||||
}
|
||||
|
||||
if (values.contactMethod) {
|
||||
const contactLabels = {
|
||||
'nostr-dm': 'DM me on Nostr',
|
||||
'replies': 'Reply to this post',
|
||||
'other': 'See details below'
|
||||
}
|
||||
content += `📞 Contact: ${contactLabels[values.contactMethod as keyof typeof contactLabels]}\n`
|
||||
}
|
||||
|
||||
if (values.details?.trim()) {
|
||||
content += `\n📝 Details: ${values.details.trim()}\n`
|
||||
}
|
||||
|
||||
content += `\n#rideshare #carpool #transport`
|
||||
|
||||
return content
|
||||
}
|
||||
|
||||
// Generate rideshare tags following Nostr best practices
|
||||
const generateRideshareTags = (values: any): string[][] => {
|
||||
const tags: string[][] = []
|
||||
|
||||
// NIP-12: Generic tags for rideshare content
|
||||
tags.push(['t', 'rideshare'])
|
||||
tags.push(['t', 'carpool'])
|
||||
tags.push(['t', 'transport'])
|
||||
|
||||
// Rideshare-specific tags (custom)
|
||||
tags.push(['rideshare_type', values.type]) // 'offering' or 'seeking'
|
||||
tags.push(['rideshare_from', values.fromLocation])
|
||||
tags.push(['rideshare_to', values.toLocation])
|
||||
tags.push(['rideshare_date', values.date])
|
||||
tags.push(['rideshare_time', values.time])
|
||||
|
||||
if (values.type === 'offering' && values.seats) {
|
||||
tags.push(['rideshare_seats', values.seats])
|
||||
}
|
||||
|
||||
if (values.price) {
|
||||
tags.push(['rideshare_price', values.price])
|
||||
}
|
||||
|
||||
if (values.contactMethod) {
|
||||
tags.push(['rideshare_contact', values.contactMethod])
|
||||
}
|
||||
|
||||
return tags
|
||||
}
|
||||
|
||||
// Submit handler
|
||||
const onSubmit = form.handleSubmit(async (values) => {
|
||||
if (!authService?.isAuthenticated.value || !authService?.user.value) {
|
||||
toast.error("Please sign in to publish rideshare posts")
|
||||
return
|
||||
}
|
||||
|
||||
if (!relayHub?.isConnected.value) {
|
||||
toast.error("Not connected to Nostr relays")
|
||||
return
|
||||
}
|
||||
|
||||
const userPubkey = authService.user.value.pubkey
|
||||
const userPrivkey = authService.user.value.prvkey
|
||||
|
||||
if (!userPubkey || !userPrivkey) {
|
||||
toast.error("User keys not available - please sign in again")
|
||||
return
|
||||
}
|
||||
|
||||
isPublishing.value = true
|
||||
|
||||
try {
|
||||
// Generate content and tags
|
||||
const content = generateRideshareContent(values)
|
||||
const tags = generateRideshareTags(values)
|
||||
|
||||
// Create rideshare event template
|
||||
// Using kind 1 (text note) with rideshare tags for maximum compatibility
|
||||
// Could also use custom kind like 31001 for structured rideshare events
|
||||
const eventTemplate: EventTemplate = {
|
||||
kind: 1, // Standard text note with rideshare tags
|
||||
content,
|
||||
tags,
|
||||
created_at: Math.floor(Date.now() / 1000)
|
||||
}
|
||||
|
||||
console.log('Creating rideshare event:', eventTemplate)
|
||||
|
||||
// Sign the event
|
||||
const privkeyBytes = hexToUint8Array(userPrivkey)
|
||||
const signedEvent = finalizeEvent(eventTemplate, privkeyBytes)
|
||||
|
||||
console.log('Publishing signed rideshare post:', signedEvent)
|
||||
|
||||
// Publish the rideshare post
|
||||
const result = await relayHub.publishEvent(signedEvent)
|
||||
|
||||
toast.success(`Rideshare post published to ${result.success}/${result.total} relays!`)
|
||||
|
||||
// Reset form
|
||||
resetForm()
|
||||
|
||||
// Emit success event
|
||||
emit('rideshare-published', signedEvent.id)
|
||||
|
||||
console.log('Rideshare post published with ID:', signedEvent.id)
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to publish rideshare post:', error)
|
||||
toast.error(error instanceof Error ? error.message : "Failed to publish rideshare post")
|
||||
} finally {
|
||||
isPublishing.value = false
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
|
@ -36,15 +36,24 @@
|
|||
|
||||
<!-- Main Feed Area - Takes remaining height -->
|
||||
<div class="flex-1 overflow-hidden">
|
||||
<!-- Collapsible Note Composer -->
|
||||
<!-- Collapsible Composer -->
|
||||
<div v-if="showComposer || replyTo" class="border-b bg-background">
|
||||
<div class="px-4 py-3 sm:px-6">
|
||||
<!-- Regular Note Composer -->
|
||||
<NoteComposer
|
||||
v-if="composerType === 'note' || replyTo"
|
||||
:reply-to="replyTo"
|
||||
@note-published="onNotePublished"
|
||||
@clear-reply="onClearReply"
|
||||
@close="onCloseComposer"
|
||||
/>
|
||||
|
||||
<!-- Rideshare Composer -->
|
||||
<RideshareComposer
|
||||
v-else-if="composerType === 'rideshare'"
|
||||
@rideshare-published="onRidesharePublished"
|
||||
@close="onCloseComposer"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -61,15 +70,44 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Floating Action Button for Compose -->
|
||||
<!-- Floating Action Buttons 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-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]" />
|
||||
</Button>
|
||||
<!-- Main compose button -->
|
||||
<div class="flex flex-col items-end gap-3">
|
||||
<!-- Secondary buttons (when expanded) -->
|
||||
<div v-if="showComposerOptions" class="flex flex-col gap-2">
|
||||
<Button
|
||||
@click="openComposer('note')"
|
||||
size="lg"
|
||||
variant="secondary"
|
||||
class="h-12 px-4 rounded-full shadow-md hover:shadow-lg transition-all gap-2"
|
||||
>
|
||||
<MessageSquare class="h-4 w-4" />
|
||||
<span class="text-sm">Note</span>
|
||||
</Button>
|
||||
<Button
|
||||
@click="openComposer('rideshare')"
|
||||
size="lg"
|
||||
variant="secondary"
|
||||
class="h-12 px-4 rounded-full shadow-md hover:shadow-lg transition-all gap-2"
|
||||
>
|
||||
<Car class="h-4 w-4" />
|
||||
<span class="text-sm">Rideshare</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- Main FAB -->
|
||||
<Button
|
||||
@click="toggleComposerOptions"
|
||||
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 }"
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quick Filters Bar (Mobile) -->
|
||||
|
|
@ -97,10 +135,11 @@
|
|||
// 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, Plus } from 'lucide-vue-next'
|
||||
import { Filter, Plus, MessageSquare, Car } 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 appConfig from '@/app.config'
|
||||
|
|
@ -113,6 +152,8 @@ 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')
|
||||
|
||||
// Feed configuration
|
||||
const selectedFilters = ref<ContentFilter[]>(FILTER_PRESETS.all)
|
||||
|
|
@ -191,6 +232,27 @@ const onReplyToNote = (note: ReplyToNote) => {
|
|||
|
||||
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
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue