Build messaging & inbox UI & message seller button#52
Conversation
|
Warning Rate limit exceeded
⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (8)
📝 WalkthroughWalkthroughAdds alias-aware participant handling and richer messaging flows backend-side, new messaging tests, and frontend inbox/conversation UI with unread badges, conversation routing, and message composer/mark-read behaviors. Changes
Sequence Diagram(s)sequenceDiagram
participant Client as Client (UI)
participant Frontend as Frontend Router/UI
participant API as Convex Backend
participant DB as Database
participant Storage as Storage Service
Client->>Frontend: Open Inbox
Frontend->>API: listUserConversations(authSubject)
activate API
API->>DB: Query conversations for participant keys (including aliases)
DB-->>API: Conversations
API->>DB: Fetch partner profiles & listings
DB-->>API: Profiles & listings
API->>Storage: Resolve local listing thumbnails
Storage-->>API: Resolved URLs
API-->>Frontend: Enriched conversation list (unread counts, thumbnails)
deactivate API
Frontend->>Client: Render Inbox rows with badges
sequenceDiagram
participant User as User (UI)
participant Frontend as Conversation Screen
participant API as Convex Backend
participant Moderation as Moderation Service
participant DB as Database
User->>Frontend: Send message(content)
Frontend->>Frontend: Optimistic UI update
Frontend->>API: sendMessage(content, senderKey, recipientKey)
activate API
API->>Moderation: Moderate content
Moderation-->>API: result
alt allowed
API->>DB: internalSendMessage -> persist message, update conversation metadata
DB-->>API: persisted message
else blocked
API-->>Frontend: moderation error
end
deactivate API
API-->>Frontend: confirm message persisted
Frontend->>User: Update success / error
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (3 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.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
backend/convex/messages.ts (1)
439-443:⚠️ Potential issue | 🟠 MajorDon't update
updatedAtwhen only the read cursor changes.
listUserConversationssorts byupdatedAt, so reading a thread currently moves it to the top even when no new message arrived.🛠️ Suggested fix
- if (isBuyer) { - await ctx.db.patch(convo._id, { buyerLastReadAt: now, updatedAt: now }); - } else if (isSeller) { - await ctx.db.patch(convo._id, { sellerLastReadAt: now, updatedAt: now }); - } + if (isBuyer) { + await ctx.db.patch(convo._id, { buyerLastReadAt: now }); + } else if (isSeller) { + await ctx.db.patch(convo._id, { sellerLastReadAt: now }); + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/convex/messages.ts` around lines 439 - 443, The current read-cursor update in messages.ts is incorrectly updating updatedAt (via ctx.db.patch on convo._id), which causes listUserConversations to reorder threads when a user simply reads them; change the ctx.db.patch calls that set buyerLastReadAt and sellerLastReadAt to only update those fields (buyerLastReadAt or sellerLastReadAt) and omit updatedAt so reads don't modify the conversation's updatedAt timestamp. Ensure any other paths that should bump updatedAt (e.g., message creation) remain unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@backend/convex/messages.ts`:
- Around line 246-259: The current dedup logic in the loop that builds
dedupedConversationMap (iterating participantConversations and keying by
`${conversation.listingId}:${canonicalParticipantId(otherUserId)}`) drops older
sibling threads and thus loses messages/unread state; instead, collect all
conversations per dedupeKey (e.g., map to arrays of Doc<'conversations'>), then
for each group merge or aggregate the needed fields before returning: compute a
combined updatedAt = max(...updatedAt), aggregate unread/preview data (sum or
choose per UX rules), and backfill or stitch message history/lastMessage from
all siblings so you don't discard older messages; update the code paths that
consume conversations to use the merged/aggregated conversation objects rather
than a single newest document.
- Around line 236-244: listUserConversations currently scans all conversations
and profiles using getParticipantKeys and matchesAnyParticipantId; change it to
query the conversations table by the buyer and seller indexes (use
ctx.db.query('conversations').index('byBuyerId', ...) and .index('bySellerId',
...) or your equivalent index names) to fetch only conversations for the current
participant keys, dedupe the returned conversation IDs, then enrich those IDs
(fetch conversations and associated profiles) rather than filtering the full
table; also stop running a per-conversation "latest message" query by using the
conversation.lastMessageId field to fetch the latest message in batch for the
deduped conversations. Ensure you update any related code in the same function
block (and the similar logic around lines 261-299) to use these indexed queries,
dedup ids, and batch message fetches.
In `@frontend/app/`(tabs)/inbox.tsx:
- Around line 53-60: ConversationRow currently does an unnecessary per-row fetch
via useQuery(api.listings.getListing) and calls useResolvedImageUrls, creating
an N+1 query; remove that fetch and resolution and use the data already returned
by listUserConversations (item.listing.title and item.listing.thumbnailUrl).
Replace references to listingId, listing, listingTitle, thumbnailSource,
useQuery(...getListing...), and useResolvedImageUrls so listingTitle =
item.listing?.title ?? 'Listing unavailable' and thumbnail =
item.listing?.thumbnailUrl ?? null (use thumbnail directly where mappedUrls[0]
was used), and delete the useQuery and mappedUrls usage from ConversationRow.
Ensure types still accept null and no new API calls are introduced.
---
Outside diff comments:
In `@backend/convex/messages.ts`:
- Around line 439-443: The current read-cursor update in messages.ts is
incorrectly updating updatedAt (via ctx.db.patch on convo._id), which causes
listUserConversations to reorder threads when a user simply reads them; change
the ctx.db.patch calls that set buyerLastReadAt and sellerLastReadAt to only
update those fields (buyerLastReadAt or sellerLastReadAt) and omit updatedAt so
reads don't modify the conversation's updatedAt timestamp. Ensure any other
paths that should bump updatedAt (e.g., message creation) remain unchanged.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 68edb253-51fa-4244-abd5-2846e06aa94a
📒 Files selected for processing (8)
backend/convex/__tests__/messages.test.tsbackend/convex/messages.tsfrontend/app/(tabs)/_layout.tsxfrontend/app/(tabs)/inbox.tsxfrontend/app/_layout.tsxfrontend/app/conversations/[id].tsxfrontend/app/listings/[id].tsxfrontend/app/messages/[id].tsx
SamanSP1386
left a comment
There was a problem hiding this comment.
Check out the code Rabbit, there is some issues.
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
backend/convex/messages.ts (1)
126-132:⚠️ Potential issue | 🟡 MinorReject whitespace-only messages.
args.body.length === 0still allows" "and"\n"through, so this endpoint can persist visually empty chat messages. Trim before the emptiness check.Suggested change
+ const trimmedBody = args.body.trim(); - if (args.body.length === 0) { + if (trimmedBody.length === 0) { throw new ConvexError('Message cannot be empty'); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/convex/messages.ts` around lines 126 - 132, The current emptiness check uses args.body.length which allows whitespace-only strings; update the validation in the message handler (backend/convex/messages.ts) to compute a trimmedBody = args.body.trim() and use trimmedBody.length === 0 to throw the ConvexError('Message cannot be empty'), while keeping the maximum-length check against PAYLOAD_BOUNDS.MESSAGE_MAX (e.g., use args.body.length or trimmedBody.length per your intended policy) so whitespace-only content is rejected before persisting.
♻️ Duplicate comments (1)
backend/convex/messages.ts (1)
305-325:⚠️ Potential issue | 🟠 MajorGrouped alias threads are still only addressable by one conversation ID.
Line 487 spreads
primaryConversation, so the inbox row still carries a single_ideven though Line 489 exposes merged sibling IDs and the preview/unread state is aggregated across them. The downstream history/read APIs in this module still operate on a singleconversationId, so opening a grouped row can miss older sibling messages and leave their unread state behind. Either merge/backfill these sibling documents into one canonical conversation or add merged history/read operations keyed by the full sibling set.Also applies to: 486-505
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@backend/convex/messages.ts` around lines 305 - 325, The groupedConversations builder currently returns a primaryConversation (spread into inbox rows) but keeps siblingConversations separate, so downstream handlers (history/read APIs) that use primaryConversation._id miss messages/unread state from others; update this code path to surface the full sibling ID set and a canonical merged ID: include siblingConversationIds (e.g., siblingConversations.map(c=>c._id)) and a mergedConversationId or canonicalOtherUserId in the returned object instead of relying solely on primaryConversation._id, then update the history/read functions (the modules that consume groupedConversations) to accept and operate on either the mergedConversationId or the array siblingConversationIds (fetch/merge/backfill messages and unread counts across all IDs) so opening a grouped row aggregates messages/unread state from all siblings.
🧹 Nitpick comments (2)
frontend/app/(tabs)/inbox.tsx (2)
200-200: ExtractItemSeparatorComponentto a stable reference.Inline arrow functions for
ItemSeparatorComponentcreate a new component reference on every render, which can affect FlatList optimization.♻️ Suggested fix
Define outside the component or use a stable reference:
+const ItemSeparator = () => <View style={styles.separator} />; + export default function InboxScreen() { // ... }Then use it:
- ItemSeparatorComponent={() => <View style={styles.separator} />} + ItemSeparatorComponent={ItemSeparator}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/app/`(tabs)/inbox.tsx at line 200, The inline arrow passed to ItemSeparatorComponent creates a new component each render; extract it to a stable reference by defining a named component or memoized callback (e.g., create a function/component ItemSeparator that returns <View style={styles.separator} /> outside the main component or use useCallback/useMemo to return that element) and then pass ItemSeparator (or the memoized reference) to the FlatList's ItemSeparatorComponent prop to avoid recreating the component on every render.
123-137: WrapformatTimestampinuseCallbackto stabilize reference.The function is passed as a prop to
ConversationRowbut recreated on every render, potentially causing unnecessary child re-renders. The formatters it depends on are already memoized.♻️ Suggested fix
- const formatTimestamp = (timestamp: number) => { + const formatTimestamp = useCallback((timestamp: number) => { const value = new Date(timestamp); const now = new Date(); const sameDay = value.toDateString() === now.toDateString(); if (sameDay) { return formatter.format(value); } const diff = now.getTime() - value.getTime(); if (diff < 1000 * 60 * 60 * 24 * 7) { return weekdayFormatter.format(value); } return shortDateFormatter.format(value); - }; + }, [formatter, weekdayFormatter, shortDateFormatter]);Also add
useCallbackto the import on line 1:-import { useMemo } from 'react'; +import { useCallback, useMemo } from 'react';🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/app/`(tabs)/inbox.tsx around lines 123 - 137, The formatTimestamp function is recreated each render and passed to ConversationRow; wrap it in React's useCallback and add useCallback to the import list so its reference is stable. Change formatTimestamp declaration to useCallback(() => { ... }, [formatter, weekdayFormatter, shortDateFormatter]) (include timestamp as parameter in the callback signature) so the dependency array references the existing memoized formatters, and then pass that stable callback to ConversationRow.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@backend/convex/messages.ts`:
- Around line 29-63: The displayNameFromProfile function currently derives names
from emails and fallbackUserId which can leak private identifiers; update
displayNameFromProfile to only use explicit public display fields (user?.name
and profile?.name after trimming) and never return parts of user.email,
profile.email, or fallbackUserId; if neither name is present return the generic
'User'. Ensure you remove or disable the branches that inspect email or
fallbackUserId and keep trimming/length checks for user?.name and profile?.name
(references: displayNameFromProfile, profile.name, user.name, user.email,
profile.email, fallbackUserId).
---
Outside diff comments:
In `@backend/convex/messages.ts`:
- Around line 126-132: The current emptiness check uses args.body.length which
allows whitespace-only strings; update the validation in the message handler
(backend/convex/messages.ts) to compute a trimmedBody = args.body.trim() and use
trimmedBody.length === 0 to throw the ConvexError('Message cannot be empty'),
while keeping the maximum-length check against PAYLOAD_BOUNDS.MESSAGE_MAX (e.g.,
use args.body.length or trimmedBody.length per your intended policy) so
whitespace-only content is rejected before persisting.
---
Duplicate comments:
In `@backend/convex/messages.ts`:
- Around line 305-325: The groupedConversations builder currently returns a
primaryConversation (spread into inbox rows) but keeps siblingConversations
separate, so downstream handlers (history/read APIs) that use
primaryConversation._id miss messages/unread state from others; update this code
path to surface the full sibling ID set and a canonical merged ID: include
siblingConversationIds (e.g., siblingConversations.map(c=>c._id)) and a
mergedConversationId or canonicalOtherUserId in the returned object instead of
relying solely on primaryConversation._id, then update the history/read
functions (the modules that consume groupedConversations) to accept and operate
on either the mergedConversationId or the array siblingConversationIds
(fetch/merge/backfill messages and unread counts across all IDs) so opening a
grouped row aggregates messages/unread state from all siblings.
---
Nitpick comments:
In `@frontend/app/`(tabs)/inbox.tsx:
- Line 200: The inline arrow passed to ItemSeparatorComponent creates a new
component each render; extract it to a stable reference by defining a named
component or memoized callback (e.g., create a function/component ItemSeparator
that returns <View style={styles.separator} /> outside the main component or use
useCallback/useMemo to return that element) and then pass ItemSeparator (or the
memoized reference) to the FlatList's ItemSeparatorComponent prop to avoid
recreating the component on every render.
- Around line 123-137: The formatTimestamp function is recreated each render and
passed to ConversationRow; wrap it in React's useCallback and add useCallback to
the import list so its reference is stable. Change formatTimestamp declaration
to useCallback(() => { ... }, [formatter, weekdayFormatter, shortDateFormatter])
(include timestamp as parameter in the callback signature) so the dependency
array references the existing memoized formatters, and then pass that stable
callback to ConversationRow.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 8556db8a-8e53-47e1-8f23-2aa8fea8df8c
📒 Files selected for processing (3)
backend/convex/__tests__/messages.test.tsbackend/convex/messages.tsfrontend/app/(tabs)/inbox.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
- backend/convex/tests/messages.test.ts
There was a problem hiding this comment.
🧹 Nitpick comments (1)
frontend/app/conversations/[id].tsx (1)
24-42: Consider extracting shared participant ID utilities.The
canonicalParticipantId,isSameParticipantId, andmatchesAnyParticipantIdfunctions duplicate the backend implementations inbackend/convex/messages.ts. Consider extracting these to a shared package to ensure consistency and reduce maintenance burden.Additionally,
formatMessageTimestampcreates a newIntl.DateTimeFormatinstance on each call. While not critical for message rendering frequency, memoizing the formatter (as done ininbox.tsx) would be more efficient.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/app/conversations/`[id].tsx around lines 24 - 42, The local participant-ID helpers (canonicalParticipantId, isSameParticipantId, matchesAnyParticipantId) duplicate backend logic—extract them into a shared utility module and replace the local implementations with imports from that shared module (create and export canonicalParticipantId/isSameParticipantId/matchesAnyParticipantId there so both backend and frontend reuse the same logic). Also avoid recreating Intl.DateTimeFormat on each call in formatMessageTimestamp: create a module-level memoized/singleton formatter (e.g., messageTimestampFormatter = new Intl.DateTimeFormat(...)) like inbox.tsx, and have formatMessageTimestamp call that formatter.format(new Date(timestamp)).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@frontend/app/conversations/`[id].tsx:
- Around line 24-42: The local participant-ID helpers (canonicalParticipantId,
isSameParticipantId, matchesAnyParticipantId) duplicate backend logic—extract
them into a shared utility module and replace the local implementations with
imports from that shared module (create and export
canonicalParticipantId/isSameParticipantId/matchesAnyParticipantId there so both
backend and frontend reuse the same logic). Also avoid recreating
Intl.DateTimeFormat on each call in formatMessageTimestamp: create a
module-level memoized/singleton formatter (e.g., messageTimestampFormatter = new
Intl.DateTimeFormat(...)) like inbox.tsx, and have formatMessageTimestamp call
that formatter.format(new Date(timestamp)).
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 755782ae-6b3e-4d6e-87e5-9b2cf0550166
📒 Files selected for processing (4)
backend/convex/__tests__/messages.test.tsbackend/convex/messages.tsfrontend/app/(tabs)/inbox.tsxfrontend/app/conversations/[id].tsx
🚧 Files skipped from review as they are similar to previous changes (1)
- backend/convex/tests/messages.test.ts
2943a12 to
611259a
Compare
Linked Issues
Closes #39
Linear: POLY-39
Summary
Connected the messaging navigation flow:
Listing detail → chat
Inbox → chat
Added unread indicators/badge support for messaging UI.
Added message read status display in chat (Sent / Read).
How to Test
Steps to verify locally:
npm run lintnpm run typechecknpm testnpm run dev:backend(in terminal A)npm run dev(in terminal B)Checklist
npm run lint)devScreenshots / Demos
![Screenshot 2026-03-06 at 3 48 44 PM]()
e06da6658eda" />Summary by CodeRabbit
New Features
Bug Fixes
Tests