Skip to content

Fix: Friends active data#656

Merged
feruzm merged 8 commits intodevelopfrom
friends
Feb 18, 2026
Merged

Fix: Friends active data#656
feruzm merged 8 commits intodevelopfrom
friends

Conversation

@feruzm
Copy link
Member

@feruzm feruzm commented Feb 18, 2026

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • Improved friends list pagination and loading behavior.
    • Enhanced fragment search to handle edge cases more robustly.
    • Strengthened profile update validation.
  • Chores

    • Updated SDK and wallet package dependencies.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 18, 2026

Caution

Review failed

The pull request is closed.

📝 Walkthrough

Walkthrough

This is a multi-package release that fixes friends list data handling and pagination across the web app and SDK. The main change streamlines friends list filtering and pagination logic, updates the FriendsList component's mode prop to a discriminated union type, improves null-safety in profile update mutations, and bumps versions in SDK and wallets packages.

Changes

Cohort / File(s) Summary
Friends List Data Flow
apps/web/src/app/(dynamicPages)/profile/[username]/_components/friends/friends-list.tsx
Refactored data aggregation and filtering logic: added type-safe mode prop ("following" | "followers"), replaced multi-page aggregation with streamlined dataFlow using flattened pages array, updated timeframe-based filtering boundaries, integrated hasNextPage for pagination control, and reworked lastSeen calculations via dayjs.fromNow().
Friends List Component Usage
apps/web/src/app/(dynamicPages)/profile/[username]/_components/friends/index.tsx
Updated FriendsList mode prop from "follower" to "followers" for type consistency with discriminated union.
Profile Update Mutations
apps/web/src/api/mutations/update-profile.ts, apps/web/src/api/mutations/pin-to-blog.ts
Added null-safety by accepting nullable account parameter (FullAccount | null) in useUpdateProfile public signature; added explicit guard to throw if account unavailable; updated mutation key to use optional chaining.
Fragment Search Logic
apps/web/src/features/shared/fragments/fragments-list.tsx
Updated search filtering to use optional chaining with includes() instead of indexOf() for safer handling of undefined titles.
Package Releases
packages/sdk/package.json, packages/sdk/CHANGELOG.md, packages/wallets/package.json, packages/wallets/CHANGELOG.md
Bumped SDK version from 2.0.2 to 2.0.3 with friends active data fix; bumped wallets version from 1.5.30 to 1.5.31 with updated SDK dependency.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • Fix: Friends active data #656: Directly related—makes the same code-level changes to friends-list.tsx including type import for Friend, Props.mode discriminated union, data flow refactoring, hasNextPage pagination, and timeframe/lastSeen logic updates.

Poem

🐰 A hop through the friends, now filtered with care,
Discriminated types ensure data so fair,
Pagination's simpler, no more complex loops,
Null-safety guards keep the mutations in groups,
Version bumps echo—2.0.3 and 1.5.31 bloom! 🌱

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Fix: Friends active data' is vague and does not clearly convey the main changes. While the PR touches friends-related code, the title doesn't specify what aspect is being fixed or what 'active data' means. Consider a more descriptive title such as 'Fix: Improve friends list filtering and pagination with discriminated union types' or 'Refactor: Friends list data flow and timeframe-based filtering' to better reflect the actual changes.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch friends

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
apps/web/src/app/(dynamicPages)/profile/[username]/_components/friends/friends-list.tsx (2)

29-37: Custom hasMore re-implements hasNextPage already provided by useInfiniteQuery.

getNextPageParam in the query options already encodes the "is there a next page" logic (lastPage.length === limit). useInfiniteQuery surfaces this as a hasNextPage boolean. The custom hasMore memo duplicates that logic and could silently diverge if getNextPageParam changes.

♻️ Proposed refactor — use built-in hasNextPage
  const {
    data,
    isFetching: isFriendsFetching,
-   fetchNextPage
+   fetchNextPage,
+   hasNextPage
  } = useInfiniteQuery(
    getFriendsInfiniteQueryOptions(account.name, mode, {
      limit: loadLimit
    })
  );

- const hasMore = useMemo(
-   () => (data?.pages?.[data.pages.length - 1]?.length ?? 0) >= loadLimit,
-   [data?.pages]
- );

Then replace hasMore with hasNextPage at the use site (line 149):

- <Button disabled={isFetching || !hasMore} onClick={() => fetchNextPage()}>
+ <Button disabled={isFetching || !hasNextPage} onClick={() => fetchNextPage()}>

