mirror of
https://github.com/Mail-0/Zero.git
synced 2026-06-28 23:06:54 +00:00
Add reusable hooks for email deletion and movement logic
Introduced `useDelete` and `useMoveTo` hooks to centralize deletion and movement logic for emails and threads. These hooks improve code reusability, streamline background queue handling, and enhance folder-specific actions like moving to bin or spam.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { useParams, useRouter, useSearchParams } from 'next/navigation';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { useTheme } from 'next-themes';
|
||||
import Image from 'next/image';
|
||||
|
||||
@@ -33,7 +33,9 @@ import { focusedIndexAtom } from '@/hooks/use-mail-navigation';
|
||||
import { backgroundQueueAtom } from '@/store/backgroundQueue';
|
||||
import { handleUnsubscribe } from '@/lib/email-utils.client';
|
||||
import { useThread, useThreads } from '@/hooks/use-threads';
|
||||
import { useAISidebar } from '@/components/ui/ai-sidebar';
|
||||
import { markAsRead, markAsUnread } from '@/actions/mail';
|
||||
import { useHotkeysContext } from 'react-hotkeys-hook';
|
||||
import { MailDisplaySkeleton } from './mail-skeleton';
|
||||
import { useIsMobile } from '@/hooks/use-mobile';
|
||||
import { Button } from '@/components/ui/button';
|
||||
@@ -51,8 +53,6 @@ import { ParsedMessage } from '@/types';
|
||||
import { useQueryState } from 'nuqs';
|
||||
import { useAtom } from 'jotai';
|
||||
import { toast } from 'sonner';
|
||||
import { useAISidebar } from '@/components/ui/ai-sidebar';
|
||||
import { useHotkeysContext } from 'react-hotkeys-hook';
|
||||
|
||||
interface ThreadDisplayProps {
|
||||
threadParam?: any;
|
||||
|
||||
50
apps/mail/hooks/driver/use-delete.ts
Normal file
50
apps/mail/hooks/driver/use-delete.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import useBackgroundQueue from '@/hooks/ui/use-background-queue';
|
||||
import { useMail } from '@/components/mail/use-mail';
|
||||
import { useThreads } from '@/hooks/use-threads';
|
||||
import { deleteThread } from '@/actions/mail';
|
||||
import { useStats } from '@/hooks/use-stats';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
const useDelete = () => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [mail, setMail] = useMail();
|
||||
const { mutate: refetchThreads } = useThreads();
|
||||
const { mutate: refetchStats } = useStats();
|
||||
const t = useTranslations();
|
||||
const { addToQueue, deleteFromQueue } = useBackgroundQueue();
|
||||
|
||||
return {
|
||||
mutate: (id: string, type: 'thread' | 'email' = 'thread') => {
|
||||
setIsLoading(true);
|
||||
addToQueue(id);
|
||||
return toast.promise(
|
||||
deleteThread({
|
||||
id,
|
||||
}),
|
||||
{
|
||||
loading: t('common.actions.deletingMail'),
|
||||
success: t('common.actions.deletedMail'),
|
||||
error: (error) => {
|
||||
console.error(`Error deleting ${type}:`, error);
|
||||
|
||||
return t('common.actions.failedToDeleteMail');
|
||||
},
|
||||
finally: async () => {
|
||||
setMail({
|
||||
...mail,
|
||||
bulkSelected: [],
|
||||
});
|
||||
deleteFromQueue(id);
|
||||
setIsLoading(false);
|
||||
await Promise.all([refetchThreads(), refetchStats()]);
|
||||
},
|
||||
},
|
||||
);
|
||||
},
|
||||
isLoading,
|
||||
};
|
||||
};
|
||||
|
||||
export default useDelete;
|
||||
89
apps/mail/hooks/driver/use-move-to.ts
Normal file
89
apps/mail/hooks/driver/use-move-to.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { moveThreadsTo, type MoveThreadOptions } from '@/lib/thread-actions';
|
||||
import useBackgroundQueue from '@/hooks/ui/use-background-queue';
|
||||
import { useMail } from '@/components/mail/use-mail';
|
||||
import { useThreads } from '@/hooks/use-threads';
|
||||
import { useStats } from '@/hooks/use-stats';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
const useMoveTo = () => {
|
||||
const t = useTranslations();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const { mutate: refetchThreads } = useThreads();
|
||||
const { mutate: refetchStats } = useStats();
|
||||
const [mail, setMail] = useMail();
|
||||
const { addToQueue, deleteFromQueue } = useBackgroundQueue();
|
||||
|
||||
const getCopyByDestination = (to?: MoveThreadOptions['destination']) => {
|
||||
switch (to) {
|
||||
case 'inbox':
|
||||
return {
|
||||
loading: t('common.actions.movingToInbox'),
|
||||
success: t('common.actions.movedToInbox'),
|
||||
};
|
||||
case 'spam':
|
||||
return {
|
||||
loading: t('common.actions.movingToSpam'),
|
||||
success: t('common.actions.movedToSpam'),
|
||||
};
|
||||
case 'bin':
|
||||
return {
|
||||
loading: t('common.actions.movingToBin'),
|
||||
success: t('common.actions.movedToBin'),
|
||||
};
|
||||
case 'archive':
|
||||
return {
|
||||
loading: t('common.actions.archiving'),
|
||||
success: t('common.actions.archived'),
|
||||
};
|
||||
default:
|
||||
return {
|
||||
loading: t('common.actions.moving'),
|
||||
success: t('common.actions.moved'),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const mutate = ({ threadIds, currentFolder, destination }: MoveThreadOptions) => {
|
||||
if (!threadIds.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
const promise = moveThreadsTo({
|
||||
threadIds,
|
||||
currentFolder,
|
||||
destination,
|
||||
});
|
||||
for (const threadId of threadIds) {
|
||||
addToQueue(threadId);
|
||||
}
|
||||
return toast.promise(promise, {
|
||||
...getCopyByDestination(destination),
|
||||
error: (error) => {
|
||||
console.error('Error moving thread(s):', error);
|
||||
|
||||
return t('common.actions.failedToMove');
|
||||
},
|
||||
finally: async () => {
|
||||
setIsLoading(false);
|
||||
for (const threadId of threadIds) {
|
||||
deleteFromQueue(threadId);
|
||||
}
|
||||
await Promise.all([refetchThreads(), refetchStats()]);
|
||||
setMail({
|
||||
...mail,
|
||||
bulkSelected: [],
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
mutate,
|
||||
isLoading,
|
||||
};
|
||||
};
|
||||
|
||||
export default useMoveTo;
|
||||
23
apps/mail/hooks/ui/use-background-queue.ts
Normal file
23
apps/mail/hooks/ui/use-background-queue.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { backgroundQueueAtom } from '@/store/backgroundQueue';
|
||||
import { useAtom } from 'jotai';
|
||||
|
||||
const useBackgroundQueue = () => {
|
||||
const [backgroundQueue, setBackgroundQueue] = useAtom(backgroundQueueAtom);
|
||||
|
||||
return {
|
||||
addToQueue: (threadId: string) =>
|
||||
setBackgroundQueue({
|
||||
type: 'add',
|
||||
threadId: threadId.startsWith('thread:') ? threadId : `thread:${threadId}`,
|
||||
}),
|
||||
deleteFromQueue: (threadId: string) =>
|
||||
setBackgroundQueue({
|
||||
type: 'delete',
|
||||
threadId: threadId.startsWith('thread:') ? threadId : `thread:${threadId}`,
|
||||
}),
|
||||
clearQueue: () => setBackgroundQueue({ type: 'clear' }),
|
||||
queue: backgroundQueue,
|
||||
};
|
||||
};
|
||||
|
||||
export default useBackgroundQueue;
|
||||
@@ -1,11 +1,12 @@
|
||||
'use client';
|
||||
|
||||
import { useThread, useThreads } from '@/hooks/use-threads';
|
||||
import { keyboardShortcuts } from '@/config/shortcuts';
|
||||
import useMoveTo from '@/hooks/driver/use-move-to';
|
||||
import useDelete from '@/hooks/driver/use-delete';
|
||||
import { useShortcuts } from './use-hotkey-utils';
|
||||
import { deleteThread } from '@/actions/mail';
|
||||
import { useThread } from '@/hooks/use-threads';
|
||||
import { useParams } from 'next/navigation';
|
||||
import { useQueryState } from 'nuqs';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
const closeView = (event: KeyboardEvent) => {
|
||||
event.preventDefault();
|
||||
@@ -17,6 +18,11 @@ export function ThreadDisplayHotkeys() {
|
||||
const [activeReplyId, setActiveReplyId] = useQueryState('activeReplyId');
|
||||
const [openThreadId] = useQueryState('threadId');
|
||||
const { data: thread } = useThread(openThreadId);
|
||||
const params = useParams<{
|
||||
folder: string;
|
||||
}>();
|
||||
const { mutate: deleteThread } = useDelete();
|
||||
const { mutate: moveTo } = useMoveTo();
|
||||
|
||||
const handlers = {
|
||||
closeView: () => closeView(new KeyboardEvent('keydown', { key: 'Escape' })),
|
||||
@@ -34,11 +40,15 @@ export function ThreadDisplayHotkeys() {
|
||||
},
|
||||
delete: () => {
|
||||
if (!openThreadId) return;
|
||||
toast.promise(deleteThread({ id: thread?.latest?.id ?? openThreadId }), {
|
||||
loading: 'Deleting email...',
|
||||
success: 'Email deleted',
|
||||
error: 'Failed to delete email',
|
||||
});
|
||||
if (params.folder === 'bin') {
|
||||
deleteThread(thread?.latest?.id ?? openThreadId);
|
||||
} else {
|
||||
moveTo({
|
||||
threadIds: [thread?.latest?.id ?? openThreadId],
|
||||
currentFolder: params.folder,
|
||||
destination: 'bin',
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { LABELS, FOLDERS } from '@/lib/utils';
|
||||
export type ThreadDestination = 'inbox' | 'archive' | 'spam' | 'bin' | null;
|
||||
export type FolderLocation = 'inbox' | 'archive' | 'spam' | 'sent' | 'bin' | string;
|
||||
|
||||
interface MoveThreadOptions {
|
||||
export interface MoveThreadOptions {
|
||||
threadIds: string[];
|
||||
currentFolder: FolderLocation;
|
||||
destination: ThreadDestination;
|
||||
@@ -43,14 +43,10 @@ export function isActionAvailable(folder: FolderLocation, action: ThreadDestinat
|
||||
|
||||
export function getAvailableActions(folder: FolderLocation): ThreadDestination[] {
|
||||
const allPossibleActions: ThreadDestination[] = ['inbox', 'archive', 'spam', 'bin'];
|
||||
return allPossibleActions.filter(action => isActionAvailable(folder, action));
|
||||
return allPossibleActions.filter((action) => isActionAvailable(folder, action));
|
||||
}
|
||||
|
||||
export async function moveThreadsTo({
|
||||
threadIds,
|
||||
currentFolder,
|
||||
destination,
|
||||
}: MoveThreadOptions) {
|
||||
export async function moveThreadsTo({ threadIds, currentFolder, destination }: MoveThreadOptions) {
|
||||
try {
|
||||
if (!threadIds.length) return;
|
||||
const isInInbox = currentFolder === FOLDERS.INBOX || !currentFolder;
|
||||
@@ -60,22 +56,28 @@ export async function moveThreadsTo({
|
||||
let addLabel = '';
|
||||
let removeLabel = '';
|
||||
|
||||
switch(destination) {
|
||||
switch (destination) {
|
||||
case 'inbox':
|
||||
addLabel = LABELS.INBOX;
|
||||
removeLabel = isInSpam ? LABELS.SPAM : (isInBin ? LABELS.TRASH : '');
|
||||
removeLabel = isInSpam ? LABELS.SPAM : isInBin ? LABELS.TRASH : '';
|
||||
break;
|
||||
case 'archive':
|
||||
addLabel = '';
|
||||
removeLabel = isInInbox ? LABELS.INBOX : (isInSpam ? LABELS.SPAM : (isInBin ? LABELS.TRASH : ''));
|
||||
removeLabel = isInInbox
|
||||
? LABELS.INBOX
|
||||
: isInSpam
|
||||
? LABELS.SPAM
|
||||
: isInBin
|
||||
? LABELS.TRASH
|
||||
: '';
|
||||
break;
|
||||
case 'spam':
|
||||
addLabel = LABELS.SPAM;
|
||||
removeLabel = isInInbox ? LABELS.INBOX : (isInBin ? LABELS.TRASH : '');
|
||||
removeLabel = isInInbox ? LABELS.INBOX : isInBin ? LABELS.TRASH : '';
|
||||
break;
|
||||
case 'bin':
|
||||
addLabel = LABELS.TRASH;
|
||||
removeLabel = isInInbox ? LABELS.INBOX : (isInSpam ? LABELS.SPAM : '');
|
||||
removeLabel = isInInbox ? LABELS.INBOX : isInSpam ? LABELS.SPAM : '';
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"loading": "Loading...",
|
||||
"featureNotImplemented": "This feature is not implemented yet",
|
||||
"moving": "Moving...",
|
||||
"moved": "Moved",
|
||||
"movedToInbox": "Moved to inbox",
|
||||
"movingToInbox": "Moving to inbox...",
|
||||
"movedToSpam": "Moved to spam",
|
||||
|
||||
Reference in New Issue
Block a user