A locally focused bluesky appview

profile page scrolling

Changed files
+46 -18
frontend
+1 -1
frontend/public/index.html
··· 24 24 work correctly both with client-side routing and a non-root public URL. 25 25 Learn how to configure a non-root public URL by running `npm run build`. 26 26 --> 27 - <title>React App</title> 27 + <title>Konbini</title> 28 28 </head> 29 29 <body> 30 30 <noscript>You need to enable JavaScript to run this app.</noscript>
+23
frontend/src/components/ProfilePage.css
··· 221 221 font-size: 16px; 222 222 } 223 223 224 + .load-more-trigger { 225 + min-height: 20px; 226 + padding: 20px 0; 227 + } 228 + 229 + .loading-more { 230 + text-align: center; 231 + padding: 20px; 232 + color: #536471; 233 + font-size: 14px; 234 + } 235 + 236 + .end-of-feed { 237 + text-align: center; 238 + padding: 40px 20px; 239 + color: #657786; 240 + font-size: 14px; 241 + } 242 + 243 + .end-of-feed p { 244 + margin: 0; 245 + } 246 + 224 247 @media (max-width: 600px) { 225 248 .profile-page { 226 249 margin: 0;
+21 -16
frontend/src/components/ProfilePage.tsx
··· 1 - import React, { useState, useEffect, useRef } from 'react'; 1 + import React, { useState, useEffect, useRef, useCallback } from 'react'; 2 2 import { useParams } from 'react-router-dom'; 3 3 import { ActorProfile, PostResponse } from '../types'; 4 4 import { ApiClient } from '../api'; ··· 67 67 fetchProfile(); 68 68 }, [account]); 69 69 70 - const fetchMorePosts = async (cursor: string) => { 70 + const fetchMorePosts = useCallback(async (cursorToUse: string) => { 71 71 if (!account || loadingMore || !hasMore) return; 72 72 73 73 try { 74 74 setLoadingMore(true); 75 - const data = await ApiClient.getProfilePosts(account, cursor); 75 + const data = await ApiClient.getProfilePosts(account, cursorToUse); 76 76 setPosts(prev => [...prev, ...data.posts]); 77 77 setCursor(data.cursor || null); 78 78 setHasMore(!!(data.cursor && data.posts.length > 0)); ··· 81 81 } finally { 82 82 setLoadingMore(false); 83 83 } 84 - }; 84 + }, [account, loadingMore, hasMore]); 85 85 86 86 useEffect(() => { 87 87 const observer = new IntersectionObserver( 88 88 (entries) => { 89 - if (entries[0].isIntersecting && hasMore && !loadingMore && !loading) { 90 - if (cursor) { 91 - fetchMorePosts(cursor); 92 - } 89 + if (entries[0].isIntersecting && hasMore && !loadingMore && !loading && cursor) { 90 + fetchMorePosts(cursor); 93 91 } 94 92 }, 95 93 { threshold: 0.1 } 96 94 ); 97 95 98 - if (observerTarget.current) { 99 - observer.observe(observerTarget.current); 96 + const currentTarget = observerTarget.current; 97 + if (currentTarget) { 98 + observer.observe(currentTarget); 100 99 } 101 100 102 101 return () => { 103 - if (observerTarget.current) { 104 - observer.unobserve(observerTarget.current); 102 + if (currentTarget) { 103 + observer.unobserve(currentTarget); 105 104 } 106 105 }; 107 - }, [hasMore, loadingMore, loading, cursor]); 106 + }, [hasMore, loadingMore, loading, cursor, fetchMorePosts]); 108 107 109 108 if (loading) { 110 109 return ( ··· 216 215 <p>{activeTab === 'posts' ? 'No posts yet' : 'No replies yet'}</p> 217 216 </div> 218 217 )} 219 - {hasMore && <div ref={observerTarget} style={{ height: '20px' }} />} 220 - {loadingMore && ( 221 - <div className="loading-more">Loading more posts...</div> 218 + {hasMore && ( 219 + <div ref={observerTarget} className="load-more-trigger"> 220 + {loadingMore && <div className="loading-more">Loading more posts...</div>} 221 + </div> 222 + )} 223 + {!hasMore && posts.length > 0 && ( 224 + <div className="end-of-feed"> 225 + <p>You've reached the end!</p> 226 + </div> 222 227 )} 223 228 </div> 224 229 </div>
+1 -1
handlers.go
··· 149 149 150 150 // Get cursor from query parameter (timestamp in RFC3339 format) 151 151 cursor := e.QueryParam("cursor") 152 - limit := 20 152 + limit := 50 153 153 154 154 tcursor := time.Now() 155 155 if cursor != "" {