feat: add 'load more' on remaining pages#47
Conversation
📝 WalkthroughWalkthroughThis PR introduces a new FeedItems component to unify feed rendering with pagination across multiple pages, updates the feed API response shape to remove cursor and enforce pagination fields, adds unread notification tracking to mobile navigation, and updates the build setup to use bun. A DisableExternalLinkWarning field is added to the backend preferences. Changes
Sequence Diagram(s)sequenceDiagram
participant App as Feed Component
participant FeedItems
participant API as API Client
participant Server
App->>FeedItems: Mount with filter params (creator, tag, motivation)
activate FeedItems
FeedItems->>FeedItems: Initialize state (items=[], offset=0, loading=true)
FeedItems->>API: getFeed({...filters, limit: 50, offset: 0})
activate API
API->>Server: GET /feed?creator=...&tag=...&limit=50&offset=0
Server-->>API: {items: [...], hasMore: bool, fetchCount: number}
API-->>FeedItems: items[], hasMore, fetchCount
deactivate API
FeedItems->>FeedItems: setState({items, hasMore, offset: 0, loading: false})
FeedItems->>App: Render items in list or mosaic layout
deactivate FeedItems
Note over FeedItems: User scrolls down, clicks Load More
FeedItems->>FeedItems: loadMore() triggered
FeedItems->>FeedItems: setState({loadingMore: true, offset += 50})
FeedItems->>API: getFeed({...filters, limit: 50, offset: 50})
activate API
API->>Server: GET /feed?creator=...&tag=...&limit=50&offset=50
Server-->>API: {items: [...], hasMore: bool, fetchCount: number}
API-->>FeedItems: items[], hasMore, fetchCount
deactivate API
FeedItems->>FeedItems: setState({items: [...prev, ...new], hasMore, loadingMore: false})
FeedItems->>App: Render updated items list
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
web/src/components/navigation/MobileNav.tsx (1)
36-42:⚠️ Potential issue | 🟠 MajorPrevent stale unread badge when user identity changes.
The effect only depends on
isAuthenticated, so switching accounts without a full unauth cycle (or rapid logout/login) can display the previous user’s unread badge until the fetch completes. This is a privacy leak and UX glitch. Reset the count on unauth or user change, and guard against stale async responses.🛠️ Suggested fix (reset + guard + user identity dependency)
- useEffect(() => { - if (isAuthenticated) { - getUnreadNotificationCount() - .then((count) => setUnreadCount(count || 0)) - .catch(() => {}); - } - }, [isAuthenticated]); + useEffect(() => { + let cancelled = false; + if (!isAuthenticated || !user?.did) { + setUnreadCount(0); + return; + } + setUnreadCount(0); + getUnreadNotificationCount() + .then((count) => { + if (!cancelled) setUnreadCount(count || 0); + }) + .catch(() => { + if (!cancelled) setUnreadCount(0); + }); + return () => { + cancelled = true; + }; + }, [isAuthenticated, user?.did]);
🤖 Fix all issues with AI agents
In `@web/src/components/feed/MasonryFeed.tsx`:
- Around line 75-79: The emptyMessage ternary in MasonryFeed.tsx uses motivation
=== "bookmarking" ? "bookmarks" : "highlights", which mislabels "commenting" and
undefined; update the logic used when activeTab !== "my" to map motivation to
the correct noun (e.g., "bookmarking" -> "bookmarks", "commenting" ->
"annotations", default/undefined -> "annotations" or a generic
"annotations/highlights" per product rules). Implement this by replacing the
inline ternary with a small helper or switch (e.g.,
getLabelForMotivation(motivation)) and use that function in the emptyMessage
prop so the global tab shows the correct fallback text.
🧹 Nitpick comments (5)
README.md (1)
59-75: Add a brief Bun prerequisite note to avoid setup friction.The commands now assume Bun is installed; a one-line prerequisite (e.g., “Install Bun first”) in the Development section would prevent confusion for new contributors.
web/src/lib/og.ts (1)
198-199: Minor style inconsistency with similar pattern.The multi-line format here differs from the equivalent pattern in
fetchAnnotationOG(line 130), which uses a single line for a similar chained||expression. Consider keeping the formatting consistent across these similar functions.📝 Optional: align with single-line style
- let description = - item.description || extractBody(item.body) || item.bodyValue || ""; + let description = item.description || extractBody(item.body) || item.bodyValue || "";web/src/views/profile/Profile.tsx (2)
81-81: Unused ref after refactor.
loadMoreTimerRefappears to be a remnant from the previous implementation. The cleanup effect at lines 179-183 still references it, but it's never populated since the load-more logic has been moved toFeedItems.♻️ Suggested cleanup
- const loadMoreTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);Also remove the cleanup effect:
- useEffect(() => { - return () => { - if (loadMoreTimerRef.current) clearTimeout(loadMoreTimerRef.current); - }; - }, []);
185-186: Duplicate computation ofisHandleandresolvedDid.These variables are computed at lines 185-186 and again inside the
loadTabContenteffect at lines 190-191. Consider removing the duplicate computation inside the effect and using the outer scope variables.♻️ Suggested fix
useEffect(() => { const loadTabContent = async () => { - const isHandle = !did.startsWith("did:"); - const resolvedDid = isHandle ? profile?.did : did; - if (!resolvedDid) return; setDataLoading(true);web/src/components/feed/MasonryFeed.tsx (1)
1-1: Redundant import alias.Both
useStoreanduseNanoStoreare imported, but they're the same function. The aliasuseNanoStoreis used on line 23, whileuseStoreis used on line 22. Consider using just one for consistency.♻️ Suggested fix
-import { useStore as useNanoStore, useStore } from "@nanostores/react"; +import { useStore } from "@nanostores/react";Then update line 23:
- const layout = useNanoStore($feedLayout); + const layout = useStore($feedLayout);
| emptyMessage={ | ||
| activeTab === "my" | ||
| ? emptyMessage | ||
| : `No ${motivation === "bookmarking" ? "bookmarks" : "highlights"} from the community yet.` | ||
| } |
There was a problem hiding this comment.
Incomplete emptyMessage fallback for global tab.
The ternary only handles "bookmarking" explicitly, defaulting everything else to "highlights". This could produce incorrect messages when motivation is "commenting" (would show "No highlights" instead of "No annotations") or undefined (would also show "No highlights").
🐛 Suggested fix
emptyMessage={
activeTab === "my"
? emptyMessage
- : `No ${motivation === "bookmarking" ? "bookmarks" : "highlights"} from the community yet.`
+ : `No ${
+ motivation === "bookmarking"
+ ? "bookmarks"
+ : motivation === "commenting"
+ ? "annotations"
+ : motivation === "highlighting"
+ ? "highlights"
+ : "items"
+ } from the community yet.`
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| emptyMessage={ | |
| activeTab === "my" | |
| ? emptyMessage | |
| : `No ${motivation === "bookmarking" ? "bookmarks" : "highlights"} from the community yet.` | |
| } | |
| emptyMessage={ | |
| activeTab === "my" | |
| ? emptyMessage | |
| : `No ${ | |
| motivation === "bookmarking" | |
| ? "bookmarks" | |
| : motivation === "commenting" | |
| ? "annotations" | |
| : motivation === "highlighting" | |
| ? "highlights" | |
| : "items" | |
| } from the community yet.` | |
| } |
🤖 Prompt for AI Agents
In `@web/src/components/feed/MasonryFeed.tsx` around lines 75 - 79, The
emptyMessage ternary in MasonryFeed.tsx uses motivation === "bookmarking" ?
"bookmarks" : "highlights", which mislabels "commenting" and undefined; update
the logic used when activeTab !== "my" to map motivation to the correct noun
(e.g., "bookmarking" -> "bookmarks", "commenting" -> "annotations",
default/undefined -> "annotations" or a generic "annotations/highlights" per
product rules). Implement this by replacing the inline ternary with a small
helper or switch (e.g., getLabelForMotivation(motivation)) and use that function
in the emptyMessage prop so the global tab shows the correct fallback text.
Built on top of #43.
Reuses some components, through which closes #44 :)
Summary by CodeRabbit
Documentation
New Features
Improvements