Also applies to: 84-87

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/web/src/app/`(dynamicPages)/profile/[username]/_components/friends/friends-list.tsx
around lines 29 - 37, The custom memo "hasMore" duplicates pagination logic
already provided by useInfiniteQuery; update the component to stop computing
hasMore and instead use the built-in hasNextPage returned by useInfiniteQuery
(from the call that uses getFriendsInfiniteQueryOptions). Replace uses of the
custom hasMore (and its related memo) with hasNextPage and keep calling
fetchNextPage when you need to load more; ensure you remove or refactor the
memoized hasMore logic so it cannot diverge from getNextPageParam.

56-81: dayjs(item.active) computed twice per item — consolidate into a single pass.

dayjs(item.active) is constructed in the filter (line 59) and then again in the map (line 80). Combine into a single .reduce() or pre-compute to one iteration:

♻️ Proposed refactor — single pass over rawItems
- const filtered = rawItems.filter((item) => {
-   if (!type) return true;
-   const lastSeenTime = dayjs(item.active).toDate();
-   const timeDifference = new Date().getTime() - lastSeenTime.getTime();
-   const daysDifference = Math.ceil(timeDifference / (1000 * 3600 * 24));
-   const yearsDifference = Math.ceil(daysDifference / 365);
-   return (
-     (type === FilterFriendsType.Recently && daysDifference < 7) || ...
-   );
- });
-
- return filtered.map((item) => ({
-   name: item.name,
-   reputation: item.reputation,
-   lastSeen: dayjs(item.active).fromNow()
- }));
+ return rawItems.reduce<Friend[]>((acc, item) => {
+   const dayjsActive = dayjs(item.active);
+   if (type) {
+     const timeDifference = new Date().getTime() - dayjsActive.toDate().getTime();
+     const daysDifference = Math.ceil(timeDifference / (1000 * 3600 * 24));
+     const yearsDifference = Math.ceil(daysDifference / 365);
+     const matches =
+       (type === FilterFriendsType.Recently && daysDifference < 7) || ...;
+     if (!matches) return acc;
+   }
+   acc.push({ name: item.name, reputation: item.reputation, lastSeen: dayjsActive.fromNow() });
+   return acc;
+ }, []);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/web/src/app/`(dynamicPages)/profile/[username]/_components/friends/friends-list.tsx
around lines 56 - 81, The code is creating dayjs(item.active) twice for each
rawItems entry; refactor the filter+map into a single pass (use
Array.prototype.reduce or a single Array.prototype.map with conditional
inclusion) over rawItems and compute const active = dayjs(item.active) once per
item, then reuse active.toDate() / active.fromNow() and the computed
daysDifference/yearsDifference to decide inclusion against FilterFriendsType and
produce the output shape ({ name, reputation, lastSeen }), replacing the current
filtered/map logic.
apps/web/src/features/shared/fragments/fragments-list.tsx (1)

47-47: Defensive null-guard is correct; consider .includes() for readability.

The ?? "" guard prevents a TypeError when title is undefined. Optionally, .indexOf(...) !== -1 can be replaced with the more idiomatic .includes():

♻️ Optional readability refactor
- .filter((x) => (x.title ?? "").toLowerCase().indexOf(searchQuery.toLowerCase()) !== -1)
+ .filter((x) => (x.title ?? "").toLowerCase().includes(searchQuery.toLowerCase()))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/features/shared/fragments/fragments-list.tsx` at line 47, The
filter callback currently uses (x.title ??
"").toLowerCase().indexOf(searchQuery.toLowerCase()) !== -1 which is defensive
but less readable; update the predicate in the fragments list filter to keep the
null-guard (x.title ?? "") and replace indexOf(...) !== -1 with .includes(...)
on the lowercased string so it reads (x.title ??
"").toLowerCase().includes(searchQuery.toLowerCase()), ensuring you still call
toLowerCase() on both x.title and searchQuery in the same filter function.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@apps/web/src/app/`(dynamicPages)/profile/[username]/_components/friends/friends-list.tsx:
- Around line 64-74: The filter predicates in friends-list.tsx (the block using
FilterFriendsType) have exclusive boundaries and an exact-match for OneYear that
create gaps; change them to use inclusive ranges and make OneYear a range
instead of daysDifference === 365. Specifically: use daysDifference <= 7 for
Recently; use daysDifference > 7 && daysDifference <= 30 for ThisMonth; use
daysDifference > 30 && daysDifference < 365 for ThisYear (or <= 364 if you
prefer 365 as the start of OneYear); make OneYear a range like daysDifference >=
365 && daysDifference < 730 (one-to-two years) instead of an exact equality; and
make MoreThanOneYear match yearsDifference >= 2 (or yearsDifference >= 1 if you
want >1 year inclusive) so there are no gaps between these predicates.

---

Nitpick comments:
In
`@apps/web/src/app/`(dynamicPages)/profile/[username]/_components/friends/friends-list.tsx:
- Around line 29-37: The custom memo "hasMore" duplicates pagination logic
already provided by useInfiniteQuery; update the component to stop computing
hasMore and instead use the built-in hasNextPage returned by useInfiniteQuery
(from the call that uses getFriendsInfiniteQueryOptions). Replace uses of the
custom hasMore (and its related memo) with hasNextPage and keep calling
fetchNextPage when you need to load more; ensure you remove or refactor the
memoized hasMore logic so it cannot diverge from getNextPageParam.
- Around line 56-81: The code is creating dayjs(item.active) twice for each
rawItems entry; refactor the filter+map into a single pass (use
Array.prototype.reduce or a single Array.prototype.map with conditional
inclusion) over rawItems and compute const active = dayjs(item.active) once per
item, then reuse active.toDate() / active.fromNow() and the computed
daysDifference/yearsDifference to decide inclusion against FilterFriendsType and
produce the output shape ({ name, reputation, lastSeen }), replacing the current
filtered/map logic.

In `@apps/web/src/features/shared/fragments/fragments-list.tsx`:
- Line 47: The filter callback currently uses (x.title ??
"").toLowerCase().indexOf(searchQuery.toLowerCase()) !== -1 which is defensive
but less readable; update the predicate in the fragments list filter to keep the
null-guard (x.title ?? "") and replace indexOf(...) !== -1 with .includes(...)
on the lowercased string so it reads (x.title ??
"").toLowerCase().includes(searchQuery.toLowerCase()), ensuring you still call
toLowerCase() on both x.title and searchQuery in the same filter function.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
apps/web/src/api/mutations/update-profile.ts (1)

42-44: onError conflates null-account errors with server errors

The null guard on lines 42-44 throws "Account is not available" but onError unconditionally shows "g.server-error", swallowing the distinction. A user who hits this path (account not yet loaded) will see a misleading server-error toast.

♻️ Proposed fix
-    onError: () => {
-      error(i18next.t("g.server-error"));
+    onError: (err: Error) => {
+      error(err.message === "Account is not available"
+        ? err.message
+        : i18next.t("g.server-error"));
     },

Or, preferably, disable the mutate call at the call site when account is null (e.g. guard the caller) so this code path is never reached at all.

Also applies to: 52-57

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/api/mutations/update-profile.ts` around lines 42 - 44, The guard
that currently does "if (!account) throw new Error('Account is not available')"
conflates a missing-account situation with server errors in the shared onError
handler; either prevent calling the mutate when account is null at the caller,
or change this check to surface a distinguishable condition (e.g., return early
or throw a specific error type/value like AccountUnavailableError or an Error
with a machine-readable code) so onError can detect and handle it separately;
apply the same change to the similar null-account guard at the other block
(lines 52-57) and update onError to look for that specific error type/code (or
rely on the caller-side guard) rather than treating every error as
"g.server-error".
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/web/src/api/mutations/pin-to-blog.ts`:
- Line 15: The TypeScript error occurs because account (from useActiveAccount)
can be undefined but useUpdateProfile expects FullAccount | null; update the
call that creates updateProfile (the line with const { mutateAsync:
updateProfile } = useUpdateProfile(account);) to pass a value of type
FullAccount | null by converting undefined to null (e.g., pass account ?? null)
so useUpdateProfile receives either a FullAccount or null and the compile error
is resolved.

In
`@apps/web/src/app/`(dynamicPages)/profile/[username]/_components/friends/friends-list.tsx:
- Around line 60-62: Guard against falsy item.active before converting with
dayjs: if item.active is undefined/null/empty, skip computing
lastSeenTime/timeDifference/daysDifference (or set a sentinel like null) so
those users aren’t treated as "Recently"; update the logic around lastSeenTime,
timeDifference and daysDifference in friends-list.tsx to early-return or branch
when item.active is falsy and ensure downstream rendering/grouping ignores or
places such items in the appropriate bucket instead of treating them as now.

---

Nitpick comments:
In `@apps/web/src/api/mutations/update-profile.ts`:
- Around line 42-44: The guard that currently does "if (!account) throw new
Error('Account is not available')" conflates a missing-account situation with
server errors in the shared onError handler; either prevent calling the mutate
when account is null at the caller, or change this check to surface a
distinguishable condition (e.g., return early or throw a specific error
type/value like AccountUnavailableError or an Error with a machine-readable
code) so onError can detect and handle it separately; apply the same change to
the similar null-account guard at the other block (lines 52-57) and update
onError to look for that specific error type/code (or rely on the caller-side
guard) rather than treating every error as "g.server-error".

@feruzm feruzm added the patch Bug fixes and patches (1.0.0 → 1.0.1), add this only if any packages/ have patch changes in PR label Feb 18, 2026
@feruzm feruzm merged commit e1b7ba7 into develop Feb 18, 2026
1 check was pending
@feruzm feruzm deleted the friends branch February 18, 2026 07:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

patch Bug fixes and patches (1.0.0 → 1.0.1), add this only if any packages/ have patch changes in PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant