fix: show tags with no local definition in the message header, as the list does #79

This commit is contained in:
Linus Rath
2026-10-19 13:02:29 +02:00
parent 86400b124a
commit 2b486cb1ef
3 changed files with 29 additions and 5 deletions
+3 -5
View File
@@ -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;
+10
View File
@@ -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');
+16
View File
@@ -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<string, boolean> | 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<string, boolean> | 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.