Squash merge nostrfeed-ui into main
This commit is contained in:
parent
5063a3e121
commit
cc5e0dbef6
10 changed files with 379 additions and 258 deletions
|
|
@ -3,12 +3,13 @@ import { computed } from 'vue'
|
|||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { formatDistanceToNow } from 'date-fns'
|
||||
import { Reply, Heart, Share, ChevronUp, ChevronDown } from 'lucide-vue-next'
|
||||
import { Reply, Heart, Share, ChevronUp, ChevronDown, Trash2 } from 'lucide-vue-next'
|
||||
import type { FeedPost } from '../services/FeedService'
|
||||
|
||||
interface Props {
|
||||
post: FeedPost
|
||||
adminPubkeys?: string[]
|
||||
currentUserPubkey?: string | null
|
||||
getDisplayName: (pubkey: string) => string
|
||||
getEventReactions: (eventId: string) => { likes: number; userHasLiked: boolean }
|
||||
depth?: number
|
||||
|
|
@ -22,6 +23,7 @@ interface Emits {
|
|||
(e: 'toggle-like', note: FeedPost): void
|
||||
(e: 'toggle-collapse', postId: string): void
|
||||
(e: 'toggle-limited', postId: string): void
|
||||
(e: 'delete-post', note: FeedPost): void
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
|
|
@ -47,6 +49,9 @@ const isVisible = computed(() => !props.parentCollapsed)
|
|||
// Check if this is an admin post
|
||||
const isAdminPost = computed(() => props.adminPubkeys.includes(props.post.pubkey))
|
||||
|
||||
// Check if this post belongs to the current user
|
||||
const isUserPost = computed(() => props.currentUserPubkey && props.post.pubkey === props.currentUserPubkey)
|
||||
|
||||
// Check if post has replies
|
||||
const hasReplies = computed(() => props.post.replies && props.post.replies.length > 0)
|
||||
|
||||
|
|
@ -117,25 +122,22 @@ function getRideshareType(post: FeedPost): string {
|
|||
|
||||
<template>
|
||||
<div v-if="isVisible" class="relative">
|
||||
<!-- Vertical line connecting to parent (for nested replies) -->
|
||||
<div
|
||||
v-if="depth > 0"
|
||||
class="absolute left-0 top-0 bottom-0 w-px bg-muted-foreground/60"
|
||||
:style="{ marginLeft: `${depth * 6 + 3}px` }"
|
||||
/>
|
||||
|
||||
<!-- Post container with indentation -->
|
||||
<!-- Post container with Lemmy-style border-left threading -->
|
||||
<div
|
||||
:class="{
|
||||
'pl-2': depth > 0,
|
||||
'hover:bg-accent/50': true,
|
||||
'transition-colors': true,
|
||||
'border-b': depth === 0,
|
||||
'border-border/40': depth === 0
|
||||
'border-l-2 border-muted-foreground/40': depth > 0,
|
||||
'ml-0.5': depth > 0,
|
||||
'pl-1.5': depth > 0,
|
||||
'hover:bg-accent/30': true,
|
||||
'transition-all duration-200': true,
|
||||
'border-b border-border/40': depth === 0,
|
||||
'md:border md:border-border/60 md:rounded-lg': depth === 0,
|
||||
'md:shadow-sm md:hover:shadow-md': depth === 0,
|
||||
'md:bg-card': depth === 0,
|
||||
'md:my-1': depth === 0
|
||||
}"
|
||||
:style="{ marginLeft: `${depth * 6}px` }"
|
||||
>
|
||||
<div class="p-3 relative">
|
||||
<div class="p-3 md:p-5 relative">
|
||||
|
||||
<!-- Post Header -->
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
|
|
@ -145,87 +147,87 @@ function getRideshareType(post: FeedPost): string {
|
|||
v-if="hasReplies"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="h-6 w-6 p-0"
|
||||
class="h-7 w-7 md:h-8 md:w-8 p-0 hover:bg-accent transition-colors"
|
||||
@click="toggleCollapse"
|
||||
>
|
||||
<ChevronDown v-if="!isCollapsed" class="h-4 w-4" />
|
||||
<ChevronUp v-else class="h-4 w-4" />
|
||||
<ChevronDown v-if="!isCollapsed" class="h-4 w-4 md:h-5 md:w-5" />
|
||||
<ChevronUp v-else class="h-4 w-4 md:h-5 md:w-5" />
|
||||
</Button>
|
||||
<div v-else class="w-6" />
|
||||
<div v-else class="w-7 md:w-8" />
|
||||
|
||||
<!-- Badges -->
|
||||
<Badge
|
||||
v-if="isAdminPost"
|
||||
variant="default"
|
||||
class="text-xs px-1.5 py-0.5"
|
||||
class="text-xs md:text-sm px-2 py-0.5"
|
||||
>
|
||||
Admin
|
||||
</Badge>
|
||||
<Badge
|
||||
v-if="post.isReply && depth === 0"
|
||||
variant="secondary"
|
||||
class="text-xs px-1.5 py-0.5"
|
||||
class="text-xs md:text-sm px-2 py-0.5"
|
||||
>
|
||||
Reply
|
||||
</Badge>
|
||||
<Badge
|
||||
v-if="isRidesharePost(post)"
|
||||
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"
|
||||
class="text-xs md:text-sm px-2 py-0.5 bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200"
|
||||
>
|
||||
🚗 {{ getRideshareType(post) }}
|
||||
</Badge>
|
||||
|
||||
<!-- Author name -->
|
||||
<span class="text-sm font-medium">{{ getDisplayName(post.pubkey) }}</span>
|
||||
<span class="text-sm md:text-base font-semibold">{{ getDisplayName(post.pubkey) }}</span>
|
||||
|
||||
<!-- Reply count badge if collapsed -->
|
||||
<Badge
|
||||
v-if="isCollapsed && hasReplies"
|
||||
variant="outline"
|
||||
class="text-xs px-1.5 py-0.5"
|
||||
class="text-xs md:text-sm px-2 py-0.5"
|
||||
>
|
||||
{{ replyCount }} {{ replyCount === 1 ? 'reply' : 'replies' }}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<!-- Timestamp -->
|
||||
<span class="text-xs text-muted-foreground">
|
||||
<span class="text-xs md:text-sm text-muted-foreground font-medium">
|
||||
{{ formatDistanceToNow(post.created_at * 1000, { addSuffix: true }) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Post Content (always visible for non-collapsed posts) -->
|
||||
<div class="text-sm leading-relaxed whitespace-pre-wrap">
|
||||
<div class="text-sm md:text-base leading-relaxed whitespace-pre-wrap mb-3">
|
||||
{{ post.content }}
|
||||
</div>
|
||||
|
||||
<!-- Post Actions (always visible) -->
|
||||
<div class="mt-2">
|
||||
<div class="flex items-center gap-1">
|
||||
<div class="mt-3">
|
||||
<div class="flex items-center gap-1 md:gap-2">
|
||||
<!-- Reply Button -->
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="h-6 w-6 p-0 text-muted-foreground hover:text-foreground hover:bg-accent/50"
|
||||
class="h-7 w-7 md:h-8 md:w-8 p-0 text-muted-foreground hover:text-primary hover:bg-accent transition-colors"
|
||||
@click="onReplyToNote"
|
||||
>
|
||||
<Reply class="h-3 w-3" />
|
||||
<Reply class="h-3.5 w-3.5 md:h-4 md:w-4" />
|
||||
</Button>
|
||||
|
||||
<!-- Like Button -->
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="h-6 px-1.5 text-xs text-muted-foreground hover:text-foreground hover:bg-accent/50"
|
||||
class="h-7 md:h-8 px-2 md:px-2.5 text-xs md:text-sm text-muted-foreground hover:text-red-500 hover:bg-accent transition-colors"
|
||||
:class="{ 'text-red-500 hover:text-red-600': getEventReactions(post.id).userHasLiked }"
|
||||
@click="onToggleLike"
|
||||
>
|
||||
<Heart
|
||||
class="h-3 w-3"
|
||||
class="h-3.5 w-3.5 md:h-4 md:w-4"
|
||||
:class="{ 'fill-current': getEventReactions(post.id).userHasLiked }"
|
||||
/>
|
||||
<span v-if="getEventReactions(post.id).likes > 0" class="ml-1">
|
||||
<span v-if="getEventReactions(post.id).likes > 0" class="ml-1.5">
|
||||
{{ getEventReactions(post.id).likes }}
|
||||
</span>
|
||||
</Button>
|
||||
|
|
@ -234,9 +236,20 @@ function getRideshareType(post: FeedPost): string {
|
|||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="h-6 w-6 p-0 text-muted-foreground hover:text-foreground hover:bg-accent/50"
|
||||
class="h-7 w-7 md:h-8 md:w-8 p-0 text-muted-foreground hover:text-primary hover:bg-accent transition-colors"
|
||||
>
|
||||
<Share class="h-3 w-3" />
|
||||
<Share class="h-3.5 w-3.5 md:h-4 md:w-4" />
|
||||
</Button>
|
||||
|
||||
<!-- Delete Button (only for user's own posts) -->
|
||||
<Button
|
||||
v-if="isUserPost"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="h-7 w-7 md:h-8 md:w-8 p-0 text-muted-foreground hover:text-destructive hover:bg-accent transition-colors"
|
||||
@click="emit('delete-post', post)"
|
||||
>
|
||||
<Trash2 class="h-3.5 w-3.5 md:h-4 md:w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -250,6 +263,7 @@ function getRideshareType(post: FeedPost): string {
|
|||
:key="reply.id"
|
||||
:post="reply"
|
||||
:admin-pubkeys="adminPubkeys"
|
||||
:current-user-pubkey="currentUserPubkey"
|
||||
:get-display-name="getDisplayName"
|
||||
:get-event-reactions="getEventReactions"
|
||||
:depth="depth + 1"
|
||||
|
|
@ -260,18 +274,18 @@ function getRideshareType(post: FeedPost): string {
|
|||
@toggle-like="$emit('toggle-like', $event)"
|
||||
@toggle-collapse="$emit('toggle-collapse', $event)"
|
||||
@toggle-limited="$emit('toggle-limited', $event)"
|
||||
@delete-post="$emit('delete-post', $event)"
|
||||
/>
|
||||
|
||||
<!-- Show "Load more replies" button when limited and there are more than 2 replies -->
|
||||
<div
|
||||
v-if="hasLimitedReplies && (post.replies?.length || 0) > 2"
|
||||
class="mt-2"
|
||||
:style="{ marginLeft: `${(depth + 1) * 6}px` }"
|
||||
class="mt-2 mb-1 ml-0.5"
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="h-6 px-2 text-xs text-muted-foreground hover:text-foreground hover:bg-accent/50"
|
||||
class="h-7 md:h-8 px-3 md:px-4 text-xs md:text-sm text-primary hover:text-primary hover:bg-accent transition-colors font-medium"
|
||||
@click="() => emit('toggle-limited', post.id)"
|
||||
>
|
||||
Show {{ (post.replies?.length || 0) - 2 }} more {{ (post.replies?.length || 0) - 2 === 1 ? 'reply' : 'replies' }}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue