feat: relay uptime
This commit is contained in:
@ -4,6 +4,7 @@ import { FormattedMessage } from "react-intl";
|
|||||||
|
|
||||||
import useRelayState from "@/Feed/RelayState";
|
import useRelayState from "@/Feed/RelayState";
|
||||||
import useLogin from "@/Hooks/useLogin";
|
import useLogin from "@/Hooks/useLogin";
|
||||||
|
import RelayUptime from "@/Pages/settings/relays/uptime";
|
||||||
import { getRelayName } from "@/Utils";
|
import { getRelayName } from "@/Utils";
|
||||||
|
|
||||||
import Icon from "../Icons/Icon";
|
import Icon from "../Icons/Icon";
|
||||||
@ -67,6 +68,9 @@ export default function Relay(props: RelayProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
<td className="text-center">
|
||||||
|
<RelayUptime url={props.addr} />
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<Icon
|
<Icon
|
||||||
name="trash"
|
name="trash"
|
||||||
|
16
packages/app/src/Components/Review.tsx
Normal file
16
packages/app/src/Components/Review.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { EventKind, NostrLink, RequestBuilder } from "@snort/system";
|
||||||
|
import { useRequestBuilder } from "@snort/system-react";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
|
||||||
|
export function ReviewSummary({ link }: { link: NostrLink }) {
|
||||||
|
const sub = useMemo(() => {
|
||||||
|
const rb = new RequestBuilder(`reviews:${link.id}`);
|
||||||
|
rb.withFilter()
|
||||||
|
.kinds([1986 as EventKind])
|
||||||
|
.replyToLink([link]);
|
||||||
|
return rb;
|
||||||
|
}, [link.id]);
|
||||||
|
|
||||||
|
const data = useRequestBuilder(sub);
|
||||||
|
return <pre>{JSON.stringify(data, undefined, 2)}</pre>;
|
||||||
|
}
|
@ -10,6 +10,8 @@ import useRelays from "@/Hooks/useRelays";
|
|||||||
import { saveRelays } from "@/Pages/settings/saveRelays";
|
import { saveRelays } from "@/Pages/settings/saveRelays";
|
||||||
import { sanitizeRelayUrl } from "@/Utils";
|
import { sanitizeRelayUrl } from "@/Utils";
|
||||||
|
|
||||||
|
import { DiscoverRelays } from "./relays/discover";
|
||||||
|
|
||||||
const RelaySettingsPage = () => {
|
const RelaySettingsPage = () => {
|
||||||
const { publisher, system } = useEventPublisher();
|
const { publisher, system } = useEventPublisher();
|
||||||
const relays = useRelays();
|
const relays = useRelays();
|
||||||
@ -79,6 +81,10 @@ const RelaySettingsPage = () => {
|
|||||||
<th>
|
<th>
|
||||||
<FormattedMessage defaultMessage="Permissions" />
|
<FormattedMessage defaultMessage="Permissions" />
|
||||||
</th>
|
</th>
|
||||||
|
<th>
|
||||||
|
<FormattedMessage defaultMessage="Uptime" />
|
||||||
|
</th>
|
||||||
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@ -100,6 +106,7 @@ const RelaySettingsPage = () => {
|
|||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
{myRelays()}
|
{myRelays()}
|
||||||
{addRelay()}
|
{addRelay()}
|
||||||
|
<DiscoverRelays />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
86
packages/app/src/Pages/settings/relays/discover.tsx
Normal file
86
packages/app/src/Pages/settings/relays/discover.tsx
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import { dedupe } from "@snort/shared";
|
||||||
|
import { OutboxModel } from "@snort/system";
|
||||||
|
import { SnortContext } from "@snort/system-react";
|
||||||
|
import { useContext, useMemo } from "react";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
|
|
||||||
|
import AsyncButton from "@/Components/Button/AsyncButton";
|
||||||
|
import { CollapsedSection } from "@/Components/Collapsed";
|
||||||
|
import { RelayFavicon } from "@/Components/Relay/RelaysMetadata";
|
||||||
|
import useLogin from "@/Hooks/useLogin";
|
||||||
|
import { getRelayName } from "@/Utils";
|
||||||
|
|
||||||
|
import RelayUptime from "./uptime";
|
||||||
|
|
||||||
|
export function DiscoverRelays() {
|
||||||
|
const { follows, relays, state } = useLogin(l => ({
|
||||||
|
follows: l.state.follows,
|
||||||
|
relays: l.state.relays,
|
||||||
|
v: l.state.version,
|
||||||
|
state: l.state,
|
||||||
|
}));
|
||||||
|
const system = useContext(SnortContext);
|
||||||
|
|
||||||
|
const topWriteRelays = useMemo(() => {
|
||||||
|
const outbox = OutboxModel.fromSystem(system);
|
||||||
|
return outbox
|
||||||
|
.pickTopRelays(follows ?? [], 1e31, "write")
|
||||||
|
.filter(a => !(relays?.some(b => b.url === a.key) ?? false));
|
||||||
|
}, [follows, relays]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<CollapsedSection
|
||||||
|
title={
|
||||||
|
<h4>
|
||||||
|
<FormattedMessage defaultMessage="Recommended Relays" />
|
||||||
|
</h4>
|
||||||
|
}>
|
||||||
|
<table className="table">
|
||||||
|
<thead>
|
||||||
|
<tr className="text-gray-light uppercase">
|
||||||
|
<th>
|
||||||
|
<FormattedMessage defaultMessage="Relay" description="Relay name (URL)" />
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
<FormattedMessage defaultMessage="Uptime" />
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
<FormattedMessage defaultMessage="Users" />
|
||||||
|
</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{dedupe(topWriteRelays.flatMap(a => a.relays))
|
||||||
|
.map(a => ({ relay: a, count: topWriteRelays.filter(b => b.relays.includes(a)).length }))
|
||||||
|
.sort((a, b) => (a.count > b.count ? -1 : 1))
|
||||||
|
.filter(a => !relays?.some(b => b.url === a.relay))
|
||||||
|
.slice(0, 20)
|
||||||
|
.map(a => (
|
||||||
|
<tr key={a.relay}>
|
||||||
|
<td className="flex gap-2 items-center">
|
||||||
|
<RelayFavicon url={a.relay} />
|
||||||
|
{getRelayName(a.relay)}
|
||||||
|
</td>
|
||||||
|
<td className="text-center">
|
||||||
|
<RelayUptime url={a.relay} />
|
||||||
|
</td>
|
||||||
|
<td className="text-center">{a.count}</td>
|
||||||
|
<td className="text-end">
|
||||||
|
<AsyncButton
|
||||||
|
className="!py-1 mb-1"
|
||||||
|
onClick={async () => {
|
||||||
|
await state.addRelay(a.relay, { read: true, write: true });
|
||||||
|
}}>
|
||||||
|
<FormattedMessage defaultMessage="Add" />
|
||||||
|
</AsyncButton>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</CollapsedSection>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
64
packages/app/src/Pages/settings/relays/uptime.tsx
Normal file
64
packages/app/src/Pages/settings/relays/uptime.tsx
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import { sanitizeRelayUrl, unixNow } from "@snort/shared";
|
||||||
|
import { EventKind, RequestBuilder } from "@snort/system";
|
||||||
|
import { useRequestBuilder } from "@snort/system-react";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
|
|
||||||
|
import { findTag } from "@/Utils";
|
||||||
|
import { Day } from "@/Utils/Const";
|
||||||
|
|
||||||
|
const MonitorRelays = [
|
||||||
|
"wss://relaypag.es",
|
||||||
|
"wss://relay.nostr.watch",
|
||||||
|
"wss://history.nostr.watch",
|
||||||
|
"wss://monitorlizard.nostr1.com",
|
||||||
|
];
|
||||||
|
export default function RelayUptime({ url }: { url: string }) {
|
||||||
|
const sub = useMemo(() => {
|
||||||
|
const u = sanitizeRelayUrl(url);
|
||||||
|
if (!u) return;
|
||||||
|
|
||||||
|
const rb = new RequestBuilder(`uptime`);
|
||||||
|
rb.withFilter()
|
||||||
|
.kinds([30_166 as EventKind])
|
||||||
|
.tag("d", [u])
|
||||||
|
.since(unixNow() - Day)
|
||||||
|
.relay(MonitorRelays);
|
||||||
|
return rb;
|
||||||
|
}, [url]);
|
||||||
|
|
||||||
|
const data = useRequestBuilder(sub);
|
||||||
|
const myData = data.filter(a => findTag(a, "d") === url);
|
||||||
|
const ping = myData.reduce(
|
||||||
|
(acc, v) => {
|
||||||
|
const read = findTag(v, "rtt-read");
|
||||||
|
if (read) {
|
||||||
|
acc.n += 1;
|
||||||
|
acc.total += Number(read);
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
n: 0,
|
||||||
|
total: 0,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const avgPing = ping.total / ping.n;
|
||||||
|
const idealPing = 500;
|
||||||
|
const badPing = idealPing * 2;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={classNames("font-semibold", {
|
||||||
|
"text-error": isNaN(avgPing) || avgPing > badPing,
|
||||||
|
"text-warning": avgPing > idealPing && avgPing < badPing,
|
||||||
|
"text-success": avgPing < idealPing,
|
||||||
|
})}
|
||||||
|
title={`${avgPing.toFixed(0)} ms`}>
|
||||||
|
{isNaN(avgPing) && <FormattedMessage defaultMessage="Dead" />}
|
||||||
|
{avgPing > badPing && <FormattedMessage defaultMessage="Poor" />}
|
||||||
|
{avgPing > idealPing && avgPing < badPing && <FormattedMessage defaultMessage="Good" />}
|
||||||
|
{avgPing < idealPing && <FormattedMessage defaultMessage="Great" />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -258,6 +258,9 @@
|
|||||||
"6uMqL1": {
|
"6uMqL1": {
|
||||||
"defaultMessage": "Unpaid"
|
"defaultMessage": "Unpaid"
|
||||||
},
|
},
|
||||||
|
"6xap9L": {
|
||||||
|
"defaultMessage": "Good"
|
||||||
|
},
|
||||||
"7+Domh": {
|
"7+Domh": {
|
||||||
"defaultMessage": "Notes"
|
"defaultMessage": "Notes"
|
||||||
},
|
},
|
||||||
@ -343,6 +346,9 @@
|
|||||||
"AkCxS/": {
|
"AkCxS/": {
|
||||||
"defaultMessage": "Reason"
|
"defaultMessage": "Reason"
|
||||||
},
|
},
|
||||||
|
"AktAk2": {
|
||||||
|
"defaultMessage": "Great"
|
||||||
|
},
|
||||||
"Am8glJ": {
|
"Am8glJ": {
|
||||||
"defaultMessage": "Game"
|
"defaultMessage": "Game"
|
||||||
},
|
},
|
||||||
@ -775,6 +781,9 @@
|
|||||||
"NndBJE": {
|
"NndBJE": {
|
||||||
"defaultMessage": "New users page"
|
"defaultMessage": "New users page"
|
||||||
},
|
},
|
||||||
|
"NxzeNU": {
|
||||||
|
"defaultMessage": "Dead"
|
||||||
|
},
|
||||||
"O3Jz4E": {
|
"O3Jz4E": {
|
||||||
"defaultMessage": "Use your invite code to earn sats!"
|
"defaultMessage": "Use your invite code to earn sats!"
|
||||||
},
|
},
|
||||||
@ -977,6 +986,9 @@
|
|||||||
"UxgyeY": {
|
"UxgyeY": {
|
||||||
"defaultMessage": "Your referral code is {code}"
|
"defaultMessage": "Your referral code is {code}"
|
||||||
},
|
},
|
||||||
|
"VL900k": {
|
||||||
|
"defaultMessage": "Recommended Relays"
|
||||||
|
},
|
||||||
"VOjC1i": {
|
"VOjC1i": {
|
||||||
"defaultMessage": "Pick which upload service you want to upload attachments to"
|
"defaultMessage": "Pick which upload service you want to upload attachments to"
|
||||||
},
|
},
|
||||||
@ -1043,6 +1055,9 @@
|
|||||||
"Xopqkl": {
|
"Xopqkl": {
|
||||||
"defaultMessage": "Your default zap amount is {number} sats, example values are calculated from this."
|
"defaultMessage": "Your default zap amount is {number} sats, example values are calculated from this."
|
||||||
},
|
},
|
||||||
|
"YDMrKK": {
|
||||||
|
"defaultMessage": "Users"
|
||||||
|
},
|
||||||
"YDURw6": {
|
"YDURw6": {
|
||||||
"defaultMessage": "Service URL"
|
"defaultMessage": "Service URL"
|
||||||
},
|
},
|
||||||
@ -1233,6 +1248,9 @@
|
|||||||
"ejEGdx": {
|
"ejEGdx": {
|
||||||
"defaultMessage": "Home"
|
"defaultMessage": "Home"
|
||||||
},
|
},
|
||||||
|
"eoV49s": {
|
||||||
|
"defaultMessage": "Poor"
|
||||||
|
},
|
||||||
"f1OxTe": {
|
"f1OxTe": {
|
||||||
"defaultMessage": "Community leaders are individuals who grow the nostr ecosystem by being active in their local communities and helping onboard new users. Anyone can become a community leader, but few hold the current honorary title."
|
"defaultMessage": "Community leaders are individuals who grow the nostr ecosystem by being active in their local communities and helping onboard new users. Anyone can become a community leader, but few hold the current honorary title."
|
||||||
},
|
},
|
||||||
@ -1659,6 +1677,9 @@
|
|||||||
"u/vOPu": {
|
"u/vOPu": {
|
||||||
"defaultMessage": "Paid"
|
"defaultMessage": "Paid"
|
||||||
},
|
},
|
||||||
|
"u81G9+": {
|
||||||
|
"defaultMessage": "Uptime"
|
||||||
|
},
|
||||||
"u9NoC1": {
|
"u9NoC1": {
|
||||||
"defaultMessage": "Name must be less than {limit} characters"
|
"defaultMessage": "Name must be less than {limit} characters"
|
||||||
},
|
},
|
||||||
|
@ -85,6 +85,7 @@
|
|||||||
"6k7xfM": "Trending notes",
|
"6k7xfM": "Trending notes",
|
||||||
"6mr8WU": "Followed by",
|
"6mr8WU": "Followed by",
|
||||||
"6uMqL1": "Unpaid",
|
"6uMqL1": "Unpaid",
|
||||||
|
"6xap9L": "Good",
|
||||||
"7+Domh": "Notes",
|
"7+Domh": "Notes",
|
||||||
"712i26": "Proxy uses HODL invoices to forward the payment, which hides the pubkey of your node",
|
"712i26": "Proxy uses HODL invoices to forward the payment, which hides the pubkey of your node",
|
||||||
"7UOvbT": "Offline",
|
"7UOvbT": "Offline",
|
||||||
@ -113,6 +114,7 @@
|
|||||||
"ASRK0S": "This author has been muted",
|
"ASRK0S": "This author has been muted",
|
||||||
"Ai8VHU": "Unlimited note retention on Snort relay",
|
"Ai8VHU": "Unlimited note retention on Snort relay",
|
||||||
"AkCxS/": "Reason",
|
"AkCxS/": "Reason",
|
||||||
|
"AktAk2": "Great",
|
||||||
"Am8glJ": "Game",
|
"Am8glJ": "Game",
|
||||||
"Aujn2T": "Count",
|
"Aujn2T": "Count",
|
||||||
"Awq32I": "Push notifications",
|
"Awq32I": "Push notifications",
|
||||||
@ -256,6 +258,7 @@
|
|||||||
"NdOYJJ": "Hmm nothing here.. Checkout {newUsersPage} to follow some recommended nostrich's!",
|
"NdOYJJ": "Hmm nothing here.. Checkout {newUsersPage} to follow some recommended nostrich's!",
|
||||||
"NepkXH": "Can't vote with {amount} sats, please set a different default zap amount",
|
"NepkXH": "Can't vote with {amount} sats, please set a different default zap amount",
|
||||||
"NndBJE": "New users page",
|
"NndBJE": "New users page",
|
||||||
|
"NxzeNU": "Dead",
|
||||||
"O3Jz4E": "Use your invite code to earn sats!",
|
"O3Jz4E": "Use your invite code to earn sats!",
|
||||||
"OEW7yJ": "Zaps",
|
"OEW7yJ": "Zaps",
|
||||||
"OKhRC6": "Share",
|
"OKhRC6": "Share",
|
||||||
@ -323,6 +326,7 @@
|
|||||||
"Ups2/p": "Your application is pending",
|
"Ups2/p": "Your application is pending",
|
||||||
"UrKTqQ": "You have an active iris.to account",
|
"UrKTqQ": "You have an active iris.to account",
|
||||||
"UxgyeY": "Your referral code is {code}",
|
"UxgyeY": "Your referral code is {code}",
|
||||||
|
"VL900k": "Recommended Relays",
|
||||||
"VOjC1i": "Pick which upload service you want to upload attachments to",
|
"VOjC1i": "Pick which upload service you want to upload attachments to",
|
||||||
"VR5eHw": "Public key (npub/nprofile)",
|
"VR5eHw": "Public key (npub/nprofile)",
|
||||||
"VcwrfF": "Yes please",
|
"VcwrfF": "Yes please",
|
||||||
@ -345,6 +349,7 @@
|
|||||||
"XgWvGA": "Reactions",
|
"XgWvGA": "Reactions",
|
||||||
"Xnimz0": "Sending from <b>{wallet}</b>",
|
"Xnimz0": "Sending from <b>{wallet}</b>",
|
||||||
"Xopqkl": "Your default zap amount is {number} sats, example values are calculated from this.",
|
"Xopqkl": "Your default zap amount is {number} sats, example values are calculated from this.",
|
||||||
|
"YDMrKK": "Users",
|
||||||
"YDURw6": "Service URL",
|
"YDURw6": "Service URL",
|
||||||
"YR2I9M": "No keys, no {app}, There is no way to reset it if you don't back up. It only takes a minute.",
|
"YR2I9M": "No keys, no {app}, There is no way to reset it if you don't back up. It only takes a minute.",
|
||||||
"YXA3AH": "Enable reactions",
|
"YXA3AH": "Enable reactions",
|
||||||
@ -408,6 +413,7 @@
|
|||||||
"eXT2QQ": "Group Chat",
|
"eXT2QQ": "Group Chat",
|
||||||
"egib+2": "{n,plural,=1{& {n} other} other{& {n} others}}",
|
"egib+2": "{n,plural,=1{& {n} other} other{& {n} others}}",
|
||||||
"ejEGdx": "Home",
|
"ejEGdx": "Home",
|
||||||
|
"eoV49s": "Poor",
|
||||||
"f1OxTe": "Community leaders are individuals who grow the nostr ecosystem by being active in their local communities and helping onboard new users. Anyone can become a community leader, but few hold the current honorary title.",
|
"f1OxTe": "Community leaders are individuals who grow the nostr ecosystem by being active in their local communities and helping onboard new users. Anyone can become a community leader, but few hold the current honorary title.",
|
||||||
"f2CAxA": "Dump",
|
"f2CAxA": "Dump",
|
||||||
"fBI91o": "Zap",
|
"fBI91o": "Zap",
|
||||||
@ -550,6 +556,7 @@
|
|||||||
"ttxS0b": "Supporter Badge",
|
"ttxS0b": "Supporter Badge",
|
||||||
"tzMNF3": "Status",
|
"tzMNF3": "Status",
|
||||||
"u/vOPu": "Paid",
|
"u/vOPu": "Paid",
|
||||||
|
"u81G9+": "Uptime",
|
||||||
"u9NoC1": "Name must be less than {limit} characters",
|
"u9NoC1": "Name must be less than {limit} characters",
|
||||||
"uCk8r+": "Already have an account?",
|
"uCk8r+": "Already have an account?",
|
||||||
"uSV4Ti": "Reposts need to be manually confirmed",
|
"uSV4Ti": "Reposts need to be manually confirmed",
|
||||||
|
@ -15,6 +15,7 @@ module.exports = {
|
|||||||
"nostr-purple": "var(--highlight)",
|
"nostr-purple": "var(--highlight)",
|
||||||
warning: "var(--warning)",
|
warning: "var(--warning)",
|
||||||
error: "var(--error)",
|
error: "var(--error)",
|
||||||
|
success: "var(--success)",
|
||||||
"gray-light": "var(--gray-light)",
|
"gray-light": "var(--gray-light)",
|
||||||
"gray-medium": "var(--gray-medium)",
|
"gray-medium": "var(--gray-medium)",
|
||||||
gray: "var(--gray)",
|
gray: "var(--gray)",
|
||||||
|
Reference in New Issue
Block a user