feat: relay info page

This commit is contained in:
2024-09-11 13:50:45 +01:00
parent 9660c07633
commit 6fafae67aa
14 changed files with 747 additions and 188 deletions

View File

@ -33,10 +33,11 @@ interface CollapsedSectionProps {
title: ReactNode;
children: ReactNode;
className?: string;
startClosed?: boolean;
}
export const CollapsedSection = ({ title, children, className }: CollapsedSectionProps) => {
const [collapsed, setCollapsed] = useState(true);
export const CollapsedSection = ({ title, children, className, startClosed }: CollapsedSectionProps) => {
const [collapsed, setCollapsed] = useState(startClosed ?? true);
const icon = (
<div className={classNames("collapse-icon", { flip: !collapsed })}>
<Icon name="arrowFront" />

View File

@ -1,6 +1,4 @@
import { RelaySettings } from "@snort/system";
import classNames from "classnames";
import { FormattedMessage } from "react-intl";
import { Link } from "react-router-dom";
import useRelayState from "@/Feed/RelayState";
import useLogin from "@/Hooks/useLogin";
@ -8,65 +6,30 @@ import RelayUptime from "@/Pages/settings/relays/uptime";
import { getRelayName } from "@/Utils";
import Icon from "../Icons/Icon";
import RelayPermissions from "./permissions";
import RelayStatusLabel from "./status-label";
export interface RelayProps {
addr: string;
}
export default function Relay(props: RelayProps) {
const { state } = useLogin(s => ({ v: s.state.version, state: s.state }));
const connection = useRelayState(props.addr);
const settings = state.relays?.find(a => a.url === props.addr)?.settings;
if (!connection || !settings) return;
async function configure(o: RelaySettings) {
await state.updateRelay(props.addr, o);
}
const { state } = useLogin(s => ({ v: s.state.version, state: s.state }));
if (!connection) return;
const name = connection.info?.name ?? getRelayName(props.addr);
return (
<tr>
<td className="text-ellipsis" title={props.addr}>
{name.length > 20 ? <>{name.slice(0, 20)}...</> : name}
<Link to={`/settings/relays/${encodeURIComponent(props.addr)}`}>
{name.length > 20 ? <>{name.slice(0, 20)}...</> : name}
</Link>
</td>
<td>
<div className="flex gap-1 items-center">
<div
className={classNames("rounded-full w-4 h-4", {
"bg-success": connection.isOpen,
"bg-error": !connection.isOpen,
})}></div>
{connection.isOpen ? (
<FormattedMessage defaultMessage="Connected" />
) : (
<FormattedMessage defaultMessage="Offline" />
)}
</div>
<RelayStatusLabel conn={connection} />
</td>
<td>
<div className="flex gap-2 cursor-pointer select-none justify-center">
<div
className={settings.read ? "" : "text-gray"}
onClick={() =>
configure({
read: !settings.read,
write: settings.write,
})
}>
<FormattedMessage defaultMessage="Read" />
</div>
<div
className={settings.write ? "" : "text-gray"}
onClick={() =>
configure({
read: settings.read,
write: !settings.write,
})
}>
<FormattedMessage defaultMessage="Write" />
</div>
</div>
<RelayPermissions conn={connection} />
</td>
<td className="text-center">
<RelayUptime url={props.addr} />

View File

@ -1,9 +0,0 @@
.favicon {
width: 21px;
height: 21px;
max-width: unset;
}
.relay-active {
color: var(--highlight);
}

View File

@ -1,12 +1,10 @@
import "./RelaysMetadata.css";
import { FullRelaySettings } from "@snort/system";
import { useState } from "react";
import Nostrich from "@/assets/img/nostrich.webp";
import Icon from "@/Components/Icons/Icon";
export const RelayFavicon = ({ url }: { url: string }) => {
export const RelayFavicon = ({ url, size }: { url: string; size?: number }) => {
const cleanUrl = url
.replace(/^wss:\/\//, "https://")
.replace(/^ws:\/\//, "http://")
@ -14,10 +12,12 @@ export const RelayFavicon = ({ url }: { url: string }) => {
const [faviconUrl, setFaviconUrl] = useState(`${cleanUrl}/favicon.ico`);
return (
<img
className="circle favicon"
className="rounded-full object-cover"
src={faviconUrl}
onError={() => setFaviconUrl(Nostrich)}
alt={`favicon for ${url}`}
width={size ?? 20}
height={size ?? 20}
/>
);
};

View File

@ -0,0 +1,17 @@
import { RelayInfo } from "@snort/system";
import classNames from "classnames";
import { FormattedMessage } from "react-intl";
export default function RelayPaymentLabel({ info }: { info: RelayInfo }) {
const isPaid = info?.limitation?.payment_required ?? false;
return (
<div
className={classNames("rounded-full px-2 py-1 font-medium", {
"bg-[var(--pro)] text-black": isPaid,
"bg-[var(--free)]": !isPaid,
})}>
{isPaid && <FormattedMessage defaultMessage="Paid" />}
{!isPaid && <FormattedMessage defaultMessage="Free" />}
</div>
);
}

View File

@ -0,0 +1,33 @@
import { ConnectionType } from "@snort/system/dist/connection-pool";
import { FormattedMessage } from "react-intl";
import useLogin from "@/Hooks/useLogin";
export default function RelayPermissions({ conn }: { conn: ConnectionType }) {
const { state } = useLogin(s => ({ v: s.state.version, state: s.state }));
return (
<div className="flex gap-2 cursor-pointer select-none">
<div
className={conn.settings.read ? "" : "text-gray"}
onClick={async () =>
await state.updateRelay(conn.address, {
read: !conn.settings.read,
write: conn.settings.write,
})
}>
<FormattedMessage defaultMessage="Read" />
</div>
<div
className={conn.settings.write ? "" : "text-gray"}
onClick={async () =>
await state.updateRelay(conn.address, {
read: conn.settings.read,
write: !conn.settings.write,
})
}>
<FormattedMessage defaultMessage="Write" />
</div>
</div>
);
}

View File

@ -0,0 +1,9 @@
import { Link } from "react-router-dom";
export default function RelaySoftware({ software }: { software: string }) {
if (software.includes("git")) {
const u = new URL(software);
return <Link to={software}>{u.pathname.split("/").at(-1)?.replace(".git", "")}</Link>;
}
return software;
}

View File

@ -0,0 +1,16 @@
import { ConnectionType } from "@snort/system/dist/connection-pool";
import classNames from "classnames";
import { FormattedMessage } from "react-intl";
export default function RelayStatusLabel({ conn }: { conn: ConnectionType }) {
return (
<div className="flex gap-1 items-center">
<div
className={classNames("rounded-full w-4 h-4", {
"bg-success": conn.isOpen,
"bg-error": !conn.isOpen,
})}></div>
{conn.isOpen ? <FormattedMessage defaultMessage="Connected" /> : <FormattedMessage defaultMessage="Offline" />}
</div>
);
}

View File

@ -0,0 +1,149 @@
import { FormattedMessage } from "react-intl";
export default function NipDescription({ nip }: { nip: number }) {
switch (nip) {
case 1:
return <FormattedMessage defaultMessage="Basic protocol flow description" />;
case 2:
return <FormattedMessage defaultMessage="Follow List" />;
case 3:
return <FormattedMessage defaultMessage="OpenTimestamps Attestations for Events" />;
case 4:
return <FormattedMessage defaultMessage="Encrypted Direct Message" />;
case 5:
return <FormattedMessage defaultMessage="Mapping Nostr keys to DNS-based internet identifiers" />;
case 6:
return <FormattedMessage defaultMessage="Basic key derivation from mnemonic seed phrase" />;
case 7:
return <FormattedMessage defaultMessage="window.nostr capability for web browsers" />;
case 8:
return <FormattedMessage defaultMessage="Handling Mentions" />;
case 9:
return <FormattedMessage defaultMessage="Event Deletion Request" />;
case 10:
return <FormattedMessage defaultMessage="Conventions for clients' use of e and p tags in text events" />;
case 11:
return <FormattedMessage defaultMessage="Relay Information Document" />;
case 13:
return <FormattedMessage defaultMessage="Proof of Work" />;
case 14:
return <FormattedMessage defaultMessage="Subject tag in text events" />;
case 15:
return <FormattedMessage defaultMessage="Nostr Marketplace (for resilient marketplaces)" />;
case 17:
return <FormattedMessage defaultMessage="Private Direct Messages" />;
case 18:
return <FormattedMessage defaultMessage="Reposts" />;
case 19:
return <FormattedMessage defaultMessage="bech32-encoded entities" />;
case 21:
return <FormattedMessage defaultMessage="nostr: URI scheme" />;
case 23:
return <FormattedMessage defaultMessage="Long-form Content" />;
case 24:
return <FormattedMessage defaultMessage="Extra metadata fields and tags" />;
case 25:
return <FormattedMessage defaultMessage="Reactions" />;
case 26:
return <FormattedMessage defaultMessage="Delegated Event Signing" />;
case 27:
return <FormattedMessage defaultMessage="Text Note References" />;
case 28:
return <FormattedMessage defaultMessage="Public Chat" />;
case 29:
return <FormattedMessage defaultMessage="Relay-based Groups" />;
case 30:
return <FormattedMessage defaultMessage="Custom Emoji" />;
case 31:
return <FormattedMessage defaultMessage="Dealing with Unknown Events" />;
case 32:
return <FormattedMessage defaultMessage="Labeling" />;
case 34:
return <FormattedMessage defaultMessage="git stuff" />;
case 35:
return <FormattedMessage defaultMessage="Torrents" />;
case 36:
return <FormattedMessage defaultMessage="Sensitive Content" />;
case 38:
return <FormattedMessage defaultMessage="User Statuses" />;
case 39:
return <FormattedMessage defaultMessage="External Identities in Profiles" />;
case 40:
return <FormattedMessage defaultMessage="Expiration Timestamp" />;
case 42:
return <FormattedMessage defaultMessage="Authentication of clients to relays" />;
case 44:
return <FormattedMessage defaultMessage="Versioned Encryption" />;
case 45:
return <FormattedMessage defaultMessage="Counting results" />;
case 46:
return <FormattedMessage defaultMessage="Nostr Connect" />;
case 47:
return <FormattedMessage defaultMessage="Wallet Connect" />;
case 48:
return <FormattedMessage defaultMessage="Proxy Tags" />;
case 49:
return <FormattedMessage defaultMessage="Private Key Encryption" />;
case 50:
return <FormattedMessage defaultMessage="Search Capability" />;
case 51:
return <FormattedMessage defaultMessage="Lists" />;
case 52:
return <FormattedMessage defaultMessage="Calendar Events" />;
case 53:
return <FormattedMessage defaultMessage="Live Activities" />;
case 54:
return <FormattedMessage defaultMessage="Wiki" />;
case 55:
return <FormattedMessage defaultMessage="Android Signer Application" />;
case 56:
return <FormattedMessage defaultMessage="Reporting" />;
case 57:
return <FormattedMessage defaultMessage="Lightning Zaps" />;
case 58:
return <FormattedMessage defaultMessage="Badges" />;
case 59:
return <FormattedMessage defaultMessage="Gift Wrap" />;
case 64:
return <FormattedMessage defaultMessage="Chess (PGN)" />;
case 65:
return <FormattedMessage defaultMessage="Relay List Metadata" />;
case 70:
return <FormattedMessage defaultMessage="Protected Events" />;
case 71:
return <FormattedMessage defaultMessage="Video Events" />;
case 72:
return <FormattedMessage defaultMessage="Moderated Communities" />;
case 73:
return <FormattedMessage defaultMessage="External Content IDs" />;
case 75:
return <FormattedMessage defaultMessage="Zap Goals" />;
case 78:
return <FormattedMessage defaultMessage="Application-specific data" />;
case 84:
return <FormattedMessage defaultMessage="Highlights" />;
case 89:
return <FormattedMessage defaultMessage="Recommended Application Handlers" />;
case 90:
return <FormattedMessage defaultMessage="Data Vending Machines" />;
case 92:
return <FormattedMessage defaultMessage="Media Attachments" />;
case 94:
return <FormattedMessage defaultMessage="File Metadata" />;
case 96:
return <FormattedMessage defaultMessage="HTTP File Storage Integration" />;
case 98:
return <FormattedMessage defaultMessage="HTTP Auth" />;
case 99:
return <FormattedMessage defaultMessage="Classified Listings" />;
default:
return (
<FormattedMessage
defaultMessage="Unknown NIP-{x}"
values={{
x: nip.toString().padStart(2, "0"),
}}
/>
);
}
}