Skip to content

curation view#2227

Merged
ragnep merged 17 commits intomainfrom
curation-wave-groups
Apr 10, 2026
Merged

curation view#2227
ragnep merged 17 commits intomainfrom
curation-wave-groups

Conversation

@ragnep
Copy link
Copy Markdown
Contributor

@ragnep ragnep commented Apr 7, 2026

Summary by CodeRabbit

  • New Features

    • Full wave curation experience: create/edit/delete curations, active‑curation header, curated‑content view with infinite scroll, per‑drop curate actions and membership management, and curated tabs.
    • Selectable group cards and improved group selection modal.
    • New profile↔wave endpoints to set/clear profile wave.
  • Improvements

    • Curation selection synced to URL across tabs, leaderboards, drops, and listings.
    • Tab/header UX: tooltips, gallery/chat toggle behavior, mobile header refinements, and better tab scrolling.
  • Chores

    • Workspace build configuration added.

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

coderabbitai Bot commented Apr 7, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds URL-backed wave curations: new curation UI (view/create/edit), tab integration to select/clear curations, API/schema renames from selection/curation-group → curation with curation_id, hooks/queries updated to accept curation filters, and data mappers adding identity_wave/profile_wave_id while removing selections.

Changes

Cohort / File(s) Summary
MyStream tab & integration
components/brain/my-stream/MyStreamWave.tsx, components/brain/my-stream/MyStreamWaveDesktopTabs.tsx, components/brain/my-stream/tabs/MyStreamWaveTabs.tsx, components/brain/my-stream/tabs/MyStreamWaveTabsDefault.tsx, components/brain/my-stream/tabs/MyStreamWaveTabsMeme.tsx
Add activeCurationId/onSelectCuration props; expose dynamic curation tabs (keys curation:{id}); handle selection via onSelectCuration and router.replace (remove/restore curation param); render curation-specific tabpanel when active.
Curation content & lifecycle
components/brain/my-stream/curations/MyStreamWaveCurationContent.tsx, components/brain/my-stream/tabs/MyStreamWaveCurationCreateDialog.tsx, components/brain/my-stream/curations/MyStreamActiveCurationContext.tsx, components/waves/groups/curation/WaveActiveCurationSection.tsx, components/waves/groups/curation/WaveCurationGroupsSection.tsx
Add paginated curation drops viewer, create/edit dialog with authenticated mutations + query cache updates, active-curation context/provider, active-curation admin UI (edit/delete), and simplified curation-groups selector.
Drop-level curation UI & hooks
components/waves/drops/WaveDropCurationsDialog.tsx, components/waves/drops/WaveDropCurationsActionIcon.tsx, components/waves/drops/WaveDropActionsMore.tsx, components/waves/drops/WaveDropMobileMenu.tsx, hooks/drops/useDropCurations.ts, hooks/drops/useDropCurationMembershipMutation.ts
Add drop curations dialog and icon, add “Curate” actions to drop menus (desktop/mobile), hook to fetch drop curation memberships, and mutation hook to add/remove drop membership with optimistic updates/toasts.
Queries / keys / hook changes
hooks/useWaveDrops.ts, hooks/waves/useWaveCurationGroups.ts, components/react-query-wrapper/ReactQueryWrapper.tsx, hooks/useWaveDropsLeaderboard.ts
useWaveDrops gains curationId param and includes it in queryKey/params; useWaveCurationGroups re-exports useWaveCurations; QueryKey member renamed to WAVE_CURATIONS; leaderboard hook uses curation_id param.
API / schema & mappers
openapi.yaml, components/waves/memes/submission/utils/buildPreviewDrop.ts, components/waves/utils/getOptimisticDrop.ts, hooks/useWaveDropsSearch.ts, components/waves/drops/reaction-utils.ts, helpers/ProfileHelpers.ts
OpenAPI and payloads updated: selection_idcuration_id; added ApiDropCuration/requests; add identity_wave and profile_wave_id fields; removed selection-related fields; mapping utilities updated accordingly.
Select-group & modal UI
components/utils/select-group/SelectGroupModalCard.tsx, components/utils/select-group/SelectGroupModalItems.tsx, components/utils/select-group/SelectGroupSearchPanel.tsx, components/utils/select-group/SelectGroupModalSearchName.tsx
Add new selectable group card component, selected-group wiring (selectedGroupId, onGroupClear), configurable modal panel props/styles, and dark-autofill input styling.
Leaderboard & misc UI / infra / deletions
components/brain/my-stream/MyStreamActionTooltip.tsx, components/waves/leaderboard/header/WaveLeaderboardCurationGroupSelect.tsx, components/waves/leaderboard/header/WaveleaderboardHeader.tsx, components/waves/specs/groups/group/edit/buttons/utils/identityGroupWorkflow.ts (deleted), components/brain/my-stream/MyStreamWaveLeaderboard.tsx, pnpm-workspace.yaml, contexts/SeizeSettingsContext.tsx, components/mobile-wrapper-dialog/MobileWrapperDialog.tsx
Adapt leaderboard prop types to ApiWaveCuration, add tooltip helper, remove legacy identity-group workflow file, add pnpm allowBuilds, initialize announcements_wave_id, and minor header/dialog layout tweaks.
Other UI tweaks
components/utils/button/PrimaryButton.tsx, components/utils/input/identity/IdentitySearch.tsx, components/utils/select-group/SelectGroupModal.tsx, components/waves/drops/DropAuthorBadges.tsx
Button loader spacing adjustment, identity-search highlight/navigation refactor, modal layout/class tweaks, and add profile_wave_id passthrough in profile mappers.

Sequence Diagram

sequenceDiagram
    participant User as User (UI)
    participant Tabs as MyStreamWaveDesktopTabs
    participant MyStream as MyStreamWave
    participant Router as Router
    participant Query as ReactQuery
    participant API as API Server

    User->>Tabs: Click curation tab "curation:123"
    Tabs->>MyStream: onSelectCuration("123")
    MyStream->>Router: router.replace(...?curation=123)
    Router-->>MyStream: updated searchParams
    MyStream->>Query: useWaveDrops(waveId, curationId="123")
    Query->>API: GET /waves/{id}/drops?curation_id=123
    API-->>Query: returns drops
    Query-->>MyStream: drops data
    MyStream->>User: render MyStreamWaveCurationContent
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • #1938 — Implements overlapping curation feature surface (URL param, MyStream curation components, hooks/queries renames).
  • #2043 — Modifies MyStream tab/header components and prop shapes used here.
  • #2216 — Changes drop construction helpers and API/schema fields that overlap with these curation/mapper edits.

Suggested reviewers

  • simo6529

"I hopped through tabs and query strings bright,
Curations blooming in moonlit byte,
Old selections folded, new waves lead,
Drops parade where curators seed,
🐇✨ — a rabbit cheers the curation night!"

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

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.
Title check ❓ Inconclusive The title 'curation view' is vague and generic, providing minimal insight into the actual changes which involve restructuring curation management, replacing curation groups with curations API, and adding new UI components for curation selection and management. Revise the title to be more specific about the core change, such as 'Migrate curation management from groups to curations API' or 'Add curation selection and management UI for wave streams'.
✅ 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 unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch curation-wave-groups

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: 6

Caution

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

⚠️ Outside diff range comments (1)
openapi.yaml (1)

6018-6022: ⚠️ Potential issue | 🟡 Minor

Keep curation identifiers typed consistently as UUIDs.

curation_id is described elsewhere in this spec as a UUID, but the path param, request body field, and ApiWaveCuration.id are plain strings here. That makes the contract internally inconsistent and weakens validation/codegen.

🧩 Align the identifier shape
       properties:
         curation_id:
           type: string
+          format: uuid
         - name: curationId
           in: path
           required: true
           schema:
             type: string
+            format: uuid
       properties:
         id:
           type: string
+          format: uuid

Also applies to: 8491-8498, 11477-11479

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

In `@openapi.yaml` around lines 6018 - 6022, The path parameter curationId and
related fields (curation_id in request bodies and ApiWaveCuration.id model) are
currently typed as plain strings; change their schema type/format to a UUID to
match the rest of the spec — update the path parameter named curationId to
schema: { type: string, format: uuid }, update any request body properties named
curation_id to the same UUID schema, and update the ApiWaveCuration.id property
to type: string with format: uuid so all references are consistent (also apply
the same change at the other occurrences noted: the blocks around lines
8491-8498 and 11477-11479).
🧹 Nitpick comments (8)
hooks/useWaveDropsLeaderboard.ts (1)

138-138: Consider finishing terminology migration in local names.

Lines 138 and 181 correctly send curation_id; optionally rename curatedByGroupId internals to curationId to reduce future parameter drift.

Also applies to: 181-181

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

In `@hooks/useWaveDropsLeaderboard.ts` at line 138, The code sends curation_id
externally but still uses the older internal names
curatedByGroupId/normalizedCuratedByGroupId; rename these internals to
curationId and normalizedCurationId (and update any function parameters,
destructured props, local vars, and all references in
hooks/useWaveDropsLeaderboard.ts) so the internal names match the outgoing field
(curation_id) — specifically replace occurrences of curatedByGroupId ->
curationId and normalizedCuratedByGroupId -> normalizedCurationId (including the
spots that supply the curation_id at the places around the previous lines 138
and 181) and run a quick grep to update call sites and tests.
components/waves/utils/getOptimisticDrop.ts (1)

90-90: Consider using nullish coalescing for consistency.

buildPreviewDrop.ts uses connectedProfile?.profile_wave_id ?? null to ensure a consistent null value when the property is undefined. Here, connectedProfile.profile_wave_id could produce undefined if the property is not set, leading to inconsistent drop shapes.

