import { useState } from "react"; import { useMutation, useQueryClient, useSuspenseQuery } from "@tanstack/react-query"; import * as stylex from "@stylexjs/stylex"; import { formatTime } from "@/lib/format"; import InlineError from "@/lib/InlineError"; import { blocklistCreateMutation, blocklistDeleteMutation, blocklistUpdateMutation, blocklistsQuery, blocklistsUpdateNowMutation, } from "@/lib/queries"; import type { Blocklist, BlocklistInput } from "@/lib/types"; import BlocklistForm from "./BlocklistForm"; import { useRefreshStatus } from "./refreshStore"; import SourceStatusSection from "./SourceStatusSection"; import ConfirmDialog from "@/ui/ConfirmDialog"; import { styles as shared } from "@/ui/styles"; import { colors } from "@/ui/tokens.stylex"; import { READ_ONLY_HINT, useReadOnlyConfig } from "@/features/settings/authority"; const styles = stylex.create({ header: { display: "flex", flexWrap: "wrap", alignItems: "center", justifyContent: "space-between", gap: "0.75rem", }, heading: { fontSize: "1.5rem", lineHeight: "2rem", fontWeight: 600, }, done: { marginTop: "0.5rem", fontSize: "0.875rem", lineHeight: "1.25rem", color: colors.primaryOnSurface, }, empty: { marginTop: "1rem", color: colors.textMuted, }, note: { marginTop: "0.5rem", color: colors.textMuted, }, table: { width: "100%", minWidth: "max-content", borderCollapse: "collapse", fontSize: "0.875rem", lineHeight: "1.25rem", }, name: { fontWeight: 500, }, badge: { marginLeft: "0.5rem", borderRadius: "0.25rem", backgroundColor: colors.border, paddingInline: "0.375rem", paddingBlock: "0.125rem", fontSize: "0.75rem", lineHeight: "1rem", color: colors.text, }, url: { display: "block", maxWidth: "18rem", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", }, actions: { display: "flex", gap: "0.75rem", }, dimWhenDisabled: { opacity: { default: 1, ":disabled": 0.5 }, }, }); export default function BlocklistsPage() { const queryClient = useQueryClient(); const { data: blocklists } = useSuspenseQuery(blocklistsQuery()); const [editing, setEditing] = useState(null); const [pendingDelete, setPendingDelete] = useState(null); const create = useMutation(blocklistCreateMutation(queryClient)); const save = useMutation(blocklistUpdateMutation(queryClient)); const toggle = useMutation(blocklistUpdateMutation(queryClient)); const remove = useMutation(blocklistDeleteMutation(queryClient)); const updateNow = useMutation(blocklistsUpdateNowMutation(queryClient)); const sources = useRefreshStatus(); const namesById = new Map(blocklists.map((b) => [b.id, b.name])); // The refresh below re-fetches the sources the config already declares, so // it stays live in file mode; every other control here writes config. const readOnly = useReadOnlyConfig(); async function submitForm(input: BlocklistInput) { if (editing === null) { await create.mutateAsync(input); } else { await save.mutateAsync({ id: editing.id, input: { ...input, is_suggested: editing.is_suggested } }); setEditing(null); } } function toggleEnabled(b: Blocklist) { toggle.mutate({ id: b.id, input: { url: b.url, name: b.name, enabled: !b.enabled, is_suggested: b.is_suggested }, }); } function confirmDelete() { if (pendingDelete === null) return; remove.mutate(pendingDelete.id); setPendingDelete(null); } const formError = editing === null ? create.error : save.error; const tableError = remove.error ?? toggle.error; return (

Blocklists

{updateNow.isSuccess && !updateNow.isPending && (

Update completed; source status refreshed below.

)} {blocklists.length === 0 ? (

No blocklist sources yet. Add one below.

) : (
{blocklists.map((b) => ( ))}
Name URL Enabled Domains Wildcards Exceptions Skipped regex Skipped unsupported Last updated Actions
{b.name} {b.is_suggested && Suggested} {b.url} toggleEnabled(b)} {...stylex.props(shared.focusRing)} /> {b.domain_count} {b.wildcard_count} {b.exception_count} {b.skipped_regex_count} {b.skipped_unsupported_count} {b.last_updated === null ? "never" : formatTime(b.last_updated)}

Both “Skipped” columns count lines nxdns read and did not take. Skipped regex lines are patterns nxdns accepts only from you — adopt one you trust as a regex rule. Skipped unsupported lines are syntax nxdns cannot translate into a DNS decision: cosmetic element hiding, browser-only modifiers. A skipped unsupported count that dwarfs the domain count usually means the list is written for a browser extension, and its DNS or hosts variant will block more here.

)} setEditing(null)} /> setPendingDelete(null)} />
); }