Conversation
📝 WalkthroughWalkthroughIntroduces a public read-only viewer mode for waves, allowing signed-out users to access wave content without interactive capabilities. Adds Changes
Sequence DiagramsequenceDiagram
participant User as Signed-Out User
participant Browser as Browser / Router
participant WavesLayout as WavesLayout Component
participant WVMProvider as WaveViewerModeProvider
participant Drop as Drop Component
participant UI as UI Elements
User->>Browser: Navigate to /waves/{waveId}
Browser->>WavesLayout: Render with activeWaveId
WavesLayout->>WavesLayout: Check if activeWaveId !== null
WavesLayout->>WVMProvider: Render with isPublicReadOnly=true
WVMProvider->>Drop: Provide isPublicReadOnly context
Drop->>Drop: useWaveViewerMode() reads isPublicReadOnly
Drop->>UI: Gate vote button: show only if !isPublicReadOnly
Drop->>UI: Gate delete action: show only if !isPublicReadOnly
Drop->>UI: Gate reaction handlers: disable if isPublicReadOnly
Drop->>UI: Render read-only view (view vote summary, no voting UI)
User->>UI: Attempt to vote
UI->>UI: isPublicReadOnly=true, handler blocked
User->>Browser: See "Connect wallet" prompt
Browser->>WavesLayout: User connects wallet
WavesLayout->>WVMProvider: Re-render with isPublicReadOnly=false
Drop->>UI: Un-gate vote button, reactions, delete
User->>UI: Now can vote/react/delete
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
components/waves/leaderboard/grid/WaveLeaderboardGridItem.tsx (1)
57-82:⚠️ Potential issue | 🟠 MajorReset transient menu/modal state when the view flips read-only.
Disabling the touch handlers and hiding the portal here leaves the existing local state intact. If the card was long-pressed first,
isActivestaystrue, and Line 195 keeps swallowing taps after the mode switch even though the mobile menu is gone.isVotingModalOpenhas the same stale-state problem when the view becomes interactive again.Suggested fix
import React, { useCallback, + useEffect, useMemo, useState, useSyncExternalStore, } from "react"; ... const { canShowVote } = useDropInteractionRules(drop); const isCuratableInView = !isPublicReadOnly && isCuratable; const canShowVoteInView = !isPublicReadOnly && canShowVote; + + useEffect(() => { + if (!isPublicReadOnly) { + return; + } + setIsActive(false); + setIsVotingModalOpen(false); + }, [isPublicReadOnly, setIsActive]);Also applies to: 220-222, 393-410
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@components/waves/leaderboard/grid/WaveLeaderboardGridItem.tsx` around lines 57 - 82, The card leaves transient UI state (isActive from useLongPressInteraction and isVotingModalOpen) stale when isPublicReadOnly flips, causing taps to be swallowed or modals/menus to persist; add an effect that watches isPublicReadOnly and when it becomes true call setIsActive(false) and setIsVotingModalOpen(false) (and any other transient setters for menus/portals), and also ensure touchHandlers are no longer considered active after the flip (by clearing isActive rather than relying on touchHandlers). Update the component to reset these transient states (isActive via setIsActive, isVotingModalOpen via setIsVotingModalOpen, and any local menu-open flags) inside a useEffect triggered by changes to isPublicReadOnly.
🧹 Nitpick comments (2)
components/waves/public/WaveViewerModeContext.tsx (1)
5-25: Make a missing provider obvious instead of silently defaulting to writable mode.All current consumers of
useWaveViewerMode()are properly wrapped byWaveViewerModeProviderthrough theWavesMessagesWrapperhierarchy. However, the context's default value offalsemakes unwrapped components indistinguishable from intentional interactive mode. Since this flag suppresses create/vote/delete entry points, missing the provider would silently fail open rather than surface quickly.Using an
undefinedsentinel and a hook assertion prevents future integration mistakes:Suggested change
-const WaveViewerModeContext = createContext(false); +const WaveViewerModeContext = createContext<boolean | undefined>(undefined); @@ export function useWaveViewerMode() { - return { - isPublicReadOnly: useContext(WaveViewerModeContext), - }; + const isPublicReadOnly = useContext(WaveViewerModeContext); + if (isPublicReadOnly === undefined) { + throw new Error( + "useWaveViewerMode must be used within WaveViewerModeProvider" + ); + } + return { isPublicReadOnly }; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@components/waves/public/WaveViewerModeContext.tsx` around lines 5 - 25, The context currently defaults to false which masks missing providers; change WaveViewerModeContext to use an undefined sentinel (i.e., createContext<boolean | undefined>(undefined)), update typing where needed (WaveViewerModeProviderProps can remain the same), and modify useWaveViewerMode to read the context and assert/throw a clear error if the value is undefined (referencing WaveViewerModeContext and useWaveViewerMode) so unwrapped components fail fast instead of silently behaving as writable.openapi.yaml (1)
11773-11833: KeepApiWaveMinlightweight.
ApiDropalready exposes drop-levelselections, so adding the full wave selection catalog toApiWaveMinmeans every nestedwavein list responses repeats the same array. I’d keep the catalog on top-level wave responses and leaveApiWaveMinas the lean embed.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@openapi.yaml` around lines 11773 - 11833, ApiWaveMin is too heavy because it includes the full selections catalog; remove the selections property from the ApiWaveMin schema so nested/embedded wave objects stay lightweight. Keep selections only on the top-level wave schema (e.g., ApiWave / the full wave response schema that ApiDrop already uses) and ensure any refs that should embed the lightweight version continue to use ApiWaveMin (update any $ref consumers if needed). Do not change other properties on ApiWaveMin.
🤖 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/MyStreamWaveLeaderboard.tsx`:
- Around line 92-96: When isPublicReadOnly flips to true we need to actively
close any open creation flows instead of merely hiding them; add a useEffect
that watches isPublicReadOnly and, when it becomes true, calls the state setters
that control the create UI (e.g., setIsCreateDropOpen(false) and any
composer/modal close setters used alongside isCreateDropOpen) so local flags are
reset; ensure this handles all related flows referenced by
showToggleableDropInput (isCreateDropOpen, submissionExperience branches) and
the other create-flow flags used around lines 328-345.
In `@components/shared/WavesMessagesWrapper.tsx`:
- Around line 148-212: The right sidebar is still rendered even in public
read-only mode; update the two places that render BrainRightSidebar (the inline
block conditioned on rightVariant and the overlay block conditioned on
rightVariant === "overlay") to also check the public-read-only state provided by
WaveViewerModeProvider (use whatever local prop/state indicates read-only, e.g.,
isPublicReadOnly) and skip rendering when public read-only is true so
BrainRightSidebar (and its props waveId, activeTab/sidebarTab,
setActiveTab/setSidebarTab) are not mounted in that mode.
In `@components/waves/drop/SingleWaveDropWrapper.tsx`:
- Around line 147-217: The desktop and mobile chat trees currently both mount
SingleWaveDropChat (causing duplicate effects); update the rendering so the
desktop pane (the lg:tw-flex container with SingleWaveDropChat key={drop.id}) is
wrapped or conditionally rendered only when !isSmallScreen, and the mobile
overlay Transition tree that renders SingleWaveDropChat is only rendered when
isSmallScreen && isChatOpen; use the existing isSmallScreen/isChatOpen state
(and keep toggleChat behavior) to ensure only one SingleWaveDropChat is mounted
per breakpoint while still passing wave and drop props and rendering into
trailingContentContainer.
In `@components/waves/drops/WaveDrop.tsx`:
- Around line 189-190: The action button trigger can remain visible in read-only
mode because only long-press and WaveDropMobileMenu are suppressed; update the
render logic that computes/showProps for the header's action trigger (the
variable/prop associated with showActionsButton and the component that renders
the header action) to also consider isPublicReadOnly and avoid rendering the
trigger when isPublicReadOnly is true; alternatively, set allowLongPress or
showActionsButton to false when isPublicReadOnly so the header button is not
rendered along with suppressing WaveDropMobileMenu and the long-press behavior.
In `@hooks/useWaveDropsSearch.ts`:
- Line 41: The mapping in useWaveDropsSearch forwards wave.selections directly
which can be undefined; update the mapper (the object with selections:
wave.selections) to guard and supply a safe default (e.g., use wave.selections
?? [] or Array.isArray check) so ApiWaveMin.selections is never undefined before
downstream wave/drop mapping; locate the mapping inside the useWaveDropsSearch
hook and replace the raw property access with the guarded/defaulted expression.
---
Outside diff comments:
In `@components/waves/leaderboard/grid/WaveLeaderboardGridItem.tsx`:
- Around line 57-82: The card leaves transient UI state (isActive from
useLongPressInteraction and isVotingModalOpen) stale when isPublicReadOnly
flips, causing taps to be swallowed or modals/menus to persist; add an effect
that watches isPublicReadOnly and when it becomes true call setIsActive(false)
and setIsVotingModalOpen(false) (and any other transient setters for
menus/portals), and also ensure touchHandlers are no longer considered active
after the flip (by clearing isActive rather than relying on touchHandlers).
Update the component to reset these transient states (isActive via setIsActive,
isVotingModalOpen via setIsVotingModalOpen, and any local menu-open flags)
inside a useEffect triggered by changes to isPublicReadOnly.
---
Nitpick comments:
In `@components/waves/public/WaveViewerModeContext.tsx`:
- Around line 5-25: The context currently defaults to false which masks missing
providers; change WaveViewerModeContext to use an undefined sentinel (i.e.,
createContext<boolean | undefined>(undefined)), update typing where needed
(WaveViewerModeProviderProps can remain the same), and modify useWaveViewerMode
to read the context and assert/throw a clear error if the value is undefined
(referencing WaveViewerModeContext and useWaveViewerMode) so unwrapped
components fail fast instead of silently behaving as writable.
In `@openapi.yaml`:
- Around line 11773-11833: ApiWaveMin is too heavy because it includes the full
selections catalog; remove the selections property from the ApiWaveMin schema so
nested/embedded wave objects stay lightweight. Keep selections only on the
top-level wave schema (e.g., ApiWave / the full wave response schema that
ApiDrop already uses) and ensure any refs that should embed the lightweight
version continue to use ApiWaveMin (update any $ref consumers if needed). Do not
change other properties on ApiWaveMin.
🪄 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: ff295a2b-7d2f-423d-81aa-51283fcc3fef
⛔ Files ignored due to path filters (9)
generated/models/ApiDrop.tsis excluded by!**/generated/**generated/models/ApiDropWithoutWave.tsis excluded by!**/generated/**generated/models/ApiWave.tsis excluded by!**/generated/**generated/models/ApiWaveMin.tsis excluded by!**/generated/**generated/models/ApiWaveSelection.tsis excluded by!**/generated/**generated/models/ApiWaveSelectionDropRequest.tsis excluded by!**/generated/**generated/models/ApiWaveSelectionRequest.tsis excluded by!**/generated/**generated/models/ObjectSerializer.tsis excluded by!**/generated/**package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (43)
__tests__/components/waves/drop/SingleWaveDropWrapper.test.tsxcomponents/brain/ContentTabContext.tsxcomponents/brain/my-stream/MyStreamWave.tsxcomponents/brain/my-stream/MyStreamWaveChat.tsxcomponents/brain/my-stream/MyStreamWaveDesktopTabs.tsxcomponents/brain/my-stream/MyStreamWaveLeaderboard.tsxcomponents/brain/my-stream/tabs/MyStreamWaveTabsDefault.tsxcomponents/brain/my-stream/tabs/MyStreamWaveTabsMeme.tsxcomponents/memes/drops/MemeParticipationDrop.tsxcomponents/memes/drops/MemeWinnerDrop.tsxcomponents/memes/drops/MemesLeaderboardDrop.tsxcomponents/shared/WavesMessagesWrapper.tsxcomponents/user/layout/userPageVisibility.tscomponents/waves/WavesDesktop.tsxcomponents/waves/drop/MemesSingleWaveDropInfoPanel.tsxcomponents/waves/drop/SingleWaveDropInfoPanel.tsxcomponents/waves/drop/SingleWaveDropWrapper.tsxcomponents/waves/drops/DropMobileMenuHandler.tsxcomponents/waves/drops/WaveDrop.tsxcomponents/waves/drops/WaveDropReactions.tsxcomponents/waves/drops/participation/EndedParticipationDrop.tsxcomponents/waves/drops/participation/OngoingParticipationDrop.tsxcomponents/waves/drops/participation/ParticipationDropFooter.tsxcomponents/waves/drops/wave-drops-all/hooks/useWaveDropsNotificationRead.tscomponents/waves/drops/wave-drops-all/index.tsxcomponents/waves/drops/wave-drops-all/subcomponents/WaveDropsContent.tsxcomponents/waves/drops/wave-drops-all/subcomponents/WaveDropsMessageListSection.tsxcomponents/waves/drops/winner/DefaultWinnerDrop.tsxcomponents/waves/layout/WavesLayout.tsxcomponents/waves/leaderboard/drops/DefaultWaveLeaderboardDrop.tsxcomponents/waves/leaderboard/gallery/WaveLeaderboardGalleryItem.tsxcomponents/waves/leaderboard/grid/WaveLeaderboardGridItem.tsxcomponents/waves/memes/submission/utils/buildPreviewDrop.tscomponents/waves/public/LoggedOutSkeleton.tsxcomponents/waves/public/PublicWaveShell.tsxcomponents/waves/public/WaveViewerModeContext.tsxcomponents/waves/public/usePublicWaveShellState.tscomponents/waves/utils/getOptimisticDrop.tsdocs/waves/README.mddocs/waves/feature-public-wave-preview.mddocs/waves/troubleshooting-wave-navigation-and-posting.mdhooks/useWaveDropsSearch.tsopenapi.yaml
💤 Files with no reviewable changes (1)
- components/waves/public/LoggedOutSkeleton.tsx
| const allowLongPress = hasTouch && !isMdUp && !isPublicReadOnly; | ||
| const compact = useCompactMode(); |
There was a problem hiding this comment.
Hide the touch actions trigger in read-only mode to avoid a dead button.
Line 189 disables long-press and Line 504 suppresses WaveDropMobileMenu, but the header trigger (showActionsButton on Line 430) can still render. In read-only mode that leaves a visible action button that opens nothing.
🔧 Proposed fix
- showActionsButton={hasTouch && showReplyAndQuote && !isEditing}
+ showActionsButton={
+ !isPublicReadOnly &&
+ hasTouch &&
+ showReplyAndQuote &&
+ !isEditing
+ }Also applies to: 504-516
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@components/waves/drops/WaveDrop.tsx` around lines 189 - 190, The action
button trigger can remain visible in read-only mode because only long-press and
WaveDropMobileMenu are suppressed; update the render logic that
computes/showProps for the header's action trigger (the variable/prop associated
with showActionsButton and the component that renders the header action) to also
consider isPublicReadOnly and avoid rendering the trigger when isPublicReadOnly
is true; alternatively, set allowLongPress or showActionsButton to false when
isPublicReadOnly so the header button is not rendered along with suppressing
WaveDropMobileMenu and the long-press behavior.
|
There was a problem hiding this comment.
🧹 Nitpick comments (1)
__tests__/components/brain/my-stream/MyStreamWaveLeaderboard.test.tsx (1)
519-548: Consider extendingleaderboardTreeto supportkeyprop for consistency.This test manually constructs the provider tree because it needs the
keyprop to force component remount on wave change. This works correctly but creates a minor inconsistency:WaveViewerModeProviderhere omitsisPublicReadOnlywhileleaderboardTreealways passes it explicitly.Consider extending the helper to accept an optional
key:♻️ Optional refactor to use consistent helper
const leaderboardTree = ({ waveOverride = wave, isPublicReadOnly = false, + componentKey, }: { readonly waveOverride?: ApiWave; readonly isPublicReadOnly?: boolean; + readonly componentKey?: string; } = {}) => ( <WaveViewerModeProvider isPublicReadOnly={isPublicReadOnly}> <AuthContext.Provider value={authContextValue}> - <MyStreamWaveLeaderboard wave={waveOverride} onDropClick={jest.fn()} /> + <MyStreamWaveLeaderboard + key={componentKey} + wave={waveOverride} + onDropClick={jest.fn()} + /> </AuthContext.Provider> </WaveViewerModeProvider> );Then the test could use:
const { rerender } = render(leaderboardTree({ waveOverride: waveA, componentKey: waveA.id })); // ... rerender(leaderboardTree({ waveOverride: waveB, componentKey: waveB.id }));🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@__tests__/components/brain/my-stream/MyStreamWaveLeaderboard.test.tsx` around lines 519 - 548, The test manually mounts WaveViewerModeProvider with a key to force remount, causing an inconsistency with the existing leaderboardTree helper which always passes isPublicReadOnly; update leaderboardTree to accept an optional componentKey (and keep its existing isPublicReadOnly behavior/default) and use that prop to set the wrapper/component key when provided (e.g., render the MyStreamWaveLeaderboard with key={componentKey} and accept waveOverride to swap waves), then update this test to call render(leaderboardTree({ waveOverride: waveA, componentKey: waveA.id })) and rerender(leaderboardTree({ waveOverride: waveB, componentKey: waveB.id })) so the helper is reused and consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@__tests__/components/brain/my-stream/MyStreamWaveLeaderboard.test.tsx`:
- Around line 519-548: The test manually mounts WaveViewerModeProvider with a
key to force remount, causing an inconsistency with the existing leaderboardTree
helper which always passes isPublicReadOnly; update leaderboardTree to accept an
optional componentKey (and keep its existing isPublicReadOnly behavior/default)
and use that prop to set the wrapper/component key when provided (e.g., render
the MyStreamWaveLeaderboard with key={componentKey} and accept waveOverride to
swap waves), then update this test to call render(leaderboardTree({
waveOverride: waveA, componentKey: waveA.id })) and rerender(leaderboardTree({
waveOverride: waveB, componentKey: waveB.id })) so the helper is reused and
consistent.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 349ecd8d-af59-4abe-8614-aba2a4611915
📒 Files selected for processing (11)
__tests__/components/brain/my-stream/MyStreamWaveLeaderboard.test.tsx__tests__/components/brain/my-stream/tabs/MyStreamWaveTabsDefault.test.tsx__tests__/components/brain/my-stream/tabs/MyStreamWaveTabsMeme.test.tsx__tests__/components/shared/WavesMessagesWrapper.test.tsx__tests__/components/waves/drop/SingleWaveDropWrapper.test.tsxcomponents/brain/my-stream/MyStreamWaveLeaderboard.tsxcomponents/brain/my-stream/tabs/MyStreamWaveTabsMeme.tsxcomponents/shared/WavesMessagesWrapper.tsxcomponents/waves/WavesDesktop.tsxcomponents/waves/drop/SingleWaveDropWrapper.tsxcomponents/waves/layout/WavesLayout.tsx
🚧 Files skipped from review as they are similar to previous changes (4)
- components/brain/my-stream/tabs/MyStreamWaveTabsMeme.tsx
- components/waves/WavesDesktop.tsx
- tests/components/waves/drop/SingleWaveDropWrapper.test.tsx
- components/brain/my-stream/MyStreamWaveLeaderboard.tsx



Summary by CodeRabbit
Release Notes
New Features
Documentation
Tests