feat: open the next message after archive or delete when the list is beside the reader

This commit is contained in:
Linus Rath
2026-11-04 07:08:31 +01:00
parent fe8fff338e
commit a19bdefdc1
38 changed files with 304 additions and 40 deletions
@@ -273,6 +273,75 @@ describe.each([
});
});
// With the list beside the reading pane (split, horizontal) the setting,
// while nobody has changed it, opens the next message instead, as Outlook
// does. A choice the user made wins, on every layout and device.
describe('Archive with the list beside the reading pane', () => {
beforeEach(() => {
useSettingsStore.setState({ interfaceLayout: 'default', mailLayout: 'split', explicitSettings: [] } as never);
useEmailStore.setState({ listRows: null } as never);
});
it('opens the next message while the setting is untouched', async () => {
render(<MailApp />);
fireEvent.click(await screen.findByText('stub archive', {}, { timeout: 5000 }));
await waitFor(() => expect(client.moveEmail).toHaveBeenCalledWith('m1', 'archive', undefined));
await waitFor(() => expect(selectedId()).toBe('m2'));
});
it('opens the next row in the order the list shows', async () => {
useEmailStore.setState({
listRows: [{ key: 'm1', members: ['m1'] }, { key: 'm3', members: ['m3'] }, { key: 'm2', members: ['m2'] }],
} as never);
render(<MailApp />);
fireEvent.click(await screen.findByText('stub archive', {}, { timeout: 5000 }));
await waitFor(() => expect(client.moveEmail).toHaveBeenCalledWith('m1', 'archive', undefined));
await waitFor(() => expect(selectedId()).toBe('m3'));
expect(opened).not.toContain('m2');
});
it('goes back to the list when the user switched the setting on', async () => {
useSettingsStore.setState({ returnToListAfterAction: true, explicitSettings: ['returnToListAfterAction'] } as never);
render(<MailApp />);
fireEvent.click(await screen.findByText('stub archive', {}, { timeout: 5000 }));
await waitFor(() => expect(useEmailStore.getState().emails.map((e) => e.id)).toEqual(['m2', 'm3']));
expect(selectedId()).toBeNull();
expect(opened).not.toContain('m2');
});
it('opens the archived message again on Undo', async () => {
client.restoreEmails = vi.fn().mockResolvedValue(undefined);
useEmailStore.setState({
refreshCurrentMailbox: vi.fn(async () => { useEmailStore.setState({ emails: inbox() } as never); }),
} as never);
render(<MailApp />);
fireEvent.click(await screen.findByText('stub archive', {}, { timeout: 5000 }));
await waitFor(() => expect(selectedId()).toBe('m2'));
const archived = await waitFor(() => {
const found = useToastStore.getState().toasts.find((toast) => toast.title === 'notifications.email_archived');
expect(found?.action).toBeTruthy();
return found!;
});
act(() => { archived.action!.onClick(); });
await waitFor(() => expect(client.restoreEmails).toHaveBeenCalled());
await waitFor(() => expect(selectedId()).toBe('m1'));
});
});
describe('Delete with the list beside the reading pane', () => {
beforeEach(() => {
useSettingsStore.setState({ interfaceLayout: 'default', mailLayout: 'horizontal', explicitSettings: [] } as never);
useEmailStore.setState({ listRows: null } as never);
});
it('opens the next message while the setting is untouched', async () => {
render(<MailApp />);
fireEvent.click(await screen.findByText('stub delete', {}, { timeout: 5000 }));
await waitFor(() => expect(client.moveToTrash).toHaveBeenCalled());
await waitFor(() => expect(selectedId()).toBe('m2'));
});
});
describe('A list collapsed under an open message (focused layout)', () => {
const list = () => screen.getByText('message list').closest('section')!;
+38 -5
View File
@@ -81,6 +81,7 @@ import { cn, formatDateTime, getMailboxPath } from "@/lib/utils";
import { localizeMailboxName } from "@/lib/mailbox-label";
import { KEYWORD_PREFIX, KEYWORD_PREFIX_LEGACY, emailKeyFor, groupEmailsByThread, threadKeyFor } from "@/lib/thread-utils";
import { stepListEmail } from "@/lib/list-step";
import { listBesideReader, returnToListAfterAction } from "@/lib/return-to-list";
import { resolveThreadRoute } from "@/lib/thread-routing";
import {
ErrorBoundary,
@@ -3341,12 +3342,41 @@ export function MailApp({ linkSegments: routeSegments }: MailAppProps = {}) {
// holds (a quick filter dropped it) stays selected: the store finds it
// through the selection (#695), and with no row to go on from it opens
// nothing after it anyway. (#1458)
//
// Left alone, the setting follows the layout (lib/return-to-list): with the
// list beside the reading pane the next message opens instead, as in
// Outlook. With the conversation in the reading pane that is the next
// conversation down the list (the one above at the end), as its row opens it.
const returnsToList = () => returnToListAfterAction(
!fullscreenReading && listBesideReader({ mailLayout, interfaceLayout, isMobile, isTablet }),
);
const leaveIfOpen = (email: Email) => {
const { selectedEmail: open, emails: rows } = useEmailStore.getState();
const { selectedEmail: open, emails: rows, listRows } = useEmailStore.getState();
const key = emailKeyFor(email);
if (!open || emailKeyFor(open) !== key || (mvShell && cursorOnly)) return;
if (!rows.some((row) => emailKeyFor(row) === key)) return;
if (useSettingsStore.getState().returnToListAfterAction) handleMobileBack();
if (returnsToList()) {
handleMobileBack();
return;
}
if (!paneConversationView || !listRows) return;
const below = stepListEmail(rows, key, 1, listRows);
const next = below && emailKeyFor(below) !== key ? below : stepListEmail(rows, key, -1, listRows);
if (!next || emailKeyFor(next) === key || threadKeyFor(next) === threadKeyFor(email)) return;
handleMobileBack();
void handleEmailSelect(next);
};
// Undo puts back the message that was open, and opens it again, where it
// was open in the reading pane. Phones stay in the list.
const reopenEmailRef = useRef<((email: Email) => Promise<unknown>) | null>(null);
const reopenAfterUndo = (email: Email) => {
const { selectedEmail: open } = useEmailStore.getState();
if (isMobile || !open || emailKeyFor(open) !== emailKeyFor(email)) return undefined;
const key = emailKeyFor(email);
return () => {
const restored = useEmailStore.getState().emails.find((e) => emailKeyFor(e) === key);
if (restored) void reopenEmailRef.current?.(restored);
};
};
const handleDelete = async (emailToDelete: Email | null = selectedEmail) => {
@@ -3390,6 +3420,7 @@ export function MailApp({ linkSegments: routeSegments }: MailAppProps = {}) {
return;
}
const reopen = reopenAfterUndo(emailToDelete);
leaveIfOpen(emailToDelete);
// The store's delete reads the "Delete action" setting: move to the
@@ -3398,7 +3429,7 @@ export function MailApp({ linkSegments: routeSegments }: MailAppProps = {}) {
try {
// false: a plugin stopped it and said why, so no "deleted" here.
if (await deleteEmail(client, emailToDelete.id, isInTrash, emailKeyFor(emailToDelete))) {
toastWithUndo(t('notifications.email_deleted'), undoMoveAction(client, restore, undo));
toastWithUndo(t('notifications.email_deleted'), undoMoveAction(client, restore, undo, undefined, reopen));
}
} catch (error) {
console.error("Failed to delete email:", error);
@@ -3498,6 +3529,7 @@ export function MailApp({ linkSegments: routeSegments }: MailAppProps = {}) {
}
let restore: EmailRestoreBatch[] | null = null;
const reopen = reopenAfterUndo(emailToArchive);
leaveIfOpen(emailToArchive);
const archived = await moveThreadToMailbox(client, emailToArchive.id, destination.id, emailKeyFor(emailToArchive), (batches) => { restore = batches; });
if (!archived) return;
@@ -3509,7 +3541,7 @@ export function MailApp({ linkSegments: routeSegments }: MailAppProps = {}) {
setConversationEmails([]);
}
toastWithUndo(t('notifications.email_archived'), undoMoveAction(client, restore, undo));
toastWithUndo(t('notifications.email_archived'), undoMoveAction(client, restore, undo, undefined, reopen));
void refreshMailboxes();
} catch (error) {
console.error("Failed to archive email:", error);
@@ -5044,6 +5076,7 @@ export function MailApp({ linkSegments: routeSegments }: MailAppProps = {}) {
};
pluginOpenEmailRef.current = handleEmailSelect;
reopenEmailRef.current = handleEmailSelect;
// Handle back navigation from viewer on mobile.
// Reset to list state directly. We can't just call window.history.back()
@@ -6099,7 +6132,7 @@ export function MailApp({ linkSegments: routeSegments }: MailAppProps = {}) {
// Marking the open message unread returns to the list
// (Gmail-style, gated on returnToListAfterAction). Staying
// in the reading pane would just re-mark it read on view.
if (!read && useSettingsStore.getState().returnToListAfterAction) {
if (!read && returnsToList()) {
handleMobileBack();
}
}
@@ -12,6 +12,7 @@ import { useNoThirdPartyTraffic } from '@/hooks/use-third-party-mode';
import { openKeyboardShortcuts } from '@/lib/app-commands';
import { voicesForLocale } from '@/lib/read-aloud';
import { useSpeechVoices } from '@/hooks/use-read-aloud';
import { useReturnToListAfterAction } from '@/hooks/use-return-to-list';
import { NOTIFICATION_SOUNDS, playNotificationSound, type NotificationSoundChoice } from '@/lib/notification-sound';
import {
ACCESSIBILITY_KEYS,
@@ -73,6 +74,8 @@ export function AccessibilitySettings() {
const [resetDone, setResetDone] = useState(false);
const locale = useLocale();
const speechVoices = useSpeechVoices();
// As it applies in this layout: left alone, it follows the layout.
const returnToList = useReturnToListAfterAction();
const toastMin = getRestriction('toastDuration')?.min;
const allowsValue = (key: string, value: unknown) =>
@@ -502,7 +505,7 @@ export function AccessibilitySettings() {
</SettingItem>
)}
<SettingItem label={tEmail('return_to_list_after_action.label')} description={tEmail('return_to_list_after_action.description')}>
<ToggleSwitch checked={settings.returnToListAfterAction} onChange={(checked) => set('returnToListAfterAction', checked)} />
<ToggleSwitch checked={returnToList.checked} onChange={(checked) => set('returnToListAfterAction', checked)} />
</SettingItem>
</SettingsSection>
+11 -3
View File
@@ -11,6 +11,7 @@ import { usePolicyStore } from '@/stores/policy-store';
import { useContactStore } from '@/stores/contact-store';
import { useAuthStore } from '@/stores/auth-store';
import { useRemoteContentBlockedByOrg } from '@/hooks/use-third-party-mode';
import { useReturnToListAfterAction } from '@/hooks/use-return-to-list';
/**
* The Reading page: the open message. Opening it, how its body is drawn,
@@ -26,12 +27,14 @@ export function ReadingSettings() {
const tLock = useTranslations('remote_content_lock');
const t = useTranslations('settings.email_behavior');
const tPage = useTranslations('settings.reading_page');
const tPane = useTranslations('conversation_pane');
// As it applies in this layout: left alone, it follows the layout.
const returnToList = useReturnToListAfterAction();
const [showTrustedModal, setShowTrustedModal] = useState(false);
const { isSettingLocked, isSettingHidden, isFeatureEnabled } = usePolicyStore();
const {
markAsReadDelay,
returnToListAfterAction,
mailAttachmentAction,
attachmentPosition,
hideInlineImageAttachments,
@@ -78,9 +81,14 @@ export function ReadingSettings() {
</SettingItem>
)}
<SettingItem label={t('return_to_list_after_action.label')} description={t('return_to_list_after_action.description')}>
<SettingItem
label={t('return_to_list_after_action.label')}
description={returnToList.followsLayout
? `${t('return_to_list_after_action.description')} ${tPane('return_to_list_follows_layout')}`
: t('return_to_list_after_action.description')}
>
<ToggleSwitch
checked={returnToListAfterAction}
checked={returnToList.checked}
onChange={(checked) => updateSetting('returnToListAfterAction', checked)}
/>
</SettingItem>
+28
View File
@@ -0,0 +1,28 @@
"use client";
import { useSettingsStore } from "@/stores/settings-store";
import { usePolicyStore } from "@/stores/policy-store";
import { useDeviceDetection } from "@/hooks/use-media-query";
import { effectiveReturnToList, listBesideReader, returnToListSetByPolicy } from "@/lib/return-to-list";
/**
* The "Return to list after…" switch as it applies on this device and in this
* layout (lib/return-to-list), and whether that comes from the layout because
* nobody has set it yet.
*/
export function useReturnToListAfterAction(): { checked: boolean; followsLayout: boolean } {
const { isMobile, isTablet } = useDeviceDetection();
const value = useSettingsStore((s) => s.returnToListAfterAction);
const explicitSettings = useSettingsStore((s) => s.explicitSettings);
const mailLayout = useSettingsStore((s) => s.mailLayout);
const interfaceLayout = useSettingsStore((s) => s.interfaceLayout);
const simpleMode = useSettingsStore((s) => s.simpleMode);
// Re-read when the policy arrives or changes.
usePolicyStore((s) => s.policy);
const policySet = returnToListSetByPolicy();
const besideReader = listBesideReader({ mailLayout, interfaceLayout, simpleMode, isMobile, isTablet });
return {
checked: effectiveReturnToList({ returnToListAfterAction: value, explicitSettings }, besideReader, policySet),
followsLayout: !policySet && !explicitSettings.includes("returnToListAfterAction"),
};
}
+41
View File
@@ -0,0 +1,41 @@
import { describe, it, expect } from 'vitest';
import { effectiveReturnToList, listBesideReader } from '../return-to-list';
// "Return to list after…": left alone it follows the layout (next message
// with the list beside the reader, back to the list elsewhere); set by the
// user or the admin, it decides everywhere.
const untouched = { returnToListAfterAction: true, explicitSettings: [] as string[] };
const chosen = (value: boolean) => ({ returnToListAfterAction: value, explicitSettings: ['returnToListAfterAction'] });
describe('listBesideReader', () => {
const desktop = { isMobile: false, isTablet: false, interfaceLayout: 'default' as const };
it('is true for the split and horizontal layouts on a desktop', () => {
expect(listBesideReader({ ...desktop, mailLayout: 'split' })).toBe(true);
expect(listBesideReader({ ...desktop, mailLayout: 'horizontal' })).toBe(true);
});
it('is false where the message takes the list’s place', () => {
expect(listBesideReader({ ...desktop, mailLayout: 'focus' })).toBe(false);
expect(listBesideReader({ ...desktop, mailLayout: 'split', interfaceLayout: 'mountain-view' })).toBe(false);
expect(listBesideReader({ ...desktop, mailLayout: 'split', simpleMode: true })).toBe(false);
expect(listBesideReader({ ...desktop, mailLayout: 'split', isMobile: true })).toBe(false);
expect(listBesideReader({ ...desktop, mailLayout: 'split', isTablet: true })).toBe(false);
});
});
describe('effectiveReturnToList', () => {
it('opens the next message beside the reader while nobody set it', () => {
expect(effectiveReturnToList(untouched, true, false)).toBe(false);
});
it('goes back to the list elsewhere while nobody set it', () => {
expect(effectiveReturnToList(untouched, false, false)).toBe(true);
});
it('follows the user’s choice on every layout', () => {
expect(effectiveReturnToList(chosen(true), true, false)).toBe(true);
expect(effectiveReturnToList(chosen(false), false, false)).toBe(false);
});
it('follows an admin preset or lock', () => {
expect(effectiveReturnToList(untouched, true, true)).toBe(true);
expect(effectiveReturnToList({ ...untouched, returnToListAfterAction: false }, false, true)).toBe(false);
});
});
+3 -1
View File
@@ -166,13 +166,14 @@ export async function setSelectionTagWithUndo(
*
* `receipts` (filled while the move runs) name the messages it took to
* another account: Undo moves those back, the same way, into the folders
* their restore points name (undoMove).
* their restore points name (undoMove). `onUndone` runs once they are back.
*/
export function undoMoveAction(
client: IJMAPClient | null | undefined,
batches: EmailRestoreBatch[] | null | undefined,
labels: UndoLabels,
receipts?: CrossAccountMoveReceipt[],
onUndone?: () => void,
): UndoToastAction | undefined {
if (!client || !batches || !batches.some(b => b.points.length > 0)) return undefined;
return {
@@ -183,6 +184,7 @@ export function undoMoveAction(
try {
await useEmailStore.getState().undoMove(client, batches, receipts ?? []);
toast.success(labels.done);
onUndone?.();
} catch (error) {
console.error("Failed to undo:", error);
toast.error(labels.failed);
+50
View File
@@ -0,0 +1,50 @@
import { effectiveMailLayout, useSettingsStore, type InterfaceLayout, type MailLayout } from '@/stores/settings-store';
import { usePolicyStore } from '@/stores/policy-store';
const KEY = 'returnToListAfterAction';
/**
* Whether the message list and the reading pane are on screen side by side:
* the split and horizontal layouts on a desktop. Phones, tablets, the focused
* layout, Simple mode and Mountain View put the message in the list's place.
*/
export function listBesideReader(opts: {
mailLayout: MailLayout;
interfaceLayout: InterfaceLayout;
simpleMode?: boolean;
isMobile: boolean;
isTablet: boolean;
}): boolean {
if (opts.isMobile || opts.isTablet) return false;
return effectiveMailLayout(opts.mailLayout, opts.interfaceLayout, opts.simpleMode) !== 'focus';
}
/**
* "Return to list after delete, archive, move or mark unread", as it applies
* here. Set by the user (on any device: the choice syncs) or by the admin
* policy, the setting decides. Left alone, it follows the layout: with the
* list beside the reading pane the next message opens, as in Outlook, so a
* folder is worked through with e, e, e; where the message takes the list's
* place it goes back to the list. Each device judges its own layout.
*/
export function effectiveReturnToList(
settings: { returnToListAfterAction: boolean; explicitSettings: readonly string[] },
besideReader: boolean,
policySet: boolean,
): boolean {
if (policySet || settings.explicitSettings.includes(KEY)) return settings.returnToListAfterAction;
return besideReader ? false : settings.returnToListAfterAction;
}
/** Whether the admin policy presets or locks the setting. */
export function returnToListSetByPolicy(): boolean {
const policy = usePolicyStore.getState();
const defaults = policy.policy?.defaults;
return (!!defaults && Object.prototype.hasOwnProperty.call(defaults, KEY)) || policy.isSettingLocked(KEY);
}
/** The setting as it applies now, read at the moment of the action. */
export function returnToListAfterAction(besideReader: boolean): boolean {
return effectiveReturnToList(useSettingsStore.getState(), besideReader, returnToListSetByPolicy());
}
+2 -1
View File
@@ -3767,7 +3767,8 @@
"collapse_named": "طي المحادثة: {subject}"
},
"conversation_pane": {
"show_older": "{count, plural, zero {عرض # رسالة أقدم} one {عرض رسالة أقدم واحدة} two {عرض رسالتين أقدم} few {عرض # رسائل أقدم} many {عرض # رسالة أقدم} other {عرض # رسالة أقدم}}"
"show_older": "{count, plural, zero {عرض # رسالة أقدم} one {عرض رسالة أقدم واحدة} two {عرض رسالتين أقدم} few {عرض # رسائل أقدم} many {عرض # رسالة أقدم} other {عرض # رسالة أقدم}}",
"return_to_list_follows_layout": "ما لم تغيّره، يتبع هذا التخطيط: متوقف عندما تكون القائمة والرسالة جنبًا إلى جنب، ومفعّل في غير ذلك."
},
"identities": {
"modal_title": "إدارة هويات الإرسال",
+2 -1
View File
@@ -3767,7 +3767,8 @@
"collapse_named": "Replega la conversa: {subject}"
},
"conversation_pane": {
"show_older": "{count, plural, one {Mostra # missatge anterior} other {Mostra # missatges anteriors}}"
"show_older": "{count, plural, one {Mostra # missatge anterior} other {Mostra # missatges anteriors}}",
"return_to_list_follows_layout": "Mentre no ho canviïs, segueix el disseny: desactivat quan la llista i el missatge són l'un al costat de l'altre, activat en els altres casos."
},
"identities": {
"modal_title": "Gestiona les identitats d'enviament",
+2 -1
View File
@@ -3767,7 +3767,8 @@
"collapse_named": "Sbalit konverzaci: {subject}"
},
"conversation_pane": {
"show_older": "{count, plural, one {Zobrazit # starší zprávu} few {Zobrazit # starší zprávy} many {Zobrazit # starší zprávy} other {Zobrazit # starších zpráv}}"
"show_older": "{count, plural, one {Zobrazit # starší zprávu} few {Zobrazit # starší zprávy} many {Zobrazit # starší zprávy} other {Zobrazit # starších zpráv}}",
"return_to_list_follows_layout": "Dokud to nezměníte, řídí se to rozvržením: vypnuto, když jsou seznam a zpráva vedle sebe, jinak zapnuto."
},
"identities": {
"modal_title": "Správa identit odesílatele",
+2 -1
View File
@@ -3767,7 +3767,8 @@
"collapse_named": "Skjul samtale: {subject}"
},
"conversation_pane": {
"show_older": "{count, plural, one {Vis # ældre besked} other {Vis # ældre beskeder}}"
"show_older": "{count, plural, one {Vis # ældre besked} other {Vis # ældre beskeder}}",
"return_to_list_follows_layout": "Indtil du ændrer det, følger det layoutet: fra, når listen og beskeden står side om side, ellers til."
},
"identities": {
"modal_title": "Administrér afsenderidentiteter",
+2 -1
View File
@@ -3767,7 +3767,8 @@
"collapse_named": "Konversation zuklappen: {subject}"
},
"conversation_pane": {
"show_older": "{count, plural, one {# ältere Nachricht anzeigen} other {# ältere Nachrichten anzeigen}}"
"show_older": "{count, plural, one {# ältere Nachricht anzeigen} other {# ältere Nachrichten anzeigen}}",
"return_to_list_follows_layout": "Solange Sie es nicht ändern, richtet sich das nach dem Layout: aus, wenn Liste und Nachricht nebeneinander stehen, sonst an."
},
"identities": {
"modal_title": "Sendeidentitäten verwalten",
+2 -1
View File
@@ -3767,7 +3767,8 @@
"collapse_named": "Collapse conversation: {subject}"
},
"conversation_pane": {
"show_older": "{count, plural, one {Show # older message} other {Show # older messages}}"
"show_older": "{count, plural, one {Show # older message} other {Show # older messages}}",
"return_to_list_follows_layout": "Until you change it, this follows the layout: off when the list and the message are side by side, on otherwise."
},
"identities": {
"modal_title": "Manage sending identities",
+2 -1
View File
@@ -3767,7 +3767,8 @@
"collapse_named": "Contraer conversación: {subject}"
},
"conversation_pane": {
"show_older": "{count, plural, one {Mostrar # mensaje anterior} other {Mostrar # mensajes anteriores}}"
"show_older": "{count, plural, one {Mostrar # mensaje anterior} other {Mostrar # mensajes anteriores}}",
"return_to_list_follows_layout": "Mientras no lo cambies, sigue el diseño: desactivado cuando la lista y el mensaje están uno al lado del otro, activado en los demás casos."
},
"identities": {
"modal_title": "Administrar Identidades de Envío",
+2 -1
View File
@@ -3767,7 +3767,8 @@
"collapse_named": "Sulge vestlus: {subject}"
},
"conversation_pane": {
"show_older": "{count, plural, one {Näita # vanemat sõnumit} other {Näita # vanemat sõnumit}}"
"show_older": "{count, plural, one {Näita # vanemat sõnumit} other {Näita # vanemat sõnumit}}",
"return_to_list_follows_layout": "Kuni te seda ei muuda, järgib see paigutust: väljas, kui loend ja sõnum on kõrvuti, muidu sees."
},
"identities": {
"modal_title": "Halda saatja identiteete",
+2 -1
View File
@@ -3767,7 +3767,8 @@
"collapse_named": "جمع کردن مکالمه: {subject}"
},
"conversation_pane": {
"show_older": "{count, plural, one {نمایش # پیام قدیمی‌تر} other {نمایش # پیام قدیمی‌تر}}"
"show_older": "{count, plural, one {نمایش # پیام قدیمی‌تر} other {نمایش # پیام قدیمی‌تر}}",
"return_to_list_follows_layout": "تا وقتی آن را تغییر ندهید، از چیدمان پیروی می‌کند: وقتی فهرست و پیام کنار هم هستند خاموش، در غیر این صورت روشن."
},
"identities": {
"modal_title": "مدیریت هویت‌های ارسال",
+2 -1
View File
@@ -3767,7 +3767,8 @@
"collapse_named": "Réduire la conversation: {subject}"
},
"conversation_pane": {
"show_older": "{count, plural, one {Afficher # message plus ancien} other {Afficher # messages plus anciens}}"
"show_older": "{count, plural, one {Afficher # message plus ancien} other {Afficher # messages plus anciens}}",
"return_to_list_follows_layout": "Tant que vous ne le modifiez pas, ce réglage suit la disposition : désactivé quand la liste et le message sont côte à côte, activé sinon."
},
"identities": {
"modal_title": "Gérer les identités d'envoi",
+2 -1
View File
@@ -3664,7 +3664,8 @@
"collapse_named": "כווץ את השיחה: {subject}"
},
"conversation_pane": {
"show_older": "{count, plural, one {הצגת הודעה ישנה אחת} two {הצגת שתי הודעות ישנות} other {הצגת # הודעות ישנות}}"
"show_older": "{count, plural, one {הצגת הודעה ישנה אחת} two {הצגת שתי הודעות ישנות} other {הצגת # הודעות ישנות}}",
"return_to_list_follows_layout": "כל עוד לא תשנו זאת, זה נקבע לפי הפריסה: כבוי כשהרשימה וההודעה זו לצד זו, ופועל בכל מצב אחר."
},
"identities": {
"modal_title": "ניהול זהויות שליחה",
+2 -1
View File
@@ -3767,7 +3767,8 @@
"collapse_named": "Beszélgetés összecsukása: {subject}"
},
"conversation_pane": {
"show_older": "{count, plural, one {# régebbi üzenet megjelenítése} other {# régebbi üzenet megjelenítése}}"
"show_older": "{count, plural, one {# régebbi üzenet megjelenítése} other {# régebbi üzenet megjelenítése}}",
"return_to_list_follows_layout": "Amíg nem módosítja, az elrendezést követi: ki van kapcsolva, ha a lista és az üzenet egymás mellett van, egyébként be."
},
"identities": {
"modal_title": "Küldő azonosságok kezelése",
+2 -1
View File
@@ -3767,7 +3767,8 @@
"collapse_named": "Comprimi conversazione: {subject}"
},
"conversation_pane": {
"show_older": "{count, plural, one {Mostra # messaggio precedente} other {Mostra # messaggi precedenti}}"
"show_older": "{count, plural, one {Mostra # messaggio precedente} other {Mostra # messaggi precedenti}}",
"return_to_list_follows_layout": "Finché non lo modifichi, segue il layout: disattivato quando l'elenco e il messaggio sono affiancati, attivato negli altri casi."
},
"identities": {
"modal_title": "Gestisci identità di invio",
+2 -1
View File
@@ -3767,7 +3767,8 @@
"collapse_named": "会話を折りたたむ: {subject}"
},
"conversation_pane": {
"show_older": "{count, plural, other {以前のメッセージを # 件表示}}"
"show_older": "{count, plural, other {以前のメッセージを # 件表示}}",
"return_to_list_follows_layout": "変更するまでは、レイアウトに合わせて切り替わります。一覧とメッセージが横に並ぶ場合はオフ、それ以外はオンです。"
},
"identities": {
"modal_title": "送信者情報を管理",
+2 -1
View File
@@ -3767,7 +3767,8 @@
"collapse_named": "대화 접기: {subject}"
},
"conversation_pane": {
"show_older": "{count, plural, other {이전 메시지 #개 보기}}"
"show_older": "{count, plural, other {이전 메시지 #개 보기}}",
"return_to_list_follows_layout": "변경하기 전까지는 레이아웃을 따릅니다. 목록과 메시지가 나란히 있으면 꺼지고, 그 밖에는 켜집니다."
},
"identities": {
"modal_title": "보내는 사람 관리",
+2 -1
View File
@@ -3767,7 +3767,8 @@
"collapse_named": "Sairt sarunu: {subject}"
},
"conversation_pane": {
"show_older": "{count, plural, zero {Rādīt # vecākas ziņas} one {Rādīt # vecāku ziņu} other {Rādīt # vecākas ziņas}}"
"show_older": "{count, plural, zero {Rādīt # vecākas ziņas} one {Rādīt # vecāku ziņu} other {Rādīt # vecākas ziņas}}",
"return_to_list_follows_layout": "Kamēr to nemaināt, tas seko izkārtojumam: izslēgts, ja saraksts un ziņa ir blakus, citādi ieslēgts."
},
"identities": {
"modal_title": "Pārvaldīt sūtītāja identitātes",
+2 -1
View File
@@ -3767,7 +3767,8 @@
"collapse_named": "Харилцан яриаг хураах: {subject}"
},
"conversation_pane": {
"show_older": "{count, plural, one {Өмнөх # захиа харуулах} other {Өмнөх # захиа харуулах}}"
"show_older": "{count, plural, one {Өмнөх # захиа харуулах} other {Өмнөх # захиа харуулах}}",
"return_to_list_follows_layout": "Та өөрчлөхөөс нааш энэ нь байрлалаас хамаарна: жагсаалт ба захиа зэрэгцэн байвал унтраалттай, бусад үед асаалттай."
},
"identities": {
"modal_title": "Илгээж буй таниулбаруудыг удирдах",
+2 -1
View File
@@ -3767,7 +3767,8 @@
"collapse_named": "Fold sammen samtalen: {subject}"
},
"conversation_pane": {
"show_older": "{count, plural, one {Vis # eldre melding} other {Vis # eldre meldinger}}"
"show_older": "{count, plural, one {Vis # eldre melding} other {Vis # eldre meldinger}}",
"return_to_list_follows_layout": "Inntil du endrer det, følger det oppsettet: av når listen og meldingen står side om side, ellers på."
},
"identities": {
"modal_title": "Administrer avsenderidentiteter",
+2 -1
View File
@@ -3767,7 +3767,8 @@
"collapse_named": "Gesprek inklappen: {subject}"
},
"conversation_pane": {
"show_older": "{count, plural, one {# ouder bericht tonen} other {# oudere berichten tonen}}"
"show_older": "{count, plural, one {# ouder bericht tonen} other {# oudere berichten tonen}}",
"return_to_list_follows_layout": "Zolang je het niet wijzigt, volgt dit de indeling: uit als de lijst en het bericht naast elkaar staan, anders aan."
},
"identities": {
"modal_title": "Verzendidentiteiten beheren",
+2 -1
View File
@@ -3767,7 +3767,8 @@
"collapse_named": "Zwiń konwersację: {subject}"
},
"conversation_pane": {
"show_older": "{count, plural, one {Pokaż # starszą wiadomość} few {Pokaż # starsze wiadomości} many {Pokaż # starszych wiadomości} other {Pokaż # starszej wiadomości}}"
"show_older": "{count, plural, one {Pokaż # starszą wiadomość} few {Pokaż # starsze wiadomości} many {Pokaż # starszych wiadomości} other {Pokaż # starszej wiadomości}}",
"return_to_list_follows_layout": "Dopóki tego nie zmienisz, ustawienie zależy od układu: wyłączone, gdy lista i wiadomość są obok siebie, w innych przypadkach włączone."
},
"identities": {
"modal_title": "Zarządzaj tożsamościami nadawcy",
+2 -1
View File
@@ -3767,7 +3767,8 @@
"collapse_named": "Recolher conversa: {subject}"
},
"conversation_pane": {
"show_older": "{count, plural, one {Mostrar # mensagem anterior} other {Mostrar # mensagens anteriores}}"
"show_older": "{count, plural, one {Mostrar # mensagem anterior} other {Mostrar # mensagens anteriores}}",
"return_to_list_follows_layout": "Enquanto não alterar, segue o layout: desligado quando a lista e a mensagem estão lado a lado, ligado nos outros casos."
},
"identities": {
"modal_title": "Gerenciar Identidades de Envio",
+2 -1
View File
@@ -3767,7 +3767,8 @@
"collapse_named": "Ascunde conversația: {subject}"
},
"conversation_pane": {
"show_older": "{count, plural, one {Afișează # mesaj mai vechi} few {Afișează # mesaje mai vechi} other {Afișează # de mesaje mai vechi}}"
"show_older": "{count, plural, one {Afișează # mesaj mai vechi} few {Afișează # mesaje mai vechi} other {Afișează # de mesaje mai vechi}}",
"return_to_list_follows_layout": "Cât timp nu îl modificați, urmează aspectul: dezactivat când lista și mesajul sunt unul lângă altul, activat în rest."
},
"identities": {
"modal_title": "Gestionarea identităților de expediere",
+2 -1
View File
@@ -3767,7 +3767,8 @@
"collapse_named": "Свернуть разговор: {subject}"
},
"conversation_pane": {
"show_older": "{count, plural, one {Показать # более раннее письмо} few {Показать # более ранних письма} many {Показать # более ранних писем} other {Показать # более раннего письма}}"
"show_older": "{count, plural, one {Показать # более раннее письмо} few {Показать # более ранних письма} many {Показать # более ранних писем} other {Показать # более раннего письма}}",
"return_to_list_follows_layout": "Пока вы это не измените, настройка зависит от макета: выключена, когда список и письмо стоят рядом, в остальных случаях включена."
},
"identities": {
"modal_title": "Управление идентификациями отправителя",
+2 -1
View File
@@ -3767,7 +3767,8 @@
"collapse_named": "Zbaliť konverzáciu: {subject}"
},
"conversation_pane": {
"show_older": "{count, plural, one {Zobraziť # staršiu správu} few {Zobraziť # staršie správy} many {Zobraziť # staršej správy} other {Zobraziť # starších správ}}"
"show_older": "{count, plural, one {Zobraziť # staršiu správu} few {Zobraziť # staršie správy} many {Zobraziť # staršej správy} other {Zobraziť # starších správ}}",
"return_to_list_follows_layout": "Kým to nezmeníte, riadi sa to rozložením: vypnuté, keď sú zoznam a správa vedľa seba, inak zapnuté."
},
"identities": {
"modal_title": "Správa identít odosielateľa",
+2 -1
View File
@@ -614,7 +614,8 @@
"collapse_named": "ย่อการสนทนา: {subject}"
},
"conversation_pane": {
"show_older": "{count, plural, other {แสดงข้อความก่อนหน้า # ฉบับ}}"
"show_older": "{count, plural, other {แสดงข้อความก่อนหน้า # ฉบับ}}",
"return_to_list_follows_layout": "จนกว่าคุณจะเปลี่ยน ค่านี้จะเป็นไปตามเลย์เอาต์: ปิดเมื่อรายการและข้อความอยู่เคียงข้างกัน และเปิดในกรณีอื่น"
},
"notifications": {
"email_sent": "ส่งอีเมลแล้ว",
+2 -1
View File
@@ -3767,7 +3767,8 @@
"collapse_named": "Konuşmayı daralt: {subject}"
},
"conversation_pane": {
"show_older": "{count, plural, one {# eski iletiyi göster} other {# eski iletiyi göster}}"
"show_older": "{count, plural, one {# eski iletiyi göster} other {# eski iletiyi göster}}",
"return_to_list_follows_layout": "Siz değiştirene kadar bu ayar düzene göre belirlenir: liste ve ileti yan yanaysa kapalı, değilse açık."
},
"identities": {
"modal_title": "Gönderme Kimliklerini Yönet",
+2 -1
View File
@@ -3767,7 +3767,8 @@
"collapse_named": "Згорнути бесіду: {subject}"
},
"conversation_pane": {
"show_older": "{count, plural, one {Показати # давніший лист} few {Показати # давніші листи} many {Показати # давніших листів} other {Показати # давнішого листа}}"
"show_older": "{count, plural, one {Показати # давніший лист} few {Показати # давніші листи} many {Показати # давніших листів} other {Показати # давнішого листа}}",
"return_to_list_follows_layout": "Поки ви це не зміните, налаштування залежить від макета: вимкнено, коли список і лист розташовані поруч, в інших випадках увімкнено."
},
"identities": {
"modal_title": "Керування ідентифікаторами надсилання",
+2 -1
View File
@@ -3779,7 +3779,8 @@
"collapse_named": "Thu gọn cuộc trò chuyện: {subject}"
},
"conversation_pane": {
"show_older": "{count, plural, other {Hiện # thư cũ hơn}}"
"show_older": "{count, plural, other {Hiện # thư cũ hơn}}",
"return_to_list_follows_layout": "Cho đến khi bạn thay đổi, cài đặt này theo bố cục: tắt khi danh sách và thư nằm cạnh nhau, bật trong các trường hợp khác."
},
"identities": {
"modal_title": "Quản lý danh tính gửi thư",
+2 -1
View File
@@ -3767,7 +3767,8 @@
"collapse_named": "收合對話串: {subject}"
},
"conversation_pane": {
"show_older": "{count, plural, other {顯示 # 封較早的郵件}}"
"show_older": "{count, plural, other {顯示 # 封較早的郵件}}",
"return_to_list_follows_layout": "在您變更之前,此項會依版面配置而定:清單與郵件並排顯示時關閉,其他情況下開啟。"
},
"identities": {
"modal_title": "管理寄件身分",
+2 -1
View File
@@ -3767,7 +3767,8 @@
"collapse_named": "折叠会话: {subject}"
},
"conversation_pane": {
"show_older": "{count, plural, other {显示 # 封较早的邮件}}"
"show_older": "{count, plural, other {显示 # 封较早的邮件}}",
"return_to_list_follows_layout": "在你更改之前,此项随布局而定:列表和邮件并排显示时关闭,其他情况下开启。"
},
"identities": {
"modal_title": "管理发送身份",