Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/mail/app/(routes)/mail/[folder]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export default function MailPage() {

const isStandardFolder = ALLOWED_FOLDERS.includes(folder);

const { data: userLabels, isLoading: isLoadingLabels } = useLabels();
const { userLabels, isLoading: isLoadingLabels } = useLabels();

useEffect(() => {
if (isStandardFolder) {
Expand Down
16 changes: 8 additions & 8 deletions apps/mail/app/(routes)/settings/labels/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,18 @@ import { HexColorPicker } from 'react-colorful';
import { Bin } from '@/components/icons/icons';
import { useLabels } from '@/hooks/use-labels';
import { GMAIL_COLORS } from '@/lib/constants';
import { Badge } from '@/components/ui/badge';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { m } from '@/paraglide/messages';
import { Input } from '@/components/ui/input';
import { Badge } from '@/components/ui/badge';
import { useForm } from 'react-hook-form';
import { m } from '@/paraglide/messages';
import { Command } from 'lucide-react';
import { COLORS } from './colors';
import { useState } from 'react';
import { toast } from 'sonner';

export default function LabelsPage() {
const { data: labels, isLoading, error, refetch } = useLabels();
const { userLabels: labels, isLoading, error, refetch } = useLabels();
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [editingLabel, setEditingLabel] = useState<LabelType | null>(null);

Expand All @@ -63,7 +63,7 @@ export default function LabelsPage() {

const handleDelete = async (id: string) => {
toast.promise(deleteLabel({ id }), {
loading: m['common.labels.deletingLabel'](),
loading: m['common.labels.deletingLabel'](),
success: m['common.labels.deleteLabelSuccess'](),
error: m['common.labels.failedToDeleteLabel'](),
finally: async () => {
Expand Down Expand Up @@ -113,7 +113,7 @@ export default function LabelsPage() {
<p className="text-muted-foreground py-4 text-center text-sm">{error.message}</p>
) : labels?.length === 0 ? (
<p className="text-muted-foreground py-4 text-center text-sm">
{m['common.mail.noLabelsAvailable']()}
{m['common.mail.noLabelsAvailable']()}
</p>
) : (
<div className="grid grid-cols-2 gap-2 sm:grid-cols-4 md:grid-cols-6">
Expand Down Expand Up @@ -147,7 +147,7 @@ export default function LabelsPage() {
</Button>
</TooltipTrigger>
<TooltipContent className="dark:bg-panelDark mb-1 bg-white">
{m['common.labels.editLabel']()}
{m['common.labels.editLabel']()}
</TooltipContent>
</Tooltip>
<Tooltip>
Expand All @@ -162,7 +162,7 @@ export default function LabelsPage() {
</Button>
</TooltipTrigger>
<TooltipContent className="dark:bg-panelDark mb-1 bg-white">
{m['common.labels.deleteLabel']()}
{m['common.labels.deleteLabel']()}
</TooltipContent>
</Tooltip>
</div>
Expand Down
2 changes: 1 addition & 1 deletion apps/mail/app/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export default [
route('/danger-zone', '(routes)/settings/danger-zone/page.tsx'),
route('/general', '(routes)/settings/general/page.tsx'),
route('/labels', '(routes)/settings/labels/page.tsx'),
route('/categories', '(routes)/settings/categories/page.tsx'),
// route('/categories', '(routes)/settings/categories/page.tsx'),
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Consider replacing commented-out route with a feature flag or disabled meta instead of leaving dead code

Commenting out the /categories route works short-term but silently grows dead code and makes future re-enablement harder to track. Prefer a feature-flag guard (e.g., enabled: false) or a // TODO(date): note so tooling can pick it up and avoid accidental rot.

Please confirm there are no deep links or tests still hitting /settings/categories. If unsure, run:


🏁 Script executed:

#!/bin/bash
# Find any references to the removed categories route
rg -n "/settings/categories"

Length of output: 376


Introduce a feature‐flag or disabled meta for the /settings/categories route

Rather than leaving both the route and its navigation entry commented out, encode the intent directly so future tooling and maintainers can detect and re‐enable it safely. For example:

• In apps/mail/app/routes.ts (around line 45):

- // route('/categories', '(routes)/settings/categories/page.tsx'),
+ route('/categories', '(routes)/settings/categories/page.tsx', { disabled: true }),

• In apps/mail/config/navigation.ts (around line 174):

- //     url: '/settings/categories',
+     {
+       url: '/settings/categories',
+       label: t('navigation/settings/categories'),
+       disabled: true,
+     },

This makes it explicit that the feature exists but is currently inactive, and can be surfaced by linters or custom scripts.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In apps/mail/app/routes.ts at line 45, replace the commented-out route for
'/categories' with a route definition that includes a feature flag or a disabled
meta property indicating the route is inactive. This explicit flag should
clearly mark the route as currently disabled but present, enabling future
tooling and maintainers to detect and safely re-enable it without relying on
commented code.

route('/notifications', '(routes)/settings/notifications/page.tsx'),
route('/privacy', '(routes)/settings/privacy/page.tsx'),
route('/security', '(routes)/settings/security/page.tsx'),
Expand Down
2 changes: 1 addition & 1 deletion apps/mail/components/context/command-palette-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ export function CommandPalette({ children }: { children: React.ReactNode }) {
const navigate = useNavigate();
const { pathname } = useLocation();

const { data: userLabels = [] } = useLabels();
const { userLabels = [] } = useLabels();
const trpc = useTRPC();
const { mutateAsync: generateSearchQuery, isPending } = useMutation(
trpc.ai.generateSearchQuery.mutationOptions(),
Expand Down
179 changes: 91 additions & 88 deletions apps/mail/components/context/thread-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ interface EmailContextMenuProps {
}

const LabelsList = ({ threadId, bulkSelected }: { threadId: string; bulkSelected: string[] }) => {
const { data: labels } = useLabels();
const { userLabels: labels } = useLabels();
const { optimisticToggleLabel } = useOptimisticActions();
const targetThreadIds = bulkSelected.length > 0 ? bulkSelected : [threadId];

Expand Down Expand Up @@ -141,35 +141,40 @@ export function ThreadContextMenu({
const [, setMode] = useQueryState('mode');
const [, setThreadId] = useQueryState('threadId');
const { data: threadData } = useThread(threadId);
const [, setActiveReplyId] = useQueryState('activeReplyId');
const optimisticState = useOptimisticThreadState(threadId);

const isUnread = useMemo(() => {
return threadData?.hasUnread ?? false;
}, [threadData]);

const isStarred = useMemo(() => {
const {
optimisticMoveThreadsTo,
optimisticToggleStar,
optimisticToggleImportant,
optimisticMarkAsRead,
optimisticMarkAsUnread,
optimisticDeleteThreads,
} = useOptimisticActions();

const { isUnread, isStarred, isImportant } = useMemo(() => {
const unread = threadData?.hasUnread ?? false;

let starred;
if (optimisticState.optimisticStarred !== null) {
return optimisticState.optimisticStarred;
starred = optimisticState.optimisticStarred;
} else {
starred = threadData?.messages.some((message) =>
message.tags?.some((tag) => tag.name.toLowerCase() === 'starred'),
);
}
return threadData?.messages.some((message) =>
message.tags?.some((tag) => tag.name.toLowerCase() === 'starred'),
);
}, [threadData, optimisticState.optimisticStarred]);

const isImportant = useMemo(() => {
let important;
if (optimisticState.optimisticImportant !== null) {
return optimisticState.optimisticImportant;
important = optimisticState.optimisticImportant;
} else {
important = threadData?.messages.some((message) =>
message.tags?.some((tag) => tag.name.toLowerCase() === 'important'),
);
}
return threadData?.messages.some((message) =>
message.tags?.some((tag) => tag.name.toLowerCase() === 'important'),
);
}, [threadData]);

const noopAction = () => async () => {
toast.info(m['common.actions.featureNotImplemented']());
};

const { optimisticMoveThreadsTo } = useOptimisticActions();
return { isUnread: unread, isStarred: starred, isImportant: important };
}, [threadData, optimisticState.optimisticStarred, optimisticState.optimisticImportant]);

const handleMove = (from: string, to: string) => () => {
try {
Expand Down Expand Up @@ -197,8 +202,6 @@ export function ThreadContextMenu({
}
};

const { optimisticToggleStar } = useOptimisticActions();

const handleFavorites = () => {
const targets = mail.bulkSelected.length ? mail.bulkSelected : [threadId];

Expand All @@ -211,8 +214,6 @@ export function ThreadContextMenu({
}
};

const { optimisticToggleImportant } = useOptimisticActions();

const handleToggleImportant = () => {
const targets = mail.bulkSelected.length ? mail.bulkSelected : [threadId];
const newImportantState = !isImportant;
Expand All @@ -226,8 +227,6 @@ export function ThreadContextMenu({
}
};

const { optimisticMarkAsRead, optimisticMarkAsUnread } = useOptimisticActions();

const handleReadUnread = () => {
const targets = mail.bulkSelected.length ? mail.bulkSelected : [threadId];
const newReadState = isUnread; // If currently unread, mark as read (true)
Expand All @@ -246,7 +245,6 @@ export function ThreadContextMenu({
setMail((prev) => ({ ...prev, bulkSelected: [] }));
}
};
const [, setActiveReplyId] = useQueryState('activeReplyId');

const handleThreadReply = () => {
setMode('reply');
Expand All @@ -266,30 +264,32 @@ export function ThreadContextMenu({
if (threadData?.latest) setActiveReplyId(threadData?.latest?.id);
};

const primaryActions: EmailAction[] = [
{
id: 'reply',
label: m['common.mail.reply'](),
icon: <Reply className="mr-2.5 h-4 w-4 opacity-60" />,
action: handleThreadReply,
disabled: false,
},
{
id: 'reply-all',
label: m['common.mail.replyAll'](),
icon: <ReplyAll className="mr-2.5 h-4 w-4 opacity-60" />,
action: handleThreadReplyAll,
disabled: false,
},
{
id: 'forward',
label: m['common.mail.forward'](),
icon: <Forward className="mr-2.5 h-4 w-4 opacity-60" />,
action: handleThreadForward,
disabled: false,
},
];
const { optimisticDeleteThreads } = useOptimisticActions();
const primaryActions: EmailAction[] = useMemo(
() => [
{
id: 'reply',
label: m['common.mail.reply'](),
icon: <Reply className="mr-2.5 h-4 w-4 opacity-60" />,
action: handleThreadReply,
disabled: false,
},
{
id: 'reply-all',
label: m['common.mail.replyAll'](),
icon: <ReplyAll className="mr-2.5 h-4 w-4 opacity-60" />,
action: handleThreadReplyAll,
disabled: false,
},
{
id: 'forward',
label: m['common.mail.forward'](),
icon: <Forward className="mr-2.5 h-4 w-4 opacity-60" />,
action: handleThreadForward,
disabled: false,
},
],
[m, handleThreadReply, handleThreadReplyAll, handleThreadForward],
);

const handleDelete = () => () => {
const targets = mail.bulkSelected.length ? mail.bulkSelected : [threadId];
Expand All @@ -308,7 +308,7 @@ export function ThreadContextMenu({
// }
};

const getActions = () => {
const getActions = useMemo(() => {
if (isSpam) {
return [
{
Expand Down Expand Up @@ -408,39 +408,42 @@ export function ThreadContextMenu({
disabled: false,
},
];
};
}, [isSpam, isBin, isArchiveFolder, isInbox, isSent, handleMove, handleDelete]);

const otherActions: EmailAction[] = [
{
id: 'toggle-read',
label: isUnread ? m['common.mail.markAsRead']() : m['common.mail.markAsUnread'](),
icon: !isUnread ? (
<Mail className="mr-2.5 h-4 w-4 fill-[#9D9D9D] dark:fill-[#9D9D9D]" />
) : (
<MailOpen className="mr-2.5 h-4 w-4 opacity-60" />
),
action: handleReadUnread,
disabled: false,
},
{
id: 'toggle-important',
label: isImportant
? m['common.mail.removeFromImportant']()
: m['common.mail.markAsImportant'](),
icon: <ExclamationCircle className="mr-2.5 h-4 w-4 opacity-60" />,
action: handleToggleImportant,
},
{
id: 'favorite',
label: isStarred ? m['common.mail.removeFavorite']() : m['common.mail.addFavorite'](),
icon: isStarred ? (
<StarOff className="mr-2.5 h-4 w-4 opacity-60" />
) : (
<Star className="mr-2.5 h-4 w-4 opacity-60" />
),
action: handleFavorites,
},
];
const otherActions: EmailAction[] = useMemo(
() => [
{
id: 'toggle-read',
label: isUnread ? m['common.mail.markAsRead']() : m['common.mail.markAsUnread'](),
icon: !isUnread ? (
<Mail className="mr-2.5 h-4 w-4 fill-[#9D9D9D] dark:fill-[#9D9D9D]" />
) : (
<MailOpen className="mr-2.5 h-4 w-4 opacity-60" />
),
action: handleReadUnread,
disabled: false,
},
{
id: 'toggle-important',
label: isImportant
? m['common.mail.removeFromImportant']()
: m['common.mail.markAsImportant'](),
icon: <ExclamationCircle className="mr-2.5 h-4 w-4 opacity-60" />,
action: handleToggleImportant,
},
{
id: 'favorite',
label: isStarred ? m['common.mail.removeFavorite']() : m['common.mail.addFavorite'](),
icon: isStarred ? (
<StarOff className="mr-2.5 h-4 w-4 opacity-60" />
) : (
<Star className="mr-2.5 h-4 w-4 opacity-60" />
),
action: handleFavorites,
},
],
[isUnread, isImportant, isStarred, m, handleReadUnread, handleToggleImportant, handleFavorites],
);

const renderAction = (action: EmailAction) => {
return (
Expand Down Expand Up @@ -482,7 +485,7 @@ export function ThreadContextMenu({

<ContextMenuSeparator className="bg-[#E7E7E7] dark:bg-[#252525]" />

{getActions().map(renderAction as any)}
{getActions.map(renderAction)}

<ContextMenuSeparator className="bg-[#E7E7E7] dark:bg-[#252525]" />

Expand Down
Loading