diff --git a/src/app/shared/RelayStatus.svelte b/src/app/shared/RelayStatus.svelte index 6b2a1833..1b391a83 100644 --- a/src/app/shared/RelayStatus.svelte +++ b/src/app/shared/RelayStatus.svelte @@ -1,48 +1,18 @@ -
+
s == status, + )} + class:bg-success={ConnectionType.Connected == status} + class:bg-warning={[ + ConnectionType.Logging, + ConnectionType.WaitReconnect, + ConnectionType.UnstableConnection, + ].some(s => s == status)} />
- {description} + {displayConnectionType(status)}
diff --git a/src/app/views/Publishes.svelte b/src/app/views/Publishes.svelte index 6509feff..5632ae09 100644 --- a/src/app/views/Publishes.svelte +++ b/src/app/views/Publishes.svelte @@ -1,13 +1,21 @@ Published Events -
- -

{recent.length}

- {pluralize(recent.length, "Event")} -
- -

{relays.size}

- {pluralize(relays.size, "Relay")} -
- -

{pending.length}

- Pending -
- -

{success.length}

- Succeeded -
- -

{recent.length - pending.length - success.length}

- Failed -
-
-{#each sortBy(t => -t.event.created_at, recent) as thunk (thunk.event.id)} - -{/each} + (activeTab = tab)} /> +{#if activeTab === "events"} +
+ +

{recent.length}

+ {pluralize(recent.length, "Event")} +
+ +

{relays.size}

+ {pluralize(relays.size, "Relay")} +
+ +

{pending.length}

+ Pending +
+ +

{success.length}

+ Succeeded +
+ +

{recent.length - pending.length - success.length}

+ Failed +
+
+ {#each sortBy(t => -t.event.created_at, recent) as thunk (thunk.event.id)} + + {/each} +{:else if activeTab === "connections"} + +{:else if activeTab === "notices"} + +{/if} diff --git a/src/app/views/PublishesConnections.svelte b/src/app/views/PublishesConnections.svelte new file mode 100644 index 00000000..28c1644d --- /dev/null +++ b/src/app/views/PublishesConnections.svelte @@ -0,0 +1,105 @@ + + + +
+ {connectionsStatus.get(option)?.size || 0} + {displayConnectionType(option)} +
+
+{#each connections as url (url)} + {@const relay = $relaysByUrl.get(url)} + { + selected = url + activeTab = "notices" + }}> +
+ {#if relay?.profile?.icon} + + {:else} +
+ +
+ {/if} +
+
+
+ {displayRelayUrl(url)} +
+
+
+ {#if relay?.profile?.supported_nips} + + {relay.profile.supported_nips.length} NIPs + + {/if} + + Connected {quantify(relay?.stats?.open_count || 0, "time")} + +
+
+
+ {#each options.filter(o => connectionsStatus.get(o)?.has(url)) as o} + {@const opt = displayConnectionType(o)} +
+ {opt} +
+
+ {/each} +
+
+ +{/each} diff --git a/src/app/views/PublishesNotices.svelte b/src/app/views/PublishesNotices.svelte new file mode 100644 index 00000000..edb4e661 --- /dev/null +++ b/src/app/views/PublishesNotices.svelte @@ -0,0 +1,89 @@ + + + + +{#if !notices.length && !!search} +
+ +
No notices found.
+
+
+{:else} + + {#each notices as notice} +
+ {#if "eventId" in notice} + + {:else} +
+ {formatTimestamp(notice.created_at)} + from {notice.url} + [{notice.notice[0]}] + {#each notice.notice.slice(1).filter(n => typeof n == "string") as item} + {item} + {/each} +
+ {/if} +
+ {/each} +
+{/if} diff --git a/src/domain/connection.ts b/src/domain/connection.ts new file mode 100644 index 00000000..b001c85e --- /dev/null +++ b/src/domain/connection.ts @@ -0,0 +1,75 @@ +import {getRelayQuality, type ThunkStatus} from "@welshman/app" +import {AuthStatus, Connection, SocketStatus} from "@welshman/net" +import {derived, writable} from "svelte/store" + +export type PublishNotice = { + eventId: string + created_at: number + eventKind: number + message: string + status: ThunkStatus + url: string +} + +export type SubscriptionNotice = {created_at: number; url: string; notice: string[]} + +export const subscriptionNotices = writable>(new Map()) + +export const subscriptionNoticesByRelay = derived(subscriptionNotices, $notices => { + return $notices.values() +}) + +const pendingStatuses = [ + AuthStatus.Requested, + AuthStatus.PendingSignature, + AuthStatus.PendingResponse, +] + +const failureStatuses = [AuthStatus.DeniedSignature, AuthStatus.Forbidden] + +export const getConnectionStatus = (cxn: Connection): ConnectionType => { + if (pendingStatuses.includes(cxn.auth.status)) { + return ConnectionType.Logging + } else if (failureStatuses.includes(cxn.auth.status)) { + return ConnectionType.LoginFailed + } else if (cxn.socket.status === SocketStatus.Error) { + return ConnectionType.ConnectFailed + } else if (cxn.socket.status === SocketStatus.Closed) { + return ConnectionType.WaitReconnect + } else if (cxn.socket.status === SocketStatus.New) { + return ConnectionType.NotConnected + } else if (getRelayQuality(cxn.url) < 0.5) { + return ConnectionType.UnstableConnection + } else { + return ConnectionType.Connected + } +} + +export enum ConnectionType { + Connected, + Logging, + LoginFailed, + ConnectFailed, + WaitReconnect, + NotConnected, + UnstableConnection, +} + +export const displayConnectionType = (type: ConnectionType) => { + switch (type) { + case ConnectionType.Connected: + return "Connected" + case ConnectionType.Logging: + return "Logging in" + case ConnectionType.LoginFailed: + return "Failed to log in" + case ConnectionType.ConnectFailed: + return "Failed to connect" + case ConnectionType.WaitReconnect: + return "Wainting to reconnect" + case ConnectionType.NotConnected: + return "Not connected" + case ConnectionType.UnstableConnection: + return "Unstable connection" + } +} diff --git a/src/engine/state.ts b/src/engine/state.ts index 4eca733e..60e684f0 100644 --- a/src/engine/state.ts +++ b/src/engine/state.ts @@ -51,8 +51,16 @@ import { uniq, uniqBy, } from "@welshman/lib" -import type {PublishRequest, Target} from "@welshman/net" -import {Executor, AuthMode, Local, Multi, Relays, SubscriptionEvent} from "@welshman/net" +import type {Connection, PublishRequest, Target} from "@welshman/net" +import { + Executor, + AuthMode, + Local, + Multi, + Relays, + SubscriptionEvent, + ConnectionEvent, +} from "@welshman/net" import {Nip01Signer, Nip59} from "@welshman/signer" import {deriveEvents, deriveEventsMapped, throttled, withGetter} from "@welshman/store" import type {EventTemplate, PublishedList, SignedEvent, TrustedEvent} from "@welshman/util" @@ -958,6 +966,7 @@ export class ThreadLoader { // Remove the old database. TODO remove this import {deleteDB} from "idb" +import {subscriptionNotices} from "src/domain/connection" deleteDB("nostr-engine/Storage") let ready: Promise = Promise.resolve() @@ -1058,6 +1067,20 @@ if (!db) { ctx.app.dufflepudUrl = getSetting("dufflepud_url") }) + ctx.net.pool.on("init", (connection: Connection) => { + connection.on(ConnectionEvent.Receive, function (cxn, [verb, ...args]) { + if (verb == "EVENT" || verb == "EOSE" || verb == "NEG-MSG") return + subscriptionNotices.update($notices => { + pushToMapKey($notices, connection.url, { + created_at: now(), + url: cxn.url, + notice: [verb, ...args], + }) + return $notices + }) + }) + }) + ready = initStorage("coracle", 2, { relays: {keyPath: "url", store: throttled(1000, relays)}, handles: {keyPath: "nip05", store: throttled(1000, handles)}, diff --git a/src/partials/ThunkNotice.svelte b/src/partials/ThunkNotice.svelte new file mode 100644 index 00000000..1bdc3b78 --- /dev/null +++ b/src/partials/ThunkNotice.svelte @@ -0,0 +1,31 @@ + + +
+ {formatTimestamp(notice.created_at)} + to {notice.url}: + [Kind {notice.eventKind}] + {notice.message || message} +