feat: close relays
This commit is contained in:
@ -2,12 +2,12 @@ import { Link } from "react-router-dom";
|
|||||||
|
|
||||||
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";
|
||||||
import RelayPermissions from "./permissions";
|
import RelayPermissions from "./permissions";
|
||||||
import RelayStatusLabel from "./status-label";
|
import RelayStatusLabel from "./status-label";
|
||||||
|
import RelayUptime from "./uptime";
|
||||||
|
|
||||||
export interface RelayProps {
|
export interface RelayProps {
|
||||||
addr: string;
|
addr: string;
|
||||||
|
@ -1,20 +1,13 @@
|
|||||||
import { sanitizeRelayUrl, unixNow } from "@snort/shared";
|
import { sanitizeRelayUrl, unixNow } from "@snort/shared";
|
||||||
import { EventKind, RequestBuilder } from "@snort/system";
|
import { EventKind, RequestBuilder } from "@snort/system";
|
||||||
import { useRequestBuilder } from "@snort/system-react";
|
import { useRequestBuilder } from "@snort/system-react";
|
||||||
import classNames from "classnames";
|
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { FormattedMessage } from "react-intl";
|
|
||||||
|
|
||||||
import { findTag } from "@/Utils";
|
import { findTag } from "@/Utils";
|
||||||
import { Day } from "@/Utils/Const";
|
import { Day, MonitorRelays } from "@/Utils/Const";
|
||||||
|
|
||||||
import UptimeLabel from "./uptime-label";
|
import UptimeLabel from "./uptime-label";
|
||||||
|
|
||||||
const MonitorRelays = [
|
|
||||||
"wss://relaypag.es",
|
|
||||||
"wss://relay.nostr.watch",
|
|
||||||
"wss://history.nostr.watch",
|
|
||||||
"wss://monitorlizard.nostr1.com",
|
|
||||||
];
|
|
||||||
export default function RelayUptime({ url }: { url: string }) {
|
export default function RelayUptime({ url }: { url: string }) {
|
||||||
const sub = useMemo(() => {
|
const sub = useMemo(() => {
|
||||||
const u = sanitizeRelayUrl(url);
|
const u = sanitizeRelayUrl(url);
|
67
packages/app/src/Hooks/useCloseRelays.ts
Normal file
67
packages/app/src/Hooks/useCloseRelays.ts
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import { unixNow } from "@snort/shared";
|
||||||
|
import { EventKind, NostrEvent, RequestBuilder } from "@snort/system";
|
||||||
|
import { useRequestBuilder } from "@snort/system-react";
|
||||||
|
import Geohash from "latlon-geohash";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
|
||||||
|
import { calculateDistance, findTag, getCountry } from "@/Utils";
|
||||||
|
import { Day, MonitorRelays } from "@/Utils/Const";
|
||||||
|
|
||||||
|
interface RelayDistance {
|
||||||
|
distance: number;
|
||||||
|
event: NostrEvent;
|
||||||
|
addr: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCloseRelays() {
|
||||||
|
const country = getCountry();
|
||||||
|
|
||||||
|
const sub = useMemo(() => {
|
||||||
|
const geoHash = Geohash.encode(country.lat, country.lon);
|
||||||
|
const geoHashes = [];
|
||||||
|
for (let x = 2; x < geoHash.length; x++) {
|
||||||
|
geoHashes.push(geoHash.substring(0, x));
|
||||||
|
}
|
||||||
|
const rb = new RequestBuilder(`close-relays:${country}`);
|
||||||
|
rb.withFilter()
|
||||||
|
.kinds([30_166 as EventKind])
|
||||||
|
.tag("g", geoHashes)
|
||||||
|
.since(unixNow() - Day)
|
||||||
|
.relay(MonitorRelays);
|
||||||
|
return rb;
|
||||||
|
}, [country]);
|
||||||
|
|
||||||
|
const data = useRequestBuilder(sub);
|
||||||
|
|
||||||
|
const distRelays = data
|
||||||
|
.map(a => {
|
||||||
|
const lowestDistance = a.tags
|
||||||
|
.filter(a => a[0] === "g" && a[1].length > 5)
|
||||||
|
.map(a => {
|
||||||
|
const g = Geohash.decode(a[1]);
|
||||||
|
return calculateDistance(g.lat, g.lon, country.lat, country.lon);
|
||||||
|
})
|
||||||
|
.reduce((acc, v) => (v < acc ? v : acc), Number.MAX_VALUE);
|
||||||
|
return {
|
||||||
|
distance: lowestDistance,
|
||||||
|
event: a,
|
||||||
|
addr: findTag(a, "d") ?? "",
|
||||||
|
} as RelayDistance;
|
||||||
|
})
|
||||||
|
.sort((a, b) => (b.distance < a.distance ? 1 : -1))
|
||||||
|
.reduce((acc, v) => {
|
||||||
|
const u = new URL(v.addr);
|
||||||
|
if (!acc.has(u.hostname)) {
|
||||||
|
acc.set(u.hostname, {
|
||||||
|
...v,
|
||||||
|
addr: `${u.protocol}//${u.host}/`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (acc.get(u.hostname)!.distance > v.distance) {
|
||||||
|
acc.set(u.hostname, v);
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, new Map<string, RelayDistance>());
|
||||||
|
|
||||||
|
return [...distRelays.values()];
|
||||||
|
}
|
@ -11,12 +11,11 @@ import RelayPermissions from "@/Components/Relay/permissions";
|
|||||||
import { RelayFavicon } from "@/Components/Relay/RelaysMetadata";
|
import { RelayFavicon } from "@/Components/Relay/RelaysMetadata";
|
||||||
import RelaySoftware from "@/Components/Relay/software";
|
import RelaySoftware from "@/Components/Relay/software";
|
||||||
import RelayStatusLabel from "@/Components/Relay/status-label";
|
import RelayStatusLabel from "@/Components/Relay/status-label";
|
||||||
|
import RelayUptime from "@/Components/Relay/uptime";
|
||||||
import ProfileImage from "@/Components/User/ProfileImage";
|
import ProfileImage from "@/Components/User/ProfileImage";
|
||||||
import useRelayState from "@/Feed/RelayState";
|
import useRelayState from "@/Feed/RelayState";
|
||||||
import { getRelayName, parseId } from "@/Utils";
|
import { getRelayName, parseId } from "@/Utils";
|
||||||
|
|
||||||
import RelayUptime from "./relays/uptime";
|
|
||||||
|
|
||||||
const RelayInfo = () => {
|
const RelayInfo = () => {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const [info, setInfo] = useState<RI>();
|
const [info, setInfo] = useState<RI>();
|
||||||
|
@ -1,19 +1,21 @@
|
|||||||
|
/* eslint-disable max-lines */
|
||||||
import { dedupe, removeUndefined, sanitizeRelayUrl } from "@snort/shared";
|
import { dedupe, removeUndefined, sanitizeRelayUrl } from "@snort/shared";
|
||||||
import { OutboxModel } from "@snort/system";
|
import { OutboxModel } from "@snort/system";
|
||||||
import { SnortContext } from "@snort/system-react";
|
import { SnortContext } from "@snort/system-react";
|
||||||
import { useContext, useMemo, useSyncExternalStore } from "react";
|
import { useContext, useMemo, useSyncExternalStore } from "react";
|
||||||
import { FormattedMessage } from "react-intl";
|
import { FormattedMessage, FormattedNumber } from "react-intl";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
import { RelayMetrics } from "@/Cache";
|
import { RelayMetrics } from "@/Cache";
|
||||||
import AsyncButton from "@/Components/Button/AsyncButton";
|
import AsyncButton from "@/Components/Button/AsyncButton";
|
||||||
import { CollapsedSection } from "@/Components/Collapsed";
|
import { CollapsedSection } from "@/Components/Collapsed";
|
||||||
import { RelayFavicon } from "@/Components/Relay/RelaysMetadata";
|
import { RelayFavicon } from "@/Components/Relay/RelaysMetadata";
|
||||||
|
import RelayUptime from "@/Components/Relay/uptime";
|
||||||
|
import UptimeLabel from "@/Components/Relay/uptime-label";
|
||||||
import useLogin from "@/Hooks/useLogin";
|
import useLogin from "@/Hooks/useLogin";
|
||||||
import { getRelayName } from "@/Utils";
|
import { getRelayName } from "@/Utils";
|
||||||
|
import { useCloseRelays } from "@/Hooks/useCloseRelays";
|
||||||
import RelayUptime from "./uptime";
|
import Uptime from "@/Components/Relay/uptime";
|
||||||
import UptimeLabel from "./uptime-label";
|
|
||||||
|
|
||||||
export function DiscoverRelays() {
|
export function DiscoverRelays() {
|
||||||
const { follows, relays, state } = useLogin(l => ({
|
const { follows, relays, state } = useLogin(l => ({
|
||||||
@ -56,6 +58,7 @@ export function DiscoverRelays() {
|
|||||||
[relays, metrics],
|
[relays, metrics],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const closeRelays = useCloseRelays();
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<CollapsedSection
|
<CollapsedSection
|
||||||
@ -161,6 +164,68 @@ export function DiscoverRelays() {
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</CollapsedSection>
|
</CollapsedSection>
|
||||||
|
|
||||||
|
<CollapsedSection
|
||||||
|
title={
|
||||||
|
<div className="text-xl">
|
||||||
|
<FormattedMessage defaultMessage="Close Relays" />
|
||||||
|
</div>
|
||||||
|
}>
|
||||||
|
<small>
|
||||||
|
<FormattedMessage defaultMessage="Relays close to your geographic location." />
|
||||||
|
</small>
|
||||||
|
<table className="table">
|
||||||
|
<thead>
|
||||||
|
<tr className="text-gray-light uppercase">
|
||||||
|
<th>
|
||||||
|
<FormattedMessage defaultMessage="Relay" description="Relay name (URL)" />
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
<FormattedMessage defaultMessage="Distance" />
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
<FormattedMessage defaultMessage="Uptime" />
|
||||||
|
</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{closeRelays
|
||||||
|
.filter(a => !isNaN(a.distance))
|
||||||
|
.slice(0, 100)
|
||||||
|
.map(a => (
|
||||||
|
<tr key={a.addr}>
|
||||||
|
<td title={a.addr}>
|
||||||
|
<Link to={`/settings/relays/${encodeURIComponent(a.addr)}`} className="flex gap-2 items-center">
|
||||||
|
<RelayFavicon url={a.addr} />
|
||||||
|
{getRelayName(a.addr)}
|
||||||
|
</Link>
|
||||||
|
</td>
|
||||||
|
<td className="text-center">
|
||||||
|
<FormattedMessage
|
||||||
|
defaultMessage="{n} km"
|
||||||
|
values={{
|
||||||
|
n: <FormattedNumber value={a.distance / 1000} maximumFractionDigits={0} />,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td className="text-center">
|
||||||
|
<Uptime url={a.addr} />
|
||||||
|
</td>
|
||||||
|
<td className="text-end">
|
||||||
|
<AsyncButton
|
||||||
|
className="!py-1 mb-1"
|
||||||
|
onClick={async () => {
|
||||||
|
await state.addRelay(a.addr, { read: true, write: true });
|
||||||
|
}}>
|
||||||
|
<FormattedMessage defaultMessage="Add" />
|
||||||
|
</AsyncButton>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</CollapsedSection>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -160,3 +160,13 @@ export const MaxAboutLength = 1000;
|
|||||||
* Snort backend publishes rates
|
* Snort backend publishes rates
|
||||||
*/
|
*/
|
||||||
export const SnortPubkey = "npub1sn0rtcjcf543gj4wsg7fa59s700d5ztys5ctj0g69g2x6802npjqhjjtws";
|
export const SnortPubkey = "npub1sn0rtcjcf543gj4wsg7fa59s700d5ztys5ctj0g69g2x6802npjqhjjtws";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of relay monitor relays
|
||||||
|
*/
|
||||||
|
export const MonitorRelays = [
|
||||||
|
"wss://relaypag.es",
|
||||||
|
"wss://relay.nostr.watch",
|
||||||
|
"wss://history.nostr.watch",
|
||||||
|
"wss://monitorlizard.nostr1.com",
|
||||||
|
];
|
||||||
|
@ -530,6 +530,22 @@ export function getCountry() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function calculateDistance(lat1: number, lon1: number, lat2: number, lon2: number): number {
|
||||||
|
const R = 6371000; // Radius of the Earth in meters
|
||||||
|
const dLat = toRadians(lat2 - lat1);
|
||||||
|
const dLon = toRadians(lon2 - lon1);
|
||||||
|
|
||||||
|
const a =
|
||||||
|
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
||||||
|
Math.cos(toRadians(lat1)) * Math.cos(toRadians(lat2)) * Math.sin(dLon / 2) * Math.sin(dLon / 2);
|
||||||
|
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||||
|
return R * c;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toRadians(degrees: number): number {
|
||||||
|
return degrees * (Math.PI / 180);
|
||||||
|
}
|
||||||
|
|
||||||
export function trackEvent(
|
export function trackEvent(
|
||||||
event: string,
|
event: string,
|
||||||
props?: Record<string, string | boolean>,
|
props?: Record<string, string | boolean>,
|
||||||
|
@ -333,6 +333,9 @@
|
|||||||
"7hp70g": {
|
"7hp70g": {
|
||||||
"defaultMessage": "NIP-05"
|
"defaultMessage": "NIP-05"
|
||||||
},
|
},
|
||||||
|
"7pFGAQ": {
|
||||||
|
"defaultMessage": "Close Relays"
|
||||||
|
},
|
||||||
"8/vBbP": {
|
"8/vBbP": {
|
||||||
"defaultMessage": "Reposts ({n})"
|
"defaultMessage": "Reposts ({n})"
|
||||||
},
|
},
|
||||||
@ -952,6 +955,9 @@
|
|||||||
"P7nJT9": {
|
"P7nJT9": {
|
||||||
"defaultMessage": "Total today (UTC): {amount} sats"
|
"defaultMessage": "Total today (UTC): {amount} sats"
|
||||||
},
|
},
|
||||||
|
"P8JC58": {
|
||||||
|
"defaultMessage": "Distance"
|
||||||
|
},
|
||||||
"PCSt5T": {
|
"PCSt5T": {
|
||||||
"defaultMessage": "Preferences"
|
"defaultMessage": "Preferences"
|
||||||
},
|
},
|
||||||
@ -1051,6 +1057,9 @@
|
|||||||
"StKzTE": {
|
"StKzTE": {
|
||||||
"defaultMessage": "The author has marked this note as a <i>sensitive topic</i>"
|
"defaultMessage": "The author has marked this note as a <i>sensitive topic</i>"
|
||||||
},
|
},
|
||||||
|
"T83nqf": {
|
||||||
|
"defaultMessage": "Relays close to your geographic location."
|
||||||
|
},
|
||||||
"TDR5ge": {
|
"TDR5ge": {
|
||||||
"defaultMessage": "Media in notes will automatically be shown for selected people, otherwise only the link will show"
|
"defaultMessage": "Media in notes will automatically be shown for selected people, otherwise only the link will show"
|
||||||
},
|
},
|
||||||
@ -1256,6 +1265,9 @@
|
|||||||
"aWpBzj": {
|
"aWpBzj": {
|
||||||
"defaultMessage": "Show more"
|
"defaultMessage": "Show more"
|
||||||
},
|
},
|
||||||
|
"abbGKq": {
|
||||||
|
"defaultMessage": "{n} km"
|
||||||
|
},
|
||||||
"b12Goz": {
|
"b12Goz": {
|
||||||
"defaultMessage": "Mnemonic"
|
"defaultMessage": "Mnemonic"
|
||||||
},
|
},
|
||||||
|
@ -110,6 +110,7 @@
|
|||||||
"7UOvbT": "Offline",
|
"7UOvbT": "Offline",
|
||||||
"7YkSA2": "Community Leader",
|
"7YkSA2": "Community Leader",
|
||||||
"7hp70g": "NIP-05",
|
"7hp70g": "NIP-05",
|
||||||
|
"7pFGAQ": "Close Relays",
|
||||||
"8/vBbP": "Reposts ({n})",
|
"8/vBbP": "Reposts ({n})",
|
||||||
"89q5wc": "Confirm Reposts",
|
"89q5wc": "Confirm Reposts",
|
||||||
"8BDFvJ": "Conventions for clients' use of e and p tags in text events",
|
"8BDFvJ": "Conventions for clients' use of e and p tags in text events",
|
||||||
@ -315,6 +316,7 @@
|
|||||||
"P61BTu": "Copy Event JSON",
|
"P61BTu": "Copy Event JSON",
|
||||||
"P7FD0F": "System (Default)",
|
"P7FD0F": "System (Default)",
|
||||||
"P7nJT9": "Total today (UTC): {amount} sats",
|
"P7nJT9": "Total today (UTC): {amount} sats",
|
||||||
|
"P8JC58": "Distance",
|
||||||
"PCSt5T": "Preferences",
|
"PCSt5T": "Preferences",
|
||||||
"PXQ0z0": "Receiving to <b>{wallet}</b>",
|
"PXQ0z0": "Receiving to <b>{wallet}</b>",
|
||||||
"PamNxw": "Unknown file header: {name}",
|
"PamNxw": "Unknown file header: {name}",
|
||||||
@ -348,6 +350,7 @@
|
|||||||
"SmuYUd": "What should we call you?",
|
"SmuYUd": "What should we call you?",
|
||||||
"Ss0sWu": "Pay Now",
|
"Ss0sWu": "Pay Now",
|
||||||
"StKzTE": "The author has marked this note as a <i>sensitive topic</i>",
|
"StKzTE": "The author has marked this note as a <i>sensitive topic</i>",
|
||||||
|
"T83nqf": "Relays close to your geographic location.",
|
||||||
"TDR5ge": "Media in notes will automatically be shown for selected people, otherwise only the link will show",
|
"TDR5ge": "Media in notes will automatically be shown for selected people, otherwise only the link will show",
|
||||||
"TH1fFo": "Telegram",
|
"TH1fFo": "Telegram",
|
||||||
"TJo5E6": "Preview",
|
"TJo5E6": "Preview",
|
||||||
@ -416,6 +419,7 @@
|
|||||||
"aRex7h": "Paid {amount} sats, fee {fee} sats",
|
"aRex7h": "Paid {amount} sats, fee {fee} sats",
|
||||||
"aSGz4J": "Connect to your own LND node with Lightning Node Connect",
|
"aSGz4J": "Connect to your own LND node with Lightning Node Connect",
|
||||||
"aWpBzj": "Show more",
|
"aWpBzj": "Show more",
|
||||||
|
"abbGKq": "{n} km",
|
||||||
"b12Goz": "Mnemonic",
|
"b12Goz": "Mnemonic",
|
||||||
"b5vAk0": "Your handle will act like a lightning address and will redirect to your chosen LNURL or Lightning address",
|
"b5vAk0": "Your handle will act like a lightning address and will redirect to your chosen LNURL or Lightning address",
|
||||||
"bF1MYT": "You are a community leader and are earning <b>{percent}</b> of referred users subscriptions!",
|
"bF1MYT": "You are a community leader and are earning <b>{percent}</b> of referred users subscriptions!",
|
||||||
|
Reference in New Issue
Block a user