Overhaul relay settings page

This commit is contained in:
Jon Staab
2026-03-11 15:48:36 -07:00
parent cd54bc2880
commit 99966a976e
12 changed files with 470 additions and 212 deletions
+65 -13
View File
@@ -1,30 +1,70 @@
<script lang="ts">
import {onMount} from "svelte"
import {SvelteSet} from "svelte/reactivity"
import type {Readable} from "svelte/store"
import {tryCatch} from "@welshman/lib"
import {isShareableRelayUrl, normalizeRelayUrl} from "@welshman/util"
import {relaySearch} from "@welshman/app"
import {isShareableRelayUrl, isIPAddress, normalizeRelayUrl} from "@welshman/util"
import type {Thunk} from "@welshman/app"
import {waitForThunkError, relaySearch} from "@welshman/app"
import {createScroller} from "@lib/html"
import {errorMessage} from "@lib/util"
import Magnifier from "@assets/icons/magnifier.svg?dataurl"
import AddCircle from "@assets/icons/add-circle.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
import Modal from "@lib/components/Modal.svelte"
import ModalBody from "@lib/components/ModalBody.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte"
import RelayItem from "@app/components/RelayItem.svelte"
import {pushToast} from "@app/util/toast"
interface Props {
relays: Readable<string[]>
addRelay: (url: string) => void
addRelay: (url: string) => Promise<Thunk>
matchRelay?: (url: string) => boolean
}
const {relays, addRelay}: Props = $props()
const {relays, addRelay, matchRelay}: Props = $props()
const back = () => history.back()
const customUrl = $derived(tryCatch(() => normalizeRelayUrl(term)))
const add = async (url: string) => {
loading.add(url)
try {
const error = await waitForThunkError(await addRelay(url))
if (error) {
pushToast({
theme: "error",
message: `Failed to add relay: ${errorMessage(error)}`,
})
}
} finally {
loading.delete(url)
}
}
let term = $state("")
let limit = $state(20)
let element: Element | undefined = $state()
const customUrl = $derived(tryCatch(() => normalizeRelayUrl(term)))
const loading = $state(new SvelteSet<string>())
const searchResults = $derived(
$relaySearch
.searchValues(term)
.filter(url => {
if (matchRelay?.(url) === false) return false
if ($relays.includes(url)) return false
if (isIPAddress(url)) return false
return true
})
.slice(0, limit),
)
onMount(() => {
const scroller = createScroller({
@@ -52,23 +92,35 @@
<RelayItem url={term}>
<Button
class="btn btn-outline btn-sm flex items-center"
onclick={() => addRelay(customUrl)}>
<Icon icon={AddCircle} />
disabled={loading.has(customUrl)}
onclick={() => add(customUrl)}>
{#if loading.has(customUrl)}
<span class="loading loading-spinner loading-sm"></span>
{:else}
<Icon icon={AddCircle} />
{/if}
Add Relay
</Button>
</RelayItem>
{/if}
{#each $relaySearch
.searchValues(term)
.filter(url => !$relays.includes(url))
.slice(0, limit) as url (url)}
{#each searchResults as url (url)}
<RelayItem {url}>
<Button class="btn btn-outline btn-sm flex items-center" onclick={() => addRelay(url)}>
<Icon icon={AddCircle} />
<Button
class="btn btn-outline btn-sm flex items-center"
disabled={loading.has(url)}
onclick={() => add(url)}>
{#if loading.has(url)}
<span class="loading loading-spinner loading-sm"></span>
{:else}
<Icon icon={AddCircle} />
{/if}
Add Relay
</Button>
</RelayItem>
{/each}
</div>
</ModalBody>
<ModalFooter>
<Button class="btn btn-primary flex-grow" onclick={back}>Done</Button>
</ModalFooter>
</Modal>
+85
View File
@@ -0,0 +1,85 @@
<script lang="ts">
import type {Readable} from "svelte/store"
import {SvelteSet} from "svelte/reactivity"
import {waitForThunkError} from "@welshman/app"
import type {Thunk} from "@welshman/app"
import CloseCircle from "@assets/icons/close-circle.svg?dataurl"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import AltArrowRight from "@assets/icons/alt-arrow-right.svg?dataurl"
import {errorMessage} from "@lib/util"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
import Modal from "@lib/components/Modal.svelte"
import ModalBody from "@lib/components/ModalBody.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte"
import RelayAdd from "@app/components/RelayAdd.svelte"
import RelayItem from "@app/components/RelayItem.svelte"
import {pushModal} from "@app/util/modal"
import {pushToast} from "@app/util/toast"
interface Props {
title: string
subtitle: string
relays: Readable<string[]>
addRelay: (url: string) => Promise<Thunk>
removeRelay: (url: string) => Promise<Thunk>
matchRelay?: (url: string) => boolean
}
const {title, subtitle, relays, addRelay, removeRelay, matchRelay}: Props = $props()
const back = () => history.back()
const add = () => pushModal(RelayAdd, {relays, addRelay, matchRelay})
const remove = async (url: string) => {
loading.add(url)
try {
const error = await waitForThunkError(await removeRelay(url))
if (error) {
pushToast({
theme: "error",
message: `Failed to remove relay: ${errorMessage(error)}`,
})
}
} finally {
loading.delete(url)
}
}
const loading = $state(new SvelteSet<string>())
</script>
<Modal>
<ModalBody>
<h2 class="text-xl">{title}</h2>
<p class="text-sm">{subtitle}</p>
{#each $relays.toSorted() as url (url)}
<RelayItem {url}>
<Button
class="btn btn-sm btn-neutral"
disabled={loading.has(url)}
onclick={() => remove(url)}>
{#if loading.has(url)}
<span class="loading loading-spinner loading-sm"></span>
{:else}
<Icon icon={CloseCircle} />
{/if}
Remove
</Button>
</RelayItem>
{/each}
</ModalBody>
<ModalFooter>
<Button class="btn btn-link" onclick={back}>
<Icon icon={AltArrowLeft} />
Go back
</Button>
<Button class="btn btn-primary" onclick={add}>
Add Relays
<Icon icon={AltArrowRight} />
</Button>
</ModalFooter>
</Modal>
@@ -0,0 +1,27 @@
<script lang="ts" module>
export type ActionItem = {
title: string
subtitle: string
action: string
apply: () => unknown
}
</script>
<script lang="ts">
import Stars from "@assets/icons/stars.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
const {title, action, subtitle, apply}: ActionItem = $props()
</script>
<div class="card2 card2-sm bg-alt flex justify-between">
<div class="flex flex-col gap-1">
<strong>{title}</strong>
<p class="text-sm">{subtitle}</p>
</div>
<Button class="btn btn-neutral btn-sm" onclick={apply}>
<Icon icon={Stars} />
{action}
</Button>
</div>
@@ -0,0 +1,58 @@
<script lang="ts">
import type {Readable} from "svelte/store"
import Stars from "@assets/icons/stars.svg?dataurl"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
import DangerTriangle from "@assets/icons/danger-triangle.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
import Modal from "@lib/components/Modal.svelte"
import ModalBody from "@lib/components/ModalBody.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte"
import type {ActionItem} from "@app/components/RelaySettingsActionItem.svelte"
import RelaySettingsActionItem from "@app/components/RelaySettingsActionItem.svelte"
interface Props {
actionItems: Readable<ActionItem[]>
}
const {actionItems}: Props = $props()
const back = () => history.back()
const applyAll = () => {
for (const actionItem of $actionItems) {
actionItem.apply()
}
}
$effect(() => {
if ($actionItems.length === 0) {
back()
}
})
</script>
<Modal>
<ModalBody>
<div class="flex gap-2 items-center">
<Icon icon={DangerTriangle} />
<strong class="text-lg">Action Items</strong>
</div>
<p class="text-sm">
Below are recommendations for adjustments to your relay selections that you might consider.
</p>
{#each $actionItems as actionItem}
<RelaySettingsActionItem {...actionItem} />
{/each}
</ModalBody>
<ModalFooter>
<Button class="btn btn-link" onclick={back}>
<Icon icon={AltArrowLeft} />
Go Back
</Button>
<Button class="btn btn-primary" onclick={applyAll}>
<Icon icon={Stars} />
Apply All Recommendations
</Button>
</ModalFooter>
</Modal>
@@ -0,0 +1,50 @@
<script lang="ts">
import type {Readable} from "svelte/store"
import Check from "@assets/icons/check.svg?dataurl"
import DangerTriangle from "@assets/icons/danger-triangle.svg?dataurl"
import Icon from "@lib/components/Icon.svelte"
import RelayList from "@app/components/RelayList.svelte"
import {pushModal} from "@app/util/modal"
interface Props {
icon: string
title: string
subtitle: string
relays: Readable<string[]>
addRelay: (url: string) => unknown
removeRelay: (url: string) => unknown
matchRelay?: (url: string) => boolean
}
const {icon, title, relays, subtitle, addRelay, removeRelay, matchRelay}: Props = $props()
const onclick = () =>
pushModal(RelayList, {title, subtitle, relays, addRelay, removeRelay, matchRelay})
</script>
<button
type="button"
class="btn font-normal flex h-[unset] w-full flex-nowrap py-4 text-left items-start justify-between"
{onclick}>
<div class="flex flex-grow flex-row items-start gap-4">
<div class="flex h-7 w-7 flex-shrink-0 items-center justify-center">
<Icon {icon} />
</div>
<div class="flex flex-col gap-1">
<p class="text-lg">
{title}
</p>
<p class="text-sm">
{subtitle}
</p>
</div>
</div>
<div class="flex items-center justify-end gap-1">
{#if $relays.length <= 1}
<Icon icon={DangerTriangle} />
{:else}
<Icon icon={Check} />
{/if}
{$relays.length}
</div>
</button>