Skip to content

profile wave selection#2259

Merged
ragnep merged 9 commits intomainfrom
wave-selection
Apr 15, 2026
Merged

profile wave selection#2259
ragnep merged 9 commits intomainfrom
wave-selection

Conversation

@ragnep
Copy link
Copy Markdown
Contributor

@ragnep ragnep commented Apr 13, 2026

Summary by CodeRabbit

Release Notes

  • New Features

    • Added "Curations" tab to user profiles for managing official waves
    • Introduced ability to set or clear an official profile wave
    • Added masonry and list view toggle for curations display
    • Enhanced drop rendering with improved identity and interaction display options
    • Added full-width media support for improved content visibility
  • Improvements

    • Improved query parameter handling during navigation
    • Enhanced permission checks for curation management

Signed-off-by: ragnep <ragneinfo@gmail.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 13, 2026

Warning

Rate limit exceeded

@ragnep has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 16 minutes and 26 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 16 minutes and 26 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 17e24304-802f-4e1a-8f41-26cd79f31a38

📥 Commits

Reviewing files that changed from the base of the PR and between 33549e8 and 28e172b.

📒 Files selected for processing (3)
  • components/user/layout/userTabs.config.ts
  • components/user/waves/UserPageProfileWave.tsx
  • components/waves/drops/WaveDropPartContentMedias.tsx
📝 Walkthrough

Walkthrough

This PR introduces comprehensive "profile waves" functionality, allowing users to designate a wave/curation as their official profile wave. It adds new route pages, API endpoints, hooks, and UI components for managing and displaying profile waves, enhances drop rendering with identity mode and interaction control flags, implements a masonry/list view toggle for curations, and updates routing and header logic accordingly.

Changes

