);
+
+ return (
+
mutate()}
+ >
+ {content}
+
+ );
},
);
diff --git a/apps/mail/components/ui/context-menu.tsx b/apps/mail/components/ui/context-menu.tsx
new file mode 100644
index 0000000000..d4e6ac526e
--- /dev/null
+++ b/apps/mail/components/ui/context-menu.tsx
@@ -0,0 +1,189 @@
+'use client';
+
+import * as ContextMenuPrimitive from '@radix-ui/react-context-menu';
+import { Check, ChevronRight, Circle } from 'lucide-react';
+import * as React from 'react';
+
+import { cn } from '@/lib/utils';
+
+const ContextMenu = ContextMenuPrimitive.Root;
+
+const ContextMenuTrigger = ContextMenuPrimitive.Trigger;
+
+const ContextMenuGroup = ContextMenuPrimitive.Group;
+
+const ContextMenuPortal = ContextMenuPrimitive.Portal;
+
+const ContextMenuSub = ContextMenuPrimitive.Sub;
+
+const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup;
+
+const ContextMenuSubTrigger = React.forwardRef<
+ React.ElementRef
,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean;
+ }
+>(({ className, inset, children, ...props }, ref) => (
+
+ {children}
+
+
+));
+ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName;
+
+const ContextMenuSubContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName;
+
+const ContextMenuContent = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+));
+ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName;
+
+const ContextMenuItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean;
+ }
+>(({ className, inset, ...props }, ref) => (
+
+));
+ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName;
+
+const ContextMenuCheckboxItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, checked, ...props }, ref) => (
+
+
+
+
+
+
+ {children}
+
+));
+ContextMenuCheckboxItem.displayName = ContextMenuPrimitive.CheckboxItem.displayName;
+
+const ContextMenuRadioItem = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, children, ...props }, ref) => (
+
+
+
+
+
+
+ {children}
+
+));
+ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName;
+
+const ContextMenuLabel = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef & {
+ inset?: boolean;
+ }
+>(({ className, inset, ...props }, ref) => (
+
+));
+ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName;
+
+const ContextMenuSeparator = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+));
+ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName;
+
+const ContextMenuShortcut = ({ className, ...props }: React.HTMLAttributes) => {
+ return (
+
+ );
+};
+ContextMenuShortcut.displayName = 'ContextMenuShortcut';
+
+export {
+ ContextMenu,
+ ContextMenuTrigger,
+ ContextMenuContent,
+ ContextMenuItem,
+ ContextMenuCheckboxItem,
+ ContextMenuRadioItem,
+ ContextMenuLabel,
+ ContextMenuSeparator,
+ ContextMenuShortcut,
+ ContextMenuGroup,
+ ContextMenuPortal,
+ ContextMenuSub,
+ ContextMenuSubContent,
+ ContextMenuSubTrigger,
+ ContextMenuRadioGroup,
+};
diff --git a/apps/mail/locales/en.json b/apps/mail/locales/en.json
index 59d1bdaf9d..921308209f 100644
--- a/apps/mail/locales/en.json
+++ b/apps/mail/locales/en.json
@@ -12,7 +12,23 @@
"signedOutSuccess": "Signed out successfully!",
"signOutError": "Error signing out",
"refresh": "Refresh",
- "loading": "Loading..."
+ "loading": "Loading...",
+ "featureNotImplemented": "This feature is not implemented yet",
+ "moving": "Moving...",
+ "movedToInbox": "Moved to inbox",
+ "movingToInbox": "Moving to inbox...",
+ "movedToSpam": "Moved to spam",
+ "movingToSpam": "Moving to spam...",
+ "archiving": "Archiving...",
+ "archived": "Archived",
+ "failedToMove": "Failed to move message",
+ "addingToFavorites": "Adding to favorites...",
+ "removingFromFavorites": "Removing from favorites...",
+ "addedToFavorites": "Added to favorites",
+ "removedFromFavorites": "Removed from favorites",
+ "failedToModifyFavorites": "Failed to modify favorites",
+ "markingAsRead": "Marking as read...",
+ "markingAsUnread": "Marking as unread..."
},
"themes": {
"dark": "Dark",
@@ -220,7 +236,8 @@
"moveToTrash": "Move to Trash",
"markAsUnread": "Mark as Unread",
"markAsRead": "Mark as Read",
- "addStar": "Add Star",
+ "addFavorite": "Favorite",
+ "removeFavorite": "Unfavorite",
"muteThread": "Mute Thread",
"moving": "Moving...",
"moved": "Moved",
From 18028392d57c792cd646472f8864db6ca498a662 Mon Sep 17 00:00:00 2001
From: user12224 <122770437+user12224@users.noreply.github.com>
Date: Sat, 5 Apr 2025 22:30:32 +0200
Subject: [PATCH 2/5] cleanup
---
apps/mail/actions/mail.ts | 40 ++++++++++++++++++----------
apps/mail/app/api/driver/google.ts | 42 +++++++++++++++++++++++-------
2 files changed, 58 insertions(+), 24 deletions(-)
diff --git a/apps/mail/actions/mail.ts b/apps/mail/actions/mail.ts
index 2097145064..2ff880b920 100644
--- a/apps/mail/actions/mail.ts
+++ b/apps/mail/actions/mail.ts
@@ -116,24 +116,36 @@ export const toggleStar = async ({ ids }: { ids: string[] }) => {
try {
const driver = await getActiveDriver();
const { threadIds } = driver.normalizeIds(ids);
+
+ if (!threadIds.length) {
+ return { success: false, error: 'No thread IDs provided' };
+ }
- if (threadIds.length && threadIds[0]) {
- const thread = await driver.get(threadIds[0]);
- if (!thread?.[0]) {
- return { success: false, error: 'Thread not found' };
+ let allStarred = true;
+ let anyValid = false;
+
+ for (const id of threadIds) {
+ try {
+ const thread = await driver.get(id);
+ if (thread?.[0]) {
+ anyValid = true;
+ if (!thread[0].tags?.includes('STARRED')) {
+ allStarred = false;
+ break;
+ }
+ }
+ } catch (error) {
+ continue;
}
-
- const isStarred = thread[0].tags?.includes('STARRED') ?? false;
-
- await driver.modifyLabels(threadIds, {
- addLabels: isStarred ? [] : ['STARRED'],
- removeLabels: isStarred ? ['STARRED'] : [],
- });
-
- return { success: true };
}
+ const shouldStar = !anyValid || !allStarred;
- return { success: false, error: 'No thread IDs provided' };
+ await driver.modifyLabels(threadIds, {
+ addLabels: shouldStar ? ['STARRED'] : [],
+ removeLabels: shouldStar ? [] : ['STARRED'],
+ });
+
+ return { success: true };
} catch (error) {
if (FatalErrors.includes((error as Error).message)) await deleteActiveConnection();
console.error('Error toggling star:', error);
diff --git a/apps/mail/app/api/driver/google.ts b/apps/mail/app/api/driver/google.ts
index bac3738372..bdd624e9f2 100644
--- a/apps/mail/app/api/driver/google.ts
+++ b/apps/mail/app/api/driver/google.ts
@@ -310,10 +310,20 @@ export const driver = async (config: IConfig): Promise => {
return { ...res.data, threads } as any;
},
get: async (id: string): Promise => {
- console.log(id);
+ console.log('Fetching thread:', id);
const res = await gmail.users.threads.get({ userId: 'me', id, format: 'full' });
if (!res.data.messages) return [];
+ // const allLabels = [...new Set(res.data.messages.flatMap(msg => msg.labelIds || []))];
+ // console.log('Thread Labels:', JSON.stringify({
+ // threadId: id,
+ // allLabels,
+ // messageLabels: res.data.messages.map(msg => ({
+ // messageId: msg.id,
+ // labels: msg.labelIds || []
+ // }))
+ // }, null, 2));
+
const messages = await Promise.all(
res.data.messages.map(async (message) => {
const bodyData =
@@ -459,15 +469,27 @@ export const driver = async (config: IConfig): Promise => {
);
return { threadIds };
},
- async modifyLabels(id: string[], options: { addLabels: string[]; removeLabels: string[] }) {
- await gmail.users.messages.batchModify({
- userId: 'me',
- requestBody: {
- ids: id,
- addLabelIds: options.addLabels,
- removeLabelIds: options.removeLabels,
- },
- });
+ async modifyLabels(threadIds: string[], options: { addLabels: string[]; removeLabels: string[] }) {
+ for (const threadId of threadIds) {
+ const thread = await gmail.users.threads.get({
+ userId: 'me',
+ id: threadId,
+ format: 'minimal'
+ });
+
+ const messageIds = thread.data.messages?.map(msg => msg.id).filter((id): id is string => !!id) || [];
+
+ if (messageIds.length > 0) {
+ await gmail.users.messages.batchModify({
+ userId: 'me',
+ requestBody: {
+ ids: messageIds,
+ addLabelIds: options.addLabels,
+ removeLabelIds: options.removeLabels,
+ },
+ });
+ }
+ }
},
getDraft: async (draftId: string) => {
try {
From bd6cb9f20e966d50ed93eb3f13833217542cff0e Mon Sep 17 00:00:00 2001
From: user12224 <122770437+user12224@users.noreply.github.com>
Date: Sat, 5 Apr 2025 22:46:58 +0200
Subject: [PATCH 3/5] cleanup
---
apps/mail/actions/mail.ts | 22 ++++++------
apps/mail/app/api/driver/google.ts | 58 ++++++++++++++++--------------
2 files changed, 43 insertions(+), 37 deletions(-)
diff --git a/apps/mail/actions/mail.ts b/apps/mail/actions/mail.ts
index 2ff880b920..f6699233e5 100644
--- a/apps/mail/actions/mail.ts
+++ b/apps/mail/actions/mail.ts
@@ -121,23 +121,23 @@ export const toggleStar = async ({ ids }: { ids: string[] }) => {
return { success: false, error: 'No thread IDs provided' };
}
+ const threadResults = await Promise.allSettled(
+ threadIds.map(id => driver.get(id))
+ );
+
let allStarred = true;
let anyValid = false;
- for (const id of threadIds) {
- try {
- const thread = await driver.get(id);
- if (thread?.[0]) {
- anyValid = true;
- if (!thread[0].tags?.includes('STARRED')) {
- allStarred = false;
- break;
- }
+ for (const result of threadResults) {
+ if (result.status === 'fulfilled' && result.value?.[0]) {
+ anyValid = true;
+ if (!result.value[0].tags?.includes('STARRED')) {
+ allStarred = false;
+ break;
}
- } catch (error) {
- continue;
}
}
+
const shouldStar = !anyValid || !allStarred;
await driver.modifyLabels(threadIds, {
diff --git a/apps/mail/app/api/driver/google.ts b/apps/mail/app/api/driver/google.ts
index bdd624e9f2..a5f3398947 100644
--- a/apps/mail/app/api/driver/google.ts
+++ b/apps/mail/app/api/driver/google.ts
@@ -314,15 +314,15 @@ export const driver = async (config: IConfig): Promise => {
const res = await gmail.users.threads.get({ userId: 'me', id, format: 'full' });
if (!res.data.messages) return [];
- // const allLabels = [...new Set(res.data.messages.flatMap(msg => msg.labelIds || []))];
- // console.log('Thread Labels:', JSON.stringify({
- // threadId: id,
- // allLabels,
- // messageLabels: res.data.messages.map(msg => ({
- // messageId: msg.id,
- // labels: msg.labelIds || []
- // }))
- // }, null, 2));
+ const allLabels = [...new Set(res.data.messages.flatMap(msg => msg.labelIds || []))];
+ console.log('Thread Labels:', JSON.stringify({
+ threadId: id,
+ allLabels,
+ messageLabels: res.data.messages.map(msg => ({
+ messageId: msg.id,
+ labels: msg.labelIds || []
+ }))
+ }, null, 2));
const messages = await Promise.all(
res.data.messages.map(async (message) => {
@@ -470,25 +470,31 @@ export const driver = async (config: IConfig): Promise => {
return { threadIds };
},
async modifyLabels(threadIds: string[], options: { addLabels: string[]; removeLabels: string[] }) {
- for (const threadId of threadIds) {
- const thread = await gmail.users.threads.get({
+ const threadResults = await Promise.allSettled(
+ threadIds.map(threadId =>
+ gmail.users.threads.get({
+ userId: 'me',
+ id: threadId,
+ format: 'minimal'
+ })
+ )
+ );
+
+ const messageIds = threadResults
+ .filter((result): result is PromiseFulfilledResult => result.status === 'fulfilled')
+ .flatMap(result => result.value.data.messages || [])
+ .map(msg => msg.id)
+ .filter((id): id is string => !!id);
+
+ if (messageIds.length > 0) {
+ await gmail.users.messages.batchModify({
userId: 'me',
- id: threadId,
- format: 'minimal'
+ requestBody: {
+ ids: messageIds,
+ addLabelIds: options.addLabels,
+ removeLabelIds: options.removeLabels,
+ },
});
-
- const messageIds = thread.data.messages?.map(msg => msg.id).filter((id): id is string => !!id) || [];
-
- if (messageIds.length > 0) {
- await gmail.users.messages.batchModify({
- userId: 'me',
- requestBody: {
- ids: messageIds,
- addLabelIds: options.addLabels,
- removeLabelIds: options.removeLabels,
- },
- });
- }
}
},
getDraft: async (draftId: string) => {
From 1c55a80c0f30892a4be55470aba409bdc51a1169 Mon Sep 17 00:00:00 2001
From: user12224 <122770437+user12224@users.noreply.github.com>
Date: Sat, 5 Apr 2025 22:52:28 +0200
Subject: [PATCH 4/5] cleanup
---
apps/mail/app/api/driver/google.ts | 18 +++++++++---------
1 file changed, 9 insertions(+), 9 deletions(-)
diff --git a/apps/mail/app/api/driver/google.ts b/apps/mail/app/api/driver/google.ts
index a5f3398947..fca0041007 100644
--- a/apps/mail/app/api/driver/google.ts
+++ b/apps/mail/app/api/driver/google.ts
@@ -314,15 +314,15 @@ export const driver = async (config: IConfig): Promise => {
const res = await gmail.users.threads.get({ userId: 'me', id, format: 'full' });
if (!res.data.messages) return [];
- const allLabels = [...new Set(res.data.messages.flatMap(msg => msg.labelIds || []))];
- console.log('Thread Labels:', JSON.stringify({
- threadId: id,
- allLabels,
- messageLabels: res.data.messages.map(msg => ({
- messageId: msg.id,
- labels: msg.labelIds || []
- }))
- }, null, 2));
+ // const allLabels = [...new Set(res.data.messages.flatMap(msg => msg.labelIds || []))];
+ // console.log('Thread Labels:', JSON.stringify({
+ // threadId: id,
+ // allLabels,
+ // messageLabels: res.data.messages.map(msg => ({
+ // messageId: msg.id,
+ // labels: msg.labelIds || []
+ // }))
+ // }, null, 2));
const messages = await Promise.all(
res.data.messages.map(async (message) => {
From dc78c0a66fa108e685b2924ff68af31e56083fcd Mon Sep 17 00:00:00 2001
From: user12224 <122770437+user12224@users.noreply.github.com>
Date: Sat, 5 Apr 2025 22:55:12 +0200
Subject: [PATCH 5/5] cleanup
---
apps/mail/app/api/driver/google.ts | 10 ----------
1 file changed, 10 deletions(-)
diff --git a/apps/mail/app/api/driver/google.ts b/apps/mail/app/api/driver/google.ts
index fca0041007..c818da94c5 100644
--- a/apps/mail/app/api/driver/google.ts
+++ b/apps/mail/app/api/driver/google.ts
@@ -314,16 +314,6 @@ export const driver = async (config: IConfig): Promise => {
const res = await gmail.users.threads.get({ userId: 'me', id, format: 'full' });
if (!res.data.messages) return [];
- // const allLabels = [...new Set(res.data.messages.flatMap(msg => msg.labelIds || []))];
- // console.log('Thread Labels:', JSON.stringify({
- // threadId: id,
- // allLabels,
- // messageLabels: res.data.messages.map(msg => ({
- // messageId: msg.id,
- // labels: msg.labelIds || []
- // }))
- // }, null, 2));
-
const messages = await Promise.all(
res.data.messages.map(async (message) => {
const bodyData =