johnandersen777.bsky.social/knotless
git that lives in your Atmosphere account
Forked from chadtmiller.com/knotless
0
0
<script lang="ts">
import type * as getAccountData from '#lib/lexicons/dev/knotless/getAccountData.ts';
import type * as deleteAccountData from '#lib/lexicons/dev/knotless/deleteAccountData.ts';
import type { DidString } from '@atproto/lex';
import { goto, invalidateAll } from '$app/navigation';
import { ask, call, type Answer } from '#lib/data.ts';
import { urlPath } from '#lib/format.ts';
import Dialog from '#lib/molecules/Dialog.svelte';
/**
* Deleting every knotless record from the account `did`, logged in as
* `handle`, as `knotless account delete-data` does: what there is, read when the
* dialog opens, its repositories others push to or read named with a
* way to transfer them first; then, once the handle is typed, the
* deleting, a part a call, and the front page, logged out.
*/
let { did, handle }: { did: string; handle: string } = $props();
const id = 'delete-data';
/** The records and spaces one call deletes: a part, for the progress shown. */
const PART = 100;
let held = $state<Answer<typeof getAccountData.main> | null>(null);
let typed = $state('');
let problem = $state('');
let reading = $state(false);
let busy = $state(false);
let deleted = $state(0);
/** How many records, and other accounts, as a person reads it. */
const n = (count: number) => count.toLocaleString('en-US');
const records = (count: number) => (count === 1 ? '1 record' : `${n(count)} records`);
const others = (count: number) =>
count === 1 ? '1 other account' : `${n(count)} other accounts`;
const total = $derived(
(held?.collections.reduce((n, c) => n + c.count, 0) ?? 0) + (held?.spaces ?? 0)
);
const summary = $derived(
held?.spaces
? `This deletes ${records(total - held.spaces)} and ${held.spaces === 1 ? '1 private repository space' : `${held.spaces} private repository spaces`} from your account, for good.`
: `This deletes ${records(total)} from your account, for good.`
);
const shared = $derived(
held?.repos.filter((r) => r.collaborators > 0 || (r.members ?? 0) > 0) ?? []
);
async function read() {
reading = true;
problem = '';
try {
held = await ask<typeof getAccountData.main>('dev.knotless.getAccountData', {});
} catch (e) {
problem = (e as Error).message;
}
reading = false;
}
async function remove(e: SubmitEvent) {
e.preventDefault();
busy = true;
problem = '';
try {
for (;;) {
const step = await call<typeof deleteAccountData.main>('dev.knotless.deleteAccountData', {
did: did as DidString,
limit: PART
});
deleted += step.deleted;
if (step.done) break;
}
await goto('/');
await invalidateAll();
} catch (err) {
problem = `${(err as Error).message} What is deleted stays deleted: try again to carry on.`;
busy = false;
}
}
</script>
<section class="setting danger account-danger">
<h2>Delete your knotless data</h2>
<div class="danger-row">
<div>
<p class="hint">
Every knotless record in your account goes for good: your repositories, private ones too,
and your pulls, issues, comments, stars and follows. You are then logged out. Your atproto
account, and what other apps keep in it, stay.
</p>
</div>
<button class="btn danger" type="button" popovertarget={id}>Delete data</button>
</div>
</section>
<Dialog
{id}
class="delete-data"
title="Delete your knotless data"
held={busy}
ontoggle={(e) => e.newState === 'open' && !held && read()}
>
{#if reading}
<p class="hint" data-loading>Reading what your account holds…</p>
{:else if held && total === 0}
<p class="hint">Your account holds no knotless data.</p>
{:else if held}
<div class="data-body">
<p class="hint">{summary}</p>
{#if held.repos.length}
<h3>Repositories</h3>
{#if shared.length}<p class="hint">
Others use a repository shared with them: transfer it to one of them first to keep it.
</p>{/if}
<ul class="data-repos">
{#each held.repos as r (r.name)}
{@const sharing = Math.max(r.collaborators, r.members ?? 0)}
<li>
<span class="clip">{r.name}</span>
{#if r.private}<span class="mute">Private</span>{/if}
{#if sharing}<span class="shared"
><span class="mute">{`Shared with ${others(sharing)}`}</span><a
href="/{urlPath(handle)}/{urlPath(r.name)}/settings">Transfer</a
></span
>{/if}
</li>
{/each}
</ul>
{/if}
{#if held.collections.length}
<h3>Records</h3>
<ul class="data-counts">
{#each held.collections as c (c.collection)}
<li>
<code>{c.collection.replace(/^dev\.knotless\./, '')}</code><span class="mute"
>{n(c.count)}</span
>
</li>
{/each}
</ul>
{/if}
</div>
<form onsubmit={remove}>
<label for="{id}-typed">{`To confirm, type ${handle}`}</label>
<input
id="{id}-typed"
name="confirm"
autocapitalize="none"
autocomplete="off"
spellcheck="false"
disabled={busy}
bind:value={typed}
/>
{#if busy}<progress max={total} value={deleted}>{`${deleted} of ${total}`}</progress>
<p class="hint" role="status">{`Deleted ${n(deleted)} of ${n(total)}…`}</p>{/if}
{#if problem}<p class="problem" role="alert">{problem}</p>{/if}
<button
class="btn danger solid"
type="submit"
disabled={busy || typed.trim() !== handle}
aria-busy={busy}>{busy ? 'Deleting…' : 'Delete all my knotless data'}</button
>
</form>
{/if}
{#if problem && !held}<p class="problem" role="alert">{problem}</p>{/if}
</Dialog>