feat: store trusted senders in a dedicated JMAP address book #176
This commit is contained in:
@@ -2,9 +2,11 @@
|
||||
|
||||
import { useState, useEffect, useRef, useMemo } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { X, ShieldCheck, Search, Trash2, Plus } from "lucide-react";
|
||||
import { X, ShieldCheck, Search, Trash2, Plus, Loader2 } from "lucide-react";
|
||||
import { Avatar } from "@/components/ui/avatar";
|
||||
import { useSettingsStore } from "@/stores/settings-store";
|
||||
import { useContactStore } from "@/stores/contact-store";
|
||||
import { useAuthStore } from "@/stores/auth-store";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface TrustedSendersModalProps {
|
||||
@@ -17,22 +19,43 @@ export function TrustedSendersModal({ isOpen, onClose }: TrustedSendersModalProp
|
||||
const modalRef = useRef<HTMLDivElement>(null);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const { trustedSenders, addTrustedSender, removeTrustedSender } = useSettingsStore();
|
||||
const { trustedSenders, addTrustedSender, removeTrustedSender, trustedSendersAddressBook } = useSettingsStore();
|
||||
const {
|
||||
trustedSenderEmails,
|
||||
trustedSendersLoaded,
|
||||
trustedSendersLoading,
|
||||
loadTrustedSendersBook,
|
||||
addToTrustedSendersBook,
|
||||
removeFromTrustedSendersBook,
|
||||
} = useContactStore();
|
||||
const { client } = useAuthStore();
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [isAdding, setIsAdding] = useState(false);
|
||||
const [newEmail, setNewEmail] = useState("");
|
||||
const [emailError, setEmailError] = useState("");
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
// When address book mode is on, load the book on first open
|
||||
useEffect(() => {
|
||||
if (isOpen && trustedSendersAddressBook && client && !trustedSendersLoaded) {
|
||||
loadTrustedSendersBook(client);
|
||||
}
|
||||
}, [isOpen, trustedSendersAddressBook, client, trustedSendersLoaded, loadTrustedSendersBook]);
|
||||
|
||||
// The active list depends on mode
|
||||
const activeSenders = trustedSendersAddressBook ? trustedSenderEmails : trustedSenders;
|
||||
const isLoading = trustedSendersAddressBook && (!trustedSendersLoaded || trustedSendersLoading);
|
||||
|
||||
// Filter senders based on search query
|
||||
const filteredSenders = useMemo(() => {
|
||||
if (!searchQuery.trim()) return trustedSenders;
|
||||
if (!searchQuery.trim()) return activeSenders;
|
||||
const query = searchQuery.toLowerCase();
|
||||
return trustedSenders.filter((email) => email.toLowerCase().includes(query));
|
||||
}, [trustedSenders, searchQuery]);
|
||||
return activeSenders.filter((email) => email.toLowerCase().includes(query));
|
||||
}, [activeSenders, searchQuery]);
|
||||
|
||||
// Show search only when 5+ senders
|
||||
const showSearch = trustedSenders.length >= 5;
|
||||
const showSearch = activeSenders.length >= 5;
|
||||
|
||||
// Close on Escape key
|
||||
useEffect(() => {
|
||||
@@ -90,7 +113,7 @@ export function TrustedSendersModal({ isOpen, onClose }: TrustedSendersModalProp
|
||||
return emailRegex.test(email);
|
||||
};
|
||||
|
||||
const handleAddSender = () => {
|
||||
const handleAddSender = async () => {
|
||||
const trimmedEmail = newEmail.trim().toLowerCase();
|
||||
|
||||
if (!trimmedEmail) {
|
||||
@@ -103,15 +126,34 @@ export function TrustedSendersModal({ isOpen, onClose }: TrustedSendersModalProp
|
||||
return;
|
||||
}
|
||||
|
||||
if (trustedSenders.includes(trimmedEmail)) {
|
||||
if (activeSenders.includes(trimmedEmail)) {
|
||||
setEmailError(t("already_added"));
|
||||
return;
|
||||
}
|
||||
|
||||
addTrustedSender(trimmedEmail);
|
||||
setNewEmail("");
|
||||
setIsAdding(false);
|
||||
setEmailError("");
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
if (trustedSendersAddressBook && client) {
|
||||
await addToTrustedSendersBook(client, trimmedEmail);
|
||||
} else {
|
||||
addTrustedSender(trimmedEmail);
|
||||
}
|
||||
setNewEmail("");
|
||||
setIsAdding(false);
|
||||
setEmailError("");
|
||||
} catch {
|
||||
setEmailError(t("save_error"));
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveSender = async (email: string) => {
|
||||
if (trustedSendersAddressBook && client) {
|
||||
await removeFromTrustedSendersBook(client, email);
|
||||
} else {
|
||||
removeTrustedSender(email);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
@@ -170,7 +212,11 @@ export function TrustedSendersModal({ isOpen, onClose }: TrustedSendersModalProp
|
||||
|
||||
{/* Content */}
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{trustedSenders.length === 0 ? (
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Loader2 className="w-6 h-6 animate-spin text-muted-foreground" />
|
||||
</div>
|
||||
) : activeSenders.length === 0 ? (
|
||||
/* Empty State */
|
||||
<div className="flex flex-col items-center justify-center py-12 px-6 text-center">
|
||||
<ShieldCheck className="w-12 h-12 text-muted-foreground/50 mb-4" />
|
||||
@@ -209,7 +255,7 @@ export function TrustedSendersModal({ isOpen, onClose }: TrustedSendersModalProp
|
||||
{email}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => removeTrustedSender(email)}
|
||||
onClick={() => handleRemoveSender(email)}
|
||||
className="p-1.5 rounded-md text-muted-foreground hover:text-destructive hover:bg-destructive/10 transition-colors opacity-0 group-hover:opacity-100 focus:opacity-100"
|
||||
aria-label={`${t("remove")} ${email}`}
|
||||
>
|
||||
@@ -222,7 +268,7 @@ export function TrustedSendersModal({ isOpen, onClose }: TrustedSendersModalProp
|
||||
</div>
|
||||
|
||||
{/* Footer - Add sender */}
|
||||
{trustedSenders.length > 0 && (
|
||||
{!isLoading && activeSenders.length > 0 && (
|
||||
<div className="px-6 py-4 border-t border-border flex-shrink-0">
|
||||
{isAdding ? (
|
||||
<div className="space-y-2">
|
||||
@@ -244,9 +290,10 @@ export function TrustedSendersModal({ isOpen, onClose }: TrustedSendersModalProp
|
||||
/>
|
||||
<button
|
||||
onClick={handleAddSender}
|
||||
className="px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90 transition-colors text-sm font-medium"
|
||||
disabled={isSubmitting}
|
||||
className="px-4 py-2 bg-primary text-primary-foreground rounded-md hover:bg-primary/90 transition-colors text-sm font-medium disabled:opacity-50"
|
||||
>
|
||||
{t("add_button")}
|
||||
{isSubmitting ? <Loader2 className="w-4 h-4 animate-spin" /> : t("add_button")}
|
||||
</button>
|
||||
</div>
|
||||
{emailError && (
|
||||
|
||||
Reference in New Issue
Block a user