mirror of
https://github.com/bulwarkmail/native.git
synced 2026-11-01 00:07:01 +00:00
fix: drop the paperclip from messages with only inline images and keep list rows from growing when chips arrive
This commit is contained in:
@@ -27,7 +27,7 @@ const MAX_CHIPS = 2;
|
||||
* The row's attachment parts: the ones the email already carries (a message
|
||||
* opened before), else loaded lazily when it has a paperclip.
|
||||
*/
|
||||
function useListAttachments(email: Email, load?: LoadListAttachments): Attachment[] | undefined {
|
||||
export function useListAttachments(email: Email, load?: LoadListAttachments): Attachment[] | undefined {
|
||||
const [loaded, setLoaded] = React.useState<{ key: string; attachments: Attachment[] } | null>(null);
|
||||
const needsLoad = !!load && !!email.hasAttachment && !email.attachments;
|
||||
// Rows of a list spanning accounts can share an id (#1082).
|
||||
@@ -47,7 +47,10 @@ function useListAttachments(email: Email, load?: LoadListAttachments): Attachmen
|
||||
|
||||
interface ChipsProps {
|
||||
email: Email;
|
||||
load?: LoadListAttachments;
|
||||
/** The row's parts (useListAttachments); undefined while they load. */
|
||||
attachments: Attachment[] | undefined;
|
||||
/** The parts are being loaded: hold the chips' line so the row doesn't grow. */
|
||||
pending?: boolean;
|
||||
onOpen: (email: Email, attachment: Attachment) => void;
|
||||
/** Selection mode: taps belong to the row. */
|
||||
disabled?: boolean;
|
||||
@@ -59,13 +62,23 @@ interface ChipsProps {
|
||||
* that file directly.
|
||||
*/
|
||||
export const ListAttachmentChips = React.memo(function ListAttachmentChips({
|
||||
email, load, onOpen, disabled,
|
||||
email, attachments, pending, onOpen, disabled,
|
||||
}: ChipsProps) {
|
||||
const c = useColors();
|
||||
const styles = React.useMemo(() => makeStyles(c), [c]);
|
||||
const dyn = useTypography();
|
||||
const attachments = useListAttachments(email, load);
|
||||
const real = React.useMemo(() => realAttachments(attachments), [attachments]);
|
||||
// The line the chips will take, held while they load (webmail 7b25d3a95):
|
||||
// the row grew under the finger when they arrived.
|
||||
if (pending && attachments === undefined) {
|
||||
return (
|
||||
<View style={styles.row} accessibilityElementsHidden importantForAccessibility="no-hide-descendants" testID="list-chips-placeholder">
|
||||
<View style={[styles.chip, styles.placeholderChip]}>
|
||||
<Text style={[styles.name, dyn.caption]}> </Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
if (real.length === 0) return null;
|
||||
|
||||
const shown = real.slice(0, MAX_CHIPS);
|
||||
@@ -214,6 +227,8 @@ function makeStyles(c: ThemePalette) {
|
||||
backgroundColor: c.surface,
|
||||
},
|
||||
chipPressed: { backgroundColor: c.surfaceHover },
|
||||
// Empty space of a chip's height; a failed load leaves no grey stub behind.
|
||||
placeholderChip: { width: 96, opacity: 0 },
|
||||
name: { ...typography.caption, color: c.textSecondary, flexShrink: 1 },
|
||||
more: {
|
||||
paddingHorizontal: 6,
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { showsPaperclip } from '../list-attachments';
|
||||
import type { Attachment } from '../../api/types';
|
||||
|
||||
// A message with nothing but inline images (a signature logo) kept its
|
||||
// paperclip in the list, and the row grew when its chips arrived (webmail
|
||||
// attachment-chips.tsx, 7b25d3a95).
|
||||
|
||||
const inline = { blobId: 'b1', type: 'image/png', name: 'logo.png', disposition: 'inline', cid: 'logo' } as Attachment;
|
||||
const file = { blobId: 'b2', type: 'application/pdf', name: 'invoice.pdf' } as Attachment;
|
||||
|
||||
describe('list row paperclip and chips', () => {
|
||||
it("follows the server's flag until the parts are in, then only real attachments", () => {
|
||||
expect(showsPaperclip(true, undefined)).toBe(true);
|
||||
expect(showsPaperclip(true, [inline])).toBe(false);
|
||||
expect(showsPaperclip(true, [inline, file])).toBe(true);
|
||||
expect(showsPaperclip(false, undefined)).toBe(false);
|
||||
});
|
||||
|
||||
it('is wired into the row, which holds the chips line while they load', () => {
|
||||
const list = fs.readFileSync(path.resolve(__dirname, '../../screens/EmailListScreen.tsx'), 'utf8');
|
||||
expect(list).toMatch(/showsPaperclip\(item\.hasAttachment, attachments\)/);
|
||||
expect(list).toMatch(/pending=\{!!loadAttachments && item\.hasAttachment && !item\.attachments\}/);
|
||||
const chips = fs.readFileSync(path.resolve(__dirname, '../../components/email/ListAttachmentChips.tsx'), 'utf8');
|
||||
expect(chips).toMatch(/if \(pending && attachments === undefined\) \{/);
|
||||
});
|
||||
});
|
||||
@@ -23,6 +23,17 @@ export function realAttachments(attachments?: readonly Attachment[] | null): Att
|
||||
return attachments.filter((a) => a.disposition !== 'inline' && !a.cid && !!a.name && !isReportPart(a.type));
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a list row shows the paperclip: the server's `hasAttachment` until
|
||||
* the row's parts are loaded, then only when a real attachment is among them,
|
||||
* so a message with nothing but inline images loses it (webmail
|
||||
* components/email/attachment-chips.tsx).
|
||||
*/
|
||||
export function showsPaperclip(hasAttachment: boolean, attachments: readonly Attachment[] | undefined): boolean {
|
||||
if (attachments === undefined) return hasAttachment;
|
||||
return realAttachments(attachments).length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Long names are unreadable cut at the tail; the extension is the most
|
||||
* identifying part, so keep it and elide the middle.
|
||||
|
||||
@@ -24,8 +24,9 @@ import { MoveSheet } from '../components/MoveSheet';
|
||||
import { TagSheet } from '../components/TagSheet';
|
||||
import { OfflineBanner } from '../components/OfflineBanner';
|
||||
import {
|
||||
ListAttachmentChips, ListAttachmentOpener, useListRowAttachments,
|
||||
ListAttachmentChips, ListAttachmentOpener, useListAttachments, useListRowAttachments,
|
||||
} from '../components/email/ListAttachmentChips';
|
||||
import { showsPaperclip } from '../lib/list-attachments';
|
||||
import { VerificationCodeChip, useListVerificationCode } from '../components/email/VerificationCodeChip';
|
||||
import { useSenderWarning } from '../components/email/use-sender-warning';
|
||||
import type { LoadListAttachments } from '../lib/list-attachments';
|
||||
@@ -193,6 +194,9 @@ const EmailRow = React.memo(function EmailRow({
|
||||
const handlePress = React.useCallback(() => onPress(key), [onPress, key]);
|
||||
const handleLongPress = React.useCallback(() => onLongPress(key), [onLongPress, key]);
|
||||
const verificationCode = useListVerificationCode(item);
|
||||
// The row's parts, loaded once it is on screen: the chips show them and
|
||||
// the paperclip goes when they are only inline images.
|
||||
const attachments = useListAttachments(item, onOpenAttachment ? loadAttachments : undefined);
|
||||
// Sent and Drafts rows show the recipient: the check says nothing about them.
|
||||
const senderWarning = useSenderWarning(showRecipient ? undefined : item);
|
||||
|
||||
@@ -272,7 +276,7 @@ const EmailRow = React.memo(function EmailRow({
|
||||
{forwarded && (
|
||||
<Forward size={componentSizes.statusIcon} color={c.textMuted} />
|
||||
)}
|
||||
{item.hasAttachment && (
|
||||
{showsPaperclip(item.hasAttachment, attachments) && (
|
||||
<Paperclip size={componentSizes.statusIcon} color={c.textMuted} />
|
||||
)}
|
||||
</View>
|
||||
@@ -312,7 +316,8 @@ const EmailRow = React.memo(function EmailRow({
|
||||
{onOpenAttachment && (
|
||||
<ListAttachmentChips
|
||||
email={item}
|
||||
load={loadAttachments}
|
||||
attachments={attachments}
|
||||
pending={!!loadAttachments && item.hasAttachment && !item.attachments}
|
||||
onOpen={onOpenAttachment}
|
||||
disabled={selectionMode}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user