Skip to content

Add swipe to delete for messages#97

Merged
SamanSP1386 merged 8 commits intodevfrom
feature/POLY-78-add-swipe-to-delete-for-messages
Apr 19, 2026
Merged

Add swipe to delete for messages#97
SamanSP1386 merged 8 commits intodevfrom
feature/POLY-78-add-swipe-to-delete-for-messages

Conversation

@lheutchy
Copy link
Copy Markdown
Collaborator

@lheutchy lheutchy commented Apr 18, 2026

Linked Issues

Closes #POLY-78
Linear: POLY-78

Summary

This PR adds moderation and inbox-management actions for messaging, aligned with the listing report flow.

  • Inbox conversation rows support horizontal swipe actions:
  • Swipe left reveals Report + Delete (conversation-level)
  • Swipe right opens the conversation
  • Tapping outside closes any open swipe state
  • Removed the inbox “Swipe left to report or delete” tip banner from the header.
  • Deleting a conversation hides it from that user’s inbox until a new message is sent in that thread.
  • Reporting a conversation opens the listing-style report modal and hides that conversation from the reporter’s inbox.
  • After reporting a conversation, that listing is hidden from the reporter’s Home/Search feed.
    In conversation view:
  • Long press on received messages shows Report only
  • Own messages do not show per-message moderation actions
  • Reporting a message redirects the reporter back to Inbox.
  • Backend includes report/hide/delete conversation support with duplicate-report protection and rate limiting.

How to Test

Steps to verify locally:

  • npm run lint
  • npm run typecheck
  • npm test
  • Manual flow:
    1. npm run dev:backend (in terminal A)
    2. npm run dev (in terminal B)
    3. Verify the change: <describe expected behavior/screens>

Checklist

  • Tests added/updated (if applicable)
  • Lint/tests pass locally (npm run lint)
  • Docs updated (README/ADR/changelog if needed)
  • Follows conventional commit format
  • No merge conflicts with dev

Screenshots / Demos

image image

Summary by CodeRabbit

Release Notes

  • New Features
    • Added ability to report and delete conversations directly from inbox with swipe actions
    • Added ability to report and delete individual messages within conversations
    • Reported conversations and messages are now hidden from the reporter's inbox while remaining visible to other users

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 18, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
poly-buys Ready Ready Preview, Comment Apr 19, 2026 0:11am

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 18, 2026

Warning

Rate limit exceeded

@lheutchy has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 24 minutes and 38 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 24 minutes and 38 seconds.

⌛ 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.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 05ed4904-e2b8-45bc-96fe-0382da8acb4f

📥 Commits

Reviewing files that changed from the base of the PR and between 2b7b428 and 29a5c50.

