feat: show typed, coloured file icons on the reader's attachment chips

This commit is contained in:
Linus Rath
2026-10-31 04:11:17 +01:00
parent 6d701c4b06
commit 4849cf233e
4 changed files with 60 additions and 26 deletions
@@ -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(/<Paperclip /);
expect(reader).toMatch(/const kind = attachmentKind\(item\.type, item\.name\);/);
expect(reader).toMatch(/<Icon size=\{14\} color=\{attachmentIconColor\(kind, c\)\} \/>/);
const list = src('email/ListAttachmentChips.tsx');
expect(list).toMatch(/ATTACHMENT_ICONS\[kind\]/);
expect(list).toMatch(/attachmentIconColor\(kind, c\)/);
});
});
+7 -2
View File
@@ -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
<View style={styles.row}>
{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 (
<Pressable
key={item.key}
@@ -241,7 +246,7 @@ export function AttachmentChips({ email, jmapAccountId, calendarBannerShown, tne
disabled={!!busyKey}
accessibilityLabel={item.name}
>
{busy ? <ActivityIndicator size="small" color={c.textMuted} /> : <Paperclip size={14} color={c.textMuted} />}
{busy ? <ActivityIndicator size="small" color={c.textMuted} /> : <Icon size={14} color={attachmentIconColor(kind, c)} />}
<Text style={styles.name} numberOfLines={1}>{item.name}</Text>
<Text style={styles.size}>{formatSize(item.size)}</Text>
<Download size={14} color={c.textMuted} />
+4 -24
View File
@@ -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<AttachmentKind, typeof FileIcon> = {
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({
<View style={styles.row}>
{shown.map((a, i) => {
const kind = attachmentKind(a.type, a.name);
const Icon = ICONS[kind];
const Icon = ATTACHMENT_ICONS[kind];
return (
<Pressable
key={`${a.blobId}-${i}`}
@@ -106,7 +86,7 @@ export const ListAttachmentChips = React.memo(function ListAttachmentChips({
accessibilityRole="button"
accessibilityLabel={a.name}
>
<Icon size={12} color={iconColor(kind, c)} />
<Icon size={12} color={attachmentIconColor(kind, c)} />
<Text style={[styles.name, dyn.caption]} numberOfLines={1}>{shortAttachmentName(a.name ?? '')}</Text>
</Pressable>
);
+28
View File
@@ -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<AttachmentKind, typeof FileIcon> = {
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;
}
}