···
+
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(false);
const resolveDid = async (did: string) => {
···
+
const Login: Component = () => {
+
const [loginInput, setLoginInput] = createSignal("");
+
const [handle, setHandle] = createSignal("");
+
const [notice, setNotice] = createSignal("");
+
let client: BrowserOAuthClient;
+
setNotice("Loading...");
+
//clientId: "https://cleanfollow-bsky.pages.dev/client-metadata.json",
+
client = new BrowserOAuthClient({
+
clientMetadata: undefined,
+
handleResolver: "https://boletus.us-west.host.bsky.network",
+
client.addEventListener("deleted", () => {
+
const result = await client.init().catch(() => {});
+
agent = new Agent(result.session);
+
setHandle(await resolveDid(agent.did!));
+
sub = result.session.sub;
+
const loginBsky = async (handle: string) => {
+
setNotice("Redirecting...");
+
await client.signIn(handle, {
+
scope: "atproto transition:generic",
+
signal: new AbortController().signal,
+
setNotice("Error during OAuth redirection");
+
const logoutBsky = async () => {
+
if (sub) await client.revoke(sub);
+
<div class="flex flex-col items-center">
+
<Show when={!loginState() && !notice().includes("Loading")}>
+
class="flex flex-col items-center"
+
onsubmit={(e) => e.preventDefault()}
+
<label for="handle">Handle:</label>
+
placeholder="user.bsky.social"
+
class="mb-3 mt-1 rounded-md px-2 py-1"
+
onInput={(e) => setLoginInput(e.currentTarget.value)}
+
onclick={() => loginBsky(loginInput())}
+
class="rounded bg-blue-500 px-4 py-2 font-bold text-white hover:bg-blue-700"
+
<Show when={loginState() && handle()}>
+
Logged in as {handle()} (
+
<a href="" class="text-red-600" onclick={() => logoutBsky()}>
+
<div class="m-3">{notice()}</div>
+
const Fetch: Component = () => {
const [progress, setProgress] = createSignal(0);
const [followCount, setFollowCount] = createSignal(0);
+
const [notice, setNotice] = createSignal("");
const fetchHiddenAccounts = async () => {
const fetchFollows = async () => {
···
await fetchFollows().then((follows) => {
setFollowCount(follows.length);
···
<div class="flex flex-col items-center">
+
<Show when={!followRecords.length}>
+
onclick={() => fetchHiddenAccounts()}
+
class="rounded bg-blue-500 px-4 py-2 font-bold text-white hover:bg-blue-700"
+
<Show when={followRecords.length}>
+
onclick={() => unfollow()}
+
class="rounded bg-green-500 px-4 py-2 font-bold text-white hover:bg-green-700"
<div class="m-3">{notice()}</div>
+
<Show when={followCount()}>
Progress: {progress()}/{followCount()}
···
+
const Follows: Component = () => {
+
field: keyof FollowRecord,
+
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" },
+
<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">
+
"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,
+
<label class="mb-2 mt-1 inline-flex cursor-pointer items-center">
+
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">
+
<div class="flex items-center">
+
class="h-4 w-4 rounded"
+
e.currentTarget.checked,
+
<label for={option.label} class="ml-2 select-none">
+
<For each={followRecords}>
+
<Show when={record.visible}>
+
<div class="mb-2 flex items-center border-b pb-2">
+
id={"record" + index()}
+
class="h-4 w-4 rounded"
+
checked={record.toBeDeleted}
+
e.currentTarget.checked,
+
<label for={"record" + index()} class="flex flex-col">
+
<span>@{record.handle}</span>
+
<span>{record.did}</span>
+
<span>{record.status_label}</span>
const App: Component = () => {
<div class="m-5 flex flex-col items-center">
···
+
<Show when={loginState()}>
+
<Show when={followRecords.length}>