Skip to content

feat(conversations): bulk spam/unspam and query optimizations#824

Merged
Israeltheminer merged 2 commits into
mainfrom
feat/bulk-spam-conversations
Mar 20, 2026
Merged

feat(conversations): bulk spam/unspam and query optimizations#824
Israeltheminer merged 2 commits into
mainfrom
feat/bulk-spam-conversations

Conversation

@Israeltheminer
Copy link
Copy Markdown
Collaborator

@Israeltheminer Israeltheminer commented Mar 20, 2026

Summary

  • Added bulk spam/unspam mutation with backend logic and comprehensive tests
  • Added spam status to conversation schema
  • Integrated spam actions into bulk action hooks and conversation panel UI
  • Optimized list_conversations_paginated and query_conversations queries
  • Updated conversation panel layout and translations

Test plan

  • Unit tests for bulk_spam_conversations
  • Updated tests for list_conversations_paginated
  • Verify bulk spam/unspam works from conversation list UI
  • Verify spam filter correctly hides/shows spam conversations
  • Verify conversation panel renders correctly with new layout

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Delete individual conversations from the panel
    • Bulk mark multiple conversations as spam
    • Enhanced conversation status display with improved visual indicators and overlay support
  • Tests

    • Added comprehensive test coverage for bulk conversation operations
  • Chores

    • Optimized conversation query performance with database indexes
    • Updated localization for new deletion and spam actions

Copy link
Copy Markdown

@greptile-apps greptile-apps Bot left a comment

Choose a reason for hiding this comment

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

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 20, 2026

📝 Walkthrough

Walkthrough

This PR adds bulk spam marking and individual conversation deletion capabilities to the conversations feature. The changes include: new Convex mutations (bulkSpamConversations, deleteConversation) and helper functions with supporting tests; backend query optimization through index strategy refactoring (replacing generic filter selection with consolidated ordered queries using by_org_lastMessageAt and by_org_status_lastMessageAt indexes); frontend component updates to ConversationListPanel (accepting an overlay prop), ConversationPanel (integrating delete mutation handling and updated status banners with formatted dates), and main conversations component (routing bulk spam actions through a new loading overlay); hook expansions in mutations and bulk-actions to expose delete and bulk-spam operations; schema updates adding the by_org_lastMessageAt index; and corresponding i18n string additions for delete actions and bulk spam success/failure messaging.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 11.76% 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 directly summarizes the main changes: it adds bulk spam actions to conversations and optimizes queries, which are the core objectives evident in the changeset.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/bulk-spam-conversations

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
services/platform/app/features/conversations/components/conversations.tsx (1)

304-317: ⚠️ Potential issue | 🟡 Minor

Use spam-specific copy for the spam-tab bulk action.

When status === 'spam', this control still announces itself as bulk.reopen, while the detail pane uses “Not spam” for the same transition. Reusing “Reopen” here makes the spam tab ambiguous and doesn't match the new bulk spam/unspam flow.

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

