Unfollow tool for Bluesky

reorganize

Changed files
+208 -187
src
+208 -187
src/App.tsx
···
-
import { createSignal, For, Show, type Component } from "solid-js";
+
import { createSignal, onMount, For, Show, type Component } from "solid-js";
import { createStore } from "solid-js/store";
import { Agent } from "@atproto/api";
···
};
const [followRecords, setFollowRecords] = createStore<FollowRecord[]>([]);
-
const [loginState, setLoginState] = createSignal<boolean>();
-
const [notice, setNotice] = createSignal("");
-
-
const client = await BrowserOAuthClient.load({
-
clientId: "https://cleanfollow-bsky.pages.dev/client-metadata.json",
-
handleResolver: "https://boletus.us-west.host.bsky.network",
-
});
-
-
client.addEventListener("deleted", () => {
-
setLoginState(false);
-
});
-
+
const [loginState, setLoginState] = createSignal(false);
let agent: Agent;
-
let userHandle: string;
const resolveDid = async (did: string) => {
const res = await fetch(
···
});
};
-
const result = await client.init().catch(() => {});
+
const Login: Component = () => {
+
const [loginInput, setLoginInput] = createSignal("");
+
const [handle, setHandle] = createSignal("");
+
const [notice, setNotice] = createSignal("");
+
let client: BrowserOAuthClient;
+
let sub: string;
-
if (result) {
-
agent = new Agent(result.session);
-
setLoginState(true);
-
userHandle = await resolveDid(agent.did!);
-
}
+
onMount(async () => {
+
setNotice("Loading...");
+
//clientId: "https://cleanfollow-bsky.pages.dev/client-metadata.json",
+
client = new BrowserOAuthClient({
+
clientMetadata: undefined,
+
handleResolver: "https://boletus.us-west.host.bsky.network",
+
});
-
const loginBsky = async (handle: string) => {
-
setNotice("Redirecting...");
-
try {
-
await client.signIn(handle, {
-
scope: "atproto transition:generic",
-
signal: new AbortController().signal,
+
client.addEventListener("deleted", () => {
+
setLoginState(false);
});
-
} catch (err) {
-
setNotice("Error during OAuth redirection");
-
}
-
};
+
const result = await client.init().catch(() => {});
-
const logoutBsky = async () => {
-
if (result) await client.revoke(result.session.sub);
-
};
+
if (result) {
+
agent = new Agent(result.session);
+
setLoginState(true);
+
setHandle(await resolveDid(agent.did!));
+
sub = result.session.sub;
+
}
+
setNotice("");
+
});
-
const Follows: Component = () => {
-
function editRecords(
-
status: RepoStatus,
-
field: keyof FollowRecord,
-
value: boolean,
-
) {
-
followRecords.forEach((record, index) => {
-
if (record.status & status) setFollowRecords(index, field, value);
-
});
-
}
+
const loginBsky = async (handle: string) => {
+
setNotice("Redirecting...");
+
try {
+
await client.signIn(handle, {
+
scope: "atproto transition:generic",
+
signal: new AbortController().signal,
+
});
+
} catch (err) {
+
setNotice("Error during OAuth redirection");
+
}
+
};
-
const options: { status: RepoStatus; label: string }[] = [
-
{ status: RepoStatus.DELETED, label: "Deleted" },
-
{ status: RepoStatus.DEACTIVATED, label: "Deactivated" },
-
{ status: RepoStatus.SUSPENDED, label: "Suspended" },
-
{ status: RepoStatus.BLOCKEDBY, label: "Blocked By" },
-
{ status: RepoStatus.BLOCKING, label: "Blocking" },
-
{ status: RepoStatus.NONMUTUAL, label: "Non Mutual" },
-
];
+
const logoutBsky = async () => {
+
if (sub) await client.revoke(sub);
+
};
return (
-
<div class="mt-3 flex flex-col sm:flex-row">
-
<div class="sticky top-0 mb-3 mr-5 flex w-full flex-wrap justify-around border-b border-b-gray-400 bg-white pb-3 sm:top-3 sm:mb-0 sm:w-auto sm:flex-col sm:self-start sm:border-none">
-
<For each={options}>
-
{(option, index) => (
-
<div
-
classList={{
-
"sm:pb-2 min-w-36 sm:mb-2 mt-3 sm:mt-0": true,
-
"sm:border-b sm:border-b-gray-300":
-
index() < options.length - 1,
-
}}
-
>
-
<div>
-
<label class="mb-2 mt-1 inline-flex cursor-pointer items-center">
-
<input
-
type="checkbox"
-
class="peer sr-only"
-
checked
-
onChange={(e) =>
-
editRecords(
-
option.status,
-
"visible",
-
e.currentTarget.checked,
-
)
-
}
-
/>
-
<span class="peer relative h-5 w-9 rounded-full bg-gray-200 after:absolute after:start-[2px] after:top-[2px] after:h-4 after:w-4 after:rounded-full after:border after:border-gray-300 after:bg-white after:transition-all after:content-[''] peer-checked:bg-blue-600 peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:border-gray-600 dark:bg-gray-700 dark:peer-focus:ring-blue-800 rtl:peer-checked:after:-translate-x-full"></span>
-
<span class="ms-3 select-none dark:text-gray-300">
-
{option.label}
-
</span>
-
</label>
-
</div>
-
<div class="flex items-center">
-
<input
-
type="checkbox"
-
id={option.label}
-
class="h-4 w-4 rounded"
-
onChange={(e) =>
-
editRecords(
-
option.status,
-
"toBeDeleted",
-
e.currentTarget.checked,
-
)
-
}
-
/>
-
<label for={option.label} class="ml-2 select-none">
-
Select All
-
</label>
-
</div>
-
</div>
-
)}
-
</For>
-
</div>
-
<div>
-
<For each={followRecords}>
-
{(record, index) => (
-
<Show when={record.visible}>
-
<div class="mb-2 flex items-center border-b pb-2">
-
<div class="mr-4">
-
<input
-
type="checkbox"
-
id={"record" + index()}
-
class="h-4 w-4 rounded"
-
checked={record.toBeDeleted}
-
onChange={(e) =>
-
setFollowRecords(
-
index(),
-
"toBeDeleted",
-
e.currentTarget.checked,
-
)
-
}
-
/>
-
</div>
-
<div>
-
<label for={"record" + index()} class="flex flex-col">
-
<span>@{record.handle}</span>
-
<span>{record.did}</span>
-
<span>{record.status_label}</span>
-
</label>
-
</div>
-
</div>
-
</Show>
-
)}
-
</For>
-
</div>
+
<div class="flex flex-col items-center">
+
<Show when={!loginState() && !notice().includes("Loading")}>
+
<form
+
class="flex flex-col items-center"
+
onsubmit={(e) => e.preventDefault()}
+
>
+
<label for="handle">Handle:</label>
+
<input
+
type="text"
+
id="handle"
+
placeholder="user.bsky.social"
+
class="mb-3 mt-1 rounded-md px-2 py-1"
+
onInput={(e) => setLoginInput(e.currentTarget.value)}
+
/>
+
<button
+
onclick={() => loginBsky(loginInput())}
+
class="rounded bg-blue-500 px-4 py-2 font-bold text-white hover:bg-blue-700"
+
>
+
Login
+
</button>
+
</form>
+
</Show>
+
<Show when={loginState() && handle()}>
+
<div class="mb-5">
+
Logged in as {handle()} (
+
<a href="" class="text-red-600" onclick={() => logoutBsky()}>
+
Logout
+
</a>
+
)
+
</div>
+
</Show>
+
<Show when={notice()}>
+
<div class="m-3">{notice()}</div>
+
</Show>
</div>
);
};
-
const Form: Component = () => {
-
const [loginInput, setLoginInput] = createSignal("");
+
const Fetch: Component = () => {
const [progress, setProgress] = createSignal(0);
const [followCount, setFollowCount] = createSignal(0);
+
const [notice, setNotice] = createSignal("");
const fetchHiddenAccounts = async () => {
const fetchFollows = async () => {
···
return follows;
};
-
setNotice("");
setProgress(0);
+
setNotice("");
await fetchFollows().then((follows) => {
setFollowCount(follows.length);
···
return (
<div class="flex flex-col items-center">
-
<Show when={!loginState()}>
-
<form
-
class="flex flex-col items-center"
-
onsubmit={(e) => e.preventDefault()}
+
<Show when={!followRecords.length}>
+
<button
+
type="button"
+
onclick={() => fetchHiddenAccounts()}
+
class="rounded bg-blue-500 px-4 py-2 font-bold text-white hover:bg-blue-700"
>
-
<label for="handle">Handle:</label>
-
<input
-
type="text"
-
id="handle"
-
placeholder="user.bsky.social"
-
class="mb-3 mt-1 rounded-md px-2 py-1"
-
onInput={(e) => setLoginInput(e.currentTarget.value)}
-
/>
-
<button
-
onclick={() => loginBsky(loginInput())}
-
class="rounded bg-blue-500 px-4 py-2 font-bold text-white hover:bg-blue-700"
-
>
-
Login
-
</button>
-
</form>
+
Preview
+
</button>
</Show>
-
<Show when={loginState()}>
-
<div class="mb-5">
-
Logged in as {userHandle} (
-
<a href="" class="text-red-600" onclick={() => logoutBsky()}>
-
Logout
-
</a>
-
)
-
</div>
-
<Show when={!followRecords.length}>
-
<button
-
type="button"
-
onclick={() => fetchHiddenAccounts()}
-
class="rounded bg-blue-500 px-4 py-2 font-bold text-white hover:bg-blue-700"
-
>
-
Preview
-
</button>
-
</Show>
-
<Show when={followRecords.length}>
-
<button
-
type="button"
-
onclick={() => unfollow()}
-
class="rounded bg-green-500 px-4 py-2 font-bold text-white hover:bg-green-700"
-
>
-
Confirm
-
</button>
-
</Show>
+
<Show when={followRecords.length}>
+
<button
+
type="button"
+
onclick={() => unfollow()}
+
class="rounded bg-green-500 px-4 py-2 font-bold text-white hover:bg-green-700"
+
>
+
Confirm
+
</button>
</Show>
<Show when={notice()}>
<div class="m-3">{notice()}</div>
</Show>
-
<Show when={loginState() && followCount()}>
+
<Show when={followCount()}>
<div class="m-3">
Progress: {progress()}/{followCount()}
</div>
···
);
};
+
const Follows: Component = () => {
+
function editRecords(
+
status: RepoStatus,
+
field: keyof FollowRecord,
+
value: boolean,
+
) {
+
followRecords.forEach((record, index) => {
+
if (record.status & status) setFollowRecords(index, field, value);
+
});
+
}
+
+
const options: { status: RepoStatus; label: string }[] = [
+
{ status: RepoStatus.DELETED, label: "Deleted" },
+
{ status: RepoStatus.DEACTIVATED, label: "Deactivated" },
+
{ status: RepoStatus.SUSPENDED, label: "Suspended" },
+
{ status: RepoStatus.BLOCKEDBY, label: "Blocked By" },
+
{ status: RepoStatus.BLOCKING, label: "Blocking" },
+
{ status: RepoStatus.NONMUTUAL, label: "Non Mutual" },
+
];
+
+
return (
+
<div class="mt-3 flex flex-col sm:flex-row">
+
<div class="sticky top-0 mb-3 mr-5 flex w-full flex-wrap justify-around border-b border-b-gray-400 bg-white pb-3 sm:top-3 sm:mb-0 sm:w-auto sm:flex-col sm:self-start sm:border-none">
+
<For each={options}>
+
{(option, index) => (
+
<div
+
classList={{
+
"sm:pb-2 min-w-36 sm:mb-2 mt-3 sm:mt-0": true,
+
"sm:border-b sm:border-b-gray-300":
+
index() < options.length - 1,
+
}}
+
>
+
<div>
+
<label class="mb-2 mt-1 inline-flex cursor-pointer items-center">
+
<input
+
type="checkbox"
+
class="peer sr-only"
+
checked
+
onChange={(e) =>
+
editRecords(
+
option.status,
+
"visible",
+
e.currentTarget.checked,
+
)
+
}
+
/>
+
<span class="peer relative h-5 w-9 rounded-full bg-gray-200 after:absolute after:start-[2px] after:top-[2px] after:h-4 after:w-4 after:rounded-full after:border after:border-gray-300 after:bg-white after:transition-all after:content-[''] peer-checked:bg-blue-600 peer-checked:after:translate-x-full peer-checked:after:border-white peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:border-gray-600 dark:bg-gray-700 dark:peer-focus:ring-blue-800 rtl:peer-checked:after:-translate-x-full"></span>
+
<span class="ms-3 select-none dark:text-gray-300">
+
{option.label}
+
</span>
+
</label>
+
</div>
+
<div class="flex items-center">
+
<input
+
type="checkbox"
+
id={option.label}
+
class="h-4 w-4 rounded"
+
onChange={(e) =>
+
editRecords(
+
option.status,
+
"toBeDeleted",
+
e.currentTarget.checked,
+
)
+
}
+
/>
+
<label for={option.label} class="ml-2 select-none">
+
Select All
+
</label>
+
</div>
+
</div>
+
)}
+
</For>
+
</div>
+
<div>
+
<For each={followRecords}>
+
{(record, index) => (
+
<Show when={record.visible}>
+
<div class="mb-2 flex items-center border-b pb-2">
+
<div class="mr-4">
+
<input
+
type="checkbox"
+
id={"record" + index()}
+
class="h-4 w-4 rounded"
+
checked={record.toBeDeleted}
+
onChange={(e) =>
+
setFollowRecords(
+
index(),
+
"toBeDeleted",
+
e.currentTarget.checked,
+
)
+
}
+
/>
+
</div>
+
<div>
+
<label for={"record" + index()} class="flex flex-col">
+
<span>@{record.handle}</span>
+
<span>{record.did}</span>
+
<span>{record.status_label}</span>
+
</label>
+
</div>
+
</div>
+
</Show>
+
)}
+
</For>
+
</div>
+
</div>
+
);
+
};
+
const App: Component = () => {
return (
<div class="m-5 flex flex-col items-center">
···
</a>
</div>
</div>
-
<Form />
-
<Show when={loginState() && followRecords.length}>
-
<Follows />
+
<Login />
+
<Show when={loginState()}>
+
<Fetch />
+
<Show when={followRecords.length}>
+
<Follows />
+
</Show>
</Show>
</div>
);