Improve relay access checks, content loading

This commit is contained in:
Jon Staab
2024-10-10 17:09:01 -07:00
parent 83d892afc3
commit 597ebddf82
14 changed files with 127 additions and 104 deletions
+32 -23
View File
@@ -189,39 +189,42 @@ export const setInboxRelayPolicy = (url: string, enabled: boolean) => {
// Relay access // Relay access
export const requestRelayAccess = (url: string, claim = "") => export const checkRelayAccess = async (url: string, claim = "") => {
publishThunk({ await ctx.net.pool.get(url).ensureAuth()
const result = await publishThunk({
event: createEvent(28934, {tags: [["claim", claim]]}), event: createEvent(28934, {tags: [["claim", claim]]}),
relays: [url], relays: [url],
}) })
export const attemptRelayAccess = async (url: string, claim = "") => {
const relay = await loadRelay(url)
// Make sure the relay has a profile
if (!relay?.profile) {
return "Sorry, we weren't able to find that relay."
}
const connection = ctx.net.pool.get(url)
// Check connection status
await connection.ensureConnected()
if (![ConnectionStatus.Ok, ConnectionStatus.Slow].includes(connection.meta.getStatus())) {
return `Failed to connect: "${connection.meta.getDescription()}"`
}
// Attempt to publish a join request
const result = await requestRelayAccess(url, claim)
if (result[url].status !== PublishStatus.Success) { if (result[url].status !== PublishStatus.Success) {
const message = result[url].message?.replace(/^.*: /, '') || "join request rejected" const message = result[url].message?.replace(/^.*: /, '') || "join request rejected"
return `Failed to join relay: ${message}` return `Failed to join relay: ${message}`
} }
}
export const checkRelayProfile = async (url: string) => {
const relay = await loadRelay(url)
if (!relay?.profile) {
return "Sorry, we weren't able to find that relay."
}
}
export const checkRelayConnection = async (url: string) => {
const connection = ctx.net.pool.get(url)
await connection.ensureConnected()
if (![ConnectionStatus.Ok, ConnectionStatus.Slow].includes(connection.meta.getStatus())) {
return `Failed to connect: "${connection.meta.getDescription()}"`
}
}
export const checkRelayAuth = async (url: string) => {
const connection = ctx.net.pool.get(url)
// Check auth status
await connection.ensureAuth() await connection.ensureAuth()
if (![AuthStatus.Ok, AuthStatus.Pending].includes(connection.meta.authStatus)) { if (![AuthStatus.Ok, AuthStatus.Pending].includes(connection.meta.authStatus)) {
@@ -229,6 +232,12 @@ export const attemptRelayAccess = async (url: string, claim = "") => {
} }
} }
export const attemptRelayAccess = async (url: string, claim = "") =>
await checkRelayProfile(url) ||
await checkRelayConnection(url) ||
await checkRelayAccess(url, claim) ||
await checkRelayAuth(url)
// Actions // Actions
export const sendWrapped = async ({ export const sendWrapped = async ({
+1 -1
View File
@@ -48,7 +48,7 @@
{@const following = getPubkeyTagValues(getListTags($userFollows)).includes(pubkey)} {@const following = getPubkeyTagValues(getListTags($userFollows)).includes(pubkey)}
<div class="divider" /> <div class="divider" />
<Button class="chat chat-start" on:click={onClick}> <Button class="chat chat-start" on:click={onClick}>
<div class="chat-bubble"> <div class="chat-bubble text-left bg-alt">
<Content hideMedia={!following} {event} /> <Content hideMedia={!following} {event} />
<p class="text-right text-xs">{formatTimestamp(event.created_at)}</p> <p class="text-right text-xs">{formatTimestamp(event.created_at)}</p>
</div> </div>
+1 -1
View File
@@ -7,5 +7,5 @@
</script> </script>
{#if $relay?.profile?.description} {#if $relay?.profile?.description}
<p>{$relay?.profile.description}</p> <p class={$$props.class}>{$relay?.profile.description}</p>
{/if} {/if}
+1 -1
View File
@@ -58,7 +58,7 @@
<Icon icon="alt-arrow-left" /> <Icon icon="alt-arrow-left" />
Go back Go back
</Button> </Button>
<Button type="submit" class="btn btn-primary"> <Button type="submit" class="btn btn-primary" disabled={loading}>
Go to Space Go to Space
<Icon icon="alt-arrow-right" class="!bg-base-300" /> <Icon icon="alt-arrow-right" class="!bg-base-300" />
</Button> </Button>
+4 -4
View File
@@ -5,8 +5,8 @@
{#if $toast} {#if $toast}
{@const theme = $toast.theme || "info"} {@const theme = $toast.theme || "info"}
{#key $toast.id} <div transition:fly class="toast z-toast">
<div transition:fly class="toast z-toast"> {#key $toast.id}
<div <div
role="alert" role="alert"
class="alert flex justify-center" class="alert flex justify-center"
@@ -14,6 +14,6 @@
class:alert-error={theme === "error"}> class:alert-error={theme === "error"}>
{$toast.message} {$toast.message}
</div> </div>
</div> {/key}
{/key} </div>
{/if} {/if}
+10 -16
View File
@@ -209,6 +209,16 @@ export const deriveEvent = (idOrAddress: string, hints: string[] = []) => {
) )
} }
export const deriveEventsForUrl = (url: string, kinds: number[]) =>
derived(trackerStore, $tracker =>
sortBy(
e => -e.created_at,
Array.from($tracker.getIds(url))
.map(id => repository.eventsById.get(id)!)
.filter(e => kinds.includes(e?.kind)),
)
)
// Membership // Membership
export const getMembershipUrls = (list?: List) => sort(getRelayTagValues(getListTags(list))) export const getMembershipUrls = (list?: List) => sort(getRelayTagValues(getListTags(list)))
@@ -375,22 +385,6 @@ export const chatSearch = derived(chats, $chats =>
}), }),
) )
// Calendar events
export const events = deriveEvents(repository, {filters: [{kinds: [EVENT_DATE, EVENT_TIME]}]})
export const eventsByUrl = derived([trackerStore, events], ([$tracker, $events]) => {
const eventsByUrl = new Map<string, TrustedEvent[]>()
for (const event of $events) {
for (const url of $tracker.getRelays(event.id)) {
pushToMapKey(eventsByUrl, url, event)
}
}
return eventsByUrl
})
// Rooms // Rooms
export const roomsByUrl = derived(channels, $channels => { export const roomsByUrl = derived(channels, $channels => {
+1
View File
@@ -32,6 +32,7 @@ export const createScroller = ({
reverse = false, reverse = false,
}: ScrollerOpts) => { }: ScrollerOpts) => {
let done = false let done = false
const check = async () => { const check = async () => {
// While we have empty space, fill it // While we have empty space, fill it
const {scrollY, innerHeight} = window const {scrollY, innerHeight} = window
+3 -1
View File
@@ -25,6 +25,8 @@
storageAdapters, storageAdapters,
tracker, tracker,
} from "@welshman/app" } from "@welshman/app"
import * as lib from "@welshman/lib"
import * as util from "@welshman/util"
import * as app from "@welshman/app" import * as app from "@welshman/app"
import ModalBox from "@lib/components/ModalBox.svelte" import ModalBox from "@lib/components/ModalBox.svelte"
import Drawer from "@lib/components/Drawer.svelte" import Drawer from "@lib/components/Drawer.svelte"
@@ -71,7 +73,7 @@
} }
onMount(async () => { onMount(async () => {
Object.assign(window, {get, ...app, ...state}) Object.assign(window, {get, ...lib, ...util, ...app, ...state})
const getScoreEvent = () => { const getScoreEvent = () => {
const ALWAYS_KEEP = Infinity const ALWAYS_KEEP = Infinity
+28 -27
View File
@@ -23,7 +23,7 @@
onMount(() => { onMount(() => {
const sub = discoverRelays() const sub = discoverRelays()
const scroller = createScroller({ const scroller = createScroller({
element: element.closest(".max-h-screen")!, element: element.closest(".overflow-auto")!,
onScroll: () => { onScroll: () => {
limit += 20 limit += 20
}, },
@@ -54,36 +54,37 @@
idKey="url" idKey="url"
let:item={relay}> let:item={relay}>
<Button <Button
class="card2 !py-20 center bg-alt flex flex-col gap-2 text-center shadow-xl transition-all hover:shadow-2xl hover:brightness-[1.1]" class="card2 bg-alt text-left flex flex-col gap-2 shadow-xl transition-all hover:shadow-2xl hover:brightness-[1.1]"
on:click={() => openSpace(relay.url)}> on:click={() => openSpace(relay.url)}>
<div class="center avatar"> <div class="flex gap-4">
<div <div class="avatar">
class="center relative !flex w-20 rounded-full border-2 border-solid border-base-300 bg-base-300"> <div class="center !flex h-12 w-12 min-w-12 rounded-full border-2 border-solid border-base-300 bg-base-300">
{#if relay.profile?.icon} {#if relay.profile?.icon}
<img alt="" src={relay.profile.icon} /> <img alt="" src={relay.profile.icon} />
{:else} {:else}
<Icon icon="ghost" size={7} /> <Icon icon="ghost" size={5} />
{/if} {/if}
</div>
</div>
{#if getMembershipUrls($userMembership).includes(relay.url)}
<div class="flex justify-center">
<div class="relative">
<div
class="tooltip absolute -top-[88px] left-5 h-5 w-5 rounded-full bg-primary"
data-tip="You are already a member of this space.">
<Icon icon="check-circle" class="scale-110" />
</div>
</div> </div>
</div> </div>
{/if} {#if getMembershipUrls($userMembership).includes(relay.url)}
<div> <div class="flex justify-center">
<h2 class="text-center text-xl"> <div class="relative">
<RelayName url={relay.url} /> <div
</h2> class="tooltip absolute -top-[88px] left-5 h-5 w-5 rounded-full bg-primary"
<p class="text-sm opacity-75">{relay.url}</p> data-tip="You are already a member of this space.">
<Icon icon="check-circle" class="scale-110" />
</div>
</div>
</div>
{/if}
<div>
<h2 class="text-xl">
<RelayName url={relay.url} />
</h2>
<p class="text-sm opacity-75">{relay.url}</p>
</div>
</div> </div>
<RelayDescription url={relay.url} /> <RelayDescription url={relay.url} class="ml-16" />
</Button> </Button>
</Masonry> </Masonry>
</div> </div>
+1 -1
View File
@@ -56,7 +56,7 @@
load({filters: [notesFilter, reactionsFilter], timeout: 30_000}) load({filters: [notesFilter, reactionsFilter], timeout: 30_000})
const scroller = createScroller({ const scroller = createScroller({
element: element.closest(".max-h-screen")!, element: element.closest(".overflow-auto")!,
onScroll: () => { onScroll: () => {
const seen = new Set(events.map(e => e.id)) const seen = new Set(events.map(e => e.id))
const eligible = sortBy( const eligible = sortBy(
+1 -1
View File
@@ -18,7 +18,7 @@
onMount(() => { onMount(() => {
const scroller = createScroller({ const scroller = createScroller({
element: element.closest(".max-h-screen")!, element: element.closest(".overflow-auto")!,
onScroll: () => { onScroll: () => {
limit += 10 limit += 10
}, },
+7 -6
View File
@@ -26,7 +26,9 @@
GENERAL, GENERAL,
MESSAGE, MESSAGE,
} from "@app/state" } from "@app/state"
import {checkRelayConnection, checkRelayAuth} from "@app/commands"
import {pushModal} from "@app/modal" import {pushModal} from "@app/modal"
import {pushToast} from "@app/toast"
import {makeSpacePath} from "@app/routes" import {makeSpacePath} from "@app/routes"
const openMenu = () => { const openMenu = () => {
@@ -60,13 +62,12 @@
$: rooms = getMembershipRoomsByUrl(url, $userMembership) $: rooms = getMembershipRoomsByUrl(url, $userMembership)
$: otherRooms = ($roomsByUrl.get(url) || []).filter(room => !rooms.concat(GENERAL).includes(room)) $: otherRooms = ($roomsByUrl.get(url) || []).filter(room => !rooms.concat(GENERAL).includes(room))
onMount(() => { onMount(async () => {
const filter = {kinds: [NOTE, REACTION, MESSAGE, EVENT_DATE, EVENT_TIME, CLASSIFIED]} const error = await checkRelayConnection(url) || await checkRelayAuth(url)
const sub = subscribe({filters: [{...filter, since: ago(30)}]})
pullConservatively({filters: [filter], relays: [url]}) if (error) {
pushToast({theme: "error", message: error})
return () => sub.close() }
}) })
</script> </script>
@@ -1,8 +1,10 @@
<script lang="ts"> <script lang="ts">
import {onMount} from 'svelte'
import {page} from "$app/stores" import {page} from "$app/stores"
import {sortBy, last} from "@welshman/lib" import {sortBy, last, ago} from "@welshman/lib"
import type {TrustedEvent} from "@welshman/util" import type {TrustedEvent} from "@welshman/util"
import {formatTimestampAsDate} from "@welshman/app" import {EVENT_DATE, EVENT_TIME} from "@welshman/util"
import {repository, subscribe, formatTimestampAsDate, trackerStore} from "@welshman/app"
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 Spinner from "@lib/components/Spinner.svelte" import Spinner from "@lib/components/Spinner.svelte"
@@ -11,9 +13,11 @@
import EventItem from "@app/components/EventItem.svelte" import EventItem from "@app/components/EventItem.svelte"
import EventCreate from "@app/components/EventCreate.svelte" import EventCreate from "@app/components/EventCreate.svelte"
import {pushModal} from "@app/modal" import {pushModal} from "@app/modal"
import {eventsByUrl, decodeNRelay} from "@app/state" import {deriveEventsForUrl, pullConservatively, decodeNRelay} from "@app/state"
const url = decodeNRelay($page.params.nrelay) const url = decodeNRelay($page.params.nrelay)
const kinds = [EVENT_DATE, EVENT_TIME]
const events = deriveEventsForUrl(url, kinds)
const createEvent = () => pushModal(EventCreate, {url}) const createEvent = () => pushModal(EventCreate, {url})
@@ -27,7 +31,8 @@
dateDisplay?: string dateDisplay?: string
} }
$: items = sortBy(getStart, $eventsByUrl.get(url) || []).reduce<Item[]>((r, event) => {
$: items = sortBy(getStart, $events).reduce<Item[]>((r, event) => {
const prevDateDisplay = const prevDateDisplay =
r.length > 0 ? formatTimestampAsDate(getStart(last(r).event)) : undefined r.length > 0 ? formatTimestampAsDate(getStart(last(r).event)) : undefined
const newDateDisplay = formatTimestampAsDate(getStart(event)) const newDateDisplay = formatTimestampAsDate(getStart(event))
@@ -36,6 +41,14 @@
return [...r, {event, dateDisplay}] return [...r, {event, dateDisplay}]
}, []) }, [])
onMount(() => {
const sub = subscribe({filters: [{kinds, since: ago(30)}]})
pullConservatively({filters: [{kinds}], relays: [url]})
return () => sub.close()
})
setTimeout(() => { setTimeout(() => {
loading = false loading = false
}, 3000) }, 3000)
+20 -18
View File
@@ -2,8 +2,9 @@
import {onMount} from "svelte" import {onMount} from "svelte"
import {page} from "$app/stores" import {page} from "$app/stores"
import {NOTE} from "@welshman/util" import {NOTE} from "@welshman/util"
import {nthEq, sortBy} from "@welshman/lib" import {feedFromFilter} from "@welshman/feeds"
import {repository, trackerStore} from "@welshman/app" import {ago, nthEq, sortBy} from "@welshman/lib"
import {repository, trackerStore, feedLoader} from "@welshman/app"
import {createScroller} from "@lib/html" import {createScroller} from "@lib/html"
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"
@@ -12,28 +13,27 @@
import ThreadItem from "@app/components/ThreadItem.svelte" import ThreadItem from "@app/components/ThreadItem.svelte"
import ThreadCreate from "@app/components/ThreadCreate.svelte" import ThreadCreate from "@app/components/ThreadCreate.svelte"
import {pushModal} from "@app/modal" import {pushModal} from "@app/modal"
import {decodeNRelay} from "@app/state" import {pullConservatively, deriveEventsForUrl, decodeNRelay} from "@app/state"
const url = decodeNRelay($page.params.nrelay) const url = decodeNRelay($page.params.nrelay)
const kinds = [NOTE]
const events = deriveEventsForUrl(url, kinds)
const loader = feedLoader.getLoader(feedFromFilter({kinds}), {})
const createThread = () => pushModal(ThreadCreate, {url}) const createThread = () => pushModal(ThreadCreate, {url})
let limit = 10 let limit = 5
let loading = true let loading = true
let element: Element let element: Element
$: events = sortBy(
e => -e.created_at,
Array.from($trackerStore.getIds(url))
.map(id => repository.eventsById.get(id)!)
.filter(e => e?.kind === NOTE && !e.tags.some(nthEq(0, "e"))),
)
onMount(() => { onMount(() => {
const scroller = createScroller({ const scroller = createScroller({
element: element.closest(".max-h-screen")!, element,
onScroll: async () => { onScroll: async () => {
limit += 10 const $loader = await loader
$loader(5)
limit += 5
}, },
}) })
@@ -45,22 +45,24 @@
}, 3000) }, 3000)
</script> </script>
<div class="relative flex h-screen flex-col" bind:this={element}> <div class="relative flex h-screen flex-col">
<PageBar> <PageBar>
<div slot="icon" class="center"> <div slot="icon" class="center">
<Icon icon="notes-minimalistic" /> <Icon icon="notes-minimalistic" />
</div> </div>
<strong slot="title">Threads</strong> <strong slot="title">Threads</strong>
</PageBar> </PageBar>
<div class="flex flex-grow flex-col gap-2 overflow-auto p-2"> <div class="flex flex-grow flex-col gap-2 overflow-auto p-2" bind:this={element}>
{#each events.slice(0, limit) as event (event.id)} {#each $events.slice(0, limit) as event (event.id)}
<ThreadItem {event} /> {#if !event.tags.some(nthEq(0, "e"))}
<ThreadItem {event} />
{/if}
{/each} {/each}
<p class="flex h-10 items-center justify-center py-20"> <p class="flex h-10 items-center justify-center py-20">
<Spinner {loading}> <Spinner {loading}>
{#if loading} {#if loading}
Looking for threads... Looking for threads...
{:else if events.length === 0} {:else if $events.length === 0}
No threads found. No threads found.
{/if} {/if}
</Spinner> </Spinner>