From ebc7885f0481258599b45a00e074dd3119e8f899 Mon Sep 17 00:00:00 2001 From: padreug Date: Sat, 20 Sep 2025 11:44:22 +0200 Subject: [PATCH] Implement threaded post structure in NostrFeed module - Introduced a new ThreadedPost component to display posts and their replies in a nested format, enhancing the readability of discussions. - Updated the useFeed composable to include a computed property for building threaded posts from flat post data. - Modified FeedService to support the extraction of reply information and build a hierarchical structure for posts, allowing for better organization of replies. - Removed deprecated rideshare-related functions from NostrFeed.vue, streamlining the component and focusing on the threaded view. These changes improve the user experience by facilitating clearer interactions within post discussions, promoting engagement and organization in the NostrFeed module. --- .../nostr-feed/components/NostrFeed.vue | 165 ++--------- .../nostr-feed/components/ThreadedPost.vue | 269 ++++++++++++++++++ src/modules/nostr-feed/composables/useFeed.ts | 7 + .../nostr-feed/services/FeedService.ts | 83 +++++- 4 files changed, 374 insertions(+), 150 deletions(-) create mode 100644 src/modules/nostr-feed/components/ThreadedPost.vue diff --git a/src/modules/nostr-feed/components/NostrFeed.vue b/src/modules/nostr-feed/components/NostrFeed.vue index 4cddac0..b4a5fa6 100644 --- a/src/modules/nostr-feed/components/NostrFeed.vue +++ b/src/modules/nostr-feed/components/NostrFeed.vue @@ -1,14 +1,13 @@ + + \ No newline at end of file diff --git a/src/modules/nostr-feed/composables/useFeed.ts b/src/modules/nostr-feed/composables/useFeed.ts index 39f8f31..b70458e 100644 --- a/src/modules/nostr-feed/composables/useFeed.ts +++ b/src/modules/nostr-feed/composables/useFeed.ts @@ -33,6 +33,12 @@ export function useFeed(config: UseFeedConfig) { return feedService.getFilteredPosts(feedConfig) }) + const threadedPosts = computed(() => { + if (!feedService) return [] + const posts = feedService.getFilteredPosts(feedConfig) + return feedService.buildThreadedPosts(posts) + }) + const loadFeed = async () => { console.log('useFeed: loadFeed called, feedService available:', !!feedService) if (!feedService) { @@ -85,6 +91,7 @@ export function useFeed(config: UseFeedConfig) { return { posts: filteredPosts, + threadedPosts, isLoading: feedService?.isLoading ?? ref(false), error: feedService?.error ?? ref(null), refreshFeed, diff --git a/src/modules/nostr-feed/services/FeedService.ts b/src/modules/nostr-feed/services/FeedService.ts index a161dd9..c15bd04 100644 --- a/src/modules/nostr-feed/services/FeedService.ts +++ b/src/modules/nostr-feed/services/FeedService.ts @@ -13,7 +13,10 @@ export interface FeedPost { tags: string[][] mentions: string[] isReply: boolean - replyTo?: string + replyTo?: string // Direct parent ID + rootId?: string // Thread root ID + replies?: FeedPost[] // Child replies + depth?: number // Depth in reply tree (0 for root posts) } export interface ContentFilter { @@ -234,6 +237,36 @@ export class FeedService extends BaseService { return } + // Extract reply information according to NIP-10 + let rootId: string | undefined + let replyTo: string | undefined + let isReply = false + + // Look for marked e-tags first (preferred method) + const markedRootTag = event.tags?.find((tag: string[]) => tag[0] === 'e' && tag[3] === 'root') + const markedReplyTag = event.tags?.find((tag: string[]) => tag[0] === 'e' && tag[3] === 'reply') + + if (markedRootTag || markedReplyTag) { + // Using marked tags (NIP-10 preferred method) + rootId = markedRootTag?.[1] + replyTo = markedReplyTag?.[1] || markedRootTag?.[1] // Direct reply to root if no reply tag + isReply = true + } else { + // Fallback to positional tags (deprecated but still in use) + const eTags = event.tags?.filter((tag: string[]) => tag[0] === 'e') || [] + if (eTags.length === 1) { + // Single e-tag means this is a direct reply + replyTo = eTags[0][1] + rootId = eTags[0][1] + isReply = true + } else if (eTags.length >= 2) { + // Multiple e-tags: first is root, last is direct reply + rootId = eTags[0][1] + replyTo = eTags[eTags.length - 1][1] + isReply = true + } + } + // Transform to FeedPost const post: FeedPost = { id: event.id, @@ -243,8 +276,11 @@ export class FeedService extends BaseService { kind: event.kind, tags: event.tags || [], mentions: event.tags?.filter((tag: string[]) => tag[0] === 'p').map((tag: string[]) => tag[1]) || [], - isReply: event.tags?.some((tag: string[]) => tag[0] === 'e' && tag[3] === 'reply') || false, - replyTo: event.tags?.find((tag: string[]) => tag[0] === 'e' && tag[3] === 'reply')?.[1] + isReply, + replyTo, + rootId, + replies: [], + depth: 0 } // Add to posts (newest first) @@ -378,6 +414,47 @@ export class FeedService extends BaseService { await this.subscribeFeed(this.currentConfig) } + /** + * Build threaded reply structure from flat posts + */ + buildThreadedPosts(posts: FeedPost[]): FeedPost[] { + // Create a map for quick lookup + const postMap = new Map() + posts.forEach(post => { + postMap.set(post.id, { ...post, replies: [], depth: 0 }) + }) + + // Build the tree structure + const rootPosts: FeedPost[] = [] + + posts.forEach(post => { + const currentPost = postMap.get(post.id)! + + if (post.isReply && post.replyTo) { + // This is a reply, attach it to its parent if parent exists + const parentPost = postMap.get(post.replyTo) + if (parentPost) { + currentPost.depth = (parentPost.depth || 0) + 1 + parentPost.replies = parentPost.replies || [] + parentPost.replies.push(currentPost) + // Sort replies by timestamp (oldest first for better reading flow) + parentPost.replies.sort((a, b) => a.created_at - b.created_at) + } else { + // Parent not found, treat as root post + rootPosts.push(currentPost) + } + } else { + // This is a root post + rootPosts.push(currentPost) + } + }) + + // Sort root posts by newest first + rootPosts.sort((a, b) => b.created_at - a.created_at) + + return rootPosts + } + /** * Get filtered posts for specific feed type */