feat: show one column and an optional paged message list in simple mode

This commit is contained in:
Linus Rath
2026-10-29 19:14:41 +01:00
parent 6e45d754f9
commit be423637fc
39 changed files with 442 additions and 89 deletions
+6
View File
@@ -253,3 +253,9 @@ html.dark[data-contrast="more"] {
outline-color: Highlight !important;
}
}
/* ---------- Simple mode: no side panels ---------- */
:root[data-simple-mode="on"] [data-simple-hide] {
display: none !important;
}
@@ -0,0 +1,78 @@
import { fireEvent, render, screen } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import React from 'react';
import { EmailList } from '../email-list';
import { useEmailStore } from '@/stores/email-store';
import { useSettingsStore } from '@/stores/settings-store';
import type { Email } from '@/lib/jmap/types';
// SR-21: simple mode can show the list in pages of 50 with Previous and Next.
vi.mock('@/hooks/use-email-drag', () => ({
useEmailDrag: () => ({ dragHandlers: {}, isDragging: false }),
}));
vi.mock('@/components/ui/avatar', () => ({ Avatar: () => null }));
vi.mock('@/stores/auth-store', async () => {
const { create } = await import('zustand');
return {
useAuthStore: create(() => ({ client: null, identities: [], username: null, activeAccountId: 'acct-1' })),
};
});
vi.mock('@/stores/account-store', () => {
const state = { getAccountById: () => undefined };
return {
useAccountStore: Object.assign(
(selector?: (s: typeof state) => unknown) => (selector ? selector(state) : state),
{ getState: () => state },
),
};
});
const MAILS: Email[] = Array.from({ length: 120 }, (_, i) => ({
id: `m${i}`,
threadId: `t${i}`,
mailboxIds: { inbox: true },
keywords: { $seen: true },
size: 1000,
receivedAt: new Date(Date.UTC(2026, 8, 30, 12, 0) - i * 60_000).toISOString(),
from: [{ name: 'Alice', email: 'alice@example.com' }],
subject: `Mail ${i}`,
hasAttachment: false,
}));
const initialEmailState = useEmailStore.getState();
beforeEach(() => {
useEmailStore.setState(initialEmailState, true);
useEmailStore.setState({ selectedMailbox: 'inbox', emails: MAILS, hasMoreEmails: false });
useSettingsStore.setState({ simpleMode: true, simpleModePagedList: true, disableThreading: true } as never);
});
describe('paged message list (SR-21)', () => {
it('shows 50 rows at a time and pages with Next and Previous', () => {
render(<EmailList emails={MAILS} />);
expect(screen.getAllByRole('row')).toHaveLength(50);
expect(screen.getByRole('row', { name: /Mail 0,/ })).toBeTruthy();
fireEvent.click(screen.getByRole('button', { name: 'pages.next' }));
expect(screen.getByRole('row', { name: /Mail 50,/ })).toBeTruthy();
expect(screen.queryByRole('row', { name: /Mail 0,/ })).toBeNull();
fireEvent.click(screen.getByRole('button', { name: 'pages.next' }));
expect(screen.getAllByRole('row')).toHaveLength(20);
expect(screen.getByRole('button', { name: 'pages.next' })).toBeDisabled();
fireEvent.click(screen.getByRole('button', { name: 'pages.previous' }));
expect(screen.getByRole('row', { name: /Mail 50,/ })).toBeTruthy();
});
it('tells each row its place in the whole list', () => {
render(<EmailList emails={MAILS} />);
fireEvent.click(screen.getByRole('button', { name: 'pages.next' }));
expect(screen.getByRole('row', { name: /Mail 50,/ })).toHaveAttribute('aria-posinset', '51');
expect(screen.getByRole('row', { name: /Mail 50,/ })).toHaveAttribute('aria-setsize', '120');
});
it('scrolls endlessly as before without the paged option', () => {
useSettingsStore.setState({ simpleModePagedList: false } as never);
render(<EmailList emails={MAILS} />);
expect(screen.queryByRole('button', { name: 'pages.next' })).toBeNull();
});
});
+129 -54
View File
@@ -143,6 +143,9 @@ function LoadingSkeleton() {
);
}
/** Rows per page in simple mode's paged list. */
const LIST_PAGE_SIZE = 50;
export function EmailList({
emails,
selectedEmailKey,
@@ -503,13 +506,6 @@ export function EmailList({
entry?.borderBoxSize?.[0]?.blockSize ?? element.getBoundingClientRect().height,
});
// One Tab stop in the list, and the focus going along with j/k (SR-01/02).
const renderedRows = virtualizer.getVirtualItems();
useListboxFocus(
parentRef,
selectedEmailKey,
`${items.length}:${renderedRows[0]?.index ?? -1}-${renderedRows[renderedRows.length - 1]?.index ?? -1}:${expandedThreadIds.size}`,
);
// Another folder, tag, account, unified view or plugin tab - or another
// search - opens at the top. The scroll container outlives the switch, so
@@ -892,6 +888,104 @@ export function EmailList({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [density, isFocusedMailLayout, showPreview]);
// One list item's content, for the virtual list and the paged one.
const renderItemContent = (index: number) => {
const listItem = items[index];
const thread = listItem?.kind === 'row' ? listItem.thread : null;
return (
<>
{listItem?.kind === 'header' && unreadSection && (
listItem.section === 'unread' ? (
<MvListSectionHeader
title={t('sections.unread')}
collapsed={unreadCollapsed}
onToggle={() => updateSetting('mvUnreadSectionCollapsed', !unreadCollapsed)}
shown={sectionRowCount}
total={unreadSection.total}
/>
) : (
<MvListSectionHeader
title={t('sections.everything_else')}
collapsed={restCollapsed}
onToggle={() => updateSetting('mvEverythingElseCollapsed', !restCollapsed)}
shown={emails.length - sectionRowCount}
total={totalEmails}
/>
)
)}
{listItem?.kind === 'unread-empty' && (
<p className="flex h-12 items-center border-b border-border px-6 text-sm text-muted-foreground">
{t('sections.unread_empty')}
</p>
)}
{thread && <MemoizedThreadListItem
thread={thread}
isExpanded={expandedThreadIds.has(thread.threadKey)}
selectedEmailKey={selectedEmailKey}
isLoading={isLoadingThread === thread.threadKey}
expandedEmails={threadEmailsCache.get(thread.threadKey)}
onToggleExpand={toggleExpandFor(thread.threadKey)}
onCollapseAllThreads={rowHandlers.onCollapseAllThreads}
onEmailSelect={rowHandlers.onEmailSelect}
onEmailDoubleClick={rowHandlers.onEmailDoubleClick}
onPrefetch={rowHandlers.onPrefetch}
onContextMenu={rowHandlers.onContextMenu}
onOpenConversation={rowHandlers.onOpenConversation}
onToggleStar={rowHandlers.onToggleStar}
onMarkAsRead={rowHandlers.onMarkAsRead}
onDelete={rowHandlers.onDelete}
onArchive={rowHandlers.onArchive}
onSetTag={rowHandlers.onSetTag}
onMarkAsSpam={rowHandlers.onMarkAsSpam}
onUndoSpam={rowHandlers.onUndoSpam}
onOpenAttachment={rowHandlers.onOpenAttachment}
loadAttachments={loadAttachments}
/>}
</>
);
};
// Simple mode's paged list (SR-21): pages of 50 rows with Previous and
// Next instead of endless scrolling, which a screen reader handles better.
const paged = useSettingsStore((state) => state.simpleMode && state.simpleModePagedList);
const [page, setPage] = useState(0);
const pageCount = Math.max(1, Math.ceil(itemOfThread.length / LIST_PAGE_SIZE));
const pageItems = useMemo(
() => (paged ? itemOfThread.slice(page * LIST_PAGE_SIZE, (page + 1) * LIST_PAGE_SIZE) : []),
[paged, page, itemOfThread],
);
const canGoNext = (page + 1) * LIST_PAGE_SIZE < itemOfThread.length || footerHasMore;
const pendingNextPage = useRef(false);
const goToNextPage = useCallback(() => {
if ((page + 1) * LIST_PAGE_SIZE < itemOfThread.length) setPage(page + 1);
else if (footerHasMore) { pendingNextPage.current = true; handleLoadMore(); }
}, [page, itemOfThread.length, footerHasMore, handleLoadMore]);
// The next page arrives from the server: show it.
useEffect(() => {
if (!pendingNextPage.current || (page + 1) * LIST_PAGE_SIZE >= itemOfThread.length) return;
pendingNextPage.current = false;
setPage(page + 1);
}, [itemOfThread.length, page]);
// Another folder or search starts on its first page.
useEffect(() => { setPage(0); }, [viewKey]);
// j/k past the end of a page turn the page.
useEffect(() => {
if (!paged || !selectedEmailKey) return;
const index = threadGroups.findIndex((thread) => thread.emails.some((e) => emailKeyFor(e) === selectedEmailKey));
if (index < 0) return;
const target = Math.floor(index / LIST_PAGE_SIZE);
if (target !== page) setPage(target);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [paged, selectedEmailKey]);
// One Tab stop in the list, and the focus going along with j/k (SR-01/02).
const renderedRows = virtualizer.getVirtualItems();
useListboxFocus(
parentRef,
selectedEmailKey,
`${items.length}:${renderedRows[0]?.index ?? -1}-${renderedRows[renderedRows.length - 1]?.index ?? -1}:${expandedThreadIds.size}:${paged ? page : -1}`,
);
return (
<TagDisplayContext.Provider value={tagDisplay}>
<div className={cn("flex flex-col min-h-0", className)}>
@@ -1128,6 +1222,32 @@ export function EmailList({
<PluginSlot name="message-list-empty" className="mt-4 w-full max-w-md px-4" extraProps={emptySlotProps} />
</div>
) : (
paged ? (
<>
<div
role="treegrid"
aria-label={t('listbox_label')}
aria-multiselectable
aria-busy={isLoading || undefined}
data-setsize={footerHasMore ? -1 : itemOfThread.length}
>
{pageItems.map((index) => (
<div key={items[index]?.kind === 'row' ? (items[index] as { thread: ThreadGroup }).thread.threadKey : `item-${index}`} role="presentation" data-pos={rowPosOfItem.get(index)}>
{renderItemContent(index)}
</div>
))}
</div>
<nav aria-label={t('pages.label')} className="flex items-center justify-between gap-3 border-t border-border px-4 py-3 text-sm">
<Button variant="outline" size="sm" onClick={() => setPage((p) => Math.max(0, p - 1))} disabled={page === 0}>
{t('pages.previous')}
</Button>
<span aria-live="polite">{t('pages.position', { page: page + 1, pages: pageCount, more: footerHasMore ? 'yes' : 'no' })}</span>
<Button variant="outline" size="sm" onClick={goToNextPage} disabled={!canGoNext}>
{footerIsLoadingMore ? t('loading_more') : t('pages.next')}
</Button>
</nav>
</>
) : (
<>
<div
role="treegrid"
@@ -1163,53 +1283,7 @@ export function EmailList({
width: '100%',
}}
>
{listItem?.kind === 'header' && unreadSection && (
listItem.section === 'unread' ? (
<MvListSectionHeader
title={t('sections.unread')}
collapsed={unreadCollapsed}
onToggle={() => updateSetting('mvUnreadSectionCollapsed', !unreadCollapsed)}
shown={sectionRowCount}
total={unreadSection.total}
/>
) : (
<MvListSectionHeader
title={t('sections.everything_else')}
collapsed={restCollapsed}
onToggle={() => updateSetting('mvEverythingElseCollapsed', !restCollapsed)}
shown={emails.length - sectionRowCount}
total={totalEmails}
/>
)
)}
{listItem?.kind === 'unread-empty' && (
<p className="flex h-12 items-center border-b border-border px-6 text-sm text-muted-foreground">
{t('sections.unread_empty')}
</p>
)}
{thread && <MemoizedThreadListItem
thread={thread}
isExpanded={expandedThreadIds.has(thread.threadKey)}
selectedEmailKey={selectedEmailKey}
isLoading={isLoadingThread === thread.threadKey}
expandedEmails={threadEmailsCache.get(thread.threadKey)}
onToggleExpand={toggleExpandFor(thread.threadKey)}
onCollapseAllThreads={rowHandlers.onCollapseAllThreads}
onEmailSelect={rowHandlers.onEmailSelect}
onEmailDoubleClick={rowHandlers.onEmailDoubleClick}
onPrefetch={rowHandlers.onPrefetch}
onContextMenu={rowHandlers.onContextMenu}
onOpenConversation={rowHandlers.onOpenConversation}
onToggleStar={rowHandlers.onToggleStar}
onMarkAsRead={rowHandlers.onMarkAsRead}
onDelete={rowHandlers.onDelete}
onArchive={rowHandlers.onArchive}
onSetTag={rowHandlers.onSetTag}
onMarkAsSpam={rowHandlers.onMarkAsSpam}
onUndoSpam={rowHandlers.onUndoSpam}
onOpenAttachment={rowHandlers.onOpenAttachment}
loadAttachments={loadAttachments}
/>}
{renderItemContent(virtualItem.index)}
</div>
);
})}
@@ -1229,6 +1303,7 @@ export function EmailList({
)}
</div>}
</>
)
)}
</div>
+3 -1
View File
@@ -1504,7 +1504,9 @@ export function EmailViewer({
/>
);
// Mountain View's toolbars are icon-only, on the phone as on the desktop.
const showToolbarLabels = showToolbarLabelsSetting && interfaceLayout !== 'mountain-view';
const simpleMode = useSettingsStore((state) => state.simpleMode);
// Simple mode always names the toolbar's buttons in words.
const showToolbarLabels = (showToolbarLabelsSetting || simpleMode) && interfaceLayout !== 'mountain-view';
// Inside a Pro pane, `isMobile` above is pane-width based: overlays that
// would go viewport-fixed must instead cover just the pane (via
// PaneOverlay + absolute positioning), and viewport CSS breakpoints like
+1
View File
@@ -64,6 +64,7 @@ export function MvSidePanel() {
<>
{active && (
<aside
data-simple-hide=""
data-mv-side-panel={active}
className="mb-4 me-2 flex w-[300px] shrink-0 flex-col overflow-hidden rounded-2xl bg-background"
aria-label={buttons.find((b) => b.id === active)?.label}
+2 -1
View File
@@ -239,6 +239,7 @@ export function NavigationRail({
const supportsContacts = client?.supportsContacts() ?? false;
const showRailAccountList = useSettingsStore((s) => s.showRailAccountList);
const showAccessibilityButton = useSettingsStore((s) => s.showAccessibilityButton);
const simpleMode = useSettingsStore((s) => s.simpleMode);
const mountainView = useSettingsStore((s) => s.interfaceLayout === 'mountain-view');
const sidebarAppsEnabled = usePolicyStore((s) => s.isFeatureEnabled('sidebarAppsEnabled'));
const filesEnabled = usePolicyStore((s) => s.isFeatureEnabled('filesEnabled'));
@@ -836,7 +837,7 @@ export function NavigationRail({
</div>
)}
{onLogout && showRailAccountList && accounts.length > 0 && (
{onLogout && showRailAccountList && !simpleMode && accounts.length > 0 && (
<>
<div className="w-8 border-t my-1" style={{ borderColor: 'rgba(128, 128, 128, 0.3)' }} />
+1 -1
View File
@@ -4,5 +4,5 @@ import { effectiveMailLayout, useSettingsStore, type MailLayout } from "@/stores
/** The mail layout in force: the saved one, or `focus` under Mountain View. */
export function useEffectiveMailLayout(): MailLayout {
return useSettingsStore((state) => effectiveMailLayout(state.mailLayout, state.interfaceLayout));
return useSettingsStore((state) => effectiveMailLayout(state.mailLayout, state.interfaceLayout, state.simpleMode));
}
+1 -1
View File
@@ -18,7 +18,7 @@ export interface ListRowShape {
* are shaped after Gmail and never take them.
*/
export function useListRowShape(): ListRowShape {
const mailLayout = useSettingsStore((state) => effectiveMailLayout(state.mailLayout, state.interfaceLayout));
const mailLayout = useSettingsStore((state) => effectiveMailLayout(state.mailLayout, state.interfaceLayout, state.simpleMode));
const mountainView = useSettingsStore((state) => state.interfaceLayout === 'mountain-view');
const listColumns = useSettingsStore((state) => state.listColumns);
const isMobile = useUIStore((state) => state.isMobile);
+7 -1
View File
@@ -381,7 +381,13 @@
},
"category_tabs": "فئات البريد الوارد",
"listbox_label": "الرسائل",
"row_actions": "الإجراءات"
"row_actions": "الإجراءات",
"pages": {
"label": "صفحات قائمة الرسائل",
"previous": "الصفحة السابقة",
"next": "الصفحة التالية",
"position": "{more, select, yes {الصفحة {page} من {pages} أو أكثر} other {الصفحة {page} من {pages}}}"
}
},
"shared_tags": {
"title": "الوسوم المشتركة",
+7 -1
View File
@@ -381,7 +381,13 @@
},
"category_tabs": "Categories de la safata d'entrada",
"listbox_label": "Missatges",
"row_actions": "Accions"
"row_actions": "Accions",
"pages": {
"label": "Pàgines de la llista de correus",
"previous": "Pàgina anterior",
"next": "Pàgina següent",
"position": "{more, select, yes {Pàgina {page} de {pages} o més} other {Pàgina {page} de {pages}}}"
}
},
"shared_tags": {
"title": "Etiquetes compartides",
+7 -1
View File
@@ -381,7 +381,13 @@
},
"category_tabs": "Kategorie doručené pošty",
"listbox_label": "Zprávy",
"row_actions": "Akce"
"row_actions": "Akce",
"pages": {
"label": "Stránky seznamu zpráv",
"previous": "Předchozí stránka",
"next": "Další stránka",
"position": "{more, select, yes {Stránka {page} z {pages} nebo více} other {Stránka {page} z {pages}}}"
}
},
"shared_tags": {
"title": "Sdílené štítky",
+7 -1
View File
@@ -381,7 +381,13 @@
},
"category_tabs": "Indbakkekategorier",
"listbox_label": "Beskeder",
"row_actions": "Handlinger"
"row_actions": "Handlinger",
"pages": {
"label": "Sider i e-mail-listen",
"previous": "Forrige side",
"next": "Næste side",
"position": "{more, select, yes {Side {page} af {pages} eller flere} other {Side {page} af {pages}}}"
}
},
"shared_tags": {
"title": "Delte tags",
+7 -1
View File
@@ -381,7 +381,13 @@
},
"category_tabs": "Posteingangskategorien",
"listbox_label": "Nachrichten",
"row_actions": "Aktionen"
"row_actions": "Aktionen",
"pages": {
"label": "Seiten der Nachrichtenliste",
"previous": "Vorherige Seite",
"next": "Nächste Seite",
"position": "{more, select, yes {Seite {page} von {pages} oder mehr} other {Seite {page} von {pages}}}"
}
},
"shared_tags": {
"title": "Geteilte Labels",
+7 -1
View File
@@ -381,7 +381,13 @@
},
"category_tabs": "Inbox categories",
"listbox_label": "Messages",
"row_actions": "Actions"
"row_actions": "Actions",
"pages": {
"label": "Pages of the message list",
"previous": "Previous page",
"next": "Next page",
"position": "{more, select, yes {Page {page} of {pages} or more} other {Page {page} of {pages}}}"
}
},
"shared_tags": {
"title": "Shared tags",
+7 -1
View File
@@ -381,7 +381,13 @@
},
"category_tabs": "Categorías de la bandeja de entrada",
"listbox_label": "Mensajes",
"row_actions": "Acciones"
"row_actions": "Acciones",
"pages": {
"label": "Páginas de la lista de correos",
"previous": "Página anterior",
"next": "Página siguiente",
"position": "{more, select, yes {Página {page} de {pages} o más} other {Página {page} de {pages}}}"
}
},
"shared_tags": {
"title": "Etiquetas compartidas",
+7 -1
View File
@@ -381,7 +381,13 @@
},
"category_tabs": "Postkasti kategooriad",
"listbox_label": "Sõnumid",
"row_actions": "Toimingud"
"row_actions": "Toimingud",
"pages": {
"label": "Kirjade loendi lehed",
"previous": "Eelmine leht",
"next": "Järgmine leht",
"position": "{more, select, yes {Leht {page} / {pages} või rohkem} other {Leht {page} / {pages}}}"
}
},
"shared_tags": {
"title": "Jagatud sildid",
+7 -1
View File
@@ -381,7 +381,13 @@
},
"category_tabs": "دسته‌های صندوق ورودی",
"listbox_label": "پیام‌ها",
"row_actions": "اقدامات"
"row_actions": "اقدامات",
"pages": {
"label": "صفحه‌های فهرست پیام‌ها",
"previous": "صفحه قبلی",
"next": "صفحه بعدی",
"position": "{more, select, yes {صفحه {page} از {pages} یا بیشتر} other {صفحه {page} از {pages}}}"
}
},
"shared_tags": {
"title": "برچسب‌های مشترک",
+7 -1
View File
@@ -381,7 +381,13 @@
},
"category_tabs": "Catégories de la boîte de réception",
"listbox_label": "Messages",
"row_actions": "Actions"
"row_actions": "Actions",
"pages": {
"label": "Pages de la liste des messages",
"previous": "Page précédente",
"next": "Page suivante",
"position": "{more, select, yes {Page {page} sur {pages} ou plus} other {Page {page} sur {pages}}}"
}
},
"shared_tags": {
"title": "Étiquettes partagées",
+7 -1
View File
@@ -346,7 +346,13 @@
},
"category_tabs": "קטגוריות של תיבת הדואר הנכנס",
"listbox_label": "הודעות",
"row_actions": "פעולות"
"row_actions": "פעולות",
"pages": {
"label": "עמודי רשימת ההודעות",
"previous": "העמוד הקודם",
"next": "העמוד הבא",
"position": "{more, select, yes {עמוד {page} מתוך {pages} או יותר} other {עמוד {page} מתוך {pages}}}"
}
},
"shared_tags": {
"title": "תגים משותפים",
+7 -1
View File
@@ -381,7 +381,13 @@
},
"category_tabs": "Beérkezett üzenetek kategóriái",
"listbox_label": "Üzenetek",
"row_actions": "Műveletek"
"row_actions": "Műveletek",
"pages": {
"label": "Az üzenetlista oldalai",
"previous": "Előző oldal",
"next": "Következő oldal",
"position": "{more, select, yes {{page}. oldal, összesen {pages} vagy több} other {{page}. oldal, összesen {pages}}}"
}
},
"shared_tags": {
"title": "Megosztott címkék",
+7 -1
View File
@@ -381,7 +381,13 @@
},
"category_tabs": "Categorie della posta in arrivo",
"listbox_label": "Messaggi",
"row_actions": "Azioni"
"row_actions": "Azioni",
"pages": {
"label": "Pagine dell'elenco dei messaggi",
"previous": "Pagina precedente",
"next": "Pagina successiva",
"position": "{more, select, yes {Pagina {page} di {pages} o più} other {Pagina {page} di {pages}}}"
}
},
"shared_tags": {
"title": "Etichette condivise",
+7 -1
View File
@@ -381,7 +381,13 @@
},
"category_tabs": "受信トレイのカテゴリ",
"listbox_label": "メッセージ",
"row_actions": "アクション"
"row_actions": "アクション",
"pages": {
"label": "メッセージ一覧のページ",
"previous": "前のページ",
"next": "次のページ",
"position": "{more, select, yes {{pages} ページ以上中 {page} ページ目} other {{pages} ページ中 {page} ページ目}}"
}
},
"shared_tags": {
"title": "共有タグ",
+7 -1
View File
@@ -381,7 +381,13 @@
},
"category_tabs": "받은편지함 카테고리",
"listbox_label": "메시지",
"row_actions": "작업"
"row_actions": "작업",
"pages": {
"label": "메시지 목록 페이지",
"previous": "이전 페이지",
"next": "다음 페이지",
"position": "{more, select, yes {{pages}페이지 이상 중 {page}페이지} other {{pages}페이지 중 {page}페이지}}"
}
},
"shared_tags": {
"title": "공유 태그",
+7 -1
View File
@@ -381,7 +381,13 @@
},
"category_tabs": "Iesūtnes kategorijas",
"listbox_label": "Vēstules",
"row_actions": "Darbības"
"row_actions": "Darbības",
"pages": {
"label": "Ziņojumu saraksta lapas",
"previous": "Iepriekšējā lapa",
"next": "Nākamā lapa",
"position": "{more, select, yes {{page}. lapa no {pages} vai vairāk} other {{page}. lapa no {pages}}}"
}
},
"shared_tags": {
"title": "Kopīgotie tagi",
+7 -1
View File
@@ -381,7 +381,13 @@
},
"category_tabs": "Ирсэн шуудангийн ангиллууд",
"listbox_label": "Зурвасууд",
"row_actions": "Үйлдлүүд"
"row_actions": "Үйлдлүүд",
"pages": {
"label": "Мессежийн жагсаалтын хуудсууд",
"previous": "Өмнөх хуудас",
"next": "Дараагийн хуудас",
"position": "{more, select, yes {{pages} буюу түүнээс олон хуудасны {page}-р хуудас} other {{pages} хуудасны {page}-р хуудас}}"
}
},
"shared_tags": {
"title": "Хуваалцсан тагууд",
+7 -1
View File
@@ -381,7 +381,13 @@
},
"category_tabs": "Innbokskategorier",
"listbox_label": "Meldinger",
"row_actions": "Handlinger"
"row_actions": "Handlinger",
"pages": {
"label": "Sider i e-postlisten",
"previous": "Forrige side",
"next": "Neste side",
"position": "{more, select, yes {Side {page} av {pages} eller flere} other {Side {page} av {pages}}}"
}
},
"shared_tags": {
"title": "Delte etiketter",
+7 -1
View File
@@ -381,7 +381,13 @@
},
"category_tabs": "Inboxcategorieën",
"listbox_label": "Berichten",
"row_actions": "Acties"
"row_actions": "Acties",
"pages": {
"label": "Pagina's van de e-maillijst",
"previous": "Vorige pagina",
"next": "Volgende pagina",
"position": "{more, select, yes {Pagina {page} van {pages} of meer} other {Pagina {page} van {pages}}}"
}
},
"shared_tags": {
"title": "Gedeelde labels",
+7 -1
View File
@@ -381,7 +381,13 @@
},
"category_tabs": "Kategorie skrzynki odbiorczej",
"listbox_label": "Wiadomości",
"row_actions": "Akcje"
"row_actions": "Akcje",
"pages": {
"label": "Strony listy wiadomości",
"previous": "Poprzednia strona",
"next": "Następna strona",
"position": "{more, select, yes {Strona {page} z {pages} lub więcej} other {Strona {page} z {pages}}}"
}
},
"shared_tags": {
"title": "Wspólne etykiety",
+7 -1
View File
@@ -381,7 +381,13 @@
},
"category_tabs": "Categorias da caixa de entrada",
"listbox_label": "Mensagens",
"row_actions": "Ações"
"row_actions": "Ações",
"pages": {
"label": "Páginas da lista de e-mails",
"previous": "Página anterior",
"next": "Próxima página",
"position": "{more, select, yes {Página {page} de {pages} ou mais} other {Página {page} de {pages}}}"
}
},
"shared_tags": {
"title": "Etiquetas compartilhadas",
+7 -1
View File
@@ -381,7 +381,13 @@
},
"category_tabs": "Categoriile căsuței de primire",
"listbox_label": "Mesaje",
"row_actions": "Acțiuni"
"row_actions": "Acțiuni",
"pages": {
"label": "Paginile listei de mesaje",
"previous": "Pagina anterioară",
"next": "Pagina următoare",
"position": "{more, select, yes {Pagina {page} din {pages} sau mai multe} other {Pagina {page} din {pages}}}"
}
},
"shared_tags": {
"title": "Etichete partajate",
+7 -1
View File
@@ -381,7 +381,13 @@
},
"category_tabs": "Категории входящих",
"listbox_label": "Сообщения",
"row_actions": "Действия"
"row_actions": "Действия",
"pages": {
"label": "Страницы списка сообщений",
"previous": "Предыдущая страница",
"next": "Следующая страница",
"position": "{more, select, yes {Страница {page} из {pages} или больше} other {Страница {page} из {pages}}}"
}
},
"shared_tags": {
"title": "Общие теги",
+7 -1
View File
@@ -381,7 +381,13 @@
},
"category_tabs": "Kategórie doručenej pošty",
"listbox_label": "Správy",
"row_actions": "Akcie"
"row_actions": "Akcie",
"pages": {
"label": "Strany zoznamu správ",
"previous": "Predchádzajúca strana",
"next": "Ďalšia strana",
"position": "{more, select, yes {Strana {page} z {pages} alebo viac} other {Strana {page} z {pages}}}"
}
},
"shared_tags": {
"title": "Zdieľané štítky",
+7 -1
View File
@@ -645,7 +645,13 @@
},
"category_tabs": "หมวดหมู่กล่องจดหมายเข้า",
"listbox_label": "ข้อความ",
"row_actions": "การดำเนินการ"
"row_actions": "การดำเนินการ",
"pages": {
"label": "หน้าของรายการข้อความ",
"previous": "หน้าก่อนหน้า",
"next": "หน้าถัดไป",
"position": "{more, select, yes {หน้า {page} จาก {pages} หรือมากกว่า} other {หน้า {page} จาก {pages}}}"
}
},
"shared_tags": {
"title": "ป้ายกำกับที่แชร์",
+7 -1
View File
@@ -381,7 +381,13 @@
},
"category_tabs": "Gelen kutusu kategorileri",
"listbox_label": "Mesajlar",
"row_actions": "Eylemler"
"row_actions": "Eylemler",
"pages": {
"label": "İleti listesi sayfaları",
"previous": "Önceki sayfa",
"next": "Sonraki sayfa",
"position": "{more, select, yes {Sayfa {page} / {pages} veya daha fazla} other {Sayfa {page} / {pages}}}"
}
},
"shared_tags": {
"title": "Paylaşılan etiketler",
+7 -1
View File
@@ -381,7 +381,13 @@
},
"category_tabs": "Категорії вхідних",
"listbox_label": "Повідомлення",
"row_actions": "Дії"
"row_actions": "Дії",
"pages": {
"label": "Сторінки списку повідомлень",
"previous": "Попередня сторінка",
"next": "Наступна сторінка",
"position": "{more, select, yes {Сторінка {page} з {pages} або більше} other {Сторінка {page} з {pages}}}"
}
},
"shared_tags": {
"title": "Спільні теги",
+7 -1
View File
@@ -381,7 +381,13 @@
},
"category_tabs": "收件匣分類",
"listbox_label": "郵件",
"row_actions": "動作"
"row_actions": "動作",
"pages": {
"label": "郵件清單分頁",
"previous": "上一頁",
"next": "下一頁",
"position": "{more, select, yes {第 {page} 頁,共 {pages} 頁或更多} other {第 {page} 頁,共 {pages} 頁}}"
}
},
"shared_tags": {
"title": "共用標籤",
+7 -1
View File
@@ -381,7 +381,13 @@
},
"category_tabs": "收件箱分类",
"listbox_label": "邮件",
"row_actions": "操作"
"row_actions": "操作",
"pages": {
"label": "邮件列表分页",
"previous": "上一页",
"next": "下一页",
"position": "{more, select, yes {第 {page} 页,共 {pages} 页或更多} other {第 {page} 页,共 {pages} 页}}"
}
},
"shared_tags": {
"title": "共享标签",
@@ -0,0 +1,14 @@
import { describe, expect, it } from 'vitest';
import { effectiveMailLayout } from '../settings-store';
describe('simple mode layout (CO-12)', () => {
it('shows one column: the list, then the message in its place', () => {
expect(effectiveMailLayout('split', 'default', true)).toBe('focus');
expect(effectiveMailLayout('horizontal', 'default', true)).toBe('focus');
});
it('keeps the saved layout without simple mode', () => {
expect(effectiveMailLayout('split', 'default', false)).toBe('split');
expect(effectiveMailLayout('split', 'default')).toBe('split');
});
});
+4 -2
View File
@@ -320,8 +320,10 @@ export type MailLayout = 'split' | 'focus' | 'horizontal';
* decides what is drawn; read `mailLayout` itself only where the saved
* preference is being shown or edited.
*/
export function effectiveMailLayout(mailLayout: MailLayout, interfaceLayout: InterfaceLayout): MailLayout {
return interfaceLayout === 'mountain-view' ? 'focus' : mailLayout;
export function effectiveMailLayout(mailLayout: MailLayout, interfaceLayout: InterfaceLayout, simpleMode = false): MailLayout {
// Simple mode (Settings > Accessibility) is one column: the list, then the
// message in its place.
return interfaceLayout === 'mountain-view' || simpleMode ? 'focus' : mailLayout;
}
/**