Skip to content

Commit 6c76d3d

Browse files
authored
Implement thread deletion functionality (#1799)
# Implement Thread Deletion Functionality ## Description This PR implements thread deletion functionality, allowing users to permanently delete threads from the bin. The implementation includes: 1. Replacing the optimistic delete with a real deletion using TRPC mutation 2. Enabling the previously disabled delete button in the thread context menu 3. Adding server-side support for thread deletion from the database 4. Adding folder name normalization to handle 'bin' vs 'trash' naming differences ## Type of Change - [x] ✨ New feature (non-breaking change which adds functionality) - [x] 🐛 Bug fix (non-breaking change which fixes an issue) ## Areas Affected - [x] Email Integration (Gmail, IMAP, etc.) - [x] User Interface/Experience - [x] Data Storage/Management ## Testing Done - [x] Manual testing performed ## Checklist - [x] I have performed a self-review of my code - [x] My changes generate no new warnings ## Additional Notes The implementation now properly deletes threads from the database when the user selects the delete option from the context menu. The UI provides toast notifications to indicate the deletion status. _By submitting this pull request, I confirm that my contribution is made under the terms of the project's license._
1 parent 101b732 commit 6c76d3d

File tree

3 files changed

+52
-37
lines changed

3 files changed

+52
-37
lines changed

apps/mail/components/context/thread-context.tsx

Lines changed: 26 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,13 @@ import {
2525
} from 'lucide-react';
2626
import { useOptimisticThreadState } from '@/components/mail/optimistic-thread-state';
2727
import { useOptimisticActions } from '@/hooks/use-optimistic-actions';
28+
import { ExclamationCircle, Mail, Clock } from '../icons/icons';
29+
import { SnoozeDialog } from '@/components/mail/snooze-dialog';
2830
import { type ThreadDestination } from '@/lib/thread-actions';
2931
import { useThread, useThreads } from '@/hooks/use-threads';
30-
import { ExclamationCircle, Mail, Clock } from '../icons/icons';
3132
import { useMemo, type ReactNode, useState } from 'react';
33+
import { useTRPC } from '@/providers/query-provider';
34+
import { useMutation } from '@tanstack/react-query';
3235
import { useLabels } from '@/hooks/use-labels';
3336
import { FOLDERS, LABELS } from '@/lib/utils';
3437
import { useMail } from '../mail/use-mail';
@@ -37,7 +40,6 @@ import { m } from '@/paraglide/messages';
3740
import { useParams } from 'react-router';
3841
import { useQueryState } from 'nuqs';
3942
import { toast } from 'sonner';
40-
import { SnoozeDialog } from '@/components/mail/snooze-dialog';
4143

4244
interface EmailAction {
4345
id: string;
@@ -93,10 +95,14 @@ const LabelsList = ({ threadId, bulkSelected }: { threadId: string; bulkSelected
9395
{labels
9496
.filter((label) => label.id)
9597
.map((label) => {
96-
let isChecked = label.id ? thread!.labels?.some((l) => l.id === label.id) ?? false : false;
98+
let isChecked = label.id
99+
? (thread!.labels?.some((l) => l.id === label.id) ?? false)
100+
: false;
97101

98102
if (rightClickedThreadOptimisticState.optimisticLabels) {
99-
if (rightClickedThreadOptimisticState.optimisticLabels.addedLabelIds.includes(label.id)) {
103+
if (
104+
rightClickedThreadOptimisticState.optimisticLabels.addedLabelIds.includes(label.id)
105+
) {
100106
isChecked = true;
101107
} else if (
102108
rightClickedThreadOptimisticState.optimisticLabels.removedLabelIds.includes(label.id)
@@ -141,16 +147,18 @@ export function ThreadContextMenu({
141147
const { data: threadData } = useThread(threadId);
142148
const [, setActiveReplyId] = useQueryState('activeReplyId');
143149
const optimisticState = useOptimisticThreadState(threadId);
150+
const trpc = useTRPC();
144151
const {
145152
optimisticMoveThreadsTo,
146153
optimisticToggleStar,
147154
optimisticToggleImportant,
148155
optimisticMarkAsRead,
149156
optimisticMarkAsUnread,
150-
optimisticDeleteThreads,
157+
// optimisticDeleteThreads,
151158
optimisticSnooze,
152159
optimisticUnsnooze,
153160
} = useOptimisticActions();
161+
const { mutateAsync: deleteThread } = useMutation(trpc.mail.delete.mutationOptions());
154162

155163
const { isUnread, isStarred, isImportant } = useMemo(() => {
156164
const unread = threadData?.hasUnread ?? false;
@@ -305,18 +313,18 @@ export function ThreadContextMenu({
305313
const handleDelete = () => () => {
306314
const targets = mail.bulkSelected.length ? mail.bulkSelected : [threadId];
307315

308-
// Use optimistic update with undo functionality
309-
optimisticDeleteThreads(targets, currentFolder);
310-
311-
// Clear bulk selection after action
312-
if (mail.bulkSelected.length) {
313-
setMail((prev) => ({ ...prev, bulkSelected: [] }));
314-
}
315-
316-
// Navigation removed to prevent route change on current thread action
317-
// if (!mail.bulkSelected.length && threadId) {
318-
// navigate(`/mail/${currentFolder}`);
319-
// }
316+
toast.promise(
317+
Promise.all(
318+
targets.map(async (id) => {
319+
return deleteThread({ id });
320+
}),
321+
),
322+
{
323+
loading: 'Deleting...',
324+
success: 'Deleted',
325+
error: 'Failed to delete',
326+
},
327+
);
320328
};
321329

322330
const getActions = useMemo(() => {
@@ -353,7 +361,6 @@ export function ThreadContextMenu({
353361
label: m['common.mail.deleteFromBin'](),
354362
icon: <Trash className="mr-2.5 h-4 w-4 opacity-60" />,
355363
action: handleDelete(),
356-
disabled: true,
357364
},
358365
];
359366
}
@@ -493,15 +500,7 @@ export function ThreadContextMenu({
493500
disabled: false,
494501
},
495502
],
496-
[
497-
isUnread,
498-
isImportant,
499-
isStarred,
500-
m,
501-
handleReadUnread,
502-
handleToggleImportant,
503-
handleFavorites,
504-
],
503+
[isUnread, isImportant, isStarred, m, handleReadUnread, handleToggleImportant, handleFavorites],
505504
);
506505

507506
const renderAction = (action: EmailAction) => {

apps/server/src/routes/agent/index.ts

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,16 @@ export class ZeroDriver extends AIChatAgent<typeof env> {
367367
DROP TABLE IF EXISTS threads;`;
368368
}
369369

370+
async deleteThread(id: string) {
371+
void this.sql`
372+
DELETE FROM threads WHERE thread_id = ${id};
373+
`;
374+
this.agent?.broadcastChatMessage({
375+
type: OutgoingMessageType.Mail_List,
376+
folder: 'bin',
377+
});
378+
}
379+
370380
async syncThread({ threadId }: { threadId: string }) {
371381
if (this.name === 'general') return;
372382
if (!this.driver) {
@@ -384,7 +394,7 @@ export class ZeroDriver extends AIChatAgent<typeof env> {
384394
}
385395
this.syncThreadsInProgress.set(threadId, true);
386396

387-
console.log('Server: syncThread called for thread', threadId);
397+
// console.log('Server: syncThread called for thread', threadId);
388398
try {
389399
const threadData = await this.getWithRetry(threadId);
390400
const latest = threadData.latest;
@@ -432,10 +442,10 @@ export class ZeroDriver extends AIChatAgent<typeof env> {
432442
threadId,
433443
});
434444
this.syncThreadsInProgress.delete(threadId);
435-
console.log('Server: syncThread result', {
436-
threadId,
437-
labels: threadData.labels,
438-
});
445+
// console.log('Server: syncThread result', {
446+
// threadId,
447+
// labels: threadData.labels,
448+
// });
439449
return { success: true, threadId, threadData };
440450
} else {
441451
this.syncThreadsInProgress.delete(threadId);
@@ -656,22 +666,26 @@ export class ZeroDriver extends AIChatAgent<typeof env> {
656666
};
657667
}
658668

669+
normalizeFolderName(folderName: string) {
670+
if (folderName === 'bin') return 'trash';
671+
return folderName;
672+
}
673+
659674
async getThreadsFromDB(params: {
660675
labelIds?: string[];
661676
folder?: string;
662677
q?: string;
663678
maxResults?: number;
664679
pageToken?: string;
665680
}): Promise<IGetThreadsResponse> {
666-
const { labelIds = [], folder, q, maxResults = 50, pageToken } = params;
681+
const { labelIds = [], q, maxResults = 50, pageToken } = params;
682+
let folder = params.folder ?? 'inbox';
667683

668684
try {
685+
folder = this.normalizeFolderName(folder);
669686
const folderThreadCount = (await this.count()).find((c) => c.label === folder)?.count;
670687
const currentThreadCount = await this.getThreadCount();
671688

672-
console.log('folderThreadCount', folderThreadCount, folder);
673-
console.log('currentThreadCount', currentThreadCount);
674-
675689
if (folderThreadCount && folderThreadCount > currentThreadCount && folder) {
676690
this.ctx.waitUntil(this.syncThreads(folder));
677691
}

apps/server/src/routes/agent/rpc.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,9 @@ export class DriverRpcDO extends RpcTarget {
161161
}
162162

163163
async delete(id: string) {
164-
return await this.mainDo.delete(id);
164+
const result = await this.mainDo.delete(id);
165+
await this.mainDo.deleteThread(id);
166+
return result;
165167
}
166168

167169
async deleteAllSpam() {

0 commit comments

Comments
 (0)