Merge pull request #1370 from maxhaomh/feat/disable-quick-reply

This commit is contained in:
Linus Rath
2026-10-26 19:24:49 +01:00
6 changed files with 35 additions and 1 deletions
@@ -76,6 +76,7 @@ function makeEmail(overrides: Record<string, unknown> = {}): Email {
let toastError: ReturnType<typeof vi.spyOn>;
beforeEach(() => {
useSettingsStore.setState({ quickReplyEnabled: true });
toastError = vi.spyOn(toast, 'error').mockImplementation(() => {});
vi.spyOn(console, 'error').mockImplementation(() => {});
});
@@ -85,6 +86,15 @@ afterEach(() => {
});
describe('viewer send failures', () => {
it('hides quick reply when disabled and restores it when enabled', () => {
useSettingsStore.setState({ quickReplyEnabled: false });
render(<EmailViewer email={makeEmail()} mailboxes={mailboxes} selectedMailbox="inbox" currentMailboxRole="inbox" />);
expect(screen.queryByPlaceholderText('quick_reply_placeholder')).not.toBeInTheDocument();
act(() => useSettingsStore.getState().updateSetting('quickReplyEnabled', true));
expect(screen.getByPlaceholderText('quick_reply_placeholder')).toBeInTheDocument();
});
it('reports a failed quick reply and keeps the text on Stalwart 1.0', async () => {
const onQuickReply = vi.fn().mockRejectedValue(new SubmissionQuotaError(QUOTA, true));
render(
+2 -1
View File
@@ -1084,6 +1084,7 @@ export function EmailViewer({
const tContextMenu = useTranslations('context_menu');
const copyLink = useCopyLink();
const tWelcome = useTranslations('welcome');
const quickReplyEnabled = useSettingsStore((state) => state.quickReplyEnabled);
const externalContentPolicy = useSettingsStore((state) => state.externalContentPolicy);
const messageSpacing = useSettingsStore((state) => state.messageSpacing);
const plainTextFont = useSettingsStore((state) => state.plainTextFont);
@@ -6380,7 +6381,7 @@ export function EmailViewer({
)}
{/* Quick Reply Section - hidden for drafts and while loading a new email */}
{!mvReplyRow && !isDraft && !isScheduled && !isBodyLoading && (effectiveEmailContent.isHtml ? iframeReady : true) && (<div className="bg-background border-t border-border px-6 mt-auto" style={{ paddingBlock: 'var(--density-header-py)' }}>
{quickReplyEnabled && !mvReplyRow && !isDraft && !isScheduled && !isBodyLoading && (effectiveEmailContent.isHtml ? iframeReady : true) && (<div className="bg-background border-t border-border px-6 mt-auto" style={{ paddingBlock: 'var(--density-header-py)' }}>
<div className="flex items-start" style={{ gap: 'var(--density-item-gap)' }}>
<div className="flex-shrink-0">
<Avatar
@@ -23,6 +23,7 @@ export function ComposingSettings() {
const [newKeyword, setNewKeyword] = useState('');
const {
quickReplyEnabled,
autoSelectReplyIdentity,
replyIdentityMatch,
plainTextMode,
@@ -84,6 +85,13 @@ export function ComposingSettings() {
</SettingsSection>
<SettingsSection id="replying" title={tPage('sections.replying')}>
<SettingItem label={t('quick_reply.label')} description={t('quick_reply.description')}>
<ToggleSwitch
checked={quickReplyEnabled}
onChange={(checked) => updateSetting('quickReplyEnabled', checked)}
/>
</SettingItem>
<SettingItem label={t('auto_select_reply_identity.label')} description={t('auto_select_reply_identity.description')}>
<ToggleSwitch
checked={autoSelectReplyIdentity}
+4
View File
@@ -1536,6 +1536,10 @@
"label": "Plain Text Only",
"description": "Disable the rich text editor and send all emails as plain text only, including replies and forwards"
},
"quick_reply": {
"label": "Quick Reply",
"description": "Show the quick reply box below messages."
},
"auto_select_reply_identity": {
"label": "Reply From Received Address",
"description": "When replying, send from the address the message was originally sent to. Your configured identities always match; whether other addresses on your domains count too (catch-all) is set by “Received Address Matching” below."
@@ -6,6 +6,14 @@ describe('settings-store attachment action', () => {
useSettingsStore.getState().resetToDefaults();
});
it('enables quick reply by default and exports the disabled preference', () => {
expect(useSettingsStore.getState().quickReplyEnabled).toBe(true);
useSettingsStore.getState().updateSetting('quickReplyEnabled', false);
expect(JSON.parse(useSettingsStore.getState().exportSettings()).quickReplyEnabled).toBe(false);
useSettingsStore.getState().resetToDefaults();
expect(useSettingsStore.getState().quickReplyEnabled).toBe(true);
});
it('defaults to preview when settings are reset', () => {
expect(useSettingsStore.getState().mailAttachmentAction).toBe('preview');
});
+3
View File
@@ -528,6 +528,7 @@ interface SettingsState {
// Composer
autoSaveDraftInterval: number; // milliseconds
sendConfirmation: boolean;
quickReplyEnabled: boolean;
defaultReplyMode: ReplyMode;
autoSelectReplyIdentity: boolean;
replyIdentityMatch: ReplyIdentityMatch; // With autoSelectReplyIdentity on: 'exact' = configured identities only, 'domain' = also same-domain catch-all addresses (rewrites From) #1000
@@ -836,6 +837,7 @@ const DEFAULT_SETTINGS = {
// Composer
autoSaveDraftInterval: 60000, // 1 minute
sendConfirmation: false,
quickReplyEnabled: true,
defaultReplyMode: 'reply' as ReplyMode,
autoSelectReplyIdentity: false,
replyIdentityMatch: 'domain' as ReplyIdentityMatch,
@@ -1202,6 +1204,7 @@ export const useSettingsStore = create<SettingsState>()(
trustedSenders: state.trustedSenders,
autoSaveDraftInterval: state.autoSaveDraftInterval,
sendConfirmation: state.sendConfirmation,
quickReplyEnabled: state.quickReplyEnabled,
defaultReplyMode: state.defaultReplyMode,
autoSelectReplyIdentity: state.autoSelectReplyIdentity,
replyIdentityMatch: state.replyIdentityMatch,