feat: implement email keyword management with color tagging

- Introduced a new settings section for managing email keywords (labels/tags) with associated colors.
- Updated email context menu, viewer, and list item components to support the new keyword system.
- Refactored color tag handling to accommodate both new $label: and legacy $color: prefixes.
- Added utility functions for retrieving email color tags based on keywords.
- Enhanced localization files to include translations for the new keyword management features.
- Updated the settings store to manage keyword definitions, including add, update, delete, and reorder functionalities.
- Added tests for the new keyword handling logic.
This commit is contained in:
Linus Rath
2026-03-11 18:40:09 +01:00
parent 61459c4923
commit ffeb534740
19 changed files with 558 additions and 100 deletions
+15 -15
View File
@@ -19,7 +19,7 @@ import {
Trash2,
Archive,
FolderInput,
Palette,
Tag,
X,
Inbox,
Send,
@@ -29,6 +29,7 @@ import {
ShieldCheck,
} from "lucide-react";
import { cn } from "@/lib/utils";
import { useSettingsStore, KEYWORD_PALETTE } from "@/stores/settings-store";
interface Position {
x: number;
@@ -84,12 +85,14 @@ const getMailboxIcon = (role?: string) => {
}
};
// Get current color from email keywords
// Get current label/color from email keywords (supports both $label: and legacy $color:)
const getCurrentColor = (keywords: Record<string, boolean> | undefined) => {
if (!keywords) return null;
for (const key of Object.keys(keywords)) {
if (key.startsWith("$color:") && keywords[key] === true) {
return key.replace("$color:", "");
if ((key.startsWith("$label:") || key.startsWith("$color:")) && keywords[key] === true) {
return key.startsWith("$label:")
? key.slice("$label:".length)
: key.slice("$color:".length);
}
}
return null;
@@ -125,22 +128,19 @@ export function EmailContextMenu({
}: EmailContextMenuProps) {
const t = useTranslations("context_menu");
const tColor = useTranslations("email_viewer.color_tag");
const emailKeywords = useSettingsStore((state) => state.emailKeywords);
const isUnread = !email.keywords?.$seen;
const isStarred = email.keywords?.$flagged;
const currentColor = getCurrentColor(email.keywords);
const showBatchActions = isMultiSelect && selectedCount > 1;
const isInJunkFolder = currentMailboxRole === 'junk';
// Color options for email tags (using translations)
const colorOptions = [
{ name: tColor("red"), value: "red", color: "bg-red-500" },
{ name: tColor("orange"), value: "orange", color: "bg-orange-500" },
{ name: tColor("yellow"), value: "yellow", color: "bg-yellow-500" },
{ name: tColor("green"), value: "green", color: "bg-green-500" },
{ name: tColor("blue"), value: "blue", color: "bg-blue-500" },
{ name: tColor("purple"), value: "purple", color: "bg-purple-500" },
{ name: tColor("pink"), value: "pink", color: "bg-pink-500" },
];
// Build color options from keyword definitions in settings
const colorOptions = emailKeywords.map((kw) => ({
name: kw.label,
value: kw.id,
color: KEYWORD_PALETTE[kw.color]?.dot || "bg-gray-500",
}));
// Filter mailboxes for move-to submenu (exclude current, drafts, virtual nodes)
const moveTargets = mailboxes.filter(
@@ -272,7 +272,7 @@ export function EmailContextMenu({
{/* Set color submenu - only for single email */}
{!showBatchActions && (
<ContextMenuSubMenu icon={Palette} label={t("color_tag")}>
<ContextMenuSubMenu icon={Tag} label={t("color_tag")}>
<div
className="px-3 py-2 flex flex-wrap gap-2"
role="group"