Move relay action items to top of page

This commit is contained in:
Jon Staab 2026-03-19 12:41:23 -07:00
parent 1f5be54cb1
commit f9c7ed4936
7 changed files with 142 additions and 158 deletions

View file

@ -1,58 +1,140 @@
<script lang="ts"> <script lang="ts">
import type {Readable} from "svelte/store" import {derived} from "svelte/store"
import {shuffle, partition, ifLet} from "@welshman/lib"
import {RelayMode} from "@welshman/util"
import {
pubkey,
derivePubkeyRelays,
getRelay,
setWriteRelays,
setReadRelays,
setSearchRelays,
setMessagingRelays,
} from "@welshman/app"
import Stars from "@assets/icons/stars.svg?dataurl" import Stars from "@assets/icons/stars.svg?dataurl"
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl" import CheckCircle from "@assets/icons/check-circle.svg?dataurl"
import DangerTriangle from "@assets/icons/danger-triangle.svg?dataurl" import DangerTriangle from "@assets/icons/danger-triangle.svg?dataurl"
import Stethoscope from "@assets/icons/stethoscope.svg?dataurl"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.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 type {ActionItem} from "@app/components/RelaySettingsActionItem.svelte"
import RelaySettingsActionItem from "@app/components/RelaySettingsActionItem.svelte" import RelaySettingsActionItem from "@app/components/RelaySettingsActionItem.svelte"
import {hasNip50, DEFAULT_RELAYS, DEFAULT_MESSAGING_RELAYS, PLATFORM_NAME} from "@app/core/state"
interface Props { const readRelayUrls = derivePubkeyRelays($pubkey!, RelayMode.Read)
actionItems: Readable<ActionItem[]> const writeRelayUrls = derivePubkeyRelays($pubkey!, RelayMode.Write)
} const searchRelayUrls = derivePubkeyRelays($pubkey!, RelayMode.Search)
const messagingRelayUrls = derivePubkeyRelays($pubkey!, RelayMode.Messaging)
const {actionItems}: Props = $props() const actionItems = derived(
[readRelayUrls, writeRelayUrls, messagingRelayUrls, searchRelayUrls],
([$readRelayUrls, $writeRelayUrls, $messagingRelayUrls, $searchRelayUrls]) => {
const $actionItems: ActionItem[] = []
const back = () => history.back() if ($readRelayUrls.length <= 1) {
$actionItems.push({
title: "Missing Inbox Relays",
subtitle: "Other people aren't currently able to reliably tag you in public notes.",
action: "Update",
apply: () => setReadRelays(DEFAULT_RELAYS),
})
}
if ($writeRelayUrls.length <= 1) {
$actionItems.push({
title: "Missing Outbox Relays",
subtitle: "Other people aren't currently able to reliably find your public notes.",
action: "Update",
apply: () => setWriteRelays(DEFAULT_RELAYS),
})
}
if ($messagingRelayUrls.length <= 1) {
$actionItems.push({
title: "Missing DM Relays",
subtitle: "You aren't currently able to reliably send or receive direct messages.",
action: "Update",
apply: () => setMessagingRelays(DEFAULT_MESSAGING_RELAYS),
})
}
if ($readRelayUrls.length > 8) {
$actionItems.push({
title: "Too Many Inbox Relays",
subtitle:
"You have more inbox relays than is really necessary, which can affect resource usage.",
action: "Prune Selections",
apply: () => setReadRelays(shuffle($readRelayUrls).slice(0, 5)),
})
}
if ($writeRelayUrls.length > 8) {
$actionItems.push({
title: "Too Many Outbox Relays",
subtitle:
"You have more outbox relays than is really necessary, which can affect resource usage.",
action: "Prune Selections",
apply: () => setWriteRelays(shuffle($writeRelayUrls).slice(0, 5)),
})
}
if ($messagingRelayUrls.length > 8) {
$actionItems.push({
title: "Too Many DM Relays",
subtitle:
"You have more DM relays than is really necessary, which can affect resource usage.",
action: "Prune Selections",
apply: () => setMessagingRelays(shuffle($messagingRelayUrls).slice(0, 5)),
})
}
const [okSearchRelays, badSearchRelays] = partition(
url => Boolean(ifLet(getRelay(url), hasNip50)),
$searchRelayUrls,
)
if (badSearchRelays.length > 0) {
$actionItems.push({
title: "Invalid Search Relays",
subtitle: `Some of your search relays don't support search.`,
action: "Remove Invalid",
apply: () => setSearchRelays(okSearchRelays),
})
}
return $actionItems
},
)
const applyAll = () => { const applyAll = () => {
for (const actionItem of $actionItems) { for (const actionItem of $actionItems) {
actionItem.apply() actionItem.apply()
} }
} }
$effect(() => {
if ($actionItems.length === 0) {
back()
}
})
</script> </script>
<Modal> <div class="card2 bg-alt flex flex-col gap-4 shadow-md">
<ModalBody> <div class="flex justify-between items-center">
<div class="flex gap-2 items-center"> <strong class="flex items-center gap-3 text-lg">
<Icon icon={DangerTriangle} /> <Icon icon={Stethoscope} />
<strong class="text-lg">Action Items</strong> Health Check
</div> </strong>
<p class="text-sm"> <span class="flex items-center gap-2 text-sm">
Below are recommendations for adjustments to your relay selections that you might consider. <Icon icon={$actionItems.length === 0 ? CheckCircle : DangerTriangle} />
</p> {$actionItems.length} Issue{$actionItems.length === 1 ? "" : "s"} Detected
{#each $actionItems as actionItem} </span>
<RelaySettingsActionItem {...actionItem} /> </div>
{/each} <p>
</ModalBody> {PLATFORM_NAME} actively checks your connection to the network in the background to discover relays
<ModalFooter> that are offline, that you don't have access to, or are otherwise causing trouble.
<Button class="btn btn-link" onclick={back}> </p>
<Icon icon={AltArrowLeft} /> {#each $actionItems as actionItem}
Go Back <RelaySettingsActionItem {...actionItem} />
</Button> {/each}
{#if $actionItems.length > 0}
<Button class="btn btn-primary" onclick={applyAll}> <Button class="btn btn-primary" onclick={applyAll}>
<Icon icon={Stars} /> <Icon icon={Stars} />
Apply All Recommendations Apply All Recommendations
</Button> </Button>
</ModalFooter> {/if}
</Modal> </div>

View file

@ -26,7 +26,7 @@
const back = () => history.back() const back = () => history.back()
const onDelete = () => { const onResolved = () => {
if ($reports.size === 0) { if ($reports.size === 0) {
back() back()
} }
@ -40,7 +40,7 @@
<ModalSubtitle>All reports for this event are shown below.</ModalSubtitle> <ModalSubtitle>All reports for this event are shown below.</ModalSubtitle>
</ModalHeader> </ModalHeader>
{#each $reports.values() as report (report.id)} {#each $reports.values() as report (report.id)}
<ReportItem {url} event={report} {onDelete} /> <ReportItem {url} event={report} {onResolved} />
{/each} {/each}
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>

View file

@ -20,10 +20,10 @@
type Props = { type Props = {
url: string url: string
event: TrustedEvent event: TrustedEvent
onDelete?: () => void onResolved?: () => void
} }
const {url, event, onDelete}: Props = $props() const {url, event, onResolved}: Props = $props()
const shouldProtect = canEnforceNip70(url) const shouldProtect = canEnforceNip70(url)
const userIsAdmin = deriveUserIsSpaceAdmin(url) const userIsAdmin = deriveUserIsSpaceAdmin(url)
@ -40,7 +40,7 @@
const deleteReport = async () => { const deleteReport = async () => {
publishDelete({event, relays: [url], protect: await shouldProtect}) publishDelete({event, relays: [url], protect: await shouldProtect})
onDelete?.() onResolved?.()
} }
const dismissReport = async () => { const dismissReport = async () => {
@ -54,7 +54,7 @@
} else { } else {
pushToast({message: "Content has successfully been deleted!"}) pushToast({message: "Content has successfully been deleted!"})
repository.removeEvent(event.id) repository.removeEvent(event.id)
onDelete?.() onResolved?.()
} }
} }
@ -77,7 +77,7 @@
repository.removeEvent(event.id) repository.removeEvent(event.id)
repository.removeEvent(id) repository.removeEvent(id)
history.back() history.back()
setTimeout(() => onDelete?.()) setTimeout(() => onResolved?.())
} }
}, },
}) })
@ -101,7 +101,7 @@
pushToast({message: "User has successfully been banned!"}) pushToast({message: "User has successfully been banned!"})
repository.removeEvent(event.id) repository.removeEvent(event.id)
history.back() history.back()
setTimeout(() => onDelete?.()) setTimeout(() => onResolved?.())
} }
}, },
}) })

