fix: grey out folder and message actions that can't work offline or with nothing unread

This commit is contained in:
Linus Rath
2026-11-04 05:32:23 +01:00
parent 551b5ce4fa
commit f92b7d942d
38 changed files with 619 additions and 38 deletions
@@ -58,10 +58,10 @@ describe('message menu key hints', () => {
it('shows the keys the list runs each action with', () => {
renderMenu();
expect(within(row('reply')).getByText('R')).toBeInTheDocument();
expect(within(row('reply_all')).getByText('Shift+R')).toBeInTheDocument();
expect(within(row('reply_all')).getByText('Shift + R')).toBeInTheDocument();
expect(within(row('forward')).getByText('F')).toBeInTheDocument();
expect(within(row('star')).getByText('S')).toBeInTheDocument();
expect(within(row('mark_read')).getByText('Shift+I')).toBeInTheDocument();
expect(within(row('mark_read')).getByText('Shift + I')).toBeInTheDocument();
expect(within(row('mark_as_spam')).getByText('!')).toBeInTheDocument();
expect(within(screen.getByTestId('ctx-delete')).getByText('Del')).toBeInTheDocument();
});
@@ -77,13 +77,38 @@ describe('message menu key hints', () => {
it('shows them on a selection too', () => {
renderMenu({ isMultiSelect: true, selectedCount: 2, onBatchDelete: vi.fn(), onBatchMarkAsRead: vi.fn(), onBatchMarkAsSpam: vi.fn() });
expect(within(screen.getByTestId('ctx-delete')).getByText('Del')).toBeInTheDocument();
expect(within(screen.getByTestId('ctx-mark-read')).getByText('Shift+I')).toBeInTheDocument();
expect(within(screen.getByTestId('ctx-mark-read')).getByText('Shift + I')).toBeInTheDocument();
expect(within(screen.getByTestId('ctx-spam')).getByText('!')).toBeInTheDocument();
});
it('matches the read state: U marks a read message unread', () => {
expect(messageActionShortcut('read', { unread: false, singleKeys: true })).toBe('U');
expect(messageActionShortcut('read', { unread: true, singleKeys: true })).toBe('Shift+I');
expect(messageActionShortcut('read', { unread: true, singleKeys: true })).toBe('Shift + I');
expect(messageActionShortcut('print', { singleKeys: true })).toBeUndefined();
});
});
describe('message menu offline', () => {
afterEach(async () => {
const { useOfflineStore } = await import('@/lib/offline/state');
useOfflineStore.setState({ browserOffline: false, enabled: [] });
});
it('greys out changes that would fail without a connection, and says why', async () => {
const { useOfflineStore } = await import('@/lib/offline/state');
useOfflineStore.setState({ browserOffline: true, enabled: [] });
renderMenu();
expect(screen.getByText('offline_header')).toBeInTheDocument();
expect((screen.getByTestId('ctx-delete') as HTMLButtonElement).disabled).toBe(true);
expect((screen.getByTestId('ctx-star') as HTMLButtonElement).disabled).toBe(true);
expect((screen.getByTestId('ctx-mark-read') as HTMLButtonElement).disabled).toBe(true);
// Writing a reply still works: the composer keeps the draft.
expect(row('reply')).not.toBeDisabled();
});
it('leaves them on while online', () => {
renderMenu();
expect(screen.queryByText('offline_header')).toBeNull();
expect((screen.getByTestId('ctx-delete') as HTMLButtonElement).disabled).toBe(false);
});
});
+27 -12
View File
@@ -31,6 +31,7 @@ import {
} from "@/components/icons";
import { useMessageActionList, type MessageActionEntry, type MessageActionKey } from "./message-actions";
import { useMessageActionShortcut } from "./message-action-shortcuts";
import { useConnectionState } from "@/hooks/use-connection-state";
import { buildMailboxTree, MailboxNode } from "@/lib/utils";
import { buildMoveTargets, resolveMoveOwnerAccountId } from "@/lib/move-targets";
import { localizeMailboxName } from "@/lib/mailbox-label";
@@ -142,6 +143,12 @@ interface EmailContextMenuProps {
label?: string;
}
/** Entries that change the message on the server, or queue the change offline. */
const CHANGES_MAIL = new Set<MessageActionKey>([
"archive", "delete", "read", "remove_from_folder", "star", "pin", "spam",
"reschedule", "cancel_send", "cancel_and_edit",
]);
export function EmailContextMenu({
email,
position,
@@ -200,6 +207,11 @@ export function EmailContextMenu({
}: EmailContextMenuProps) {
const t = useTranslations("context_menu");
const shortcutOf = useMessageActionShortcut();
// Offline, a change to a message waits in the outbox only where the account keeps
// its mail on this device; otherwise it would fail, so it's greyed out. Rules and
// copies to another account always need the server.
const { offline, canChangeMail } = useConnectionState();
const tMenu = useTranslations("menuAudit");
const archiveActionEnabled = useArchiveActionEnabled();
const mountainView = useSettingsStore((state) => state.interfaceLayout === "mountain-view");
const tSidebar = useTranslations("sidebar");
@@ -360,7 +372,7 @@ export function EmailContextMenu({
label={t("archive")}
shortcut={shortcutOf("archive")}
onClick={() => handleAction(onBatchArchive!)}
disabled={!onBatchArchive}
disabled={!onBatchArchive || !canChangeMail}
/>
</>
);
@@ -373,7 +385,7 @@ export function EmailContextMenu({
testId="ctx-delete"
shortcut={shortcutOf("delete")}
onClick={() => handleAction(onBatchDelete!)}
disabled={!onBatchDelete}
disabled={!onBatchDelete || !canChangeMail}
destructive={!mountainView}
/>
</>
@@ -386,6 +398,7 @@ export function EmailContextMenu({
label={isUnread ? t("mark_read") : t("mark_unread")}
testId={isUnread ? "ctx-mark-read" : "ctx-mark-unread"}
shortcut={shortcutOf("read", isUnread)}
disabled={!canChangeMail}
onClick={() => handleAction(() => onBatchMarkAsRead?.(isUnread))}
/>
</>
@@ -394,7 +407,7 @@ export function EmailContextMenu({
<>
{/* Move to submenu */}
{moveTree.length > 0 && (
<ContextMenuSubMenu icon={FolderInput} label={t("move_to")} testId="ctx-move-to">
<ContextMenuSubMenu icon={FolderInput} label={t("move_to")} testId="ctx-move-to" disabled={!canChangeMail}>
{renderMailboxNodes(moveTree, moveTargetIds, "move-to:", (mailboxId) => onBatchMoveToMailbox?.(mailboxId))}
</ContextMenuSubMenu>
)}
@@ -403,6 +416,7 @@ export function EmailContextMenu({
icon={FolderX}
label={t("remove_from_folder")}
testId="ctx-remove-from-folder"
disabled={!canChangeMail}
onClick={() => handleAction(onBatchRemoveFromFolder!)}
/>
)}
@@ -422,7 +436,7 @@ export function EmailContextMenu({
<>
{/* Copy to another connected account, keeping the originals */}
{onCopyToAccount && copyTrees.length > 0 && (
<ContextMenuSubMenu icon={Copy} label={t("copy_to_account")} testId="ctx-copy-to">
<ContextMenuSubMenu icon={Copy} label={t("copy_to_account")} testId="ctx-copy-to" disabled={offline}>
{copyTargetsContent}
</ContextMenuSubMenu>
)}
@@ -431,14 +445,14 @@ export function EmailContextMenu({
const rulesItem = (
<>
{/* Rules: filter rules made from the sender(s), saved as Sieve */}
<RulesContextSubMenu anchor={email} senderEmails={ruleSenderEmails} onClose={onClose} />
<RulesContextSubMenu anchor={email} senderEmails={ruleSenderEmails} onClose={onClose} disabled={offline} />
</>
);
const tagItem = (
<>
{/* Set tag submenu - the whole selection's tags (#1077) */}
{onBatchToggleTag && (
<ContextMenuSubMenu icon={Tag} label={t("tag")} testId="ctx-tag">
<ContextMenuSubMenu icon={Tag} label={t("tag")} testId="ctx-tag" disabled={!canChangeMail}>
<div className="w-56 max-w-[18rem]">
<TagPicker
selectedIds={batchTagIds ?? []}
@@ -464,7 +478,7 @@ export function EmailContextMenu({
testId={isInJunkFolder ? "ctx-not-spam" : "ctx-spam"}
shortcut={shortcutOf("spam")}
onClick={() => handleAction(isInJunkFolder ? onBatchUndoSpam! : onBatchMarkAsSpam!)}
disabled={isInJunkFolder ? !onBatchUndoSpam : !onBatchMarkAsSpam}
disabled={(isInJunkFolder ? !onBatchUndoSpam : !onBatchMarkAsSpam) || !canChangeMail}
destructive={!isInJunkFolder && !mountainView}
/>
{batchPhishing.available && (
@@ -483,26 +497,26 @@ export function EmailContextMenu({
switch (entry.key) {
case "move":
return (
<ContextMenuSubMenu key={entry.key} icon={entry.icon} label={entry.label} testId={entry.testId}>
<ContextMenuSubMenu key={entry.key} icon={entry.icon} label={entry.label} testId={entry.testId} disabled={!canChangeMail}>
{renderMailboxNodes(moveTree, moveTargetIds, "move-to:", (mailboxId) => onMoveToMailbox?.(mailboxId))}
</ContextMenuSubMenu>
);
case "copy_to_account":
return (
<ContextMenuSubMenu key={entry.key} icon={entry.icon} label={entry.label} testId={entry.testId}>
<ContextMenuSubMenu key={entry.key} icon={entry.icon} label={entry.label} testId={entry.testId} disabled={offline}>
{copyTargetsContent}
</ContextMenuSubMenu>
);
case "tag":
return (
<ContextMenuSubMenu key={entry.key} icon={entry.icon} label={entry.label} testId={entry.testId}>
<ContextMenuSubMenu key={entry.key} icon={entry.icon} label={entry.label} testId={entry.testId} disabled={!canChangeMail}>
<div className="w-56 max-w-[18rem]">
<TagPicker selectedIds={currentTagIds} onToggle={(tagId) => onSetTag?.(tagId)} scope={tagScope} />
</div>
</ContextMenuSubMenu>
);
case "rules":
return <RulesContextSubMenu key={entry.key} anchor={email} senderEmails={ruleSenderEmails} onClose={onClose} />;
return <RulesContextSubMenu key={entry.key} anchor={email} senderEmails={ruleSenderEmails} onClose={onClose} disabled={offline} />;
}
};
@@ -515,7 +529,7 @@ export function EmailContextMenu({
icon={entry.icon}
label={entry.label}
testId={entry.testId}
disabled={entry.disabled}
disabled={entry.disabled || (!canChangeMail && CHANGES_MAIL.has(entry.key))}
destructive={entry.destructive}
shortcut={shortcutOf(entry.key, isUnread)}
onClick={() => handleAction(entry.run)}
@@ -531,6 +545,7 @@ export function EmailContextMenu({
onClose={onClose}
label={label}
>
{offline && <ContextMenuHeader>{tMenu("offline_header")}</ContextMenuHeader>}
{showBatchActions ? (
<>
<ContextMenuHeader>
+2 -2
View File
@@ -12,7 +12,7 @@ import type { MessageActionKey } from "./message-actions";
*/
const KEYS: Partial<Record<MessageActionKey, { key: string; single: boolean }>> = {
reply: { key: "R", single: true },
reply_all: { key: "Shift+R", single: true },
reply_all: { key: "Shift + R", single: true },
forward: { key: "F", single: true },
archive: { key: "E", single: true },
delete: { key: "Del", single: false },
@@ -25,7 +25,7 @@ export function messageActionShortcut(
options: { unread?: boolean; singleKeys: boolean },
): string | undefined {
const entry = key === "read"
? { key: options.unread ? "Shift+I" : "U", single: true }
? { key: options.unread ? "Shift + I" : "U", single: true }
: KEYS[key];
if (!entry) return undefined;
if (entry.single && !options.singleKeys) return undefined;
+6 -3
View File
@@ -407,10 +407,13 @@ export function RulesContextSubMenu({
anchor,
senderEmails,
onClose,
disabled = false,
}: {
anchor: Email;
senderEmails: Email[];
onClose: () => void;
/** Rules are saved on the server: greyed out offline. */
disabled?: boolean;
}) {
const t = useTranslations("context_menu");
const accountEmails = useMemo(
@@ -424,7 +427,7 @@ export function RulesContextSubMenu({
icon={Filter}
label={t("rules.title")}
testId="ctx-rules"
disabled={availability === "cross_account"}
disabled={disabled || availability === "cross_account"}
hint={availability === "cross_account" ? t("rules.cross_account") : undefined}
>
{target && <ContextRulesContent anchor={anchor} senderEmails={senderEmails} target={target} onDone={onClose} />}
@@ -801,7 +804,7 @@ function FolderRulesContent({
* saved rules over the mail already in the folder (#1166). Hidden for shared
* folders and accounts without Sieve.
*/
export function FolderRulesSubMenu({ mailbox, onClose }: { mailbox: Mailbox; onClose: () => void }) {
export function FolderRulesSubMenu({ mailbox, onClose, disabled = false }: { mailbox: Mailbox; onClose: () => void; disabled?: boolean }) {
const t = useTranslations("context_menu");
const client = useAuthStore((s) => s.client);
const target = useMemo(
@@ -810,7 +813,7 @@ export function FolderRulesSubMenu({ mailbox, onClose }: { mailbox: Mailbox; onC
);
if (!target || !target.supportsSieve) return null;
return (
<ContextMenuSubMenu icon={Filter} label={t("rules.title")} testId="mailbox-rules">
<ContextMenuSubMenu icon={Filter} label={t("rules.title")} testId="mailbox-rules" disabled={disabled}>
<FolderRulesContent mailbox={mailbox} target={target} onDone={onClose} />
</ContextMenuSubMenu>
);
@@ -0,0 +1,81 @@
import { render, screen } from '@testing-library/react';
import { afterEach, describe, expect, it, vi } from 'vitest';
import type { Mailbox } from '@/lib/jmap/types';
// A folder menu greys out what can't work: nothing to mark read, or no connection
// for what needs the server.
vi.mock('@/components/email/rules-menu', () => ({ FolderRulesSubMenu: () => null }));
vi.mock('@/lib/offline/offline-mode', () => ({ offlineAllowed: () => true }));
const { MailboxContextMenu, unreadInTree } = await import('../mailbox-context-menu');
const { useOfflineStore } = await import('@/lib/offline/state');
const { useAuthStore } = await import('@/stores/auth-store');
const box = (id: string, unread: number, parentId: string | null = null) =>
({ id, name: id, parentId, sortOrder: 1, totalEmails: 5, unreadEmails: unread, myRights: {} }) as unknown as Mailbox;
function renderMenu(mailbox: Mailbox, mailboxes: Mailbox[], hasChildren = false) {
render(
<MailboxContextMenu
target={{ kind: 'mailbox', mailbox, hasChildren }}
position={{ x: 0, y: 0 }}
isOpen
onClose={vi.fn()}
menuRef={{ current: null }}
mailboxes={mailboxes}
onMarkFolderRead={vi.fn()}
onMarkFolderTreeRead={vi.fn()}
onCreateSubfolder={vi.fn()}
onRenameFolder={vi.fn()}
onDeleteFolder={vi.fn()}
onEmptyFolder={vi.fn()}
onImportEmail={vi.fn()}
onRefresh={vi.fn()}
/>,
);
}
const item = (name: string) => screen.getByRole('menuitem', { name }) as HTMLButtonElement;
describe('folder menu states', () => {
afterEach(() => {
useOfflineStore.setState({ browserOffline: false, downServers: [], enabled: [] });
});
it('greys out Mark folder as read when nothing in it is unread', () => {
const read = box('read', 0);
renderMenu(read, [read]);
expect(item('mark_folder_read').disabled).toBe(true);
});
it('keeps it for a folder with unread mail, and the tree entry for unread mail below', () => {
const parent = box('parent', 0);
const child = box('child', 3, 'parent');
renderMenu(parent, [parent, child], true);
expect(item('mark_folder_read').disabled).toBe(true);
expect(item('mark_folder_tree_read').disabled).toBe(false);
expect(unreadInTree(parent, [parent, child])).toBe(3);
});
it('offline, greys out what needs the server and says why', () => {
useOfflineStore.setState({ browserOffline: true });
const folder = box('f', 2);
renderMenu(folder, [folder]);
expect(screen.getByText('offline_header')).toBeInTheDocument();
for (const name of ['new_subfolder', 'rename', 'menu_import', 'refresh', 'empty_folder_generic', 'delete_folder']) {
expect(item(name).disabled).toBe(true);
}
// No mail kept on this device: marking read would fail too.
expect(item('mark_folder_read').disabled).toBe(true);
});
it('offline with the mail kept on this device, marking read still works', () => {
useAuthStore.setState({ activeAccountId: 'acc' } as never);
useOfflineStore.setState({ browserOffline: true, enabled: ['acc'] });
const folder = box('f', 2);
renderMenu(folder, [folder]);
expect(item('mark_folder_read').disabled).toBe(false);
expect(item('rename').disabled).toBe(true);
});
});
+38 -14
View File
@@ -31,6 +31,7 @@ import {
import { FolderRulesSubMenu } from "@/components/email/rules-menu";
import { openMailExport } from "@/stores/mail-transfer-store";
import { useDevicePolicy } from "@/hooks/use-device-policy";
import { useConnectionState } from "@/hooks/use-connection-state";
interface Position {
x: number;
@@ -45,6 +46,20 @@ export type MailboxContextTarget =
| { kind: "folders-section"; accountId?: string };
const PATH_SEPARATOR = " › ";
/** Unread messages in a folder and the folders under it; undefined where a count isn't known. */
export function unreadInTree(mailbox: Mailbox, mailboxes: Mailbox[]): number | undefined {
let total = 0;
const seen = new Set<string>();
const visit = (box: Mailbox): boolean => {
if (seen.has(box.id)) return true;
seen.add(box.id);
if (typeof box.unreadEmails !== "number") return false;
total += box.unreadEmails;
return mailboxes.filter((child) => child.parentId === box.id).every(visit);
};
return visit(mailbox) ? total : undefined;
}
const MAX_PATH_LENGTH = 40;
const MAX_SEGMENT_LENGTH = 16;
@@ -147,6 +162,11 @@ export function MailboxContextMenu({
const tExport = useTranslations("mail_export");
// Organisation > Devices > No export hides it; import stays.
const exportOff = useDevicePolicy().noExport;
// Folders, rules and sharing need the server. Marking read waits in the outbox
// where the account keeps its mail on this device.
const { offline, canChangeMail } = useConnectionState();
const tMenu = useTranslations("menuAudit");
const offlineHeader = offline ? <ContextMenuHeader>{tMenu("offline_header")}</ContextMenuHeader> : null;
const handleAction = (action: () => void) => {
action();
@@ -198,13 +218,14 @@ export function MailboxContextMenu({
if (target.kind === "folders-section") {
return (
<ContextMenu ref={menuRef} isOpen={isOpen} position={position} onClose={onClose}>
{offlineHeader}
{!target.accountId && (
<>
<ContextMenuItem
icon={CheckCheck}
label={t("mark_all_folders_read")}
onClick={() => handleAction(onMarkAllFoldersRead!)}
disabled={!onMarkAllFoldersRead}
disabled={!onMarkAllFoldersRead || !canChangeMail}
/>
<ContextMenuSeparator />
</>
@@ -213,14 +234,14 @@ export function MailboxContextMenu({
icon={FolderPlus}
label={t("new_folder")}
onClick={() => handleAction(() => onCreateFolder?.(target.accountId))}
disabled={!onCreateFolder}
disabled={!onCreateFolder || offline}
testId="mailbox-new-folder"
/>
<ContextMenuItem
icon={RefreshCw}
label={t("refresh")}
onClick={() => handleAction(onRefresh!)}
disabled={!onRefresh}
disabled={!onRefresh || offline}
/>
{accountItems(target.accountId)}
</ContextMenu>
@@ -238,6 +259,8 @@ export function MailboxContextMenu({
const canSetSeen = mailbox.myRights?.maySetSeen !== false;
const canRemoveItems = mailbox.myRights?.mayRemoveItems !== false;
const canAddItems = mailbox.myRights?.mayAddItems !== false;
const nothingUnread = mailbox.unreadEmails === 0;
const nothingUnreadBelow = unreadInTree(mailbox, mailboxes) === 0;
// Sharing needs the mail:share extension (handler present) and, on a folder
// someone else shared with us, their permission to re-share it.
const canShare = !!onShareFolder && (!mailbox.isShared || mailbox.myRights?.mayShare === true);
@@ -252,19 +275,20 @@ export function MailboxContextMenu({
<ContextMenuHeader>
<span title={fullPath}>{renderShortenedPath(fullPath)}</span>
</ContextMenuHeader>
{offlineHeader}
<ContextMenuItem
icon={MailOpen}
label={t("mark_folder_read")}
onClick={() => handleAction(() => onMarkFolderRead?.(mailbox.id))}
disabled={!onMarkFolderRead || !canSetSeen}
disabled={!onMarkFolderRead || !canSetSeen || !canChangeMail || nothingUnread}
/>
{target.hasChildren && (
<ContextMenuItem
icon={Mails}
label={t("mark_folder_tree_read")}
onClick={() => handleAction(() => onMarkFolderTreeRead?.(mailbox.id))}
disabled={!onMarkFolderTreeRead || !canSetSeen}
disabled={!onMarkFolderTreeRead || !canSetSeen || !canChangeMail || nothingUnreadBelow}
/>
)}
@@ -274,14 +298,14 @@ export function MailboxContextMenu({
icon={FolderPlus}
label={t("new_subfolder")}
onClick={() => handleAction(() => onCreateSubfolder?.(mailbox.id))}
disabled={!onCreateSubfolder || !canCreateChild}
disabled={!onCreateSubfolder || !canCreateChild || offline}
testId="mailbox-new-subfolder"
/>
<ContextMenuItem
icon={Pencil}
label={t("rename")}
onClick={() => handleAction(() => onRenameFolder?.(mailbox.id))}
disabled={!onRenameFolder || !canRename}
disabled={!onRenameFolder || !canRename || offline}
testId="mailbox-rename"
/>
{onShareFolder && (
@@ -289,11 +313,11 @@ export function MailboxContextMenu({
icon={Users}
label={t("share")}
onClick={() => handleAction(() => onShareFolder(mailbox.id))}
disabled={!canShare}
disabled={!canShare || offline}
testId="mailbox-share"
/>
)}
<FolderRulesSubMenu mailbox={mailbox} onClose={onClose} />
<FolderRulesSubMenu mailbox={mailbox} onClose={onClose} disabled={offline} />
{notifications && (() => {
const muted = notifications.isMailboxMuted(mailbox);
return (
@@ -313,7 +337,7 @@ export function MailboxContextMenu({
icon={FolderPlus}
label={t("new_folder")}
onClick={() => handleAction(() => onCreateFolder?.(target.sharedAccountId))}
disabled={!onCreateFolder}
disabled={!onCreateFolder || offline}
testId="mailbox-new-folder"
/>
{accountItems(target.sharedAccountId)}
@@ -326,7 +350,7 @@ export function MailboxContextMenu({
icon={Upload}
label={tImport("menu_import")}
onClick={() => handleAction(() => onImportEmail?.(mailbox.id))}
disabled={!onImportEmail || !canAddItems}
disabled={!onImportEmail || !canAddItems || offline}
/>
{!exportOff && (
<ContextMenuItem
@@ -341,7 +365,7 @@ export function MailboxContextMenu({
icon={RefreshCw}
label={t("refresh")}
onClick={() => handleAction(onRefresh!)}
disabled={!onRefresh}
disabled={!onRefresh || offline}
/>
{/* Destructive entries last (R6). */}
@@ -351,14 +375,14 @@ export function MailboxContextMenu({
icon={FolderX}
label={isTrashOrJunk ? t("empty_folder") : t("empty_folder_generic")}
onClick={() => handleAction(() => onEmptyFolder?.(mailbox.id))}
disabled={!onEmptyFolder || mailbox.totalEmails === 0 || !canRemoveItems}
disabled={!onEmptyFolder || mailbox.totalEmails === 0 || !canRemoveItems || offline}
destructive
/>
<ContextMenuItem
icon={Trash2}
label={t("delete_folder")}
onClick={() => handleAction(() => onDeleteFolder?.(mailbox.id))}
disabled={!onDeleteFolder || !canDelete}
disabled={!onDeleteFolder || !canDelete || offline}
destructive
testId="mailbox-delete"
/>
+5 -3
View File
@@ -2335,6 +2335,8 @@ export function MailApp({ linkSegments: routeSegments }: MailAppProps = {}) {
empty: () => { if (selectedMailbox) void handleEmptyFolderFromContextMenu(selectedMailbox); },
};
const folderMenuMarkRead = useCallback(() => folderMenuLatest.current.markRead(), []);
// "Mark folder as read" is greyed out with nothing unread in the open folder.
const selectedFolderHasUnread = mailboxes.find((m) => m.id === selectedMailbox)?.unreadEmails !== 0;
const folderMenuMarkAllRead = useCallback(() => folderMenuLatest.current.markAllRead(), []);
const folderMenuEmpty = useCallback(() => folderMenuLatest.current.empty(), []);
useEffect(() => {
@@ -5541,7 +5543,7 @@ export function MailApp({ linkSegments: routeSegments }: MailAppProps = {}) {
onClick={() => { close(); void handleManualRefresh(); }}
/>
<MailFolderMenuItems
onMarkFolderRead={selectedMailbox ? () => handleMarkFolderRead(selectedMailbox) : undefined}
onMarkFolderRead={selectedMailbox && selectedFolderHasUnread ? () => handleMarkFolderRead(selectedMailbox) : undefined}
onMarkAllFoldersRead={handleMarkAllFoldersRead}
onEmptyFolder={folderIsEmptyable && selectedMailbox ? () => void handleEmptyFolderFromContextMenu(selectedMailbox) : undefined}
onPicked={close}
@@ -5581,7 +5583,7 @@ export function MailApp({ linkSegments: routeSegments }: MailAppProps = {}) {
totalCount={searchQuery || !isFilterEmpty(searchFilters) ? undefined : mailboxes.find((mb) => mb.id === selectedMailbox)?.totalEmails}
onRefresh={handleManualRefresh}
isRefreshing={isManualRefreshing}
onMarkFolderRead={selectedMailbox ? () => handleMarkFolderRead(selectedMailbox) : undefined}
onMarkFolderRead={selectedMailbox && selectedFolderHasUnread ? () => handleMarkFolderRead(selectedMailbox) : undefined}
onMarkAllFoldersRead={handleMarkAllFoldersRead}
onEmptyFolder={selectedMailbox ? () => void handleEmptyFolderFromContextMenu(selectedMailbox) : undefined}
onCreateFolder={() => handleCreateFolderFromContextMenu()}
@@ -5651,7 +5653,7 @@ export function MailApp({ linkSegments: routeSegments }: MailAppProps = {}) {
folderRole={conversationRole}
onRefresh={handleManualRefresh}
refreshDisabled={!client || isManualRefreshing}
onMarkFolderRead={selectedMailbox ? folderMenuMarkRead : undefined}
onMarkFolderRead={selectedMailbox && selectedFolderHasUnread ? folderMenuMarkRead : undefined}
onMarkAllFoldersRead={folderMenuMarkAllRead}
onEmptyFolder={folderIsEmptyable && selectedMailbox ? folderMenuEmpty : undefined}
/>
+41
View File
@@ -0,0 +1,41 @@
"use client";
import { useSyncExternalStore } from "react";
import { serverKey, useOfflineStore } from "@/lib/offline/state";
import { offlineAllowed } from "@/lib/offline/offline-mode";
import { useAuthStore } from "@/stores/auth-store";
function subscribeOnline(callback: () => void): () => void {
window.addEventListener("online", callback);
window.addEventListener("offline", callback);
return () => {
window.removeEventListener("online", callback);
window.removeEventListener("offline", callback);
};
}
export interface ConnectionState {
/** No network, or the open account's mail server can't be reached. */
offline: boolean;
/**
* Changes to messages still work: online, or the account keeps its mail on this
* device and the change waits for the connection. Folders, rules and sharing
* always need the server.
*/
canChangeMail: boolean;
}
/** What the menus go by to grey out what can't work without a connection. */
export function useConnectionState(): ConnectionState {
const account = useAuthStore((s) => s.activeAccountId);
const server = useAuthStore((s) => s.serverUrl);
const navigatorOffline = useSyncExternalStore(
subscribeOnline,
() => navigator.onLine === false,
() => false,
);
const storeOffline = useOfflineStore((s) => s.browserOffline || (!!server && s.downServers.includes(serverKey(server))));
const keepsMail = useOfflineStore((s) => !!account && s.enabled.includes(account));
const offline = navigatorOffline || storeOffline;
return { offline, canChangeMail: !offline || (keepsMail && offlineAllowed(account)) };
}
+13
View File
@@ -6455,6 +6455,19 @@
"group_account": "الحساب",
"switch_to": "التبديل إلى {account}"
},
"menuAudit": {
"offline_header": "أنت غير متصل. بعض الإجراءات تحتاج إلى اتصال.",
"files_new_folder": "مجلد جديد…",
"files_new_file": "ملف جديد…",
"files_new_text_file": "ملف نصي جديد…",
"files_upload_files": "رفع الملفات…",
"files_upload_folder": "رفع مجلد…",
"files_share": "مشاركة…",
"files_share_button": "مشاركة",
"contacts_add_to_group": "إضافة إلى مجموعة…",
"contacts_bulk_add_to_group": "إضافة إلى مجموعة…",
"schedule_send": "جدولة الإرسال…"
},
"notification_center": {
"title": "الإشعارات",
"mark_all_read": "تعليم الكل كمقروء",
+13
View File
@@ -6455,6 +6455,19 @@
"group_account": "Compte",
"switch_to": "Canvia a {account}"
},
"menuAudit": {
"offline_header": "No teniu connexió. Algunes accions la necessiten.",
"files_new_folder": "Carpeta nova…",
"files_new_file": "Fitxer nou…",
"files_new_text_file": "Fitxer de text nou…",
"files_upload_files": "Puja fitxers…",
"files_upload_folder": "Puja una carpeta…",
"files_share": "Comparteix…",
"files_share_button": "Comparteix",
"contacts_add_to_group": "Afegeix al grup…",
"contacts_bulk_add_to_group": "Afegeix al grup…",
"schedule_send": "Programa l'enviament…"
},
"notification_center": {
"title": "Notificacions",
"mark_all_read": "Marca-ho tot com a llegit",
+13
View File
@@ -6455,6 +6455,19 @@
"group_account": "Účet",
"switch_to": "Přepnout na {account}"
},
"menuAudit": {
"offline_header": "Jste offline. Některé akce potřebují připojení.",
"files_new_folder": "Nová složka…",
"files_new_file": "Nový soubor…",
"files_new_text_file": "Nový textový soubor…",
"files_upload_files": "Nahrát soubory…",
"files_upload_folder": "Nahrát složku…",
"files_share": "Sdílet…",
"files_share_button": "Sdílet",
"contacts_add_to_group": "Přidat do skupiny…",
"contacts_bulk_add_to_group": "Přidat do skupiny…",
"schedule_send": "Naplánovat odeslání…"
},
"notification_center": {
"title": "Oznámení",
"mark_all_read": "Označit vše jako přečtené",
+13
View File
@@ -6455,6 +6455,19 @@
"group_account": "Konto",
"switch_to": "Skift til {account}"
},
"menuAudit": {
"offline_header": "Du er offline. Nogle handlinger kræver forbindelse.",
"files_new_folder": "Ny mappe…",
"files_new_file": "Ny fil…",
"files_new_text_file": "Ny tekstfil…",
"files_upload_files": "Upload filer…",
"files_upload_folder": "Upload mappe…",
"files_share": "Del…",
"files_share_button": "Del",
"contacts_add_to_group": "Tilføj til gruppe…",
"contacts_bulk_add_to_group": "Tilføj til gruppe…",
"schedule_send": "Planlæg afsendelse…"
},
"notification_center": {
"title": "Notifikationer",
"mark_all_read": "Markér alle som læst",
+13
View File
@@ -6455,6 +6455,19 @@
"group_account": "Konto",
"switch_to": "Zu {account} wechseln"
},
"menuAudit": {
"offline_header": "Sie sind offline. Manche Aktionen brauchen eine Verbindung.",
"files_new_folder": "Neuer Ordner…",
"files_new_file": "Neue Datei…",
"files_new_text_file": "Neue Textdatei…",
"files_upload_files": "Dateien hochladen…",
"files_upload_folder": "Ordner hochladen…",
"files_share": "Teilen…",
"files_share_button": "Teilen",
"contacts_add_to_group": "Zur Gruppe hinzufügen…",
"contacts_bulk_add_to_group": "Zur Gruppe hinzufügen…",
"schedule_send": "Senden planen…"
},
"notification_center": {
"title": "Benachrichtigungen",
"mark_all_read": "Alle als gelesen markieren",
+13
View File
@@ -6455,6 +6455,19 @@
"group_account": "Account",
"switch_to": "Switch to {account}"
},
"menuAudit": {
"offline_header": "You're offline. Some actions need a connection.",
"files_new_folder": "New folder…",
"files_new_file": "New file…",
"files_new_text_file": "New text file…",
"files_upload_files": "Upload files…",
"files_upload_folder": "Upload folder…",
"files_share": "Share…",
"files_share_button": "Share",
"contacts_add_to_group": "Add to group…",
"contacts_bulk_add_to_group": "Add to group…",
"schedule_send": "Schedule send…"
},
"notification_center": {
"title": "Notifications",
"mark_all_read": "Mark all read",
+13
View File
@@ -6455,6 +6455,19 @@
"group_account": "Cuenta",
"switch_to": "Cambiar a {account}"
},
"menuAudit": {
"offline_header": "No tiene conexión. Algunas acciones la necesitan.",
"files_new_folder": "Nueva carpeta…",
"files_new_file": "Nuevo archivo…",
"files_new_text_file": "Nuevo archivo de texto…",
"files_upload_files": "Subir archivos…",
"files_upload_folder": "Subir carpeta…",
"files_share": "Compartir…",
"files_share_button": "Compartir",
"contacts_add_to_group": "Añadir al grupo…",
"contacts_bulk_add_to_group": "Agregar al grupo…",
"schedule_send": "Programar envío…"
},
"notification_center": {
"title": "Notificaciones",
"mark_all_read": "Marcar todo como leído",
+13
View File
@@ -6455,6 +6455,19 @@
"group_account": "Konto",
"switch_to": "Lülitu kontole {account}"
},
"menuAudit": {
"offline_header": "Olete võrguühenduseta. Mõni toiming vajab ühendust.",
"files_new_folder": "Uus kaust…",
"files_new_file": "Uus fail…",
"files_new_text_file": "Uus tekstifail…",
"files_upload_files": "Laadi failid üles…",
"files_upload_folder": "Laadi kaust üles…",
"files_share": "Jaga…",
"files_share_button": "Jaga",
"contacts_add_to_group": "Lisa gruppi…",
"contacts_bulk_add_to_group": "Lisa gruppi…",
"schedule_send": "Ajasta saatmine…"
},
"notification_center": {
"title": "Märguanded",
"mark_all_read": "Märgi kõik loetuks",
+13
View File
@@ -6455,6 +6455,19 @@
"group_account": "حساب",
"switch_to": "رفتن به {account}"
},
"menuAudit": {
"offline_header": "آفلاین هستید. برخی کارها به اتصال نیاز دارند.",
"files_new_folder": "پوشه جدید…",
"files_new_file": "فایل جدید…",
"files_new_text_file": "فایل متنی جدید…",
"files_upload_files": "بارگذاری فایل‌ها…",
"files_upload_folder": "بارگذاری پوشه…",
"files_share": "اشتراک‌گذاری…",
"files_share_button": "اشتراک‌گذاری",
"contacts_add_to_group": "افزودن به گروه…",
"contacts_bulk_add_to_group": "افزودن به گروه…",
"schedule_send": "زمان‌بندی ارسال…"
},
"notification_center": {
"title": "اعلان‌ها",
"mark_all_read": "علامت‌گذاری همه به‌عنوان خوانده‌شده",
+13
View File
@@ -6455,6 +6455,19 @@
"group_account": "Compte",
"switch_to": "Passer à {account}"
},
"menuAudit": {
"offline_header": "Vous êtes hors ligne. Certaines actions ont besoin d’une connexion.",
"files_new_folder": "Nouveau dossier…",
"files_new_file": "Nouveau fichier…",
"files_new_text_file": "Nouveau fichier texte…",
"files_upload_files": "Téléverser des fichiers…",
"files_upload_folder": "Téléverser un dossier…",
"files_share": "Partager…",
"files_share_button": "Partager",
"contacts_add_to_group": "Ajouter au groupe…",
"contacts_bulk_add_to_group": "Ajouter au groupe…",
"schedule_send": "Planifier l’envoi…"
},
"notification_center": {
"title": "Notifications",
"mark_all_read": "Tout marquer comme lu",
+13
View File
@@ -6456,6 +6456,19 @@
"group_account": "חשבון",
"switch_to": "מעבר אל {account}"
},
"menuAudit": {
"offline_header": "אין חיבור. חלק מהפעולות צריכות חיבור.",
"files_new_folder": "תיקיה חדשה…",
"files_new_file": "קובץ חדש…",
"files_new_text_file": "קובץ טקסט חדש…",
"files_upload_files": "העלה קבצים…",
"files_upload_folder": "העלה תיקייה…",
"files_share": "שתף…",
"files_share_button": "שתף",
"contacts_add_to_group": "הוסף לקבוצה…",
"contacts_bulk_add_to_group": "הוסף לקבוצה…",
"schedule_send": "תזמן שליחה…"
},
"notification_center": {
"title": "התראות",
"mark_all_read": "סימון הכול כנקרא",
+13
View File
@@ -6455,6 +6455,19 @@
"group_account": "Fiók",
"switch_to": "Váltás erre: {account}"
},
"menuAudit": {
"offline_header": "Nincs kapcsolat. Néhány művelethez kapcsolat kell.",
"files_new_folder": "Új mappa…",
"files_new_file": "Új fájl…",
"files_new_text_file": "Új szöveges fájl…",
"files_upload_files": "Fájlok feltöltése…",
"files_upload_folder": "Mappa feltöltése…",
"files_share": "Megosztás…",
"files_share_button": "Megosztás",
"contacts_add_to_group": "Hozzáadás a csoporthoz…",
"contacts_bulk_add_to_group": "Hozzáadás a csoporthoz…",
"schedule_send": "Késleltetett küldés…"
},
"notification_center": {
"title": "Értesítések",
"mark_all_read": "Összes megjelölése olvasottként",
+13
View File
@@ -6455,6 +6455,19 @@
"group_account": "Account",
"switch_to": "Passa a {account}"
},
"menuAudit": {
"offline_header": "Sei offline. Alcune azioni richiedono una connessione.",
"files_new_folder": "Nuova cartella…",
"files_new_file": "Nuovo file…",
"files_new_text_file": "Nuovo file di testo…",
"files_upload_files": "Carica file…",
"files_upload_folder": "Carica cartella…",
"files_share": "Condividi…",
"files_share_button": "Condividi",
"contacts_add_to_group": "Aggiungi al gruppo…",
"contacts_bulk_add_to_group": "Aggiungi al gruppo…",
"schedule_send": "Programma invio…"
},
"notification_center": {
"title": "Notifiche",
"mark_all_read": "Segna tutte come lette",
+13
View File
@@ -6455,6 +6455,19 @@
"group_account": "アカウント",
"switch_to": "{account} に切り替え"
},
"menuAudit": {
"offline_header": "オフラインです。一部の操作には接続が必要です。",
"files_new_folder": "新しいフォルダー…",
"files_new_file": "新規ファイル…",
"files_new_text_file": "新規テキストファイル…",
"files_upload_files": "ファイルをアップロード…",
"files_upload_folder": "フォルダをアップロード…",
"files_share": "共有…",
"files_share_button": "共有",
"contacts_add_to_group": "グループに追加…",
"contacts_bulk_add_to_group": "グループに追加…",
"schedule_send": "送信を予約…"
},
"notification_center": {
"title": "通知",
"mark_all_read": "すべて既読にする",
+13
View File
@@ -6455,6 +6455,19 @@
"group_account": "계정",
"switch_to": "{account}(으)로 전환"
},
"menuAudit": {
"offline_header": "오프라인 상태입니다. 일부 작업에는 연결이 필요합니다.",
"files_new_folder": "새 폴더…",
"files_new_file": "새 파일…",
"files_new_text_file": "새 텍스트 파일…",
"files_upload_files": "파일 업로드…",
"files_upload_folder": "폴더 업로드…",
"files_share": "공유…",
"files_share_button": "공유",
"contacts_add_to_group": "그룹에 추가…",
"contacts_bulk_add_to_group": "그룹에 추가…",
"schedule_send": "보내기 예약…"
},
"notification_center": {
"title": "알림",
"mark_all_read": "모두 읽음으로 표시",
+13
View File
@@ -6455,6 +6455,19 @@
"group_account": "Konts",
"switch_to": "Pārslēgties uz {account}"
},
"menuAudit": {
"offline_header": "Jūs esat bezsaistē. Dažām darbībām vajag savienojumu.",
"files_new_folder": "Jauna mape…",
"files_new_file": "Jauns fails…",
"files_new_text_file": "Jauns teksta fails…",
"files_upload_files": "Augšupielādēt failus…",
"files_upload_folder": "Augšupielādēt mapi…",
"files_share": "Kopīgot…",
"files_share_button": "Kopīgot",
"contacts_add_to_group": "Pievienot grupai…",
"contacts_bulk_add_to_group": "Pievienot grupai…",
"schedule_send": "Ieplānot sūtīšanu…"
},
"notification_center": {
"title": "Paziņojumi",
"mark_all_read": "Atzīmēt visus kā lasītus",
+13
View File
@@ -6455,6 +6455,19 @@
"group_account": "Бүртгэл",
"switch_to": "{account} руу шилжих"
},
"menuAudit": {
"offline_header": "Та офлайн байна. Зарим үйлдэлд холболт хэрэгтэй.",
"files_new_folder": "Шинэ хавтас…",
"files_new_file": "Шинэ файл…",
"files_new_text_file": "Шинэ текст файл…",
"files_upload_files": "Файл байршуулах…",
"files_upload_folder": "Хавтас байршуулах…",
"files_share": "Хуваалцах…",
"files_share_button": "Хуваалцах",
"contacts_add_to_group": "Бүлэгт нэмэх…",
"contacts_bulk_add_to_group": "Бүлэгт нэмэх…",
"schedule_send": "Хуваарь илгээх…"
},
"notification_center": {
"title": "Мэдэгдэл",
"mark_all_read": "Бүгдийг уншсан болгох",
+13
View File
@@ -6455,6 +6455,19 @@
"group_account": "Konto",
"switch_to": "Bytt til {account}"
},
"menuAudit": {
"offline_header": "Du er frakoblet. Noen handlinger trenger tilkobling.",
"files_new_folder": "Ny mappe…",
"files_new_file": "Ny fil…",
"files_new_text_file": "Ny tekstfil…",
"files_upload_files": "Last opp filer…",
"files_upload_folder": "Last opp mappe…",
"files_share": "Del…",
"files_share_button": "Del",
"contacts_add_to_group": "Legg til i gruppe…",
"contacts_bulk_add_to_group": "Legg til i gruppe…",
"schedule_send": "Planlegg sending…"
},
"notification_center": {
"title": "Varsler",
"mark_all_read": "Merk alle som lest",
+13
View File
@@ -6455,6 +6455,19 @@
"group_account": "Account",
"switch_to": "Overschakelen naar {account}"
},
"menuAudit": {
"offline_header": "U bent offline. Sommige acties hebben een verbinding nodig.",
"files_new_folder": "Nieuwe map…",
"files_new_file": "Nieuw bestand…",
"files_new_text_file": "Nieuw tekstbestand…",
"files_upload_files": "Bestanden uploaden…",
"files_upload_folder": "Map uploaden…",
"files_share": "Delen…",
"files_share_button": "Delen",
"contacts_add_to_group": "Aan groep toevoegen…",
"contacts_bulk_add_to_group": "Aan groep toevoegen…",
"schedule_send": "Verzenden plannen…"
},
"notification_center": {
"title": "Meldingen",
"mark_all_read": "Alles als gelezen markeren",
+13
View File
@@ -6455,6 +6455,19 @@
"group_account": "Konto",
"switch_to": "Przełącz na {account}"
},
"menuAudit": {
"offline_header": "Jesteś offline. Niektóre działania wymagają połączenia.",
"files_new_folder": "Nowy folder…",
"files_new_file": "Nowy plik…",
"files_new_text_file": "Nowy plik tekstowy…",
"files_upload_files": "Prześlij pliki…",
"files_upload_folder": "Prześlij folder…",
"files_share": "Udostępnij…",
"files_share_button": "Udostępnij",
"contacts_add_to_group": "Dodaj do grupy…",
"contacts_bulk_add_to_group": "Dodaj do grupy…",
"schedule_send": "Zaplanuj wysyłkę…"
},
"notification_center": {
"title": "Powiadomienia",
"mark_all_read": "Oznacz wszystkie jako przeczytane",
+13
View File
@@ -6455,6 +6455,19 @@
"group_account": "Conta",
"switch_to": "Mudar para {account}"
},
"menuAudit": {
"offline_header": "Está offline. Algumas ações precisam de ligação.",
"files_new_folder": "Nova pasta…",
"files_new_file": "Novo arquivo…",
"files_new_text_file": "Novo arquivo de texto…",
"files_upload_files": "Carregar arquivos…",
"files_upload_folder": "Carregar pasta…",
"files_share": "Compartilhar…",
"files_share_button": "Compartilhar",
"contacts_add_to_group": "Adicionar ao grupo…",
"contacts_bulk_add_to_group": "Adicionar ao grupo…",
"schedule_send": "Agendar envio…"
},
"notification_center": {
"title": "Notificações",
"mark_all_read": "Marcar todas como lidas",
+13
View File
@@ -6455,6 +6455,19 @@
"group_account": "Cont",
"switch_to": "Comută la {account}"
},
"menuAudit": {
"offline_header": "Sunteți offline. Unele acțiuni au nevoie de conexiune.",
"files_new_folder": "Dosar nou…",
"files_new_file": "Fișier nou…",
"files_new_text_file": "Fișier text nou…",
"files_upload_files": "Încărcați fișiere…",
"files_upload_folder": "Încărcare folder…",
"files_share": "Distribuie…",
"files_share_button": "Distribuie",
"contacts_add_to_group": "Adaugă la grup…",
"contacts_bulk_add_to_group": "Adaugă la grup…",
"schedule_send": "Programează trimiterea…"
},
"notification_center": {
"title": "Notificări",
"mark_all_read": "Marchează toate ca citite",
+13
View File
@@ -6455,6 +6455,19 @@
"group_account": "Аккаунт",
"switch_to": "Переключиться на {account}"
},
"menuAudit": {
"offline_header": "Нет подключения. Для некоторых действий нужна сеть.",
"files_new_folder": "Новая папка…",
"files_new_file": "Новый файл…",
"files_new_text_file": "Новый текстовый файл…",
"files_upload_files": "Загрузить файлы…",
"files_upload_folder": "Загрузить папку…",
"files_share": "Поделиться…",
"files_share_button": "Поделиться",
"contacts_add_to_group": "Добавить в группу…",
"contacts_bulk_add_to_group": "Добавить в группу…",
"schedule_send": "Запланировать отправку…"
},
"notification_center": {
"title": "Уведомления",
"mark_all_read": "Отметить все как прочитанные",
+13
View File
@@ -6455,6 +6455,19 @@
"group_account": "Účet",
"switch_to": "Prepnúť na {account}"
},
"menuAudit": {
"offline_header": "Ste offline. Niektoré akcie potrebujú pripojenie.",
"files_new_folder": "Nový priečinok…",
"files_new_file": "Nový súbor…",
"files_new_text_file": "Nový textový súbor…",
"files_upload_files": "Nahrať súbory…",
"files_upload_folder": "Nahrať priečinok…",
"files_share": "Zdieľať…",
"files_share_button": "Zdieľať",
"contacts_add_to_group": "Pridať do skupiny…",
"contacts_bulk_add_to_group": "Pridať do skupiny…",
"schedule_send": "Naplánovať odoslanie…"
},
"notification_center": {
"title": "Oznámenia",
"mark_all_read": "Označiť všetko ako prečítané",
+13
View File
@@ -98,6 +98,19 @@
"group_account": "บัญชี",
"switch_to": "สลับไปที่ {account}"
},
"menuAudit": {
"offline_header": "คุณออฟไลน์อยู่ บางการทำงานต้องใช้การเชื่อมต่อ",
"files_new_folder": "โฟลเดอร์ใหม่…",
"files_new_file": "ไฟล์ใหม่…",
"files_new_text_file": "ไฟล์ข้อความใหม่…",
"files_upload_files": "อัปโหลดไฟล์…",
"files_upload_folder": "อัปโหลดโฟลเดอร์…",
"files_share": "แชร์…",
"files_share_button": "แชร์",
"contacts_add_to_group": "เพิ่มในกลุ่ม…",
"contacts_bulk_add_to_group": "เพิ่มในกลุ่ม…",
"schedule_send": "กำหนดเวลาส่ง…"
},
"notification_center": {
"title": "การแจ้งเตือน",
"mark_all_read": "ทำเครื่องหมายว่าอ่านแล้วทั้งหมด",
+13
View File
@@ -6455,6 +6455,19 @@
"group_account": "Hesap",
"switch_to": "{account} hesabına geç"
},
"menuAudit": {
"offline_header": "Çevrimdışısınız. Bazı işlemler bağlantı gerektirir.",
"files_new_folder": "Yeni Klasör…",
"files_new_file": "Yeni Dosya…",
"files_new_text_file": "Yeni Metin Dosyası…",
"files_upload_files": "Dosya Yükle…",
"files_upload_folder": "Klasör Yükle…",
"files_share": "Paylaş…",
"files_share_button": "Paylaş",
"contacts_add_to_group": "Gruba ekle…",
"contacts_bulk_add_to_group": "Gruba ekle…",
"schedule_send": "Göndermeyi zamanla…"
},
"notification_center": {
"title": "Bildirimler",
"mark_all_read": "Tümünü okundu işaretle",
+13
View File
@@ -6455,6 +6455,19 @@
"group_account": "Обліковий запис",
"switch_to": "Перейти до {account}"
},
"menuAudit": {
"offline_header": "Немає з’єднання. Для деяких дій потрібна мережа.",
"files_new_folder": "Нова папка…",
"files_new_file": "Новий файл…",
"files_new_text_file": "Новий текстовий файл…",
"files_upload_files": "Завантажити файли…",
"files_upload_folder": "Завантажити папку…",
"files_share": "Поділитися…",
"files_share_button": "Поділитися",
"contacts_add_to_group": "Додати до групи…",
"contacts_bulk_add_to_group": "Додати до групи…",
"schedule_send": "Запланувати надсилання…"
},
"notification_center": {
"title": "Сповіщення",
"mark_all_read": "Позначити всі як прочитані",
+13
View File
@@ -6467,6 +6467,19 @@
"group_account": "Tài khoản",
"switch_to": "Chuyển sang {account}"
},
"menuAudit": {
"offline_header": "Bạn đang ngoại tuyến. Một số thao tác cần kết nối.",
"files_new_folder": "Thư mục mới…",
"files_new_file": "Tệp mới…",
"files_new_text_file": "Tệp văn bản mới…",
"files_upload_files": "Tải tệp lên…",
"files_upload_folder": "Tải lên thư mục…",
"files_share": "Chia sẻ…",
"files_share_button": "Chia sẻ",
"contacts_add_to_group": "Thêm vào nhóm…",
"contacts_bulk_add_to_group": "Thêm vào nhóm…",
"schedule_send": "Lên lịch gửi…"
},
"notification_center": {
"title": "Thông báo",
"mark_all_read": "Đánh dấu tất cả đã đọc",
+13
View File
@@ -6455,6 +6455,19 @@
"group_account": "帳戶",
"switch_to": "切換到 {account}"
},
"menuAudit": {
"offline_header": "您已離線。部分操作需要連線。",
"files_new_folder": "新增資料夾…",
"files_new_file": "新增檔案…",
"files_new_text_file": "新增文字檔案…",
"files_upload_files": "上傳檔案…",
"files_upload_folder": "上傳資料夾…",
"files_share": "分享…",
"files_share_button": "分享",
"contacts_add_to_group": "新增至群組…",
"contacts_bulk_add_to_group": "新增至群組…",
"schedule_send": "排程寄送…"
},
"notification_center": {
"title": "通知",
"mark_all_read": "全部標示為已讀",
+13
View File
@@ -6455,6 +6455,19 @@
"group_account": "账户",
"switch_to": "切换到 {account}"
},
"menuAudit": {
"offline_header": "您已离线。部分操作需要联网。",
"files_new_folder": "新建文件夹…",
"files_new_file": "新建文件…",
"files_new_text_file": "新建文本文件…",
"files_upload_files": "上传文件…",
"files_upload_folder": "上传文件夹…",
"files_share": "共享…",
"files_share_button": "共享",
"contacts_add_to_group": "加入群组…",
"contacts_bulk_add_to_group": "添加到群组…",
"schedule_send": "计划发送…"
},
"notification_center": {
"title": "通知",
"mark_all_read": "全部标为已读",