mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 00:12:39 +00:00
fix: hide the paperclip in the list when a message only has inline images
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { render, screen, act } from "@testing-library/react";
|
||||
import { realAttachments, ListAttachmentChips } from "../attachment-chips";
|
||||
import { render, screen, act, renderHook } from "@testing-library/react";
|
||||
import { realAttachments, ListAttachmentChips, useListHasAttachment } from "../attachment-chips";
|
||||
import type { Attachment, Email } from "@/lib/jmap/types";
|
||||
import type { LoadListAttachments } from "@/lib/list-attachments";
|
||||
|
||||
@@ -118,3 +118,56 @@ describe("ListAttachmentChips", () => {
|
||||
expect(load).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
// Stalwart flags a message whose only parts are inline images (a logo in a
|
||||
// notification mail) with hasAttachment; the viewer then lists nothing.
|
||||
describe("useListHasAttachment", () => {
|
||||
const row = (over: Partial<Email>) => ({ id: "m1", hasAttachment: true, ...over }) as Email;
|
||||
const deferred = () => {
|
||||
let deliver!: (a: Attachment[]) => void;
|
||||
const load = vi.fn((_email: Email, onLoad: (a: Attachment[]) => void) => { deliver = onLoad; return () => {}; });
|
||||
return { load, deliver: (a: Attachment[]) => deliver(a) };
|
||||
};
|
||||
|
||||
it("trusts the server flag until the parts arrive", () => {
|
||||
const { load } = deferred();
|
||||
const { result } = renderHook(() => useListHasAttachment(row({}), load));
|
||||
expect(result.current).toBe(true);
|
||||
});
|
||||
|
||||
it("drops the paperclip when every part is an inline image", () => {
|
||||
const { load, deliver } = deferred();
|
||||
const { result } = renderHook(() => useListHasAttachment(row({}), load));
|
||||
act(() => deliver([
|
||||
att({ name: "logo.png", type: "image/png", disposition: "inline", cid: "logo" }),
|
||||
att({ partId: "2", blobId: "b2", name: "github.png", type: "image/png", cid: "gh" }),
|
||||
]));
|
||||
expect(result.current).toBe(false);
|
||||
});
|
||||
|
||||
it("keeps the paperclip when a real attachment is among the parts", () => {
|
||||
const { load, deliver } = deferred();
|
||||
const { result } = renderHook(() => useListHasAttachment(row({}), load));
|
||||
act(() => deliver([
|
||||
att({ name: "logo.png", type: "image/png", disposition: "inline", cid: "logo" }),
|
||||
att({ partId: "2", blobId: "b2", name: "invoice.pdf" }),
|
||||
]));
|
||||
expect(result.current).toBe(true);
|
||||
});
|
||||
|
||||
it("uses parts the email already carries", () => {
|
||||
const load = vi.fn(() => () => {});
|
||||
const { result } = renderHook(() => useListHasAttachment(
|
||||
row({ attachments: [att({ name: "logo.png", type: "image/png", disposition: "inline" })] }), load,
|
||||
));
|
||||
expect(result.current).toBe(false);
|
||||
expect(load).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("shows nothing for a message the server did not flag", () => {
|
||||
const load = vi.fn(() => () => {});
|
||||
const { result } = renderHook(() => useListHasAttachment(row({ hasAttachment: false }), load));
|
||||
expect(result.current).toBe(false);
|
||||
expect(load).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -134,6 +134,18 @@ export function useListAttachments(email: Email, load?: LoadListAttachments): At
|
||||
return loaded?.id === email.id ? loaded.attachments : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a list row shows the paperclip. Stalwart sets `hasAttachment` for
|
||||
* inline images too (a logo in a notification mail), which the viewer never
|
||||
* lists, so once the parts are in the flag only counts if one of them is a
|
||||
* real attachment. Until then the server's flag stands.
|
||||
*/
|
||||
export function useListHasAttachment(email: Email, load?: LoadListAttachments): boolean {
|
||||
const attachments = useListAttachments(email, load);
|
||||
if (!email.hasAttachment) return false;
|
||||
return attachments === undefined || realAttachments(attachments).length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a list row will draw a chip row for this email, counting the room
|
||||
* it holds while the parts load. Lets the list estimate a row's height
|
||||
|
||||
@@ -4,7 +4,7 @@ import React, { useCallback } from "react";
|
||||
import { cleanPreview, formatDate, formatDateTime } from "@/lib/utils";
|
||||
import { Email, ThreadGroup } from "@/lib/jmap/types";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ListAttachmentChips } from "./attachment-chips";
|
||||
import { ListAttachmentChips, useListHasAttachment } from "./attachment-chips";
|
||||
import { VerificationCodeChip, useListVerificationCode } from "./verification-code-chip";
|
||||
import type { Attachment } from "@/lib/jmap/types";
|
||||
import type { LoadListAttachments } from "@/lib/list-attachments";
|
||||
@@ -281,6 +281,7 @@ const SingleEmailItem = React.forwardRef<HTMLDivElement, SingleEmailItemProps>(
|
||||
const isPinned = email.keywords?.['$pinned'] === true;
|
||||
const isAnswered = email.keywords?.$answered;
|
||||
const isForwarded = email.keywords?.$forwarded;
|
||||
const showPaperclip = useListHasAttachment(email, loadAttachments);
|
||||
const { selectedMailbox, mailboxes, selectedEmailKeys, toggleEmailSelection, selectRangeEmails, clearSelection, isUnifiedView, unifiedRole } = useEmailStore();
|
||||
// In Sent/Drafts folders, show recipient instead of sender (which is always
|
||||
// "me"). In aggregate role-views the selected mailbox is virtual → fall back
|
||||
@@ -598,7 +599,7 @@ const SingleEmailItem = React.forwardRef<HTMLDivElement, SingleEmailItemProps>(
|
||||
<StatusIcon icon={Forward} label={tStatus('forwarded')} className="w-3.5 h-3.5 text-muted-foreground" />
|
||||
</>
|
||||
)}
|
||||
{email.hasAttachment && <StatusIcon icon={Paperclip} label={tStatus('has_attachment')} className="w-3.5 h-3.5 text-muted-foreground" />}
|
||||
{showPaperclip && <StatusIcon icon={Paperclip} label={tStatus('has_attachment')} className="w-3.5 h-3.5 text-muted-foreground" />}
|
||||
{showSourceFolder && <SourceFolderTag name={email.sourceFolder!} />}
|
||||
{scheduledSendLabel ? (
|
||||
<span
|
||||
@@ -669,7 +670,7 @@ const SingleEmailItem = React.forwardRef<HTMLDivElement, SingleEmailItemProps>(
|
||||
<StatusIcon icon={Forward} label={tStatus('forwarded')} className="w-3.5 h-3.5 text-muted-foreground" />
|
||||
</>
|
||||
)}
|
||||
{email.hasAttachment && (
|
||||
{showPaperclip && (
|
||||
<StatusIcon icon={Paperclip} label={tStatus('has_attachment')} className="w-3.5 h-3.5 text-muted-foreground" />
|
||||
)}
|
||||
</div>
|
||||
@@ -804,7 +805,10 @@ export const ThreadListItem = React.forwardRef<HTMLDivElement, ThreadListItemPro
|
||||
const showAvatarsInJunk = useSettingsStore((state) => state.showAvatarsInJunk);
|
||||
const interfaceLayout = useSettingsStore((state) => state.interfaceLayout);
|
||||
const isMobile = useUIStore((state) => state.isMobile);
|
||||
const { latestEmail, participantNames, hasUnread, hasStarred, hasPinned, hasAttachment, hasAnswered, hasForwarded, emailCount } = thread;
|
||||
const { latestEmail, participantNames, hasUnread, hasStarred, hasPinned, hasAnswered, hasForwarded, emailCount } = thread;
|
||||
// Only the latest email's parts are loaded; an older one with the flag keeps the paperclip.
|
||||
const latestHasAttachment = useListHasAttachment(latestEmail, loadAttachments);
|
||||
const hasAttachment = latestHasAttachment || thread.emails.some((e) => e !== latestEmail && e.hasAttachment);
|
||||
// The horizontal one-line "focus" layout doesn't fit on narrow screens; fall back to multi-line on mobile.
|
||||
const isFocusedMailLayout = mailLayout === 'focus' && !isMobile;
|
||||
// Mountain View leads its rows with a checkbox and a clickable star
|
||||
|
||||
Reference in New Issue
Block a user