View file

@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import {getTagValue} from "@welshman/util" import {getTagValue, ManagementMethod} from "@welshman/util"
import type {TrustedEvent, PublishedRoomMeta} from "@welshman/util" import type {TrustedEvent, PublishedRoomMeta} from "@welshman/util"
import {repository} from "@welshman/app" import {repository, manageRelay} from "@welshman/app"
import Button from "@lib/components/Button.svelte" import Button from "@lib/components/Button.svelte"
import ProfileName from "@app/components/ProfileName.svelte" import ProfileName from "@app/components/ProfileName.svelte"
import ProfileDetail from "@app/components/ProfileDetail.svelte" import ProfileDetail from "@app/components/ProfileDetail.svelte"
@ -30,7 +30,7 @@
try { try {
const {error} = await manageRelay(url, { const {error} = await manageRelay(url, {
method: ManagementMethod.BanEvent, method: ManagementMethod.BanEvent,
params: [event.id, "Join request dismissed"] params: [event.id, "Join request dismissed"],
}) })
if (error) { if (error) {

View file

@ -23,7 +23,7 @@
const back = () => history.back() const back = () => history.back()
const onDelete = () => { const onResolved = () => {
if ($reports.length === 0) { if ($reports.length === 0) {
back() back()
} }
@ -38,7 +38,7 @@
</ModalHeader> </ModalHeader>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
{#each $reports as event (event.id)} {#each $reports as event (event.id)}
<ReportItem {url} {event} {onDelete} /> <ReportItem {url} {event} {onResolved} />
{:else} {:else}
<p class="py-12 text-center">No reports found.</p> <p class="py-12 text-center">No reports found.</p>
{/each} {/each}

View file

@ -917,7 +917,8 @@ export const deriveSpaceActionItems = (url: string) =>
}, },
]), ]),
$events => { $events => {
const getRoomId = (e: TrustedEvent) => getTagValue(e.kind === ROOM_MEMBERS ? "d" : "h", e.tags) const getRoomId = (e: TrustedEvent) =>
getTagValue(e.kind === ROOM_MEMBERS ? "d" : "h", e.tags)
const reports = $events.filter(e => e.kind === REPORT) const reports = $events.filter(e => e.kind === REPORT)
const pendingJoins: TrustedEvent[] = [] const pendingJoins: TrustedEvent[] = []
@ -938,8 +939,7 @@ export const deriveSpaceActionItems = (url: string) =>
).filter(({pubkey, created_at}) => { ).filter(({pubkey, created_at}) => {
if (roomMembers.includes(pubkey)) return false if (roomMembers.includes(pubkey)) return false
if (gt(roomMembersEvent?.created_at, created_at)) return false if (gt(roomMembersEvent?.created_at, created_at)) return false
if (roomLeaves.some(e => e.pubkey === pubkey && e.created_at > created_at)) if (roomLeaves.some(e => e.pubkey === pubkey && e.created_at > created_at)) return false
return false
return true return true
}), }),

View file

@ -1,7 +1,5 @@
<script lang="ts"> <script lang="ts">
import {onMount} from "svelte" import {onMount} from "svelte"
import {derived} from "svelte/store"
import {shuffle, partition, ifLet} from "@welshman/lib"
import { import {
pubkey, pubkey,
getRelayLists, getRelayLists,
@ -15,10 +13,6 @@
addSearchRelay, addSearchRelay,
removeSearchRelay, removeSearchRelay,
getRelay, getRelay,
setWriteRelays,
setReadRelays,
setSearchRelays,
setMessagingRelays,
} from "@welshman/app" } from "@welshman/app"
import {RelayMode} from "@welshman/util" import {RelayMode} from "@welshman/util"
import Plane from "@assets/icons/plane.svg?dataurl" import Plane from "@assets/icons/plane.svg?dataurl"
@ -26,15 +20,11 @@
import Server from "@assets/icons/server.svg?dataurl" import Server from "@assets/icons/server.svg?dataurl"
import Mailbox from "@assets/icons/mailbox.svg?dataurl" import Mailbox from "@assets/icons/mailbox.svg?dataurl"
import Magnifier from "@assets/icons/magnifier.svg?dataurl" import Magnifier from "@assets/icons/magnifier.svg?dataurl"
import DangerTriangle from "@assets/icons/danger-triangle.svg?dataurl"
import ForbiddenCircle from "@assets/icons/forbidden-circle.svg?dataurl" import ForbiddenCircle from "@assets/icons/forbidden-circle.svg?dataurl"
import Icon from "@lib/components/Icon.svelte" import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
import RelaySettingsItem from "@app/components/RelaySettingsItem.svelte" import RelaySettingsItem from "@app/components/RelaySettingsItem.svelte"
import type {ActionItem} from "@app/components/RelaySettingsActionItem.svelte"
import RelaySettingsActionItems from "@app/components/RelaySettingsActionItems.svelte" import RelaySettingsActionItems from "@app/components/RelaySettingsActionItems.svelte"
import {pushModal} from "@app/util/modal" import {hasNip50} from "@app/core/state"
import {hasNip50, DEFAULT_RELAYS, DEFAULT_MESSAGING_RELAYS} from "@app/core/state"
import {discoverRelays} from "@app/core/requests" import {discoverRelays} from "@app/core/requests"
const readRelayUrls = derivePubkeyRelays($pubkey!, RelayMode.Read) const readRelayUrls = derivePubkeyRelays($pubkey!, RelayMode.Read)
@ -47,108 +37,20 @@
const removeReadRelay = (url: string) => removeRelay(url, RelayMode.Read) const removeReadRelay = (url: string) => removeRelay(url, RelayMode.Read)
const addWriteRelay = (url: string) => addRelay(url, RelayMode.Write) const addWriteRelay = (url: string) => addRelay(url, RelayMode.Write)
const removeWriteRelay = (url: string) => removeRelay(url, RelayMode.Write) const removeWriteRelay = (url: string) => removeRelay(url, RelayMode.Write)
const showActionItems = () => pushModal(RelaySettingsActionItems, {actionItems})
const actionItems = derived(
[readRelayUrls, writeRelayUrls, messagingRelayUrls, searchRelayUrls],
([$readRelayUrls, $writeRelayUrls, $messagingRelayUrls, $searchRelayUrls]) => {
const $actionItems: ActionItem[] = []
if ($readRelayUrls.length <= 1) {
$actionItems.push({
title: "Missing Inbox Relays",
subtitle: "Other people aren't currently able to reliably tag you in public notes.",
action: "Update",
apply: () => setReadRelays(DEFAULT_RELAYS),
})
}
if ($writeRelayUrls.length <= 1) {
$actionItems.push({
title: "Missing Outbox Relays",
subtitle: "Other people aren't currently able to reliably find your public notes.",
action: "Update",
apply: () => setWriteRelays(DEFAULT_RELAYS),
})
}
if ($messagingRelayUrls.length <= 1) {
$actionItems.push({
title: "Missing DM Relays",
subtitle: "You aren't currently able to reliably send or receive direct messages.",
action: "Update",
apply: () => setMessagingRelays(DEFAULT_MESSAGING_RELAYS),
})
}
if ($readRelayUrls.length > 8) {
$actionItems.push({
title: "Too Many Inbox Relays",
subtitle:
"You have more inbox relays than is really necessary, which can affect resource usage.",
action: "Prune Selections",
apply: () => setReadRelays(shuffle($readRelayUrls).slice(0, 5)),
})
}
if ($writeRelayUrls.length > 8) {
$actionItems.push({
title: "Too Many Outbox Relays",
subtitle:
"You have more outbox relays than is really necessary, which can affect resource usage.",
action: "Prune Selections",
apply: () => setWriteRelays(shuffle($writeRelayUrls).slice(0, 5)),
})
}
if ($messagingRelayUrls.length > 8) {
$actionItems.push({
title: "Too Many DM Relays",
subtitle:
"You have more DM relays than is really necessary, which can affect resource usage.",
action: "Prune Selections",
apply: () => setMessagingRelays(shuffle($messagingRelayUrls).slice(0, 5)),
})
}
const [okSearchRelays, badSearchRelays] = partition(
url => Boolean(ifLet(getRelay(url), hasNip50)),
$searchRelayUrls,
)
if (badSearchRelays.length > 0) {
$actionItems.push({
title: "Invalid Search Relays",
subtitle: `Some of your search relays don't support search.`,
action: "Remove Invalid",
apply: () => setSearchRelays(okSearchRelays),
})
}
return $actionItems
},
)
onMount(() => { onMount(() => {
discoverRelays(getRelayLists()) discoverRelays(getRelayLists())
}) })
</script> </script>
<div class="content"> <div class="content flex flex-col gap-4">
<RelaySettingsActionItems />
<div class="card2 bg-alt flex flex-col gap-4 shadow-md"> <div class="card2 bg-alt flex flex-col gap-4 shadow-md">
<div class="flex justify-between"> <strong class="flex items-center gap-3 text-lg">
<strong class="flex items-center gap-3 text-lg"> <Icon icon={Server} />
<Icon icon={Server} /> Your Relays
Your Relays </strong>
</strong> <p class="mb-2">
{#if $actionItems.length > 0}
<Button class="btn btn-neutral btn-sm" onclick={showActionItems}>
<Icon icon={DangerTriangle} />
{$actionItems.length} Issue{$actionItems.length === 1 ? "" : "s"} Detected
</Button>
{/if}
</div>
<p class="text-sm mb-2">
Relays are servers which store your data, or allow you to find data from across the Nostr Relays are servers which store your data, or allow you to find data from across the Nostr
network. We've set you up with some reasonable defaults, but if you're a power user, you can network. We've set you up with some reasonable defaults, but if you're a power user, you can
customize your relay selections below. customize your relay selections below.