forked from coracle/flotilla
Improve relay access checks, content loading
This commit is contained in:
+32
-23
@@ -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 ({
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
@@ -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 => {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user