mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 00:12:39 +00:00
feat: confirm sending without a subject instead of blocking it #684
This commit is contained in:
@@ -0,0 +1,256 @@
|
||||
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import React from 'react';
|
||||
import { EmailComposer } from '../email-composer';
|
||||
|
||||
// ─── Heavy component mocks (mirrors reply-addressing.test.tsx) ────────────────
|
||||
|
||||
vi.mock('@/components/email/rich-text-editor', () => ({
|
||||
RichTextEditor: () => React.createElement('div', { 'data-testid': 'rich-text-editor' }),
|
||||
}));
|
||||
|
||||
vi.mock('@/components/plugins/plugin-slot', () => ({ PluginSlot: () => null }));
|
||||
vi.mock('@/components/identity/sub-address-helper', () => ({ SubAddressHelper: () => null }));
|
||||
vi.mock('@/components/templates/template-picker', () => ({ TemplatePicker: () => null }));
|
||||
vi.mock('@/components/templates/template-form', () => ({ TemplateForm: () => null }));
|
||||
vi.mock('@/components/files/file-preview-modal', () => ({ FilePreviewModal: () => null }));
|
||||
vi.mock('@/hooks/use-focus-trap', () => ({
|
||||
useFocusTrap: () => ({ current: null }),
|
||||
}));
|
||||
vi.mock('@/hooks/use-pro-multi-account-identities', () => ({
|
||||
useProMultiAccountIdentities: () => ({ enabled: false, groups: [], allIdentities: [] }),
|
||||
stripCrossAccountIdentityPrefix: (id: string) => ({ localAccountId: null, rawId: id }),
|
||||
}));
|
||||
|
||||
// ─── Store mocks ──────────────────────────────────────────────────────────────
|
||||
|
||||
vi.mock('@/stores/auth-store', () => {
|
||||
const state = {
|
||||
client: null,
|
||||
identities: [],
|
||||
primaryIdentity: null,
|
||||
isAuthenticated: false,
|
||||
isDemoMode: false,
|
||||
activeAccountId: null,
|
||||
connectionLost: false,
|
||||
getClientForAccount: () => undefined,
|
||||
getAllConnectedClients: () => new Map(),
|
||||
syncIdentities: () => {},
|
||||
refreshIdentities: async () => {},
|
||||
};
|
||||
const hook = (sel?: (s: typeof state) => unknown) =>
|
||||
typeof sel === 'function' ? sel(state) : state;
|
||||
hook.getState = () => state;
|
||||
hook.setState = (p: Partial<typeof state>) => Object.assign(state, p);
|
||||
return { useAuthStore: hook };
|
||||
});
|
||||
|
||||
vi.mock('@/stores/identity-store', () => {
|
||||
const state = {
|
||||
identities: [{ id: 'id-me', email: 'me@example.com', name: 'Me' }],
|
||||
defaultIdentityId: 'id-me',
|
||||
};
|
||||
const hook = (sel?: (s: typeof state) => unknown) =>
|
||||
typeof sel === 'function' ? sel(state) : state;
|
||||
hook.getState = () => state;
|
||||
hook.setState = (p: Partial<typeof state>) => Object.assign(state, p);
|
||||
return { useIdentityStore: hook };
|
||||
});
|
||||
|
||||
vi.mock('@/stores/account-store', () => {
|
||||
const state = { accounts: [], getAccountById: () => undefined };
|
||||
const hook = (sel?: (s: typeof state) => unknown) =>
|
||||
typeof sel === 'function' ? sel(state) : state;
|
||||
hook.getState = () => state;
|
||||
hook.setState = (p: Partial<typeof state>) => Object.assign(state, p);
|
||||
return { useAccountStore: hook };
|
||||
});
|
||||
|
||||
vi.mock('@/stores/email-store', () => {
|
||||
const state = {
|
||||
draftSaveEnabled: false,
|
||||
sendRawEmail: async () => ({ sent: true }),
|
||||
};
|
||||
const hook = (sel?: (s: typeof state) => unknown) =>
|
||||
typeof sel === 'function' ? sel(state) : state;
|
||||
hook.getState = () => state;
|
||||
hook.setState = (p: Partial<typeof state>) => Object.assign(state, p);
|
||||
return { useEmailStore: hook };
|
||||
});
|
||||
|
||||
const updateSetting = vi.fn();
|
||||
|
||||
vi.mock('@/stores/settings-store', () => {
|
||||
const state = {
|
||||
timeFormat: '24h',
|
||||
plainTextMode: false,
|
||||
subAddressDelimiter: '+',
|
||||
autoSelectReplyIdentity: true,
|
||||
attachmentReminderEnabled: false,
|
||||
attachmentReminderKeywords: [],
|
||||
emptySubjectWarningEnabled: true,
|
||||
sendDelaySeconds: 0,
|
||||
signaturePosition: 'above_quote',
|
||||
signatureSeparatorEnabled: false,
|
||||
requestReadReceiptDefault: false,
|
||||
addTrustedSender: () => {},
|
||||
trustedSendersAddressBook: null,
|
||||
updateSetting: (...args: unknown[]) => updateSetting(...args),
|
||||
};
|
||||
const hook = (sel?: (s: typeof state) => unknown) =>
|
||||
typeof sel === 'function' ? sel(state) : state;
|
||||
hook.getState = () => state;
|
||||
hook.setState = (p: Partial<typeof state>) => Object.assign(state, p);
|
||||
return { useSettingsStore: hook };
|
||||
});
|
||||
|
||||
vi.mock('@/stores/contact-store', () => {
|
||||
const state = {
|
||||
contacts: [],
|
||||
getAutocomplete: async () => [],
|
||||
addToTrustedSendersBook: async () => {},
|
||||
};
|
||||
const hook = (sel?: (s: typeof state) => unknown) =>
|
||||
typeof sel === 'function' ? sel(state) : state;
|
||||
hook.getState = () => state;
|
||||
hook.setState = (p: Partial<typeof state>) => Object.assign(state, p);
|
||||
return { useContactStore: hook };
|
||||
});
|
||||
|
||||
vi.mock('@/stores/template-store', () => {
|
||||
const state = { templates: [], addTemplate: async () => {} };
|
||||
const hook = (sel?: (s: typeof state) => unknown) =>
|
||||
typeof sel === 'function' ? sel(state) : state;
|
||||
hook.getState = () => state;
|
||||
hook.setState = (p: Partial<typeof state>) => Object.assign(state, p);
|
||||
return { useTemplateStore: hook };
|
||||
});
|
||||
|
||||
// ─── Misc dependency mocks ────────────────────────────────────────────────────
|
||||
|
||||
vi.mock('@/stores/toast-store', () => ({
|
||||
toast: { info: () => {}, error: () => {}, success: () => {} },
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/plugin-hooks', () => ({
|
||||
emailHooks: {
|
||||
onComposerOpen: { call: async () => [] },
|
||||
onRecipientChange: { call: async () => [] },
|
||||
getRecipientSuggestions: { call: async () => [] },
|
||||
onRecipientChipsChange: { transform: async (chips: unknown) => chips },
|
||||
onDraftChange: { emit: () => {} },
|
||||
onBeforeEmailSend: { intercept: async () => true },
|
||||
onComposeSend: { intercept: async () => true },
|
||||
onTransformOutgoingEmail: { transform: async (email: unknown) => email },
|
||||
},
|
||||
contactHooks: {
|
||||
search: { call: async () => [] },
|
||||
onProvideRecipientSuggestions: { transform: async (initial: unknown) => initial },
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/email-sanitization', () => ({
|
||||
sanitizeSignatureHtml: (v: string) => v,
|
||||
sanitizeEmailHtml: (v: string) => v,
|
||||
parseHtmlSafely: (html: string) => new DOMParser().parseFromString(html, 'text/html'),
|
||||
}));
|
||||
|
||||
vi.mock('@/lib/email-threading', () => ({
|
||||
computeReplyThreadingHeaders: () => ({ inReplyTo: [], references: [] }),
|
||||
}));
|
||||
vi.mock('@/lib/signature-utils', () => ({
|
||||
appendPlainTextSignature: (body: string) => body,
|
||||
getPlainTextSignature: () => '',
|
||||
}));
|
||||
vi.mock('@/lib/sub-addressing', () => ({ generateSubAddress: () => '' }));
|
||||
vi.mock('@/lib/debug', () => ({ debug: { log: () => {}, warn: () => {}, error: () => {} } }));
|
||||
vi.mock('@/components/email/quoted-html', () => ({
|
||||
buildQuotedHtmlBlock: () => '',
|
||||
serializeEditorContent: () => '',
|
||||
}));
|
||||
vi.mock('@/lib/template-utils', () => ({ substitutePlaceholders: (s: string) => s }));
|
||||
|
||||
// ─── Tests ────────────────────────────────────────────────────────────────────
|
||||
|
||||
/** A ready-to-send draft with everything but a subject. */
|
||||
const DRAFT_WITHOUT_SUBJECT = {
|
||||
to: 'bob@example.com',
|
||||
cc: '',
|
||||
bcc: '',
|
||||
subject: '',
|
||||
body: '<p>Hello there</p>',
|
||||
showCc: false,
|
||||
showBcc: false,
|
||||
selectedIdentityId: 'id-me',
|
||||
subAddressTag: '',
|
||||
mode: 'compose' as const,
|
||||
draftId: null,
|
||||
};
|
||||
|
||||
const sendButton = () => screen.getAllByTestId('composer-send')[0] as HTMLButtonElement;
|
||||
|
||||
describe('composer empty subject warning', () => {
|
||||
beforeEach(() => {
|
||||
updateSetting.mockClear();
|
||||
});
|
||||
|
||||
it('keeps Send enabled when the subject is empty', () => {
|
||||
render(<EmailComposer initialData={DRAFT_WITHOUT_SUBJECT} />);
|
||||
expect(sendButton()).not.toBeDisabled();
|
||||
});
|
||||
|
||||
it('asks for confirmation instead of sending', async () => {
|
||||
const onSend = vi.fn();
|
||||
render(<EmailComposer initialData={DRAFT_WITHOUT_SUBJECT} onSend={onSend} />);
|
||||
|
||||
fireEvent.click(sendButton());
|
||||
|
||||
expect(await screen.findByText('empty_subject.title')).toBeInTheDocument();
|
||||
expect(onSend).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('sends with an empty subject once confirmed', async () => {
|
||||
const onSend = vi.fn();
|
||||
render(<EmailComposer initialData={DRAFT_WITHOUT_SUBJECT} onSend={onSend} />);
|
||||
|
||||
fireEvent.click(sendButton());
|
||||
fireEvent.click(await screen.findByText('empty_subject.send_anyway'));
|
||||
|
||||
await waitFor(() => expect(onSend).toHaveBeenCalledTimes(1));
|
||||
expect(onSend.mock.calls[0][0]).toMatchObject({ subject: '', to: ['bob@example.com'] });
|
||||
expect(updateSetting).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('turns the warning off when "don\'t ask again" is checked', async () => {
|
||||
render(<EmailComposer initialData={DRAFT_WITHOUT_SUBJECT} onSend={vi.fn()} />);
|
||||
|
||||
fireEvent.click(sendButton());
|
||||
fireEvent.click(await screen.findByText('empty_subject.dont_ask_again'));
|
||||
fireEvent.click(screen.getByText('empty_subject.send_anyway'));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(updateSetting).toHaveBeenCalledWith('emptySubjectWarningEnabled', false)
|
||||
);
|
||||
});
|
||||
|
||||
it('goes back to editing without sending', async () => {
|
||||
const onSend = vi.fn();
|
||||
render(<EmailComposer initialData={DRAFT_WITHOUT_SUBJECT} onSend={onSend} />);
|
||||
|
||||
fireEvent.click(sendButton());
|
||||
fireEvent.click(await screen.findByText('empty_subject.back'));
|
||||
|
||||
await waitFor(() => expect(screen.queryByText('empty_subject.title')).not.toBeInTheDocument());
|
||||
expect(onSend).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('still sends normally when a subject is present', async () => {
|
||||
const onSend = vi.fn();
|
||||
render(<EmailComposer initialData={{ ...DRAFT_WITHOUT_SUBJECT, subject: 'Lunch' }} onSend={onSend} />);
|
||||
|
||||
fireEvent.click(sendButton());
|
||||
|
||||
await waitFor(() => expect(onSend).toHaveBeenCalledTimes(1));
|
||||
expect(screen.queryByText('empty_subject.title')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -279,6 +279,8 @@ export function EmailComposer({
|
||||
const autoSelectReplyIdentity = useSettingsStore((state) => state.autoSelectReplyIdentity);
|
||||
const attachmentReminderEnabled = useSettingsStore((state) => state.attachmentReminderEnabled);
|
||||
const attachmentReminderKeywords = useSettingsStore((state) => state.attachmentReminderKeywords);
|
||||
const emptySubjectWarningEnabled = useSettingsStore((state) => state.emptySubjectWarningEnabled);
|
||||
const updateSetting = useSettingsStore((state) => state.updateSetting);
|
||||
const sendDelaySeconds = useSettingsStore((state) => state.sendDelaySeconds);
|
||||
const signaturePosition = useSettingsStore((state) => state.signaturePosition);
|
||||
const signatureSeparatorEnabled = useSettingsStore((state) => state.signatureSeparatorEnabled);
|
||||
@@ -530,7 +532,7 @@ export function EmailComposer({
|
||||
});
|
||||
const inlineImagesRef = useRef<Array<{ cid: string; blobId: string; type: string; name: string; size: number; dataUrl: string }>>([]);
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const [validationErrors, setValidationErrors] = useState<{ to?: boolean; subject?: boolean; body?: boolean }>({});
|
||||
const [validationErrors, setValidationErrors] = useState<{ to?: boolean; body?: boolean }>({});
|
||||
const [shakeField, setShakeField] = useState<string | null>(null);
|
||||
const [selectedIdentityId, setSelectedIdentityId] = useState<string | null>(initialData?.selectedIdentityId ?? null);
|
||||
const [subAddressTag, setSubAddressTag] = useState<string>(initialData?.subAddressTag ?? '');
|
||||
@@ -545,6 +547,9 @@ export function EmailComposer({
|
||||
const [showAttachmentWarning, setShowAttachmentWarning] = useState(false);
|
||||
const [attachmentWarningKeyword, setAttachmentWarningKeyword] = useState('');
|
||||
const [attachmentWarningDelayedUntil, setAttachmentWarningDelayedUntil] = useState<string | undefined>();
|
||||
const [showEmptySubjectWarning, setShowEmptySubjectWarning] = useState(false);
|
||||
const [emptySubjectDelayedUntil, setEmptySubjectDelayedUntil] = useState<string | undefined>();
|
||||
const [emptySubjectDontAskAgain, setEmptySubjectDontAskAgain] = useState(false);
|
||||
const [showScheduleDialog, setShowScheduleDialog] = useState(false);
|
||||
const [scheduleValue, setScheduleValue] = useState('');
|
||||
const [scheduleError, setScheduleError] = useState('');
|
||||
@@ -569,6 +574,12 @@ export function EmailComposer({
|
||||
restoreFocus: true,
|
||||
});
|
||||
|
||||
const emptySubjectWarningRef = useFocusTrap({
|
||||
isActive: showEmptySubjectWarning,
|
||||
onEscape: () => setShowEmptySubjectWarning(false),
|
||||
restoreFocus: true,
|
||||
});
|
||||
|
||||
const { client } = useAuthStore();
|
||||
const currentIdentity = selectedIdentityId
|
||||
? identities.find((identity) => identity.id === selectedIdentityId) || primaryIdentity
|
||||
@@ -1616,13 +1627,14 @@ export function EmailComposer({
|
||||
const toAddresses = expandRecipients(withInput(to, toInput));
|
||||
const bodyPlainText = plainTextMode ? body.trim() : htmlToPlainText(body).trim();
|
||||
const hasContent = bodyPlainText || attachments.some(att => att.blobId && !att.uploading);
|
||||
const canSend = toAddresses.length > 0 && !!subject && hasContent;
|
||||
// A missing subject no longer blocks Send (#684) - users hit the disabled
|
||||
// button without understanding why. It is confirmed in a dialog instead.
|
||||
const canSend = toAddresses.length > 0 && hasContent;
|
||||
|
||||
const getSendTooltip = (): string | undefined => {
|
||||
if (isWaitingForUploads) return t('validation.attachments_uploading');
|
||||
if (canSend) return undefined;
|
||||
if (toAddresses.length === 0) return t('validation.recipient_required');
|
||||
if (!subject) return t('validation.subject_required');
|
||||
if (!hasContent) return t('validation.body_required');
|
||||
return undefined;
|
||||
};
|
||||
@@ -1718,7 +1730,11 @@ export function EmailComposer({
|
||||
const [isWaitingForUploads, setIsWaitingForUploads] = useState(false);
|
||||
const sendCancelledRef = useRef(false);
|
||||
|
||||
const handleSend = async (skipAttachmentCheck = false, delayedUntil?: string) => {
|
||||
const handleSend = async ({ skipAttachmentCheck = false, skipSubjectCheck = false, delayedUntil }: {
|
||||
skipAttachmentCheck?: boolean;
|
||||
skipSubjectCheck?: boolean;
|
||||
delayedUntil?: string;
|
||||
} = {}) => {
|
||||
if (isSendingRef.current) return;
|
||||
|
||||
if (attachmentsRef.current.some(att => att.uploading)) {
|
||||
@@ -1746,9 +1762,8 @@ export function EmailComposer({
|
||||
const bccAddresses = expandRecipients(withInput(bcc, bccInput));
|
||||
|
||||
if (!canSend) {
|
||||
const errors: { to?: boolean; subject?: boolean; body?: boolean } = {};
|
||||
const errors: { to?: boolean; body?: boolean } = {};
|
||||
if (toAddresses.length === 0) errors.to = true;
|
||||
if (!subject) errors.subject = true;
|
||||
if (!hasContent) errors.body = true;
|
||||
setValidationErrors(errors);
|
||||
|
||||
@@ -1760,6 +1775,14 @@ export function EmailComposer({
|
||||
return;
|
||||
}
|
||||
|
||||
// Empty subject confirmation (#684)
|
||||
if (!skipSubjectCheck && emptySubjectWarningEnabled && !subject.trim()) {
|
||||
setEmptySubjectDontAskAgain(false);
|
||||
setEmptySubjectDelayedUntil(delayedUntil);
|
||||
setShowEmptySubjectWarning(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Attachment reminder check
|
||||
if (!skipAttachmentCheck && attachmentReminderEnabled) {
|
||||
const hasAttachments = attachmentsRef.current.some(att => att.blobId && !att.uploading && !att.error);
|
||||
@@ -2039,7 +2062,7 @@ export function EmailComposer({
|
||||
setScheduleError(error);
|
||||
return;
|
||||
}
|
||||
handleSend(false, new Date(scheduleValue).toISOString());
|
||||
handleSend({ delayedUntil: new Date(scheduleValue).toISOString() });
|
||||
};
|
||||
|
||||
// Ctrl+Enter (Win/Linux) / Cmd+Enter (macOS) sends the open compose
|
||||
@@ -2050,7 +2073,7 @@ export function EmailComposer({
|
||||
// on a single keystroke. handleSend is rebound every render, so we
|
||||
// route through a ref to keep the listener stable.
|
||||
const composerRootRef = useRef<HTMLDivElement | null>(null);
|
||||
const handleSendRef = useRef<((skipAttachmentCheck?: boolean) => Promise<void>) | undefined>(undefined);
|
||||
const handleSendRef = useRef<typeof handleSend | undefined>(undefined);
|
||||
handleSendRef.current = handleSend;
|
||||
useEffect(() => {
|
||||
const handleSendShortcut = (e: KeyboardEvent) => {
|
||||
@@ -2151,6 +2174,7 @@ export function EmailComposer({
|
||||
showSaveAsTemplate ||
|
||||
showScheduleDialog ||
|
||||
showAttachmentWarning ||
|
||||
showEmptySubjectWarning ||
|
||||
showCloseDialog
|
||||
) return;
|
||||
|
||||
@@ -2516,21 +2540,14 @@ export function EmailComposer({
|
||||
type="text"
|
||||
placeholder={t('subject_placeholder')}
|
||||
value={subject}
|
||||
onChange={(e) => {
|
||||
setSubject(e.target.value);
|
||||
if (validationErrors.subject) setValidationErrors(prev => ({ ...prev, subject: false }));
|
||||
}}
|
||||
onChange={(e) => setSubject(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Tab' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
focusBody();
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
"flex-1 border-0 focus-visible:ring-0 h-8 px-0 text-sm",
|
||||
validationErrors.subject && "ring-2 ring-red-500 dark:ring-red-400"
|
||||
)}
|
||||
aria-invalid={validationErrors.subject || undefined}
|
||||
className="flex-1 border-0 focus-visible:ring-0 h-8 px-0 text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2866,7 +2883,7 @@ export function EmailComposer({
|
||||
<Button variant="outline" onClick={() => setShowAttachmentWarning(false)}>
|
||||
{t('forgot_attachment.back')}
|
||||
</Button>
|
||||
<Button onClick={() => { setShowAttachmentWarning(false); handleSend(true, attachmentWarningDelayedUntil); setAttachmentWarningDelayedUntil(undefined); }}>
|
||||
<Button onClick={() => { setShowAttachmentWarning(false); handleSend({ skipAttachmentCheck: true, skipSubjectCheck: true, delayedUntil: attachmentWarningDelayedUntil }); setAttachmentWarningDelayedUntil(undefined); }}>
|
||||
{t('forgot_attachment.send_anyway')}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -2874,6 +2891,59 @@ export function EmailComposer({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showEmptySubjectWarning && (
|
||||
<div
|
||||
className="fixed inset-0 bg-black/50 backdrop-blur-[1px] flex items-center justify-center z-[60] p-4 animate-in fade-in duration-150"
|
||||
onClick={() => setShowEmptySubjectWarning(false)}
|
||||
>
|
||||
<div
|
||||
ref={emptySubjectWarningRef}
|
||||
role="alertdialog"
|
||||
aria-modal="true"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="bg-background border border-border rounded-lg shadow-xl w-full max-w-md animate-in zoom-in-95 duration-200"
|
||||
>
|
||||
<div className="p-6">
|
||||
<h2 className="text-lg font-semibold text-foreground">{t('empty_subject.title')}</h2>
|
||||
<p className="mt-2 text-sm text-muted-foreground">{t('empty_subject.message')}</p>
|
||||
<label className="mt-4 flex items-center gap-2 text-sm text-muted-foreground cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={emptySubjectDontAskAgain}
|
||||
onChange={(e) => setEmptySubjectDontAskAgain(e.target.checked)}
|
||||
className="rounded border-input"
|
||||
/>
|
||||
{t('empty_subject.dont_ask_again')}
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex items-center justify-end gap-3 px-6 pb-6">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setShowEmptySubjectWarning(false);
|
||||
setEmptySubjectDelayedUntil(undefined);
|
||||
// After the focus trap tears down it restores focus to the
|
||||
// Send button, so claim the subject field on the next tick.
|
||||
setTimeout(() => subjectInputRef.current?.focus(), 0);
|
||||
}}
|
||||
>
|
||||
{t('empty_subject.back')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (emptySubjectDontAskAgain) updateSetting('emptySubjectWarningEnabled', false);
|
||||
setShowEmptySubjectWarning(false);
|
||||
handleSend({ skipSubjectCheck: true, delayedUntil: emptySubjectDelayedUntil });
|
||||
setEmptySubjectDelayedUntil(undefined);
|
||||
}}
|
||||
>
|
||||
{t('empty_subject.send_anyway')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showCloseDialog && (
|
||||
<div
|
||||
className="fixed inset-0 bg-black/50 backdrop-blur-[1px] flex items-center justify-center z-[60] p-4 animate-in fade-in duration-150"
|
||||
|
||||
@@ -26,6 +26,7 @@ export function ComposingSettings() {
|
||||
rtlEditingSupport,
|
||||
attachmentReminderEnabled,
|
||||
attachmentReminderKeywords,
|
||||
emptySubjectWarningEnabled,
|
||||
sendDelaySeconds,
|
||||
subAddressDelimiter,
|
||||
signaturePosition,
|
||||
@@ -158,6 +159,13 @@ export function ComposingSettings() {
|
||||
</div>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem label={t('empty_subject_warning.label')} description={t('empty_subject_warning.description')}>
|
||||
<ToggleSwitch
|
||||
checked={emptySubjectWarningEnabled}
|
||||
onChange={(checked) => updateSetting('emptySubjectWarningEnabled', checked)}
|
||||
/>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem label={t('attachment_reminder.label')} description={t('attachment_reminder.description')}>
|
||||
<ToggleSwitch
|
||||
checked={attachmentReminderEnabled}
|
||||
|
||||
@@ -214,6 +214,7 @@ const tabSearchPaths: Record<Tab, string[]> = {
|
||||
'settings.email_behavior.auto_select_reply_identity',
|
||||
'settings.email_behavior.plain_text_mode',
|
||||
'settings.email_behavior.default_mail_program',
|
||||
'settings.email_behavior.empty_subject_warning',
|
||||
'settings.email_behavior.signature_position',
|
||||
'settings.email_behavior.sub_address_delimiter',
|
||||
],
|
||||
|
||||
+11
-1
@@ -636,7 +636,6 @@
|
||||
"save_as_template": "حفظ كقالب",
|
||||
"validation": {
|
||||
"recipient_required": "أضف مستلمًا للإرسال",
|
||||
"subject_required": "أضف موضوعًا",
|
||||
"body_required": "اكتب رسالة أو أرفق ملفًا",
|
||||
"attachments_uploading": "المرفقات لا تزال قيد الرفع — سيتم الإرسال فور انتهائها",
|
||||
"attachment_upload_failed": "لم يتم الإرسال — فشل رفع أحد المرفقات. أزله وحاول مرة أخرى."
|
||||
@@ -666,6 +665,13 @@
|
||||
"send_anyway": "الإرسال على أي حال",
|
||||
"back": "العودة إلى التعديل"
|
||||
},
|
||||
"empty_subject": {
|
||||
"title": "إرسال بدون موضوع؟",
|
||||
"message": "لا يحتوي هذا البريد على موضوع. هل تريد إرساله على أي حال؟",
|
||||
"dont_ask_again": "عدم السؤال مرة أخرى",
|
||||
"send_anyway": "الإرسال على أي حال",
|
||||
"back": "العودة إلى التعديل"
|
||||
},
|
||||
"schedule_send": "جدولة الإرسال",
|
||||
"schedule_send_description": "اختر متى يجب على الخادم إرسال هذه الرسالة.",
|
||||
"schedule_send_required": "اختر تاريخًا ووقتًا.",
|
||||
@@ -1321,6 +1327,10 @@
|
||||
"add": "إضافة",
|
||||
"remove": "إزالة"
|
||||
},
|
||||
"empty_subject_warning": {
|
||||
"label": "تحذير الموضوع الفارغ",
|
||||
"description": "طلب تأكيد قبل إرسال رسالة بدون موضوع."
|
||||
},
|
||||
"hide_inline_image_attachments": {
|
||||
"label": "إخفاء الصور المضمّنة من المرفقات",
|
||||
"description": "الصور المضمّنة في نص الرسالة لا تُدرج كمرفقات منفصلة"
|
||||
|
||||
+11
-1
@@ -636,7 +636,6 @@
|
||||
"save_as_template": "Desa com a plantilla",
|
||||
"validation": {
|
||||
"recipient_required": "Afegiu un destinatari per enviar",
|
||||
"subject_required": "Afegiu un assumpte",
|
||||
"body_required": "Escriviu un missatge o adjunteu un fitxer",
|
||||
"attachments_uploading": "Els fitxers adjunts encara s'estan pujant — s'enviarà en acabar",
|
||||
"attachment_upload_failed": "No s'ha enviat — no s'ha pogut pujar un fitxer adjunt. Elimineu-lo i torneu-ho a provar."
|
||||
@@ -666,6 +665,13 @@
|
||||
"send_anyway": "Envia igualment",
|
||||
"back": "Torna a l'edició"
|
||||
},
|
||||
"empty_subject": {
|
||||
"title": "Voleu enviar-lo sense assumpte?",
|
||||
"message": "Aquest missatge no té assumpte. Voleu enviar-lo igualment?",
|
||||
"dont_ask_again": "No ho tornis a preguntar",
|
||||
"send_anyway": "Envia igualment",
|
||||
"back": "Torna a l'edició"
|
||||
},
|
||||
"schedule_send": "Programa l'enviament",
|
||||
"schedule_send_description": "Trieu quan ha d'alliberar el servidor aquest missatge.",
|
||||
"schedule_send_required": "Trieu una data i hora.",
|
||||
@@ -1321,6 +1327,10 @@
|
||||
"add": "Afegeix",
|
||||
"remove": "Elimina"
|
||||
},
|
||||
"empty_subject_warning": {
|
||||
"label": "Avís d'assumpte buit",
|
||||
"description": "Demana confirmació abans d'enviar un missatge sense assumpte."
|
||||
},
|
||||
"hide_inline_image_attachments": {
|
||||
"label": "Amaga les imatges incrustades dels fitxers adjunts",
|
||||
"description": "Les imatges incrustades al cos del missatge no es llisten com a fitxers adjunts separats"
|
||||
|
||||
+11
-1
@@ -633,7 +633,6 @@
|
||||
"save_as_template": "Uložit jako šablonu",
|
||||
"validation": {
|
||||
"recipient_required": "Chcete-li zprávu odeslat, přidejte příjemce",
|
||||
"subject_required": "Přidejte předmět",
|
||||
"body_required": "Napište zprávu nebo připojte soubor",
|
||||
"attachments_uploading": "Přílohy se stále nahrávají – odeslání proběhne po dokončení",
|
||||
"attachment_upload_failed": "Neodesláno - přílohu se nepodařilo nahrát. Odeberte ji a zkuste to znovu."
|
||||
@@ -663,6 +662,13 @@
|
||||
"send_anyway": "Přesto odeslat",
|
||||
"back": "Zpět k úpravám"
|
||||
},
|
||||
"empty_subject": {
|
||||
"title": "Odeslat bez předmětu?",
|
||||
"message": "Tato zpráva nemá předmět. Přesto odeslat?",
|
||||
"dont_ask_again": "Znovu se neptat",
|
||||
"send_anyway": "Přesto odeslat",
|
||||
"back": "Zpět k úpravám"
|
||||
},
|
||||
"add_link": "Přidat odkaz",
|
||||
"link_url_prompt": "Zadejte URL",
|
||||
"sending": "Odesílání...",
|
||||
@@ -1318,6 +1324,10 @@
|
||||
"add": "Přidat",
|
||||
"remove": "Odstranit"
|
||||
},
|
||||
"empty_subject_warning": {
|
||||
"label": "Upozornění na prázdný předmět",
|
||||
"description": "Před odesláním zprávy bez předmětu požádat o potvrzení."
|
||||
},
|
||||
"hide_inline_image_attachments": {
|
||||
"label": "Skrýt vložené obrázky ze seznamu příloh",
|
||||
"description": "Obrázky vložené přímo do textu zprávy se nezobrazí jako samostatné přílohy"
|
||||
|
||||
+11
-1
@@ -635,7 +635,6 @@
|
||||
"save_as_template": "Gem som skabelon",
|
||||
"validation": {
|
||||
"recipient_required": "Tilføj en modtager for at sende",
|
||||
"subject_required": "Tilføj et emne",
|
||||
"body_required": "Skriv en besked eller vedhæft en fil",
|
||||
"attachments_uploading": "Vedhæftninger uploades stadig — sendes, når de er færdige",
|
||||
"attachment_upload_failed": "Ikke sendt - en vedhæftet fil kunne ikke uploades. Fjern den, og prøv igen."
|
||||
@@ -665,6 +664,13 @@
|
||||
"send_anyway": "Send alligevel",
|
||||
"back": "Tilbage til redigering"
|
||||
},
|
||||
"empty_subject": {
|
||||
"title": "Send uden emne?",
|
||||
"message": "Denne besked har intet emne. Send alligevel?",
|
||||
"dont_ask_again": "Spørg ikke igen",
|
||||
"send_anyway": "Send alligevel",
|
||||
"back": "Tilbage til redigering"
|
||||
},
|
||||
"link_url_prompt": "Indtast URL'en",
|
||||
"schedule_send": "Planlæg afsendelse",
|
||||
"schedule_send_description": "Vælg hvornår serveren skal frigive denne besked.",
|
||||
@@ -1321,6 +1327,10 @@
|
||||
"add": "Tilføj",
|
||||
"remove": "Fjern"
|
||||
},
|
||||
"empty_subject_warning": {
|
||||
"label": "Advarsel om tomt emne",
|
||||
"description": "Bed om bekræftelse, før en besked uden emne sendes."
|
||||
},
|
||||
"hide_inline_image_attachments": {
|
||||
"label": "Skjul indlejrede billeder fra vedhæftninger",
|
||||
"description": "Billeder indlejret i beskedteksten vises ikke som separate vedhæftninger"
|
||||
|
||||
+11
-1
@@ -633,7 +633,6 @@
|
||||
"save_as_template": "Als Vorlage speichern",
|
||||
"validation": {
|
||||
"recipient_required": "Empfänger hinzufügen zum Senden",
|
||||
"subject_required": "Betreff hinzufügen",
|
||||
"body_required": "Nachricht schreiben oder Datei anhängen",
|
||||
"attachments_uploading": "Anhänge werden noch hochgeladen – wird gesendet, sobald sie fertig sind",
|
||||
"attachment_upload_failed": "Nicht gesendet - ein Anhang konnte nicht hochgeladen werden. Entfernen Sie ihn und versuchen Sie es erneut."
|
||||
@@ -663,6 +662,13 @@
|
||||
"send_anyway": "Trotzdem senden",
|
||||
"back": "Zurück zur Bearbeitung"
|
||||
},
|
||||
"empty_subject": {
|
||||
"title": "Ohne Betreff senden?",
|
||||
"message": "Diese Nachricht hat keinen Betreff. Trotzdem senden?",
|
||||
"dont_ask_again": "Nicht mehr fragen",
|
||||
"send_anyway": "Trotzdem senden",
|
||||
"back": "Zurück zur Bearbeitung"
|
||||
},
|
||||
"add_link": "Link hinzufügen",
|
||||
"link_url_prompt": "URL eingeben",
|
||||
"sending": "Wird gesendet...",
|
||||
@@ -1318,6 +1324,10 @@
|
||||
"add": "Hinzufügen",
|
||||
"remove": "Entfernen"
|
||||
},
|
||||
"empty_subject_warning": {
|
||||
"label": "Warnung bei leerem Betreff",
|
||||
"description": "Vor dem Senden einer Nachricht ohne Betreff nachfragen."
|
||||
},
|
||||
"hide_inline_image_attachments": {
|
||||
"label": "Eingebettete Bilder in Anhangsliste ausblenden",
|
||||
"description": "Im Nachrichtentext eingebettete Bilder werden nicht als separate Anhänge aufgeführt"
|
||||
|
||||
+11
-1
@@ -636,7 +636,6 @@
|
||||
"save_as_template": "Save as Template",
|
||||
"validation": {
|
||||
"recipient_required": "Add a recipient to send",
|
||||
"subject_required": "Add a subject",
|
||||
"body_required": "Write a message or attach a file",
|
||||
"attachments_uploading": "Attachments are still uploading — sending as soon as they finish",
|
||||
"attachment_upload_failed": "Not sent — an attachment failed to upload. Remove it and try again."
|
||||
@@ -665,6 +664,13 @@
|
||||
"send_anyway": "Send anyway",
|
||||
"back": "Back to editing"
|
||||
},
|
||||
"empty_subject": {
|
||||
"title": "Send without a subject?",
|
||||
"message": "This message has no subject. Send it anyway?",
|
||||
"dont_ask_again": "Don't ask again",
|
||||
"send_anyway": "Send anyway",
|
||||
"back": "Back to editing"
|
||||
},
|
||||
"schedule_send": "Schedule send",
|
||||
"schedule_send_description": "Choose when the server should release this message.",
|
||||
"schedule_send_required": "Choose a date and time.",
|
||||
@@ -1320,6 +1326,10 @@
|
||||
"add": "Add",
|
||||
"remove": "Remove"
|
||||
},
|
||||
"empty_subject_warning": {
|
||||
"label": "Empty subject warning",
|
||||
"description": "Ask for confirmation before sending a message with no subject."
|
||||
},
|
||||
"hide_inline_image_attachments": {
|
||||
"label": "Hide inline images from attachments",
|
||||
"description": "Images embedded in the message body are not listed as separate attachments"
|
||||
|
||||
+11
-1
@@ -633,7 +633,6 @@
|
||||
"save_as_template": "Guardar como plantilla",
|
||||
"validation": {
|
||||
"recipient_required": "Agregue un destinatario para enviar",
|
||||
"subject_required": "Agregue un asunto",
|
||||
"body_required": "Escriba un mensaje o adjunte un archivo",
|
||||
"attachments_uploading": "Los archivos adjuntos aún se están subiendo; se enviará en cuanto terminen",
|
||||
"attachment_upload_failed": "No enviado: no se pudo subir un adjunto. Elimínalo e inténtalo de nuevo."
|
||||
@@ -663,6 +662,13 @@
|
||||
"send_anyway": "Enviar de todos modos",
|
||||
"back": "Volver a editar"
|
||||
},
|
||||
"empty_subject": {
|
||||
"title": "¿Enviar sin asunto?",
|
||||
"message": "Este mensaje no tiene asunto. ¿Enviarlo de todos modos?",
|
||||
"dont_ask_again": "No volver a preguntar",
|
||||
"send_anyway": "Enviar de todos modos",
|
||||
"back": "Volver a editar"
|
||||
},
|
||||
"add_link": "Añadir enlace",
|
||||
"link_url_prompt": "Introduce la URL",
|
||||
"sending": "Enviando...",
|
||||
@@ -1318,6 +1324,10 @@
|
||||
"add": "Add",
|
||||
"remove": "Eliminar"
|
||||
},
|
||||
"empty_subject_warning": {
|
||||
"label": "Aviso de asunto vacío",
|
||||
"description": "Pedir confirmación antes de enviar un mensaje sin asunto."
|
||||
},
|
||||
"hide_inline_image_attachments": {
|
||||
"label": "Ocultar imágenes incrustadas de los adjuntos",
|
||||
"description": "Las imágenes incrustadas en el cuerpo del mensaje no se listan como adjuntos separados"
|
||||
|
||||
+11
-1
@@ -636,7 +636,6 @@
|
||||
"save_as_template": "ذخیره به عنوان قالب",
|
||||
"validation": {
|
||||
"recipient_required": "یک گیرنده اضافه کنید",
|
||||
"subject_required": "یک موضوع اضافه کنید",
|
||||
"body_required": "پیامی بنویسید یا فایلی پیوست کنید",
|
||||
"attachments_uploading": "پیوستها هنوز در حال بارگذاری هستند — پس از اتمام ارسال میشود",
|
||||
"attachment_upload_failed": "ارسال نشد - بارگذاری یک پیوست ناموفق بود. آن را حذف کنید و دوباره تلاش کنید."
|
||||
@@ -666,6 +665,13 @@
|
||||
"send_anyway": "ارسال به هر حال",
|
||||
"back": "بازگشت به ویرایش"
|
||||
},
|
||||
"empty_subject": {
|
||||
"title": "بدون موضوع ارسال شود؟",
|
||||
"message": "این پیام موضوعی ندارد. با این حال ارسال شود؟",
|
||||
"dont_ask_again": "دیگر نپرس",
|
||||
"send_anyway": "ارسال به هر حال",
|
||||
"back": "بازگشت به ویرایش"
|
||||
},
|
||||
"schedule_send": "زمانبندی ارسال",
|
||||
"schedule_send_description": "زمان انتشار پیام توسط سرور را انتخاب کنید.",
|
||||
"schedule_send_required": "تاریخ و زمان را انتخاب کنید.",
|
||||
@@ -1321,6 +1327,10 @@
|
||||
"add": "افزودن",
|
||||
"remove": "حذف"
|
||||
},
|
||||
"empty_subject_warning": {
|
||||
"label": "هشدار موضوع خالی",
|
||||
"description": "پیش از ارسال پیام بدون موضوع تأیید بگیرید."
|
||||
},
|
||||
"hide_inline_image_attachments": {
|
||||
"label": "مخفی کردن تصاویر داخلی",
|
||||
"description": "عدم نمایش به عنوان پیوست"
|
||||
|
||||
+11
-1
@@ -633,7 +633,6 @@
|
||||
"save_as_template": "Enregistrer comme modèle",
|
||||
"validation": {
|
||||
"recipient_required": "Ajoutez un destinataire pour envoyer",
|
||||
"subject_required": "Ajoutez un objet",
|
||||
"body_required": "Rédigez un message ou joignez un fichier",
|
||||
"attachments_uploading": "Les pièces jointes sont en cours d'envoi — le message partira une fois terminé",
|
||||
"attachment_upload_failed": "Non envoyé - le téléversement d'une pièce jointe a échoué. Supprimez-la et réessayez."
|
||||
@@ -663,6 +662,13 @@
|
||||
"send_anyway": "Envoyer quand même",
|
||||
"back": "Retour à l'édition"
|
||||
},
|
||||
"empty_subject": {
|
||||
"title": "Envoyer sans objet ?",
|
||||
"message": "Ce message n'a pas d'objet. L'envoyer quand même ?",
|
||||
"dont_ask_again": "Ne plus me demander",
|
||||
"send_anyway": "Envoyer quand même",
|
||||
"back": "Retour à l'édition"
|
||||
},
|
||||
"add_link": "Ajouter un lien",
|
||||
"link_url_prompt": "Saisissez l'URL",
|
||||
"sending": "Envoi...",
|
||||
@@ -1318,6 +1324,10 @@
|
||||
"add": "Add",
|
||||
"remove": "Supprimer"
|
||||
},
|
||||
"empty_subject_warning": {
|
||||
"label": "Avertissement d'objet vide",
|
||||
"description": "Demander confirmation avant d'envoyer un message sans objet."
|
||||
},
|
||||
"hide_inline_image_attachments": {
|
||||
"label": "Masquer les images intégrées des pièces jointes",
|
||||
"description": "Les images intégrées au corps du message ne sont pas listées comme pièces jointes séparées"
|
||||
|
||||
+11
-1
@@ -585,7 +585,6 @@
|
||||
"save_as_template": "שמור כתבנית",
|
||||
"validation": {
|
||||
"recipient_required": "הוסף נמען לשליחה",
|
||||
"subject_required": "הוסף נושא",
|
||||
"body_required": "כתוב הודעה או צרף קובץ",
|
||||
"attachments_uploading": "קבצים מצורפים עדיין בהעלאה - ההודעה תישלח מיד בסיום",
|
||||
"attachment_upload_failed": "לא נשלח - העלאת קובץ מצורף נכשלה. הסירו אותו ונסו שוב."
|
||||
@@ -631,6 +630,13 @@
|
||||
"send_anyway": "שלח בכל זאת",
|
||||
"back": "חזור לעריכה"
|
||||
},
|
||||
"empty_subject": {
|
||||
"title": "לשלוח ללא נושא?",
|
||||
"message": "להודעה זו אין נושא. לשלוח בכל זאת?",
|
||||
"dont_ask_again": "אל תשאל שוב",
|
||||
"send_anyway": "שלח בכל זאת",
|
||||
"back": "חזור לעריכה"
|
||||
},
|
||||
"schedule_send": "תזמן שליחה",
|
||||
"schedule_send_description": "בחר מתי השרת צריך לשחרר הודעה זו.",
|
||||
"schedule_send_required": "בחר תאריך ושעה.",
|
||||
@@ -1283,6 +1289,10 @@
|
||||
"add": "הוסף",
|
||||
"remove": "הסר"
|
||||
},
|
||||
"empty_subject_warning": {
|
||||
"label": "אזהרה על נושא ריק",
|
||||
"description": "בקש אישור לפני שליחת הודעה ללא נושא."
|
||||
},
|
||||
"hide_inline_image_attachments": {
|
||||
"label": "הסתר תמונות מוטבעות מקבצים מצורפים",
|
||||
"description": "תמונות מוטבעות בגוף ההודעה אינן מופיעות כקבצים מצורפים נפרדים"
|
||||
|
||||
+11
-1
@@ -636,7 +636,6 @@
|
||||
"save_as_template": "Mentés sablonként",
|
||||
"validation": {
|
||||
"recipient_required": "Adj meg egy címzettet a küldéshez",
|
||||
"subject_required": "Adj meg egy tárgyat",
|
||||
"body_required": "Írj üzenetet vagy csatolj fájlt",
|
||||
"attachments_uploading": "A mellékletek még feltöltés alatt vannak – küldés a feltöltés befejezése után",
|
||||
"attachment_upload_failed": "Nincs elküldve - egy melléklet feltöltése nem sikerült. Távolítsa el, és próbálja újra."
|
||||
@@ -666,6 +665,13 @@
|
||||
"send_anyway": "Küldés így is",
|
||||
"back": "Vissza a szerkesztéshez"
|
||||
},
|
||||
"empty_subject": {
|
||||
"title": "Küldés tárgy nélkül?",
|
||||
"message": "Ennek az üzenetnek nincs tárgya. Mégis elküldi?",
|
||||
"dont_ask_again": "Ne kérdezze többé",
|
||||
"send_anyway": "Küldés így is",
|
||||
"back": "Vissza a szerkesztéshez"
|
||||
},
|
||||
"schedule_send": "Késleltetett küldés",
|
||||
"schedule_send_description": "Válaszd ki, mikor küldje el a szerver az üzenetet.",
|
||||
"schedule_send_required": "Válassz dátumot és időpontot.",
|
||||
@@ -1321,6 +1327,10 @@
|
||||
"add": "Hozzáadás",
|
||||
"remove": "Eltávolítás"
|
||||
},
|
||||
"empty_subject_warning": {
|
||||
"label": "Figyelmeztetés üres tárgy esetén",
|
||||
"description": "Megerősítés kérése tárgy nélküli üzenet küldése előtt."
|
||||
},
|
||||
"hide_inline_image_attachments": {
|
||||
"label": "Beágyazott képek elrejtése a csatolmányok közül",
|
||||
"description": "Az üzenet törzsébe ágyazott képek nem jelennek meg külön csatolmányként"
|
||||
|
||||
+11
-1
@@ -633,7 +633,6 @@
|
||||
"save_as_template": "Salva come modello",
|
||||
"validation": {
|
||||
"recipient_required": "Aggiungi un destinatario per inviare",
|
||||
"subject_required": "Aggiungi un oggetto",
|
||||
"body_required": "Scrivi un messaggio o allega un file",
|
||||
"attachments_uploading": "Gli allegati sono ancora in fase di caricamento: verrà inviato al termine",
|
||||
"attachment_upload_failed": "Non inviato: caricamento di un allegato non riuscito. Rimuovilo e riprova."
|
||||
@@ -663,6 +662,13 @@
|
||||
"send_anyway": "Invia comunque",
|
||||
"back": "Torna alla modifica"
|
||||
},
|
||||
"empty_subject": {
|
||||
"title": "Inviare senza oggetto?",
|
||||
"message": "Questo messaggio non ha un oggetto. Inviarlo comunque?",
|
||||
"dont_ask_again": "Non chiedere più",
|
||||
"send_anyway": "Invia comunque",
|
||||
"back": "Torna alla modifica"
|
||||
},
|
||||
"add_link": "Aggiungi link",
|
||||
"link_url_prompt": "Inserisci l'URL",
|
||||
"sending": "Invio in corso...",
|
||||
@@ -1318,6 +1324,10 @@
|
||||
"add": "Add",
|
||||
"remove": "Rimuovi"
|
||||
},
|
||||
"empty_subject_warning": {
|
||||
"label": "Avviso oggetto vuoto",
|
||||
"description": "Chiedi conferma prima di inviare un messaggio senza oggetto."
|
||||
},
|
||||
"hide_inline_image_attachments": {
|
||||
"label": "Nascondi le immagini inline dagli allegati",
|
||||
"description": "Le immagini incorporate nel corpo del messaggio non vengono elencate come allegati separati"
|
||||
|
||||
+11
-1
@@ -633,7 +633,6 @@
|
||||
"save_as_template": "テンプレートとして保存",
|
||||
"validation": {
|
||||
"recipient_required": "送信するには宛先を追加してください",
|
||||
"subject_required": "件名を追加してください",
|
||||
"body_required": "メッセージを入力するかファイルを添付してください",
|
||||
"attachments_uploading": "添付ファイルをアップロード中です。完了次第送信します",
|
||||
"attachment_upload_failed": "送信されませんでした。添付ファイルのアップロードに失敗しました。削除してもう一度お試しください。"
|
||||
@@ -663,6 +662,13 @@
|
||||
"send_anyway": "そのまま送信",
|
||||
"back": "編集に戻る"
|
||||
},
|
||||
"empty_subject": {
|
||||
"title": "件名なしで送信しますか?",
|
||||
"message": "このメッセージには件名がありません。このまま送信しますか?",
|
||||
"dont_ask_again": "今後は確認しない",
|
||||
"send_anyway": "そのまま送信",
|
||||
"back": "編集に戻る"
|
||||
},
|
||||
"add_link": "リンクを追加",
|
||||
"link_url_prompt": "URLを入力してください",
|
||||
"sending": "送信中...",
|
||||
@@ -1318,6 +1324,10 @@
|
||||
"add": "Add",
|
||||
"remove": "削除"
|
||||
},
|
||||
"empty_subject_warning": {
|
||||
"label": "件名なしの警告",
|
||||
"description": "件名のないメッセージを送信する前に確認します。"
|
||||
},
|
||||
"hide_inline_image_attachments": {
|
||||
"label": "添付ファイル一覧からインライン画像を隠す",
|
||||
"description": "本文に埋め込まれた画像を個別の添付ファイルとして表示しません"
|
||||
|
||||
+11
-1
@@ -633,7 +633,6 @@
|
||||
"save_as_template": "템플릿으로 저장",
|
||||
"validation": {
|
||||
"recipient_required": "보내려면 받는 사람을 추가해 주세요",
|
||||
"subject_required": "제목을 입력해 주세요",
|
||||
"body_required": "메시지를 작성하거나 파일을 첨부해 주세요",
|
||||
"attachments_uploading": "첨부 파일을 업로드하는 중입니다. 완료되면 자동으로 전송됩니다",
|
||||
"attachment_upload_failed": "전송되지 않았습니다. 첨부 파일 업로드에 실패했습니다. 해당 파일을 제거하고 다시 시도하세요."
|
||||
@@ -663,6 +662,13 @@
|
||||
"send_anyway": "그래도 보내기",
|
||||
"back": "편집으로 돌아가기"
|
||||
},
|
||||
"empty_subject": {
|
||||
"title": "제목 없이 보낼까요?",
|
||||
"message": "이 메시지에는 제목이 없습니다. 그래도 보낼까요?",
|
||||
"dont_ask_again": "다시 묻지 않기",
|
||||
"send_anyway": "그래도 보내기",
|
||||
"back": "편집으로 돌아가기"
|
||||
},
|
||||
"add_link": "링크 추가",
|
||||
"link_url_prompt": "URL을 입력하세요",
|
||||
"sending": "전송 중...",
|
||||
@@ -1318,6 +1324,10 @@
|
||||
"add": "Add",
|
||||
"remove": "삭제"
|
||||
},
|
||||
"empty_subject_warning": {
|
||||
"label": "빈 제목 경고",
|
||||
"description": "제목 없는 메시지를 보내기 전에 확인을 요청합니다."
|
||||
},
|
||||
"hide_inline_image_attachments": {
|
||||
"label": "첨부 파일 목록에서 인라인 이미지 숨기기",
|
||||
"description": "메시지 본문에 포함된 이미지는 별도의 첨부 파일로 표시되지 않습니다"
|
||||
|
||||
+11
-1
@@ -633,7 +633,6 @@
|
||||
"save_as_template": "Saglabāt kā veidni",
|
||||
"validation": {
|
||||
"recipient_required": "Norādiet vismaz vienu saņēmēju",
|
||||
"subject_required": "Norādiet tematu",
|
||||
"body_required": "Ierakstiet ziņojumu vai pievienojiet failu",
|
||||
"attachments_uploading": "Pielikumi joprojām tiek augšupielādēti — nosūtīs, tiklīdz tas pabeigts",
|
||||
"attachment_upload_failed": "Nav nosūtīts - pielikumu neizdevās augšupielādēt. Noņemiet to un mēģiniet vēlreiz."
|
||||
@@ -663,6 +662,13 @@
|
||||
"send_anyway": "Sūtīt tik un tā",
|
||||
"back": "Atpakaļ pie rediģēšanas"
|
||||
},
|
||||
"empty_subject": {
|
||||
"title": "Sūtīt bez temata?",
|
||||
"message": "Šai ziņai nav temata. Vai tomēr sūtīt?",
|
||||
"dont_ask_again": "Vairs nejautāt",
|
||||
"send_anyway": "Sūtīt tik un tā",
|
||||
"back": "Atpakaļ pie rediģēšanas"
|
||||
},
|
||||
"add_link": "Pievienot saiti",
|
||||
"link_url_prompt": "Ievadiet URL",
|
||||
"sending": "Sūta...",
|
||||
@@ -1318,6 +1324,10 @@
|
||||
"add": "Add",
|
||||
"remove": "Noņemt"
|
||||
},
|
||||
"empty_subject_warning": {
|
||||
"label": "Brīdinājums par tukšu tematu",
|
||||
"description": "Pieprasīt apstiprinājumu pirms ziņas sūtīšanas bez temata."
|
||||
},
|
||||
"hide_inline_image_attachments": {
|
||||
"label": "Slēpt iegultos attēlus no pielikumu saraksta",
|
||||
"description": "Ziņojuma pamattekstā iegultie attēli netiek rādīti kā atsevišķi pielikumi"
|
||||
|
||||
+11
-1
@@ -633,7 +633,6 @@
|
||||
"save_as_template": "Opslaan als sjabloon",
|
||||
"validation": {
|
||||
"recipient_required": "Voeg een ontvanger toe om te verzenden",
|
||||
"subject_required": "Voeg een onderwerp toe",
|
||||
"body_required": "Schrijf een bericht of voeg een bestand toe",
|
||||
"attachments_uploading": "Bijlagen worden nog geüpload — wordt verzonden zodra dit klaar is",
|
||||
"attachment_upload_failed": "Niet verzonden - een bijlage kon niet worden geüpload. Verwijder deze en probeer het opnieuw."
|
||||
@@ -663,6 +662,13 @@
|
||||
"send_anyway": "Toch verzenden",
|
||||
"back": "Terug naar bewerken"
|
||||
},
|
||||
"empty_subject": {
|
||||
"title": "Verzenden zonder onderwerp?",
|
||||
"message": "Dit bericht heeft geen onderwerp. Toch verzenden?",
|
||||
"dont_ask_again": "Niet meer vragen",
|
||||
"send_anyway": "Toch verzenden",
|
||||
"back": "Terug naar bewerken"
|
||||
},
|
||||
"add_link": "Link toevoegen",
|
||||
"link_url_prompt": "Voer de URL in",
|
||||
"sending": "Bezig met verzenden...",
|
||||
@@ -1318,6 +1324,10 @@
|
||||
"add": "Add",
|
||||
"remove": "Verwijderen"
|
||||
},
|
||||
"empty_subject_warning": {
|
||||
"label": "Waarschuwing bij leeg onderwerp",
|
||||
"description": "Vraag om bevestiging voordat een bericht zonder onderwerp wordt verzonden."
|
||||
},
|
||||
"hide_inline_image_attachments": {
|
||||
"label": "Inline-afbeeldingen verbergen uit bijlagenlijst",
|
||||
"description": "Afbeeldingen die in de berichttekst zijn ingesloten worden niet als aparte bijlagen weergegeven"
|
||||
|
||||
+11
-1
@@ -633,7 +633,6 @@
|
||||
"save_as_template": "Zapisz jako szablon",
|
||||
"validation": {
|
||||
"recipient_required": "Dodaj odbiorcę, aby wysłać",
|
||||
"subject_required": "Dodaj temat",
|
||||
"body_required": "Napisz wiadomość lub załącz plik",
|
||||
"attachments_uploading": "Załączniki wciąż się przesyłają — wiadomość zostanie wysłana po zakończeniu",
|
||||
"attachment_upload_failed": "Nie wysłano - nie udało się przesłać załącznika. Usuń go i spróbuj ponownie."
|
||||
@@ -663,6 +662,13 @@
|
||||
"send_anyway": "Wyślij mimo to",
|
||||
"back": "Wróć do edycji"
|
||||
},
|
||||
"empty_subject": {
|
||||
"title": "Wysłać bez tematu?",
|
||||
"message": "Ta wiadomość nie ma tematu. Wysłać mimo to?",
|
||||
"dont_ask_again": "Nie pytaj ponownie",
|
||||
"send_anyway": "Wyślij mimo to",
|
||||
"back": "Wróć do edycji"
|
||||
},
|
||||
"add_link": "Dodaj link",
|
||||
"link_url_prompt": "Wprowadź adres URL",
|
||||
"sending": "Wysyłanie...",
|
||||
@@ -1318,6 +1324,10 @@
|
||||
"add": "Add",
|
||||
"remove": "Usuń"
|
||||
},
|
||||
"empty_subject_warning": {
|
||||
"label": "Ostrzeżenie o pustym temacie",
|
||||
"description": "Poproś o potwierdzenie przed wysłaniem wiadomości bez tematu."
|
||||
},
|
||||
"hide_inline_image_attachments": {
|
||||
"label": "Ukryj obrazy osadzone z listy załączników",
|
||||
"description": "Obrazy osadzone w treści wiadomości nie są wyświetlane jako osobne załączniki"
|
||||
|
||||
+11
-1
@@ -633,7 +633,6 @@
|
||||
"save_as_template": "Salvar como modelo",
|
||||
"validation": {
|
||||
"recipient_required": "Adicione um destinatário para enviar",
|
||||
"subject_required": "Adicione um assunto",
|
||||
"body_required": "Escreva uma mensagem ou anexe um arquivo",
|
||||
"attachments_uploading": "Os anexos ainda estão sendo enviados — será enviado assim que terminar",
|
||||
"attachment_upload_failed": "Não enviado - falha ao carregar um anexo. Remova-o e tente novamente."
|
||||
@@ -663,6 +662,13 @@
|
||||
"send_anyway": "Enviar mesmo assim",
|
||||
"back": "Voltar à edição"
|
||||
},
|
||||
"empty_subject": {
|
||||
"title": "Enviar sem assunto?",
|
||||
"message": "Esta mensagem não tem assunto. Enviar mesmo assim?",
|
||||
"dont_ask_again": "Não perguntar novamente",
|
||||
"send_anyway": "Enviar mesmo assim",
|
||||
"back": "Voltar à edição"
|
||||
},
|
||||
"add_link": "Adicionar link",
|
||||
"link_url_prompt": "Insira a URL",
|
||||
"sending": "Enviando...",
|
||||
@@ -1318,6 +1324,10 @@
|
||||
"add": "Add",
|
||||
"remove": "Remover"
|
||||
},
|
||||
"empty_subject_warning": {
|
||||
"label": "Aviso de assunto vazio",
|
||||
"description": "Pedir confirmação antes de enviar uma mensagem sem assunto."
|
||||
},
|
||||
"hide_inline_image_attachments": {
|
||||
"label": "Ocultar imagens incorporadas dos anexos",
|
||||
"description": "As imagens incorporadas no corpo da mensagem não são listadas como anexos separados"
|
||||
|
||||
+11
-1
@@ -636,7 +636,6 @@
|
||||
"save_as_template": "Salvează ca șablon",
|
||||
"validation": {
|
||||
"recipient_required": "Adăugați un destinatar căruia să îi trimiteți mesajul",
|
||||
"subject_required": "Adăugați un subiect",
|
||||
"body_required": "Scrieți un mesaj sau atașați un fișier",
|
||||
"attachments_uploading": "Atașamentele încă se încarcă — se va trimite imediat ce se termină",
|
||||
"attachment_upload_failed": "Netrimis - încărcarea unui atașament a eșuat. Eliminați-l și încercați din nou."
|
||||
@@ -666,6 +665,13 @@
|
||||
"send_anyway": "Trimite oricum",
|
||||
"back": "Înapoi la editare"
|
||||
},
|
||||
"empty_subject": {
|
||||
"title": "Trimiteți fără subiect?",
|
||||
"message": "Acest mesaj nu are subiect. Îl trimiteți oricum?",
|
||||
"dont_ask_again": "Nu mai întreba",
|
||||
"send_anyway": "Trimite oricum",
|
||||
"back": "Înapoi la editare"
|
||||
},
|
||||
"schedule_send": "Programează trimiterea",
|
||||
"schedule_send_description": "Alegeți momentul în care serverul ar trebui să transmită acest mesaj.",
|
||||
"schedule_send_required": "Alegeți o dată și o oră.",
|
||||
@@ -1321,6 +1327,10 @@
|
||||
"add": "Adăugați",
|
||||
"remove": "Eliminați"
|
||||
},
|
||||
"empty_subject_warning": {
|
||||
"label": "Avertisment pentru subiect gol",
|
||||
"description": "Cere confirmare înainte de a trimite un mesaj fără subiect."
|
||||
},
|
||||
"hide_inline_image_attachments": {
|
||||
"label": "Ascundeți imaginile încorporate din atașamente",
|
||||
"description": "Imaginile încorporate în corpul mesajului nu sunt listate ca atașamente separate"
|
||||
|
||||
+11
-1
@@ -633,7 +633,6 @@
|
||||
"save_as_template": "Сохранить как шаблон",
|
||||
"validation": {
|
||||
"recipient_required": "Укажите получателя для отправки",
|
||||
"subject_required": "Укажите тему",
|
||||
"body_required": "Напишите сообщение или прикрепите файл",
|
||||
"attachments_uploading": "Вложения ещё загружаются — отправка начнётся, как только загрузка завершится",
|
||||
"attachment_upload_failed": "Не отправлено — не удалось загрузить вложение. Удалите его и попробуйте снова."
|
||||
@@ -663,6 +662,13 @@
|
||||
"send_anyway": "Отправить всё равно",
|
||||
"back": "Вернуться к редактированию"
|
||||
},
|
||||
"empty_subject": {
|
||||
"title": "Отправить без темы?",
|
||||
"message": "У этого письма нет темы. Всё равно отправить?",
|
||||
"dont_ask_again": "Больше не спрашивать",
|
||||
"send_anyway": "Отправить всё равно",
|
||||
"back": "Вернуться к редактированию"
|
||||
},
|
||||
"add_link": "Добавить ссылку",
|
||||
"link_url_prompt": "Введите URL",
|
||||
"sending": "Отправка...",
|
||||
@@ -1318,6 +1324,10 @@
|
||||
"add": "Add",
|
||||
"remove": "Удалить"
|
||||
},
|
||||
"empty_subject_warning": {
|
||||
"label": "Предупреждение о пустой теме",
|
||||
"description": "Запрашивать подтверждение перед отправкой письма без темы."
|
||||
},
|
||||
"hide_inline_image_attachments": {
|
||||
"label": "Скрывать встроенные изображения из вложений",
|
||||
"description": "Изображения, встроенные в тело сообщения, не отображаются как отдельные вложения"
|
||||
|
||||
+11
-1
@@ -636,7 +636,6 @@
|
||||
"save_as_template": "Uložiť ako šablónu",
|
||||
"validation": {
|
||||
"recipient_required": "Pridajte príjemcu na odoslanie",
|
||||
"subject_required": "Pridajte predmet",
|
||||
"body_required": "Napíšte správu alebo priložte súbor",
|
||||
"attachments_uploading": "Prílohy sa stále nahrávajú - odošle sa hneď po ich dokončení",
|
||||
"attachment_upload_failed": "Neodoslané - prílohu sa nepodarilo nahrať. Odstráňte ju a skúste to znova."
|
||||
@@ -666,6 +665,13 @@
|
||||
"send_anyway": "Odoslať aj tak",
|
||||
"back": "Späť na úpravu"
|
||||
},
|
||||
"empty_subject": {
|
||||
"title": "Odoslať bez predmetu?",
|
||||
"message": "Táto správa nemá predmet. Odoslať aj tak?",
|
||||
"dont_ask_again": "Znova sa nepýtať",
|
||||
"send_anyway": "Odoslať aj tak",
|
||||
"back": "Späť na úpravu"
|
||||
},
|
||||
"schedule_send": "Naplánovať odoslanie",
|
||||
"schedule_send_description": "Vyberte, kedy má server túto správu odoslať.",
|
||||
"schedule_send_required": "Vyberte dátum a čas.",
|
||||
@@ -1321,6 +1327,10 @@
|
||||
"add": "Pridať",
|
||||
"remove": "Odstrániť"
|
||||
},
|
||||
"empty_subject_warning": {
|
||||
"label": "Upozornenie na prázdny predmet",
|
||||
"description": "Pred odoslaním správy bez predmetu požiadať o potvrdenie."
|
||||
},
|
||||
"hide_inline_image_attachments": {
|
||||
"label": "Skryť vložené obrázky zo zoznamu príloh",
|
||||
"description": "Obrázky vložené priamo do textu správy sa nezobrazia ako samostatné prílohy"
|
||||
|
||||
+11
-1
@@ -636,7 +636,6 @@
|
||||
"save_as_template": "Şablon Olarak Kaydet",
|
||||
"validation": {
|
||||
"recipient_required": "Göndermek için bir alıcı ekleyin",
|
||||
"subject_required": "Bir konu ekleyin",
|
||||
"body_required": "Bir ileti yazın veya dosya ekleyin",
|
||||
"attachments_uploading": "Ekler hâlâ yükleniyor — yükleme bitince gönderilecek",
|
||||
"attachment_upload_failed": "Gönderilmedi - bir ek yüklenemedi. Eki kaldırıp tekrar deneyin."
|
||||
@@ -666,6 +665,13 @@
|
||||
"send_anyway": "Yine de gönder",
|
||||
"back": "Düzenlemeye dön"
|
||||
},
|
||||
"empty_subject": {
|
||||
"title": "Konu olmadan gönderilsin mi?",
|
||||
"message": "Bu iletinin konusu yok. Yine de gönderilsin mi?",
|
||||
"dont_ask_again": "Bir daha sorma",
|
||||
"send_anyway": "Yine de gönder",
|
||||
"back": "Düzenlemeye dön"
|
||||
},
|
||||
"schedule_send": "Göndermeyi zamanla",
|
||||
"schedule_send_description": "Sunucunun bu iletiyi ne zaman göndereceğini seçin.",
|
||||
"schedule_send_required": "Bir tarih ve saat seçin.",
|
||||
@@ -1318,6 +1324,10 @@
|
||||
"add": "Ekle",
|
||||
"remove": "Kaldır"
|
||||
},
|
||||
"empty_subject_warning": {
|
||||
"label": "Boş konu uyarısı",
|
||||
"description": "Konusu olmayan bir ileti gönderilmeden önce onay iste."
|
||||
},
|
||||
"hide_inline_image_attachments": {
|
||||
"label": "Satır içi görüntüleri eklerden gizle",
|
||||
"description": "İleti gövdesine gömülü görseller ayrı ek olarak listelenmez"
|
||||
|
||||
+11
-1
@@ -633,7 +633,6 @@
|
||||
"save_as_template": "Зберегти як шаблон",
|
||||
"validation": {
|
||||
"recipient_required": "Додайте одержувача для надсилання",
|
||||
"subject_required": "Додайте тему",
|
||||
"body_required": "Напишіть повідомлення або прикріпіть файл",
|
||||
"attachments_uploading": "Вкладення ще завантажуються — надсилання почнеться після завершення",
|
||||
"attachment_upload_failed": "Не надіслано — не вдалося завантажити вкладення. Видаліть його та спробуйте ще раз."
|
||||
@@ -663,6 +662,13 @@
|
||||
"send_anyway": "Все одно надішліть",
|
||||
"back": "Назад до редагування"
|
||||
},
|
||||
"empty_subject": {
|
||||
"title": "Надіслати без теми?",
|
||||
"message": "Цей лист не має теми. Надіслати попри це?",
|
||||
"dont_ask_again": "Більше не запитувати",
|
||||
"send_anyway": "Все одно надішліть",
|
||||
"back": "Назад до редагування"
|
||||
},
|
||||
"add_link": "Додати посилання",
|
||||
"link_url_prompt": "Введіть URL",
|
||||
"sending": "Надсилання...",
|
||||
@@ -1318,6 +1324,10 @@
|
||||
"add": "додати",
|
||||
"remove": "видалити"
|
||||
},
|
||||
"empty_subject_warning": {
|
||||
"label": "Попередження про порожню тему",
|
||||
"description": "Запитувати підтвердження перед надсиланням листа без теми."
|
||||
},
|
||||
"hide_inline_image_attachments": {
|
||||
"label": "Приховувати вбудовані зображення з вкладень",
|
||||
"description": "Зображення, вбудовані в тіло повідомлення, не відображаються як окремі вкладення"
|
||||
|
||||
+11
-1
@@ -633,7 +633,6 @@
|
||||
"save_as_template": "保存为模板",
|
||||
"validation": {
|
||||
"recipient_required": "请添加收件人",
|
||||
"subject_required": "请添加主题",
|
||||
"body_required": "请输入邮件内容或添加附件",
|
||||
"attachments_uploading": "附件仍在上传中,上传完成后将自动发送",
|
||||
"attachment_upload_failed": "未发送 - 附件上传失败。请移除该附件后重试。"
|
||||
@@ -663,6 +662,13 @@
|
||||
"send_anyway": "仍然发送",
|
||||
"back": "返回编辑"
|
||||
},
|
||||
"empty_subject": {
|
||||
"title": "没有主题就发送吗?",
|
||||
"message": "此邮件没有主题。仍要发送吗?",
|
||||
"dont_ask_again": "不再提示",
|
||||
"send_anyway": "仍然发送",
|
||||
"back": "返回编辑"
|
||||
},
|
||||
"add_link": "添加链接",
|
||||
"link_url_prompt": "输入 URL",
|
||||
"sending": "发送中...",
|
||||
@@ -1318,6 +1324,10 @@
|
||||
"add": "Add",
|
||||
"remove": "删除"
|
||||
},
|
||||
"empty_subject_warning": {
|
||||
"label": "空主题提醒",
|
||||
"description": "发送没有主题的邮件前请求确认。"
|
||||
},
|
||||
"hide_inline_image_attachments": {
|
||||
"label": "在附件列表中隐藏内嵌图片",
|
||||
"description": "嵌入到邮件正文中的图片不会作为单独的附件显示"
|
||||
|
||||
@@ -376,6 +376,9 @@ interface SettingsState {
|
||||
attachmentReminderEnabled: boolean;
|
||||
attachmentReminderKeywords: string[];
|
||||
|
||||
// Ask for confirmation when sending a message with an empty subject
|
||||
emptySubjectWarningEnabled: boolean;
|
||||
|
||||
// Hide inline images (images referenced by cid in the HTML body) from the
|
||||
// attachment list shown above the message body.
|
||||
hideInlineImageAttachments: boolean;
|
||||
@@ -602,6 +605,8 @@ const DEFAULT_SETTINGS = {
|
||||
'pielikumā',
|
||||
] as string[],
|
||||
|
||||
emptySubjectWarningEnabled: true,
|
||||
|
||||
hideInlineImageAttachments: true,
|
||||
attachmentImagePreviewsEnabled: true,
|
||||
|
||||
@@ -749,6 +754,7 @@ export const useSettingsStore = create<SettingsState>()(
|
||||
nestedTags: state.nestedTags,
|
||||
attachmentReminderEnabled: state.attachmentReminderEnabled,
|
||||
attachmentReminderKeywords: state.attachmentReminderKeywords,
|
||||
emptySubjectWarningEnabled: state.emptySubjectWarningEnabled,
|
||||
hideInlineImageAttachments: state.hideInlineImageAttachments,
|
||||
attachmentImagePreviewsEnabled: state.attachmentImagePreviewsEnabled,
|
||||
sidebarApps: state.sidebarApps,
|
||||
|
||||
Reference in New Issue
Block a user