fix: move generated key backup to home health checks (#587)
This commit is contained in:
parent
a50b0160bd
commit
929b6e4e41
10 changed files with 128 additions and 69 deletions
|
|
@ -48,12 +48,16 @@ key in the app, so that I can start participating without an external tool.
|
||||||
|
|
||||||
Acceptance:
|
Acceptance:
|
||||||
|
|
||||||
- Entering a display name advances to a key-backup step whose "Continue" button
|
- Entering a display name advances directly to the completion step without showing
|
||||||
stays disabled until a key file has been downloaded.
|
the key-backup modal.
|
||||||
- Choosing the encrypted download requires a password of at least 12 characters
|
|
||||||
and produces a file containing an ncryptsec rather than a plain nsec.
|
|
||||||
- Finishing the flow logs the new user in, dismisses the dialog, and lands on
|
- Finishing the flow logs the new user in, dismisses the dialog, and lands on
|
||||||
the dashboard with the platform's default space visible.
|
the dashboard with the platform's default space visible.
|
||||||
|
- The dashboard shows a pending "Back Up Your Key" health check until the existing
|
||||||
|
backup flow completes successfully; leaving the modal keeps the check pending.
|
||||||
|
- Reloading preserves both the pending reminder and a completed backup.
|
||||||
|
- Applying all recommendations opens the backup flow alongside the automatic relay fixes.
|
||||||
|
- Choosing the encrypted download requires a password of at least 12 characters
|
||||||
|
and produces a file containing an ncryptsec rather than a plain nsec.
|
||||||
- The display name entered during signup appears on the new user's own profile.
|
- The display name entered during signup appears on the new user's own profile.
|
||||||
|
|
||||||
### US-003 — Log in with an existing private key
|
### US-003 — Log in with an existing private key
|
||||||
|
|
@ -68,6 +72,7 @@ Acceptance:
|
||||||
- Pasting an ncryptsec reveals a password field: the correct password logs in, a
|
- Pasting an ncryptsec reveals a password field: the correct password logs in, a
|
||||||
wrong one shows an error and stays logged out.
|
wrong one shows an error and stays logged out.
|
||||||
- Text that is not a valid key leaves the submit button disabled.
|
- Text that is not a valid key leaves the submit button disabled.
|
||||||
|
- Logging in with an existing private key does not enable the "Back Up Your Key" health check.
|
||||||
- bob logging in with his own key in a separate context sees his own identity,
|
- bob logging in with his own key in a separate context sees his own identity,
|
||||||
not alice's.
|
not alice's.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -100,36 +100,7 @@ test("US-002 sign up by generating a new key", async ({seed, visit}) => {
|
||||||
await page.locator('input[type="text"]').fill("Nova Tester")
|
await page.locator('input[type="text"]').fill("Nova Tester")
|
||||||
await page.getByRole("button", {name: "Create Account"}).click()
|
await page.getByRole("button", {name: "Create Account"}).click()
|
||||||
|
|
||||||
await expect(page.getByRole("heading", {name: "Your Keys are Ready!"})).toBeVisible()
|
await expect(page.getByRole("heading", {name: "Backup your Key"})).toHaveCount(0)
|
||||||
|
|
||||||
const continueButton = page.getByRole("button", {name: "Continue"})
|
|
||||||
const password = page.locator('input[type="password"]')
|
|
||||||
|
|
||||||
await expect(continueButton).toBeDisabled()
|
|
||||||
|
|
||||||
await page.getByRole("button", {name: "I want to download an encrypted version"}).click()
|
|
||||||
await password.fill("hunter2")
|
|
||||||
await page.getByRole("button", {name: "Download my key"}).click()
|
|
||||||
|
|
||||||
await expect(page.getByRole("alert")).toContainText(
|
|
||||||
"Your password must be at least 12 characters long.",
|
|
||||||
)
|
|
||||||
await expect(continueButton).toBeDisabled()
|
|
||||||
|
|
||||||
await password.fill("correct horse battery staple")
|
|
||||||
|
|
||||||
const download = page.waitForEvent("download")
|
|
||||||
|
|
||||||
await page.getByRole("button", {name: "Download my key"}).click()
|
|
||||||
|
|
||||||
const contents = await readFile(await (await download).path(), "utf8")
|
|
||||||
|
|
||||||
expect(contents).toContain("ncryptsec1")
|
|
||||||
expect(contents).not.toContain("nsec1")
|
|
||||||
|
|
||||||
await expect(continueButton).toBeEnabled()
|
|
||||||
await continueButton.click()
|
|
||||||
|
|
||||||
await expect(page.getByRole("heading", {name: "You're all set!"})).toBeVisible()
|
await expect(page.getByRole("heading", {name: "You're all set!"})).toBeVisible()
|
||||||
|
|
||||||
await page.getByRole("button", {name: "Go to Dashboard"}).click()
|
await page.getByRole("button", {name: "Go to Dashboard"}).click()
|
||||||
|
|
@ -138,6 +109,7 @@ test("US-002 sign up by generating a new key", async ({seed, visit}) => {
|
||||||
await expect(page.locator(".primary-nav")).toBeVisible()
|
await expect(page.locator(".primary-nav")).toBeVisible()
|
||||||
await expect(page.getByRole("heading", {name: "You're all set!"})).toHaveCount(0)
|
await expect(page.getByRole("heading", {name: "You're all set!"})).toHaveCount(0)
|
||||||
await expect(gate(page)).toHaveCount(0)
|
await expect(gate(page)).toHaveCount(0)
|
||||||
|
await expect(page.getByText("Back Up Your Key")).toBeVisible()
|
||||||
|
|
||||||
// A space's nav item is a button rather than a link — PrimaryNavItemSpace passes an onclick, and
|
// A space's nav item is a button rather than a link — PrimaryNavItemSpace passes an onclick, and
|
||||||
// PrimaryNavItem renders a Button whenever it has one — so it is addressed by the tooltip it
|
// PrimaryNavItem renders a Button whenever it has one — so it is addressed by the tooltip it
|
||||||
|
|
@ -151,6 +123,58 @@ test("US-002 sign up by generating a new key", async ({seed, visit}) => {
|
||||||
await openSettings(page)
|
await openSettings(page)
|
||||||
|
|
||||||
await expect(page.getByText("Nova Tester")).toBeVisible()
|
await expect(page.getByText("Nova Tester")).toBeVisible()
|
||||||
|
|
||||||
|
await page.goto("/home")
|
||||||
|
await expect(page.getByText("Back Up Your Key")).toBeVisible()
|
||||||
|
|
||||||
|
await page.reload()
|
||||||
|
|
||||||
|
await expect(page.getByText("Back Up Your Key")).toBeVisible()
|
||||||
|
|
||||||
|
const backupCheck = page.getByRole("group", {name: "Back Up Your Key"})
|
||||||
|
|
||||||
|
await page.getByRole("button", {name: "Apply all recommendations"}).click()
|
||||||
|
await expect(page.getByRole("heading", {name: "Backup your Key"})).toBeVisible()
|
||||||
|
await page.getByRole("button", {name: "Go back"}).click()
|
||||||
|
await expect(backupCheck).toBeVisible()
|
||||||
|
|
||||||
|
await backupCheck.getByRole("button", {name: "Back Up"}).click()
|
||||||
|
await expect(page.getByRole("heading", {name: "Backup your Key"})).toBeVisible()
|
||||||
|
await page.getByRole("button", {name: "Go back"}).click()
|
||||||
|
await expect(page.getByText("Back Up Your Key")).toBeVisible()
|
||||||
|
|
||||||
|
await backupCheck.getByRole("button", {name: "Back Up"}).click()
|
||||||
|
|
||||||
|
const doneButton = page.getByRole("button", {name: "Done"})
|
||||||
|
const password = page.locator('input[type="password"]')
|
||||||
|
|
||||||
|
await page.getByRole("button", {name: "I want to download an encrypted version"}).click()
|
||||||
|
await password.fill("hunter2")
|
||||||
|
await page.getByRole("button", {name: "Download my key"}).click()
|
||||||
|
|
||||||
|
await expect(page.getByRole("alert")).toContainText(
|
||||||
|
"Your password must be at least 12 characters long.",
|
||||||
|
)
|
||||||
|
await expect(doneButton).toBeDisabled()
|
||||||
|
|
||||||
|
await password.fill("correct horse battery staple")
|
||||||
|
|
||||||
|
const download = page.waitForEvent("download")
|
||||||
|
|
||||||
|
await page.getByRole("button", {name: "Download my key"}).click()
|
||||||
|
|
||||||
|
const contents = await readFile(await (await download).path(), "utf8")
|
||||||
|
|
||||||
|
expect(contents).toContain("ncryptsec1")
|
||||||
|
expect(contents).not.toContain("nsec1")
|
||||||
|
|
||||||
|
await expect(doneButton).toBeEnabled()
|
||||||
|
await doneButton.click()
|
||||||
|
|
||||||
|
await expect(page.getByText("Back Up Your Key")).toHaveCount(0)
|
||||||
|
await page.reload()
|
||||||
|
await expect(page.locator(".primary-nav")).toBeVisible()
|
||||||
|
await expect(page.getByText("Back Up Your Key")).toHaveCount(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
test("US-003 log in with an existing private key", async ({seed, visit}) => {
|
test("US-003 log in with an existing private key", async ({seed, visit}) => {
|
||||||
|
|
@ -166,6 +190,7 @@ test("US-003 log in with an existing private key", async ({seed, visit}) => {
|
||||||
|
|
||||||
await logInWithKey(withNsec, nsecFor(users.alice))
|
await logInWithKey(withNsec, nsecFor(users.alice))
|
||||||
await expect(withNsec.locator(".primary-nav")).toBeVisible()
|
await expect(withNsec.locator(".primary-nav")).toBeVisible()
|
||||||
|
await expect(withNsec.getByText("Back Up Your Key")).toHaveCount(0)
|
||||||
await openSettings(withNsec)
|
await openSettings(withNsec)
|
||||||
await expect(npubField(withNsec)).toHaveValue(aliceNpub)
|
await expect(npubField(withNsec)).toHaveValue(aliceNpub)
|
||||||
|
|
||||||
|
|
@ -173,6 +198,7 @@ test("US-003 log in with an existing private key", async ({seed, visit}) => {
|
||||||
|
|
||||||
await logInWithKey(withHex, users.alice.secret)
|
await logInWithKey(withHex, users.alice.secret)
|
||||||
await expect(withHex.locator(".primary-nav")).toBeVisible()
|
await expect(withHex.locator(".primary-nav")).toBeVisible()
|
||||||
|
await expect(withHex.getByText("Back Up Your Key")).toHaveCount(0)
|
||||||
await openSettings(withHex)
|
await openSettings(withHex)
|
||||||
await expect(npubField(withHex)).toHaveValue(aliceNpub)
|
await expect(npubField(withHex)).toHaveValue(aliceNpub)
|
||||||
|
|
||||||
|
|
@ -201,6 +227,7 @@ test("US-003 log in with an existing private key", async ({seed, visit}) => {
|
||||||
await submit.click()
|
await submit.click()
|
||||||
|
|
||||||
await expect(withNcryptsec.locator(".primary-nav")).toBeVisible()
|
await expect(withNcryptsec.locator(".primary-nav")).toBeVisible()
|
||||||
|
await expect(withNcryptsec.getByText("Back Up Your Key")).toHaveCount(0)
|
||||||
await openSettings(withNcryptsec)
|
await openSettings(withNcryptsec)
|
||||||
await expect(npubField(withNcryptsec)).toHaveValue(aliceNpub)
|
await expect(npubField(withNcryptsec)).toHaveValue(aliceNpub)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,10 @@
|
||||||
const apply = () => $healthChecks.apply(healthCheck)
|
const apply = () => $healthChecks.apply(healthCheck)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex items-start justify-between gap-3 px-4 py-3">
|
<div
|
||||||
|
role="group"
|
||||||
|
aria-label={healthCheck.title}
|
||||||
|
class="flex items-start justify-between gap-3 px-4 py-3">
|
||||||
<div class="flex min-w-0 flex-col gap-1">
|
<div class="flex min-w-0 flex-col gap-1">
|
||||||
<strong class="text-sm">{healthCheck.title}</strong>
|
<strong class="text-sm">{healthCheck.title}</strong>
|
||||||
<p class="text-sm opacity-75">{healthCheck.description}</p>
|
<p class="text-sm opacity-75">{healthCheck.description}</p>
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@
|
||||||
</p>
|
</p>
|
||||||
{:else}
|
{:else}
|
||||||
<div class="flex flex-col divide-y divide-line border-t border-line">
|
<div class="flex flex-col divide-y divide-line border-t border-line">
|
||||||
{#each $pending as healthCheck (healthCheck.title)}
|
{#each $pending as healthCheck (healthCheck.id)}
|
||||||
<HealthCheckItem {healthCheck} />
|
<HealthCheckItem {healthCheck} />
|
||||||
{/each}
|
{/each}
|
||||||
{#if $pending.length > 1}
|
{#if $pending.length > 1}
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,7 @@
|
||||||
const ncryptsec = encrypt(hexToBytes(secret), password)
|
const ncryptsec = encrypt(hexToBytes(secret), password)
|
||||||
instructions = `
|
instructions = `
|
||||||
This file contains a backup of your Nostr secret key, downloaded from ${PLATFORM_NAME} and encrypted using
|
This file contains a backup of your Nostr secret key, downloaded from ${PLATFORM_NAME} and encrypted using
|
||||||
a password you chose when you signed up.
|
a password you chose when you backed up your key.
|
||||||
|
|
||||||
${sharedCopy}
|
${sharedCopy}
|
||||||
|
|
||||||
|
|
@ -95,15 +95,15 @@
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await downloadText("Nostr Secret Key.txt", cleanupCopy(instructions))
|
await downloadText("Nostr Secret Key.txt", cleanupCopy(instructions))
|
||||||
|
didDownload = true
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Dismissing the native share sheet rejects with "Share canceled" — leave
|
// Dismissing the native share sheet rejects with "Share canceled" — leave
|
||||||
// the flow gated so the user can try again rather than showing an error.
|
// the flow gated so the user can try again rather than showing an error.
|
||||||
if (!errorMessage(e).toLowerCase().includes("cancel")) {
|
if (!errorMessage(e).toLowerCase().includes("cancel")) {
|
||||||
|
console.error(e)
|
||||||
pushToast({theme: "error", message: "We couldn't save your key. Please try again."})
|
pushToast({theme: "error", message: "We couldn't save your key. Please try again."})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
didDownload = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const onPasswordChange = () => {
|
const onPasswordChange = () => {
|
||||||
|
|
@ -123,7 +123,7 @@
|
||||||
<Modal tag="form" onsubmit={preventDefault(next)}>
|
<Modal tag="form" onsubmit={preventDefault(next)}>
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<ModalHeader>
|
<ModalHeader>
|
||||||
<ModalTitle>Your Keys are Ready!</ModalTitle>
|
<ModalTitle>Backup your Key</ModalTitle>
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
<p>
|
<p>
|
||||||
A cryptographic key pair has two parts: your <strong>public key</strong> identifies your
|
A cryptographic key pair has two parts: your <strong>public key</strong> identifies your
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import cx from "classnames"
|
import cx from "classnames"
|
||||||
import type {ClientOptions} from "@pomade/core"
|
import type {ClientOptions} from "@pomade/core"
|
||||||
|
import {assoc} from "@welshman/lib"
|
||||||
import {makeSecret, RELAYS, MESSAGING_RELAYS, makeEvent} from "@welshman/util"
|
import {makeSecret, RELAYS, MESSAGING_RELAYS, makeEvent} from "@welshman/util"
|
||||||
import {Profile} from "@welshman/domain"
|
import {Profile} from "@welshman/domain"
|
||||||
import {Thunks, nip01, publish, toSession} from "@welshman/app"
|
import {Thunks, nip01, publish, toSession} from "@welshman/app"
|
||||||
|
|
@ -13,7 +14,6 @@
|
||||||
import ModalBody from "@lib/components/ModalBody.svelte"
|
import ModalBody from "@lib/components/ModalBody.svelte"
|
||||||
import ModalTitle from "@lib/components/ModalTitle.svelte"
|
import ModalTitle from "@lib/components/ModalTitle.svelte"
|
||||||
import LogIn from "@app/components/LogIn.svelte"
|
import LogIn from "@app/components/LogIn.svelte"
|
||||||
import SignUpKey from "@app/components/SignUpKey.svelte"
|
|
||||||
import SignUpEmail from "@app/components/SignUpEmail.svelte"
|
import SignUpEmail from "@app/components/SignUpEmail.svelte"
|
||||||
import SignUpProfile from "@app/components/SignUpProfile.svelte"
|
import SignUpProfile from "@app/components/SignUpProfile.svelte"
|
||||||
import type {ProfileValues} from "@app/components/ProfileEditForm.svelte"
|
import type {ProfileValues} from "@app/components/ProfileEditForm.svelte"
|
||||||
|
|
@ -27,6 +27,7 @@
|
||||||
DEFAULT_SPACES,
|
DEFAULT_SPACES,
|
||||||
} from "@app/env"
|
} from "@app/env"
|
||||||
import {setChecked} from "@app/notifications"
|
import {setChecked} from "@app/notifications"
|
||||||
|
import {forceHealthChecks} from "@app/healthChecks"
|
||||||
import {loginWithPomade} from "@app/pomade"
|
import {loginWithPomade} from "@app/pomade"
|
||||||
import {pushModal, clearModals} from "@app/modal"
|
import {pushModal, clearModals} from "@app/modal"
|
||||||
import {app, domain, login, roomLists} from "@app/core"
|
import {app, domain, login, roomLists} from "@app/core"
|
||||||
|
|
@ -89,14 +90,14 @@
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
nostr: {
|
nostr: {
|
||||||
start: () => pushModal(SignUpProfile, {next: flows.nostr.key, step: 1, totalSteps: 3}),
|
start: () => pushModal(SignUpProfile, {next: flows.nostr.complete, step: 1, totalSteps: 2}),
|
||||||
key: () => pushModal(SignUpKey, {next: flows.nostr.complete, step: 2, totalSteps: 3}),
|
|
||||||
complete: () =>
|
complete: () =>
|
||||||
pushModal(SignUpComplete, {next: flows.nostr.finalize, step: 3, totalSteps: 3}),
|
pushModal(SignUpComplete, {next: flows.nostr.finalize, step: 2, totalSteps: 2}),
|
||||||
finalize: async () => {
|
finalize: async () => {
|
||||||
const secret = getKey<string>("signup.secret")!
|
const secret = getKey<string>("signup.secret")!
|
||||||
|
|
||||||
await login(toSession(nip01, {secret}))
|
await login(toSession(nip01, {secret}))
|
||||||
|
forceHealthChecks.update(assoc("backup-key", true))
|
||||||
await completeSignup()
|
await completeSignup()
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -41,8 +41,8 @@
|
||||||
<ModalTitle>You're all set!</ModalTitle>
|
<ModalTitle>You're all set!</ModalTitle>
|
||||||
</ModalHeader>
|
</ModalHeader>
|
||||||
<p>
|
<p>
|
||||||
You've created your profile, saved your keys, and now you're ready to start chatting — all
|
You've created your profile, and now you're ready to start chatting — all without asking
|
||||||
without asking permission!
|
permission!
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
From your dashboard, you can use invite links, discover community spaces, and keep up-to-date
|
From your dashboard, you can use invite links, discover community spaces, and keep up-to-date
|
||||||
|
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import {getKey} from "@lib/implicit"
|
|
||||||
import KeyDownload from "@app/components/KeyDownload.svelte"
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
next: () => void
|
|
||||||
step?: number
|
|
||||||
totalSteps?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
const {next, step, totalSteps}: Props = $props()
|
|
||||||
|
|
||||||
const secret = getKey<string>("signup.secret")!
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<KeyDownload {secret} {next} {step} {totalSteps} />
|
|
||||||
|
|
@ -1,17 +1,18 @@
|
||||||
import {derived} from "svelte/store"
|
import {derived, get, writable} from "svelte/store"
|
||||||
import {sample} from "@welshman/lib"
|
import {assoc, sample} from "@welshman/lib"
|
||||||
import {
|
import {
|
||||||
MessagingRelayLists,
|
MessagingRelayLists,
|
||||||
RelayLists,
|
RelayLists,
|
||||||
Relays,
|
Relays,
|
||||||
SearchRelayLists,
|
SearchRelayLists,
|
||||||
projectFrom,
|
|
||||||
projection,
|
projection,
|
||||||
publish,
|
publish,
|
||||||
} from "@welshman/app"
|
} from "@welshman/app"
|
||||||
import type {IApp, Projection} from "@welshman/app"
|
import type {IApp, Projection} from "@welshman/app"
|
||||||
import {usePlugin} from "@app/core"
|
import KeyDownload from "@app/components/KeyDownload.svelte"
|
||||||
|
import {session, usePlugin} from "@app/core"
|
||||||
import {DEFAULT_RELAYS, DEFAULT_MESSAGING_RELAYS} from "@app/env"
|
import {DEFAULT_RELAYS, DEFAULT_MESSAGING_RELAYS} from "@app/env"
|
||||||
|
import {pushModal} from "@app/modal"
|
||||||
|
|
||||||
export type HealthCheckContext = {
|
export type HealthCheckContext = {
|
||||||
readRelays: string[]
|
readRelays: string[]
|
||||||
|
|
@ -21,6 +22,7 @@ export type HealthCheckContext = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type HealthCheck = {
|
export type HealthCheck = {
|
||||||
|
id: string
|
||||||
title: string
|
title: string
|
||||||
description: string
|
description: string
|
||||||
action: string
|
action: string
|
||||||
|
|
@ -28,6 +30,8 @@ export type HealthCheck = {
|
||||||
apply: (context: HealthCheckContext) => unknown
|
apply: (context: HealthCheckContext) => unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const forceHealthChecks = writable<Record<string, boolean>>({})
|
||||||
|
|
||||||
export class HealthChecks {
|
export class HealthChecks {
|
||||||
context: Projection<HealthCheckContext>
|
context: Projection<HealthCheckContext>
|
||||||
pending: Projection<HealthCheck[]>
|
pending: Projection<HealthCheck[]>
|
||||||
|
|
@ -55,8 +59,10 @@ export class HealthChecks {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
this.pending = projectFrom(this.context, $context =>
|
this.pending = projection(
|
||||||
this.checks.filter(check => check.isPending($context)),
|
derived([this.context.$, forceHealthChecks], ([$context, $forceHealthChecks]) =>
|
||||||
|
this.checks.filter(check => $forceHealthChecks[check.id] ?? check.isPending($context)),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -64,6 +70,7 @@ export class HealthChecks {
|
||||||
|
|
||||||
private checks: HealthCheck[] = [
|
private checks: HealthCheck[] = [
|
||||||
{
|
{
|
||||||
|
id: "missing-inbox-relays",
|
||||||
title: "Missing Inbox Relays",
|
title: "Missing Inbox Relays",
|
||||||
description: "Other people aren't currently able to reliably tag you in public notes.",
|
description: "Other people aren't currently able to reliably tag you in public notes.",
|
||||||
action: "Update",
|
action: "Update",
|
||||||
|
|
@ -71,6 +78,7 @@ export class HealthChecks {
|
||||||
apply: () => this.app.use(RelayLists).setReadUrls(DEFAULT_RELAYS).then(publish),
|
apply: () => this.app.use(RelayLists).setReadUrls(DEFAULT_RELAYS).then(publish),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
id: "missing-outbox-relays",
|
||||||
title: "Missing Outbox Relays",
|
title: "Missing Outbox Relays",
|
||||||
description: "Other people aren't currently able to reliably find your public notes.",
|
description: "Other people aren't currently able to reliably find your public notes.",
|
||||||
action: "Update",
|
action: "Update",
|
||||||
|
|
@ -78,6 +86,7 @@ export class HealthChecks {
|
||||||
apply: () => this.app.use(RelayLists).setWriteUrls(DEFAULT_RELAYS).then(publish),
|
apply: () => this.app.use(RelayLists).setWriteUrls(DEFAULT_RELAYS).then(publish),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
id: "missing-dm-relays",
|
||||||
title: "Missing DM Relays",
|
title: "Missing DM Relays",
|
||||||
description: "You aren't currently able to reliably send or receive direct messages.",
|
description: "You aren't currently able to reliably send or receive direct messages.",
|
||||||
action: "Update",
|
action: "Update",
|
||||||
|
|
@ -86,6 +95,7 @@ export class HealthChecks {
|
||||||
this.app.use(MessagingRelayLists).setUrls(DEFAULT_MESSAGING_RELAYS).then(publish),
|
this.app.use(MessagingRelayLists).setUrls(DEFAULT_MESSAGING_RELAYS).then(publish),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
id: "too-many-inbox-relays",
|
||||||
title: "Too Many Inbox Relays",
|
title: "Too Many Inbox Relays",
|
||||||
description:
|
description:
|
||||||
"You have more inbox relays than is really necessary, which can affect resource usage.",
|
"You have more inbox relays than is really necessary, which can affect resource usage.",
|
||||||
|
|
@ -95,6 +105,7 @@ export class HealthChecks {
|
||||||
this.app.use(RelayLists).setReadUrls(sample(5, context.readRelays)).then(publish),
|
this.app.use(RelayLists).setReadUrls(sample(5, context.readRelays)).then(publish),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
id: "too-many-outbox-relays",
|
||||||
title: "Too Many Outbox Relays",
|
title: "Too Many Outbox Relays",
|
||||||
description:
|
description:
|
||||||
"You have more outbox relays than is really necessary, which can affect resource usage.",
|
"You have more outbox relays than is really necessary, which can affect resource usage.",
|
||||||
|
|
@ -104,6 +115,7 @@ export class HealthChecks {
|
||||||
this.app.use(RelayLists).setWriteUrls(sample(5, context.writeRelays)).then(publish),
|
this.app.use(RelayLists).setWriteUrls(sample(5, context.writeRelays)).then(publish),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
id: "too-many-dm-relays",
|
||||||
title: "Too Many DM Relays",
|
title: "Too Many DM Relays",
|
||||||
description:
|
description:
|
||||||
"You have more DM relays than is really necessary, which can affect resource usage.",
|
"You have more DM relays than is really necessary, which can affect resource usage.",
|
||||||
|
|
@ -113,6 +125,7 @@ export class HealthChecks {
|
||||||
this.app.use(MessagingRelayLists).setUrls(sample(5, context.messagingRelays)).then(publish),
|
this.app.use(MessagingRelayLists).setUrls(sample(5, context.messagingRelays)).then(publish),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
id: "invalid-search-relays",
|
||||||
title: "Invalid Search Relays",
|
title: "Invalid Search Relays",
|
||||||
description: "Some of your search relays don't support search.",
|
description: "Some of your search relays don't support search.",
|
||||||
action: "Remove Invalid",
|
action: "Remove Invalid",
|
||||||
|
|
@ -123,9 +136,29 @@ export class HealthChecks {
|
||||||
.setUrls(context.searchRelays.filter(this.supportsSearch))
|
.setUrls(context.searchRelays.filter(this.supportsSearch))
|
||||||
.then(publish),
|
.then(publish),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "backup-key",
|
||||||
|
title: "Back Up Your Key",
|
||||||
|
description: "Save a backup of your private key in a secure place.",
|
||||||
|
action: "Back Up",
|
||||||
|
isPending: () => false,
|
||||||
|
apply: () => {
|
||||||
|
const {secret} = session.get()!.data as {secret: string}
|
||||||
|
|
||||||
|
pushModal(KeyDownload, {
|
||||||
|
secret,
|
||||||
|
next: () => {
|
||||||
|
forceHealthChecks.update(assoc("backup-key", false))
|
||||||
|
history.back()
|
||||||
|
},
|
||||||
|
submitText: "Done",
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
isPending = (healthCheck: HealthCheck) => healthCheck.isPending(this.context.get())
|
isPending = (healthCheck: HealthCheck) =>
|
||||||
|
get(forceHealthChecks)[healthCheck.id] ?? healthCheck.isPending(this.context.get())
|
||||||
|
|
||||||
apply = (healthCheck: HealthCheck) => healthCheck.apply(this.context.get())
|
apply = (healthCheck: HealthCheck) => healthCheck.apply(this.context.get())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@
|
||||||
import {kv, ss, storage} from "@app/storage"
|
import {kv, ss, storage} from "@app/storage"
|
||||||
import {device} from "@app/device"
|
import {device} from "@app/device"
|
||||||
import {userSettingsValues, notificationSettings} from "@app/settings"
|
import {userSettingsValues, notificationSettings} from "@app/settings"
|
||||||
|
import {forceHealthChecks} from "@app/healthChecks"
|
||||||
import {setupShareIntents, shareFromNative} from "@app/share"
|
import {setupShareIntents, shareFromNative} from "@app/share"
|
||||||
import {shouldUnwrap, syncApplicationData} from "@app/sync"
|
import {shouldUnwrap, syncApplicationData} from "@app/sync"
|
||||||
import * as env from "@app/env"
|
import * as env from "@app/env"
|
||||||
|
|
@ -194,6 +195,11 @@
|
||||||
store: pushState,
|
store: pushState,
|
||||||
storage: kv,
|
storage: kv,
|
||||||
}),
|
}),
|
||||||
|
sync({
|
||||||
|
key: "forceHealthChecks",
|
||||||
|
store: forceHealthChecks,
|
||||||
|
storage: kv,
|
||||||
|
}),
|
||||||
])
|
])
|
||||||
|
|
||||||
// Wait for critical storage data only
|
// Wait for critical storage data only
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue