Barazo default frontend barazo.forum
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix(replies): count all descendants in collapsed thread message (#176)

* test(mocks): add deep reply thread and subcategories to mock data

Add mockDeepReplies (15-level chain) for testing deeply nested comment
rendering on mobile. Add subcategories under Feedback and AT Protocol
in mockCategories to match barazo-web#175. Wire deep replies into MSW
handler so they appear when browsing with mock data.

* fix(replies): count all descendants in collapsed thread message

The "N replies hidden" message on collapsed threads only counted direct
children, not the full subtree. A thread with 12 nested replies would
show "1 reply hidden" when collapsed. Add countDescendants() to
recursively count all descendants and use it in the collapse indicator.

* style(replies): fix prettier formatting in reply-branch

authored by

Guido X Jansen and committed by
GitHub
0c5a1c7f d0ef5357

+195 -11
+2 -1
src/components/reply-branch.test.tsx
··· 164 164 currentVisualDepth={1} 165 165 /> 166 166 ) 167 - expect(screen.getByText(/1 reply hidden/)).toBeInTheDocument() 167 + // depth3 node has 2 descendants: depth4 and depth5 168 + expect(screen.getByText(/2 replies hidden/)).toBeInTheDocument() 168 169 }) 169 170 170 171 it('toggles collapse when ThreadLine is clicked', async () => {
+11 -6
src/components/reply-branch.tsx
··· 12 12 13 13 import { useState, useCallback } from 'react' 14 14 import type { Reply } from '@/lib/api/types' 15 - import type { ReplyTreeNode } from '@/lib/build-reply-tree' 15 + import { type ReplyTreeNode, countDescendants } from '@/lib/build-reply-tree' 16 16 import { 17 17 DEFAULT_EXPANDED_LEVELS, 18 18 AUTO_COLLAPSE_SIBLING_THRESHOLD, ··· 160 160 /> 161 161 </div> 162 162 ))} 163 - {hasChildren && isCollapsed && ( 164 - <p className="ml-12 mt-1 text-xs text-muted-foreground" aria-live="polite"> 165 - {node.children.length} {node.children.length === 1 ? 'reply' : 'replies'} hidden 166 - </p> 167 - )} 163 + {hasChildren && 164 + isCollapsed && 165 + (() => { 166 + const totalHidden = countDescendants(node) 167 + return ( 168 + <p className="ml-12 mt-1 text-xs text-muted-foreground" aria-live="polite"> 169 + {totalHidden} {totalHidden === 1 ? 'reply' : 'replies'} hidden 170 + </p> 171 + ) 172 + })()} 168 173 </li> 169 174 ) 170 175 })}
+36 -1
src/lib/build-reply-tree.test.ts
··· 4 4 5 5 import { describe, it, expect } from 'vitest' 6 6 import type { Reply } from '@/lib/api/types' 7 - import { buildReplyTree, flattenReplyTree } from './build-reply-tree' 7 + import { buildReplyTree, flattenReplyTree, countDescendants } from './build-reply-tree' 8 8 9 9 const TOPIC_URI = 'at://did:plc:user-001/forum.barazo.topic.post/abc123' 10 10 const TOPIC_CID = 'bafyreib1' ··· 149 149 expect(result[0]!.reply.uri).toBe(parent.uri) 150 150 expect(result[0]!.children).toHaveLength(1) 151 151 expect(result[0]!.children[0]!.reply.uri).toBe(child.uri) 152 + }) 153 + }) 154 + 155 + describe('countDescendants', () => { 156 + it('returns 0 for a leaf node', () => { 157 + const leaf = makeReply({ uri: 'at://user/reply/aaa', parentUri: TOPIC_URI, depth: 1 }) 158 + expect(countDescendants({ reply: leaf, children: [] })).toBe(0) 159 + }) 160 + 161 + it('counts all descendants recursively', () => { 162 + const replies = [ 163 + makeReply({ uri: 'at://user/reply/a', parentUri: TOPIC_URI, depth: 1 }), 164 + makeReply({ uri: 'at://user/reply/b', parentUri: 'at://user/reply/a', depth: 2 }), 165 + makeReply({ uri: 'at://user/reply/c', parentUri: 'at://user/reply/b', depth: 3 }), 166 + makeReply({ uri: 'at://user/reply/d', parentUri: 'at://user/reply/c', depth: 4 }), 167 + ] 168 + const tree = buildReplyTree(replies, TOPIC_URI) 169 + // Root node has 3 descendants: b, c, d 170 + expect(countDescendants(tree[0]!)).toBe(3) 171 + // Node b has 2 descendants: c, d 172 + expect(countDescendants(tree[0]!.children[0]!)).toBe(2) 173 + // Node c has 1 descendant: d 174 + expect(countDescendants(tree[0]!.children[0]!.children[0]!)).toBe(1) 175 + }) 176 + 177 + it('counts branching descendants', () => { 178 + const replies = [ 179 + makeReply({ uri: 'at://user/reply/a', parentUri: TOPIC_URI, depth: 1 }), 180 + makeReply({ uri: 'at://user/reply/b', parentUri: 'at://user/reply/a', depth: 2 }), 181 + makeReply({ uri: 'at://user/reply/c', parentUri: 'at://user/reply/a', depth: 2 }), 182 + makeReply({ uri: 'at://user/reply/d', parentUri: 'at://user/reply/b', depth: 3 }), 183 + ] 184 + const tree = buildReplyTree(replies, TOPIC_URI) 185 + // Root has 3 descendants: b, c, d 186 + expect(countDescendants(tree[0]!)).toBe(3) 152 187 }) 153 188 }) 154 189
+11
src/lib/build-reply-tree.ts
··· 48 48 } 49 49 50 50 /** 51 + * Count all descendants (children, grandchildren, etc.) of a node. 52 + */ 53 + export function countDescendants(node: ReplyTreeNode): number { 54 + let count = node.children.length 55 + for (const child of node.children) { 56 + count += countDescendants(child) 57 + } 58 + return count 59 + } 60 + 61 + /** 51 62 * Flatten a reply tree into depth-first order. 52 63 * Useful for rendering a flat list with indentation or for counting. 53 64 */
+133 -2
src/mocks/data.ts
··· 155 155 maturityRating: 'safe', 156 156 createdAt: TWO_DAYS_AGO, 157 157 updatedAt: TWO_DAYS_AGO, 158 - children: [], 158 + children: [ 159 + { 160 + id: 'cat-fb-features', 161 + slug: 'feature-requests', 162 + name: 'Feature Requests', 163 + description: 'Suggest new features and improvements', 164 + parentId: 'cat-feedback', 165 + sortOrder: 0, 166 + communityDid: COMMUNITY_DID, 167 + maturityRating: 'safe', 168 + createdAt: TWO_DAYS_AGO, 169 + updatedAt: TWO_DAYS_AGO, 170 + children: [], 171 + }, 172 + { 173 + id: 'cat-fb-bugs', 174 + slug: 'bug-reports', 175 + name: 'Bug Reports', 176 + description: 'Report bugs and unexpected behavior', 177 + parentId: 'cat-feedback', 178 + sortOrder: 1, 179 + communityDid: COMMUNITY_DID, 180 + maturityRating: 'safe', 181 + createdAt: TWO_DAYS_AGO, 182 + updatedAt: TWO_DAYS_AGO, 183 + children: [], 184 + }, 185 + ], 186 + }, 187 + { 188 + id: 'cat-atproto', 189 + slug: 'atproto', 190 + name: 'AT Protocol', 191 + description: 'AT Protocol ecosystem, standards, and tooling', 192 + parentId: null, 193 + sortOrder: 3, 194 + communityDid: COMMUNITY_DID, 195 + maturityRating: 'safe', 196 + createdAt: TWO_DAYS_AGO, 197 + updatedAt: TWO_DAYS_AGO, 198 + children: [ 199 + { 200 + id: 'cat-atp-lexicons', 201 + slug: 'lexicons', 202 + name: 'Lexicons', 203 + description: 'Schema definitions and data model discussions', 204 + parentId: 'cat-atproto', 205 + sortOrder: 0, 206 + communityDid: COMMUNITY_DID, 207 + maturityRating: 'safe', 208 + createdAt: TWO_DAYS_AGO, 209 + updatedAt: TWO_DAYS_AGO, 210 + children: [], 211 + }, 212 + { 213 + id: 'cat-atp-identity', 214 + slug: 'identity', 215 + name: 'Identity', 216 + description: 'DIDs, handles, and portable identity', 217 + parentId: 'cat-atproto', 218 + sortOrder: 1, 219 + communityDid: COMMUNITY_DID, 220 + maturityRating: 'safe', 221 + createdAt: TWO_DAYS_AGO, 222 + updatedAt: TWO_DAYS_AGO, 223 + children: [], 224 + }, 225 + ], 159 226 }, 160 227 { 161 228 id: 'cat-meta', ··· 163 230 name: 'Meta', 164 231 description: 'About this community', 165 232 parentId: null, 166 - sortOrder: 3, 233 + sortOrder: 4, 167 234 communityDid: COMMUNITY_DID, 168 235 maturityRating: 'safe', 169 236 createdAt: TWO_DAYS_AGO, ··· 590 657 createdAt: NOW, 591 658 indexedAt: NOW, 592 659 } 660 + 661 + // --- Deep Thread Replies (15 levels deep, for testing nested threading on mobile) --- 662 + 663 + const DEEP_TOPIC_URI = `at://${mockUsers[0]!.did}/forum.barazo.topic.post/3kf1abc` 664 + const DEEP_TOPIC_CID = 'bafyreit1' 665 + 666 + const deepThreadContent: string[] = [ 667 + 'Has anyone tried running Barazo on a Raspberry Pi?', 668 + 'I actually have it running on a Pi 4 with 8GB RAM. Works surprisingly well.', 669 + 'What about the database? PostgreSQL on a Pi seems heavy.', 670 + 'SQLite would be lighter but you lose concurrent writes. PostgreSQL is fine with proper tuning.', 671 + 'What pg settings did you change? I keep running out of shared memory.', 672 + 'Set shared_buffers to 256MB and work_mem to 16MB. Also reduce max_connections to 20.', 673 + 'That helped a lot, thanks! But now the firehose consumer is lagging behind.', 674 + 'The firehose needs dedicated resources. Consider running it as a separate service.', 675 + 'Separate service means another container though. The Pi is already running 4.', 676 + 'You could use a lightweight process manager instead of Docker for some services.', 677 + 'PM2 or systemd? I tried PM2 but it added 100MB of memory overhead.', 678 + 'systemd is the way to go. Zero overhead and it handles restarts natively.', 679 + 'Good call. One more question -- how do you handle SSL termination?', 680 + 'Caddy is perfect for this. Auto-HTTPS with minimal config and low resource usage.', 681 + 'This whole thread is gold. Someone should turn this into a self-hosting guide.', 682 + ] 683 + 684 + /** 685 + * A deeply nested reply thread (15 levels) for testing deep threading behavior, 686 + * especially on mobile viewports where indentation becomes a concern. 687 + * Each reply is a child of the previous one, forming a single chain. 688 + * Uses the same topic as mockReplies (mockTopics[0]). 689 + */ 690 + export const mockDeepReplies: Reply[] = deepThreadContent.map((content, i) => { 691 + const depth = i + 1 692 + const userIndex = i % mockUsers.length 693 + const rkey = `3kf7${String(i + 1).padStart(3, '0')}` 694 + const prevRkey = i > 0 ? `3kf7${String(i).padStart(3, '0')}` : null 695 + const prevUserIndex = i > 0 ? (i - 1) % mockUsers.length : null 696 + 697 + const parentUri = 698 + i === 0 699 + ? DEEP_TOPIC_URI 700 + : `at://${mockUsers[prevUserIndex!]!.did}/forum.barazo.reply.post/${prevRkey}` 701 + const parentCid = i === 0 ? DEEP_TOPIC_CID : `bafydeep${i}` 702 + 703 + return { 704 + uri: `at://${mockUsers[userIndex]!.did}/forum.barazo.reply.post/${rkey}`, 705 + rkey, 706 + authorDid: mockUsers[userIndex]!.did, 707 + author: mockAuthorProfiles[userIndex]!, 708 + content, 709 + contentFormat: null, 710 + rootUri: DEEP_TOPIC_URI, 711 + rootCid: DEEP_TOPIC_CID, 712 + parentUri, 713 + parentCid, 714 + communityDid: COMMUNITY_DID, 715 + cid: `bafydeep${depth}`, 716 + depth, 717 + reactionCount: Math.max(0, 15 - depth), 718 + isAuthorDeleted: false, 719 + isModDeleted: false, 720 + createdAt: new Date(Date.parse(TWO_DAYS_AGO) + i * 3600000).toISOString(), 721 + indexedAt: new Date(Date.parse(TWO_DAYS_AGO) + i * 3600000).toISOString(), 722 + } 723 + }) 593 724 594 725 // --- Community Settings --- 595 726
+2 -1
src/mocks/handlers.ts
··· 12 12 mockCategoryWithTopicCount, 13 13 mockTopics, 14 14 mockReplies, 15 + mockDeepReplies, 15 16 mockSearchResults, 16 17 mockNotifications, 17 18 mockCommunitySettings, ··· 210 211 const limitParam = url.searchParams.get('limit') 211 212 const limit = limitParam ? parseInt(limitParam, 10) : 20 212 213 213 - const replies = mockReplies.filter((r) => r.rootUri === topicUri) 214 + const replies = [...mockReplies, ...mockDeepReplies].filter((r) => r.rootUri === topicUri) 214 215 const limited = replies.slice(0, limit) 215 216 const hasMore = replies.length > limit 216 217