mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-05 00:14:18 +00:00
feat: open the next message after archive or delete when the list is beside the reader
This commit is contained in:
@@ -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')!;
|
||||
|
||||
|
||||
@@ -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,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>
|
||||
|
||||
@@ -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"),
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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": "إدارة هويات الإرسال",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "مدیریت هویتهای ارسال",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "ניהול זהויות שליחה",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "送信者情報を管理",
|
||||
|
||||
@@ -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": "보내는 사람 관리",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "Илгээж буй таниулбаруудыг удирдах",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "Управление идентификациями отправителя",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "ส่งอีเมลแล้ว",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "Керування ідентифікаторами надсилання",
|
||||
|
||||
@@ -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ư",
|
||||
|
||||
@@ -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": "管理寄件身分",
|
||||
|
||||
@@ -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": "管理发送身份",
|
||||
|
||||
Reference in New Issue
Block a user