All files / src/components/settings TrustedSendersPanel.tsx

100% Statements 20/20
100% Branches 6/6
100% Functions 13/13
100% Lines 18/18

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92            27x 27x 27x   27x 10x 10x       9x     27x 2x     2x 1x     1x   2x     27x                                                     25x                 4x                               2x 1x            
import { useEffect, useState } from "react";
import { api, type TrustedSender } from "../../api";
import { ConfirmDialog } from "../ConfirmDialog";
import { toast } from "../Toast";
 
export function TrustedSendersPanel({ onClose }: { onClose: () => void }) {
	const [senders, setSenders] = useState<TrustedSender[]>([]);
	const [loading, setLoading] = useState(true);
	const [deleteConfirm, setDeleteConfirm] = useState<TrustedSender | null>(null);
 
	useEffect(() => {
		setLoading(true);
		api.trustedSenders
			.list()
			.then(setSenders)
			.catch(() => {})
			.finally(() => setLoading(false));
	}, []);
 
	const handleRemove = (sender: TrustedSender) => {
		api.trustedSenders
			.remove(sender.sender_address)
			.then(() => {
				setSenders((prev) => prev.filter((s) => s.id !== sender.id));
				toast(`Removed ${sender.sender_address} from trusted senders`, "success");
			})
			.catch(() => {
				toast("Failed to remove trusted sender", "error");
			});
		setDeleteConfirm(null);
	};
 
	return (
		<div className="px-4 py-3 border-t border-gray-200 dark:border-gray-700">
			<div className="flex items-center justify-between mb-2">
				<h4 className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
					Trusted Senders
				</h4>
				<button
					type="button"
					onClick={onClose}
					className="text-xs text-gray-400 hover:text-gray-600 dark:hover:text-gray-200"
					aria-label="Close trusted senders"
				>
					Close
				</button>
			</div>
			<p className="text-xs text-gray-500 dark:text-gray-400 mb-3">
				Remote images from these senders are always loaded. Tracking pixels are still blocked.
			</p>
			{loading ? (
				<p className="text-xs text-gray-400 py-2">Loading…</p>
			) : senders.length === 0 ? (
				<p className="text-xs text-gray-400 py-2 text-center">
					No trusted senders yet. Use "Always show from this sender" when viewing a message.
				</p>
			) : (
				<ul className="space-y-1">
					{senders.map((sender) => (
						<li
							key={sender.id}
							className="flex items-center justify-between py-1 px-2 rounded hover:bg-gray-50 dark:hover:bg-gray-800"
						>
							<span className="text-xs text-gray-700 dark:text-gray-300">
								{sender.sender_address}
							</span>
							<button
								type="button"
								onClick={() => setDeleteConfirm(sender)}
								className="text-xs text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300 transition-colors"
								aria-label={`Remove ${sender.sender_address} from trusted senders`}
							>
								Remove
							</button>
						</li>
					))}
				</ul>
			)}
			{deleteConfirm && (
				<ConfirmDialog
					title="Remove trusted sender"
					message={`Remote images from "${deleteConfirm.sender_address}" will be hidden again. You can re-trust them from the message view.`}
					confirmLabel="Remove"
					variant="danger"
					onConfirm={() => handleRemove(deleteConfirm)}
					onCancel={() => setDeleteConfirm(null)}
				/>
			)}
		</div>
	);
}