⛔ Files ignored due to path filters (1)
  • backend/convex/_generated/api.d.ts is excluded by !**/_generated/**
📒 Files selected for processing (8)
  • backend/convex/__tests__/listings.test.ts
  • backend/convex/__tests__/messages.test.ts
  • backend/convex/lib/reportedConversationListings.ts
  • backend/convex/listings.ts
  • backend/convex/messages.ts
  • frontend/app/(tabs)/inbox.tsx
  • frontend/app/conversations/[id].tsx
  • frontend/components/ReportModal.tsx
📝 Walkthrough

Walkthrough

Adds comprehensive conversation and message reporting, inbox hiding, and message deletion functionality across backend and frontend, including new mutations/queries, schema updates, and swipeable UI components for managing reported content.

Changes

Cohort / File(s) Summary
Test Suites
backend/convex/__tests__/listings.test.ts, backend/convex/__tests__/messages.test.ts
Added test coverage for listing exclusion when reported and comprehensive message/conversation reporting, deletion, hiding, and duplicate-report prevention behaviors.
Backend Reporting & Hiding Logic
backend/convex/messages.ts
Added report/hide/delete mutations (reportMessage, reportConversation, deleteMessage, hideConversationFromInbox), query getReportedConversationListingIds, constants/validators for report reasons/limits, helpers to preserve/clear hidden states on new messages, and updated internalSendMessage/listUserConversations to respect conversation hiding.
Backend Listing Filtering
backend/convex/listings.ts
Added getReportedConversationListingIdSetForViewer helper and updated searchAndFilterListings/getListings/searchListings to exclude reported listings from results.
Database Schema
backend/convex/schema.ts
Extended reports.targetType to include 'conversation' and 'message'; added optional buyerInboxHiddenAt, sellerInboxHiddenAt, buyerInboxHiddenReason, sellerInboxHiddenReason fields to conversations table with reason values `'deleted'
Inbox UI
frontend/app/(tabs)/inbox.tsx
Refactored conversation rows to support swipe gestures with inline "Report" and "Delete" actions; integrated hideConversationFromInbox mutation with confirmation and ReportModal for reporting.
Conversation Message UI
frontend/app/conversations/[id].tsx
Introduced SwipeableMessageRow component with long-press action panel for non-sent messages; integrated reportMessage mutation via modal, message deletion tracking, and keyboard/panel dismiss behavior on background tap.
Report Modal
frontend/components/ReportModal.tsx
Extended ReportModal to support 'conversation' and 'message' target types; conditionally routes to new backend mutations (reportConversation, reportMessage) or existing createReport; updated title text for new target types.

Sequence Diagram(s)

sequenceDiagram
    participant User as User (Reporter)
    participant UI as Frontend
    participant Backend as Backend
    participant DB as Database
    
    User->>UI: Report message/conversation
    UI->>UI: Open ReportModal (reason, notes)
    User->>UI: Confirm report
    UI->>Backend: reportMessage/reportConversation<br/>(with notes, reason)
    Backend->>DB: Validate: not duplicate, rate limit ok,<br/>caller not sender/owner
    DB-->>Backend: ✓ Valid
    Backend->>DB: Insert report record
    DB-->>Backend: reportId
    Backend->>DB: Hide conversation (reason='reported')<br/>for reporter side
    DB-->>Backend: ✓ Updated
    Backend-->>UI: { reportId }
    UI->>UI: Navigate/dismiss
    Note over Backend,DB: Conversation now hidden in reporter's<br/>listUserConversations & excluded<br/>from listings search results
Loading
sequenceDiagram
    participant User1 as User1 (Inbox)
    participant User2 as User2 (Other)
    participant UI as Frontend
    participant Backend as Backend
    participant DB as Database
    
    User1->>UI: Swipe conversation left,<br/>tap "Delete"
    UI->>UI: Alert confirmation
    User1->>UI: Confirm
    UI->>Backend: hideConversationFromInbox<br/>(reason='deleted')
    Backend->>DB: Patch conversation:<br/>buyerInboxHiddenAt/Reason
    DB-->>Backend: ✓ Updated
    Backend-->>UI: { ok: true }
    UI->>UI: Remove from inbox view
    
    Note over User1,DB: Conversation hidden for User1 only
    
    User2->>Backend: sendMessage to User1
    Backend->>DB: Fetch conversation
    DB-->>Backend: Conversation with hidden flags
    Backend->>Backend: Check hidden reason,<br/>if 'deleted' preserve, else clear
    Backend->>DB: Clear hidden fields if not 'reported'<br/>Update lastMessageId
    DB-->>Backend: ✓ Updated
    Note over Backend,DB: If reason was 'deleted',<br/>inbox stays hidden for User1.<br/>If was 'reported', clears<br/>and conversation reappears
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 Report and hide! A whisker-twitching delight,
Conversations vanish from the inbox sight,
Messages deleted, cleaner chats unfold,
The finest moderation system, I'm told! 🌿✨

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Title check ⚠️ Warning The PR title 'Add swipe to delete for messages' is partially accurate but incomplete—the PR actually implements a broader set of messaging moderation features including swipe-to-report for conversations, message-level reporting, inbox hiding, and backend report/delete support, not just message deletion. Revise the title to capture the full scope, such as 'Add conversation/message reporting and inbox management with swipe actions' or 'Implement messaging moderation with swipe actions and conversation hiding'.
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.
✅ Passed checks (1 passed)
Check name Status Explanation
Description check ✅ Passed The PR description covers linked issues, comprehensive summary of inbox/conversation/message-level changes, manual testing flow, checklist, and screenshots, closely following the template structure with all key sections present.

✏️ 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 feature/POLY-78-add-swipe-to-delete-for-messages

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
Contributor

@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: 5

🧹 Nitpick comments (5)
backend/convex/messages.ts (2)

588-623: Consider consolidating with listings.ts helper.

This query duplicates getReportedConversationListingIdSetForViewer in backend/convex/listings.ts (lines 67‑107). Besides DRY, there's a subtle divergence: the listings helper normalizeIds each targetId before looking it up, while this query casts directly — an unnormalized/foreign ID would silently propagate here but be skipped there. Extracting a single helper (or having listings.ts call this query's underlying function) keeps both paths in lockstep.

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

In `@backend/convex/messages.ts` around lines 588 - 623,
getReportedConversationListingIds duplicates logic from
getReportedConversationListingIdSetForViewer in listings.ts; extract a shared
helper (e.g., getReportedConversationListingIdsHelper) that accepts ctx and
reporter userId, reuse it from both getReportedConversationListingIds and
getReportedConversationListingIdSetForViewer, and ensure the helper normalizes
each report.targetId via normalizeId before looking up ctx.db.get so
unnormalized/foreign IDs aren't silently propagated; update both functions to
call the new helper and remove the duplicated mapping/lookup logic.

920-1018: deleteMessage/reportMessage flows look correct; small nit.

Authorization (senderId === userId for delete, senderId !== userId for report), duplicate‑report guard via by_target index, rate limiting, and lastMessageId restoration via updateConversationAfterDeletedMessage all look good. One nit: reportMessage passes no siblingConversationIds and therefore only hides the primary conversation, while reportConversation hides all siblings — worth a one‑line comment explaining the intentional asymmetry so future readers don't "fix" it.

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

In `@backend/convex/messages.ts` around lines 920 - 1018, Add a one-line inline
comment in the reportMessage flow (near the call to
hideConversationForParticipant inside the reportMessage handler) explaining that
we intentionally do not pass siblingConversationIds here and therefore only hide
the primary conversation, unlike reportConversation/hideConversationFromInbox
which hides siblings via siblingConversationIds; reference the functions
reportMessage, reportConversation, hideConversationForParticipant, and
hideConversationFromInbox and the arg siblingConversationIds so future readers
understand the asymmetry is intentional.
backend/convex/listings.ts (1)

67-107: Duplicated with getReportedConversationListingIds in messages.ts.

This helper and backend/convex/messages.ts lines 588‑623 derive the same reporter→listing‑ID set by the same algorithm (read reports by reporter, keep targetType === 'conversation', map through conversations.listingId). The only difference is Set vs array and the normalizeId call. Consider exporting a single shared helper (e.g. from messages.ts) so the server query and the filtering path can't drift. Especially important because the listings helper uses normalizeId but the exported query uses a raw cast — subtle behavioral inconsistency.

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

In `@backend/convex/listings.ts` around lines 67 - 107, These two functions
duplicate the same reporter→listing-ID logic:
getReportedConversationListingIdSetForViewer (in listings.ts) and
getReportedConversationListingIds (in messages.ts); extract the shared behavior
into a single exported helper (e.g., export
getReportedConversationListingIdsShared) that performs the reports query
(ctx.db.query('reports').withIndex('by_reporter', q => q.eq('reporterId',
viewerId))), filters targetType === 'conversation', calls
ctx.db.normalizeId('conversations', report.targetId) for each report, loads
conversations with ctx.db.get, and returns a Set or array consistently; then
replace both getReportedConversationListingIdSetForViewer and
getReportedConversationListingIds to call that shared helper so normalization
and return type are unified.
frontend/app/conversations/[id].tsx (1)

41-113: Component name doesn't match behavior.

SwipeableMessageRow doesn't implement any swipe gesture — it only wires long‑press to reveal a Report button. Consider renaming to e.g. MessageRowWithActions / ReportableMessageRow to avoid misleading future readers (especially given inbox uses actual swipe gestures for the same feature).

🤖 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 41 - 113, The component is
misnamed—rename the function and any exports from SwipeableMessageRow to a
clearer name like ReportableMessageRow (or MessageRowWithActions), keep the
props and behavior unchanged, and update all import sites to use the new
identifier (including story/tests/parent components that render it); also update
any TypeScript types or exported names that reference SwipeableMessageRow so the
build and type checks remain consistent.
frontend/components/ReportModal.tsx (1)

57-97: Submit branches look correct; minor consolidation opportunity.

Routing by targetType is clean and trimmedNotes is now shared. One small tweak: the same three { reason, notes: trimmedNotes } payload shape is duplicated across branches — you could factor the call into a single await per branch and keep everything else identical, e.g.:

♻️ Optional refactor
-      if (targetType === 'conversation') {
-        await reportConversation({
-          conversationId: targetId as Id<'conversations'>,
-          reason,
-          notes: trimmedNotes,
-        });
-      } else if (targetType === 'message') {
-        await reportMessage({
-          messageId: targetId as Id<'messages'>,
-          reason,
-          notes: trimmedNotes,
-        });
-      } else {
-        await createReport({
-          targetId,
-          targetType,
-          reason,
-          notes: trimmedNotes,
-        });
-      }
+      const common = { reason, notes: trimmedNotes };
+      if (targetType === 'conversation') {
+        await reportConversation({ conversationId: targetId as Id<'conversations'>, ...common });
+      } else if (targetType === 'message') {
+        await reportMessage({ messageId: targetId as Id<'messages'>, ...common });
+      } else {
+        await createReport({ targetId, targetType, ...common });
+      }

Not blocking.

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

In `@frontend/components/ReportModal.tsx` around lines 57 - 97, The handleSubmit
function has duplicated payload objects for reportConversation, reportMessage,
and createReport calls with the same shape { reason, notes: trimmedNotes }.
Refactor this by creating a single payload object containing reason and
trimmedNotes before the if statement, then await the appropriate reporting
function with this shared payload. This reduces duplication and keeps the
branching logic focused only on calling the correct function based on
targetType.
🤖 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/listings.ts`:
- Around line 200-202: The current logic filters reported listings after
paginate() in searchAndFilterListings and getListings which can shrink returned
pages and leave continueCursor/isDone inconsistent; fix by detecting when
reportedConversationListingIdSet.size > 0 and route those calls into the
existing “collect up to MAX_COLLECT” post-filtering path (the branch that
gathers candidates until MAX_COLLECT before computing pagination) instead of the
fast non-post-filter branch, so you paginate over the already-filtered/collected
items (update usage around paginate(), paginationResult, and
continueCursor/isDone accordingly) — apply the same change to the other
occurrences you noted (lines around 359-368 and 473-482).

In `@backend/convex/messages.ts`:
- Around line 948-965: The handler hideConversationFromInbox currently calls
hideConversationForParticipant(..., 'deleted', now) which unconditionally
overwrites any existing hide reason; update the logic in
hideConversationFromInbox (or inside hideConversationForParticipant) to first
fetch the participant's current hidden reason and skip calling the overwrite
when that reason === 'reported' so a prior 'reported' state is preserved (ensure
the check uses the same identity for 'reported' and keep now and other args
unchanged); reference hideConversationFromInbox and
hideConversationForParticipant when locating the change and ensure
shouldPreserveHiddenConversationState semantics remain intact.

In `@frontend/app/`(tabs)/inbox.tsx:
- Around line 159-212: The pan responder can read a stale swipeStartOffset
because translateX.stopAnimation(cb) calls back asynchronously; update
onPanResponderGrant to synchronously snapshot the current animated value into
swipeStartOffset.current (e.g., read the current value from the Animated.Value
instance or maintain a mirrored ref via translateX.addListener on mount) instead
of relying only on the stopAnimation callback, so subsequent onPanResponderMove
uses the correct start offset (update references in onPanResponderGrant and
ensure the addListener teardown if you choose the listener approach).

In `@frontend/app/conversations/`[id].tsx:
- Around line 639-647: The Report action label currently forces fontSize: 8
inside the messageActionLabel style (spreading typography.footnoteMed then
overriding), which makes the uppercase label unreadable; remove the fontSize
override or change it to a readable size (around 11–12) so the style uses
typography.footnoteMed’s base size or a sensible bump, and keep other properties
(fontWeight, letterSpacing, textTransform) intact—update the messageActionLabel
definition accordingly.
- Around line 432-476: The FlatList currently sets scrollEnabled={false} which
prevents users from scrolling message history and prevents onScrollBeginDrag
from firing; remove the scrollEnabled prop from the FlatList (or change it to
scrollEnabled={activeMessageActionId === null} if you intentionally want to
disable scrolling only while an action panel is active) so that manual scrolling
and onScrollBeginDrag used to clear activeMessageActionId work correctly; look
for FlatList, scrollEnabled, activeMessageActionId, and onScrollBeginDrag in
this component to make the change.

---

Nitpick comments:
In `@backend/convex/listings.ts`:
- Around line 67-107: These two functions duplicate the same reporter→listing-ID
logic: getReportedConversationListingIdSetForViewer (in listings.ts) and
getReportedConversationListingIds (in messages.ts); extract the shared behavior
into a single exported helper (e.g., export
getReportedConversationListingIdsShared) that performs the reports query
(ctx.db.query('reports').withIndex('by_reporter', q => q.eq('reporterId',
viewerId))), filters targetType === 'conversation', calls
ctx.db.normalizeId('conversations', report.targetId) for each report, loads
conversations with ctx.db.get, and returns a Set or array consistently; then
replace both getReportedConversationListingIdSetForViewer and
getReportedConversationListingIds to call that shared helper so normalization
and return type are unified.

In `@backend/convex/messages.ts`:
- Around line 588-623: getReportedConversationListingIds duplicates logic from
getReportedConversationListingIdSetForViewer in listings.ts; extract a shared
helper (e.g., getReportedConversationListingIdsHelper) that accepts ctx and
reporter userId, reuse it from both getReportedConversationListingIds and
getReportedConversationListingIdSetForViewer, and ensure the helper normalizes
each report.targetId via normalizeId before looking up ctx.db.get so
unnormalized/foreign IDs aren't silently propagated; update both functions to
call the new helper and remove the duplicated mapping/lookup logic.
- Around line 920-1018: Add a one-line inline comment in the reportMessage flow
(near the call to hideConversationForParticipant inside the reportMessage
handler) explaining that we intentionally do not pass siblingConversationIds
here and therefore only hide the primary conversation, unlike
reportConversation/hideConversationFromInbox which hides siblings via
siblingConversationIds; reference the functions reportMessage,
reportConversation, hideConversationForParticipant, and
hideConversationFromInbox and the arg siblingConversationIds so future readers
understand the asymmetry is intentional.

In `@frontend/app/conversations/`[id].tsx:
- Around line 41-113: The component is misnamed—rename the function and any
exports from SwipeableMessageRow to a clearer name like ReportableMessageRow (or
MessageRowWithActions), keep the props and behavior unchanged, and update all
import sites to use the new identifier (including story/tests/parent components
that render it); also update any TypeScript types or exported names that
reference SwipeableMessageRow so the build and type checks remain consistent.

In `@frontend/components/ReportModal.tsx`:
- Around line 57-97: The handleSubmit function has duplicated payload objects
for reportConversation, reportMessage, and createReport calls with the same
shape { reason, notes: trimmedNotes }. Refactor this by creating a single
payload object containing reason and trimmedNotes before the if statement, then
await the appropriate reporting function with this shared payload. This reduces
duplication and keeps the branching logic focused only on calling the correct
function based on targetType.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f2f5015b-162c-4259-bc7c-7e00f0d2fd1d

📥 Commits

Reviewing files that changed from the base of the PR and between 5dfa982 and 2b7b428.

📒 Files selected for processing (8)
  • backend/convex/__tests__/listings.test.ts
  • backend/convex/__tests__/messages.test.ts
  • backend/convex/listings.ts
  • backend/convex/messages.ts
  • backend/convex/schema.ts
  • frontend/app/(tabs)/inbox.tsx
  • frontend/app/conversations/[id].tsx
  • frontend/components/ReportModal.tsx

Comment thread backend/convex/listings.ts
Comment thread backend/convex/messages.ts
Comment thread frontend/app/(tabs)/inbox.tsx
Comment thread frontend/app/conversations/[id].tsx
Comment thread frontend/app/conversations/[id].tsx
Copy link
Copy Markdown
Collaborator

@SamanSP1386 SamanSP1386 left a comment

Choose a reason for hiding this comment

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

Nice job

@SamanSP1386 SamanSP1386 merged commit 7ebf98b into dev Apr 19, 2026
5 checks passed
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