diff --git a/apps/mail/components/context/thread-context.tsx b/apps/mail/components/context/thread-context.tsx
index 592d0b2cc..1094380c9 100644
--- a/apps/mail/components/context/thread-context.tsx
+++ b/apps/mail/components/context/thread-context.tsx
@@ -32,6 +32,7 @@ import { useThread, useThreads } from '@/hooks/use-threads';
import { useSearchValue } from '@/hooks/use-search-value';
import { useParams, useRouter } from 'next/navigation';
import { useTRPC } from '@/providers/query-provider';
+import { ExclamationCircle } from '../icons/icons';
import { useLabels } from '@/hooks/use-labels';
import { LABELS, FOLDERS } from '@/lib/utils';
import { useStats } from '@/hooks/use-stats';
@@ -142,6 +143,7 @@ export function ThreadContextMenu({
trpc.mail.markAsUnread.mutationOptions({ onSuccess: () => invalidateCount() }),
);
const { mutateAsync: toggleStar } = useMutation(trpc.mail.toggleStar.mutationOptions());
+ const { mutateAsync: toggleImportant } = useMutation(trpc.mail.toggleImportant.mutationOptions());
const { mutateAsync: deleteThread } = useMutation(trpc.mail.delete.mutationOptions());
const selectedThreads = useMemo(() => {
@@ -162,6 +164,12 @@ export function ThreadContextMenu({
);
}, [threadData]);
+ const isImportant = useMemo(() => {
+ return threadData?.messages.some((message) =>
+ message.tags?.some((tag) => tag.name.toLowerCase() === 'important'),
+ );
+ }, [threadData]);
+
const noopAction = () => async () => {
toast.info(t('common.actions.featureNotImplemented'));
};
@@ -212,6 +220,13 @@ export function ThreadContextMenu({
return await Promise.allSettled([refetchThread(), refetch()]);
};
+ const handleToggleImportant = async () => {
+ const targets = mail.bulkSelected.length ? mail.bulkSelected : [threadId];
+ await toggleImportant({ ids: targets });
+ setMail((prev) => ({ ...prev, bulkSelected: [] }));
+ return await Promise.allSettled([refetchThread(), refetch()]);
+ };
+
const handleReadUnread = () => {
const targets = mail.bulkSelected.length ? mail.bulkSelected : [threadId];
const action = isUnread ? markAsRead : markAsUnread;
@@ -399,6 +414,12 @@ export function ThreadContextMenu({
action: handleReadUnread,
disabled: false,
},
+ {
+ id: 'toggle-important',
+ label: isImportant ? t('common.mail.removeFromImportant') : t('common.mail.markAsImportant'),
+ icon: ,
+ action: handleToggleImportant,
+ },
{
id: 'favorite',
label: isStarred ? t('common.mail.removeFavorite') : t('common.mail.addFavorite'),
diff --git a/apps/mail/components/icons/icons.tsx b/apps/mail/components/icons/icons.tsx
index 74a5b13ec..b7cda0786 100644
--- a/apps/mail/components/icons/icons.tsx
+++ b/apps/mail/components/icons/icons.tsx
@@ -1490,3 +1490,20 @@ export const Search = ({ className }: { className?: string }) => (
/>
);
+
+export const Folders = ({ className }: { className?: string }) => (
+
+);
diff --git a/apps/mail/components/mail/mail-list.tsx b/apps/mail/components/mail/mail-list.tsx
index 103e49c52..4f9115f56 100644
--- a/apps/mail/components/mail/mail-list.tsx
+++ b/apps/mail/components/mail/mail-list.tsx
@@ -4,6 +4,7 @@ import {
Archive2,
Bell,
ChevronDown,
+ ExclamationCircle,
GroupPeople,
Lightning,
People,
@@ -183,16 +184,22 @@ const Thread = memo(
refetch: refetchThread,
} = useThread(demo ? null : message.id);
const [isStarred, setIsStarred] = useState(false);
+ const [isImportant, setIsImportant] = useState(false);
const trpc = useTRPC();
const queryClient = useQueryClient();
const { mutateAsync: toggleStar } = useMutation(trpc.mail.toggleStar.mutationOptions());
+ const { mutateAsync: toggleImportant } = useMutation(
+ trpc.mail.toggleImportant.mutationOptions(),
+ );
const [id, setThreadId] = useQueryState('threadId');
const [activeReplyId, setActiveReplyId] = useQueryState('activeReplyId');
const [focusedIndex, setFocusedIndex] = useAtom(focusedIndexAtom);
useEffect(() => {
if (getThreadData?.latest?.tags) {
+ console.log(getThreadData.latest.tags);
setIsStarred(getThreadData.latest.tags.some((tag) => tag.name === 'STARRED'));
+ setIsImportant(getThreadData.latest.tags.some((tag) => tag.name === 'IMPORTANT'));
}
}, [getThreadData?.latest?.tags]);
@@ -214,6 +221,23 @@ const Thread = memo(
[getThreadData, message.id, isStarred, refetchThreads, t],
);
+ const handleToggleImportant = useCallback(
+ async (e: React.MouseEvent) => {
+ e.stopPropagation();
+ if (!getThreadData || !message.id) return;
+ const newImportantState = !isImportant;
+ setIsImportant(newImportantState);
+ await toggleImportant({ ids: [message.id] });
+ if (newImportantState) {
+ toast.success(t('common.actions.addedToImportant'));
+ } else {
+ toast.success(t('common.actions.removedFromImportant'));
+ }
+ await refetchThread();
+ },
+ [getThreadData, message.id, refetchThreads],
+ );
+
const handleNext = useCallback(
(id: string) => {
if (!id || !threads.length || focusedIndex === null) return setThreadId(null);
@@ -469,13 +493,28 @@ const Thread = memo(
{isStarred ? t('common.threadDisplay.unstar') : t('common.threadDisplay.star')}
+ {/*
+
+
+
+
+ {t('common.mail.toggleImportant')}
+
+ */}
+
@@ -578,12 +596,18 @@ export function ThreadDisplay() {
{emailData.latest?.listUnsubscribe ||
emailData.latest?.listUnsubscribePost ? (
-
+
Unsubscribe
) : null}
>
)}
+ {!isImportant && (
+
+
+ {t('common.mail.markAsImportant')}
+
+ )}
diff --git a/apps/mail/locales/en.json b/apps/mail/locales/en.json
index 8ee552228..83614786b 100644
--- a/apps/mail/locales/en.json
+++ b/apps/mail/locales/en.json
@@ -29,7 +29,9 @@
"addingToFavorites": "Adding to favorites...",
"removingFromFavorites": "Removing from favorites...",
"addedToFavorites": "Added to favorites",
+ "addedToImportant": "Added to important",
"removedFromFavorites": "Removed from favorites",
+ "removedFromImportant": "Removed from important",
"failedToAddToFavorites": "Failed to add to favorites",
"failedToRemoveFromFavorites": "Failed to remove from favorites",
"failedToModifyFavorites": "Failed to modify favorites",
@@ -284,6 +286,10 @@
"noSearchResults": "No search results found",
"clearSearch": "Clear search",
"markAsImportant": "Mark as Important",
+ "removeFromImportant": "Remove from Important",
+ "markedAsImportant": "Marked as Important",
+ "markedAsUnimportant": "Unmarked as Unimportant",
+ "toggleImportant": "Toggle important",
"unSubscribeFromAll": "Unsubscribe from all",
"starAll": "Star all",
"mute": "Mute",
diff --git a/apps/server/src/trpc/routes/mail.ts b/apps/server/src/trpc/routes/mail.ts
index 524539a3e..cbe00a58c 100644
--- a/apps/server/src/trpc/routes/mail.ts
+++ b/apps/server/src/trpc/routes/mail.ts
@@ -157,6 +157,47 @@ export const mailRouter = router({
removeLabels: shouldStar ? [] : ['STARRED'],
});
+ return { success: true };
+ }),
+ toggleImportant: activeDriverProcedure
+ .input(
+ z.object({
+ ids: z.string().array(),
+ }),
+ )
+ .mutation(async ({ input, ctx }) => {
+ const { driver } = ctx;
+ const { threadIds } = driver.normalizeIds(input.ids);
+
+ if (!threadIds.length) {
+ return { success: false, error: 'No thread IDs provided' };
+ }
+
+ const threadResults = await Promise.allSettled(threadIds.map((id) => driver.get(id)));
+
+ let anyImportant = false;
+ let processedThreads = 0;
+
+ for (const result of threadResults) {
+ if (result.status === 'fulfilled' && result.value && result.value.messages.length > 0) {
+ processedThreads++;
+ const isThreadImportant = result.value.messages.some((message) =>
+ message.tags?.some((tag) => tag.name.toLowerCase().startsWith('important')),
+ );
+ if (isThreadImportant) {
+ anyImportant = true;
+ break;
+ }
+ }
+ }
+
+ const shouldMarkImportant = processedThreads > 0 && !anyImportant;
+
+ await driver.modifyLabels(threadIds, {
+ addLabels: shouldMarkImportant ? ['IMPORTANT'] : [],
+ removeLabels: shouldMarkImportant ? [] : ['IMPORTANT'],
+ });
+
return { success: true };
}),
bulkStar: activeDriverProcedure
@@ -169,6 +210,16 @@ export const mailRouter = router({
const { driver } = ctx;
return driver.modifyLabels(input.ids, { addLabels: ['STARRED'], removeLabels: [] });
}),
+ bulkMarkImportant: activeDriverProcedure
+ .input(
+ z.object({
+ ids: z.string().array(),
+ }),
+ )
+ .mutation(async ({ input, ctx }) => {
+ const { driver } = ctx;
+ return driver.modifyLabels(input.ids, { addLabels: ['IMPORTANT'], removeLabels: [] });
+ }),
bulkUnstar: activeDriverProcedure
.input(
z.object({
@@ -179,6 +230,16 @@ export const mailRouter = router({
const { driver } = ctx;
return driver.modifyLabels(input.ids, { addLabels: [], removeLabels: ['STARRED'] });
}),
+ bulkUnmarkImportant: activeDriverProcedure
+ .input(
+ z.object({
+ ids: z.string().array(),
+ }),
+ )
+ .mutation(async ({ input, ctx }) => {
+ const { driver } = ctx;
+ return driver.modifyLabels(input.ids, { addLabels: [], removeLabels: ['IMPORTANT'] });
+ }),
send: activeDriverProcedure
.input(
z.object({