feat: make the attachment bar the default and retire attachments beside the sender

This commit is contained in:
Linus Rath
2026-10-31 23:35:54 +01:00
parent 417f512765
commit eb2e3b2baa
35 changed files with 58 additions and 211 deletions
@@ -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" />);
+3 -172
View File
@@ -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>
)}
+1 -2
View File
@@ -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') },
]}
-1
View File
@@ -1903,7 +1903,6 @@
"attachment_position": {
"label": "موضع المرفق",
"description": "أين تُعرض المرفقات في ترويسة الرسالة",
"beside-sender": "بجانب المرسل",
"below-header": "أسفل الترويسة"
},
"emails_per_page": {
-1
View File
@@ -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": {
-1
View File
@@ -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": {
-1
View File
@@ -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": {
+1 -2
View File
@@ -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": {
+1 -2
View File
@@ -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": {
-1
View File
@@ -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": {
-1
View File
@@ -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": {
-1
View File
@@ -1903,7 +1903,6 @@
"attachment_position": {
"label": "موقعیت پیوست",
"description": "محل پیوست‌ها",
"beside-sender": "کنار فرستنده",
"below-header": "زیر هدر"
},
"emails_per_page": {
-1
View File
@@ -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": {
-1
View File
@@ -1837,7 +1837,6 @@
"attachment_position": {
"label": "עמדת התקשרות",
"description": "היכן להציג קבצים מצורפים בכותרת הדוא\"ל",
"beside-sender": "ליד השולח",
"below-header": "מתחת לכותרת"
},
"emails_per_page": {
-1
View File
@@ -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": {
-1
View File
@@ -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": {
-1
View File
@@ -1900,7 +1900,6 @@
"attachment_position": {
"label": "添付ファイルの位置",
"description": "メールヘッダー内での添付ファイルの表示位置",
"beside-sender": "送信者の横",
"below-header": "ヘッダーの下"
},
"emails_per_page": {
-1
View File
@@ -1900,7 +1900,6 @@
"attachment_position": {
"label": "첨부파일 위치",
"description": "메일 상단에서 첨부파일을 어디에 표시할지 설정해요",
"beside-sender": "보낸 사람 옆",
"below-header": "제목 아래"
},
"emails_per_page": {
-1
View File
@@ -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": {
-1
View File
@@ -1903,7 +1903,6 @@
"attachment_position": {
"label": "Хавсралтын байрлал",
"description": "Имэйл толгой хэсэгт хавсралтуудыг хаана харуулах вэ",
"beside-sender": "Илгээгчийн хажууд",
"below-header": "Толгойн доор"
},
"emails_per_page": {
-1
View File
@@ -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": {
-1
View File
@@ -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": {
-1
View File
@@ -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": {
-1
View File
@@ -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": {
-1
View File
@@ -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": {
-1
View File
@@ -1900,7 +1900,6 @@
"attachment_position": {
"label": "Расположение вложений",
"description": "Где отображать вложения в заголовке письма",
"beside-sender": "Рядом с отправителем",
"below-header": "Под заголовком"
},
"emails_per_page": {
-1
View File
@@ -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": {
-1
View File
@@ -4344,7 +4344,6 @@
"attachment_position": {
"label": "ตำแหน่งไฟล์แนบ",
"description": "ตำแหน่งที่จะแสดงไฟล์แนบในส่วนหัวของอีเมล",
"beside-sender": "ข้างผู้ส่ง",
"below-header": "ใต้ส่วนหัว"
},
"emails_per_page": {
-1
View File
@@ -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": {
-1
View File
@@ -1900,7 +1900,6 @@
"attachment_position": {
"label": "Позиція прикріплення",
"description": "Де відображати вкладення в заголовку електронної пошти",
"beside-sender": "Поруч із відправником",
"below-header": "Під заголовком"
},
"emails_per_page": {
-1
View File
@@ -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": {
-1
View File
@@ -1903,7 +1903,6 @@
"attachment_position": {
"label": "附件位置",
"description": "選擇在郵件標頭的哪個位置顯示附件",
"beside-sender": "寄件者旁",
"below-header": "標頭下方"
},
"emails_per_page": {
-1
View File
@@ -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');
});
+19 -4
View File
@@ -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