From 4849cf233ef192f70d9587625b0ae2dea195ad70 Mon Sep 17 00:00:00 2001
From: Linus Rath <139418639+rathlinus@users.noreply.github.com>
Date: Sat, 31 Oct 2026 04:11:17 +0100
Subject: [PATCH] feat: show typed, coloured file icons on the reader's
attachment chips
---
.../__tests__/reader-attachment-icons.test.ts | 21 ++++++++++++++
src/components/email/AttachmentChips.tsx | 9 ++++--
src/components/email/ListAttachmentChips.tsx | 28 +++----------------
src/components/email/attachment-icons.ts | 28 +++++++++++++++++++
4 files changed, 60 insertions(+), 26 deletions(-)
create mode 100644 src/components/__tests__/reader-attachment-icons.test.ts
create mode 100644 src/components/email/attachment-icons.ts
diff --git a/src/components/__tests__/reader-attachment-icons.test.ts b/src/components/__tests__/reader-attachment-icons.test.ts
new file mode 100644
index 00000000..c4dacbc9
--- /dev/null
+++ b/src/components/__tests__/reader-attachment-icons.test.ts
@@ -0,0 +1,21 @@
+import { describe, expect, it } from 'vitest';
+import fs from 'node:fs';
+import path from 'node:path';
+
+// List chips showed a typed, coloured icon per file; the reader's chips a
+// grey paperclip for everything (webmail a78d03a18 uses the list's icons in
+// the reader too).
+
+const src = (p: string) => fs.readFileSync(path.resolve(__dirname, '..', p), 'utf8');
+
+describe('reader attachment chip icons', () => {
+ it('draws the same typed icon and colour as the list chips', () => {
+ const reader = src('email/AttachmentChips.tsx');
+ expect(reader).not.toMatch(//);
+ const list = src('email/ListAttachmentChips.tsx');
+ expect(list).toMatch(/ATTACHMENT_ICONS\[kind\]/);
+ expect(list).toMatch(/attachmentIconColor\(kind, c\)/);
+ });
+});
diff --git a/src/components/email/AttachmentChips.tsx b/src/components/email/AttachmentChips.tsx
index b194485f..88599955 100644
--- a/src/components/email/AttachmentChips.tsx
+++ b/src/components/email/AttachmentChips.tsx
@@ -1,6 +1,6 @@
import React from 'react';
import { View, Text, StyleSheet, Pressable, ActivityIndicator, Alert } from 'react-native';
-import { Paperclip, Download, Eye, ExternalLink, Save, Share2, FolderArchive, FolderOpen } from '../icons';
+import { Download, Eye, ExternalLink, Save, Share2, FolderArchive, FolderOpen } from '../icons';
import type { Attachment, Email } from '../../api/types';
import { spacing, radius, typography, type ThemePalette } from '../../theme/tokens';
import { useColors } from '../../theme/colors';
@@ -13,6 +13,8 @@ import {
getAttachmentDisplayName, visibleAttachments, formatSize,
} from '../../lib/attachment-display';
import { emailExportFilename } from '../../lib/download-filename';
+import { attachmentKind } from '../../lib/list-attachments';
+import { ATTACHMENT_ICONS, attachmentIconColor } from './attachment-icons';
import { ActionSheet, type ActionSheetItem } from './ActionSheet';
import { AttachmentPreviewModal } from './AttachmentPreviewModal';
import type { ExtractedAttachment } from './use-body-override';
@@ -232,6 +234,9 @@ export function AttachmentChips({ email, jmapAccountId, calendarBannerShown, tne
{visible.map((item) => {
const busy = busyKey === item.key;
+ // Typed and coloured like the list's chips (webmail a78d03a18).
+ const kind = attachmentKind(item.type, item.name);
+ const Icon = ATTACHMENT_ICONS[kind];
return (
- {busy ? : }
+ {busy ? : }
{item.name}
{formatSize(item.size)}
diff --git a/src/components/email/ListAttachmentChips.tsx b/src/components/email/ListAttachmentChips.tsx
index a457666f..51f68c9a 100644
--- a/src/components/email/ListAttachmentChips.tsx
+++ b/src/components/email/ListAttachmentChips.tsx
@@ -1,6 +1,5 @@
import React from 'react';
import { Alert, Pressable, StyleSheet, Text, View } from 'react-native';
-import { File as FileIcon, FileArchive, FileImage, FileSpreadsheet, FileText } from '../icons';
import type { Attachment, Email, Mailbox } from '../../api/types';
import { getEmailAttachments } from '../../api/email';
import { radius, spacing, typography, type ThemePalette } from '../../theme/tokens';
@@ -13,8 +12,9 @@ import { accountIdOfRow } from '../../stores/email-store';
import { accountScopedId } from '../../lib/thread-utils';
import {
attachmentKind, realAttachments, requestListAttachments, shortAttachmentName,
- type AttachmentKind, type LoadListAttachments,
+ type LoadListAttachments,
} from '../../lib/list-attachments';
+import { ATTACHMENT_ICONS, attachmentIconColor } from './attachment-icons';
import { getAttachmentDisplayName } from '../../lib/attachment-display';
import { attachmentSaveName, downloadAttachment } from '../../lib/email-export';
import { AttachmentPreviewModal } from './AttachmentPreviewModal';
@@ -23,26 +23,6 @@ import { useFilePreview } from './use-file-preview';
/** How many chips a row shows before collapsing the rest into a count. */
const MAX_CHIPS = 2;
-const ICONS: Record = {
- image: FileImage,
- pdf: FileText,
- sheet: FileSpreadsheet,
- archive: FileArchive,
- document: FileText,
- other: FileIcon,
-};
-
-function iconColor(kind: AttachmentKind, c: ThemePalette): string {
- switch (kind) {
- case 'image': return c.tags.purple.dot;
- case 'pdf': return c.tags.red.dot;
- case 'sheet': return c.tags.green.dot;
- case 'archive': return c.tags.amber.dot;
- case 'document': return c.tags.blue.dot;
- default: return c.textMuted;
- }
-}
-
/**
* The row's attachment parts: the ones the email already carries (a message
* opened before), else loaded lazily when it has a paperclip.
@@ -94,7 +74,7 @@ export const ListAttachmentChips = React.memo(function ListAttachmentChips({
{shown.map((a, i) => {
const kind = attachmentKind(a.type, a.name);
- const Icon = ICONS[kind];
+ const Icon = ATTACHMENT_ICONS[kind];
return (
-
+
{shortAttachmentName(a.name ?? '')}
);
diff --git a/src/components/email/attachment-icons.ts b/src/components/email/attachment-icons.ts
new file mode 100644
index 00000000..88920a0b
--- /dev/null
+++ b/src/components/email/attachment-icons.ts
@@ -0,0 +1,28 @@
+import { File as FileIcon, FileArchive, FileImage, FileSpreadsheet, FileText } from '../icons';
+import type { ThemePalette } from '../../theme/tokens';
+import type { AttachmentKind } from '../../lib/list-attachments';
+
+/**
+ * The icon of an attachment chip by file type, the same in the list and the
+ * reader (webmail components/email/attachment-chips.tsx, a78d03a18).
+ */
+export const ATTACHMENT_ICONS: Record = {
+ image: FileImage,
+ pdf: FileText,
+ sheet: FileSpreadsheet,
+ archive: FileArchive,
+ document: FileText,
+ other: FileIcon,
+};
+
+/** The icon's colour by file type. */
+export function attachmentIconColor(kind: AttachmentKind, c: ThemePalette): string {
+ switch (kind) {
+ case 'image': return c.tags.purple.dot;
+ case 'pdf': return c.tags.red.dot;
+ case 'sheet': return c.tags.green.dot;
+ case 'archive': return c.tags.amber.dot;
+ case 'document': return c.tags.blue.dot;
+ default: return c.textMuted;
+ }
+}