Add a blacklist editor that saves to e621
The Online sidebar's "Your blacklist" section is editable now: typing a
tag appends it and each entry gets an x to remove it. Changes are written
straight to the e621 account (PATCH /users/{id}.json with
user[blacklisted_tags]); the store keeps the fetched profile for the id,
updates the list optimistically and reloads it from e621. The Followed
page's blacklist cloud is rebuilt afterwards via the new
/api/follows/cloud/?refresh=1 force flag.
Verified against the live API with a reversible add/verify/restore test.
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||
import { keepPreviousData, useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { X } from "lucide-react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { Link, useLocation, useSearchParams } from "react-router-dom";
|
||||
|
||||
import { CollapsibleSidebar } from "@/components/CollapsibleSidebar";
|
||||
import { TagChip } from "@/components/TagChip";
|
||||
import { Button, EmptyState, Spinner } from "@/components/ui";
|
||||
import { Button, EmptyState, Spinner, inputClass } from "@/components/ui";
|
||||
import { PostCard } from "@/features/online/PostCard";
|
||||
import { TagAutocomplete } from "@/features/online/TagAutocomplete";
|
||||
import { api } from "@/lib/api";
|
||||
@@ -30,9 +31,46 @@ export default function OnlinePage() {
|
||||
|
||||
const credentials = useE621((state) => state.credentials);
|
||||
const blacklist = useE621((state) => state.blacklist);
|
||||
const saveBlacklist = useE621((state) => state.saveBlacklist);
|
||||
const queryClient = useQueryClient();
|
||||
const [draft, setDraft] = useState<string | null>(null);
|
||||
const [blacklistDraft, setBlacklistDraft] = useState("");
|
||||
const tagInput = draft ?? tags;
|
||||
|
||||
const blacklistMutation = useMutation({
|
||||
mutationFn: (lines: string[]) => saveBlacklist(lines),
|
||||
onSuccess: () => {
|
||||
setBlacklistDraft("");
|
||||
// The Followed page's cloud counted the old list; rebuild it.
|
||||
void api("/api/follows/cloud/?refresh=1")
|
||||
.catch(() => undefined)
|
||||
.finally(() => {
|
||||
void queryClient.invalidateQueries({ queryKey: ["follows-cloud"] });
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const blacklistEntries = useMemo(
|
||||
() =>
|
||||
blacklist
|
||||
.map((line, index) => ({ line, index }))
|
||||
.filter(({ line }) => !line.startsWith("#")),
|
||||
[blacklist],
|
||||
);
|
||||
|
||||
function addBlacklistEntry() {
|
||||
const tag = blacklistDraft.trim().toLowerCase();
|
||||
if (!tag || blacklist.includes(tag)) {
|
||||
setBlacklistDraft("");
|
||||
return;
|
||||
}
|
||||
blacklistMutation.mutate([...blacklist, tag]);
|
||||
}
|
||||
|
||||
function removeBlacklistEntry(index: number) {
|
||||
blacklistMutation.mutate(blacklist.filter((_, position) => position !== index));
|
||||
}
|
||||
|
||||
const postsQuery = useQuery({
|
||||
queryKey: ["e621", "posts", tags, page, credentials?.username ?? ""],
|
||||
queryFn: ({ signal }) =>
|
||||
@@ -180,19 +218,74 @@ export default function OnlinePage() {
|
||||
<span className="text-xs font-medium uppercase tracking-wide text-ctp-overlay1">
|
||||
Your blacklist
|
||||
</span>
|
||||
{blacklist.length > 0 ? (
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{blacklist.map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="rounded border border-ctp-red/30 bg-ctp-red/10 px-1.5 py-0.5 font-mono text-[11px] text-ctp-red"
|
||||
{credentials?.configured ? (
|
||||
<>
|
||||
{blacklistEntries.length > 0 ? (
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{blacklistEntries.map(({ line, index }) => (
|
||||
<span
|
||||
key={`${line}-${index}`}
|
||||
className="inline-flex items-center gap-1 rounded border border-ctp-red/30 bg-ctp-red/10 px-1.5 py-0.5 font-mono text-[11px] text-ctp-red"
|
||||
>
|
||||
{line}
|
||||
<button
|
||||
type="button"
|
||||
title={`Remove ${line}`}
|
||||
disabled={blacklistMutation.isPending}
|
||||
onClick={() => removeBlacklistEntry(index)}
|
||||
className="text-ctp-red/70 transition hover:text-ctp-red disabled:opacity-40"
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-xs text-ctp-overlay0">
|
||||
No blacklisted tags.
|
||||
</p>
|
||||
)}
|
||||
<form
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
addBlacklistEntry();
|
||||
}}
|
||||
className="flex gap-2"
|
||||
>
|
||||
<input
|
||||
className={cn(inputClass, "font-mono")}
|
||||
placeholder="Add a tag…"
|
||||
value={blacklistDraft}
|
||||
onChange={(event) => setBlacklistDraft(event.target.value)}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="secondary"
|
||||
disabled={!blacklistDraft.trim() || blacklistMutation.isPending}
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
{blacklistMutation.isPending ? (
|
||||
<Spinner className="h-3.5 w-3.5" />
|
||||
) : (
|
||||
"Add"
|
||||
)}
|
||||
</Button>
|
||||
</form>
|
||||
{blacklistMutation.isError ? (
|
||||
<p className="text-xs text-ctp-red">
|
||||
{e621ErrorMessage(blacklistMutation.error)}
|
||||
</p>
|
||||
) : null}
|
||||
<p className="text-[11px] text-ctp-overlay0">
|
||||
Saved to your e621 account.
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<p className="text-xs text-ctp-overlay0">No blacklisted tags.</p>
|
||||
<p className="text-xs text-ctp-overlay0">
|
||||
<Link to="/account" className="text-ctp-blue hover:underline">
|
||||
Add e621 credentials
|
||||
</Link>{" "}
|
||||
to manage your blacklist.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user