feat: full relay info page

This commit is contained in:
Kieran 2023-01-25 13:10:31 +00:00
parent fe868a601a
commit 1af814b26a
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
8 changed files with 110 additions and 24 deletions

View File

@ -1,6 +1,6 @@
import "./Relay.css"
import { faPlug, faTrash, faSquareCheck, faSquareXmark, faWifi, faUpload, faDownload, faPlugCircleXmark, faEllipsisVertical } from "@fortawesome/free-solid-svg-icons";
import { faPlug, faTrash, faSquareCheck, faSquareXmark, faWifi, faPlugCircleXmark, faGear } from "@fortawesome/free-solid-svg-icons";
import useRelayState from "Feed/RelayState";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useMemo, useState } from "react";
@ -8,6 +8,7 @@ import { useDispatch, useSelector } from "react-redux";
import { removeRelay, setRelays } from "State/Login";
import { RootState } from "State/Store";
import { RelaySettings } from "Nostr/Connection";
import { useNavigate } from "react-router-dom";
export interface RelayProps {
addr: string
@ -15,11 +16,11 @@ export interface RelayProps {
export default function Relay(props: RelayProps) {
const dispatch = useDispatch();
const navigate = useNavigate();
const allRelaySettings = useSelector<RootState, Record<string, RelaySettings>>(s => s.login.relays);
const relaySettings = allRelaySettings[props.addr];
const state = useRelayState(props.addr);
const name = useMemo(() => new URL(props.addr).host, [props.addr]);
const [showExtra, setShowExtra] = useState(false);
function configure(o: RelaySettings) {
dispatch(setRelays({
@ -62,28 +63,13 @@ export default function Relay(props: RelayProps) {
<FontAwesomeIcon icon={faPlugCircleXmark} /> {state?.disconnects}
</div>
<div>
<span className="icon-btn" onClick={() => setShowExtra(s => !s)}>
<FontAwesomeIcon icon={faEllipsisVertical} />
<span className="icon-btn" onClick={() => navigate(name)}>
<FontAwesomeIcon icon={faGear} />
</span>
</div>
</div>
</div>
</div>
{showExtra ? <div className="flex relay-extra w-max">
<div className="f-1">
<FontAwesomeIcon icon={faUpload} /> {state?.events.send}
</div>
<div className="f-1">
<FontAwesomeIcon icon={faDownload} /> {state?.events.received}
</div>
<div className="f-1">
Delete
<span className="icon-btn" onClick={() => dispatch(removeRelay(props.addr))}>
<FontAwesomeIcon icon={faTrash} />
</span>
</div>
</div> : null}
</>
)
}

View File

@ -6,6 +6,7 @@ import { default as NEvent } from "Nostr/Event";
import { DefaultConnectTimeout } from "Const";
import { ConnectionStats } from "Nostr/ConnectionStats";
import { RawEvent, TaggedRawEvent, u256 } from "Nostr";
import { RelayInfo } from "./RelayInfo";
export type CustomHook = (state: Readonly<StateSnapshot>) => void;
@ -27,7 +28,8 @@ export type StateSnapshot = {
events: {
received: number,
send: number
}
},
info?: RelayInfo
};
export default class Connection {
@ -36,6 +38,7 @@ export default class Connection {
Pending: Subscriptions[];
Subscriptions: Map<string, Subscriptions>;
Settings: RelaySettings;
Info?: RelayInfo;
ConnectTimeout: number;
Stats: ConnectionStats;
StateHooks: Map<string, CustomHook>;
@ -72,7 +75,29 @@ export default class Connection {
this.Connect();
}
Connect() {
async Connect() {
try {
if (this.Info === undefined) {
let u = new URL(this.Address);
let rsp = await fetch(`https://${u.host}`, {
headers: {
"accept": "application/nostr+json"
}
});
if (rsp.ok) {
let data = await rsp.json();
for (let [k, v] of Object.entries(data)) {
if (v === "unset" || v === "") {
data[k] = undefined;
}
}
this.Info = data;
}
}
} catch (e) {
console.warn("Could not load relay information", e);
}
this.IsClosed = false;
this.Socket = new WebSocket(this.Address);
this.Socket.onopen = (e) => this.OnOpen(e);
@ -259,6 +284,7 @@ export default class Connection {
this.CurrentState.events.send = this.Stats.EventsSent;
this.CurrentState.avgLatency = this.Stats.Latency.length > 0 ? (this.Stats.Latency.reduce((acc, v) => acc + v, 0) / this.Stats.Latency.length) : 0;
this.CurrentState.disconnects = this.Stats.Disconnects;
this.CurrentState.info = this.Info;
this.Stats.Latency = this.Stats.Latency.slice(-20); // trim
this.HasStateChange = true;
this._NotifyState();

View File

@ -1,4 +1,3 @@
/**
* Stats class for tracking metrics per connection
*/

9
src/Nostr/RelayInfo.ts Normal file
View File

@ -0,0 +1,9 @@
export interface RelayInfo {
name?: string,
description?: string,
pubkey?: string,
contact?: string,
supported_nips?: number[],
software?: string,
version?: string
}

View File

@ -3,6 +3,7 @@ import SettingsIndex from "Pages/settings/Index";
import Profile from "Pages/settings/Profile";
import Relay from "Pages/settings/Relays";
import Preferences from "Pages/settings/Preferences";
import RelayInfo from "Pages/settings/RelayInfo";
export default function SettingsPage() {
const navigate = useNavigate();
@ -26,7 +27,11 @@ export const SettingsRoutes: RouteObject[] = [
},
{
path: "relays",
element: <Relay />
element: <Relay />,
},
{
path: "relays/:addr",
element: <RelayInfo />
},
{
path: "preferences",

View File

@ -0,0 +1,57 @@
import ProfilePreview from "Element/ProfilePreview";
import useRelayState from "Feed/RelayState";
import { System } from "Nostr/System";
import { useDispatch } from "react-redux";
import { useNavigate, useParams } from "react-router-dom";
import { removeRelay } from "State/Login";
import { parseId } from "Util";
const RelayInfo = () => {
const params = useParams();
const navigate = useNavigate();
const dispatch = useDispatch();
const addr: string = `wss://${params.addr}`;
const con = System.Sockets.get(addr) ?? System.Sockets.get(`${addr}/`);
const stats = useRelayState(con?.Address ?? addr);
return (
<>
<h3 className="pointer" onClick={() => navigate("/settings/relays")}>Relays</h3>
<div className="card">
<h3>{stats?.info?.name ?? addr}</h3>
<p>{stats?.info?.description}</p>
{stats?.info?.pubkey && (<>
<h4>Owner</h4>
<ProfilePreview pubkey={parseId(stats.info.pubkey)} />
</>)}
{stats?.info?.software && (<div className="flex">
<h4 className="f-grow">Software</h4>
<div className="flex f-col">
{stats.info.software.startsWith("http") ? <a href={stats.info.software} target="_blank" rel="noreferrer">{stats.info.software}</a> : <>{stats.info.software}</>}
<small>{!stats.info.version?.startsWith("v") && "v"}{stats.info.version}</small>
</div>
</div>)}
{stats?.info?.contact && (<div className="flex">
<h4 className="f-grow">Contact</h4>
<a href={`${stats.info.contact.startsWith("mailto:") ? "" : "mailto:"}${stats.info.contact}`} target="_blank" rel="noreferrer">{stats.info.contact}</a>
</div>)}
{stats?.info?.supported_nips && (<>
<h4>Supports</h4>
<div className="f-grow">
{stats.info.supported_nips.map(a => <span className="pill" onClick={() => navigate(`https://github.com/nostr-protocol/nips/blob/master/${a.toString().padStart(2, "0")}.md`)}>NIP-{a.toString().padStart(2, "0")}</span>)}
</div>
</>)}
<div className="flex mt10 f-end">
<div className="btn error" onClick={() => {
dispatch(removeRelay(con!.Address));
navigate("/settings/relays")
}}>Remove</div>
</div>
</div>
</>
)
}
export default RelayInfo;

View File

@ -52,7 +52,7 @@ const RelaySettingsPage = () => {
<div className="flex f-col">
{Object.keys(relays || {}).map(a => <Relay addr={a} key={a} />)}
</div>
<div className="flex actions">
<div className="flex mt10">
<div className="f-grow"></div>
<div className="btn" onClick={() => saveRelays()}>Save</div>
</div>

View File

@ -222,6 +222,10 @@ textarea:placeholder {
align-items: flex-start !important;
}
.f-end {
justify-content: flex-end;
}
.w-max {
width: 100%;
width: -moz-available;