Cohort / File(s) Summary
Routing & Navigation
app/[user]/waves/page.tsx, app/[user]/curations/page.tsx, components/user/layout/userTabs.config.ts, components/user/layout/UserPageTabs.tsx, components/user/layout/UserPageLayout.tsx
Adds new curations route page with redirect logic to preserve query params; introduces new "Curation" tab to user page tabs config; updates tab visibility to require hasProfileWave boolean; passes initialProfile to UserPageTabs and derives profile wave state through component hierarchy.
Profile Wave Management
components/user/waves/UserPageProfileWave.tsx, components/waves/header/options/profile-wave/WaveProfileWaveAction.tsx, hooks/useProfileWaveMutation.ts, services/api/profile-wave-api.ts, helpers/waves/wave.helpers.ts
Implements user-facing profile wave selection/clearing UI with API integration; adds mutation hook for profile wave operations with toast notifications; creates API endpoints for setting/clearing profile waves; introduces helper to identify public non-direct-message waves.
Drop Rendering with Identity & Interaction Control
components/waves/drops/Drop.tsx, components/waves/drops/WaveDrop.tsx, components/waves/drops/WaveDropContent.tsx, components/waves/drops/WaveDropPart.tsx, components/waves/drops/WaveDropPartContent.tsx, components/waves/drops/WaveDropPartContentMedias.tsx, components/waves/drops/WaveDropPartDrop.tsx, components/waves/drops/drop.types.ts, components/waves/drops/DropMinimalIdentityRow.tsx, components/memes/drops/MemeParticipationDrop.tsx, components/waves/drops/participation/*, components/waves/drops/winner/*
Extends drop component hierarchy with identityMode (default/minimal/hidden) and showInteractions (boolean) props; refactors WaveDrop with extracted helpers for grouping/author rendering; gates interactions (long-press, actions, reactions) and identity display based on new flags; adds DropMinimalIdentityRow for compact author display; propagates props through all drop variants and participation/winner subcomponents.
Curation Display & Masonry
components/user/waves/UserPageProfileWaveMasonry.tsx, hooks/useProfileCurationViewMode.ts, components/brain/my-stream/curations/MyStreamWaveCurationContent.tsx, components/brain/my-stream/curations/CurationEmptyState.tsx, hooks/useCurationManagementPermission.ts
Adds masonry/list view for curations with pagination; new hook for toggling between view modes; updates curation content component with optional constrainToViewport prop and permission-checking refactor; introduces reusable empty-state component; adds permission hook for curation management checks.
Media Rendering
components/waves/drops/WaveDropPartContentFullWidthImage.tsx
New full-width image component with load/error handling, retry capability, modal pan/zoom, and fullscreen support.
Header & Utility
components/header/AppHeader.tsx, components/waves/header/options/WaveHeaderOptions.tsx, components/utils/button/SecondaryButton.tsx, components/brain/my-stream/tabs/MyStreamWaveCreateCurationAction.tsx
Extracts inline header logic into helpers (getHeaderTitle, HeaderTitleContent, HeaderGalleryToggle, HeaderWaveLinkAction); refactors wave header options menu from framer-motion to shared dropdown wrapper; adjusts secondary button text sizing; removes padding from curation action wrapper.
Dependencies
package.json
Adds masonic (v4.1.0) for masonry grid rendering.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant ProfilePage as Profile Page
    participant WaveComponent as UserPageProfileWave
    participant API as Profile Wave API
    participant State as React Query State
    
    User->>ProfilePage: Visit profile with waves enabled
    ProfilePage->>WaveComponent: Load profile wave component
    WaveComponent->>State: Check if profileWaveId exists
    alt No Profile Wave
        WaveComponent->>User: Return null (no wave set)
    else Has Profile Wave
        WaveComponent->>API: Fetch wave details
        API-->>State: Return wave data
        WaveComponent->>API: Fetch wave curations
        API-->>State: Return curations list
        WaveComponent->>WaveComponent: Select earliest curation
        WaveComponent->>User: Render official wave panel
        User->>WaveComponent: Click "Clear official wave"
        WaveComponent->>API: Call clearProfileWave
        API-->>State: Update profile (removed wave_id)
        WaveComponent->>User: Show success toast & refresh
    end
Loading
sequenceDiagram
    actor User
    participant WaveHeader as Wave Header Options
    participant WaveAction as WaveProfileWaveAction
    participant Mutation as useProfileWaveMutation
    participant API as Profile Wave API
    
    User->>WaveHeader: Click wave options menu
    WaveHeader->>WaveAction: Render action if user is author
    alt User can manage profile waves
        User->>WaveAction: Click "Set as profile wave"
        WaveAction->>Mutation: Call updateProfileWave(waveId)
        Mutation->>API: POST /profiles/{identity}/wave
        API-->>Mutation: Return updated profile
        Mutation->>User: Show success toast
    else User cannot manage
        WaveAction->>WaveAction: Return null (hidden)
    end
Loading
sequenceDiagram
    participant Masonry as UserPageProfileWaveMasonry
    participant Drops as Drop Components
    participant DropRenderer as DropRenderer Logic
    
    Masonry->>Drops: Load wave drops with pagination
    Masonry->>Drops: Pass identityMode="minimal" showInteractions=false
    Drops->>DropRenderer: Render drops with identity constraints
    DropRenderer->>DropRenderer: Check identityMode flag
    alt identityMode === "minimal"
        DropRenderer->>DropRenderer: Render DropMinimalIdentityRow instead of full header
    else identityMode === "hidden"
        DropRenderer->>DropRenderer: Hide all identity UI
    else identityMode === "default"
        DropRenderer->>DropRenderer: Render full identity (default behavior)
    end
    DropRenderer->>DropRenderer: Gate interactions based on showInteractions flag
    alt showInteractions === false
        DropRenderer->>DropRenderer: Hide actions, reactions, long-press
    end
    Drops->>Masonry: Display rendered drops in grid
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • ipad long press fix #1766: Modifies Wave drop components and touch/long-press handling within WaveDrop and related components, overlapping with this PR's drop rendering enhancements.
  • Wave header impro #2043: Updates components/header/AppHeader.tsx with gallery toggle and header action logic, directly related to this PR's header refactoring.
  • logged out placeholder in the wave view #2153: Changes useWaveById hook signature and behavior; new wave components added in this PR depend on these wave-fetching hooks.

Suggested reviewers

  • simo6529

🐰 A rabbit hops through waves so grand,
With profiles and curations hand in hand,
Masonry grids and droplets dance,
While identity modes take their stance,
The official wave now claims its place—
Hopping together, setting the pace! 🌊✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'profile wave selection' directly reflects the main change: adding functionality for users to select/manage their profile wave, which is implemented across multiple new components and modified files.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch wave-selection

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
Copy Markdown

@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: 3

🧹 Nitpick comments (1)
services/api/profile-wave-api.ts (1)

6-17: Unify the POST and DELETE paths for this endpoint.

setProfileWave goes through the shared API helper, while clearProfileWave reimplements URL building, auth headers, and error parsing locally. That split is easy to drift the next time request semantics change. Please hide both operations behind the same internal request helper (or add a shared DELETE variant) so this endpoint stays consistent.

Also applies to: 19-49, 54-83

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

In `@services/api/profile-wave-api.ts` around lines 6 - 17, setProfileWave and
clearProfileWave are inconsistent: setProfileWave uses the shared API helper
while clearProfileWave rebuilds the URL, headers, and error parsing locally;
refactor so both use the same internal request helper (or add a shared DELETE
variant) to avoid drift. Update clearProfileWave to call
buildProfileWaveUrl(identity) and buildProfileWaveHeaders() and then invoke the
existing shared request helper (or add a delete method on that helper) instead
of reimplementing fetch/authorization/error handling; ensure you use the same
error parsing and response handling logic as setProfileWave so URL/header
symbols (buildProfileWaveUrl, buildProfileWaveHeaders) and function names
(setProfileWave, clearProfileWave) remain consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@components/user/layout/UserPageLayout.tsx`:
- Line 18: The current assignment of mainAddress from
initialProfile.primary_wallet can become undefined and wipe out the route-level
fallback; change the logic so mainAddress uses initialProfile.primary_wallet
when present and otherwise falls back to the route-level fallbackMainAddress
(i.e., mainAddress = initialProfile.primary_wallet if defined, else
fallbackMainAddress) so UserPageHeader continues to receive the fallback when
handleOrWallet identifies the page.

In `@components/user/waves/UserPageProfileWave.tsx`:
- Around line 114-118: The current useWaveCurations call defaults data to [] so
failures are indistinguishable from an empty wave; update the hook usage
(useWaveCurations) to stop forcing data = [] and destructure isError and error
(e.g., const { data: curations, isLoading: areCurationsLoading, isError:
areCurationsError, error: curationsError } = useWaveCurations(...)), then change
the render logic to explicitly handle areCurationsError (render an error
state/message using curationsError) before falling back to "No curations yet" or
empty list; apply the same change to the other occurrence around lines 216-244.

In `@components/waves/header/options/profile-wave/WaveProfileWaveAction.tsx`:
- Around line 22-26: The ownership check in canManageProfileWave currently fails
when connectedProfile.handle is absent; change it to use the same
identity-fallback logic as useProfileWaveMutation: replace the handle-only
comparison (connectedProfile?.handle === wave.author.handle) with a robust
equality that checks handle, then primary_wallet, then id (e.g., compare
connectedProfile.handle || connectedProfile.primary_wallet ||
connectedProfile.id to wave.author.handle || wave.author.primary_wallet ||
wave.author.id), or call the existing identity-equality helper if one exists;
keep the other guards (activeProfileProxy and isPublicNonDirectMessageWave)
unchanged.

---

Nitpick comments:
In `@services/api/profile-wave-api.ts`:
- Around line 6-17: setProfileWave and clearProfileWave are inconsistent:
setProfileWave uses the shared API helper while clearProfileWave rebuilds the
URL, headers, and error parsing locally; refactor so both use the same internal
request helper (or add a shared DELETE variant) to avoid drift. Update
clearProfileWave to call buildProfileWaveUrl(identity) and
buildProfileWaveHeaders() and then invoke the existing shared request helper (or
add a delete method on that helper) instead of reimplementing
fetch/authorization/error handling; ensure you use the same error parsing and
response handling logic as setProfileWave so URL/header symbols
(buildProfileWaveUrl, buildProfileWaveHeaders) and function names
(setProfileWave, clearProfileWave) remain consistent.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 293ff679-389e-466a-9e78-6b9207b2b90c

📥 Commits

Reviewing files that changed from the base of the PR and between ed35da5 and a8e7d80.

📒 Files selected for processing (12)
  • app/[user]/waves/page.tsx
  • components/brain/my-stream/curations/MyStreamWaveCurationContent.tsx
  • components/brain/my-stream/tabs/MyStreamWaveCreateCurationAction.tsx
  • components/user/layout/UserPageLayout.tsx
  • components/user/layout/UserPageTabs.tsx
  • components/user/layout/userTabs.config.ts
  • components/user/waves/UserPageProfileWave.tsx
  • components/waves/header/options/WaveHeaderOptions.tsx
  • components/waves/header/options/profile-wave/WaveProfileWaveAction.tsx
  • helpers/waves/wave.helpers.ts
  • hooks/useProfileWaveMutation.ts
  • services/api/profile-wave-api.ts

Comment thread components/user/layout/UserPageLayout.tsx Outdated
Comment thread components/user/waves/UserPageProfileWave.tsx Outdated
Signed-off-by: ragnep <ragneinfo@gmail.com>
Copy link
Copy Markdown

@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: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
components/waves/drops/winner/WinnerDrop.tsx (1)

28-38: ⚠️ Potential issue | 🟠 Major

MemeWinnerDrop does not accept the identityMode prop.

While DefaultWinnerDrop accepts this prop, MemeWinnerDrop's interface only includes drop, showReplyAndQuote, and onReply. Spreading {...props} passes an unhandled prop to meme wave drops, silently ignoring the identityMode setting and creating inconsistent behavior between meme and default waves.

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

In `@components/waves/drops/winner/WinnerDrop.tsx` around lines 28 - 38, The
WinnerDrop component spreads WinnerDropProps (including identityMode) into
MemeWinnerDrop but MemeWinnerDrop's props type/definition doesn't include
identityMode, causing it to be dropped; update MemeWinnerDrop's prop interface
or component signature to include identityMode (or extend WinnerDropProps / the
shared prop type), accept the prop in the MemeWinnerDrop component, and
forward/use it where appropriate (e.g., pass it to any child components or logic
that DefaultWinnerDrop currently uses) so meme-wave drops honor the identityMode
consistently with DefaultWinnerDrop.
♻️ Duplicate comments (1)
components/user/waves/UserPageProfileWave.tsx (1)

213-217: ⚠️ Potential issue | 🟠 Major

Handle curation fetch failures separately from “no curations”.

data: curations = [] still collapses a failed useWaveCurations() request into the empty-state path, so backend/access errors will render as “No curations yet”. Please surface isError (and ideally error) here before falling back to the empty state.

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

In `@components/user/waves/UserPageProfileWave.tsx` around lines 213 - 217, The
current destructure defaults `data: curations = []` from useWaveCurations hides
request failures by turning errors into the “no curations” path; change the
destructure to also pull out isError and error (e.g., const { data: curations,
isLoading: areCurationsLoading, isError: areCurationsError, error:
curationsError } = useWaveCurations({ waveId: wave?.id ?? "", enabled:
!!wave?.id })), keep curations as undefined when the request fails, and update
the rendering logic that uses curations to first check areCurationsError (and
optionally display curationsError) before treating an empty array as “no
curations yet”.
🧹 Nitpick comments (4)
hooks/useCurationManagementPermission.ts (1)

5-20: Consider making probeDropId type explicitly allow falsy values.

The type declares probeDropId: string but the enabled: Boolean(probeDropId) check suggests empty strings are valid to disable the query. Consider typing it as probeDropId: string | undefined or adding a JSDoc comment to clarify the contract.

♻️ Suggested type improvement
 export function useCurationManagementPermission({
   curationId,
   probeDropId,
 }: {
   readonly curationId: string;
-  readonly probeDropId: string;
+  readonly probeDropId: string | undefined;
 }) {
   const { data: probeCurations = [] } = useDropCurations({
-    dropId: probeDropId,
+    dropId: probeDropId ?? "",
     enabled: Boolean(probeDropId),
   });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hooks/useCurationManagementPermission.ts` around lines 5 - 20, The parameter
type for probeDropId in useCurationManagementPermission is declared as string
but treated as possibly falsy; change the prop signature in
useCurationManagementPermission to accept probeDropId: string | undefined (or
null) so Boolean(probeDropId) correctly reflects the type, update any callers
that pass optional values to satisfy the new union type, and adjust any related
types/props that flow into this hook to maintain type-safety for
useDropCurations({ dropId: probeDropId, enabled: Boolean(probeDropId) }).
components/waves/drops/DropMinimalIdentityRow.tsx (1)

19-26: Consider simplifying the click handler.

The handleNavigation callback prevents default Link behavior and then manually calls router.push() with the same path. Since stopPropagation() alone would prevent bubbling to parent handlers while still allowing the <Link> to navigate naturally, this approach duplicates navigation logic.

♻️ Simplified approach
-  const handleNavigation = useCallback(
-    (event: React.MouseEvent, path: string) => {
-      event.preventDefault();
-      event.stopPropagation();
-      router.push(path);
-    },
-    [router]
-  );
+  const handleClick = useCallback((event: React.MouseEvent) => {
+    event.stopPropagation();
+  }, []);

Then use onClick={handleClick} on the Link. This lets Next.js handle navigation optimally (prefetching, client-side transitions) while still preventing event bubbling.

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

In `@components/waves/drops/DropMinimalIdentityRow.tsx` around lines 19 - 26, The
click handler handleNavigation currently calls event.preventDefault() and
router.push(path), duplicating Link's navigation; change it to a simpler handler
(e.g., handleClick) that only calls event.stopPropagation() so the Link's native
Next.js navigation/prefetching is preserved, remove the router.push call and
router dependency from the useCallback dependencies, and update places using
handleNavigation to use the new handler on the Link's onClick.
components/header/AppHeader.tsx (1)

101-141: getHeaderTitle may return undefined.

When waveId is truthy and the wave has loaded but wave.name is null or undefined, line 128 returns undefined. While this may render as nothing (React handles undefined gracefully), consider whether this is the intended behavior or if a fallback should be provided.

♻️ Optional fallback
   if (waveId) {
     if (isWaveResolving) return <Spinner />;
-    return wave?.name;
+    return wave?.name ?? "Wave";
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/header/AppHeader.tsx` around lines 101 - 141, getHeaderTitle can
return undefined when waveId is set but wave.name is null/undefined; update the
waveId branch in getHeaderTitle to return a safe fallback instead of possibly
undefined: when waveId is truthy, if isWaveResolving return <Spinner /> as
before, otherwise return wave?.name ?? a fallback (for example
sliceString(capitalizeEveryWord(pageTitle), 20) or another sensible string);
ensure you reference getHeaderTitle, waveId, wave, isWaveResolving, Spinner, and
sliceString/capitalizeEveryWord so the change is applied in the correct branch.
components/user/waves/UserPageProfileWaveMasonry.tsx (1)

166-188: Move the masonry reset bookkeeping out of render.

useProfileMasonryRenderState() is calling setState() while the hook is rendering. That forces an extra render whenever the curation changes or the drop count shrinks, and it’s a brittle pattern under React 19. Please move this synchronization into useEffect/useReducer and derive shouldResetMasonry from committed state instead.

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

In `@components/user/waves/UserPageProfileWaveMasonry.tsx` around lines 166 - 188,
The hook useProfileMasonryRenderState() is calling setState() during render to
update resetVersion/lastDropCount when curationId or dropCount change; move this
synchronization out of render into a dedicated useEffect or switch to a
useReducer so state updates are committed instead of invoked during render,
compute shouldResetMasonry from the committed state (resetVersion/lastDropCount)
rather than setting it inline, and update the logic that currently checks
curationId, dropCount, and uses setState(...) to run inside the effect/reducer
(preserve the same updates to resetVersion, lastDropCount, and curationId).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@components/drops/view/item/content/media/DropListItemContentMediaImage.tsx`:
- Around line 92-108: The branch in DropListItemContentMediaImage renders a raw
<img> with src=getScaledImageUri(src,imageScale) which skips the existing
FallbackImage behavior and causes broken images when the scaled derivative is
missing; change this to preserve primary→fallback logic by using the same
FallbackImage approach (or replicate its behavior) here: attempt the scaled URL
from getScaledImageUri(src,imageScale) first, and on its onError swap to the
original src without incrementing errorCount/retryTick so the retry counter only
advances when the original also fails; adjust handleError/ onError wiring
accordingly so DropListItemContentMediaImage still falls back to src before
calling the failure/retry logic.

In `@components/waves/drops/WaveDrop.tsx`:
- Around line 458-469: The reactions/metadata row still hardcodes the avatar
gutter offset (tw-ml-11 / tw-ml-[3.25rem]) causing the footer to shift when
showAuthorInfo is false; update that row to compute its left margin using the
same logic used for the content block by deriving the offset from showAuthorInfo
and the grouping state (shouldGroupWithPreviousDrop/shouldGroupWithNextDrop) —
reuse the output of shouldShowAuthorInfo(...) and getGroupingClass(...) (or
replicate that branching) so the reactions row uses the same conditional margin
classes as the content area when identityMode is "hidden" or when drops are
grouped.

---

Outside diff comments:
In `@components/waves/drops/winner/WinnerDrop.tsx`:
- Around line 28-38: The WinnerDrop component spreads WinnerDropProps (including
identityMode) into MemeWinnerDrop but MemeWinnerDrop's props type/definition
doesn't include identityMode, causing it to be dropped; update MemeWinnerDrop's
prop interface or component signature to include identityMode (or extend
WinnerDropProps / the shared prop type), accept the prop in the MemeWinnerDrop
component, and forward/use it where appropriate (e.g., pass it to any child
components or logic that DefaultWinnerDrop currently uses) so meme-wave drops
honor the identityMode consistently with DefaultWinnerDrop.

---

Duplicate comments:
In `@components/user/waves/UserPageProfileWave.tsx`:
- Around line 213-217: The current destructure defaults `data: curations = []`
from useWaveCurations hides request failures by turning errors into the “no
curations” path; change the destructure to also pull out isError and error
(e.g., const { data: curations, isLoading: areCurationsLoading, isError:
areCurationsError, error: curationsError } = useWaveCurations({ waveId: wave?.id
?? "", enabled: !!wave?.id })), keep curations as undefined when the request
fails, and update the rendering logic that uses curations to first check
areCurationsError (and optionally display curationsError) before treating an
empty array as “no curations yet”.

---

Nitpick comments:
In `@components/header/AppHeader.tsx`:
- Around line 101-141: getHeaderTitle can return undefined when waveId is set
but wave.name is null/undefined; update the waveId branch in getHeaderTitle to
return a safe fallback instead of possibly undefined: when waveId is truthy, if
isWaveResolving return <Spinner /> as before, otherwise return wave?.name ?? a
fallback (for example sliceString(capitalizeEveryWord(pageTitle), 20) or another
sensible string); ensure you reference getHeaderTitle, waveId, wave,
isWaveResolving, Spinner, and sliceString/capitalizeEveryWord so the change is
applied in the correct branch.

In `@components/user/waves/UserPageProfileWaveMasonry.tsx`:
- Around line 166-188: The hook useProfileMasonryRenderState() is calling
setState() during render to update resetVersion/lastDropCount when curationId or
dropCount change; move this synchronization out of render into a dedicated
useEffect or switch to a useReducer so state updates are committed instead of
invoked during render, compute shouldResetMasonry from the committed state
(resetVersion/lastDropCount) rather than setting it inline, and update the logic
that currently checks curationId, dropCount, and uses setState(...) to run
inside the effect/reducer (preserve the same updates to resetVersion,
lastDropCount, and curationId).

In `@components/waves/drops/DropMinimalIdentityRow.tsx`:
- Around line 19-26: The click handler handleNavigation currently calls
event.preventDefault() and router.push(path), duplicating Link's navigation;
change it to a simpler handler (e.g., handleClick) that only calls
event.stopPropagation() so the Link's native Next.js navigation/prefetching is
preserved, remove the router.push call and router dependency from the
useCallback dependencies, and update places using handleNavigation to use the
new handler on the Link's onClick.

In `@hooks/useCurationManagementPermission.ts`:
- Around line 5-20: The parameter type for probeDropId in
useCurationManagementPermission is declared as string but treated as possibly
falsy; change the prop signature in useCurationManagementPermission to accept
probeDropId: string | undefined (or null) so Boolean(probeDropId) correctly
reflects the type, update any callers that pass optional values to satisfy the
new union type, and adjust any related types/props that flow into this hook to
maintain type-safety for useDropCurations({ dropId: probeDropId, enabled:
Boolean(probeDropId) }).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: bafd1d0f-8dd9-4d59-a2bd-6a6fa293a78d

📥 Commits

Reviewing files that changed from the base of the PR and between a8e7d80 and 47dd8dc.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (26)
  • components/brain/my-stream/curations/CurationEmptyState.tsx
  • components/brain/my-stream/curations/MyStreamWaveCurationContent.tsx
  • components/drops/view/item/content/media/DropListItemContentMedia.tsx
  • components/drops/view/item/content/media/DropListItemContentMediaImage.tsx
  • components/header/AppHeader.tsx
  • components/user/waves/UserPageProfileWave.tsx
  • components/user/waves/UserPageProfileWaveMasonry.tsx
  • components/utils/button/SecondaryButton.tsx
  • components/waves/drops/Drop.tsx
  • components/waves/drops/DropMinimalIdentityRow.tsx
  • components/waves/drops/WaveDrop.tsx
  • components/waves/drops/WaveDropContent.tsx
  • components/waves/drops/WaveDropPart.tsx
  • components/waves/drops/WaveDropPartContent.tsx
  • components/waves/drops/WaveDropPartContentMedias.tsx
  • components/waves/drops/WaveDropPartDrop.tsx
  • components/waves/drops/drop.types.ts
  • components/waves/drops/participation/DefaultParticipationDrop.tsx
  • components/waves/drops/participation/EndedParticipationDrop.tsx
  • components/waves/drops/participation/OngoingParticipationDrop.tsx
  • components/waves/drops/participation/ParticipationDrop.tsx
  • components/waves/drops/winner/DefaultWinnerDrop.tsx
  • components/waves/drops/winner/WinnerDrop.tsx
  • hooks/useCurationManagementPermission.ts
  • hooks/useProfileCurationViewMode.ts
  • package.json
✅ Files skipped from review due to trivial changes (2)
  • package.json
  • components/waves/drops/drop.types.ts

Comment thread components/drops/view/item/content/media/DropListItemContentMediaImage.tsx Outdated
Comment thread components/waves/drops/WaveDrop.tsx
Comment thread components/waves/drops/WaveDropPartContentMedias.tsx Outdated
Signed-off-by: ragnep <ragneinfo@gmail.com>
Copy link
Copy Markdown

@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: 4

♻️ Duplicate comments (3)
components/waves/drops/WaveDrop.tsx (1)

710-716: ⚠️ Potential issue | 🟡 Minor

Reactions row still reserves the avatar gutter when identity is hidden.

This still hardcodes tw-ml-11 / tw-ml-[3.25rem], so identityMode="hidden" leaves the metadata/reactions row shifted to the right of the content block.

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

In `@components/waves/drops/WaveDrop.tsx` around lines 710 - 716, The reactionsRow
JSX currently always applies the avatar gutter classes ("tw-ml-11" /
"tw-ml-[3.25rem]") even when the drop identity is hidden; update the className
expression in the reactionsRow declaration to conditionally use zero-left-margin
when identityMode === "hidden" (e.g., use "tw-ml-0" or omit the ml classes) and
keep the existing compact branch otherwise, referencing the reactionsRow
variable and the compact and identityMode props to decide between "tw-ml-11" /
"tw-ml-[3.25rem]" and the hidden-case margin class.
components/drops/view/item/content/media/DropListItemContentMediaImage.tsx (1)

96-109: ⚠️ Potential issue | 🟠 Major

Keep the original-source fallback on the naturalHeight path.

This branch still loads only the scaled URL in a raw <img>, so a missing derivative now fails hard even when the original src is still valid. Please preserve the same scaled → original fallback behavior before advancing the retry counter.

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

In `@components/drops/view/item/content/media/DropListItemContentMediaImage.tsx`
around lines 96 - 109, The naturalHeight branch currently renders only the
scaled URL (getScaledImageUri) so a missing derivative causes a hard failure;
restore the original-source fallback by preserving the scaled→original behavior:
render the image so the browser can fall back to the original src (either by
using a <picture> with a <source> for getScaledImageUri(...) and an <img
src={src}> or keep getScaledImageUri(...) as src and update handleError to
detect a scaled failure and swap the img.src to the original src before you
increment errorCount/retryTick). Update handleError/img onError logic
(handleError, imgRef, retryTick, errorCount, maxRetries) to try the original src
first and only advance the retry counter if the original also fails.
components/user/waves/UserPageProfileWave.tsx (1)

296-300: ⚠️ Potential issue | 🟠 Major

Differentiate curation load failures from an empty official wave.

useWaveCurations is still defaulting data to [] and ignoring the error state, so failed requests render the same "No curations yet" UI as a genuinely empty wave. That makes backend/access failures invisible to users and much harder to debug.

💡 Suggested fix
-  const { data: curations = [], isLoading: areCurationsLoading } =
+  const {
+    data: curations,
+    isLoading: areCurationsLoading,
+    isError: areCurationsError,
+  } =
     useWaveCurations({
       waveId: wave?.id ?? "",
       enabled: !!wave?.id,
     });
@@
 function ProfileCurationBody({
   areCurationsLoading,
+  areCurationsError,
   profileCuration,
   profileIdentity,
   viewMode,
   wave,
 }: {
   readonly areCurationsLoading: boolean;
+  readonly areCurationsError: boolean;
   readonly profileCuration: ApiWaveCuration | null;
@@
   if (areCurationsLoading) {
     return (
       <div className="tw-flex tw-min-h-32 tw-items-center tw-justify-center tw-text-sm tw-text-iron-400">
         <Spinner dimension={18} />
         <span className="tw-ml-3">Loading curation...</span>
       </div>
     );
   }
+
+  if (areCurationsError) {
+    return (
+      <div className="tw-p-6 tw-text-sm tw-text-iron-400">
+        Unable to load curations right now.
+      </div>
+    );
+  }
@@
   const profileCuration = useMemo(
-    () => resolveProfileCuration(curations),
+    () => resolveProfileCuration(curations ?? []),
     [curations]
   );
@@
             <ProfileCurationBody
               areCurationsLoading={areCurationsLoading}
+              areCurationsError={areCurationsError}
               profileCuration={profileCuration}
               profileIdentity={profileIdentityForMasonry}
               viewMode={viewMode}
               wave={wave}
             />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/user/waves/UserPageProfileWave.tsx` around lines 296 - 300, The
hook usage currently masks failed requests by defaulting data to []—update the
destructure from useWaveCurations to include isError (and error) instead of
defaulting data: e.g. const { data: curations, isLoading: areCurationsLoading,
isError: areCurationsError, error } = useWaveCurations({ waveId: wave?.id ?? "",
enabled: !!wave?.id }); then change the render logic that checks
curations.length to first check areCurationsLoading, then areCurationsError
(render an error state/message using error), and only treat curations as "no
curations yet" when not loading and not error and curations is empty; do not
coerce data to [] in the destructure so you can distinguish empty vs failed.
🧹 Nitpick comments (2)
components/utils/button/SecondaryButton.tsx (2)

22-23: Follow-up cleanup opportunity in call sites using size="sm" + !tw-text-xs.

Since Line 22 now applies tw-text-xs for sm, callers that still pass className="!tw-text-xs" are likely redundant and can be simplified to reduce utility-class overrides.

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

In `@components/utils/button/SecondaryButton.tsx` around lines 22 - 23, Call sites
that pass size="sm" and also include className="!tw-text-xs" are redundant
because SecondaryButton now sets tw-text-xs for size "sm"; remove the explicit
override from those callers by deleting the "!tw-text-xs" utility in their
className props (or simplify the entire className if it only contained that
override). Search for usages of the SecondaryButton component that pass the size
prop "sm" and inspect className values containing "!tw-text-xs", then update
each call site to omit the override so the built-in sizing in SecondaryButton
controls the text size.

29-35: Optional cleanup: compute a single isInactive flag to avoid condition drift.

disabled || loading is repeated across class branches and behavior checks. Extracting it once makes future style/behavior edits safer.

Refactor suggestion
 export default function SecondaryButton({
   onClicked,
   children,
   size = "default",
   disabled = false,
   className = "",
   loading = false,
 }: SecondaryButtonProps) {
+  const isInactive = disabled || loading;
   const sizeClasses =
     size === "sm"
       ? "tw-px-3 tw-py-2 tw-text-xs"
       : "tw-px-3.5 tw-py-2.5 tw-text-sm";

   return (
     <button
       type="button"
-      disabled={disabled || loading}
+      disabled={isInactive}
       className={`tw-border tw-border-solid ${
-        disabled || loading
+        isInactive
           ? "tw-cursor-not-allowed tw-border-iron-900"
           : "tw-border-iron-800 hover:tw-border-iron-700 hover:tw-bg-iron-700 hover:tw-ring-iron-650"
       } tw-rounded-lg tw-bg-iron-800 tw-ring-1 tw-ring-iron-700 ${sizeClasses} tw-font-semibold ${
-        disabled || loading ? "tw-text-iron-600" : "tw-text-iron-300"
+        isInactive ? "tw-text-iron-600" : "tw-text-iron-300"
       } tw-flex tw-items-center tw-justify-center tw-gap-x-2 tw-shadow-sm tw-transition tw-duration-300 tw-ease-out focus-visible:tw-outline focus-visible:tw-outline-2 focus-visible:tw-outline-offset-2 focus-visible:tw-outline-iron-700 ${className}`}
-      onClick={disabled || loading ? undefined : onClicked}
+      onClick={isInactive ? undefined : onClicked}
     >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/utils/button/SecondaryButton.tsx` around lines 29 - 35, In
SecondaryButton, compute a single boolean (e.g., isInactive = disabled ||
loading) at the top of the component and use that variable everywhere instead of
repeating `disabled || loading`; update the className template (and any other
conditional checks) to reference isInactive so the branches that now use
`disabled || loading` (the border, cursor, text color and hover/ring classes)
are driven by a single flag; ensure you still pass through existing variables
like sizeClasses and className unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@components/user/waves/UserPageProfileWave.tsx`:
- Around line 368-370: In UserPageProfileWave
(components/user/waves/UserPageProfileWave.tsx) don't short-circuit to return
null when profileWaveId is falsy; instead render an explicit empty/unavailable
state (e.g., a new UnavailableWaveState component or reuse an existing
EmptyState component) or perform a redirect to a safe profile tab (e.g., push to
the default profile route) when !profileWaveId && !shouldForceUnavailableState;
update the conditional branch that currently reads "if (!profileWaveId &&
!shouldForceUnavailableState) { return null; }" to either return the
unavailable-state JSX (with messaging about missing official wave and a link to
/curations) or call the router redirect, ensuring accessibility and that the tab
for /curations remains visible and the page body shows a helpful message instead
of disappearing.

In `@components/user/waves/UserPageProfileWaveMasonry.tsx`:
- Around line 167-210: The hook useProfileMasonryRenderState is mutating local
state during render by calling setState when state.curationId or dropCount
differs, which can cause render warnings/loops; move that synchronization into a
useEffect that watches [curationId, dropCount] (and optionally containerWidth if
needed) and performs the setState updates (updating curationId, lastDropCount
and incrementing resetVersion when dropCount decreases) so that setState is only
called after render; keep the returned masonryKey and shouldResetMasonry
computed from stable state/resetVersion (use a ref or derived values) so the
hook no longer calls setState during render and behavior of
useProfileMasonryRenderState remains the same.

In `@components/waves/drops/participation/OngoingParticipationDrop.tsx`:
- Around line 162-166: The ParticipationDropFooter is not receiving the identity
layout state, so when identityMode="hidden" the footer still applies the avatar
gutter classes; update the OngoingParticipationDrop component to pass the
identity/layout state (e.g., identityMode or a boolean like showIdentityAvatar)
into ParticipationDropFooter, and then update ParticipationDropFooter
(references: ParticipationDropFooter component and its classnames at the
locations applying sm:tw-ml-[3.25rem] / tw-ml-[3.25rem]) to conditionally apply
or omit the avatar gutter classes based on that prop.

In `@components/waves/drops/WaveDrop.tsx`:
- Around line 354-359: The className template in the WaveDrop JSX div
concatenates conditionals without a separating space so when both showAuthorInfo
and shouldGroupWithPreviousDrop are true it produces "tw-mt-2tw-pl-[3.25rem]";
update the template for the div (the JSX element in WaveDrop.tsx that references
showAuthorInfo, shouldGroupWithPreviousDrop and isProfileView) to ensure a space
is inserted before the pl class when the second conditional is true (for
example, include a leading space in the second conditional's string or append "
" before it) so class names are always separated.

---

Duplicate comments:
In `@components/drops/view/item/content/media/DropListItemContentMediaImage.tsx`:
- Around line 96-109: The naturalHeight branch currently renders only the scaled
URL (getScaledImageUri) so a missing derivative causes a hard failure; restore
the original-source fallback by preserving the scaled→original behavior: render
the image so the browser can fall back to the original src (either by using a
<picture> with a <source> for getScaledImageUri(...) and an <img src={src}> or
keep getScaledImageUri(...) as src and update handleError to detect a scaled
failure and swap the img.src to the original src before you increment
errorCount/retryTick). Update handleError/img onError logic (handleError,
imgRef, retryTick, errorCount, maxRetries) to try the original src first and
only advance the retry counter if the original also fails.

In `@components/user/waves/UserPageProfileWave.tsx`:
- Around line 296-300: The hook usage currently masks failed requests by
defaulting data to []—update the destructure from useWaveCurations to include
isError (and error) instead of defaulting data: e.g. const { data: curations,
isLoading: areCurationsLoading, isError: areCurationsError, error } =
useWaveCurations({ waveId: wave?.id ?? "", enabled: !!wave?.id }); then change
the render logic that checks curations.length to first check
areCurationsLoading, then areCurationsError (render an error state/message using
error), and only treat curations as "no curations yet" when not loading and not
error and curations is empty; do not coerce data to [] in the destructure so you
can distinguish empty vs failed.

In `@components/waves/drops/WaveDrop.tsx`:
- Around line 710-716: The reactionsRow JSX currently always applies the avatar
gutter classes ("tw-ml-11" / "tw-ml-[3.25rem]") even when the drop identity is
hidden; update the className expression in the reactionsRow declaration to
conditionally use zero-left-margin when identityMode === "hidden" (e.g., use
"tw-ml-0" or omit the ml classes) and keep the existing compact branch
otherwise, referencing the reactionsRow variable and the compact and
identityMode props to decide between "tw-ml-11" / "tw-ml-[3.25rem]" and the
hidden-case margin class.

---

Nitpick comments:
In `@components/utils/button/SecondaryButton.tsx`:
- Around line 22-23: Call sites that pass size="sm" and also include
className="!tw-text-xs" are redundant because SecondaryButton now sets
tw-text-xs for size "sm"; remove the explicit override from those callers by
deleting the "!tw-text-xs" utility in their className props (or simplify the
entire className if it only contained that override). Search for usages of the
SecondaryButton component that pass the size prop "sm" and inspect className
values containing "!tw-text-xs", then update each call site to omit the override
so the built-in sizing in SecondaryButton controls the text size.
- Around line 29-35: In SecondaryButton, compute a single boolean (e.g.,
isInactive = disabled || loading) at the top of the component and use that
variable everywhere instead of repeating `disabled || loading`; update the
className template (and any other conditional checks) to reference isInactive so
the branches that now use `disabled || loading` (the border, cursor, text color
and hover/ring classes) are driven by a single flag; ensure you still pass
through existing variables like sizeClasses and className unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a5c84682-6a29-463e-96f0-a1a8410de1a7

📥 Commits

Reviewing files that changed from the base of the PR and between 47dd8dc and 56e7e63.

📒 Files selected for processing (22)
  • app/[user]/curations/page.tsx
  • app/[user]/waves/page.tsx
  • components/brain/my-stream/curations/MyStreamWaveCurationContent.tsx
  • components/drops/view/item/content/media/DropListItemContentMediaImage.tsx
  • components/memes/drops/MemeParticipationDrop.tsx
  • components/memes/drops/MemeWinnerDrop.tsx
  • components/user/layout/userTabs.config.ts
  • components/user/waves/UserPageProfileWave.tsx
  • components/user/waves/UserPageProfileWaveMasonry.tsx
  • components/utils/button/SecondaryButton.tsx
  • components/waves/drops/Drop.tsx
  • components/waves/drops/WaveDrop.tsx
  • components/waves/drops/WaveDropPartContentMedias.tsx
  • components/waves/drops/participation/DefaultParticipationDrop.tsx
  • components/waves/drops/participation/EndedParticipationDrop.tsx
  • components/waves/drops/participation/OngoingParticipationDrop.tsx
  • components/waves/drops/participation/ParticipationDrop.tsx
  • components/waves/drops/participation/ParticipationDropFooter.tsx
  • components/waves/drops/winner/DefaultWinnerDrop.tsx
  • components/waves/drops/winner/WinnerDrop.tsx
  • components/waves/header/options/profile-wave/WaveProfileWaveAction.tsx
  • hooks/useProfileCurationViewMode.ts
🚧 Files skipped from review as they are similar to previous changes (7)
  • components/waves/drops/participation/ParticipationDrop.tsx
  • components/waves/drops/Drop.tsx
  • components/waves/drops/winner/DefaultWinnerDrop.tsx
  • components/waves/drops/participation/DefaultParticipationDrop.tsx
  • components/waves/header/options/profile-wave/WaveProfileWaveAction.tsx
  • components/waves/drops/winner/WinnerDrop.tsx
  • hooks/useProfileCurationViewMode.ts

Comment thread components/user/waves/UserPageProfileWave.tsx Outdated
Comment thread components/user/waves/UserPageProfileWaveMasonry.tsx Outdated
Comment thread components/waves/drops/participation/OngoingParticipationDrop.tsx
Comment thread components/waves/drops/WaveDrop.tsx
ragnep added 4 commits April 14, 2026 15:21
Signed-off-by: ragnep <ragneinfo@gmail.com>
Signed-off-by: ragnep <ragneinfo@gmail.com>
Signed-off-by: ragnep <ragneinfo@gmail.com>
Copy link
Copy Markdown

@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: 4

♻️ Duplicate comments (2)
components/user/waves/UserPageProfileWave.tsx (1)

474-476: ⚠️ Potential issue | 🟠 Major

Redirect or render a state here instead of returning null.

After clearSelectedProfileWave() succeeds, profileWaveId becomes falsy and this branch blanks the /curations page until the user reloads. The server redirect in app/[user]/curations/page.tsx does not run on that client-side state change.

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

In `@components/user/waves/UserPageProfileWave.tsx` around lines 474 - 476, When
profileWaveId becomes falsy after clearSelectedProfileWave() (and
!shouldForceUnavailableState) don’t return null; instead perform a client-side
redirect to the curations listing or render the unavailable state UI. Locate the
branch in UserPageProfileWave.tsx that checks profileWaveId and
shouldForceUnavailableState and replace the early return with either a
router.replace('/curations') call (using the app-router hook) or render the
existing unavailable/fallback component (e.g., ProfileWaveUnavailable or
similar) so the page doesn’t blank on client-side state changes. Ensure the
chosen approach runs only when profileWaveId is cleared (and not during initial
loading).
components/user/waves/UserPageProfileWaveMasonry.tsx (1)

349-359: ⚠️ Potential issue | 🟠 Major

Handle failed drop loads separately from an empty curation.

This component only distinguishes “still fetching” from drops.length === 0, so an initial useWaveDrops() failure can render CurationEmptyState instead of a retryable error. That masks access/backend problems as “no drops yet.”

Also applies to: 394-404

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

In `@components/user/waves/UserPageProfileWaveMasonry.tsx` around lines 349 - 359,
The component treats an empty drops array during isFetching as the only
"loading" case, which lets a failed useWaveDrops call render CurationEmptyState;
update the rendering logic to check the hook's error state (e.g. isError or
error returned from useWaveDrops) and surface a retry/error UI instead of
CurationEmptyState when loading failed. Locate the useWaveDrops call and the
isInitialLoading calculation (variables: drops, isFetching, isFetchingNextPage,
isInitialLoading, permissionProbeDropId, canManageActiveCuration) and add a
separate isError (or error) branch: if the hook reports an error show a
retryable error component, otherwise use the existing empty-state logic when
drops is empty and not errored.
🧹 Nitpick comments (3)
components/waves/drops/WaveDropPartContentMedias.tsx (1)

48-49: MIME type check is case-sensitive.

The check media.mime_type.includes("image") is case-sensitive. While MIME types are typically lowercase, some servers may return mixed-case values like "Image/PNG". Consider using a case-insensitive check for robustness.

♻️ Proposed fix for case-insensitive MIME type check
         const useNaturalHeightImage =
-          fullWidthMedia && media.mime_type.includes("image");
+          fullWidthMedia && media.mime_type.toLowerCase().includes("image");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/waves/drops/WaveDropPartContentMedias.tsx` around lines 48 - 49,
The MIME type check in the computation of useNaturalHeightImage (currently using
media.mime_type.includes("image")) is case-sensitive; update the check to be
case-insensitive by normalizing media.mime_type (e.g., call toLowerCase())
before calling includes or use a case-insensitive regex, and guard against
null/undefined mime_type to avoid runtime errors; locate the
useNaturalHeightImage variable in WaveDropPartContentMedias.tsx and replace the
includes-based check accordingly.
components/waves/drops/WaveDropPartContentFullWidthImage.tsx (2)

166-166: Tooltip IDs using raw URL could cause invalid selectors.

The data-tooltip-id attributes use the raw src URL (e.g., `reset-zoom-${src}`). If src contains special characters common in URLs (like ?, &, =), these become part of the element ID, which could cause issues with CSS selectors or tooltip matching.

Consider using a sanitized or hashed ID instead.

♻️ Proposed fix using stable ID generation
+  // Generate a stable ID from the src URL
+  const tooltipId = React.useMemo(
+    () => src.replace(/[^a-zA-Z0-9]/g, "-").slice(0, 50),
+    [src]
+  );

   // Then use it in data-tooltip-id attributes:
-  data-tooltip-id={`reset-zoom-${src}`}
+  data-tooltip-id={`reset-zoom-${tooltipId}`}

Also applies to: 181-181, 192-192, 203-203

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

In `@components/waves/drops/WaveDropPartContentFullWidthImage.tsx` at line 166,
The tooltip ID currently concatenates the raw image src (e.g.,
`reset-zoom-${src}`) which can create invalid/unstable IDs when src contains URL
characters; replace these with a sanitized or hashed stable ID generation used
consistently across the component (WaveDropPartContentFullWidthImage) — for
example add a helper (sanitize or hash) that takes `src` and returns a safe
string, then use that helper for every data-tooltip-id occurrence
(`reset-zoom-${...}`, and the other two instances) and ensure the tooltip
library is given the same generated ID so matching still works.

121-123: Escape key handler fires even when modal is closed.

The useKeyPressEvent hook is always active, calling handleCloseModal on every Escape press regardless of modal state. While this is functionally safe (the handler is idempotent), it could interfere with other Escape handlers on the page.

♻️ Proposed optimization to only handle Escape when modal is open
-  useKeyPressEvent("Escape", () => {
-    handleCloseModal();
-  });
+  useKeyPressEvent("Escape", isModalOpen ? handleCloseModal : null);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/waves/drops/WaveDropPartContentFullWidthImage.tsx` around lines
121 - 123, The Escape handler is being registered always via useKeyPressEvent,
so change it to only register or run when the modal is open: either call
useKeyPressEvent conditionally (guard the hook call with the modal open state)
or keep the hook but wrap the callback to early-return unless the modal-open
state (e.g., isOpen / isModalOpen) is true; update the
useKeyPressEvent("Escape", ...) usage and reference handleCloseModal so the
callback only invokes handleCloseModal when the modal is actually open.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/`[user]/waves/page.tsx:
- Around line 65-69: The code uses canonicalUser = profile.handle ??
profile.primary_wallet which becomes empty when a profile lacks both values;
change the assignment to fall back to the already-initialized user slug (e.g.,
use profile.handle ?? profile.primary_wallet ?? user) so canonicalUser is never
empty, then build basePath from that canonicalUser (keep the existing basePath
logic using encodeURIComponent(canonicalUser)); update the canonicalUser
definition in page.tsx (the block that currently sets canonicalUser and
basePath) to use this three-way fallback.

In `@components/user/waves/UserPageProfileWave.tsx`:
- Around line 374-375: The mock-unavailable toggle (shouldForceUnavailableState)
is currently honored in all environments; change it to only apply in
non-production (e.g., wrap the searchParams check with an env/dev-only guard
such as process.env.NODE_ENV !== 'production' or an isDev/isLocal flag) so
?mockProfileWaveUnavailable=1 cannot force the unavailable UI in production;
update the same pattern where it’s used later (the block around lines
referencing the same flag/behavior) so both the declaration of
shouldForceUnavailableState and any conditional rendering that uses it only
trigger in development environments.

In `@components/user/waves/UserPageProfileWaveMasonry.tsx`:
- Around line 170-188: The remove action currently calls
updateMembership(curationId, "remove") but does not refresh the drops list from
useWaveDrops, so removed cards remain visible; fix by triggering a refresh after
the mutation completes—use the React Query client (useQueryClient) to invalidate
or refetch the useWaveDrops query (or call the refetch function returned by
useWaveDrops) in the updateMembership success/then handler so the masonry
reflects the updated curation membership; update the onClick flow around
updateMembership in UserPageProfileWaveMasonry (and the analogous block at
349-353) to perform this invalidation/refetch.

In `@components/waves/drops/WaveDropPartContentFullWidthImage.tsx`:
- Around line 40-51: The component's local state (currentSrc, usedFallback)
isn't reset when the primarySrc prop changes; add a useEffect that watches
primarySrc (and retryTick if present) and calls setCurrentSrc(primarySrc) and
setUsedFallback(false) so the component reloads the new image and can attempt
the fallback anew; update the dependency array to include primarySrc (and
retryTick if the parent uses it) so state stays in sync with prop changes while
leaving handleError, fallbackSrc, and onFinalError logic untouched.

---

Duplicate comments:
In `@components/user/waves/UserPageProfileWave.tsx`:
- Around line 474-476: When profileWaveId becomes falsy after
clearSelectedProfileWave() (and !shouldForceUnavailableState) don’t return null;
instead perform a client-side redirect to the curations listing or render the
unavailable state UI. Locate the branch in UserPageProfileWave.tsx that checks
profileWaveId and shouldForceUnavailableState and replace the early return with
either a router.replace('/curations') call (using the app-router hook) or render
the existing unavailable/fallback component (e.g., ProfileWaveUnavailable or
similar) so the page doesn’t blank on client-side state changes. Ensure the
chosen approach runs only when profileWaveId is cleared (and not during initial
loading).

In `@components/user/waves/UserPageProfileWaveMasonry.tsx`:
- Around line 349-359: The component treats an empty drops array during
isFetching as the only "loading" case, which lets a failed useWaveDrops call
render CurationEmptyState; update the rendering logic to check the hook's error
state (e.g. isError or error returned from useWaveDrops) and surface a
retry/error UI instead of CurationEmptyState when loading failed. Locate the
useWaveDrops call and the isInitialLoading calculation (variables: drops,
isFetching, isFetchingNextPage, isInitialLoading, permissionProbeDropId,
canManageActiveCuration) and add a separate isError (or error) branch: if the
hook reports an error show a retryable error component, otherwise use the
existing empty-state logic when drops is empty and not errored.

---

Nitpick comments:
In `@components/waves/drops/WaveDropPartContentFullWidthImage.tsx`:
- Line 166: The tooltip ID currently concatenates the raw image src (e.g.,
`reset-zoom-${src}`) which can create invalid/unstable IDs when src contains URL
characters; replace these with a sanitized or hashed stable ID generation used
consistently across the component (WaveDropPartContentFullWidthImage) — for
example add a helper (sanitize or hash) that takes `src` and returns a safe
string, then use that helper for every data-tooltip-id occurrence
(`reset-zoom-${...}`, and the other two instances) and ensure the tooltip
library is given the same generated ID so matching still works.
- Around line 121-123: The Escape handler is being registered always via
useKeyPressEvent, so change it to only register or run when the modal is open:
either call useKeyPressEvent conditionally (guard the hook call with the modal
open state) or keep the hook but wrap the callback to early-return unless the
modal-open state (e.g., isOpen / isModalOpen) is true; update the
useKeyPressEvent("Escape", ...) usage and reference handleCloseModal so the
callback only invokes handleCloseModal when the modal is actually open.

In `@components/waves/drops/WaveDropPartContentMedias.tsx`:
- Around line 48-49: The MIME type check in the computation of
useNaturalHeightImage (currently using media.mime_type.includes("image")) is
case-sensitive; update the check to be case-insensitive by normalizing
media.mime_type (e.g., call toLowerCase()) before calling includes or use a
case-insensitive regex, and guard against null/undefined mime_type to avoid
runtime errors; locate the useNaturalHeightImage variable in
WaveDropPartContentMedias.tsx and replace the includes-based check accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4b120423-0c43-4b34-bb38-9e1fd15b720b

📥 Commits

Reviewing files that changed from the base of the PR and between 56e7e63 and 33549e8.

📒 Files selected for processing (8)
  • app/[user]/curations/page.tsx
  • app/[user]/waves/page.tsx
  • components/user/layout/UserPageLayout.tsx
  • components/user/waves/UserPageProfileWave.tsx
  • components/user/waves/UserPageProfileWaveMasonry.tsx
  • components/waves/drops/WaveDropPartContentFullWidthImage.tsx
  • components/waves/drops/WaveDropPartContentMedias.tsx
  • components/waves/header/options/profile-wave/WaveProfileWaveAction.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • components/user/layout/UserPageLayout.tsx

Comment thread app/[user]/waves/page.tsx
Comment thread components/user/waves/UserPageProfileWave.tsx Outdated
Comment thread components/user/waves/UserPageProfileWaveMasonry.tsx
Comment thread components/waves/drops/WaveDropPartContentFullWidthImage.tsx
ragnep added 2 commits April 14, 2026 16:55
Signed-off-by: ragnep <ragneinfo@gmail.com>
Signed-off-by: ragnep <ragneinfo@gmail.com>
@sonarqubecloud
Copy link
Copy Markdown

@ragnep ragnep merged commit daf6959 into main Apr 15, 2026
8 checks passed
@ragnep ragnep deleted the wave-selection branch April 15, 2026 05:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants