Skip to content

feat(inbox): add unified inbox view with DMs and channels - #327

Merged
2witstudios merged 6 commits into
masterfrom
dashboard-views
Feb 2, 2026
Merged

2witstudios merged 6 commits into
masterfrom
dashboard-views

Conversation

@2witstudios

@2witstudios 2witstudios commented Feb 2, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • Add unified inbox view aggregating direct messages and channel conversations across drives
  • Create InboxSidebar component showing recent conversations with search and pagination
  • Add DashboardFooter with quick action links (Tasks, Activity, Collaborators, Settings, Trash)
  • Add API endpoint (/api/inbox) supporting dashboard-wide and drive-specific views
  • Update sidebar routing to show appropriate sidebar based on current path

Changes

File Change
apps/web/src/app/api/inbox/route.ts New API route for unified inbox data
apps/web/src/app/dashboard/inbox/page.tsx Dashboard-level inbox page
apps/web/src/app/dashboard/[driveId]/inbox/page.tsx Drive-specific inbox page
apps/web/src/components/layout/left-sidebar/InboxSidebar.tsx Inbox navigation sidebar
apps/web/src/components/layout/left-sidebar/DashboardFooter.tsx Collapsible quick actions footer
apps/web/src/components/layout/left-sidebar/MemoizedSidebar.tsx Route-based sidebar switching
apps/web/src/components/layout/left-sidebar/index.tsx Add Inbox link + DashboardFooter
apps/web/src/components/shared/UserDropdown.tsx Remove items moved to DashboardFooter
apps/web/src/stores/useLayoutStore.ts Add dashboardFooterCollapsed state
apps/web/src/app/dashboard/layout.tsx Register inbox as full-page route

Test plan

  • Navigate to /dashboard/inbox and verify empty state displays
  • Create DMs and verify they appear in inbox sidebar
  • Navigate to /dashboard/[driveId]/inbox and verify channels display
  • Test search filtering in inbox sidebar
  • Test pagination (load more) with many conversations
  • Verify DashboardFooter collapses/expands and persists state
  • Test Cmd/Ctrl+click opens links in new tabs
  • Test on mobile breakpoint with sidebar sheet
  • Test on Electron Mac desktop app (stoplight button accommodation)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Introduced unified Inbox displaying direct messages and channels in one consolidated view with last message previews and unread counts.
    • Added Dashboard footer with quick-access links to Tasks, Activity, Collaborators, Settings, and Trash.
    • Enabled inbox search and filtering across conversations with cursor-based pagination.

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@coderabbitai

coderabbitai Bot commented Feb 2, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Rate limit exceeded

@2witstudios has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 11 minutes and 53 seconds before requesting another review.

⌛ How to resolve this issue?

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

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

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

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

Please see our FAQ for further information.

📝 Walkthrough

Walkthrough

This PR introduces a comprehensive inbox feature with a new API endpoint for fetching unified inbox items (direct messages and channels), new dashboard pages for inbox views, a sidebar component with pagination and search capabilities, and navigation updates to route users to the inbox interface.

Changes

Cohort / File(s) Summary
Inbox API Route
apps/web/src/app/api/inbox/route.ts
New GET handler to fetch paginated inbox items with support for drive-specific and dashboard-wide views; queries databases for channels with last messages, DMs with unread counts, and includes sender information and timestamps.
Inbox Pages
apps/web/src/app/dashboard/inbox/page.tsx, apps/web/src/app/dashboard/[driveId]/inbox/page.tsx
New client-side pages for dashboard-level and drive-specific inbox views; dashboard page shows static instructions, drive page includes loading skeleton and "Drive not found" state handling.
Inbox Sidebar & Routing
apps/web/src/components/layout/left-sidebar/InboxSidebar.tsx, apps/web/src/components/layout/left-sidebar/MemoizedSidebar.tsx
New InboxSidebar component fetches and displays inbox items with client-side search, pagination (cursor-based), unread counts, and last message previews; MemoizedSidebar routes to InboxSidebar when in inbox context.
Sidebar & Navigation Updates
apps/web/src/components/layout/left-sidebar/index.tsx, apps/web/src/components/layout/left-sidebar/DashboardFooter.tsx
Added Inbox navigation link (context-aware routing based on driveId), new DashboardFooter component with collapsible "User Actions" panel (Tasks, Activity, Collaborators, Settings, Trash) supporting new-tab opens and responsive sheet closing.
Layout & Layout Store Updates
apps/web/src/app/dashboard/layout.tsx, apps/web/src/stores/useLayoutStore.ts
Added inbox route to FULL_PAGE_ROUTES and updated route-matching regex; introduced dashboardFooterCollapsed state property to track footer collapse state with persistence.
Navigation & UI Refinements
apps/web/src/components/shared/UserDropdown.tsx, apps/web/src/components/layout/navbar/DriveSwitcher.tsx
Removed dashboard items (Messages, Connections, Activity, Tasks) from UserDropdown and added Account link to /settings/account; adjusted DriveSwitcher padding (px-3 to px-2).

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant InboxSidebar
    participant APIServer as /api/inbox
    participant Database
    
    User->>InboxSidebar: Navigate to inbox or mount
    InboxSidebar->>APIServer: GET /api/inbox?driveId=X&limit=20
    APIServer->>Database: Query channels with last messages
    APIServer->>Database: Query DMs with unread counts
    APIServer->>Database: Fetch sender info and timestamps
    Database-->>APIServer: Return aggregated results
    APIServer-->>InboxSidebar: Return paginated InboxItem[]
    InboxSidebar->>InboxSidebar: Normalize timestamps & render items
    InboxSidebar-->>User: Display inbox with last messages & unread counts
    
    User->>InboxSidebar: Scroll or type search query
    InboxSidebar->>InboxSidebar: Filter items client-side or fetch more (pagination)
    InboxSidebar-->>User: Update displayed items
    
    User->>InboxSidebar: Click conversation
    InboxSidebar->>User: Navigate to conversation page
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰 Messages flutter, conversations nest,
In an inbox where your chats find rest,
Paginate and search with rabbit grace,
Unread counts light up the space,
Organized chaos? Not a trace!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(inbox): add unified inbox view with DMs and channels' directly and clearly summarizes the main change—introducing a unified inbox that aggregates direct messages and channels.

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

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch dashboard-views

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.

Add a unified inbox view that aggregates direct messages and channel
conversations across all drives. The inbox sidebar shows recent activity
and provides quick access to all communication in one place.

Changes:
- Add Inbox link to main sidebar navigation
- Create InboxSidebar component for inbox-specific navigation
- Create DashboardFooter with quick action links (Tasks, Activity, etc.)
- Add API route for unified inbox with pagination
- Add inbox pages for dashboard and drive-specific views
- Update UserDropdown to remove items now in DashboardFooter
- Add dashboardFooterCollapsed state to layout store

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In `@apps/web/src/app/api/inbox/route.ts`:
- Around line 263-267: The hasMore calculation is unreliable because
filteredItems is bounded by the SQL LIMIT in the drive-specific path; change the
fetch logic to request limit + 1 items (so filteredItems can be one item larger
when there are more) then set hasMore = filteredItems.length > limit, set
paginatedItems = filteredItems.slice(0, limit) (so paginatedItems contains only
the real page) and compute nextCursor from the last element of paginatedItems if
any; update any code referencing hasMore, filteredItems, paginatedItems, limit,
and nextCursor accordingly.
- Around line 115-151: The DM query (dmResults, dm_data CTE and unread_counts)
currently ignores the incoming cursor parameter; modify the dm_data WHERE clause
to apply the same cursor filter used for channels so results are paginated
consistently — e.g., when a cursor is provided add a condition like AND
dd."lastMessageAt" < ${cursor} (matching the DESC ordering) or an equivalent
safe comparison (handle nulls if needed) so the final SELECT returns only
conversations before the cursor; keep the rest of the CTEs (unread_counts,
joins) intact.

In `@apps/web/src/components/layout/left-sidebar/InboxSidebar.tsx`:
- Around line 83-101: The loadMore function currently parses response JSON
without checking HTTP status; update the logic in loadMore (the block using
fetchWithAuth and the response variable) to verify response.ok before calling
response.json(), and handle non-ok cases by reading the error body/text and
throwing or logging a clear error so the catch block can run; ensure you don't
call setAllItems or setPagination when the response is an error and still clear
isLoadingMore in finally.
- Around line 114-119: The isItemActive function can produce false positives
because it uses pathname?.includes() to match IDs; change it to perform exact
path-segment matching instead (e.g., for dm items compare pathname to
`/messages/${item.id}` or ensure it starts with that plus a trailing slash, and
for other items compare the segment `/…/${item.id}` exactly or compare the last
path segment via pathname.split('/').pop()). Update isItemActive (and any usage
of InboxItem id matching) to use these exact equality/segment checks rather than
includes().
🧹 Nitpick comments (4)
apps/web/src/components/shared/UserDropdown.tsx (1)

106-109: Consider a more semantically appropriate icon for Account.

LayoutDashboard typically represents dashboard/grid layouts. For an "Account" menu item, consider using User or UserCircle from lucide-react for better semantic clarity.

💡 Suggested icon change
-import { LogOut, MessageSquareText, Settings, LayoutDashboard, Sun, Moon, Monitor, HardDrive, CreditCard } from 'lucide-react';
+import { LogOut, MessageSquareText, Settings, User, Sun, Moon, Monitor, HardDrive, CreditCard } from 'lucide-react';
 <DropdownMenuItem onClick={() => router.push('/settings/account')}>
-  <LayoutDashboard className="mr-2 h-4 w-4" />
+  <User className="mr-2 h-4 w-4" />
   <span>Account</span>
 </DropdownMenuItem>
apps/web/src/app/api/inbox/route.ts (1)

86-94: Duplicate ChannelRow interface definition.

The ChannelRow interface is defined identically in both the drive-specific branch (lines 86-94) and the dashboard branch (lines 215-223). Extract it to a single definition near the top of the file with InboxItem and DMRow.

♻️ Proposed refactor

Move the interface near line 20 (after InboxItem):

+interface ChannelRow {
+  id: string;
+  name: string;
+  drive_id: string;
+  drive_name: string;
+  last_message: string | null;
+  last_message_at: string | null;
+  sender_name: string | null;
+}
+
+interface DMRow {
+  id: string;
+  last_message_at: string | null;
+  last_message: string | null;
+  other_user_name: string;
+  other_user_display_name: string | null;
+  other_user_avatar_url: string | null;
+  unread_count: string;
+}

Then remove the duplicate definitions at lines 86-94, 153-161, and 215-223.

Also applies to: 215-223

apps/web/src/app/dashboard/[driveId]/inbox/page.tsx (1)

49-61: Suspense boundary provides limited value here.

The Suspense wrapper shows a skeleton fallback, but DriveInboxContent is a client component that handles its own loading state via isLoading from the store (lines 20-26). The Suspense boundary won't catch the store's async fetch.

This is fine to keep for consistency with other pages, but note it won't actually suspend during the fetchDrives() call.

apps/web/src/components/layout/left-sidebar/InboxSidebar.tsx (1)

68-70: Add revalidateOnFocus: false to SWR config.

Per coding guidelines, SWR should be configured with revalidateOnFocus: false to prevent unexpected refetches when the user returns to the tab.

♻️ Proposed fix
   const { data, error } = useSWR<InboxResponse>(apiUrl, fetcher, {
     refreshInterval: 10000,
+    revalidateOnFocus: false,
   });

As per coding guidelines: "Use SWR for server state and caching with proper configuration including revalidateOnFocus: false".

Comment thread apps/web/src/app/api/inbox/route.ts
Comment thread apps/web/src/app/api/inbox/route.ts
Comment thread apps/web/src/components/layout/left-sidebar/InboxSidebar.tsx
Comment thread apps/web/src/components/layout/left-sidebar/InboxSidebar.tsx
2witstudios and others added 3 commits February 2, 2026 09:08
…cking

- Fix Settings route in DashboardFooter (/dashboard/settings -> /settings)
- Fix hasMore pagination by fetching limit+1 rows
- Fix nextCursor null handling with composite cursor (id:<itemId>)
- Add channel_read_status schema for tracking unread messages
- Add channel read tracking endpoint (POST /api/channels/[pageId]/read)
- Add real-time socket broadcasts for DM and channel updates
- Replace polling with socket-based updates in InboxSidebar
- Add UI refresh protection during editing sessions
- Calculate actual channel unread counts from lastReadAt watermark

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add response.ok check in loadMore before parsing JSON
- Fix isItemActive to use exact path matching instead of includes()

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Remove unused channelReadStatus import from read route
- Remove unused useRef import from InboxSidebar
- Remove unused InboxItem import from useInboxSocket

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@2witstudios

Copy link
Copy Markdown
Owner Author

Code review

Found 3 issues:

  1. New conversations do not appear in real-time inbox. The socket update handler only updates existing items (if (existingIndex >= 0)) but never adds new conversations to the cache. When a user receives a message in a conversation not yet in their inbox, it won't appear until they refresh.

const updatedItems = [...currentData.items];
const existingIndex = updatedItems.findIndex(
item => item.id === payload.id && item.type === payload.type
);
if (existingIndex >= 0) {
// Update existing item
const existingItem = updatedItems[existingIndex];
updatedItems[existingIndex] = {
...existingItem,
lastMessageAt: payload.lastMessageAt || existingItem.lastMessageAt,
lastMessagePreview: payload.lastMessagePreview || existingItem.lastMessagePreview,
lastMessageSender: payload.lastMessageSender || existingItem.lastMessageSender,
unreadCount: payload.unreadCount !== undefined
? payload.unreadCount
: (payload.operation === 'read_status_changed' ? 0 : (existingItem.unreadCount + 1)),
};
}

  1. Dashboard inbox (non-driveId path) loads all DMs and channels into memory before pagination. Both SQL queries at lines 143-179 and 206-252 have no LIMIT clause, fetching everything then sorting in JavaScript. This will cause performance issues for users with many conversations.

LEFT JOIN unread_counts uc ON uc."conversationId" = dd.id
ORDER BY dd."lastMessageAt" DESC NULLS LAST
`);

  1. Inbox broadcasts sent to all drive members without page-level permission checks. The code fetches all drive members and sends message previews to each, but PageSpace supports page-level permissions. Users without canUserViewPage permission for a channel may receive message previews they should not see.

if (channel?.driveId) {
// Get all drive members
const members = await db.query.driveMembers.findMany({
where: eq(driveMembers.driveId, channel.driveId),
columns: { userId: true },
});
// Create message preview
const messagePreview = content.length > 100
? content.substring(0, 100) + '...'
: content;
// Broadcast to all members except the sender
const broadcastPromises = members
.filter(m => m.userId !== userId)
.map(member =>
broadcastInboxEvent(member.userId, {
operation: 'channel_updated',
type: 'channel',
id: pageId,
driveId: channel.driveId,
lastMessageAt: newMessage?.createdAt?.toISOString() || new Date().toISOString(),
lastMessagePreview: messagePreview,
lastMessageSender: newMessage?.user?.name || undefined,
})
);
await Promise.all(broadcastPromises);
}


Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

- Add page-level permission check before broadcasting channel updates
  to prevent message preview leakage to unauthorized users
- Add LIMIT to dashboard inbox queries to prevent unbounded memory usage
- Handle new conversations in socket hook by triggering SWR revalidation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@2witstudios

Copy link
Copy Markdown
Owner Author

Addressed code review issues in commit 378f46d

Issue 1: New conversations not appearing in real-time inbox

  • Fixed in apps/web/src/hooks/useInboxSocket.ts - When a socket event arrives for a conversation not in the cache, we now trigger a SWR revalidation to fetch the full data from the API.

Issue 2: Dashboard inbox unbounded queries

  • Fixed in apps/web/src/app/api/inbox/route.ts - Added LIMIT ${fetchLimit} to both the DM and channel queries in the dashboard inbox path to prevent loading all items into memory.

Issue 3: Permission check for inbox broadcasts

  • Fixed in apps/web/src/app/api/channels/[pageId]/messages/route.ts - Now checks canUserViewPage for each drive member before broadcasting channel updates. Only members with view permission receive the message preview.

Generated with Claude Code

- Add canUserViewPage check to inbox API route to filter channels
  by page-level permissions instead of just drive membership
- Update InboxSidebar to preserve loaded pages when SWR revalidates
  by merging first-page updates with previously loaded items
- Reset pagination state when driveId changes to avoid mixing contexts

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@2witstudios

Copy link
Copy Markdown
Owner Author

Additional fixes in commit 677fb29

Building on the earlier review feedback, this commit adds two more improvements:

1. Page permission filtering for inbox channels (P1 security fix)

  • Added canUserViewPage check to filter channels in the inbox API route
  • Channels are now filtered by page-level permissions, not just drive membership
  • Applied to both drive-specific and dashboard-wide queries
  • File: apps/web/src/app/api/inbox/route.ts

2. Preserve paginated data on SWR revalidation (P2 UX fix)

  • Fixed issue where "Load more" results were lost when SWR revalidated
  • Added hasLoadedMore state to track when additional pages have been loaded
  • When hasLoadedMore is true, incoming first-page data is merged with previously loaded items
  • Resets hasLoadedMore when switching drives to avoid mixing data
  • File: apps/web/src/components/layout/left-sidebar/InboxSidebar.tsx

Generated with Claude Code

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.

1 participant