From 2b486cb1ef0104b9369b5c5170f37e7c93ea72ff Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Mon, 19 Oct 2026 13:02:29 +0200 Subject: [PATCH] fix: show tags with no local definition in the message header, as the list does #79 --- src/components/email/MessageHeader.tsx | 8 +++----- src/lib/__tests__/thread-utils.test.ts | 10 ++++++++++ src/lib/thread-utils.ts | 16 ++++++++++++++++ 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/src/components/email/MessageHeader.tsx b/src/components/email/MessageHeader.tsx index 3d8bca3a..bd599b3c 100644 --- a/src/components/email/MessageHeader.tsx +++ b/src/components/email/MessageHeader.tsx @@ -9,7 +9,7 @@ import { useColors } from '../../theme/colors'; import SenderAvatar from '../SenderAvatar'; import { useSettingsStore } from '../../stores/settings-store'; import { useLocaleStore } from '../../stores/locale-store'; -import { useKeywordsStore, keywordToken } from '../../stores/keywords-store'; +import { useKeywordsStore } from '../../stores/keywords-store'; import { emailDisplayDate, formatHeaderDate, formatHeaderTime, formatFullDateTime } from '../../lib/email-date'; import { deriveHeaderInfo, deliveryDeltaMs, formatDelta, isAuthenticationSpoofed, findReceivingIdentity, @@ -17,6 +17,7 @@ import { } from '../../lib/email-headers'; import { formatSize } from '../../lib/attachment-display'; import { isSmimeEmail } from '../../lib/smime'; +import { resolveEmailTags } from '../../lib/thread-utils'; interface Props { email: Email; @@ -121,10 +122,7 @@ export function MessageHeader({ email, identities, headerInfo, onToggleStar, onA return null; }, [spoofed, identities, from?.email, email]); - const tags = React.useMemo( - () => keywordDefs.filter((kw) => !!email.keywords?.[keywordToken(kw.id)]), - [keywordDefs, email.keywords], - ); + const tags = React.useMemo(() => resolveEmailTags(email.keywords, keywordDefs), [keywordDefs, email.keywords]); const important = !!email.keywords?.$important; const answered = !!email.keywords?.$answered; const forwarded = !!email.keywords?.$forwarded; diff --git a/src/lib/__tests__/thread-utils.test.ts b/src/lib/__tests__/thread-utils.test.ts index 13ff3814..80099775 100644 --- a/src/lib/__tests__/thread-utils.test.ts +++ b/src/lib/__tests__/thread-utils.test.ts @@ -4,6 +4,7 @@ import { expandThreadSelection, getEmailTagIds, getThreadTagIds, + resolveEmailTags, tagIdFromKeyword, threadKeyOf, rowKeyOf, @@ -30,6 +31,15 @@ describe('tag ids', () => { ])).toEqual(['work', 'todo']); }); + it('keeps a tag with no local definition, grey and under its raw id (#79)', () => { + const defs = [{ id: 'work', label: 'Work', color: 'blue' as const }]; + expect(resolveEmailTags({ '$label:work': true, '$label:pgp-encrypted': true, $seen: true }, defs)).toEqual([ + { id: 'work', label: 'Work', color: 'blue' }, + { id: 'pgp-encrypted', label: 'pgp-encrypted', color: 'gray' }, + ]); + expect(resolveEmailTags(undefined, defs)).toEqual([]); + }); + it('extracts the tag id from a keyword', () => { expect(tagIdFromKeyword('$label:work')).toBe('work'); expect(tagIdFromKeyword('$color:work')).toBe('work'); diff --git a/src/lib/thread-utils.ts b/src/lib/thread-utils.ts index ad65ced3..84a4529c 100644 --- a/src/lib/thread-utils.ts +++ b/src/lib/thread-utils.ts @@ -1,4 +1,5 @@ import type { Email } from '../api/types'; +import type { KeywordDef } from '../stores/keywords-store'; // Port of the webmail's `lib/thread-utils.ts` (tag id extraction, thread // grouping helpers) trimmed to what the native list needs. @@ -23,6 +24,21 @@ export function getEmailTagIds(keywords: Record | undefined): s return [...tags]; } +/** + * The tags to draw for a message: every tag id on it, with its local + * definition where there is one. A tag no local definition explains (set by + * another client or by the server) is kept, grey and labelled with its raw + * id, so the header shows the same tags as the list row (#79). + */ +export function resolveEmailTags( + keywords: Record | undefined, + defs: readonly KeywordDef[], +): KeywordDef[] { + return getEmailTagIds(keywords).map( + (id) => defs.find((d) => d.id === id) ?? { id, label: id, color: 'gray' }, + ); +} + /** * Every tag anywhere in a thread, deduplicated. A collapsed thread row stands * in for all its messages, so it has to account for all their tags.