(null)
const [composerContent, setComposerContent] = useState('')
diff --git a/src/components/topic-view.test.tsx b/src/components/topic-view.test.tsx
index b2042ca..0a9acb4 100644
--- a/src/components/topic-view.test.tsx
+++ b/src/components/topic-view.test.tsx
@@ -67,7 +67,7 @@ describe('TopicView', () => {
render()
expect(screen.getByText('Jay')).toBeInTheDocument()
const authorLink = screen.getByRole('link', { name: /Jay/ })
- expect(authorLink).toHaveAttribute('href', `/u/${mockUsers[0]!.handle}`)
+ expect(authorLink).toHaveAttribute('href', `/profile/${mockUsers[0]!.handle}`)
})
it('falls back to DID when author profile is missing', () => {
diff --git a/src/components/topic-view.tsx b/src/components/topic-view.tsx
index a89d8c1..d3a5c78 100644
--- a/src/components/topic-view.tsx
+++ b/src/components/topic-view.tsx
@@ -110,7 +110,7 @@ export function TopicView({
{/* Author + timestamp */}
{topic.author?.avatarUrl ? (
diff --git a/src/components/user-profile-card.test.tsx b/src/components/user-profile-card.test.tsx
index 2cbb36b..89dc863 100644
--- a/src/components/user-profile-card.test.tsx
+++ b/src/components/user-profile-card.test.tsx
@@ -76,7 +76,7 @@ describe('UserProfileCard', () => {
it('renders as link to user profile', () => {
render()
const link = screen.getByRole('link', { name: /jay\.bsky\.team/i })
- expect(link).toHaveAttribute('href', '/u/jay.bsky.team')
+ expect(link).toHaveAttribute('href', '/profile/jay.bsky.team')
})
it('passes axe accessibility check', async () => {
diff --git a/src/components/user-profile-card.tsx b/src/components/user-profile-card.tsx
index 74008b7..b4b1294 100644
--- a/src/components/user-profile-card.tsx
+++ b/src/components/user-profile-card.tsx
@@ -80,7 +80,7 @@ export function UserProfileCard({ user, className }: UserProfileCardProps) {
onBlur={hide}
>
{user.handle}
diff --git a/src/lib/api/client.ts b/src/lib/api/client.ts
index 01d200e..b609dbd 100644
--- a/src/lib/api/client.ts
+++ b/src/lib/api/client.ts
@@ -240,10 +240,22 @@ export function getTopics(
return apiFetch(`/api/topics${query}`, options)
}
+/** @deprecated Use getTopicByAuthorAndRkey for author-scoped lookup */
export function getTopicByRkey(rkey: string, options?: FetchOptions): Promise {
return apiFetch(`/api/topics/by-rkey/${encodeURIComponent(rkey)}`, options)
}
+export function getTopicByAuthorAndRkey(
+ handle: string,
+ rkey: string,
+ options?: FetchOptions
+): Promise {
+ return apiFetch(
+ `/api/topics/by-author-rkey/${encodeURIComponent(handle)}/${encodeURIComponent(rkey)}`,
+ options
+ )
+}
+
export function createTopic(
input: CreateTopicInput,
accessToken: string,
@@ -329,6 +341,17 @@ export function updateReply(
})
}
+export function getReplyByAuthorAndRkey(
+ handle: string,
+ rkey: string,
+ options?: FetchOptions
+): Promise {
+ return apiFetch(
+ `/api/replies/by-author-rkey/${encodeURIComponent(handle)}/${encodeURIComponent(rkey)}`,
+ options
+ )
+}
+
export function deleteReply(
uri: string,
accessToken: string,
diff --git a/src/lib/api/types.ts b/src/lib/api/types.ts
index ec33024..b7f9d48 100644
--- a/src/lib/api/types.ts
+++ b/src/lib/api/types.ts
@@ -158,6 +158,7 @@ export interface CreateTopicResponse {
rkey: string
title: string
category: string
+ authorHandle: string
moderationStatus: ModerationStatus
createdAt: string
}
@@ -237,6 +238,7 @@ export interface SearchResult {
uri: string
rkey: string
authorDid: string
+ authorHandle: string | null
title: string | null
content: string
category: string | null
@@ -339,6 +341,8 @@ export interface Notification {
actorHandle: string
subjectUri: string
subjectTitle: string | null
+ subjectAuthorDid: string
+ subjectAuthorHandle: string | null
message: string
read: boolean
createdAt: string
diff --git a/src/lib/format.test.ts b/src/lib/format.test.ts
index 5a1e977..3e6f748 100644
--- a/src/lib/format.test.ts
+++ b/src/lib/format.test.ts
@@ -3,7 +3,14 @@
*/
import { describe, it, expect } from 'vitest'
-import { formatRelativeTime, formatCompactNumber, slugify, getTopicUrl, isEdited } from './format'
+import {
+ formatRelativeTime,
+ formatCompactNumber,
+ slugify,
+ getTopicUrl,
+ getReplyUrl,
+ isEdited,
+} from './format'
describe('formatRelativeTime', () => {
it('returns "just now" for recent timestamps', () => {
@@ -67,18 +74,31 @@ describe('slugify', () => {
describe('getTopicUrl', () => {
it('generates correct URL from topic', () => {
const topic = {
- title: 'Welcome to Barazo Forums',
+ authorHandle: 'jay.bsky.team',
rkey: '3kf1abc',
}
- expect(getTopicUrl(topic)).toBe('/t/welcome-to-barazo-forums/3kf1abc')
+ expect(getTopicUrl(topic)).toBe('/jay.bsky.team/3kf1abc')
})
- it('handles special characters in title', () => {
+ it('handles different handles', () => {
const topic = {
- title: 'Feature Request: Dark Mode!',
+ authorHandle: 'alex.example.com',
rkey: '3kf3ghi',
}
- expect(getTopicUrl(topic)).toBe('/t/feature-request-dark-mode/3kf3ghi')
+ expect(getTopicUrl(topic)).toBe('/alex.example.com/3kf3ghi')
+ })
+})
+
+describe('getReplyUrl', () => {
+ it('generates correct reply permalink URL', () => {
+ expect(
+ getReplyUrl({
+ topicAuthorHandle: 'jay.bsky.team',
+ topicRkey: '3kf1abc',
+ replyAuthorHandle: 'alex.example.com',
+ replyRkey: '3kf2def',
+ })
+ ).toBe('/jay.bsky.team/3kf1abc/alex.example.com/3kf2def')
})
})
diff --git a/src/lib/format.ts b/src/lib/format.ts
index 4ffb14a..ccbdbfe 100644
--- a/src/lib/format.ts
+++ b/src/lib/format.ts
@@ -95,10 +95,22 @@ export function slugify(text: string): string {
}
/**
- * Generates a topic URL from a topic's title and rkey.
+ * Generates a topic URL from a topic's author handle and rkey.
*/
-export function getTopicUrl(topic: { title: string; rkey: string }): string {
- return `/t/${slugify(topic.title)}/${topic.rkey}`
+export function getTopicUrl(topic: { authorHandle: string; rkey: string }): string {
+ return `/${topic.authorHandle}/${topic.rkey}`
+}
+
+/**
+ * Generates a reply permalink URL.
+ */
+export function getReplyUrl(params: {
+ topicAuthorHandle: string
+ topicRkey: string
+ replyAuthorHandle: string
+ replyRkey: string
+}): string {
+ return `/${params.topicAuthorHandle}/${params.topicRkey}/${params.replyAuthorHandle}/${params.replyRkey}`
}
/**
diff --git a/src/mocks/data.ts b/src/mocks/data.ts
index 05da482..402136a 100644
--- a/src/mocks/data.ts
+++ b/src/mocks/data.ts
@@ -411,6 +411,7 @@ export const mockSearchResults: SearchResult[] = [
uri: mockTopics[0]!.uri,
rkey: mockTopics[0]!.rkey,
authorDid: mockTopics[0]!.authorDid,
+ authorHandle: mockUsers[0]!.handle,
title: 'Welcome to Barazo Forums',
content: 'This is the first topic on our new federated forum platform.',
category: 'general',
@@ -427,6 +428,7 @@ export const mockSearchResults: SearchResult[] = [
uri: mockTopics[1]!.uri,
rkey: mockTopics[1]!.rkey,
authorDid: mockTopics[1]!.authorDid,
+ authorHandle: mockUsers[1]!.handle,
title: 'Building with the AT Protocol',
content: 'A deep dive into building applications on the AT Protocol.',
category: 'development',
@@ -443,6 +445,7 @@ export const mockSearchResults: SearchResult[] = [
uri: `at://${mockUsers[1]!.did}/forum.barazo.reply.post/3kf6aaa`,
rkey: '3kf6aaa',
authorDid: mockUsers[1]!.did,
+ authorHandle: mockUsers[1]!.handle,
title: null,
content: 'Welcome! Excited to see this forum take shape.',
category: null,
@@ -467,6 +470,8 @@ export const mockNotifications: Notification[] = [
actorHandle: mockUsers[1]!.handle,
subjectUri: mockTopics[0]!.uri,
subjectTitle: 'Welcome to Barazo Forums',
+ subjectAuthorDid: mockUsers[0]!.did,
+ subjectAuthorHandle: mockUsers[0]!.handle,
message: 'alex.bsky.team replied to your topic',
read: false,
createdAt: NOW,
@@ -479,6 +484,8 @@ export const mockNotifications: Notification[] = [
actorHandle: mockUsers[2]!.handle,
subjectUri: mockTopics[0]!.uri,
subjectTitle: 'Welcome to Barazo Forums',
+ subjectAuthorDid: mockUsers[0]!.did,
+ subjectAuthorHandle: mockUsers[0]!.handle,
message: 'sam.example.com reacted to your topic',
read: false,
createdAt: YESTERDAY,
@@ -491,6 +498,8 @@ export const mockNotifications: Notification[] = [
actorHandle: mockUsers[3]!.handle,
subjectUri: `at://${mockUsers[3]!.did}/forum.barazo.reply.post/3kf6ddd`,
subjectTitle: null,
+ subjectAuthorDid: mockUsers[3]!.did,
+ subjectAuthorHandle: mockUsers[3]!.handle,
message: 'robin.bsky.team mentioned you in a reply',
read: true,
createdAt: YESTERDAY,
@@ -503,6 +512,8 @@ export const mockNotifications: Notification[] = [
actorHandle: mockUsers[4]!.handle,
subjectUri: mockTopics[0]!.uri,
subjectTitle: 'Welcome to Barazo Forums',
+ subjectAuthorDid: mockUsers[0]!.did,
+ subjectAuthorHandle: mockUsers[0]!.handle,
message: 'Your topic was pinned by a moderator',
read: true,
createdAt: TWO_DAYS_AGO,
diff --git a/src/mocks/handlers.ts b/src/mocks/handlers.ts
index 3a123c6..7a0bd7b 100644
--- a/src/mocks/handlers.ts
+++ b/src/mocks/handlers.ts
@@ -194,6 +194,19 @@ export const handlers = [
})
}),
+ // GET /api/topics/by-author-rkey/:handle/:rkey
+ http.get(`${API_URL}/api/topics/by-author-rkey/:handle/:rkey`, ({ params }) => {
+ const handle = decodeURIComponent(params['handle'] as string)
+ const rkey = params['rkey'] as string
+ const topic = mockTopics.find(
+ (t) => t.rkey === rkey && (t.author?.handle === handle || t.authorDid === handle)
+ )
+ if (!topic) {
+ return HttpResponse.json({ error: 'Topic not found' }, { status: 404 })
+ }
+ return HttpResponse.json(topic)
+ }),
+
// GET /api/topics/by-rkey/:rkey (must be before :uri handler)
http.get(`${API_URL}/api/topics/by-rkey/:rkey`, ({ params }) => {
const rkey = params['rkey'] as string