1- import { useEffect , useState } from 'react' ;
1+ import { useEffect , useRef , useState } from 'react' ;
22import { Link , useNavigate , useParams } from 'react-router-dom' ;
33import ForumList from '@components/ForumList' ;
44import { useForumsStore } from '@state/forums' ;
55import { getInputPeerForForumId } from '@lib/telegram/peers' ;
66import { useQuery } from '@tanstack/react-query' ;
7- import { ThreadMeta , searchThreadCards , searchPostCards , composeThreadCard , composePostCard , generateIdHash , searchBoardCards , getLastPostForThread } from '@lib/protocol' ;
7+ import { ThreadMeta , searchThreadCards , composeThreadCard , composePostCard , generateIdHash , searchBoardCards , getLastPostForThread , fetchPostPage } from '@lib/protocol' ;
88import { deleteMessages , sendPlainMessage , getClient , editMessage } from '@lib/telegram/client' ;
99import MessageList from '@components/MessageList' ;
1010import { getAvatarBlob , setAvatarBlob } from '@lib/db' ;
@@ -15,7 +15,7 @@ import SidebarToggle from '@components/SidebarToggle';
1515import { formatTimeSince } from '@lib/time' ;
1616
1717export default function BoardPage ( ) {
18- const { id, boardId, threadId } = useParams ( ) ;
18+ const { id, boardId, threadId, page } = useParams ( ) ;
1919 const forumId = Number ( id ) ;
2020 const navigate = useNavigate ( ) ;
2121 const initForums = useForumsStore ( ( s ) => s . initFromStorage ) ;
@@ -97,12 +97,21 @@ export default function BoardPage() {
9797 const activeThread = ( threads || [ ] ) . find ( ( t ) => t . id === activeThreadId ) || null ;
9898 const [ openMenuForThreadId , setOpenMenuForThreadId ] = useState < string | null > ( null ) ;
9999
100- const { data : posts = [ ] , isLoading : loadingPosts , error : postsError , refetch : refetchPosts } = useQuery ( {
101- queryKey : [ 'posts' , forumId , boardId , activeThreadId ] ,
100+ // Normalize current page, default to 1 and redirect to include page param if missing.
101+ const currentPage = Math . max ( 1 , Number . isFinite ( Number ( page ) ) ? Number ( page ) : 1 ) ;
102+ useEffect ( ( ) => {
103+ // Only redirect when the URL actually contains a threadId but lacks a page.
104+ if ( threadId && ! page ) {
105+ navigate ( `/forum/${ forumId } /board/${ boardId } /thread/${ threadId } /page/1` , { replace : true } ) ;
106+ }
107+ } , [ threadId , page , forumId , boardId , navigate ] ) ;
108+
109+ const { data : pageData , isLoading : loadingPosts , error : postsError , refetch : refetchPosts } = useQuery < { items : any [ ] ; count : number ; pages : number } > ( {
110+ queryKey : [ 'posts' , forumId , boardId , activeThreadId , currentPage ] ,
102111 queryFn : async ( ) => {
103- if ( ! activeThreadId ) return [ ] as any [ ] ;
112+ if ( ! activeThreadId ) return { items : [ ] as any [ ] , count : 0 , pages : 1 } ;
104113 const input = getInputPeerForForumId ( forumId ) ;
105- const items = await searchPostCards ( input , String ( activeThreadId ) ) ;
114+ const { items, count , pages } = await fetchPostPage ( input , String ( activeThreadId ) , currentPage , 10 ) ;
106115 // Build author map and load avatars once per unique user.
107116 const uniqueUserIds = Array . from ( new Set ( items . map ( ( p ) => p . fromUserId ) . filter ( Boolean ) ) ) as number [ ] ;
108117 const client = await getClient ( ) ;
@@ -137,7 +146,6 @@ export default function BoardPage() {
137146 userIdToUrl [ uid ] = undefined ;
138147 }
139148 }
140- // Map to display messages, preserving metadata used by both branches.
141149 const mapped = items . map ( ( p ) => ( {
142150 id : p . messageId ,
143151 from : p . fromUserId ? ( p . user ?. username ? '@' + p . user . username : [ p . user ?. firstName , p . user ?. lastName ] . filter ( Boolean ) . join ( ' ' ) ) : 'unknown' ,
@@ -171,7 +179,7 @@ export default function BoardPage() {
171179 canDelete : false ,
172180 } ) ) ;
173181 mapped . sort ( ( a , b ) => a . date - b . date ) ;
174- return mapped as any [ ] ;
182+ return { items : mapped as any [ ] , count , pages } ;
175183 } ,
176184 enabled : Number . isFinite ( forumId ) && Boolean ( activeThreadId ) ,
177185 staleTime : 5_000 ,
@@ -228,6 +236,8 @@ export default function BoardPage() {
228236 const [ draftAttachments , setDraftAttachments ] = useState < DraftAttachment [ ] > ( [ ] ) ;
229237 const [ isEditing , setIsEditing ] = useState ( false ) ;
230238 const [ editingMessageId , setEditingMessageId ] = useState < number | null > ( null ) ;
239+ const [ showPostSubmitted , setShowPostSubmitted ] = useState ( false ) ;
240+ const hidePostSubmittedTimerRef = useRef < number | undefined > ( undefined ) ;
231241
232242 async function prepareUploadedInputMedia ( uploaded : any , file : File ) : Promise < PreparedInputMedia > {
233243 // Always send uploads as files (documents).
@@ -318,13 +328,15 @@ export default function BoardPage() {
318328 const firstText = composePostCard ( idHash , activeThreadId , { content : composerText } ) ;
319329 await sendMediaMessage ( input , firstText , prepared [ 0 ] ! . inputMedia ) ;
320330 }
331+ try { if ( hidePostSubmittedTimerRef . current ) { clearTimeout ( hidePostSubmittedTimerRef . current ) ; } } catch { }
332+ setShowPostSubmitted ( true ) ;
333+ hidePostSubmittedTimerRef . current = window . setTimeout ( ( ) => { setShowPostSubmitted ( false ) ; } , 10000 ) ;
321334 }
322335
323336 setComposerText ( '' ) ;
324337 setDraftAttachments ( [ ] ) ;
325338 setIsEditing ( false ) ;
326339 setEditingMessageId ( null ) ;
327- setTimeout ( ( ) => { refetchPosts ( ) ; } , 250 ) ;
328340 } catch ( e : any ) {
329341 alert ( e ?. message ?? 'Failed to send post' ) ;
330342 }
@@ -423,6 +435,11 @@ export default function BoardPage() {
423435 </ aside >
424436 < SidebarToggle />
425437 < main className = "main" >
438+ { showPostSubmitted && (
439+ < div onClick = { ( ) => setShowPostSubmitted ( false ) } style = { { position : 'fixed' , top : 56 , left : 0 , right : 0 , zIndex : 20 , display : 'flex' , justifyContent : 'center' } } >
440+ < div className = "card" style = { { padding : 8 , cursor : 'pointer' } } > Post submitted. It should become visible within a few minutes.</ div >
441+ </ div >
442+ ) }
426443 { ! activeThreadId ? (
427444 < div className = "card" style = { { padding : 12 } } >
428445 < div className = "row" style = { { alignItems : 'center' } } >
@@ -444,7 +461,7 @@ export default function BoardPage() {
444461 ) : (
445462 < div className = "gallery boards" style = { { marginTop : 12 } } >
446463 { threads . map ( ( t ) => (
447- < div key = { t . id } className = "chiclet" style = { { position : 'relative' } } onClick = { ( ) => { setSelectedThreadId ( t . id ) ; navigate ( `/forum/${ forumId } /board/${ boardId } /thread/${ t . id } ` ) ; } } >
464+ < div key = { t . id } className = "chiclet" style = { { position : 'relative' } } onClick = { ( ) => { setSelectedThreadId ( t . id ) ; navigate ( `/forum/${ forumId } /board/${ boardId } /thread/${ t . id } /page/1 ` ) ; } } >
448465 < div className = "title" > { t . title } </ div >
449466 { ( ( ) => {
450467 const lp : any = ( lastPostByThreadId as any ) [ t . id ] ;
@@ -478,8 +495,9 @@ export default function BoardPage() {
478495 </ div >
479496 ) : (
480497 < div className = "card" style = { { height : '100%' , display : 'flex' , flexDirection : 'column' } } >
481- < div style = { { padding : 12 , borderBottom : '1px solid var(--border)' } } >
482- < div className = "row" style = { { alignItems : 'center' } } >
498+ < div style = { { flex : 1 , overflow : 'auto' , display : 'flex' , flexDirection : 'column' } } >
499+ < div style = { { padding : 12 , borderBottom : '1px solid var(--border)' } } >
500+ < div className = "row" style = { { alignItems : 'center' } } >
483501 < h3 style = { { margin : 0 } } >
484502 < Link to = { `/forum/${ forumId } ` } > { forumTitle } </ Link >
485503 { ' > ' }
@@ -488,12 +506,31 @@ export default function BoardPage() {
488506 < span > { activeThread ? activeThread . title : 'Thread' } </ span >
489507 </ h3 >
490508 < div className = "spacer" />
509+ { ( ( ) => {
510+ const totalPages = pageData ?. pages ?? 1 ;
511+ const onFirst = ( ) => activeThreadId && navigate ( `/forum/${ forumId } /board/${ boardId } /thread/${ activeThreadId } /page/1` ) ;
512+ const onPrev = ( ) => activeThreadId && navigate ( `/forum/${ forumId } /board/${ boardId } /thread/${ activeThreadId } /page/${ Math . max ( 1 , currentPage - 1 ) } ` ) ;
513+ const onNext = ( ) => activeThreadId && navigate ( `/forum/${ forumId } /board/${ boardId } /thread/${ activeThreadId } /page/${ Math . min ( totalPages , currentPage + 1 ) } ` ) ;
514+ const onLast = ( ) => activeThreadId && navigate ( `/forum/${ forumId } /board/${ boardId } /thread/${ activeThreadId } /page/${ totalPages } ` ) ;
515+ const atFirst = currentPage <= 1 ;
516+ const atLast = currentPage >= totalPages ;
517+ return (
518+ < div className = "row" style = { { alignItems : 'center' , gap : 6 } } >
519+ < button className = "btn" disabled = { atFirst } onClick = { onFirst } title = "First" > ⏮️</ button >
520+ < button className = "btn" disabled = { atFirst } onClick = { onPrev } title = "Previous" > ◀️</ button >
521+ < button className = "btn" disabled = { atLast } onClick = { onNext } title = "Next" > ▶️</ button >
522+ < button className = "btn" disabled = { atLast } onClick = { onLast } title = "Last" > ⏭️</ button >
523+ < div style = { { marginLeft : 8 , color : 'var(--muted)' } } > { currentPage } /{ totalPages } </ div >
524+ </ div >
525+ ) ;
526+ } ) ( ) }
527+ </ div >
491528 </ div >
492- </ div >
493- < div style = { { flex : 1 , overflow : 'auto' , padding : 0 } } >
529+ < div style = { { padding : 0 } } >
494530 { loadingPosts ? < div style = { { padding : 12 } } > Loading...</ div > : postsError ? < div style = { { padding : 12 , color : 'var(--danger)' } } > { ( postsError as any ) ?. message ?? 'Error' } </ div > : (
495- < MessageList messages = { posts as any [ ] } currentUserId = { resolvedUserId } onEditPost = { onEditPost } onDeletePost = { onDeletePost } />
531+ < MessageList messages = { ( pageData ?. items ?? [ ] ) as any [ ] } currentUserId = { resolvedUserId } onEditPost = { onEditPost } onDeletePost = { onDeletePost } />
496532 ) }
533+ </ div >
497534 </ div >
498535 < div className = "composer" >
499536 < div className = "col" style = { { display : 'flex' , flexDirection : 'column' , gap : 8 } } >
0 commit comments