♻️ Suggested fix
-      profile_wave_id: connectedProfile.profile_wave_id,
+      profile_wave_id: connectedProfile.profile_wave_id ?? null,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/waves/utils/getOptimisticDrop.ts` at line 90, The drop object in
getOptimisticDrop.ts sets profile_wave_id directly from connectedProfile
(profile_wave_id: connectedProfile.profile_wave_id) which can be undefined and
cause inconsistent shapes; change it to use optional chaining and nullish
coalescing (connectedProfile?.profile_wave_id ?? null) so the field is
explicitly null when missing—update the assignment inside the function that
builds the optimistic drop (look for profile_wave_id in the object returned by
the function) to use the optional chain and ?? null.
components/brain/my-stream/tabs/MyStreamWaveTabsMeme.tsx (1)

122-143: Initial countdown may briefly show zeros before first calculation.

The timeLeft state initializes to EMPTY_TIME_LEFT (all zeros), but the interval only starts calculating after mount. This could cause a brief flash of 00:00:00:00 before the actual time is computed.

Consider calculating the initial value immediately:

🔧 Suggested fix
-  const [timeLeft, setTimeLeft] = useState<TimeLeft>(EMPTY_TIME_LEFT);
+  const [timeLeft, setTimeLeft] = useState<TimeLeft>(() =>
+    typeof nextDecisionTime === "number"
+      ? calculateTimeLeft(nextDecisionTime)
+      : EMPTY_TIME_LEFT
+  );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/brain/my-stream/tabs/MyStreamWaveTabsMeme.tsx` around lines 122 -
143, The countdown flashes zeros because timeLeft is initialized to
EMPTY_TIME_LEFT and the interval waits one second before the first
calculateTimeLeft; to fix, compute and set the initial value synchronously when
nextDecisionTime is a number (call calculateTimeLeft(nextDecisionTime) and
setTimeLeft(...) before creating the interval) and then start the setInterval
that updates timeLeft; ensure you still clearInterval on unmount and keep the
displayedTimeLeft fallback to EMPTY_TIME_LEFT when nextDecisionTime is not a
number.
components/brain/my-stream/curations/MyStreamWaveCurationContent.tsx (1)

34-40: Async handler doesn't need async/await here.

The handleBottomIntersection function is marked async but the await fetchNextPage() return value isn't used. Since React Query's fetchNextPage returns a promise that's safe to ignore, you can simplify this.

♻️ Optional simplification
-  const handleBottomIntersection = async (isIntersecting: boolean) => {
+  const handleBottomIntersection = (isIntersecting: boolean) => {
     if (!isIntersecting || !hasNextPage || isFetchingNextPage) {
       return;
     }
 
-    await fetchNextPage();
+    void fetchNextPage();
   };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/brain/my-stream/curations/MyStreamWaveCurationContent.tsx` around
lines 34 - 40, The handleBottomIntersection function is unnecessarily marked
async and awaits fetchNextPage even though its promise isn't used; change
handleBottomIntersection to a synchronous function (remove the async keyword)
and call fetchNextPage() without awaiting it, keeping the existing guard logic
that checks isIntersecting, hasNextPage, and isFetchingNextPage so behavior and
early returns remain the same.
components/brain/my-stream/MyStreamWave.tsx (1)

178-184: curationName prop is not passed to MyStreamWaveCurationContent.

The MyStreamWaveCurationContent component accepts an optional curationName prop (used for the empty state title), but it's not provided here. The component will fall back to the generic "Curation" label. Consider fetching and passing the curation name for a better user experience.

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

In `@components/brain/my-stream/MyStreamWave.tsx` around lines 178 - 184, The
MyStreamWaveWave rendering omits the optional curationName prop when rendering
MyStreamWaveCurationContent, so the empty state falls back to a generic label;
locate where activeCurationId is resolved (the MyStreamWave component) and
derive the name for that curation (e.g., find the matching item by id in
wave.curations or call the existing helper that returns a curation title) and
pass it as curationName={derivedName} into the MyStreamWaveCurationContent
alongside wave, curationId={activeCurationId}, and onDropClick so the component
can show the actual curation title.
components/waves/groups/curation/WaveActiveCurationSection.tsx (2)

104-111: Redundant cache operations: optimistic update followed by immediate invalidation.

The onSuccess handler sets the cache optimistically with setQueryData, then immediately calls invalidateQueries which will refetch and potentially overwrite the optimistic update. Either:

  1. Remove the setQueryData and rely only on invalidation, or
  2. Keep setQueryData for instant UI feedback but move invalidateQueries to onSettled to ensure it runs after both success and error cases.
♻️ Suggested approach: keep optimistic update, defer invalidation
     onSuccess: () => {
       if (!activeCuration) {
         return;
       }
 
       queryClient.setQueryData<ApiWaveCuration[]>(
         getWaveCurationsQueryKey(wave.id),
         (current) =>
           current?.filter((item) => item.id !== activeCuration.id) ?? current
       );
-      void queryClient.invalidateQueries({
-        queryKey: getWaveCurationsQueryKey(wave.id),
-      });
       setToast({
         type: "success",
         message: "Curation deleted.",
       });
       setIsDeleteOpen(false);
       removeActiveCurationFromUrl();
     },
+    onSettled: () => {
+      void queryClient.invalidateQueries({
+        queryKey: getWaveCurationsQueryKey(wave.id),
+      });
+    },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/waves/groups/curation/WaveActiveCurationSection.tsx` around lines
104 - 111, The onSuccess currently performs an optimistic update via
queryClient.setQueryData for getWaveCurationsQueryKey(wave.id) then immediately
calls queryClient.invalidateQueries, which makes the optimistic update
redundant; keep the optimistic update (queryClient.setQueryData(..., current =>
current?.filter(item => item.id !== activeCuration.id) ?? current)) for instant
UI feedback and move the queryClient.invalidateQueries call into the mutation's
onSettled handler (or remove it entirely) so refetching happens after success or
error; update the mutation handlers (onSuccess -> remove immediate
invalidateQueries, and add invalidateQueries to onSettled) referencing
getWaveCurationsQueryKey, queryClient.setQueryData,
queryClient.invalidateQueries, and activeCuration.

177-181: Fallback displays raw group ID which may be confusing to users.

When the group is not yet loaded and isFetchingActiveGroup is false, the UI shows the raw activeCuration.group_id (a UUID). Consider showing a more user-friendly placeholder like "Unknown group" or hiding the row until data is available.

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

In `@components/waves/groups/curation/WaveActiveCurationSection.tsx` around lines
177 - 181, Fallback shows raw UUID to users; update the UI to show a friendly
placeholder when group details aren't available. In WaveActiveCurationSection,
change the branch that currently renders activeCuration.group_id when
isFetchingActiveGroup is false so it instead renders a user-friendly string
(e.g., "Unknown group" or an empty/hide state) — locate the span that references
activeCuration.group_id and replace the raw id with a conditional expression
like: if isFetchingActiveGroup show "Loading...", else if
activeCuration.group_id and resolved group name available show name, otherwise
show "Unknown group" (or hide the row). Ensure you only touch the rendering
logic around isFetchingActiveGroup / activeCuration.group_id and preserve
existing CSS classes.
components/brain/my-stream/tabs/MyStreamWaveCurationCreateDialog.tsx (1)

143-156: Consider improving accessibility for the Group selector.

The "Group" label is a plain span without an accessible connection to SelectGroupSearchPanel. For better screen reader support, consider adding an id to the span and passing an aria-labelledby prop to the panel (if supported).

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

In `@components/brain/my-stream/tabs/MyStreamWaveCurationCreateDialog.tsx` around
lines 143 - 156, The "Group" label is a plain span and isn't programmatically
associated with SelectGroupSearchPanel, hurting accessibility; add an id on the
label span (e.g., "group-label") and pass that id into SelectGroupSearchPanel
via an aria-labelledby prop (or ariaLabelledby camel/prop name the component
expects) so screen readers associate the label with the control; update the span
element and the SelectGroupSearchPanel invocation (referencing
SelectGroupSearchPanel and handleGroupSelect/selectedGroup props) to include the
id/aria-labelledby pairing and adjust the component prop name if it uses a
different attribute (ariaLabel or labelledBy).
🤖 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/brain/my-stream/MyStreamWaveDesktopTabs.tsx`:
- Around line 214-223: The current logic may cast a curation option key like
"curation:{id}" to MyStreamWaveTab when computing firstOption.key; update the
block that checks activeCurationId/isMyVotesHidden/isSalesHidden/isFaqHidden and
options so you first select/filter a standard tab option (e.g., filter options
to those whose key matches the MyStreamWaveTab set or does not start with
"curation:") and then call setActiveTab with that key after a safe
type-check/guard instead of directly casting firstOption.key; reference
activeCurationId, options, firstOption.key, setActiveTab and MyStreamWaveTab
when making the change.

In `@components/brain/my-stream/MyStreamWaveLeaderboard.tsx`:
- Line 152: The URL query param key used in MyStreamWaveLeaderboard.tsx is
inconsistent with the parent component: replace any usage of "curation_id"
(e.g., the call reading into rawCuratedByGroupId via
searchParams.get("curation_id") and the other occurrences around lines ~198-200)
with "curation" so it matches the parent MyStreamWave.tsx read/write key; update
the variable/name where needed to reflect the "curation" param and ensure all
get/set operations and comparisons (e.g., rawCuratedByGroupId and any related
parsing/assignment logic) use the unified "curation" key.

In `@components/brain/my-stream/tabs/MyStreamWaveCurationCreateDialog.tsx`:
- Around line 69-74: The update branch currently calls commonApiPost for both
create and update; change the update call to use commonApiPut when isEditMode is
true so the endpoint `waves/${wave.id}/curations/${curation.id}` is issued with
HTTP PUT; locate the code in MyStreamWaveCurationCreateDialog where
commonApiPost is invoked (the return statement using endpoint conditional) and
replace the update path invocation with commonApiPut while keeping the create
branch using commonApiPost and passing the same body.

In `@components/react-query-wrapper/ReactQueryWrapper.tsx`:
- Line 98: The enum removed the key WAVE_CURATION_GROUPS causing tests to fail
and cache-key divergence with the new WAVE_CURATIONS; restore backward
compatibility by adding a deprecated alias entry WAVE_CURATION_GROUPS that maps
to the same value as WAVE_CURATIONS in the enum (keep the canonical name
WAVE_CURATIONS for production), then update the test reference in the failing
test to use WAVE_CURATIONS and remove the alias once tests are migrated. Ensure
you modify the enum containing WAVE_CURATIONS (add WAVE_CURATION_GROUPS =
WAVE_CURATIONS) and then change the test at the referenced test to use
WAVE_CURATIONS.

In `@hooks/waves/useWaveCurationGroups.ts`:
- Around line 8-9: Update the test to reference the new enum value
QueryKey.WAVE_CURATIONS instead of the old QueryKey.WAVE_CURATION_GROUPS so
query invalidation matches the hook; locate usages in tests that
assert/invalidate by query key and replace the enum. Also rename the hook and
related identifiers from useWaveCurationGroups to useWaveCurations (and update
any imports/usages) to reflect that getWaveCurationsQueryKey and the hook return
ApiWaveCuration[] from the waves/${waveId}/curations endpoint, ensuring test
names and mocks align with the new data shape.

In `@openapi.yaml`:
- Around line 1534-1542: The DELETE operation removeDropCuration should not use
a JSON requestBody; remove the requestBody block and add a required query
parameter named curation_id (type: string or the same type used in
ApiDropCurationRequest.curation_id) under parameters for the operation (keep
operationId: removeDropCuration if present); update the OpenAPI schema reference
usage accordingly (remove or repurpose ApiDropCurationRequest) so generated
clients will send curation_id as ?curation_id=... instead of relying on a DELETE
body.

---

Outside diff comments:
In `@openapi.yaml`:
- Around line 6018-6022: The path parameter curationId and related fields
(curation_id in request bodies and ApiWaveCuration.id model) are currently typed
as plain strings; change their schema type/format to a UUID to match the rest of
the spec — update the path parameter named curationId to schema: { type: string,
format: uuid }, update any request body properties named curation_id to the same
UUID schema, and update the ApiWaveCuration.id property to type: string with
format: uuid so all references are consistent (also apply the same change at the
other occurrences noted: the blocks around lines 8491-8498 and 11477-11479).

---

Nitpick comments:
In `@components/brain/my-stream/curations/MyStreamWaveCurationContent.tsx`:
- Around line 34-40: The handleBottomIntersection function is unnecessarily
marked async and awaits fetchNextPage even though its promise isn't used; change
handleBottomIntersection to a synchronous function (remove the async keyword)
and call fetchNextPage() without awaiting it, keeping the existing guard logic
that checks isIntersecting, hasNextPage, and isFetchingNextPage so behavior and
early returns remain the same.

In `@components/brain/my-stream/MyStreamWave.tsx`:
- Around line 178-184: The MyStreamWaveWave rendering omits the optional
curationName prop when rendering MyStreamWaveCurationContent, so the empty state
falls back to a generic label; locate where activeCurationId is resolved (the
MyStreamWave component) and derive the name for that curation (e.g., find the
matching item by id in wave.curations or call the existing helper that returns a
curation title) and pass it as curationName={derivedName} into the
MyStreamWaveCurationContent alongside wave, curationId={activeCurationId}, and
onDropClick so the component can show the actual curation title.

In `@components/brain/my-stream/tabs/MyStreamWaveCurationCreateDialog.tsx`:
- Around line 143-156: The "Group" label is a plain span and isn't
programmatically associated with SelectGroupSearchPanel, hurting accessibility;
add an id on the label span (e.g., "group-label") and pass that id into
SelectGroupSearchPanel via an aria-labelledby prop (or ariaLabelledby camel/prop
name the component expects) so screen readers associate the label with the
control; update the span element and the SelectGroupSearchPanel invocation
(referencing SelectGroupSearchPanel and handleGroupSelect/selectedGroup props)
to include the id/aria-labelledby pairing and adjust the component prop name if
it uses a different attribute (ariaLabel or labelledBy).

In `@components/brain/my-stream/tabs/MyStreamWaveTabsMeme.tsx`:
- Around line 122-143: The countdown flashes zeros because timeLeft is
initialized to EMPTY_TIME_LEFT and the interval waits one second before the
first calculateTimeLeft; to fix, compute and set the initial value synchronously
when nextDecisionTime is a number (call calculateTimeLeft(nextDecisionTime) and
setTimeLeft(...) before creating the interval) and then start the setInterval
that updates timeLeft; ensure you still clearInterval on unmount and keep the
displayedTimeLeft fallback to EMPTY_TIME_LEFT when nextDecisionTime is not a
number.

In `@components/waves/groups/curation/WaveActiveCurationSection.tsx`:
- Around line 104-111: The onSuccess currently performs an optimistic update via
queryClient.setQueryData for getWaveCurationsQueryKey(wave.id) then immediately
calls queryClient.invalidateQueries, which makes the optimistic update
redundant; keep the optimistic update (queryClient.setQueryData(..., current =>
current?.filter(item => item.id !== activeCuration.id) ?? current)) for instant
UI feedback and move the queryClient.invalidateQueries call into the mutation's
onSettled handler (or remove it entirely) so refetching happens after success or
error; update the mutation handlers (onSuccess -> remove immediate
invalidateQueries, and add invalidateQueries to onSettled) referencing
getWaveCurationsQueryKey, queryClient.setQueryData,
queryClient.invalidateQueries, and activeCuration.
- Around line 177-181: Fallback shows raw UUID to users; update the UI to show a
friendly placeholder when group details aren't available. In
WaveActiveCurationSection, change the branch that currently renders
activeCuration.group_id when isFetchingActiveGroup is false so it instead
renders a user-friendly string (e.g., "Unknown group" or an empty/hide state) —
locate the span that references activeCuration.group_id and replace the raw id
with a conditional expression like: if isFetchingActiveGroup show "Loading...",
else if activeCuration.group_id and resolved group name available show name,
otherwise show "Unknown group" (or hide the row). Ensure you only touch the
rendering logic around isFetchingActiveGroup / activeCuration.group_id and
preserve existing CSS classes.

In `@components/waves/utils/getOptimisticDrop.ts`:
- Line 90: The drop object in getOptimisticDrop.ts sets profile_wave_id directly
from connectedProfile (profile_wave_id: connectedProfile.profile_wave_id) which
can be undefined and cause inconsistent shapes; change it to use optional
chaining and nullish coalescing (connectedProfile?.profile_wave_id ?? null) so
the field is explicitly null when missing—update the assignment inside the
function that builds the optimistic drop (look for profile_wave_id in the object
returned by the function) to use the optional chain and ?? null.

In `@hooks/useWaveDropsLeaderboard.ts`:
- Line 138: The code sends curation_id externally but still uses the older
internal names curatedByGroupId/normalizedCuratedByGroupId; rename these
internals to curationId and normalizedCurationId (and update any function
parameters, destructured props, local vars, and all references in
hooks/useWaveDropsLeaderboard.ts) so the internal names match the outgoing field
(curation_id) — specifically replace occurrences of curatedByGroupId ->
curationId and normalizedCuratedByGroupId -> normalizedCurationId (including the
spots that supply the curation_id at the places around the previous lines 138
and 181) and run a quick grep to update call sites and tests.
🪄 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: bd310971-c294-44fb-96b6-5c8d845fbe1d

📥 Commits

Reviewing files that changed from the base of the PR and between c11f6fc and b3da878.

⛔ Files ignored due to path filters (14)
  • generated/models/ApiDrop.ts is excluded by !**/generated/**
  • generated/models/ApiDropCurationRequest.ts is excluded by !**/generated/**
  • generated/models/ApiDropResolvedIdentityProfile.ts is excluded by !**/generated/**
  • generated/models/ApiDropWithoutWave.ts is excluded by !**/generated/**
  • generated/models/ApiIdentity.ts is excluded by !**/generated/**
  • generated/models/ApiProfileMin.ts is excluded by !**/generated/**
  • generated/models/ApiSeizeSettings.ts is excluded by !**/generated/**
  • generated/models/ApiSetProfileWaveRequest.ts is excluded by !**/generated/**
  • generated/models/ApiWave.ts is excluded by !**/generated/**
  • generated/models/ApiWaveCuration.ts is excluded by !**/generated/**
  • generated/models/ApiWaveCurationRequest.ts is excluded by !**/generated/**
  • generated/models/ApiWaveMin.ts is excluded by !**/generated/**
  • generated/models/ApiWaveSelection.ts is excluded by !**/generated/**
  • generated/models/ObjectSerializer.ts is excluded by !**/generated/**
📒 Files selected for processing (28)
  • components/brain/my-stream/MyStreamWave.tsx
  • components/brain/my-stream/MyStreamWaveDesktopTabs.tsx
  • components/brain/my-stream/MyStreamWaveLeaderboard.tsx
  • components/brain/my-stream/curations/MyStreamWaveCurationContent.tsx
  • components/brain/my-stream/tabs/MyStreamWaveCurationCreateDialog.tsx
  • components/brain/my-stream/tabs/MyStreamWaveTabs.tsx
  • components/brain/my-stream/tabs/MyStreamWaveTabsDefault.tsx
  • components/brain/my-stream/tabs/MyStreamWaveTabsMeme.tsx
  • components/react-query-wrapper/ReactQueryWrapper.tsx
  • components/utils/select-group/SelectGroupModalItems.tsx
  • components/utils/select-group/SelectGroupSearchPanel.tsx
  • components/waves/drops/DropAuthorBadges.tsx
  • components/waves/drops/reaction-utils.ts
  • components/waves/groups/WaveGroups.tsx
  • components/waves/groups/curation/WaveActiveCurationSection.tsx
  • components/waves/groups/curation/WaveCurationGroupsSection.tsx
  • components/waves/leaderboard/header/WaveLeaderboardCurationGroupSelect.tsx
  • components/waves/leaderboard/header/WaveleaderboardHeader.tsx
  • components/waves/memes/submission/utils/buildPreviewDrop.ts
  • components/waves/specs/groups/group/edit/buttons/utils/identityGroupWorkflow.ts
  • components/waves/utils/getOptimisticDrop.ts
  • contexts/SeizeSettingsContext.tsx
  • helpers/ProfileHelpers.ts
  • hooks/useWaveDrops.ts
  • hooks/useWaveDropsLeaderboard.ts
  • hooks/useWaveDropsSearch.ts
  • hooks/waves/useWaveCurationGroups.ts
  • openapi.yaml
💤 Files with no reviewable changes (2)
  • components/waves/groups/curation/WaveCurationGroupsSection.tsx
  • components/waves/specs/groups/group/edit/buttons/utils/identityGroupWorkflow.ts

Comment thread components/brain/my-stream/MyStreamWaveDesktopTabs.tsx Outdated
Comment thread components/brain/my-stream/MyStreamWaveLeaderboard.tsx
Comment thread components/react-query-wrapper/ReactQueryWrapper.tsx
Comment thread hooks/waves/useWaveCurationGroups.ts
Comment thread openapi.yaml
ragnep added 2 commits April 8, 2026 08:30
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.

♻️ Duplicate comments (1)
components/brain/my-stream/MyStreamWaveDesktopTabs.tsx (1)

216-223: ⚠️ Potential issue | 🟠 Major

Fallback tab selection still uses an unsafe enum cast.

This still picks from options[0] (which may be a curation:* key) and casts it to MyStreamWaveTab. Use standardOptions[0] (or an explicit enum guard) before calling setActiveTab.

Suggested fix
     if (
       !activeCurationId &&
       (isMyVotesHidden || isSalesHidden || isFaqHidden) &&
-      options.length > 0
+      standardOptions.length > 0
     ) {
-      const firstOption = options[0];
+      const firstOption = standardOptions[0];
       if (firstOption) {
-        setActiveTab(firstOption.key as MyStreamWaveTab);
+        setActiveTab(firstOption.key as MyStreamWaveTab);
       }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/brain/my-stream/MyStreamWaveDesktopTabs.tsx` around lines 216 -
223, The fallback tab selection uses options[0] and casts it unsafely to
MyStreamWaveTab; change the logic to pick from standardOptions[0] (or validate
the key with an enum guard) before calling setActiveTab so you never pass a
curation:* key: inside the block that checks !activeCurationId &&
(isMyVotesHidden || isSalesHidden || isFaqHidden) && options.length > 0, replace
the use of options[0] with standardOptions[0] (or run a predicate like
isMyStreamWaveTab(key) that ensures the key is a valid MyStreamWaveTab) and only
then call setActiveTab(firstStandardKey as MyStreamWaveTab).
🧹 Nitpick comments (1)
components/waves/groups/curation/WaveCurationGroupsSection.tsx (1)

34-35: Normalize unknown curation query values to avoid a “no active option” state.

If the URL has a stale/invalid curation id, neither “Chat” nor any curation button is active. Treat unknown ids as null for UI state (and optionally clean the URL) so selection stays consistent.

Proposed change
-  const activeCurationId = searchParams.get("curation");
+  const requestedCurationId = searchParams.get("curation");
+  const activeCurationId = curations.some(
+    (curation) => curation.id === requestedCurationId
+  )
+    ? requestedCurationId
+    : null;

Also applies to: 85-93

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

In `@components/waves/groups/curation/WaveCurationGroupsSection.tsx` around lines
34 - 35, The UI reads activeCurationId from searchParams.get("curation") and can
end up with an unknown/stale id which causes no button to be active; change the
logic that computes activeCurationId to validate the value against the current
list of curation ids (e.g., the collection used to render buttons) and normalize
any value not present to null (and optionally remove/replace the query param via
the same searchParams setter). Update both occurrences where activeCurationId is
derived (the initial const activeCurationId = searchParams.get("curation") and
the similar logic around lines 85-93) so components rendering “Chat” and
curation buttons use the normalized value to determine active state.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@components/brain/my-stream/MyStreamWaveDesktopTabs.tsx`:
- Around line 216-223: The fallback tab selection uses options[0] and casts it
unsafely to MyStreamWaveTab; change the logic to pick from standardOptions[0]
(or validate the key with an enum guard) before calling setActiveTab so you
never pass a curation:* key: inside the block that checks !activeCurationId &&
(isMyVotesHidden || isSalesHidden || isFaqHidden) && options.length > 0, replace
the use of options[0] with standardOptions[0] (or run a predicate like
isMyStreamWaveTab(key) that ensures the key is a valid MyStreamWaveTab) and only
then call setActiveTab(firstStandardKey as MyStreamWaveTab).

---

Nitpick comments:
In `@components/waves/groups/curation/WaveCurationGroupsSection.tsx`:
- Around line 34-35: The UI reads activeCurationId from
searchParams.get("curation") and can end up with an unknown/stale id which
causes no button to be active; change the logic that computes activeCurationId
to validate the value against the current list of curation ids (e.g., the
collection used to render buttons) and normalize any value not present to null
(and optionally remove/replace the query param via the same searchParams
setter). Update both occurrences where activeCurationId is derived (the initial
const activeCurationId = searchParams.get("curation") and the similar logic
around lines 85-93) so components rendering “Chat” and curation buttons use the
normalized value to determine active state.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5db45b88-7dac-4d25-b668-17dc7d2a3e5c

📥 Commits

Reviewing files that changed from the base of the PR and between b3da878 and 4a5dba6.

📒 Files selected for processing (4)
  • components/brain/my-stream/MyStreamWave.tsx
  • components/brain/my-stream/MyStreamWaveDesktopTabs.tsx
  • components/waves/groups/WaveGroups.tsx
  • components/waves/groups/curation/WaveCurationGroupsSection.tsx
✅ Files skipped from review due to trivial changes (1)
  • components/waves/groups/WaveGroups.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • components/brain/my-stream/MyStreamWave.tsx

ragnep added 2 commits April 8, 2026 13:12
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: 2

Caution

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

⚠️ Outside diff range comments (1)
contexts/SeizeSettingsContext.tsx (1)

51-60: ⚠️ Potential issue | 🔴 Critical

Remove duplicate announcements_wave_id property.

The announcements_wave_id property is declared twice in the initial state object (lines 56 and 59). In JavaScript, duplicate object keys cause the second declaration to overwrite the first, making line 56 ineffective.

🐛 Proposed fix to remove the duplicate property
   const [seizeSettings, setSeizeSettings] = useState<TempApiSeizeSettings>({
     rememes_submission_tdh_threshold: 0,
     all_drops_notifications_subscribers_limit: 0,
     memes_wave_id: null,
     curation_wave_id: null,
     announcements_wave_id: null,
     distribution_admin_wallets: [],
     claims_admin_wallets: [],
-    announcements_wave_id: null,
   });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contexts/SeizeSettingsContext.tsx` around lines 51 - 60, The initial state
object for seizeSettings in useState<TempApiSeizeSettings> contains a duplicate
key announcements_wave_id; remove the redundant property so the object only
declares announcements_wave_id once (update the object literal used when calling
useState in contexts/SeizeSettingsContext.tsx, e.g., the seizeSettings /
setSeizeSettings initial value) to prevent accidental overwrites and ensure the
intended default is preserved.
♻️ Duplicate comments (2)
components/brain/my-stream/tabs/MyStreamWaveCurationCreateDialog.tsx (1)

140-145: ⚠️ Potential issue | 🟠 Major

Use commonApiPut for updates instead of POST.

At Line 140, edit mode still sends POST to waves/${wave.id}/curations/${curation.id} (Line 142). Keep POST for create and use PUT for update.

Suggested fix
-import { commonApiPost } from "@/services/api/common-api";
+import { commonApiPost, commonApiPut } from "@/services/api/common-api";
...
-      return await commonApiPost<ApiWaveCurationRequest, ApiWaveCuration>({
-        endpoint: isEditMode
-          ? `waves/${wave.id}/curations/${curation.id}`
-          : `waves/${wave.id}/curations`,
-        body,
-      });
+      if (isEditMode) {
+        return await commonApiPut<ApiWaveCurationRequest, ApiWaveCuration>({
+          endpoint: `waves/${wave.id}/curations/${curation.id}`,
+          body,
+        });
+      }
+      return await commonApiPost<ApiWaveCurationRequest, ApiWaveCuration>({
+        endpoint: `waves/${wave.id}/curations`,
+        body,
+      });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/brain/my-stream/tabs/MyStreamWaveCurationCreateDialog.tsx` around
lines 140 - 145, The update branch is incorrectly using commonApiPost when
isEditMode is true; change the call so create (isEditMode false) continues to
call commonApiPost with endpoint `waves/${wave.id}/curations` and update
(isEditMode true) calls commonApiPut with endpoint
`waves/${wave.id}/curations/${curation.id}` while passing the same body; locate
this in MyStreamWaveCurationCreateDialog where commonApiPost is used and switch
to conditional invocation of commonApiPut for edits.
components/brain/my-stream/MyStreamWaveDesktopTabs.tsx (1)

217-224: ⚠️ Potential issue | 🟡 Minor

Use standardOptions for fallback tab selection.

At Line 221, options[0] can be a curation key and is cast at Line 223 to MyStreamWaveTab. Pick from standardOptions instead.

Suggested fix
     if (
       !activeCurationId &&
       (isMyVotesHidden || isSalesHidden || isFaqHidden) &&
-      options.length > 0
+      standardOptions.length > 0
     ) {
-      const firstOption = options[0];
+      const firstOption = standardOptions[0];
       if (firstOption) {
         setActiveTab(firstOption.key as MyStreamWaveTab);
       }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/brain/my-stream/MyStreamWaveDesktopTabs.tsx` around lines 217 -
224, The fallback tab selection currently uses options[0] and casts it to
MyStreamWaveTab which can be a curation key; change the fallback to use
standardOptions instead: when !activeCurationId && (isMyVotesHidden ||
isSalesHidden || isFaqHidden) && options.length > 0, pick const fallback =
standardOptions[0] (ensure it exists) and call setActiveTab(fallback.key as
MyStreamWaveTab) rather than using firstOption from options; update any
null/undefined guards accordingly so setActiveTab only runs with a valid
standardOptions[0].
🧹 Nitpick comments (2)
components/utils/select-group/SelectGroupModalCard.tsx (2)

11-11: Minor: Redundant type annotation.

The | undefined is redundant when combined with the optional ? modifier. Both achieve the same result.

♻️ Simplified type
-  readonly onClear?: (() => void) | undefined;
+  readonly onClear?: () => void;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/utils/select-group/SelectGroupModalCard.tsx` at line 11, The
onClear prop declaration redundantly uses "| undefined" together with the
optional modifier; update the SelectGroupModalCard prop/interface by removing
the " | undefined" from the readonly onClear?: declaration so it reads simply as
an optional function type (readonly onClear?: () => void) and leave other
optional props unchanged; locate the onClear symbol in SelectGroupModalCard.tsx
to apply the change.

29-45: Consider using a semantic <button> element instead of role="button" on a <div>.

SonarCloud flagged this as an accessibility concern. While your implementation includes proper ARIA attributes (aria-pressed, aria-label) and keyboard handling (Enter/Space), using a native <button> element provides better accessibility guarantees across assistive technologies and devices.

If the complex nested content makes <button> styling difficult, the current implementation is acceptable since all necessary accessibility features are present. However, a <button> with tw-appearance-none and reset styles would be the preferred approach.

♻️ Suggested refactor using semantic button
-      <div
-        role="button"
-        tabIndex={0}
+      <button
+        type="button"
         aria-pressed={isSelected}
         aria-label={`Select ${group.name}`}
         onClick={() => onSelect(group)}
-        onKeyDown={(event) => {
-          if (event.key === "Enter" || event.key === " ") {
-            event.preventDefault();
-            onSelect(group);
-          }
-        }}
-        className={`tw-group tw-relative tw-cursor-pointer tw-overflow-hidden tw-rounded-xl tw-border tw-border-solid tw-bg-iron-950 tw-no-underline tw-shadow-sm tw-shadow-black/20 tw-backdrop-blur-sm tw-transition-all tw-duration-300 tw-ease-out desktop-hover:hover:tw-bg-iron-900 desktop-hover:hover:tw-shadow-md desktop-hover:hover:tw-shadow-black/25 ${
+        className={`tw-group tw-relative tw-w-full tw-cursor-pointer tw-overflow-hidden tw-rounded-xl tw-border tw-border-solid tw-bg-iron-950 tw-p-0 tw-text-left tw-no-underline tw-shadow-sm tw-shadow-black/20 tw-backdrop-blur-sm tw-transition-all tw-duration-300 tw-ease-out desktop-hover:hover:tw-bg-iron-900 desktop-hover:hover:tw-shadow-md desktop-hover:hover:tw-shadow-black/25 ${
           isSelected
             ? "tw-shadow-primary-900/20 tw-border-primary-300 tw-shadow-md tw-ring-1 tw-ring-primary-400/25"
             : "tw-border-white/10"
         } focus-visible:tw-outline focus-visible:tw-outline-2 focus-visible:tw-outline-offset-2 focus-visible:tw-outline-primary-500`}
       >
         ...
-      </div>
+      </button>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/utils/select-group/SelectGroupModalCard.tsx` around lines 29 - 45,
Replace the non-semantic clickable <div> in the SelectGroupModalCard component
with a native <button type="button"> to address accessibility concerns: remove
role="button" and tabIndex, keep aria-pressed and aria-label, preserve the
onClick={onSelect(group)} handler and conditional className logic that uses
isSelected, and drop the manual onKeyDown Enter/Space handling since the button
natively supports keyboard activation; add reset styles (e.g.,
tw-appearance-none and any existing focus/outline classes) to the button so
styling and behavior remain identical.
🤖 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/brain/my-stream/MyStreamWaveDesktopTabs.tsx`:
- Line 208: The file MyStreamWaveDesktopTabs.tsx contains two eslint-disable
comments for the unregistered rule
react-you-might-not-need-an-effect/no-pass-data-to-parent; remove those
suppression comments in the MyStreamWaveDesktopTabs component (or replace them
with a suppression for a rule that exists in eslint.config.mjs) so the file no
longer references an unconfigured plugin, or alternatively add the plugin and
rule to eslint.config.mjs so the suppression becomes valid; locate occurrences
by searching for "react-you-might-not-need-an-effect/no-pass-data-to-parent" in
MyStreamWaveDesktopTabs and update accordingly.

In `@components/brain/my-stream/tabs/MyStreamWaveCurationCreateDialog.tsx`:
- Around line 205-208: The standalone <label> in the
MyStreamWaveCurationCreateDialog component is used without an associated form
control; replace it with proper grouped form semantics by wrapping the preset
button group in a <fieldset> and move the "Name" text into a <legend> (or render
plain text styled like the current label) so the heading correctly groups the
related controls; update the surrounding markup where the current label and the
div with className "tw-flex tw-flex-wrap tw-gap-2.5" are defined to use
fieldset/legend and keep existing classes for styling.

---

Outside diff comments:
In `@contexts/SeizeSettingsContext.tsx`:
- Around line 51-60: The initial state object for seizeSettings in
useState<TempApiSeizeSettings> contains a duplicate key announcements_wave_id;
remove the redundant property so the object only declares announcements_wave_id
once (update the object literal used when calling useState in
contexts/SeizeSettingsContext.tsx, e.g., the seizeSettings / setSeizeSettings
initial value) to prevent accidental overwrites and ensure the intended default
is preserved.

---

Duplicate comments:
In `@components/brain/my-stream/MyStreamWaveDesktopTabs.tsx`:
- Around line 217-224: The fallback tab selection currently uses options[0] and
casts it to MyStreamWaveTab which can be a curation key; change the fallback to
use standardOptions instead: when !activeCurationId && (isMyVotesHidden ||
isSalesHidden || isFaqHidden) && options.length > 0, pick const fallback =
standardOptions[0] (ensure it exists) and call setActiveTab(fallback.key as
MyStreamWaveTab) rather than using firstOption from options; update any
null/undefined guards accordingly so setActiveTab only runs with a valid
standardOptions[0].

In `@components/brain/my-stream/tabs/MyStreamWaveCurationCreateDialog.tsx`:
- Around line 140-145: The update branch is incorrectly using commonApiPost when
isEditMode is true; change the call so create (isEditMode false) continues to
call commonApiPost with endpoint `waves/${wave.id}/curations` and update
(isEditMode true) calls commonApiPut with endpoint
`waves/${wave.id}/curations/${curation.id}` while passing the same body; locate
this in MyStreamWaveCurationCreateDialog where commonApiPost is used and switch
to conditional invocation of commonApiPut for edits.

---

Nitpick comments:
In `@components/utils/select-group/SelectGroupModalCard.tsx`:
- Line 11: The onClear prop declaration redundantly uses "| undefined" together
with the optional modifier; update the SelectGroupModalCard prop/interface by
removing the " | undefined" from the readonly onClear?: declaration so it reads
simply as an optional function type (readonly onClear?: () => void) and leave
other optional props unchanged; locate the onClear symbol in
SelectGroupModalCard.tsx to apply the change.
- Around line 29-45: Replace the non-semantic clickable <div> in the
SelectGroupModalCard component with a native <button type="button"> to address
accessibility concerns: remove role="button" and tabIndex, keep aria-pressed and
aria-label, preserve the onClick={onSelect(group)} handler and conditional
className logic that uses isSelected, and drop the manual onKeyDown Enter/Space
handling since the button natively supports keyboard activation; add reset
styles (e.g., tw-appearance-none and any existing focus/outline classes) to the
button so styling and behavior remain identical.
🪄 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: aa9afb00-31f6-4a56-8fff-290f49c2a82a

📥 Commits

Reviewing files that changed from the base of the PR and between 4a5dba6 and 0160686.

📒 Files selected for processing (12)
  • components/brain/my-stream/MyStreamActionTooltip.tsx
  • components/brain/my-stream/MyStreamWaveDesktopTabs.tsx
  • components/brain/my-stream/tabs/MyStreamWaveCurationCreateDialog.tsx
  • components/brain/my-stream/tabs/MyStreamWaveTabsDefault.tsx
  • components/brain/my-stream/tabs/MyStreamWaveTabsMeme.tsx
  • components/mobile-wrapper-dialog/MobileWrapperDialog.tsx
  • components/utils/select-group/SelectGroupModalCard.tsx
  • components/utils/select-group/SelectGroupModalItems.tsx
  • components/utils/select-group/SelectGroupModalSearchName.tsx
  • components/utils/select-group/SelectGroupSearchPanel.tsx
  • contexts/SeizeSettingsContext.tsx
  • pnpm-workspace.yaml
✅ Files skipped from review due to trivial changes (1)
  • pnpm-workspace.yaml
🚧 Files skipped from review as they are similar to previous changes (2)
  • components/utils/select-group/SelectGroupModalItems.tsx
  • components/utils/select-group/SelectGroupSearchPanel.tsx

Comment thread components/brain/my-stream/MyStreamWaveDesktopTabs.tsx Outdated
Comment thread components/brain/my-stream/tabs/MyStreamWaveCurationCreateDialog.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: 2

Caution

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

⚠️ Outside diff range comments (1)
openapi.yaml (1)

6018-6022: ⚠️ Potential issue | 🟡 Minor

Keep the curation identifier typed consistently.

The new filters document curation IDs as UUIDs, but ApiWaveCuration.id, {curationId}, and ApiDropCurationRequest.curation_id are still plain strings. That makes the same identifier inconsistent across read/write endpoints and weakens generated-client validation.

Suggested schema alignment
   /waves/{id}/curations/{curationId}:
     post:
       parameters:
         - name: curationId
           in: path
           required: true
           schema:
             type: string
+            format: uuid
     ApiDropCurationRequest:
       type: object
       required:
         - curation_id
       properties:
         curation_id:
           type: string
+          format: uuid
     ApiWaveCuration:
       type: object
       required:
         - id
         - name
         - wave_id
         - group_id
         - created_at
         - updated_at
       properties:
         id:
           type: string
+          format: uuid

Also applies to: 8503-8510, 11489-11514

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

In `@openapi.yaml` around lines 6018 - 6022, The curation identifier is
inconsistently typed; update the OpenAPI definitions so the curation ID is a
UUID everywhere: change the path parameter schema for {curationId} to schema:
type: string with format: uuid (or an equivalent UUID pattern), and update the
component schemas ApiWaveCuration.id and ApiDropCurationRequest.curation_id to
the same type+format so read/write endpoints match; apply the same change to the
other occurrences mentioned (lines referenced in the comment).
♻️ Duplicate comments (4)
openapi.yaml (1)

1523-1539: ⚠️ Potential issue | 🟠 Major

Model removeDropCuration without a DELETE body.

DELETE request bodies are not portable in OpenAPI 3.0.3, so generated clients and intermediaries may ignore ApiDropCurationRequest entirely and drop curation_id. Put the identifier in the path or query instead.

Suggested contract change
   /drops/{dropId}/curations:
     delete:
       tags:
         - Drops
       summary: Remove a drop from a curation
       operationId: removeDropCuration
       parameters:
         - name: dropId
           in: path
           required: true
           schema:
             type: string
-      requestBody:
-        required: true
-        content:
-          application/json:
-            schema:
-              $ref: "#/components/schemas/ApiDropCurationRequest"
+        - name: curation_id
+          in: query
+          required: true
+          schema:
+            type: string
+            format: uuid
       responses:
         "200":
           description: successful operation
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@openapi.yaml` around lines 1523 - 1539, The DELETE operation with operationId
removeDropCuration currently requires a request body referencing
ApiDropCurationRequest which is non-portable in OpenAPI 3.0.3; change the
contract to remove the requestBody and instead add a required path or query
parameter (e.g., curation_id) to the delete operation (alongside existing dropId
parameter) and update any references to ApiDropCurationRequest accordingly so
servers and generated clients receive curation_id via the path/query; ensure
operationId removeDropCuration and the Drops tag remain unchanged.
components/brain/my-stream/tabs/MyStreamWaveCurationCreateDialog.tsx (3)

115-120: ⚠️ Potential issue | 🟠 Major

Use commonApiPut for edit-mode updates.

Line 115-Line 120 still sends edit requests with POST; update operations should use PUT on waves/${wave.id}/curations/${curation.id}.

Suggested fix
-import { commonApiPost } from "@/services/api/common-api";
+import { commonApiPost, commonApiPut } from "@/services/api/common-api";
...
-      return await commonApiPost<ApiWaveCurationRequest, ApiWaveCuration>({
-        endpoint: isEditMode
-          ? `waves/${wave.id}/curations/${curation.id}`
-          : `waves/${wave.id}/curations`,
-        body,
-      });
+      if (isEditMode && curation) {
+        return await commonApiPut<ApiWaveCurationRequest, ApiWaveCuration>({
+          endpoint: `waves/${wave.id}/curations/${curation.id}`,
+          body,
+        });
+      }
+
+      return await commonApiPost<ApiWaveCurationRequest, ApiWaveCuration>({
+        endpoint: `waves/${wave.id}/curations`,
+        body,
+      });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/brain/my-stream/tabs/MyStreamWaveCurationCreateDialog.tsx` around
lines 115 - 120, The code always calls commonApiPost in submit logic (see
commonApiPost, isEditMode, wave.id and curation.id) so edit-mode requests are
sent as POST; change the branch so when isEditMode is true you call commonApiPut
with endpoint `waves/${wave.id}/curations/${curation.id}` and the same body, and
keep commonApiPost only for the create branch `waves/${wave.id}/curations`.

181-205: ⚠️ Potential issue | 🟡 Minor

Replace standalone <label> with grouped form semantics.

Line 181 uses a <label> not explicitly bound to a single control; for this preset group + custom input, use <fieldset>/<legend> (or plain text) to keep valid semantics.

Suggested fix
-            <div className="tw-space-y-4">
-              <label className="tw-block tw-text-sm tw-font-medium tw-text-iron-300">
-                Name
-              </label>
+            <fieldset className="tw-space-y-4">
+              <legend className="tw-block tw-text-sm tw-font-medium tw-text-iron-300">
+                Name
+              </legend>
               <div className="tw-flex tw-flex-wrap tw-gap-2.5">
                 {CURATION_NAME_PRESETS.map((preset) => {
                   const isSelected = selectedPresetLabel === preset;
...
               <div className="tw-relative tw-w-full sm:tw-max-w-[260px]">
                 <input
...
                 />
               </div>
-            </div>
+            </fieldset>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/brain/my-stream/tabs/MyStreamWaveCurationCreateDialog.tsx` around
lines 181 - 205, The <label> used for the preset buttons in
MyStreamWaveCurationCreateDialog.tsx is not bound to a single form control;
change it to proper grouped semantics by replacing the standalone <label> with a
<fieldset> containing a <legend> (e.g., "Name") and move the button list inside
that fieldset so CURATION_NAME_PRESETS rendering, selectedPresetLabel state, and
handlePresetSelect remain unchanged; keep buttons' aria-pressed attributes for
accessibility and ensure any adjacent custom input is also inside the same
fieldset so the group is semantically associated.

62-70: ⚠️ Potential issue | 🟠 Major

Reset form state when dialog context changes.

Line 67-Line 70 only initializes from props once. If this dialog remains mounted, reopening for another curation/group can keep stale name/selectedGroup and submit incorrect payload.

Suggested fix
-import { useRef, useState, type ChangeEvent } from "react";
+import { useEffect, useRef, useState, type ChangeEvent } from "react";
...
   const [name, setName] = useState(() => initialName);
   const [selectedGroup, setSelectedGroup] = useState<ApiGroupFull | null>(
     () => initialGroup ?? null
   );
+  useEffect(() => {
+    if (!isOpen) {
+      return;
+    }
+    setName(curation?.name ?? "");
+    setSelectedGroup(initialGroup ?? null);
+  }, [isOpen, curation?.id, curation?.name, initialGroup?.id]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/brain/my-stream/tabs/MyStreamWaveCurationCreateDialog.tsx` around
lines 62 - 70, The component MyStreamWaveCurationCreateDialog initializes name
and selectedGroup only once which causes stale values when the dialog is
reopened for a different curation/group; add an effect that watches curation (or
initialName/initialGroup) and the dialog open state and calls
setName(initialName) and setSelectedGroup(initialGroup ?? null) to reset state
on context changes—use the existing identifiers initialName, initialGroup, name,
setName, selectedGroup, setSelectedGroup and ensure the effect dependencies
include curation/initialName, initialGroup, and whatever prop signals the dialog
is open (e.g., isEditMode or isOpen).
🧹 Nitpick comments (5)
components/utils/select-group/SelectGroupModalSearchName.tsx (1)

1-2: Consider de-duplicating autofill utility fragments.

DARK_AUTOFILL_CLASSES is correct, but the repeated default/hover/focus blocks are hard to maintain. Consider splitting shared tokens to reduce drift risk in future tweaks.

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

In `@components/utils/select-group/SelectGroupModalSearchName.tsx` around lines 1
- 2, DARK_AUTOFILL_CLASSES repeats nearly identical blocks for
default/hover/focus; extract the common tokens into smaller constants (e.g.,
AUTOFILL_BASE and AUTOFILL_STATE or AUTOFILL_PSEUDO) and compose
DARK_AUTOFILL_CLASSES from those to avoid duplication and drift. Update the
constant definitions in SelectGroupModalSearchName.tsx so that shared fragments
(like -webkit-text-fill-color, caret-color, and tw-shadow inset theme tokens)
live in a single reusable symbol and the hover/focus variants reuse that symbol
when building the final DARK_AUTOFILL_CLASSES string.
components/utils/input/identity/IdentitySearch.tsx (1)

150-153: Consider wrapping in useCallback for stable reference.

The closeDropdown function is recreated on every render and passed to useClickAway and useKeyPressEvent. While these hooks may handle this internally, wrapping in useCallback ensures a stable reference and prevents potential re-subscriptions.

♻️ Suggested refactor
-  const closeDropdown = () => {
-    setIsOpen(false);
-    setManualHighlightedIndex(null);
-  };
+  const closeDropdown = useCallback(() => {
+    setIsOpen(false);
+    setManualHighlightedIndex(null);
+  }, []);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/utils/input/identity/IdentitySearch.tsx` around lines 150 - 153,
closeDropdown is recreated each render and passed into useClickAway and
useKeyPressEvent; wrap it with React's useCallback to provide a stable
reference. Replace the inline const closeDropdown = () => { setIsOpen(false);
setManualHighlightedIndex(null); } with a useCallback that depends only on
setIsOpen and setManualHighlightedIndex so the function identity remains stable
when used with useClickAway and useKeyPressEvent.
components/utils/select-group/SelectGroupModalItems.tsx (1)

29-39: Scope clear action to the selected card and preserve list semantics.

Consider limiting onClear to the selected item and using ul/li to keep collection semantics for assistive tech.

Proposed refactor
-    <div className="tw-grid tw-grid-cols-1 tw-gap-2">
+    <ul className="tw-grid tw-grid-cols-1 tw-gap-2" role="list">
       {groups.map((group) => (
-        <SelectGroupModalCard
-          key={group.id}
-          group={group}
-          isSelected={selectedGroupId === group.id}
-          onSelect={onGroupSelect}
-          onClear={onGroupClear}
-        />
+        <li key={group.id}>
+          <SelectGroupModalCard
+            group={group}
+            isSelected={selectedGroupId === group.id}
+            onSelect={onGroupSelect}
+            onClear={selectedGroupId === group.id ? onGroupClear : undefined}
+          />
+        </li>
       ))}
-    </div>
+    </ul>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/utils/select-group/SelectGroupModalItems.tsx` around lines 29 -
39, The current rendering of groups uses a div grid and passes the same onClear
to every SelectGroupModalCard; change the container to a semantic list (use ul
with li per item) and only pass the onClear handler to the card when that card
is the selected one (compare selectedGroupId === group.id), so non-selected
cards do not receive/trigger clear; update SelectGroupModalItems to render each
group inside an li and conditionally provide onClear to SelectGroupModalCard
(keep onSelect always passed) to scope clear actions to the selected item.
components/waves/drops/WaveDropCurationsDialog.tsx (1)

337-349: Optional: Consider adding a section title for read-only curations.

The read-only curations are rendered without a section title, unlike "Included" and "Available to curate" sections. For consistency and clarity, consider wrapping them in CurationsSection with a title like "Other curations" or "View only".

♻️ Optional improvement
                 {readOnlyCurations.length > 0 && (
-                  <div className="tw-flex tw-flex-col tw-gap-2">
+                  <CurationsSection title="Other curations">
                     {readOnlyCurations.map((curation) => (
                       <CurationMembershipRow
                         key={curation.id}
                         curation={curation}
                         disabled={true}
                         loading={false}
                         onUpdateMembership={() => undefined}
                       />
                     ))}
-                  </div>
+                  </CurationsSection>
                 )}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/waves/drops/WaveDropCurationsDialog.tsx` around lines 337 - 349,
Add a titled section wrapper for the read-only list to match the other sections:
wrap the readOnlyCurations rendering in a CurationsSection (or the component
used for "Included"/"Available to curate") and pass a title such as "Other
curations" or "View only"; keep the inner mapping over readOnlyCurations and
CurationMembershipRow (with disabled and onUpdateMembership unchanged) so
behavior is identical but now clearly labeled.
components/brain/my-stream/curations/MyStreamWaveCurationContent.tsx (1)

107-133: Permission check relies on first drop only.

The canManageActiveCuration flag is derived from the first drop's curations permissions. This assumes all drops in the curation have the same permission model, which is likely correct but worth noting.

If the first drop is removed (causing a re-render with a different first drop), the permission check will re-evaluate, which could cause a brief UI flicker if permissions differ.

This is acceptable for now since curations are typically wave-level (not drop-level) permissions, but consider caching the permission result or fetching it from a dedicated endpoint if this becomes an issue.

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

In `@components/brain/my-stream/curations/MyStreamWaveCurationContent.tsx` around
lines 107 - 133, The permission check currently probes only the first drop
(permissionProbeDropId derived from drops[0]?.id) and derives
canManageActiveCuration from permissionProbeCurations, which can flicker if the
first drop changes; update the logic to request permissions in a curation-scoped
manner (e.g., call a curation-level permission endpoint using curationId) or
request permissions for all drops (pass all drop ids to useDropCurations) and
then compute canManageActiveCuration by aggregating permissionProbeCurations
(e.g., any matching curation.id === curationId with
authenticated_user_can_curate true), and optionally persist the resolved
permission in local state to avoid transient UI flicker during re-renders.
🤖 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/utils/select-group/SelectGroupModalSearchName.tsx`:
- Line 36: The floating label uses a physical left margin (tw-ml-6) which breaks
in RTL; in SelectGroupModalSearchName.tsx update the label's className to use a
logical start margin instead (replace tw-ml-6 with the logical equivalent, e.g.,
tw-ms-6 or the project's configured logical margin utility) so the label
respects tw-start-1 in both LTR/RTL, and remove or adjust any conflicting
rtl:peer-focus:left/translate classes (like rtl:peer-focus:tw-left-auto
rtl:peer-focus:tw-translate-x-1/4) to rely on logical positioning instead.

In `@components/waves/drops/WaveDropMobileMenu.tsx`:
- Around line 105-114: When navigator.clipboard.writeText is not available the
current branch simply calls closeMenu() and returns with no user feedback;
update the copy handler (the branch checking typeof
navigator.clipboard.writeText) to provide user-visible failure feedback instead
of silently returning — e.g., invoke an existing toast/notification API or set a
failure state (similar to setCopied) with a message like "Copy not supported"
and keep closeMenu behavior consistent; optionally implement a fallback
(select+execCommand or showing the link so the user can manually copy)
referencing navigator.clipboard.writeText, closeMenu, setCopied, and dropLink to
locate where to make the change.

---

Outside diff comments:
In `@openapi.yaml`:
- Around line 6018-6022: The curation identifier is inconsistently typed; update
the OpenAPI definitions so the curation ID is a UUID everywhere: change the path
parameter schema for {curationId} to schema: type: string with format: uuid (or
an equivalent UUID pattern), and update the component schemas ApiWaveCuration.id
and ApiDropCurationRequest.curation_id to the same type+format so read/write
endpoints match; apply the same change to the other occurrences mentioned (lines
referenced in the comment).

---

Duplicate comments:
In `@components/brain/my-stream/tabs/MyStreamWaveCurationCreateDialog.tsx`:
- Around line 115-120: The code always calls commonApiPost in submit logic (see
commonApiPost, isEditMode, wave.id and curation.id) so edit-mode requests are
sent as POST; change the branch so when isEditMode is true you call commonApiPut
with endpoint `waves/${wave.id}/curations/${curation.id}` and the same body, and
keep commonApiPost only for the create branch `waves/${wave.id}/curations`.
- Around line 181-205: The <label> used for the preset buttons in
MyStreamWaveCurationCreateDialog.tsx is not bound to a single form control;
change it to proper grouped semantics by replacing the standalone <label> with a
<fieldset> containing a <legend> (e.g., "Name") and move the button list inside
that fieldset so CURATION_NAME_PRESETS rendering, selectedPresetLabel state, and
handlePresetSelect remain unchanged; keep buttons' aria-pressed attributes for
accessibility and ensure any adjacent custom input is also inside the same
fieldset so the group is semantically associated.
- Around line 62-70: The component MyStreamWaveCurationCreateDialog initializes
name and selectedGroup only once which causes stale values when the dialog is
reopened for a different curation/group; add an effect that watches curation (or
initialName/initialGroup) and the dialog open state and calls
setName(initialName) and setSelectedGroup(initialGroup ?? null) to reset state
on context changes—use the existing identifiers initialName, initialGroup, name,
setName, selectedGroup, setSelectedGroup and ensure the effect dependencies
include curation/initialName, initialGroup, and whatever prop signals the dialog
is open (e.g., isEditMode or isOpen).

In `@openapi.yaml`:
- Around line 1523-1539: The DELETE operation with operationId
removeDropCuration currently requires a request body referencing
ApiDropCurationRequest which is non-portable in OpenAPI 3.0.3; change the
contract to remove the requestBody and instead add a required path or query
parameter (e.g., curation_id) to the delete operation (alongside existing dropId
parameter) and update any references to ApiDropCurationRequest accordingly so
servers and generated clients receive curation_id via the path/query; ensure
operationId removeDropCuration and the Drops tag remain unchanged.

---

Nitpick comments:
In `@components/brain/my-stream/curations/MyStreamWaveCurationContent.tsx`:
- Around line 107-133: The permission check currently probes only the first drop
(permissionProbeDropId derived from drops[0]?.id) and derives
canManageActiveCuration from permissionProbeCurations, which can flicker if the
first drop changes; update the logic to request permissions in a curation-scoped
manner (e.g., call a curation-level permission endpoint using curationId) or
request permissions for all drops (pass all drop ids to useDropCurations) and
then compute canManageActiveCuration by aggregating permissionProbeCurations
(e.g., any matching curation.id === curationId with
authenticated_user_can_curate true), and optionally persist the resolved
permission in local state to avoid transient UI flicker during re-renders.

In `@components/utils/input/identity/IdentitySearch.tsx`:
- Around line 150-153: closeDropdown is recreated each render and passed into
useClickAway and useKeyPressEvent; wrap it with React's useCallback to provide a
stable reference. Replace the inline const closeDropdown = () => {
setIsOpen(false); setManualHighlightedIndex(null); } with a useCallback that
depends only on setIsOpen and setManualHighlightedIndex so the function identity
remains stable when used with useClickAway and useKeyPressEvent.

In `@components/utils/select-group/SelectGroupModalItems.tsx`:
- Around line 29-39: The current rendering of groups uses a div grid and passes
the same onClear to every SelectGroupModalCard; change the container to a
semantic list (use ul with li per item) and only pass the onClear handler to the
card when that card is the selected one (compare selectedGroupId === group.id),
so non-selected cards do not receive/trigger clear; update SelectGroupModalItems
to render each group inside an li and conditionally provide onClear to
SelectGroupModalCard (keep onSelect always passed) to scope clear actions to the
selected item.

In `@components/utils/select-group/SelectGroupModalSearchName.tsx`:
- Around line 1-2: DARK_AUTOFILL_CLASSES repeats nearly identical blocks for
default/hover/focus; extract the common tokens into smaller constants (e.g.,
AUTOFILL_BASE and AUTOFILL_STATE or AUTOFILL_PSEUDO) and compose
DARK_AUTOFILL_CLASSES from those to avoid duplication and drift. Update the
constant definitions in SelectGroupModalSearchName.tsx so that shared fragments
(like -webkit-text-fill-color, caret-color, and tw-shadow inset theme tokens)
live in a single reusable symbol and the hover/focus variants reuse that symbol
when building the final DARK_AUTOFILL_CLASSES string.

In `@components/waves/drops/WaveDropCurationsDialog.tsx`:
- Around line 337-349: Add a titled section wrapper for the read-only list to
match the other sections: wrap the readOnlyCurations rendering in a
CurationsSection (or the component used for "Included"/"Available to curate")
and pass a title such as "Other curations" or "View only"; keep the inner
mapping over readOnlyCurations and CurationMembershipRow (with disabled and
onUpdateMembership unchanged) so behavior is identical but now clearly labeled.
🪄 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: ee31e579-9656-482c-8205-e40853011f2b

📥 Commits

Reviewing files that changed from the base of the PR and between 0160686 and 9cd57f8.

⛔ Files ignored due to path filters (2)
  • generated/models/ApiDropCuration.ts is excluded by !**/generated/**
  • generated/models/ObjectSerializer.ts is excluded by !**/generated/**
📒 Files selected for processing (18)
  • components/brain/ContentTabContext.tsx
  • components/brain/my-stream/MyStreamWaveDesktopTabs.tsx
  • components/brain/my-stream/curations/MyStreamActiveCurationContext.tsx
  • components/brain/my-stream/curations/MyStreamWaveCurationContent.tsx
  • components/brain/my-stream/tabs/MyStreamWaveCurationCreateDialog.tsx
  • components/utils/button/PrimaryButton.tsx
  • components/utils/input/identity/IdentitySearch.tsx
  • components/utils/select-group/SelectGroupModal.tsx
  • components/utils/select-group/SelectGroupModalCard.tsx
  • components/utils/select-group/SelectGroupModalItems.tsx
  • components/utils/select-group/SelectGroupModalSearchName.tsx
  • components/waves/drops/WaveDropActionsMore.tsx
  • components/waves/drops/WaveDropCurationsActionIcon.tsx
  • components/waves/drops/WaveDropCurationsDialog.tsx
  • components/waves/drops/WaveDropMobileMenu.tsx
  • hooks/drops/useDropCurationMembershipMutation.ts
  • hooks/drops/useDropCurations.ts
  • openapi.yaml
✅ Files skipped from review due to trivial changes (1)
  • components/waves/drops/WaveDropCurationsActionIcon.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • components/utils/select-group/SelectGroupModalCard.tsx

Comment thread components/utils/select-group/SelectGroupModalSearchName.tsx Outdated
ragnep added 2 commits April 9, 2026 13:23
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: 2

♻️ Duplicate comments (3)
components/utils/select-group/SelectGroupModalSearchName.tsx (1)

14-14: ⚠️ Potential issue | 🟡 Minor

Keep RTL alignment fully logical and drop conflicting RTL focus offsets.

Line 36 still has rtl:peer-focus:tw-left-auto rtl:peer-focus:tw-translate-x-1/4, while Lines 14 and 32 still use physical left/pl/pr classes. This leaves RTL positioning inconsistent after moving the label to tw-start-*.

💡 Suggested patch
-        className="tw-pointer-events-none tw-absolute tw-left-3 tw-top-3.5 tw-h-4 tw-w-4 tw-text-iron-300"
+        className="tw-pointer-events-none tw-absolute tw-start-3 tw-top-3.5 tw-h-4 tw-w-4 tw-text-iron-300"

-        className={`tw-peer tw-form-input tw-block tw-w-full tw-appearance-none tw-rounded-lg tw-border-0 tw-border-iron-700 tw-bg-iron-900 tw-pb-3 tw-pl-9 tw-pr-4 tw-pt-3 tw-text-sm tw-font-medium tw-text-iron-100 tw-caret-primary-400 tw-shadow-sm tw-ring-1 tw-ring-inset tw-ring-iron-700 tw-transition tw-duration-300 tw-ease-out placeholder:tw-text-iron-500 hover:tw-ring-iron-650 focus:tw-bg-iron-900 focus:tw-outline-none focus:tw-ring-1 focus:tw-ring-inset focus:tw-ring-primary-400 ${DARK_AUTOFILL_CLASSES}`}
+        className={`tw-peer tw-form-input tw-block tw-w-full tw-appearance-none tw-rounded-lg tw-border-0 tw-border-iron-700 tw-bg-iron-900 tw-pb-3 tw-pl-9 tw-pr-4 rtl:tw-pl-4 rtl:tw-pr-9 tw-pt-3 tw-text-sm tw-font-medium tw-text-iron-100 tw-caret-primary-400 tw-shadow-sm tw-ring-1 tw-ring-inset tw-ring-iron-700 tw-transition tw-duration-300 tw-ease-out placeholder:tw-text-iron-500 hover:tw-ring-iron-650 focus:tw-bg-iron-900 focus:tw-outline-none focus:tw-ring-1 focus:tw-ring-inset focus:tw-ring-primary-400 ${DARK_AUTOFILL_CLASSES}`}

-        className="tw-absolute tw-start-7 tw-top-2 tw-origin-[0] -tw-translate-y-4 tw-scale-75 tw-transform tw-cursor-text tw-rounded-lg tw-bg-iron-900 tw-px-2 tw-text-sm tw-font-medium tw-text-iron-500 tw-duration-300 peer-placeholder-shown:tw-top-1/2 peer-placeholder-shown:-tw-translate-y-1/2 peer-placeholder-shown:tw-scale-100 peer-focus:tw-top-2 peer-focus:-tw-translate-y-4 peer-focus:tw-scale-75 peer-focus:tw-bg-iron-900 peer-focus:tw-px-2 peer-focus:tw-text-primary-400 rtl:peer-focus:tw-left-auto rtl:peer-focus:tw-translate-x-1/4"
+        className="tw-absolute tw-start-7 tw-top-2 tw-origin-[0] -tw-translate-y-4 tw-scale-75 tw-transform tw-cursor-text tw-rounded-lg tw-bg-iron-900 tw-px-2 tw-text-sm tw-font-medium tw-text-iron-500 tw-duration-300 peer-placeholder-shown:tw-top-1/2 peer-placeholder-shown:-tw-translate-y-1/2 peer-placeholder-shown:tw-scale-100 peer-focus:tw-top-2 peer-focus:-tw-translate-y-4 peer-focus:tw-scale-75 peer-focus:tw-bg-iron-900 peer-focus:tw-px-2 peer-focus:tw-text-primary-400"

Also applies to: 32-32, 36-36

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

In `@components/utils/select-group/SelectGroupModalSearchName.tsx` at line 14, The
component SelectGroupModalSearchName uses physical LTR classes (tw-left-3,
tw-pr-*, etc.) and conflicting RTL-specific focus offsets
(rtl:peer-focus:tw-left-auto rtl:peer-focus:tw-translate-x-1/4); change all
physical positioning to logical utilities (tw-start-3, tw-start-auto, tw-end-*,
tw-pl/pr -> tw-start/tw-end padding) and remove the rtl:peer-focus
translation/left overrides so peer-focus uses the same logical offsets (or use
peer-focus:tw-start-... / peer-focus:tw-end-... if needed); update the className
occurrences in SelectGroupModalSearchName that contain tw-left-3, tw-pr-*, and
rtl:peer-focus:tw-left-auto rtl:peer-focus:tw-translate-x-1/4 to their logical
equivalents to keep RTL alignment consistent.
components/brain/my-stream/tabs/MyStreamWaveCurationCreateDialog.tsx (2)

180-217: ⚠️ Potential issue | 🟡 Minor

Replace this standalone label with grouped form semantics.

This label is not bound to a single control, so the section still has invalid form semantics for assistive tech. fieldset/legend fits this preset-group-plus-input UI better.

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

In `@components/brain/my-stream/tabs/MyStreamWaveCurationCreateDialog.tsx` around
lines 180 - 217, Replace the standalone label with a fieldset/legend that groups
the preset buttons and the input to provide correct form semantics: wrap the
preset button list and the input (the JSX containing CURATION_NAME_PRESETS,
selectedPresetLabel, handlePresetSelect, nameInputRef, and handleNameChange) in
a <fieldset> and use a <legend> containing the same "Name" text and styling
instead of the label element; ensure the legend carries the visible label
styling and remove the original <label/>; keep existing IDs/classes (e.g.,
id="curation-name") so the input remains identifiable and assistive tech will
now see the grouped controls.

12-12: ⚠️ Potential issue | 🟠 Major

Use commonApiPut for edits.

The edit branch still sends POST to waves/${wave.id}/curations/${curation.id}. That makes create and update indistinguishable at the HTTP layer and can break servers or caches that key behavior off the method.

Suggested fix
-import { commonApiPost } from "@/services/api/common-api";
+import { commonApiPost, commonApiPut } from "@/services/api/common-api";-      return await commonApiPost<ApiWaveCurationRequest, ApiWaveCuration>({
-        endpoint: isEditMode
-          ? `waves/${wave.id}/curations/${curation.id}`
-          : `waves/${wave.id}/curations`,
-        body,
-      });
+      return isEditMode
+        ? await commonApiPut<ApiWaveCurationRequest, ApiWaveCuration>({
+            endpoint: `waves/${wave.id}/curations/${curation.id}`,
+            body,
+          })
+        : await commonApiPost<ApiWaveCurationRequest, ApiWaveCuration>({
+            endpoint: `waves/${wave.id}/curations`,
+            body,
+          });

Also applies to: 115-120

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

In `@components/brain/my-stream/tabs/MyStreamWaveCurationCreateDialog.tsx` at line
12, The edit path in MyStreamWaveCurationCreateDialog currently uses
commonApiPost for updates; change it to use commonApiPut instead: update the
import to pull commonApiPut (replace commonApiPost) and in the submit/update
branch where you call the endpoint "waves/${wave.id}/curations/${curation.id}"
(the edit flow inside the component) call commonApiPut with the same payload and
URL so edits send HTTP PUT; ensure all other occurrences in this file that
target that specific curation update route (including the other call around the
create/edit branching) are switched from commonApiPost to commonApiPut.
🤖 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/waves/drops/WaveDropMobileMenu.tsx`:
- Around line 105-114: The clipboard access can throw if navigator.clipboard is
undefined and the writeText promise is unhandled; update the clipboard branch in
WaveDropMobileMenu so it first checks navigator.clipboard exists (e.g., if
(!navigator.clipboard || typeof navigator.clipboard.writeText !== "function") {
closeMenu(); return; }), then call
navigator.clipboard.writeText(dropLink).then(() => { setCopied(true);
setTimeout(() => setCopied(false), 2000); }).catch(() => { /* optionally show
error or noop */ }).finally(() => closeMenu()); this ensures no TypeError,
handles promise rejection, and always closes the menu via closeMenu, referencing
closeMenu, setCopied, and dropLink in the fix.

In `@hooks/waves/useWaveCurationGroups.ts`:
- Around line 3-4: The shim currently re-exports getWaveCurationsQueryKey and
useWaveCurations as useWaveCurationGroups but no longer exposes the legacy
getWaveCurationGroupsQueryKey symbol; add a re-export for the legacy name so
existing imports don't break. In hooks/waves/useWaveCurationGroups.ts re-export
getWaveCurationGroupsQueryKey from "./useWaveCurations" (either directly or by
aliasing getWaveCurationsQueryKey -> getWaveCurationGroupsQueryKey) alongside
the existing exports (getWaveCurationsQueryKey and useWaveCurations as
useWaveCurationGroups).

---

Duplicate comments:
In `@components/brain/my-stream/tabs/MyStreamWaveCurationCreateDialog.tsx`:
- Around line 180-217: Replace the standalone label with a fieldset/legend that
groups the preset buttons and the input to provide correct form semantics: wrap
the preset button list and the input (the JSX containing CURATION_NAME_PRESETS,
selectedPresetLabel, handlePresetSelect, nameInputRef, and handleNameChange) in
a <fieldset> and use a <legend> containing the same "Name" text and styling
instead of the label element; ensure the legend carries the visible label
styling and remove the original <label/>; keep existing IDs/classes (e.g.,
id="curation-name") so the input remains identifiable and assistive tech will
now see the grouped controls.
- Line 12: The edit path in MyStreamWaveCurationCreateDialog currently uses
commonApiPost for updates; change it to use commonApiPut instead: update the
import to pull commonApiPut (replace commonApiPost) and in the submit/update
branch where you call the endpoint "waves/${wave.id}/curations/${curation.id}"
(the edit flow inside the component) call commonApiPut with the same payload and
URL so edits send HTTP PUT; ensure all other occurrences in this file that
target that specific curation update route (including the other call around the
create/edit branching) are switched from commonApiPost to commonApiPut.

In `@components/utils/select-group/SelectGroupModalSearchName.tsx`:
- Line 14: The component SelectGroupModalSearchName uses physical LTR classes
(tw-left-3, tw-pr-*, etc.) and conflicting RTL-specific focus offsets
(rtl:peer-focus:tw-left-auto rtl:peer-focus:tw-translate-x-1/4); change all
physical positioning to logical utilities (tw-start-3, tw-start-auto, tw-end-*,
tw-pl/pr -> tw-start/tw-end padding) and remove the rtl:peer-focus
translation/left overrides so peer-focus uses the same logical offsets (or use
peer-focus:tw-start-... / peer-focus:tw-end-... if needed); update the className
occurrences in SelectGroupModalSearchName that contain tw-left-3, tw-pr-*, and
rtl:peer-focus:tw-left-auto rtl:peer-focus:tw-translate-x-1/4 to their logical
equivalents to keep RTL alignment 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: e4fe6651-58e9-4ce6-b850-860a8f4eb363

📥 Commits

Reviewing files that changed from the base of the PR and between 9cd57f8 and cff14cd.

📒 Files selected for processing (10)
  • components/brain/ContentTabContext.tsx
  • components/brain/my-stream/MyStreamWaveDesktopTabs.tsx
  • components/brain/my-stream/MyStreamWaveLeaderboard.tsx
  • components/brain/my-stream/tabs/MyStreamWaveCurationCreateDialog.tsx
  • components/utils/select-group/SelectGroupModalSearchName.tsx
  • components/waves/drops/WaveDropMobileMenu.tsx
  • components/waves/groups/curation/WaveActiveCurationSection.tsx
  • components/waves/groups/curation/WaveCurationGroupsSection.tsx
  • hooks/waves/useWaveCurationGroups.ts
  • hooks/waves/useWaveCurations.ts
✅ Files skipped from review due to trivial changes (1)
  • components/waves/groups/curation/WaveCurationGroupsSection.tsx
🚧 Files skipped from review as they are similar to previous changes (3)
  • components/brain/ContentTabContext.tsx
  • components/waves/groups/curation/WaveActiveCurationSection.tsx
  • components/brain/my-stream/MyStreamWaveLeaderboard.tsx

Comment thread components/waves/drops/WaveDropMobileMenu.tsx Outdated
Comment thread hooks/waves/useWaveCurationGroups.ts Outdated
ragnep added 9 commits April 9, 2026 13:56
Signed-off-by: ragnep <ragneinfo@gmail.com>
Signed-off-by: ragnep <ragneinfo@gmail.com>
Signed-off-by: ragnep <ragneinfo@gmail.com>
Signed-off-by: ragnep <ragneinfo@gmail.com>
Signed-off-by: ragnep <ragneinfo@gmail.com>
Signed-off-by: ragnep <ragneinfo@gmail.com>
Signed-off-by: ragnep <ragneinfo@gmail.com>
@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud Bot commented Apr 10, 2026

Quality Gate Passed Quality Gate passed

Issues
0 New issues
2 Accepted issues

Measures
0 Security Hotspots
No data about Coverage
1.9% Duplication on New Code

See analysis details on SonarQube Cloud

@ragnep ragnep merged commit 76e7437 into main Apr 10, 2026
8 checks passed
@coderabbitai coderabbitai Bot mentioned this pull request Apr 22, 2026
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