@@ -6,10 +6,9 @@ import { requireSlot } from '@forum/theme-kit'
66import { getContainer } from '@/server/container'
77import { getActor } from '@/server/context'
88import { activeTheme } from '@/server/theme'
9+ import { POSTS_PER_PAGE } from '@/view/paging'
910import { buildThreadView } from '@/view/thread-view'
1011
11- const POSTS_PER_PAGE = 20
12-
1312export const metadata : Metadata = { title : 'Thread' }
1413
1514function threadId ( value : string ) : number | null {
@@ -32,7 +31,7 @@ export default async function ThreadPage({
3231 searchParams,
3332} : {
3433 params : Promise < { slug : string } >
35- searchParams : Promise < { after ?: string ; page ?: string } >
34+ searchParams : Promise < { after ?: string ; page ?: string ; replied ?: string ; posted ?: string } >
3635} ) {
3736 const [ { slug } , query ] = await Promise . all ( [ params , searchParams ] )
3837 const id = threadId ( slug )
@@ -41,7 +40,7 @@ export default async function ThreadPage({
4140 if ( id === null || after === null || ! Number . isSafeInteger ( page ) || page < 1 ) notFound ( )
4241
4342 const actor = await getActor ( )
44- const { forums, posts, threads, authorizer } = getContainer ( )
43+ const { forums, posts, threads, authorizer, threadViews , threadWrites } = getContainer ( )
4544 const thread = await threads . findVisibleById ( id )
4645 if ( ! thread ) notFound ( )
4746
@@ -50,15 +49,37 @@ export default async function ThreadPage({
5049 const matrix = await authorizer . forumMatrix ( actor , forum . id )
5150 if ( ! authorizer . can ( actor , 'thread.view' , { forumId : forum . id , forum : matrix } ) ) notFound ( )
5251
52+ /*
53+ * Count the view only after the permission check, and only on the first page:
54+ * paging through a long thread is one visit, and a viewer who cannot see the
55+ * thread has not viewed it. The write is buffered (F38) rather than applied to
56+ * `threads`, and a failure is swallowed — a view counter is never a reason to
57+ * fail a page that has already been authorised and read.
58+ */
59+ if ( threadViews && after === undefined ) {
60+ await threadViews . record ( thread . id ) . catch ( ( ) => undefined )
61+ }
62+
5363 const postPage = await posts . listThread (
5464 thread . id ,
5565 after === undefined ? { limit : POSTS_PER_PAGE } : { afterId : after , limit : POSTS_PER_PAGE } ,
5666 )
5767 const nextHref = postPage . nextAfterId === null
5868 ? null
5969 : `/thread/${ thread . id } -${ thread . slug } ?after=${ postPage . nextAfterId } &page=${ page + 1 } `
70+ /*
71+ * The reply link is offered only where the actor may actually use it, and a
72+ * locked thread offers it to nobody but a moderator — the same answer the
73+ * action gives, computed twice because a link is not authorisation.
74+ */
75+ const canReply =
76+ threadWrites !== null &&
77+ authorizer . can ( actor , 'reply.post' , { forumId : forum . id , forum : matrix } ) &&
78+ ( ! thread . isLocked || authorizer . can ( actor , 'content.viewUnapproved' , { forumId : forum . id , forum : matrix } ) )
79+
6080 const view = buildThreadView ( {
6181 thread,
82+ replyHref : canReply ? `/thread/${ thread . id } -${ thread . slug } /reply` : null ,
6283 forum,
6384 page : postPage ,
6485 pageNumber : page ,
@@ -71,12 +92,29 @@ export default async function ThreadPage({
7192 } )
7293
7394 const ThreadView = requireSlot ( activeTheme , 'ThreadView' )
95+ const Notice = requireSlot ( activeTheme , 'Notice' )
7496 const PostBit = requireSlot ( activeTheme , 'PostBit' )
7597 const PostActions = requireSlot ( activeTheme , 'PostActions' )
7698 const Pagination = requireSlot ( activeTheme , 'Pagination' )
7799
100+ const notice =
101+ query . replied === 'race'
102+ ? 'Somebody else replied while you were writing. Your reply was posted below theirs.'
103+ : query . posted === 'moderated'
104+ ? 'Your reply was posted and is waiting for a moderator to approve it.'
105+ : null
106+
78107 return (
79108 < main id = "board-content" tabIndex = { - 1 } className = "flex-1" >
109+ { notice !== null && (
110+ < div className = "px-6 pt-6" >
111+ < Notice
112+ kind = "info"
113+ message = { notice }
114+ dismissHref = { `/thread/${ thread . id } -${ thread . slug } ` }
115+ />
116+ </ div >
117+ ) }
80118 < ThreadView
81119 { ...view . view }
82120 regions = { {
0 commit comments