From 11c8c8e1a8f3feec74c2c435be73f4e21b424e9a Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Wed, 28 Oct 2026 03:17:33 +0100 Subject: [PATCH] fix: let "Add sender manually" open the address field when there are no trusted senders yet --- .../__tests__/trusted-senders-modal.test.tsx | 27 ++ components/trusted-senders-modal.tsx | 311 +++++++----------- 2 files changed, 147 insertions(+), 191 deletions(-) create mode 100644 components/__tests__/trusted-senders-modal.test.tsx diff --git a/components/__tests__/trusted-senders-modal.test.tsx b/components/__tests__/trusted-senders-modal.test.tsx new file mode 100644 index 000000000..39293462a --- /dev/null +++ b/components/__tests__/trusted-senders-modal.test.tsx @@ -0,0 +1,27 @@ +import { fireEvent, render, screen } from '@testing-library/react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { useSettingsStore } from '@/stores/settings-store'; +import { TrustedSendersModal } from '../trusted-senders-modal'; + +beforeEach(() => { + useSettingsStore.setState({ trustedSenders: [], trustedSendersAddressBook: false } as never); +}); + +describe('trusted senders', () => { + it('offers an address field when the list is empty', () => { + render( {}} />); + fireEvent.click(screen.getByRole('button', { name: 'add_manually' })); + const field = screen.getByRole('textbox', { name: 'add_placeholder' }); + fireEvent.change(field, { target: { value: 'News@Example.org' } }); + fireEvent.keyDown(field, { key: 'Enter' }); + expect(useSettingsStore.getState().trustedSenders).toEqual(['news@example.org']); + }); + + it('closes on the first Escape after "Add sender manually"', () => { + const onClose = vi.fn(); + render(); + fireEvent.click(screen.getByRole('button', { name: 'add_manually' })); + fireEvent.keyDown(document.activeElement ?? document.body, { key: 'Escape' }); + expect(onClose).toHaveBeenCalledTimes(1); + }); +}); diff --git a/components/trusted-senders-modal.tsx b/components/trusted-senders-modal.tsx index 253cd4226..1b1579e16 100644 --- a/components/trusted-senders-modal.tsx +++ b/components/trusted-senders-modal.tsx @@ -2,8 +2,13 @@ import { useState, useEffect, useRef, useMemo } from "react"; import { useTranslations } from "next-intl"; -import { X, ShieldCheck, Search, Trash2, Plus, Loader2 } from "@/components/icons"; +import { ShieldCheck, Search, Trash2, Plus } from "@/components/icons"; import { Avatar } from "@/components/ui/avatar"; +import { Button } from "@/components/ui/button"; +import { Dialog } from "@/components/ui/dialog"; +import { EmptyState } from "@/components/ui/empty-state"; +import { IconButton } from "@/components/ui/icon-button"; +import { PaneSpinner, Spinner } from "@/components/ui/spinner"; import { useSettingsStore } from "@/stores/settings-store"; import { useContactStore } from "@/stores/contact-store"; import { useAuthStore } from "@/stores/auth-store"; @@ -16,7 +21,6 @@ interface TrustedSendersModalProps { export function TrustedSendersModal({ isOpen, onClose }: TrustedSendersModalProps) { const t = useTranslations("settings.email_behavior.trusted_senders"); - const modalRef = useRef(null); const inputRef = useRef(null); const { trustedSenders, addTrustedSender, removeTrustedSender, trustedSendersAddressBook } = useSettingsStore(); @@ -57,40 +61,6 @@ export function TrustedSendersModal({ isOpen, onClose }: TrustedSendersModalProp // Show search only when 5+ senders const showSearch = activeSenders.length >= 5; - // Close on Escape key - useEffect(() => { - const handleKeyDown = (e: KeyboardEvent) => { - if (e.key === "Escape") { - if (isAdding) { - setIsAdding(false); - setNewEmail(""); - setEmailError(""); - } else { - onClose(); - } - } - }; - - if (isOpen) { - window.addEventListener("keydown", handleKeyDown); - return () => window.removeEventListener("keydown", handleKeyDown); - } - }, [isOpen, isAdding, onClose]); - - // Close on click outside - useEffect(() => { - const handleClickOutside = (e: MouseEvent) => { - if (modalRef.current && !modalRef.current.contains(e.target as Node)) { - onClose(); - } - }; - - if (isOpen) { - document.addEventListener("mousedown", handleClickOutside); - return () => document.removeEventListener("mousedown", handleClickOutside); - } - }, [isOpen, onClose]); - // Focus input when adding mode is enabled useEffect(() => { if (isAdding && inputRef.current) { @@ -113,6 +83,14 @@ export function TrustedSendersModal({ isOpen, onClose }: TrustedSendersModalProp return emailRegex.test(email); }; + const addSender = async (email: string) => { + if (trustedSendersAddressBook && client) { + await addToTrustedSendersBook(client, email); + } else { + addTrustedSender(email); + } + }; + const handleAddSender = async () => { const trimmedEmail = newEmail.trim().toLowerCase(); @@ -133,11 +111,7 @@ export function TrustedSendersModal({ isOpen, onClose }: TrustedSendersModalProp setIsSubmitting(true); try { - if (trustedSendersAddressBook && client) { - await addToTrustedSendersBook(client, trimmedEmail); - } else { - addTrustedSender(trimmedEmail); - } + await addSender(trimmedEmail); setNewEmail(""); setIsAdding(false); setEmailError(""); @@ -156,162 +130,117 @@ export function TrustedSendersModal({ isOpen, onClose }: TrustedSendersModalProp } }; - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === "Enter") { - handleAddSender(); - } - }; + const addRow = isAdding ? ( +
+
+ { + setNewEmail(e.target.value); + setEmailError(""); + }} + onKeyDown={(e) => { + if (e.key === "Enter") { + e.preventDefault(); + void handleAddSender(); + } + }} + className={cn( + "flex-1 min-w-0 px-3 py-2 text-sm bg-background border rounded-md focus:outline-none focus:ring-2 focus:ring-primary/50", + emailError ? "border-destructive" : "border-border" + )} + /> + +
+ {emailError && ( +

{emailError}

+ )} +
+ ) : ( + + ); - if (!isOpen) return null; + const isEmpty = !isLoading && activeSenders.length === 0; return ( -
-
- {/* Header */} -
-
- -

- {t("modal_title")} -

+ {addRow}
: undefined} + > + {/* Search (only when 5+ senders) */} + {showSearch && ( +
+
+ + setSearchQuery(e.target.value)} + className="w-full ps-9 pe-3 py-2 text-sm bg-muted border border-border rounded-md focus:outline-none focus:ring-2 focus:ring-primary/50" + />
-
+ )} - {/* Search (only when 5+ senders) */} - {showSearch && ( -
-
- - setSearchQuery(e.target.value)} - className="w-full ps-9 pe-3 py-2 text-sm bg-muted border border-border rounded-md focus:outline-none focus:ring-2 focus:ring-primary/50" - /> -
-
- )} - - {/* Content */} -
- {isLoading ? ( -
- -
- ) : activeSenders.length === 0 ? ( - /* Empty State */ -
- -

- {t("empty_title")} -

-

- {t("empty_description")} -

- + ) : undefined} + /> + ) : filteredSenders.length === 0 ? ( + + ) : ( +
    + {filteredSenders.map((email) => ( +
  • + + + {email} + + void handleRemoveSender(email)} + className="text-muted-foreground hover:text-destructive hover:bg-destructive/10 opacity-0 group-hover:opacity-100 focus-visible:opacity-100 pointer-coarse:opacity-100" > - - {t("add_manually")} - -
- ) : filteredSenders.length === 0 ? ( - /* No search results */ -
- -

- {t("no_results")} -

-
- ) : ( - /* Sender list */ -
- {filteredSenders.map((email) => ( -
- - - {email} - - -
- ))} -
- )} -
- - {/* Footer - Add sender */} - {!isLoading && activeSenders.length > 0 && ( -
- {isAdding ? ( -
-
- { - setNewEmail(e.target.value); - setEmailError(""); - }} - onKeyDown={handleKeyDown} - className={cn( - "flex-1 px-3 py-2 text-sm bg-background border rounded-md focus:outline-none focus:ring-2 focus:ring-primary/50", - emailError ? "border-destructive" : "border-border" - )} - /> - -
- {emailError && ( -

{emailError}

- )} -
- ) : ( - - )} -
- )} -
-
+ + + + ))} + + )} + ); }