)}
diff --git a/components/email/tag-badge.tsx b/components/email/tag-badge.tsx
index 9c61f6d8..ca36f35f 100644
--- a/components/email/tag-badge.tsx
+++ b/components/email/tag-badge.tsx
@@ -1,5 +1,7 @@
"use client";
+import { useTranslations } from "next-intl";
+import { X } from "lucide-react";
import { cn } from "@/lib/utils";
import { useKeywordFormat } from "@/hooks/use-keyword-format";
import { useShortenedText } from "@/hooks/use-shortened-text";
@@ -38,12 +40,19 @@ export const TAG_GROUP_CLASS = "flex shrink-0 items-center gap-1";
export function TagBadge({
tagId,
variant,
+ onRemove,
className,
}: {
tagId: string;
variant: TagBadgeVariant;
+ /**
+ * Takes the tag off the message. Only the named form offers it - a dot is the
+ * size of the control it would have to hold.
+ */
+ onRemove?: () => void;
className?: string;
}) {
+ const t = useTranslations("email_viewer");
const { tagName, tagNameCandidates, tagColor } = useKeywordFormat();
const [labelRef, shortenedName] = useShortenedText(tagNameCandidates(tagId));
const color = tagColor(tagId);
@@ -61,10 +70,9 @@ export function TagBadge({
return (
- {shortenedName}
+
+ {shortenedName}
+
+ {onRemove && (
+
+ )}
);
}
diff --git a/components/email/tag-picker.tsx b/components/email/tag-picker.tsx
index e2df5e1d..0f0948ef 100644
--- a/components/email/tag-picker.tsx
+++ b/components/email/tag-picker.tsx
@@ -2,7 +2,7 @@
import { useMemo, useState } from "react";
import { useTranslations } from "next-intl";
-import { Check, Search, X } from "lucide-react";
+import { Check, Search } from "lucide-react";
import { cn } from "@/lib/utils";
import { useSettingsStore } from "@/stores/settings-store";
import { buildKeywordTree, type KeywordNode } from "@/lib/keyword-nesting";
@@ -26,12 +26,10 @@ const SEARCH_THRESHOLD = 10;
export function TagPicker({
selectedIds,
onToggle,
- onClearAll,
touch = false,
}: {
selectedIds: string[];
onToggle: (tagId: string) => void;
- onClearAll?: () => void;
/** Larger hit areas for the mobile sheet. */
touch?: boolean;
}) {
@@ -42,15 +40,32 @@ export function TagPicker({
const [query, setQuery] = useState("");
const trimmedQuery = query.trim().toLowerCase();
- const showSearch = keywords.length >= SEARCH_THRESHOLD;
+
+ /**
+ * Tags on the message this client has no definition for - set from another
+ * client, or outliving the tag they were made with. Listing them is the only
+ * way to take one off, and they leave the list as they are deselected because
+ * nothing but the message itself records that they exist.
+ */
+ const unknownIds = useMemo(
+ () =>
+ selectedIds
+ .filter((id) => !keywords.some((keyword) => keyword.id === id))
+ .sort((a, b) => tagName(a).localeCompare(tagName(b))),
+ // `tagName` is rebuilt whenever the definitions or the nesting setting change.
+ [selectedIds, keywords, tagName],
+ );
+
+ const showSearch = keywords.length + unknownIds.length >= SEARCH_THRESHOLD;
const matches = useMemo(
() =>
trimmedQuery
- ? keywords.filter((keyword) => tagName(keyword.id).toLowerCase().includes(trimmedQuery))
+ ? [...keywords.map((keyword) => keyword.id), ...unknownIds].filter((id) =>
+ tagName(id).toLowerCase().includes(trimmedQuery),
+ )
: [],
- // `tagName` is rebuilt whenever the definitions or the nesting setting change.
- [keywords, trimmedQuery, tagName],
+ [keywords, unknownIds, trimmedQuery, tagName],
);
const tree = useMemo(
@@ -111,28 +126,22 @@ export function TagPicker({