Implement centralized collapse state management in NostrFeed component
- Introduced a centralized mechanism for managing the collapse state of posts, allowing for automatic collapsing of posts with more than 2 direct replies. - Enhanced the ThreadedPost component to utilize the centralized collapse state, improving the visibility and interaction of nested replies. - Added cascading collapse functionality to ensure that all descendant posts are collapsed when a parent post is collapsed. These changes contribute to a more organized and user-friendly experience within the NostrFeed module.
This commit is contained in:
parent
1e1cd69aaf
commit
872954d5ce
2 changed files with 86 additions and 8 deletions
|
|
@ -1,5 +1,5 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, watch } from 'vue'
|
import { computed, watch, ref } from 'vue'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Megaphone, RefreshCw, AlertCircle } from 'lucide-vue-next'
|
import { Megaphone, RefreshCw, AlertCircle } from 'lucide-vue-next'
|
||||||
import { useFeed } from '../composables/useFeed'
|
import { useFeed } from '../composables/useFeed'
|
||||||
|
|
@ -34,6 +34,32 @@ const { posts: notes, threadedPosts, isLoading, error, refreshFeed } = useFeed({
|
||||||
contentFilters: props.contentFilters
|
contentFilters: props.contentFilters
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Centralized collapse state management
|
||||||
|
const collapsedPosts = ref(new Set<string>())
|
||||||
|
|
||||||
|
// Initialize collapsed state for posts with many replies
|
||||||
|
watch(threadedPosts, (newPosts) => {
|
||||||
|
if (newPosts.length > 0) {
|
||||||
|
const newCollapsed = new Set(collapsedPosts.value)
|
||||||
|
|
||||||
|
// Auto-collapse posts with more than 2 direct replies
|
||||||
|
const addCollapsedPosts = (posts: any[]) => {
|
||||||
|
posts.forEach(post => {
|
||||||
|
if ((post.replies?.length || 0) > 2) {
|
||||||
|
newCollapsed.add(post.id)
|
||||||
|
}
|
||||||
|
// Recursively check nested replies
|
||||||
|
if (post.replies?.length > 0) {
|
||||||
|
addCollapsedPosts(post.replies)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
addCollapsedPosts(newPosts)
|
||||||
|
collapsedPosts.value = newCollapsed
|
||||||
|
}
|
||||||
|
}, { immediate: true })
|
||||||
|
|
||||||
// Use profiles service for display names
|
// Use profiles service for display names
|
||||||
const { getDisplayName, fetchProfiles } = useProfiles()
|
const { getDisplayName, fetchProfiles } = useProfiles()
|
||||||
|
|
||||||
|
|
@ -103,6 +129,44 @@ async function onToggleLike(note: FeedPost) {
|
||||||
console.error('Failed to toggle like:', error)
|
console.error('Failed to toggle like:', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle collapse toggle with cascading behavior
|
||||||
|
function onToggleCollapse(postId: string) {
|
||||||
|
const newCollapsed = new Set(collapsedPosts.value)
|
||||||
|
|
||||||
|
if (newCollapsed.has(postId)) {
|
||||||
|
// Expand this post (remove from collapsed set)
|
||||||
|
newCollapsed.delete(postId)
|
||||||
|
} else {
|
||||||
|
// Collapse this post (add to collapsed set)
|
||||||
|
newCollapsed.add(postId)
|
||||||
|
|
||||||
|
// Find all descendant posts and collapse them too (cascading collapse)
|
||||||
|
const collapseDescendants = (posts: any[], targetId: string) => {
|
||||||
|
posts.forEach(post => {
|
||||||
|
if (post.id === targetId && post.replies) {
|
||||||
|
// Found the target post, collapse all its descendants
|
||||||
|
const addAllDescendants = (replies: any[]) => {
|
||||||
|
replies.forEach(reply => {
|
||||||
|
newCollapsed.add(reply.id)
|
||||||
|
if (reply.replies?.length > 0) {
|
||||||
|
addAllDescendants(reply.replies)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
addAllDescendants(post.replies)
|
||||||
|
} else if (post.replies?.length > 0) {
|
||||||
|
// Keep searching in nested replies
|
||||||
|
collapseDescendants(post.replies, targetId)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
collapseDescendants(threadedPosts.value, postId)
|
||||||
|
}
|
||||||
|
|
||||||
|
collapsedPosts.value = newCollapsed
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
@ -181,8 +245,11 @@ async function onToggleLike(note: FeedPost) {
|
||||||
:get-display-name="getDisplayName"
|
:get-display-name="getDisplayName"
|
||||||
:get-event-reactions="getEventReactions"
|
:get-event-reactions="getEventReactions"
|
||||||
:depth="0"
|
:depth="0"
|
||||||
|
:parent-collapsed="false"
|
||||||
|
:collapsed-posts="collapsedPosts"
|
||||||
@reply-to-note="onReplyToNote"
|
@reply-to-note="onReplyToNote"
|
||||||
@toggle-like="onToggleLike"
|
@toggle-like="onToggleLike"
|
||||||
|
@toggle-collapse="onToggleCollapse"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { formatDistanceToNow } from 'date-fns'
|
import { formatDistanceToNow } from 'date-fns'
|
||||||
|
|
@ -12,22 +12,30 @@ interface Props {
|
||||||
getDisplayName: (pubkey: string) => string
|
getDisplayName: (pubkey: string) => string
|
||||||
getEventReactions: (eventId: string) => { likes: number; userHasLiked: boolean }
|
getEventReactions: (eventId: string) => { likes: number; userHasLiked: boolean }
|
||||||
depth?: number
|
depth?: number
|
||||||
|
parentCollapsed?: boolean
|
||||||
|
collapsedPosts?: Set<string>
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Emits {
|
interface Emits {
|
||||||
(e: 'reply-to-note', note: { id: string; content: string; pubkey: string }): void
|
(e: 'reply-to-note', note: { id: string; content: string; pubkey: string }): void
|
||||||
(e: 'toggle-like', note: FeedPost): void
|
(e: 'toggle-like', note: FeedPost): void
|
||||||
|
(e: 'toggle-collapse', postId: string): void
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
adminPubkeys: () => [],
|
adminPubkeys: () => [],
|
||||||
depth: 0
|
depth: 0,
|
||||||
|
parentCollapsed: false,
|
||||||
|
collapsedPosts: () => new Set<string>()
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits<Emits>()
|
const emit = defineEmits<Emits>()
|
||||||
|
|
||||||
// Collapse state for this post's replies - auto-collapse if more than 2 direct replies
|
// Check if this post is collapsed based on centralized state
|
||||||
const isCollapsed = ref((props.post.replies?.length || 0) > 2)
|
const isCollapsed = computed(() => props.collapsedPosts?.has(props.post.id) || false)
|
||||||
|
|
||||||
|
// Check if this post should be visible (not hidden by parent collapse)
|
||||||
|
const isVisible = computed(() => !props.parentCollapsed)
|
||||||
|
|
||||||
// Check if this is an admin post
|
// Check if this is an admin post
|
||||||
const isAdminPost = computed(() => props.adminPubkeys.includes(props.post.pubkey))
|
const isAdminPost = computed(() => props.adminPubkeys.includes(props.post.pubkey))
|
||||||
|
|
@ -47,9 +55,9 @@ const replyCount = computed(() => {
|
||||||
return countReplies(props.post)
|
return countReplies(props.post)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Toggle collapse state
|
// Toggle collapse state - emit to parent for centralized management
|
||||||
function toggleCollapse() {
|
function toggleCollapse() {
|
||||||
isCollapsed.value = !isCollapsed.value
|
emit('toggle-collapse', props.post.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle reply action
|
// Handle reply action
|
||||||
|
|
@ -101,7 +109,7 @@ function getRideshareType(post: FeedPost): string {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="relative">
|
<div v-if="isVisible" class="relative">
|
||||||
<!-- Vertical line connecting to parent (for nested replies) -->
|
<!-- Vertical line connecting to parent (for nested replies) -->
|
||||||
<div
|
<div
|
||||||
v-if="depth > 0"
|
v-if="depth > 0"
|
||||||
|
|
@ -241,8 +249,11 @@ function getRideshareType(post: FeedPost): string {
|
||||||
:get-display-name="getDisplayName"
|
:get-display-name="getDisplayName"
|
||||||
:get-event-reactions="getEventReactions"
|
:get-event-reactions="getEventReactions"
|
||||||
:depth="depth + 1"
|
:depth="depth + 1"
|
||||||
|
:parent-collapsed="isCollapsed"
|
||||||
|
:collapsed-posts="collapsedPosts"
|
||||||
@reply-to-note="$emit('reply-to-note', $event)"
|
@reply-to-note="$emit('reply-to-note', $event)"
|
||||||
@toggle-like="$emit('toggle-like', $event)"
|
@toggle-like="$emit('toggle-like', $event)"
|
||||||
|
@toggle-collapse="$emit('toggle-collapse', $event)"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Show "Load more replies" button when collapsed and there are more than 2 replies -->
|
<!-- Show "Load more replies" button when collapsed and there are more than 2 replies -->
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue