Conversation
For both programs and partners a few issues were happening on smaller screens: - When clicking "new message" and it shows the partner/program drawer, after selecting a partner/program, nothing else shows. It should open the compose window, with the message input in focus. - When you select and existing conversation, the far panel opens over the conversation and you need to close it to see the thread. - `Program only` When you click the menu button in the top left, it opens the nav panel, but everything is hidden until you click on the partner icon again, and it takes you back to the program overview. on larger screens this makes sense, but on mobile we need to show the full nav.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds URL-driven composer autofocus via Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant Router as Router/Navigation
participant Layout as Messages Layout
participant Page as Page Client
participant Panel as MessagesPanel
participant Input as MessageInput
User->>Router: select program/partner
Router->>Layout: navigate to /messages/{id}?new=1
Layout->>Layout: setTargetThreadId(id), setCurrentPanel("main")
Layout->>Page: render page-client for selection
Page->>Page: read searchParams (?new=1) and viewport width
Page->>Router: router.replace(...) to remove ?new=1
Page->>Panel: render with autoFocusComposer=true (if param present)
Panel->>Input: render with autoFocus = !isMobile || autoFocusComposer
Input->>User: focus composer input
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 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 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.
🧹 Nitpick comments (2)
apps/web/app/(ee)/partners.dub.co/(dashboard)/messages/[programSlug]/page-client.tsx (2)
119-122: URL cleanup drops all search params — align with the program-side implementation
router.replace(\/messages/${programSlug}`)unconditionally strips every search param on the URL, not justnew. The analogous effect in the program-side page ([partnerId]/page-client.tsx, lines 106-114) usesURLSearchParamsto delete only"new"` and preserves the rest. Adopt the same pattern here to avoid silently losing any other params that may be present now or in future.♻️ Proposed fix
+ import { usePathname } from "next/navigation"; // ... + const pathname = usePathname(); useEffect(() => { if (!shouldAutoFocusComposer) return; - router.replace(`/messages/${programSlug}`); + const nextSearchParams = new URLSearchParams(searchParams.toString()); + nextSearchParams.delete("new"); + const nextSearch = nextSearchParams.toString(); + router.replace(nextSearch ? `${pathname}?${nextSearch}` : pathname); - }, [programSlug, router, shouldAutoFocusComposer]); + }, [pathname, router, searchParams, shouldAutoFocusComposer]);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/app/`(ee)/partners.dub.co/(dashboard)/messages/[programSlug]/page-client.tsx around lines 119 - 122, The current useEffect in page-client.tsx unconditionally calls router.replace(`/messages/${programSlug}`) which strips all search params; change it to read the current URLSearchParams, delete only the "new" param, and then call router.replace with the same pathname (`/messages/${programSlug}`) plus the preserved search string so other params remain intact; update the useEffect that references shouldAutoFocusComposer, programSlug, and router to build the new query via URLSearchParams and pass the resulting search (or empty string) to router.replace.
114-117: Width effect overrides manual panel toggle on any resizeThe
widthdependency means that every resize event that changeswidthforcibly resetsisRightPanelOpentowidth >= 1082, regardless of whether the user previously toggled the panel intentionally. A user on a 1200px viewport who deliberately closes the panel will have it reopened immediately if they resize by even a single pixel.If preserving intentional user toggles matters, consider tracking "user has explicitly set the panel" to suppress the automatic override:
♻️ Suggested approach
- const [isRightPanelOpen, setIsRightPanelOpen] = useState(false); + const [isRightPanelOpen, setIsRightPanelOpen] = useState(false); + const [isPanelUserControlled, setIsPanelUserControlled] = useState(false); useEffect(() => { if (typeof width !== "number") return; - setIsRightPanelOpen(width >= 1082); + if (!isPanelUserControlled) { + setIsRightPanelOpen(width >= 1082); + } }, [programSlug, width]); // Reset user-controlled flag when navigating to a different program + useEffect(() => { + setIsPanelUserControlled(false); + }, [programSlug]);Then set
setIsPanelUserControlled(true)in any click handler that callssetIsRightPanelOpen.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/app/`(ee)/partners.dub.co/(dashboard)/messages/[programSlug]/page-client.tsx around lines 114 - 117, The effect that auto-sets isRightPanelOpen on width changes (useEffect referencing width, programSlug, setIsRightPanelOpen) currently overrides any user toggle; add a new state flag (e.g., isPanelUserControlled with setter setIsPanelUserControlled) and update the resize effect to only auto-set isRightPanelOpen when isPanelUserControlled is false, and include isPanelUserControlled in the effect dependencies; also ensure every user toggle handler that calls setIsRightPanelOpen sets setIsPanelUserControlled(true) so subsequent resizes won’t overwrite the user’s explicit choice.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In
`@apps/web/app/app.dub.co/`(dashboard)/[slug]/(ee)/program/messages/[partnerId]/page-client.tsx:
- Around line 101-104: The effect that watches width and partnerId resets
isRightPanelOpen on every resize and thus overrides user manual toggles; change
the logic in the useEffect (the one using width, partnerId and calling
setIsRightPanelOpen) to only auto-set when there has been no user toggle (for
example track a manual toggle flag like hasManualRightPanelToggle or initialize
isRightPanelOpen to null/undefined and only set it from width when it is unset),
and ensure the manual toggle handler updates that flag so subsequent resizes do
not clobber the user's choice.
---
Nitpick comments:
In
`@apps/web/app/`(ee)/partners.dub.co/(dashboard)/messages/[programSlug]/page-client.tsx:
- Around line 119-122: The current useEffect in page-client.tsx unconditionally
calls router.replace(`/messages/${programSlug}`) which strips all search params;
change it to read the current URLSearchParams, delete only the "new" param, and
then call router.replace with the same pathname (`/messages/${programSlug}`)
plus the preserved search string so other params remain intact; update the
useEffect that references shouldAutoFocusComposer, programSlug, and router to
build the new query via URLSearchParams and pass the resulting search (or empty
string) to router.replace.
- Around line 114-117: The effect that auto-sets isRightPanelOpen on width
changes (useEffect referencing width, programSlug, setIsRightPanelOpen)
currently overrides any user toggle; add a new state flag (e.g.,
isPanelUserControlled with setter setIsPanelUserControlled) and update the
resize effect to only auto-set isRightPanelOpen when isPanelUserControlled is
false, and include isPanelUserControlled in the effect dependencies; also ensure
every user toggle handler that calls setIsRightPanelOpen sets
setIsPanelUserControlled(true) so subsequent resizes won’t overwrite the user’s
explicit choice.
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
apps/web/app/(ee)/partners.dub.co/(dashboard)/messages/layout.tsx (1)
68-72:targetThreadIdstores a slug here but a UUID in the program-side layout.In the program-side layout (
messages/layout.tsx),setTargetThreadId(id)receives a partner UUID, while heresetTargetThreadId(slug)receives a program slug. The field nametargetThreadIdsuggests an ID, but it's actually a generic "active thread key" that matches againstgroup.id/activeId. This works correctly but could be confusing for future maintainers.Consider renaming to something like
targetThreadKeyif you revisit this area, but not blocking.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@apps/web/app/`(ee)/partners.dub.co/(dashboard)/messages/layout.tsx around lines 68 - 72, The code uses targetThreadId to store either a program slug or a partner UUID which is confusing; rename the state and setter to a neutral name like targetThreadKey (update the state variable, setter setTargetThreadKey and all callers such as setSelectedProgramSlug where you call setTargetThreadId(slug)) and update every comparison/usage that matches against group.id or activeId to use targetThreadKey instead so the meaning is consistent; ensure prop and handler names (e.g., setSelectedProgramSlug, setTargetThreadKey, any layout handlers) and any URL/navigation logic (router.push calls) are updated to reference the new identifier name.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In
`@apps/web/app/app.dub.co/`(dashboard)/[slug]/(ee)/program/messages/[partnerId]/page-client.tsx:
- Around line 104-107: The effect that auto-sets panel open state should not
unconditionally run on every width change; update the logic in the useEffect
(the effect that currently reads width and calls setIsRightPanelOpen) to respect
a user toggle flag or to run only when partnerId changes: add a new state like
userHasToggled (initially false), set userHasToggled = true inside the manual
toggle handler that calls setIsRightPanelOpen, and change the effect to only
auto-set when userHasToggled is false (or change its dependency array to only
[partnerId] so it runs on partner change only); reference the existing
useEffect, setIsRightPanelOpen, isRightPanelOpen, partnerId and width when
making the changes.
- Around line 101-102: The right-panel opens after initial paint causing a
layout shift because isRightPanelOpen is initialized to false; change the
initialization so the state is computed synchronously from the current viewport
(e.g. use a media-query check in the useState initializer) instead of always
false. Update the isRightPanelOpen / setIsRightPanelOpen initialization to
derive its initial value from a synchronous media check (window.matchMedia or
your useMediaQuery sync value) with proper SSR guarding, leaving the existing
effect that updates it in place.
---
Nitpick comments:
In `@apps/web/app/`(ee)/partners.dub.co/(dashboard)/messages/layout.tsx:
- Around line 68-72: The code uses targetThreadId to store either a program slug
or a partner UUID which is confusing; rename the state and setter to a neutral
name like targetThreadKey (update the state variable, setter setTargetThreadKey
and all callers such as setSelectedProgramSlug where you call
setTargetThreadId(slug)) and update every comparison/usage that matches against
group.id or activeId to use targetThreadKey instead so the meaning is
consistent; ensure prop and handler names (e.g., setSelectedProgramSlug,
setTargetThreadKey, any layout handlers) and any URL/navigation logic
(router.push calls) are updated to reference the new identifier name.
| const [isRightPanelOpen, setIsRightPanelOpen] = useState(false); | ||
| const shouldAutoFocusComposer = searchParams.get("new") === "1"; |
There was a problem hiding this comment.
Minor layout shift on desktop: isRightPanelOpen defaults to false.
On wide screens (≥960px), the right panel will be closed on the initial render, then opened by the effect on lines 104–107 after paint. This may cause a brief visible layout shift. Previously this defaulted to true.
Consider initializing based on width if available synchronously, e.g.:
- const [isRightPanelOpen, setIsRightPanelOpen] = useState(false);
+ const [isRightPanelOpen, setIsRightPanelOpen] = useState(
+ typeof width === "number" ? width >= 960 : false,
+ );Though this depends on whether useMediaQuery provides a synchronous initial value or not.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@apps/web/app/app.dub.co/`(dashboard)/[slug]/(ee)/program/messages/[partnerId]/page-client.tsx
around lines 101 - 102, The right-panel opens after initial paint causing a
layout shift because isRightPanelOpen is initialized to false; change the
initialization so the state is computed synchronously from the current viewport
(e.g. use a media-query check in the useState initializer) instead of always
false. Update the isRightPanelOpen / setIsRightPanelOpen initialization to
derive its initial value from a synchronous media check (window.matchMedia or
your useMediaQuery sync value) with proper SSR guarding, leaving the existing
effect that updates it in place.
| useEffect(() => { | ||
| if (typeof width !== "number") return; | ||
| setIsRightPanelOpen(width >= 960); | ||
| }, [partnerId, width]); |
There was a problem hiding this comment.
Right panel effect re-fires on every width change, potentially overriding manual user toggle.
If a user manually closes the right panel on a wide screen (≥960px), any subsequent resize event (even a 1px change) will reopen it because the effect unconditionally sets isRightPanelOpen based on width. Similarly, manually opening the panel on a narrow screen gets overridden on resize.
Consider tracking whether the user has manually toggled the panel and skipping the auto-set in that case, or only running this logic on partnerId change (not width).
♻️ Possible approach: only auto-set on partnerId change
useEffect(() => {
if (typeof width !== "number") return;
setIsRightPanelOpen(width >= 960);
- }, [partnerId, width]);
+ }, [partnerId]); // eslint-disable-line react-hooks/exhaustive-depsThis would set the initial panel state based on current viewport when switching partners, without overriding manual toggles during resizes.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| useEffect(() => { | |
| if (typeof width !== "number") return; | |
| setIsRightPanelOpen(width >= 960); | |
| }, [partnerId, width]); | |
| useEffect(() => { | |
| if (typeof width !== "number") return; | |
| setIsRightPanelOpen(width >= 960); | |
| }, [partnerId]); // eslint-disable-line react-hooks/exhaustive-deps |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In
`@apps/web/app/app.dub.co/`(dashboard)/[slug]/(ee)/program/messages/[partnerId]/page-client.tsx
around lines 104 - 107, The effect that auto-sets panel open state should not
unconditionally run on every width change; update the logic in the useEffect
(the effect that currently reads width and calls setIsRightPanelOpen) to respect
a user toggle flag or to run only when partnerId changes: add a new state like
userHasToggled (initially false), set userHasToggled = true inside the manual
toggle handler that calls setIsRightPanelOpen, and change the effect to only
auto-set when userHasToggled is false (or change its dependency array to only
[partnerId] so it runs on partner change only); reference the existing
useEffect, setIsRightPanelOpen, isRightPanelOpen, partnerId and width when
making the changes.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@apps/web/ui/layout/sidebar/app-sidebar-nav.tsx`:
- Around line 555-556: The first render treats unresolved width as desktop and
causes a one-frame flash; update the guard in app-sidebar-nav.tsx (the
isMobileDrawerViewport computation that uses width and isMobile from the
useMediaQuery hook) to optimistically assume mobile when width is undefined (or
otherwise render a skeleton) so that isMobileDrawerViewport becomes true on
initial render, which prevents currentArea from being null and avoids the
layout/data-fetch flash for routes like /program/messages, /program/campaigns,
and /program/payouts/success.
| const isMobileDrawerViewport = | ||
| typeof width === "number" ? width < 768 : isMobile; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Find the useMediaQuery hook definition
rg -n "useMediaQuery" --type=ts --type=tsx -A 5 -B 2 | head -100Repository: dubinc/dub
Length of output: 81
🏁 Script executed:
# Examine the specific file and context around lines 555-556
cat -n apps/web/ui/layout/sidebar/app-sidebar-nav.tsx | sed -n '545,570p'Repository: dubinc/dub
Length of output: 1049
🏁 Script executed:
# Look for the currentArea variable and its dependencies
rg -n "currentArea" apps/web/ui/layout/sidebar/app-sidebar-nav.tsx -B 2 -A 2Repository: dubinc/dub
Length of output: 1471
🏁 Script executed:
# Find useMediaQuery hook definition and where width/isMobile come from
rg -n "const.*useMediaQuery\|function useMediaQuery\|export.*useMediaQuery" --type=ts -A 10 | head -50Repository: dubinc/dub
Length of output: 36
🏁 Script executed:
# Search in the same file to see where width and isMobile are declared
cat -n apps/web/ui/layout/sidebar/app-sidebar-nav.tsx | head -100Repository: dubinc/dub
Length of output: 3737
🏁 Script executed:
# Look for imports and variable declarations in the file
cat -n apps/web/ui/layout/sidebar/app-sidebar-nav.tsx | grep -n "import\|width\|isMobile" | head -30Repository: dubinc/dub
Length of output: 1929
🏁 Script executed:
# Find the useMediaQuery hook implementation in `@dub/ui`
find . -type f -name "*.ts" -o -name "*.tsx" | xargs grep -l "export.*useMediaQuery\|function useMediaQuery" | head -10Repository: dubinc/dub
Length of output: 96
🏁 Script executed:
# Search more broadly for useMediaQuery definition
rg -n "useMediaQuery" --type=ts -A 20 | grep -A 20 "function\|export\|useState\|initialValue" | head -80Repository: dubinc/dub
Length of output: 4959
🏁 Script executed:
# Examine the useMediaQuery hook implementation
cat -n ./packages/ui/src/hooks/use-media-query.tsRepository: dubinc/dub
Length of output: 1738
Potential one-frame flash on mobile for focused program paths.
The useMediaQuery hook initializes with width = undefined and isMobile = false during SSR/hydration. On mobile clients visiting /program/messages/, /program/campaigns/, or /program/payouts/success, the first render evaluates isMobileDrawerViewport = false (since width is not yet a number), causing currentArea to be null. After the hook's useEffect runs and detects actual mobile dimensions, isMobileDrawerViewport flips to true and currentArea becomes "program", enabling data fetches and changing the sidebar layout. This causes a brief visual shift.
Consider initializing the guard optimistically (e.g., treat an unresolved width as mobile-like, or render a skeleton) if this flash is observable in practice.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@apps/web/ui/layout/sidebar/app-sidebar-nav.tsx` around lines 555 - 556, The
first render treats unresolved width as desktop and causes a one-frame flash;
update the guard in app-sidebar-nav.tsx (the isMobileDrawerViewport computation
that uses width and isMobile from the useMediaQuery hook) to optimistically
assume mobile when width is undefined (or otherwise render a skeleton) so that
isMobileDrawerViewport becomes true on initial render, which prevents
currentArea from being null and avoids the layout/data-fetch flash for routes
like /program/messages, /program/campaigns, and /program/payouts/success.
For both programs and partners a few issues were happening on smaller screens:
Program onlyWhen you click the menu button in the top left, it opens the nav panel, but everything is hidden until you click on the partner icon again, and it takes you back to the program overview. on larger screens this makes sense, but on mobile we need to show the full nav.CleanShot.2026-02-11.at.14.26.21.mp4
Summary by CodeRabbit