Barazo default frontend barazo.forum
2
fork

Configure Feed

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

fix(topics): make OP reply icon clickable to open reply composer (#164)

The ChatCircle icon on the original post looked like a reply button but
was a static display. Now it opens the reply composer targeting the OP,
matching the behavior of reply buttons on comment cards.

authored by

Guido X Jansen and committed by
GitHub
44f1123a f3a291fd

+82 -17
+29 -8
src/components/topic-detail-client.test.tsx
··· 141 141 142 142 it('shows reply buttons on reply cards when authenticated', () => { 143 143 render(<TopicDetailClient topic={topic} replies={replies} />) 144 - const replyButtons = screen.getAllByRole('button', { name: /reply to/i }) 145 - expect(replyButtons.length).toBeGreaterThan(0) 144 + // Reply buttons on comment cards (excludes the OP reply button) 145 + const replyCardButtons = screen.getAllByRole('button', { name: /^Reply to (?!this topic)/i }) 146 + expect(replyCardButtons.length).toBeGreaterThan(0) 147 + }) 148 + 149 + it('shows reply button on the original post when authenticated', () => { 150 + render(<TopicDetailClient topic={topic} replies={replies} />) 151 + expect(screen.getByRole('button', { name: /reply to this topic/i })).toBeInTheDocument() 146 152 }) 147 153 }) 148 154 ··· 211 217 const user = userEvent.setup() 212 218 render(<TopicDetailClient topic={topic} replies={replies} />) 213 219 214 - const replyButtons = screen.getAllByRole('button', { name: /reply to/i }) 215 - await user.click(replyButtons[0]!) 220 + // Click a reply card button (not the OP reply button) 221 + const replyCardButtons = screen.getAllByRole('button', { name: /^Reply to (?!this topic)/i }) 222 + await user.click(replyCardButtons[0]!) 216 223 217 224 // The composer should expand and show the reply target banner 218 225 const firstReply = replies[0]! ··· 220 227 expect(screen.getByText(`Replying to @${expectedHandle}`)).toBeInTheDocument() 221 228 }) 222 229 230 + it('sets reply target when reply button is clicked on the original post', async () => { 231 + const user = userEvent.setup() 232 + render(<TopicDetailClient topic={topic} replies={replies} />) 233 + 234 + await user.click(screen.getByRole('button', { name: /reply to this topic/i })) 235 + 236 + // The composer should expand and show the reply target banner with the topic author 237 + expect(screen.getByText(`Replying to @${topic.authorDid}`)).toBeInTheDocument() 238 + }) 239 + 223 240 it('clears reply target when dismiss button is clicked', async () => { 224 241 const user = userEvent.setup() 225 242 render(<TopicDetailClient topic={topic} replies={replies} />) 226 243 227 - // Click reply to set a target 228 - const replyButtons = screen.getAllByRole('button', { name: /reply to/i }) 229 - await user.click(replyButtons[0]!) 244 + // Click reply on a reply card to set a target 245 + const replyCardButtons = screen.getAllByRole('button', { name: /^Reply to (?!this topic)/i }) 246 + await user.click(replyCardButtons[0]!) 230 247 231 248 const firstReply = replies[0]! 232 249 const expectedHandle = firstReply.author?.handle ?? firstReply.authorDid ··· 241 258 describe('locked topic', () => { 242 259 it('hides reply buttons when topic is locked', () => { 243 260 render(<TopicDetailClient topic={topic} replies={replies} isLocked />) 244 - expect(screen.queryByRole('button', { name: /reply to/i })).not.toBeInTheDocument() 261 + // Neither OP reply button nor reply card buttons should be present 262 + expect(screen.queryByRole('button', { name: /reply to this topic/i })).not.toBeInTheDocument() 263 + expect( 264 + screen.queryByRole('button', { name: /^Reply to (?!this topic)/i }) 265 + ).not.toBeInTheDocument() 245 266 }) 246 267 247 268 it('shows locked notice in composer when topic is locked', () => {
+16 -1
src/components/topic-detail-client.tsx
··· 98 98 <> 99 99 {/* Topic with edit button for author */} 100 100 <div className="mt-4"> 101 - <TopicView topic={topic} canEdit={canEdit} onEdit={handleEdit} /> 101 + <TopicView 102 + topic={topic} 103 + canEdit={canEdit} 104 + onEdit={handleEdit} 105 + onReply={ 106 + isAuthenticated && !isLocked 107 + ? () => 108 + handleReply({ 109 + uri: topic.uri, 110 + cid: topic.cid, 111 + authorHandle: topic.authorDid, 112 + snippet: topic.content.slice(0, 100), 113 + }) 114 + : undefined 115 + } 116 + /> 102 117 </div> 103 118 104 119 {/* Reply thread with reply buttons */}
+16 -1
src/components/topic-view.test.tsx
··· 81 81 } 82 82 }) 83 83 84 - it('renders reply count', () => { 84 + it('renders reply count as static text when onReply is not provided', () => { 85 85 render(<TopicView topic={topic} />) 86 86 expect(screen.getByLabelText(`${topic.replyCount} replies`)).toBeInTheDocument() 87 + expect(screen.getByLabelText(`${topic.replyCount} replies`).tagName).toBe('SPAN') 88 + }) 89 + 90 + it('renders reply count as clickable button when onReply is provided', () => { 91 + render(<TopicView topic={topic} onReply={vi.fn()} />) 92 + const replyButton = screen.getByRole('button', { name: /reply to this topic/i }) 93 + expect(replyButton).toBeInTheDocument() 94 + }) 95 + 96 + it('calls onReply when reply button is clicked', async () => { 97 + const user = userEvent.setup() 98 + const onReply = vi.fn() 99 + render(<TopicView topic={topic} onReply={onReply} />) 100 + await user.click(screen.getByRole('button', { name: /reply to this topic/i })) 101 + expect(onReply).toHaveBeenCalledTimes(1) 87 102 }) 88 103 89 104 it('renders reaction count', () => {
+21 -7
src/components/topic-view.tsx
··· 33 33 onModerationAction?: (action: ModerationAction) => void 34 34 canEdit?: boolean 35 35 onEdit?: () => void 36 + onReply?: () => void 36 37 canReport?: boolean 37 38 onReport?: (report: ReportSubmission) => void 38 39 selfLabels?: string[] ··· 49 50 onModerationAction, 50 51 canEdit, 51 52 onEdit, 53 + onReply, 52 54 canReport, 53 55 onReport, 54 56 selfLabels, ··· 161 163 {reactions && onReactionToggle && ( 162 164 <ReactionBar reactions={reactions} onToggle={onReactionToggle} /> 163 165 )} 164 - <span 165 - className="flex items-center gap-1.5" 166 - aria-label={`${formatCompactNumber(topic.replyCount)} replies`} 167 - > 168 - <ChatCircle className="h-4 w-4" weight="regular" aria-hidden="true" /> 169 - {formatCompactNumber(topic.replyCount)} 170 - </span> 166 + {onReply ? ( 167 + <button 168 + type="button" 169 + className="flex items-center gap-1.5 text-muted-foreground transition-colors hover:text-foreground" 170 + aria-label={`Reply to this topic (${formatCompactNumber(topic.replyCount)} replies)`} 171 + onClick={onReply} 172 + > 173 + <ChatCircle className="h-4 w-4" weight="regular" aria-hidden="true" /> 174 + {formatCompactNumber(topic.replyCount)} 175 + </button> 176 + ) : ( 177 + <span 178 + className="flex items-center gap-1.5" 179 + aria-label={`${formatCompactNumber(topic.replyCount)} replies`} 180 + > 181 + <ChatCircle className="h-4 w-4" weight="regular" aria-hidden="true" /> 182 + {formatCompactNumber(topic.replyCount)} 183 + </span> 184 + )} 171 185 <LikeButton 172 186 subjectUri={topic.uri} 173 187 subjectCid={topic.cid}