In `@services/platform/app/features/conversations/components/conversations.tsx`
around lines 304 - 317, The bulk action button uses the generic "bulk.reopen"
text/aria-label/tooltip even when status === 'spam', causing ambiguity; update
the Tooltip content, Button children text/icon label and aria-label to use a
spam-specific translation key when status === 'spam' (e.g., use
tConversations('bulk.not_spam') or similar) while keeping the existing
tConversations('bulk.reopen') for other statuses; change the conditional logic
around Tooltip content, aria-label and the rendered label/icon in the Button
that wraps handleBulkReopen/isBulkProcessing so the UI and accessibility copy
matches the spam tab flow.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@services/platform/app/features/conversations/components/conversation-panel.tsx`:
- Around line 633-665: The delete button currently calls deleteConversation
immediately; change it to open the existing confirm-delete dialog pattern
instead and only call deleteConversation from that dialog’s onConfirm handler.
Replace the onClick in this Button (the destructive Button rendering
deleteConversation for conversationId via
toId<'conversations'>(conversation.id)) with code that launches the shared
confirm-delete dialog, and move the deleteConversation invocation (including the
toId conversion, the onSuccess toast + onSelectedConversationChange(null), and
the onError toast/console.error) into the dialog’s confirm callback; keep the
disabled state (isDeleting || isReopening) and button text logic unchanged so
the UI reflects in-progress status.

In `@services/platform/convex/conversations/mutations.ts`:
- Around line 153-161: The bulk mutation handlers (bulkSpamConversations and the
bulk-delete counterpart that calls ConversationsHelpers) accept only
conversationIds so they can't enforce org isolation; add an organizationId arg
to the mutation signatures (e.g. args.organizationId: v.id('organizations')) and
update calls into
ConversationsHelpers.bulkSpamConversations/bulkDeleteConversations to accept it,
then inside the helper(s) verify for each fetched conversation that
conversation.organizationId === args.organizationId and skip/reject any that do
not match before performing any patch/delete or building audit context; ensure
you return an error or omit mutated IDs for non-matching orgs to prevent
cross-organization writes.

In `@services/platform/convex/conversations/query_conversations.ts`:
- Around line 29-47: The current buildOrderedQuery falls back to an org-wide
lastMessageAt index when args.status is undefined, forcing non-status filters
(direction, channel, priority, customerId) to be applied in-memory and causing
pagination to truncate due to the 500-row scan limit; update buildOrderedQuery
to choose an indexed query path for those filters (e.g., use
withIndex('by_org_channel_lastMessageAt'),
withIndex('by_org_priority_lastMessageAt'), etc.) when the corresponding arg is
present and add .eq clauses for organizationId plus the filter field so the DB
can paginate efficiently, or if such *_lastMessageAt indexes do not exist, add
them to the schema before relying on the in-memory fallback.

---

Outside diff comments:
In `@services/platform/app/features/conversations/components/conversations.tsx`:
- Around line 304-317: The bulk action button uses the generic "bulk.reopen"
text/aria-label/tooltip even when status === 'spam', causing ambiguity; update
the Tooltip content, Button children text/icon label and aria-label to use a
spam-specific translation key when status === 'spam' (e.g., use
tConversations('bulk.not_spam') or similar) while keeping the existing
tConversations('bulk.reopen') for other statuses; change the conditional logic
around Tooltip content, aria-label and the rendered label/icon in the Button
that wraps handleBulkReopen/isBulkProcessing so the UI and accessibility copy
matches the spam tab flow.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 772f16d9-aa48-459c-9c82-12feb61eb9ba

📥 Commits

Reviewing files that changed from the base of the PR and between 80c7a53 and bb0a484.

⛔ Files ignored due to path filters (2)
  • services/platform/convex/_generated/api.d.ts is excluded by !**/_generated/**
  • services/platform/convex/betterAuth/_generated/component.ts is excluded by !**/_generated/**
📒 Files selected for processing (15)
  • services/platform/app/features/conversations/components/conversation-list-panel.tsx
  • services/platform/app/features/conversations/components/conversation-panel.tsx
  • services/platform/app/features/conversations/components/conversations.tsx
  • services/platform/app/features/conversations/hooks/mutations.ts
  • services/platform/app/features/conversations/hooks/use-bulk-actions.ts
  • services/platform/convex/conversations/bulk_spam_conversations.test.ts
  • services/platform/convex/conversations/bulk_spam_conversations.ts
  • services/platform/convex/conversations/helpers.ts
  • services/platform/convex/conversations/list_conversations_paginated.test.ts
  • services/platform/convex/conversations/list_conversations_paginated.ts
  • services/platform/convex/conversations/mutations.ts
  • services/platform/convex/conversations/queries.ts
  • services/platform/convex/conversations/query_conversations.ts
  • services/platform/convex/conversations/schema.ts
  • services/platform/messages/en.json

Comment on lines +633 to +665
<Button
variant="destructive"
size="sm"
disabled={isDeleting || isReopening}
className="h-auto px-3 py-1 text-[13px]"
onClick={() => {
deleteConversation(
{
conversationId: toId<'conversations'>(conversation.id),
},
{
onSuccess: () => {
toast({
title: tConversations('panel.deleteSuccess'),
variant: 'success',
});
onSelectedConversationChange(null);
},
onError: (error) => {
console.error('Error deleting conversation:', error);
toast({
title: tConversations('panel.deleteFailed'),
variant: 'destructive',
});
},
},
);
}}
>
{isDeleting
? tConversations('panel.deleting')
: tConversations('panel.delete')}
</Button>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add a confirmation step before deleting a conversation.

This button calls deleteConversation immediately from a single press. For a permanent delete, that makes accidental data loss too easy. Please route this through the existing confirm-delete dialog pattern before firing the mutation.

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

In
`@services/platform/app/features/conversations/components/conversation-panel.tsx`
around lines 633 - 665, The delete button currently calls deleteConversation
immediately; change it to open the existing confirm-delete dialog pattern
instead and only call deleteConversation from that dialog’s onConfirm handler.
Replace the onClick in this Button (the destructive Button rendering
deleteConversation for conversationId via
toId<'conversations'>(conversation.id)) with code that launches the shared
confirm-delete dialog, and move the deleteConversation invocation (including the
toId conversion, the onSuccess toast + onSelectedConversationChange(null), and
the onError toast/console.error) into the dialog’s confirm callback; keep the
disabled state (isDeleting || isReopening) and button text logic unchanged so
the UI reflects in-progress status.

Comment on lines +153 to +161
export const bulkSpamConversations = mutationWithRLS({
args: {
conversationIds: v.array(v.id('conversations')),
},
returns: bulkOperationResultValidator,
handler: async (ctx, args) => {
return await ConversationsHelpers.bulkSpamConversations(ctx, args);
},
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Scope bulk spam and delete to the active organization.

Both new write mutations only accept conversation IDs, so the implementation has no way to verify that every fetched record belongs to the current workspace before patch/delete. A user who belongs to multiple orgs can therefore submit IDs from another workspace, and bulk_spam_conversations.ts will even build its audit context from whichever conversation is found first. Add organizationId to both args and reject any fetched conversation whose organizationId does not match before mutating it.

Based on learnings: Enforce organization isolation for conversation write paths by verifying args.organizationId === conversation.organizationId before performing inserts or patches.

Also applies to: 173-180

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

In `@services/platform/convex/conversations/mutations.ts` around lines 153 - 161,
The bulk mutation handlers (bulkSpamConversations and the bulk-delete
counterpart that calls ConversationsHelpers) accept only conversationIds so they
can't enforce org isolation; add an organizationId arg to the mutation
signatures (e.g. args.organizationId: v.id('organizations')) and update calls
into ConversationsHelpers.bulkSpamConversations/bulkDeleteConversations to
accept it, then inside the helper(s) verify for each fetched conversation that
conversation.organizationId === args.organizationId and skip/reject any that do
not match before performing any patch/delete or building audit context; ensure
you return an error or omit mutated IDs for non-matching orgs to prevent
cross-organization writes.

Comment thread services/platform/convex/conversations/query_conversations.ts
…ry optimizations

- Add bulk spam/unspam mutations with organization isolation verification
- Add delete confirmation dialog for spam conversations
- Optimize conversation query with consolidated filtering
- Use proper ConversationStatus type instead of string
- Add spam status filter to conversation list
@Israeltheminer Israeltheminer force-pushed the feat/bulk-spam-conversations branch from 611b345 to 304344c Compare March 20, 2026 20:58
@Israeltheminer Israeltheminer merged commit 57a2157 into main Mar 20, 2026
17 checks passed
@Israeltheminer Israeltheminer deleted the feat/bulk-spam-conversations branch March 20, 2026 21:53
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