mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-03 00:13:09 +00:00
feat: make the attachment bar the default and retire attachments beside the sender
This commit is contained in:
@@ -73,6 +73,16 @@ describe('attachment position in the reader', () => {
|
||||
expect(screen.queryByTestId('attachments-jump')).toBeNull();
|
||||
});
|
||||
|
||||
it('draws the bar, and nothing beside the sender, for the retired beside-sender value', async () => {
|
||||
useSettingsStore.setState({ attachmentPosition: 'beside-sender' as never });
|
||||
const { container } = render(<EmailViewer email={email} mailboxes={[]} selectedMailbox="inbox" />);
|
||||
await screen.findByText('Here is the invoice.');
|
||||
expect(container.querySelectorAll('[data-testid="attachment"]')).toHaveLength(2);
|
||||
const sections = container.querySelectorAll('[data-testid="reader-attachments"]');
|
||||
expect(sections).toHaveLength(1);
|
||||
expect((sections[0] as HTMLElement).dataset.layout).toBe('bar');
|
||||
});
|
||||
|
||||
it('draws cards after the body, with a jump link in the header', async () => {
|
||||
useSettingsStore.setState({ attachmentPosition: 'below-message' });
|
||||
const { container } = render(<EmailViewer email={email} mailboxes={[]} selectedMailbox="inbox" />);
|
||||
|
||||
@@ -13,7 +13,6 @@ import { applyMailTextSize, mailFitOptions } from "@/lib/mail-reading";
|
||||
import { useMailReading } from "@/hooks/use-mail-reading";
|
||||
import { useReadAloud } from "@/hooks/use-read-aloud";
|
||||
import { ReadAloudBar } from "./read-aloud-bar";
|
||||
import { attachmentIcon } from "./attachment-chips";
|
||||
import { ReaderAttachments, type AttachmentDragProps } from "./reader-attachments";
|
||||
import { ReaderViewDialog } from "./reader-view-dialog";
|
||||
import { ReadingMask } from "./reading-mask";
|
||||
@@ -70,15 +69,12 @@ import {
|
||||
ChevronUp,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Download,
|
||||
Mail,
|
||||
MailOpen,
|
||||
Loader2,
|
||||
Printer,
|
||||
FileText,
|
||||
FileArchive,
|
||||
File,
|
||||
Eye,
|
||||
Shield,
|
||||
Image,
|
||||
Tag,
|
||||
@@ -1545,7 +1541,6 @@ export function EmailViewer({
|
||||
const resolvedTheme = useThemeStore((state) => state.resolvedTheme);
|
||||
const { startTour } = useTour();
|
||||
const [showFullHeaders, setShowFullHeaders] = useState(false);
|
||||
const [showAllBesideAttachments, setShowAllBesideAttachments] = useState(false);
|
||||
const [isDownloadingAll, setIsDownloadingAll] = useState(false);
|
||||
const [imageThumbUrls, setImageThumbUrls] = useState<Record<string, string>>({});
|
||||
const [allowExternalContent, setAllowExternalContent] = useState(false);
|
||||
@@ -3063,21 +3058,6 @@ export function EmailViewer({
|
||||
),
|
||||
};
|
||||
|
||||
// Shared "Download all" chip, shown only when bundling is worthwhile (2+).
|
||||
const downloadAllButton = effectiveAttachments.length > 1 ? (
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); handleDownloadAllAttachments(); }}
|
||||
disabled={isDownloadingAll}
|
||||
title={t('download_all')}
|
||||
className="inline-flex items-center gap-1 px-2.5 py-1.5 text-xs text-muted-foreground hover:text-foreground hover:bg-muted/60 rounded-md border border-border/50 transition-colors flex-shrink-0 disabled:opacity-60 disabled:cursor-wait"
|
||||
>
|
||||
{isDownloadingAll
|
||||
? <Loader2 className="w-3.5 h-3.5 animate-spin" />
|
||||
: <FileArchive className="w-3.5 h-3.5" />}
|
||||
{t('download_all')}
|
||||
</button>
|
||||
) : null;
|
||||
|
||||
// Pre-fetch URLs for image attachments so their actual contents can be
|
||||
// rendered as thumbnails inside the chip. Skips images larger than 10 MB.
|
||||
// The thumbnail can be opened on its own (context menu, drag to the tab
|
||||
@@ -5068,154 +5048,6 @@ export function EmailViewer({
|
||||
<bdi>{effectiveAttachments.length}</bdi>
|
||||
</button>
|
||||
)}
|
||||
{/* Attachments on the right (beside-sender mode) */}
|
||||
{attachmentPosition === 'beside-sender' && effectiveAttachments.length > 0 && (
|
||||
<div className="relative flex flex-col items-end justify-start gap-1 flex-shrink-0 max-w-[50%]">
|
||||
{effectiveAttachments.slice(0, 2).map((attachment) => {
|
||||
const { icon: FileIcon, className: fileIconClass } = attachmentIcon(attachment.type, attachment.name);
|
||||
const isPreviewable = isAttachmentPreviewable(attachment);
|
||||
const opensPreview = isPreviewable && mailAttachmentAction === 'preview';
|
||||
const thumbUrl = imageThumbUrls[attachment.id];
|
||||
return (
|
||||
<DraggableAttachmentChip key={attachment.id} attachment={attachment} client={blobClient} accountId={blobAccountId} enabled={dragOutActive} downloadName={resolveAttachmentName(attachment)}>
|
||||
{(dragProps) => (<>
|
||||
<div
|
||||
className={cn(
|
||||
"bg-muted/60 hover:bg-muted rounded-md border border-border/50 group relative cursor-pointer overflow-hidden",
|
||||
thumbUrl
|
||||
? "inline-flex flex-col w-40"
|
||||
: "inline-flex items-center gap-1.5 px-2 py-1",
|
||||
)}
|
||||
title={`${opensPreview ? tFiles('preview') : t('download')} ${getAttachmentDisplayName(attachment.name, attachment.type)}`}
|
||||
onClick={() => handleEffectiveAttachmentOpen(attachment)}
|
||||
data-testid="attachment"
|
||||
data-attachment-name={attachment.name}
|
||||
draggable={dragProps.draggable}
|
||||
onPointerEnter={dragProps.onPointerEnter}
|
||||
onDragStart={dragProps.onDragStart}
|
||||
onDragEnd={dragProps.onDragEnd}
|
||||
>
|
||||
{thumbUrl && (
|
||||
<div className="w-full h-16 bg-background/40 flex items-center justify-center overflow-hidden">
|
||||
<img src={thumbUrl} alt="" className="w-full h-full object-cover" loading="lazy" />
|
||||
</div>
|
||||
)}
|
||||
<div className={cn(
|
||||
"flex items-center gap-1.5",
|
||||
thumbUrl && "px-2 py-1 border-t border-border/50 w-full",
|
||||
)}>
|
||||
<FileIcon className={cn("w-3.5 h-3.5 flex-shrink-0", fileIconClass)} />
|
||||
<span className={cn(
|
||||
"text-xs text-foreground truncate",
|
||||
thumbUrl ? "flex-1 min-w-0" : "max-w-[140px]",
|
||||
)}>
|
||||
{getAttachmentDisplayName(attachment.name, attachment.type)}
|
||||
</span>
|
||||
<span className="text-[0.625rem] text-muted-foreground flex-shrink-0">
|
||||
<bdi>{formatFileSize(attachment.size)}</bdi>
|
||||
</span>
|
||||
</div>
|
||||
<div className={cn(
|
||||
"absolute bg-background/95 opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto group-focus-within:opacity-100 group-focus-within:pointer-events-auto transition-opacity flex items-center justify-center gap-1 px-1.5 rounded-md",
|
||||
thumbUrl ? "top-1 end-1" : "inset-y-0 end-0 rounded-s-none rounded-e-md",
|
||||
)}>
|
||||
<IconButton
|
||||
label={t('download')}
|
||||
onClick={(e) => { e.stopPropagation(); handleEffectiveAttachmentDownload(attachment); }}
|
||||
>
|
||||
<Download />
|
||||
</IconButton>
|
||||
{opensPreview && (
|
||||
<IconButton
|
||||
label={tFiles('preview')}
|
||||
onClick={(e) => { e.stopPropagation(); handleEffectiveAttachmentOpen(attachment); }}
|
||||
>
|
||||
<Eye />
|
||||
</IconButton>
|
||||
)}
|
||||
{canSaveToFiles && attachment.blobId && (
|
||||
<IconButton label={t("save_to_files.title")} onClick={(e) => { e.stopPropagation(); setSaveToFilesTarget(attachment); }}>
|
||||
<FolderInput className="w-4 h-4" />
|
||||
</IconButton>
|
||||
)}
|
||||
<AttachmentPluginMenu emailId={email?.id} attachment={attachment} />
|
||||
</div>
|
||||
</div>
|
||||
<AttachmentPluginFrames emailId={email?.id} attachment={attachment} />
|
||||
</>)}
|
||||
</DraggableAttachmentChip>
|
||||
);
|
||||
})}
|
||||
{effectiveAttachments.length > 2 && (
|
||||
<button
|
||||
onClick={() => setShowAllBesideAttachments(!showAllBesideAttachments)}
|
||||
className="text-xs text-muted-foreground hover:text-foreground transition-colors px-2 py-0.5"
|
||||
>
|
||||
+{effectiveAttachments.length - 2} {t('more')}
|
||||
</button>
|
||||
)}
|
||||
{downloadAllButton}
|
||||
{/* Floating popup for remaining attachments */}
|
||||
{showAllBesideAttachments && effectiveAttachments.length > 2 && (
|
||||
<>
|
||||
<div className="fixed inset-0 z-40" onClick={() => setShowAllBesideAttachments(false)} />
|
||||
<div className="absolute top-full end-0 mt-1 z-50 bg-background border border-border rounded-lg shadow-lg p-2 flex flex-col gap-1 min-w-[220px]">
|
||||
{effectiveAttachments.slice(2).map((attachment) => {
|
||||
const { icon: FileIcon, className: fileIconClass } = attachmentIcon(attachment.type, attachment.name);
|
||||
const isPreviewable = isAttachmentPreviewable(attachment);
|
||||
const opensPreview = isPreviewable && mailAttachmentAction === 'preview';
|
||||
return (
|
||||
<DraggableAttachmentChip key={attachment.id} attachment={attachment} client={blobClient} accountId={blobAccountId} enabled={dragOutActive} downloadName={resolveAttachmentName(attachment)}>
|
||||
{(dragProps) => (<>
|
||||
<div
|
||||
className="flex items-center gap-1.5 px-2 py-1 rounded-md hover:bg-muted/60 group relative cursor-pointer w-full"
|
||||
title={`${opensPreview ? tFiles('preview') : t('download')} ${getAttachmentDisplayName(attachment.name, attachment.type)}`}
|
||||
onClick={() => { handleEffectiveAttachmentOpen(attachment); setShowAllBesideAttachments(false); }}
|
||||
draggable={dragProps.draggable}
|
||||
onPointerEnter={dragProps.onPointerEnter}
|
||||
onDragStart={dragProps.onDragStart}
|
||||
onDragEnd={dragProps.onDragEnd}
|
||||
>
|
||||
<FileIcon className={cn("w-3.5 h-3.5 flex-shrink-0", fileIconClass)} />
|
||||
<span className="text-xs text-foreground truncate max-w-[180px]">
|
||||
{getAttachmentDisplayName(attachment.name, attachment.type)}
|
||||
</span>
|
||||
<span className="text-[0.625rem] text-muted-foreground ms-auto flex-shrink-0">
|
||||
<bdi>{formatFileSize(attachment.size)}</bdi>
|
||||
</span>
|
||||
<div className="absolute inset-y-0 end-0 rounded-e-md bg-background/95 opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto group-focus-within:opacity-100 group-focus-within:pointer-events-auto transition-opacity flex items-center justify-center gap-1 px-1.5">
|
||||
<IconButton
|
||||
label={t('download')}
|
||||
onClick={(e) => { e.stopPropagation(); handleEffectiveAttachmentDownload(attachment); setShowAllBesideAttachments(false); }}
|
||||
>
|
||||
<Download />
|
||||
</IconButton>
|
||||
{opensPreview && (
|
||||
<IconButton
|
||||
label={tFiles('preview')}
|
||||
onClick={(e) => { e.stopPropagation(); handleEffectiveAttachmentOpen(attachment); setShowAllBesideAttachments(false); }}
|
||||
>
|
||||
<Eye />
|
||||
</IconButton>
|
||||
)}
|
||||
{canSaveToFiles && attachment.blobId && (
|
||||
<IconButton label={t("save_to_files.title")} onClick={(e) => { e.stopPropagation(); setSaveToFilesTarget(attachment); }}>
|
||||
<FolderInput className="w-4 h-4" />
|
||||
</IconButton>
|
||||
)}
|
||||
<AttachmentPluginMenu emailId={email?.id} attachment={attachment} />
|
||||
</div>
|
||||
</div>
|
||||
<AttachmentPluginFrames emailId={email?.id} attachment={attachment} />
|
||||
</>)}
|
||||
</DraggableAttachmentChip>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -5906,11 +5738,10 @@ export function EmailViewer({
|
||||
|
||||
<PluginSlot name="email-banner" extraProps={pluginEmailProps} />
|
||||
|
||||
{/* === ATTACHMENTS: a bar of tiles under the header. Beside-sender
|
||||
mode keeps its own column on wide screens; cards mode draws them
|
||||
after the body instead. === */}
|
||||
{/* === ATTACHMENTS: a bar of tiles under the header, unless cards
|
||||
mode draws them after the body. === */}
|
||||
{attachmentPosition !== 'below-message' && effectiveAttachments.length > 0 && (
|
||||
<div className={cn("bg-background border-b border-border px-4 lg:px-6 py-3", attachmentPosition === 'beside-sender' && "lg:hidden")}>
|
||||
<div className="bg-background border-b border-border px-4 lg:px-6 py-3">
|
||||
<ReaderAttachments {...readerAttachmentProps} layout="bar" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -162,10 +162,9 @@ export function ReadingSettings() {
|
||||
<SettingsSection id="attachments" title={tPage('sections.attachments')}>
|
||||
<SettingItem label={t('attachment_position.label')} description={t('attachment_position.description')}>
|
||||
<Select
|
||||
value={attachmentPosition}
|
||||
value={attachmentPosition === 'below-message' ? 'below-message' : 'below-header'}
|
||||
onChange={(value) => updateSetting('attachmentPosition', value as AttachmentPosition)}
|
||||
options={[
|
||||
{ value: 'beside-sender', label: t('attachment_position.beside-sender') },
|
||||
{ value: 'below-header', label: t('attachment_position.below-header') },
|
||||
{ value: 'below-message', label: t('attachment_position.below-message') },
|
||||
]}
|
||||
|
||||
@@ -1903,7 +1903,6 @@
|
||||
"attachment_position": {
|
||||
"label": "موضع المرفق",
|
||||
"description": "أين تُعرض المرفقات في ترويسة الرسالة",
|
||||
"beside-sender": "بجانب المرسل",
|
||||
"below-header": "أسفل الترويسة"
|
||||
},
|
||||
"emails_per_page": {
|
||||
|
||||
@@ -1903,7 +1903,6 @@
|
||||
"attachment_position": {
|
||||
"label": "Posició dels fitxers adjunts",
|
||||
"description": "On mostrar els fitxers adjunts a la capçalera del correu",
|
||||
"beside-sender": "Al costat del remitent",
|
||||
"below-header": "Sota la capçalera"
|
||||
},
|
||||
"emails_per_page": {
|
||||
|
||||
@@ -1900,7 +1900,6 @@
|
||||
"attachment_position": {
|
||||
"label": "Pozice příloh",
|
||||
"description": "Kde zobrazovat přílohy v hlavičce e-mailu",
|
||||
"beside-sender": "Vedle odesílatele",
|
||||
"below-header": "Pod hlavičkou"
|
||||
},
|
||||
"emails_per_page": {
|
||||
|
||||
@@ -1903,7 +1903,6 @@
|
||||
"attachment_position": {
|
||||
"label": "Placering af vedhæftninger",
|
||||
"description": "Hvor vedhæftninger skal vises i e-mail-headeren",
|
||||
"beside-sender": "Ved siden af afsender",
|
||||
"below-header": "Under header"
|
||||
},
|
||||
"emails_per_page": {
|
||||
|
||||
@@ -1903,8 +1903,7 @@
|
||||
"attachment_position": {
|
||||
"label": "Anhangsposition",
|
||||
"description": "Wo die Anhänge einer geöffneten E-Mail angezeigt werden",
|
||||
"beside-sender": "Neben dem Absender",
|
||||
"below-header": "Unter dem Header",
|
||||
"below-header": "Als Leiste unter dem Header",
|
||||
"below-message": "Als Karten unter der Nachricht"
|
||||
},
|
||||
"emails_per_page": {
|
||||
|
||||
@@ -1906,8 +1906,7 @@
|
||||
"attachment_position": {
|
||||
"label": "Attachment position",
|
||||
"description": "Where to display the attachments of an open email",
|
||||
"beside-sender": "Next to sender",
|
||||
"below-header": "Below header",
|
||||
"below-header": "Bar below the header",
|
||||
"below-message": "Cards below the message"
|
||||
},
|
||||
"emails_per_page": {
|
||||
|
||||
@@ -1900,7 +1900,6 @@
|
||||
"attachment_position": {
|
||||
"label": "Posición del adjunto",
|
||||
"description": "Dónde mostrar los adjuntos en el encabezado del correo",
|
||||
"beside-sender": "Junto al remitente",
|
||||
"below-header": "Debajo del encabezado"
|
||||
},
|
||||
"emails_per_page": {
|
||||
|
||||
@@ -1903,7 +1903,6 @@
|
||||
"attachment_position": {
|
||||
"label": "Manuse asukoht",
|
||||
"description": "Kuhu kuvada manused kirja päises",
|
||||
"beside-sender": "Saatja kõrval",
|
||||
"below-header": "Päise all"
|
||||
},
|
||||
"emails_per_page": {
|
||||
|
||||
@@ -1903,7 +1903,6 @@
|
||||
"attachment_position": {
|
||||
"label": "موقعیت پیوست",
|
||||
"description": "محل پیوستها",
|
||||
"beside-sender": "کنار فرستنده",
|
||||
"below-header": "زیر هدر"
|
||||
},
|
||||
"emails_per_page": {
|
||||
|
||||
@@ -1900,7 +1900,6 @@
|
||||
"attachment_position": {
|
||||
"label": "Position des pièces jointes",
|
||||
"description": "Où afficher les pièces jointes dans l'en-tête de l'email",
|
||||
"beside-sender": "À côté de l'expéditeur",
|
||||
"below-header": "Sous l'en-tête"
|
||||
},
|
||||
"emails_per_page": {
|
||||
|
||||
@@ -1837,7 +1837,6 @@
|
||||
"attachment_position": {
|
||||
"label": "עמדת התקשרות",
|
||||
"description": "היכן להציג קבצים מצורפים בכותרת הדוא\"ל",
|
||||
"beside-sender": "ליד השולח",
|
||||
"below-header": "מתחת לכותרת"
|
||||
},
|
||||
"emails_per_page": {
|
||||
|
||||
@@ -1903,7 +1903,6 @@
|
||||
"attachment_position": {
|
||||
"label": "Csatolmány pozíciója",
|
||||
"description": "Hol jelenjenek meg a csatolmányok az e-mail fejlécben",
|
||||
"beside-sender": "Feladó mellett",
|
||||
"below-header": "Fejléc alatt"
|
||||
},
|
||||
"emails_per_page": {
|
||||
|
||||
@@ -1900,7 +1900,6 @@
|
||||
"attachment_position": {
|
||||
"label": "Posizione degli allegati",
|
||||
"description": "Dove visualizzare gli allegati nell'intestazione dell'email",
|
||||
"beside-sender": "Accanto al mittente",
|
||||
"below-header": "Sotto l'intestazione"
|
||||
},
|
||||
"emails_per_page": {
|
||||
|
||||
@@ -1900,7 +1900,6 @@
|
||||
"attachment_position": {
|
||||
"label": "添付ファイルの位置",
|
||||
"description": "メールヘッダー内での添付ファイルの表示位置",
|
||||
"beside-sender": "送信者の横",
|
||||
"below-header": "ヘッダーの下"
|
||||
},
|
||||
"emails_per_page": {
|
||||
|
||||
@@ -1900,7 +1900,6 @@
|
||||
"attachment_position": {
|
||||
"label": "첨부파일 위치",
|
||||
"description": "메일 상단에서 첨부파일을 어디에 표시할지 설정해요",
|
||||
"beside-sender": "보낸 사람 옆",
|
||||
"below-header": "제목 아래"
|
||||
},
|
||||
"emails_per_page": {
|
||||
|
||||
@@ -1900,7 +1900,6 @@
|
||||
"attachment_position": {
|
||||
"label": "Pielikumu novietojums",
|
||||
"description": "Kur rādīt pielikumus vēstules galvenē",
|
||||
"beside-sender": "Blakus sūtītājam",
|
||||
"below-header": "Zem galvenes"
|
||||
},
|
||||
"emails_per_page": {
|
||||
|
||||
@@ -1903,7 +1903,6 @@
|
||||
"attachment_position": {
|
||||
"label": "Хавсралтын байрлал",
|
||||
"description": "Имэйл толгой хэсэгт хавсралтуудыг хаана харуулах вэ",
|
||||
"beside-sender": "Илгээгчийн хажууд",
|
||||
"below-header": "Толгойн доор"
|
||||
},
|
||||
"emails_per_page": {
|
||||
|
||||
@@ -1903,7 +1903,6 @@
|
||||
"attachment_position": {
|
||||
"label": "Plassering av vedlegg",
|
||||
"description": "Velg hvor vedlegg skal vises i e-postoverskriften",
|
||||
"beside-sender": "Ved siden av avsender",
|
||||
"below-header": "Under overskriften"
|
||||
},
|
||||
"emails_per_page": {
|
||||
|
||||
@@ -1900,7 +1900,6 @@
|
||||
"attachment_position": {
|
||||
"label": "Positie van bijlagen",
|
||||
"description": "Waar bijlagen in de e-mailkop worden weergegeven",
|
||||
"beside-sender": "Naast de afzender",
|
||||
"below-header": "Onder de kop"
|
||||
},
|
||||
"emails_per_page": {
|
||||
|
||||
@@ -1900,7 +1900,6 @@
|
||||
"attachment_position": {
|
||||
"label": "Pozycja załączników",
|
||||
"description": "Gdzie wyświetlać załączniki w nagłówku wiadomości e-mail",
|
||||
"beside-sender": "Obok nadawcy",
|
||||
"below-header": "Pod nagłówkiem"
|
||||
},
|
||||
"emails_per_page": {
|
||||
|
||||
@@ -1900,7 +1900,6 @@
|
||||
"attachment_position": {
|
||||
"label": "Posição dos anexos",
|
||||
"description": "Onde exibir os anexos no cabeçalho do e-mail",
|
||||
"beside-sender": "Ao lado do remetente",
|
||||
"below-header": "Abaixo do cabeçalho"
|
||||
},
|
||||
"emails_per_page": {
|
||||
|
||||
@@ -1903,7 +1903,6 @@
|
||||
"attachment_position": {
|
||||
"label": "Poziția atașamentului",
|
||||
"description": "Unde să afișați atașamentele în antetul e-mailului",
|
||||
"beside-sender": "Lângă expeditor",
|
||||
"below-header": "Sub antet"
|
||||
},
|
||||
"emails_per_page": {
|
||||
|
||||
@@ -1900,7 +1900,6 @@
|
||||
"attachment_position": {
|
||||
"label": "Расположение вложений",
|
||||
"description": "Где отображать вложения в заголовке письма",
|
||||
"beside-sender": "Рядом с отправителем",
|
||||
"below-header": "Под заголовком"
|
||||
},
|
||||
"emails_per_page": {
|
||||
|
||||
@@ -1903,7 +1903,6 @@
|
||||
"attachment_position": {
|
||||
"label": "Pozícia príloh",
|
||||
"description": "Kde zobrazovať prílohy v hlavičke e-mailu",
|
||||
"beside-sender": "Vedľa odosielateľa",
|
||||
"below-header": "Pod hlavičkou"
|
||||
},
|
||||
"emails_per_page": {
|
||||
|
||||
@@ -4344,7 +4344,6 @@
|
||||
"attachment_position": {
|
||||
"label": "ตำแหน่งไฟล์แนบ",
|
||||
"description": "ตำแหน่งที่จะแสดงไฟล์แนบในส่วนหัวของอีเมล",
|
||||
"beside-sender": "ข้างผู้ส่ง",
|
||||
"below-header": "ใต้ส่วนหัว"
|
||||
},
|
||||
"emails_per_page": {
|
||||
|
||||
@@ -1900,7 +1900,6 @@
|
||||
"attachment_position": {
|
||||
"label": "Ek Konumu",
|
||||
"description": "E-posta başlığında eklerin nerede görüntüleneceği",
|
||||
"beside-sender": "Göndericinin yanı",
|
||||
"below-header": "Başlığın altı"
|
||||
},
|
||||
"emails_per_page": {
|
||||
|
||||
@@ -1900,7 +1900,6 @@
|
||||
"attachment_position": {
|
||||
"label": "Позиція прикріплення",
|
||||
"description": "Де відображати вкладення в заголовку електронної пошти",
|
||||
"beside-sender": "Поруч із відправником",
|
||||
"below-header": "Під заголовком"
|
||||
},
|
||||
"emails_per_page": {
|
||||
|
||||
@@ -1915,7 +1915,6 @@
|
||||
"attachment_position": {
|
||||
"label": "Vị trí tệp đính kèm",
|
||||
"description": "Vị trí hiển thị tệp đính kèm trong phần đầu thư",
|
||||
"beside-sender": "Bên cạnh người gửi",
|
||||
"below-header": "Bên dưới phần đầu thư"
|
||||
},
|
||||
"emails_per_page": {
|
||||
|
||||
@@ -1903,7 +1903,6 @@
|
||||
"attachment_position": {
|
||||
"label": "附件位置",
|
||||
"description": "選擇在郵件標頭的哪個位置顯示附件",
|
||||
"beside-sender": "寄件者旁",
|
||||
"below-header": "標頭下方"
|
||||
},
|
||||
"emails_per_page": {
|
||||
|
||||
@@ -1900,7 +1900,6 @@
|
||||
"attachment_position": {
|
||||
"label": "附件位置",
|
||||
"description": "在邮件标题中的何处显示附件",
|
||||
"beside-sender": "发件人旁",
|
||||
"below-header": "标题下方"
|
||||
},
|
||||
"emails_per_page": {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
import { useSettingsStore } from '../settings-store';
|
||||
import { migrateSettings, useSettingsStore } from '../settings-store';
|
||||
|
||||
describe('settings-store attachment action', () => {
|
||||
beforeEach(() => {
|
||||
@@ -14,6 +14,28 @@ describe('settings-store attachment action', () => {
|
||||
expect(useSettingsStore.getState().quickReplyEnabled).toBe(true);
|
||||
});
|
||||
|
||||
it('shows attachments as a bar under the header by default', () => {
|
||||
expect(useSettingsStore.getState().attachmentPosition).toBe('below-header');
|
||||
});
|
||||
|
||||
it('moves the retired beside-sender position to the bar', () => {
|
||||
const migrated = migrateSettings({ attachmentPosition: 'beside-sender', explicitSettings: [] }, 11) as unknown as Record<string, unknown>;
|
||||
expect(migrated.attachmentPosition).toBe('below-header');
|
||||
const cards = migrateSettings({ attachmentPosition: 'below-message', explicitSettings: [] }, 11) as unknown as Record<string, unknown>;
|
||||
expect(cards.attachmentPosition).toBe('below-message');
|
||||
});
|
||||
|
||||
it('does not count the old default as chosen when older settings migrate', () => {
|
||||
const migrated = migrateSettings({ attachmentPosition: 'beside-sender' }, 7) as unknown as Record<string, unknown>;
|
||||
expect(migrated.explicitSettings).not.toContain('attachmentPosition');
|
||||
});
|
||||
|
||||
it('moves beside-sender to the bar in settings synced from an older build', () => {
|
||||
useSettingsStore.getState().updateSetting('attachmentPosition', 'below-message');
|
||||
expect(useSettingsStore.getState().importSettings(JSON.stringify({ attachmentPosition: 'beside-sender' }))).toBe(true);
|
||||
expect(useSettingsStore.getState().attachmentPosition).toBe('below-header');
|
||||
});
|
||||
|
||||
it('defaults to preview when settings are reset', () => {
|
||||
expect(useSettingsStore.getState().mailAttachmentAction).toBe('preview');
|
||||
});
|
||||
|
||||
@@ -302,8 +302,8 @@ export type FirstDayOfWeek = 0 | 1 | 6; // 0 = Sunday, 1 = Monday, 6 = Saturday
|
||||
export type TimeZoneSetting = 'auto' | (string & {});
|
||||
export type ExternalContentPolicy = 'ask' | 'block' | 'allow';
|
||||
export type MailAttachmentAction = 'preview' | 'download';
|
||||
/** 'below-message' shows larger preview cards after the body. */
|
||||
export type AttachmentPosition = 'beside-sender' | 'below-header' | 'below-message';
|
||||
/** 'below-header': a bar of tiles under the header. 'below-message': preview cards after the body. */
|
||||
export type AttachmentPosition = 'below-header' | 'below-message';
|
||||
export type ToolbarPosition = 'top' | 'below-subject';
|
||||
export type ArchiveMode = 'single' | 'year' | 'month';
|
||||
export type MailLayout = 'split' | 'focus' | 'horizontal';
|
||||
@@ -901,7 +901,7 @@ const DEFAULT_SETTINGS = {
|
||||
messageSpacing: 'auto' as MessageSpacing,
|
||||
plainTextFont: 'sans' as PlainTextFont,
|
||||
mailAttachmentAction: 'preview' as MailAttachmentAction,
|
||||
attachmentPosition: 'beside-sender' as AttachmentPosition,
|
||||
attachmentPosition: 'below-header' as AttachmentPosition,
|
||||
emailAlwaysLightMode: false,
|
||||
archiveMode: 'single' as ArchiveMode,
|
||||
hoverActions: ['delete', 'star', 'markRead', 'archive'] as HoverAction[],
|
||||
@@ -1547,6 +1547,7 @@ export const useSettingsStore = create<SettingsState>()(
|
||||
migrateAnimationsSetting(settings);
|
||||
migrateNotificationSoundDefault(settings);
|
||||
migrateTintListRowsDefault(settings);
|
||||
migrateAttachmentPosition(settings);
|
||||
|
||||
// Apply settings, noting which keys were taken over for the
|
||||
// onSettingsImport hook.
|
||||
@@ -1948,7 +1949,7 @@ export const useSettingsStore = create<SettingsState>()(
|
||||
},
|
||||
{
|
||||
name: 'settings-storage',
|
||||
version: 11,
|
||||
version: 12,
|
||||
migrate: migrateSettings,
|
||||
onRehydrateStorage: () => {
|
||||
return (state) => {
|
||||
@@ -2053,6 +2054,8 @@ export function migrateSettings(persisted: unknown, version: number): SettingsSt
|
||||
// taken as chosen below.
|
||||
migrateNotificationSoundDefault(state);
|
||||
migrateTintListRowsDefault(state);
|
||||
// The old default must not be taken as chosen below either.
|
||||
migrateAttachmentPosition(state);
|
||||
// v8: admin policy defaults (#1248) skip settings the user chose.
|
||||
// Earlier builds did not record that, so a setting that differs from
|
||||
// the build default is taken as chosen.
|
||||
@@ -2063,6 +2066,18 @@ export function migrateSettings(persisted: unknown, version: number): SettingsSt
|
||||
return state as unknown as SettingsState;
|
||||
}
|
||||
|
||||
/**
|
||||
* v12: the attachments next to the sender ('beside-sender', the old default)
|
||||
* gave way to the bar under the header. Imports and server copies from older
|
||||
* builds still carry the old value and go through this too.
|
||||
*/
|
||||
export function migrateAttachmentPosition(state: Record<string, unknown>): void {
|
||||
if (state.attachmentPosition === undefined) return;
|
||||
if (state.attachmentPosition !== 'below-header' && state.attachmentPosition !== 'below-message') {
|
||||
state.attachmentPosition = 'below-header';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* v9: the Animations on/off switch became Reduce motion (System / On /
|
||||
* Off). Off meant "no animations"; On let the OS setting decide, which is
|
||||
|
||||
Reference in New Issue
Block a user