Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhance note status #487

Draft
wants to merge 9 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 17 additions & 35 deletions src/app/shared/RelayStatus.svelte
Original file line number Diff line number Diff line change
@@ -1,48 +1,18 @@
<script lang="ts">
import {onMount} from "svelte"
import {ctx} from "@welshman/lib"
import {SocketStatus, AuthStatus} from "@welshman/net"
import {getRelayQuality} from "@welshman/app"
import Popover from "src/partials/Popover.svelte"
import {ConnectionType, displayConnectionType, getConnectionStatus} from "src/domain/connection"

export let url

const pendingStatuses = [
AuthStatus.Requested,
AuthStatus.PendingSignature,
AuthStatus.PendingResponse,
]
const failureStatuses = [AuthStatus.DeniedSignature, AuthStatus.Forbidden]

let description = "Not connected"
let className = "bg-neutral-600"
let status = ConnectionType.NotConnected

onMount(() => {
const interval = setInterval(() => {
const cxn = ctx.net.pool.get(url)

if (pendingStatuses.includes(cxn.auth.status)) {
className = "bg-warning"
description = "Logging in"
} else if (failureStatuses.includes(cxn.auth.status)) {
className = "bg-danger"
description = "Failed to log in"
} else if (cxn.socket.status === SocketStatus.Error) {
className = "bg-danger"
description = "Failed to connect"
} else if (cxn.socket.status === SocketStatus.Closed) {
className = "bg-warning"
description = "Waiting to reconnect"
} else if (cxn.socket.status === SocketStatus.New) {
className = "bg-neutral-600"
description = "Not connected"
} else if (getRelayQuality(cxn.url) < 0.5) {
className = "bg-warning"
description = "Unstable connection"
} else {
className = "bg-success"
description = "Connected"
}
status = getConnectionStatus(cxn)
}, 800)

return () => {
Expand All @@ -52,8 +22,20 @@
</script>

<Popover triggerType="mouseenter">
<div slot="trigger" class="h-2 w-2 cursor-pointer rounded-full {className}" />
<div
slot="trigger"
class="h-2 w-2 cursor-pointer rounded-full bg-neutral-600"
class:bg-neutral-600={ConnectionType.NotConnected == status}
class:bg-danger={[ConnectionType.LoginFailed, ConnectionType.ConnectFailed].some(
s => s == status,
)}
class:bg-success={ConnectionType.Connected == status}
class:bg-warning={[
ConnectionType.Logging,
ConnectionType.WaitReconnect,
ConnectionType.UnstableConnection,
].some(s => s == status)} />
<div slot="tooltip" class="transition-all sm:block">
{description}
{displayConnectionType(status)}
</div>
</Popover>
77 changes: 46 additions & 31 deletions src/app/views/Publishes.svelte
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
<script lang="ts">
import {pluralize, seconds} from "hurdak"
import {thunks, type Thunk} from "@welshman/app"
import {assoc, now, remove, sortBy} from "@welshman/lib"
import {LOCAL_RELAY_URL} from "@welshman/util"
import {PublishStatus} from "@welshman/net"
import Tile from "src/partials/Tile.svelte"
import Subheading from "src/partials/Subheading.svelte"
import PublishCard from "src/app/shared/PublishCard.svelte"
import {thunks, type Thunk} from "@welshman/app"
import {LOCAL_RELAY_URL} from "@welshman/util"
import {get} from "svelte/store"
import {pluralize, seconds} from "hurdak"
import PublishCard from "src/app/shared/PublishCard.svelte"
import Subheading from "src/partials/Subheading.svelte"
import Tabs from "src/partials/Tabs.svelte"
import Tile from "src/partials/Tile.svelte"
import PublishesConnections from "src/app/views/PublishesConnections.svelte"
import PublishesNotices from "src/app/views/PublishesNotices.svelte"

const tabs = ["events", "connections", "notices"]
let activeTab = "events"

let selected: string

const hasStatus = (thunk: Thunk, statuses: PublishStatus[]) =>
Object.values(get(thunk.status)).some(s => statuses.includes(s.status))
Expand Down Expand Up @@ -43,28 +51,35 @@
</script>

<Subheading>Published Events</Subheading>
<div class="grid grid-cols-4 justify-between gap-2 sm:grid-cols-5">
<Tile background>
<p class="text-lg sm:text-2xl">{recent.length}</p>
<span class="text-sm">{pluralize(recent.length, "Event")}</span>
</Tile>
<Tile background>
<p class="text-lg sm:text-2xl">{relays.size}</p>
<span class="text-sm">{pluralize(relays.size, "Relay")}</span>
</Tile>
<Tile background lass="hidden sm:block">
<p class="text-lg sm:text-2xl">{pending.length}</p>
<span class="text-sm">Pending</span>
</Tile>
<Tile background>
<p class="text-lg sm:text-2xl">{success.length}</p>
<span class="text-sm">Succeeded</span>
</Tile>
<Tile background>
<p class="text-lg sm:text-2xl">{recent.length - pending.length - success.length}</p>
<span class="text-sm">Failed</span>
</Tile>
</div>
{#each sortBy(t => -t.event.created_at, recent) as thunk (thunk.event.id)}
<PublishCard {thunk} />
{/each}
<Tabs {tabs} {activeTab} setActiveTab={tab => (activeTab = tab)} />
{#if activeTab === "events"}
<div class="grid grid-cols-4 justify-between gap-2 sm:grid-cols-5">
<Tile background>
<p class="text-lg sm:text-2xl">{recent.length}</p>
<span class="text-sm">{pluralize(recent.length, "Event")}</span>
</Tile>
<Tile background>
<p class="text-lg sm:text-2xl">{relays.size}</p>
<span class="text-sm">{pluralize(relays.size, "Relay")}</span>
</Tile>
<Tile background lass="hidden sm:block">
<p class="text-lg sm:text-2xl">{pending.length}</p>
<span class="text-sm">Pending</span>
</Tile>
<Tile background>
<p class="text-lg sm:text-2xl">{success.length}</p>
<span class="text-sm">Succeeded</span>
</Tile>
<Tile background>
<p class="text-lg sm:text-2xl">{recent.length - pending.length - success.length}</p>
<span class="text-sm">Failed</span>
</Tile>
</div>
{#each sortBy(t => -t.event.created_at, recent) as thunk (thunk.event.id)}
<PublishCard {thunk} />
{/each}
{:else if activeTab === "connections"}
<PublishesConnections bind:selected bind:activeTab />
{:else if activeTab === "notices"}
<PublishesNotices search={selected} />
{/if}
105 changes: 105 additions & 0 deletions src/app/views/PublishesConnections.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
<script lang="ts">
import {relaysByUrl} from "@welshman/app"
import {addToMapKey, ctx} from "@welshman/lib"
import {displayRelayUrl} from "@welshman/util"
import {quantify} from "hurdak"
import {onMount} from "svelte"
import AltColor from "src/partials/AltColor.svelte"
import SelectButton from "src/partials/SelectButton.svelte"
import {ConnectionType, displayConnectionType, getConnectionStatus} from "src/domain/connection"

export let selected: string
export let activeTab: string

let selectedOptions: ConnectionType[] = []
let connectionsStatus: Map<ConnectionType, Set<string>> = new Map()

const options = [
ConnectionType.Connected,
ConnectionType.Logging,
ConnectionType.LoginFailed,
ConnectionType.ConnectFailed,
ConnectionType.WaitReconnect,
ConnectionType.NotConnected,
ConnectionType.UnstableConnection,
]
ticruz38 marked this conversation as resolved.
Show resolved Hide resolved

$: connections = Array.from(ctx.net.pool.data.keys()).filter(url =>
selectedOptions.length ? selectedOptions.some(s => connectionsStatus.get(s)?.has(url)) : true,
)

function fetchConnectionStatus() {
const newConnectionStatus: Map<ConnectionType, Set<string>> = new Map()
for (const [url, cxn] of ctx.net.pool.data.entries()) {
addToMapKey(newConnectionStatus, getConnectionStatus(cxn), url)
}
connectionsStatus = newConnectionStatus
}

onMount(() => {
fetchConnectionStatus()
const interval = setInterval(fetchConnectionStatus, 800)

return () => {
clearInterval(interval)
}
})
</script>

<SelectButton {options} bind:value={selectedOptions} multiple class="text-left">
<div class="flex items-center gap-2" slot="item" let:option>
{connectionsStatus.get(option)?.size || 0}
{displayConnectionType(option)}
</div>
</SelectButton>
{#each connections as url (url)}
{@const relay = $relaysByUrl.get(url)}
<AltColor
background
class="cursor-pointer justify-between rounded-md p-6 shadow"
on:click={() => {
selected = url
activeTab = "notices"
}}>
<div class="flex min-w-0 shrink-0 items-start gap-3">
{#if relay?.profile?.icon}
<img class="h-9 w-9 shrink-0 rounded-full border" src={relay.profile.icon} />
{:else}
<div class="flex h-9 w-9 shrink-0 items-center justify-center rounded-full border">
<i class="fa fa-server text-xl text-neutral-100"></i>
</div>
{/if}
<div class="shrink-0">
<div class="flex items-center gap-2">
<div class="text-md overflow-hidden text-ellipsis whitespace-nowrap">
{displayRelayUrl(url)}
</div>
</div>
<div class="flex gap-4 text-xs text-neutral-400">
{#if relay?.profile?.supported_nips}
<span>
{relay.profile.supported_nips.length} NIPs
</span>
{/if}
<span>
Connected {quantify(relay?.stats?.open_count || 0, "time")}
</span>
</div>
</div>
<div class="flex w-full items-center justify-end gap-2 text-sm">
{#each options.filter(o => connectionsStatus.get(o)?.has(url)) as o}
{@const opt = displayConnectionType(o)}
<div class="flex items-center gap-2">
<span>{opt}</span>
<div
class:!bg-danger={opt.includes("Failed") || opt.includes("Not")}
class:!bg-warning={opt == "Logging in" ||
o == ConnectionType.WaitReconnect ||
o == ConnectionType.UnstableConnection}
class="h-3 w-3 rounded-full bg-success" />
</div>
{/each}
</div>
</div>
</AltColor>
{/each}
89 changes: 89 additions & 0 deletions src/app/views/PublishesNotices.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<script lang="ts">
import {formatTimestamp, thunks, type Thunk} from "@welshman/app"
import {ctx} from "@welshman/lib"
import {type Connection} from "@welshman/net"
import {sortBy} from "ramda"
import {get} from "svelte/store"
import {fly} from "svelte/transition"
import AltColor from "src/partials/AltColor.svelte"
import Input from "src/partials/Input.svelte"
import ThunkNotice from "src/partials/ThunkNotice.svelte"
import {subscriptionNotices, type PublishNotice} from "src/domain/connection"

export let search: string = ""

$: subNotices = Array.from($subscriptionNotices.values())
.flatMap(n => n)
.filter(
n =>
n.url.toLowerCase().includes(search.toLowerCase()) ||
n.notice.some(n => String(n).toLowerCase().includes(search.toLowerCase())),
)

function getPubNotices(connections: Connection[]) {
return Object.values($thunks).filter(
t => connections.some(cxn => get(t.status)[cxn.url]?.status) && "event" in t,
) as Thunk[]
}

// for subscription notices
function colorFromVerb(verb: string) {
switch (verb) {
case "OK":
return "text-success"
case "NOTICE":
return "text-accent"
case "CLOSED":
return "text-danger"
}
}

$: pubNotices = getPubNotices(Array.from(ctx.net.pool.data.values())).flatMap(p =>
Object.keys(get(p.status))
.filter(k => k.includes(search) || get(p.status)[k].message.includes(search))
.map(k => ({
eventId: p.event.id,
created_at: p.event.created_at,
eventKind: p.event.kind,
url: k,
message: get(p.status)[k].message,
status: get(p.status)[k],
})),
) as PublishNotice[]

$: notices = sortBy(
n => {
return -n.created_at
},
[...pubNotices, ...subNotices],
)
</script>

<Input placeholder="Search notices" type="search" bind:value={search} />

{#if !notices.length && !!search}
<div in:fly|local={{y: 20}}>
<AltColor background class="rounded-md p-6 shadow">
<div class="place text-center text-neutral-100">No notices found.</div>
</AltColor>
</div>
{:else}
<AltColor background class="rounded-md p-2">
{#each notices as notice}
<div>
{#if "eventId" in notice}
<ThunkNotice {notice} />
{:else}
<div class="flex flex-wrap items-center gap-2 overflow-hidden p-2">
<span class="shrink-0 text-neutral-400">{formatTimestamp(notice.created_at)}</span>
<strong class={colorFromVerb(notice.notice[0])}>from {notice.url}</strong>
<span class="shrink-0">[{notice.notice[0]}]</span>
{#each notice.notice.slice(1).filter(n => typeof n == "string") as item}
<span class="text-neutral-300">{item}</span>
{/each}
</div>
{/if}
</div>
{/each}
</AltColor>
{/if}
Loading
Loading