mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-07-28 21:12:17 +02:00
use users relays to fetch users notes
cleanup relay urls
This commit is contained in:
@@ -57,7 +57,9 @@ const RequireCurrentAccount = ({ children }: { children: JSX.Element }) => {
|
|||||||
const RootPage = () => (
|
const RootPage = () => (
|
||||||
<RequireCurrentAccount>
|
<RequireCurrentAccount>
|
||||||
<Page>
|
<Page>
|
||||||
<Outlet />
|
<Suspense fallback={<Spinner />}>
|
||||||
|
<Outlet />
|
||||||
|
</Suspense>
|
||||||
</Page>
|
</Page>
|
||||||
</RequireCurrentAccount>
|
</RequireCurrentAccount>
|
||||||
);
|
);
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import {
|
import {
|
||||||
Text,
|
Text,
|
||||||
useDisclosure,
|
useDisclosure,
|
||||||
@@ -9,6 +9,14 @@ import {
|
|||||||
ModalBody,
|
ModalBody,
|
||||||
ModalCloseButton,
|
ModalCloseButton,
|
||||||
Button,
|
Button,
|
||||||
|
TableContainer,
|
||||||
|
Table,
|
||||||
|
Thead,
|
||||||
|
Tbody,
|
||||||
|
Td,
|
||||||
|
Tr,
|
||||||
|
Th,
|
||||||
|
Flex,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import relayPoolService from "../services/relay-pool";
|
import relayPoolService from "../services/relay-pool";
|
||||||
import { useInterval } from "react-use";
|
import { useInterval } from "react-use";
|
||||||
@@ -16,11 +24,14 @@ import { RelayStatus } from "./relay-status";
|
|||||||
import { useIsMobile } from "../hooks/use-is-mobile";
|
import { useIsMobile } from "../hooks/use-is-mobile";
|
||||||
import { RelayIcon } from "./icons";
|
import { RelayIcon } from "./icons";
|
||||||
import { Relay } from "../classes/relay";
|
import { Relay } from "../classes/relay";
|
||||||
|
import { RelayFavicon } from "./relay-favicon";
|
||||||
|
import relayScoreboardService from "../services/relay-scoreboard";
|
||||||
|
|
||||||
export const ConnectedRelays = () => {
|
export const ConnectedRelays = () => {
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||||
const [relays, setRelays] = useState<Relay[]>(relayPoolService.getRelays());
|
const [relays, setRelays] = useState<Relay[]>(relayPoolService.getRelays());
|
||||||
|
const sortedRelays = useMemo(() => relayScoreboardService.getRankedRelays(relays.map((r) => r.url)), [relays]);
|
||||||
|
|
||||||
useInterval(() => {
|
useInterval(() => {
|
||||||
setRelays(relayPoolService.getRelays());
|
setRelays(relayPoolService.getRelays());
|
||||||
@@ -41,17 +52,43 @@ export const ConnectedRelays = () => {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
<Modal isOpen={isOpen} onClose={onClose}>
|
<Modal isOpen={isOpen} onClose={onClose} size="5xl">
|
||||||
<ModalOverlay />
|
<ModalOverlay />
|
||||||
<ModalContent>
|
<ModalContent>
|
||||||
<ModalHeader pb="0">Connected Relays</ModalHeader>
|
<ModalHeader pb="0">Connected Relays</ModalHeader>
|
||||||
<ModalCloseButton />
|
<ModalCloseButton />
|
||||||
<ModalBody>
|
<ModalBody p="2">
|
||||||
{relays.map((relay) => (
|
<TableContainer>
|
||||||
<Text key={relay.url}>
|
<Table size="sm">
|
||||||
<RelayStatus url={relay.url} /> {relay.url}
|
<Thead>
|
||||||
</Text>
|
<Tr>
|
||||||
))}
|
<Th>Relay</Th>
|
||||||
|
<Th isNumeric>Claims</Th>
|
||||||
|
<Th isNumeric>Avg Response</Th>
|
||||||
|
<Th isNumeric>Disconnects</Th>
|
||||||
|
<Th isNumeric>Status</Th>
|
||||||
|
</Tr>
|
||||||
|
</Thead>
|
||||||
|
<Tbody>
|
||||||
|
{sortedRelays.map((url) => (
|
||||||
|
<Tr key={url}>
|
||||||
|
<Td>
|
||||||
|
<Flex alignItems="center" maxW="sm" overflow="hidden">
|
||||||
|
<RelayFavicon size="xs" relay={url} mr="2" />
|
||||||
|
<Text>{url}</Text>
|
||||||
|
</Flex>
|
||||||
|
</Td>
|
||||||
|
<Td isNumeric>{relayPoolService.getRelayClaims(url).size}</Td>
|
||||||
|
<Td isNumeric>{relayScoreboardService.getAverageResponseTime(url).toFixed(2)}ms</Td>
|
||||||
|
<Td isNumeric>{relayScoreboardService.getDisconnects(url)}</Td>
|
||||||
|
<Td isNumeric>
|
||||||
|
<RelayStatus url={url} />
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
))}
|
||||||
|
</Tbody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
12
src/helpers/relay.ts
Normal file
12
src/helpers/relay.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { RelayConfig } from "../classes/relay";
|
||||||
|
import { safeRelayUrl } from "./url";
|
||||||
|
|
||||||
|
export function normalizeRelayConfigs(relays: RelayConfig[]) {
|
||||||
|
return relays.reduce((newArr, r) => {
|
||||||
|
const safeUrl = safeRelayUrl(r.url);
|
||||||
|
if (safeUrl) {
|
||||||
|
newArr.push({ ...r, url: safeUrl });
|
||||||
|
}
|
||||||
|
return newArr;
|
||||||
|
}, [] as RelayConfig[]);
|
||||||
|
}
|
@@ -1,17 +1,20 @@
|
|||||||
import { utils } from "nostr-tools";
|
export function normalizeRelayUrl(relayUrl: string) {
|
||||||
|
const url = new URL(relayUrl);
|
||||||
export function validateRelayUrl(relayUrl: string) {
|
|
||||||
const normalized = utils.normalizeURL(relayUrl);
|
|
||||||
const url = new URL(normalized);
|
|
||||||
|
|
||||||
if (url.protocol !== "wss:" && url.protocol !== "ws:") throw new Error("Incorrect protocol");
|
if (url.protocol !== "wss:" && url.protocol !== "ws:") throw new Error("Incorrect protocol");
|
||||||
|
|
||||||
return url.toString();
|
url.pathname = url.pathname.replace(/\/+/g, "/");
|
||||||
|
if (url.pathname.endsWith("/")) url.pathname = url.pathname.slice(0, -1);
|
||||||
|
if ((url.port === "80" && url.protocol === "ws:") || (url.port === "443" && url.protocol === "wss:")) url.port = "";
|
||||||
|
url.searchParams.sort();
|
||||||
|
url.hash = "";
|
||||||
|
|
||||||
|
return url.origin + (url.pathname === "/" ? "" : url.pathname) + url.search;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function safeRelayUrl(relayUrl: string) {
|
export function safeRelayUrl(relayUrl: string) {
|
||||||
try {
|
try {
|
||||||
return validateRelayUrl(relayUrl);
|
return normalizeRelayUrl(relayUrl);
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
26
src/hooks/use-merged-user-relays.tsx
Normal file
26
src/hooks/use-merged-user-relays.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { useMemo } from "react";
|
||||||
|
import { RelayConfig, RelayMode } from "../classes/relay";
|
||||||
|
import { normalizeRelayConfigs } from "../helpers/relay";
|
||||||
|
import relayScoreboardService from "../services/relay-scoreboard";
|
||||||
|
import { useUserContacts } from "./use-user-contacts";
|
||||||
|
import { useUserRelays } from "./use-user-relays";
|
||||||
|
|
||||||
|
export default function useMergedUserRelays(pubkey: string) {
|
||||||
|
const contacts = useUserContacts(pubkey);
|
||||||
|
const userRelays = useUserRelays(pubkey);
|
||||||
|
|
||||||
|
return useMemo(() => {
|
||||||
|
let relays: RelayConfig[] = userRelays?.relays ?? [];
|
||||||
|
|
||||||
|
// use the relays stored in contacts if there are no relay config
|
||||||
|
if (relays.length === 0 && contacts) {
|
||||||
|
relays = contacts.relays;
|
||||||
|
}
|
||||||
|
|
||||||
|
// normalize relay urls and remove bad ones
|
||||||
|
const normalized = normalizeRelayConfigs(relays);
|
||||||
|
|
||||||
|
const rankedUrls = relayScoreboardService.getRankedRelays(normalized.map((r) => r.url));
|
||||||
|
return rankedUrls.map((u) => normalized.find((r) => r.url === u) as RelayConfig);
|
||||||
|
}, [userRelays, contacts]);
|
||||||
|
}
|
@@ -5,12 +5,11 @@ import { useReadRelayUrls } from "./use-client-relays";
|
|||||||
import useSubject from "./use-subject";
|
import useSubject from "./use-subject";
|
||||||
|
|
||||||
export function useUserContacts(pubkey: string, additionalRelays: string[] = [], alwaysRequest = false) {
|
export function useUserContacts(pubkey: string, additionalRelays: string[] = [], alwaysRequest = false) {
|
||||||
const clientRelays = useReadRelayUrls();
|
const readRelays = useReadRelayUrls(additionalRelays);
|
||||||
const relays = useMemo(() => unique(clientRelays.concat(additionalRelays)), [additionalRelays.join(",")]);
|
|
||||||
|
|
||||||
const observable = useMemo(
|
const observable = useMemo(
|
||||||
() => userContactsService.requestContacts(pubkey, relays, alwaysRequest),
|
() => userContactsService.requestContacts(pubkey, readRelays, alwaysRequest),
|
||||||
[pubkey, relays, alwaysRequest]
|
[pubkey, readRelays, alwaysRequest]
|
||||||
);
|
);
|
||||||
const contacts = useSubject(observable);
|
const contacts = useSubject(observable);
|
||||||
|
|
||||||
|
@@ -1,18 +1,16 @@
|
|||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { unique } from "../helpers/array";
|
|
||||||
import userRelaysService from "../services/user-relays";
|
import userRelaysService from "../services/user-relays";
|
||||||
import { useReadRelayUrls } from "./use-client-relays";
|
import { useReadRelayUrls } from "./use-client-relays";
|
||||||
import useSubject from "./use-subject";
|
import useSubject from "./use-subject";
|
||||||
|
|
||||||
export function useUserRelays(pubkey: string, additionalRelays: string[] = [], alwaysRequest = false) {
|
export function useUserRelays(pubkey: string, additionalRelays: string[] = [], alwaysRequest = false) {
|
||||||
const clientRelays = useReadRelayUrls();
|
const readRelays = useReadRelayUrls(additionalRelays);
|
||||||
const relays = useMemo(() => unique(clientRelays.concat(additionalRelays)), [additionalRelays.join(",")]);
|
|
||||||
|
|
||||||
const observable = useMemo(
|
const observable = useMemo(
|
||||||
() => userRelaysService.requestRelays(pubkey, relays, alwaysRequest),
|
() => userRelaysService.requestRelays(pubkey, readRelays, alwaysRequest),
|
||||||
[pubkey, relays, alwaysRequest]
|
[pubkey, readRelays.join("|"), alwaysRequest]
|
||||||
);
|
);
|
||||||
const contacts = useSubject(observable);
|
const userRelays = useSubject(observable);
|
||||||
|
|
||||||
return contacts;
|
return userRelays;
|
||||||
}
|
}
|
||||||
|
@@ -3,8 +3,6 @@ import { createRoot } from "react-dom/client";
|
|||||||
import { App } from "./app";
|
import { App } from "./app";
|
||||||
import { Providers } from "./providers";
|
import { Providers } from "./providers";
|
||||||
|
|
||||||
import "./services/pubkey-relay-weights";
|
|
||||||
|
|
||||||
const element = document.getElementById("root");
|
const element = document.getElementById("root");
|
||||||
if (!element) throw new Error("missing mount point");
|
if (!element) throw new Error("missing mount point");
|
||||||
const root = createRoot(element);
|
const root = createRoot(element);
|
||||||
|
@@ -38,8 +38,6 @@ const MIGRATIONS: MigrationFunction[] = [
|
|||||||
dnsIdentifiers.createIndex("domain", "domain", { unique: false });
|
dnsIdentifiers.createIndex("domain", "domain", { unique: false });
|
||||||
dnsIdentifiers.createIndex("updated", "updated", { unique: false });
|
dnsIdentifiers.createIndex("updated", "updated", { unique: false });
|
||||||
|
|
||||||
db.createObjectStore("pubkeyRelayWeights", { keyPath: "pubkey" });
|
|
||||||
|
|
||||||
db.createObjectStore("settings");
|
db.createObjectStore("settings");
|
||||||
db.createObjectStore("relayInfo");
|
db.createObjectStore("relayInfo");
|
||||||
db.createObjectStore("relayScoreboardStats", { keyPath: "relay" });
|
db.createObjectStore("relayScoreboardStats", { keyPath: "relay" });
|
||||||
@@ -69,7 +67,7 @@ export async function clearCacheData() {
|
|||||||
await db.clear("userRelays");
|
await db.clear("userRelays");
|
||||||
await db.clear("relayInfo");
|
await db.clear("relayInfo");
|
||||||
await db.clear("dnsIdentifiers");
|
await db.clear("dnsIdentifiers");
|
||||||
await db.clear("pubkeyRelayWeights");
|
await db.clear("relayScoreboardStats");
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -30,11 +30,6 @@ export interface CustomSchema extends DBSchema {
|
|||||||
indexes: { name: string; domain: string; pubkey: string; updated: number };
|
indexes: { name: string; domain: string; pubkey: string; updated: number };
|
||||||
};
|
};
|
||||||
relayInfo: { key: string; value: RelayInformationDocument };
|
relayInfo: { key: string; value: RelayInformationDocument };
|
||||||
pubkeyRelayWeights: {
|
|
||||||
key: string;
|
|
||||||
value: { pubkey: string; relays: Record<string, number>; updated: number };
|
|
||||||
indexes: { pubkey: string };
|
|
||||||
};
|
|
||||||
relayScoreboardStats: {
|
relayScoreboardStats: {
|
||||||
key: string;
|
key: string;
|
||||||
value: { relay: string; responseTimes: [number, Date][]; disconnects: Date[] };
|
value: { relay: string; responseTimes: [number, Date][]; disconnects: Date[] };
|
||||||
|
@@ -1,70 +0,0 @@
|
|||||||
import moment from "moment";
|
|
||||||
import db from "./db";
|
|
||||||
import { UserContacts } from "./user-contacts";
|
|
||||||
|
|
||||||
const changed = new Set();
|
|
||||||
const cache: Record<string, Record<string, number> | undefined> = {};
|
|
||||||
|
|
||||||
async function populateCacheFromDb(pubkey: string) {
|
|
||||||
if (!cache[pubkey]) {
|
|
||||||
cache[pubkey] = (await db.get("pubkeyRelayWeights", pubkey))?.relays;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function addWeight(pubkey: string, relay: string, weight: number = 1) {
|
|
||||||
await populateCacheFromDb(pubkey);
|
|
||||||
|
|
||||||
const relays = cache[pubkey] || (cache[pubkey] = {});
|
|
||||||
|
|
||||||
if (relays[relay]) {
|
|
||||||
relays[relay] += weight;
|
|
||||||
} else {
|
|
||||||
relays[relay] = weight;
|
|
||||||
}
|
|
||||||
changed.add(pubkey);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveCache() {
|
|
||||||
const now = moment().unix();
|
|
||||||
const transaction = db.transaction("pubkeyRelayWeights", "readwrite");
|
|
||||||
|
|
||||||
for (const [pubkey, relays] of Object.entries(cache)) {
|
|
||||||
if (changed.has(pubkey)) {
|
|
||||||
if (relays) transaction.store?.put({ pubkey, relays, updated: now });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
changed.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleContactList(contacts: UserContacts) {
|
|
||||||
// save the relays for contacts
|
|
||||||
for (const [pubkey, relay] of Object.entries(contacts.contactRelay)) {
|
|
||||||
if (relay) await addWeight(pubkey, relay);
|
|
||||||
}
|
|
||||||
|
|
||||||
// save this pubkeys relays
|
|
||||||
for (const [relay, opts] of Object.entries(contacts.relays)) {
|
|
||||||
// only save relays this users writes to
|
|
||||||
if (opts.write) {
|
|
||||||
await addWeight(contacts.pubkey, relay);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getPubkeyRelays(pubkey: string) {
|
|
||||||
await populateCacheFromDb(pubkey);
|
|
||||||
return cache[pubkey] || {};
|
|
||||||
}
|
|
||||||
const pubkeyRelayWeightsService = {
|
|
||||||
handleContactList,
|
|
||||||
getPubkeyRelays,
|
|
||||||
};
|
|
||||||
|
|
||||||
setInterval(() => saveCache(), 1000);
|
|
||||||
|
|
||||||
if (import.meta.env.DEV) {
|
|
||||||
// @ts-ignore
|
|
||||||
window.pubkeyRelayWeightsService = pubkeyRelayWeightsService;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default pubkeyRelayWeightsService;
|
|
@@ -66,6 +66,11 @@ export class RelayPoolService {
|
|||||||
|
|
||||||
const relayPoolService = new RelayPoolService();
|
const relayPoolService = new RelayPoolService();
|
||||||
|
|
||||||
|
setInterval(() => {
|
||||||
|
relayPoolService.reconnectRelays();
|
||||||
|
relayPoolService.pruneRelays();
|
||||||
|
}, 1000 * 15);
|
||||||
|
|
||||||
if (import.meta.env.DEV) {
|
if (import.meta.env.DEV) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
window.relayPoolService = relayPoolService;
|
window.relayPoolService = relayPoolService;
|
||||||
|
@@ -47,8 +47,7 @@ class RelayScoreboardService {
|
|||||||
|
|
||||||
getAverageResponseTime(relay: string) {
|
getAverageResponseTime(relay: string) {
|
||||||
const times = this.relayResponseTimes.get(relay);
|
const times = this.relayResponseTimes.get(relay);
|
||||||
// TODO: not sure if this is a good fix for keeping unconnected relays from the top of the list
|
if (times.length === 0) return Infinity;
|
||||||
if (times.length === 0) return 200;
|
|
||||||
const total = times.reduce((total, [time]) => total + time, 0);
|
const total = times.reduce((total, [time]) => total + time, 0);
|
||||||
return total / times.length;
|
return total / times.length;
|
||||||
}
|
}
|
||||||
|
@@ -4,17 +4,31 @@ import db from "./db";
|
|||||||
import { CachedPubkeyEventRequester } from "../classes/cached-pubkey-event-requester";
|
import { CachedPubkeyEventRequester } from "../classes/cached-pubkey-event-requester";
|
||||||
import { SuperMap } from "../classes/super-map";
|
import { SuperMap } from "../classes/super-map";
|
||||||
import Subject from "../classes/subject";
|
import Subject from "../classes/subject";
|
||||||
|
import { RelayConfig, RelayMode } from "../classes/relay";
|
||||||
|
|
||||||
export type UserContacts = {
|
export type UserContacts = {
|
||||||
pubkey: string;
|
pubkey: string;
|
||||||
relays: Record<string, { read: boolean; write: boolean }>;
|
relays: RelayConfig[];
|
||||||
contacts: string[];
|
contacts: string[];
|
||||||
contactRelay: Record<string, string | undefined>;
|
contactRelay: Record<string, string | undefined>;
|
||||||
created_at: number;
|
created_at: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type RelayJson = Record<string, { read: boolean; write: boolean }>;
|
||||||
|
function relayJsonToRelayConfig(relayJson: RelayJson) {
|
||||||
|
try {
|
||||||
|
return Array.from(Object.entries(relayJson)).map(([url, opts]) => ({
|
||||||
|
url,
|
||||||
|
mode: (opts.write ? RelayMode.WRITE : 0) | (opts.read ? RelayMode.READ : 0),
|
||||||
|
}));
|
||||||
|
} catch (e) {}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
function parseContacts(event: NostrEvent): UserContacts {
|
function parseContacts(event: NostrEvent): UserContacts {
|
||||||
const relays = safeJson(event.content, {}) as UserContacts["relays"];
|
const relayJson = safeJson(event.content, {}) as RelayJson;
|
||||||
|
const relays = relayJsonToRelayConfig(relayJson);
|
||||||
|
|
||||||
const pubkeys = event.tags.filter(isPTag).map((tag) => tag[1]);
|
const pubkeys = event.tags.filter(isPTag).map((tag) => tag[1]);
|
||||||
const contactRelay = event.tags.filter(isPTag).reduce((dir, tag) => {
|
const contactRelay = event.tags.filter(isPTag).reduce((dir, tag) => {
|
||||||
if (tag[2]) {
|
if (tag[2]) {
|
||||||
|
@@ -83,7 +83,7 @@ function receiveEvent(event: NostrEvent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
subscription.onEvent.subscribe((event) => {
|
subscription.onEvent.subscribe((event) => {
|
||||||
// pass the event ot the contacts service
|
// pass the event to the contacts service
|
||||||
userContactsService.receiveEvent(event);
|
userContactsService.receiveEvent(event);
|
||||||
receiveEvent(event);
|
receiveEvent(event);
|
||||||
});
|
});
|
||||||
|
@@ -25,7 +25,7 @@ import { RelayUrlInput } from "../../components/relay-url-input";
|
|||||||
import useSubject from "../../hooks/use-subject";
|
import useSubject from "../../hooks/use-subject";
|
||||||
import { RelayStatus } from "../../components/relay-status";
|
import { RelayStatus } from "../../components/relay-status";
|
||||||
import relayScoreboardService from "../../services/relay-scoreboard";
|
import relayScoreboardService from "../../services/relay-scoreboard";
|
||||||
import { validateRelayUrl } from "../../helpers/url";
|
import { normalizeRelayUrl } from "../../helpers/url";
|
||||||
|
|
||||||
export default function RelaysView() {
|
export default function RelaysView() {
|
||||||
const relays = useSubject(clientRelaysService.relays);
|
const relays = useSubject(clientRelaysService.relays);
|
||||||
@@ -54,7 +54,7 @@ export default function RelaysView() {
|
|||||||
const handleAddRelay = (event: SyntheticEvent<HTMLFormElement>) => {
|
const handleAddRelay = (event: SyntheticEvent<HTMLFormElement>) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
try {
|
try {
|
||||||
const url = validateRelayUrl(relayInputValue);
|
const url = normalizeRelayUrl(relayInputValue);
|
||||||
if (!relays.some((r) => r.url === url) && !pendingAdd.some((r) => r.url === url)) {
|
if (!relays.some((r) => r.url === url) && !pendingAdd.some((r) => r.url === url)) {
|
||||||
addActions.push({ url, mode: RelayMode.ALL });
|
addActions.push({ url, mode: RelayMode.ALL });
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { Flex, Tab, TabList, TabPanel, TabPanels, Tabs } from "@chakra-ui/react";
|
import { Flex, Spinner, Tab, TabList, TabPanel, TabPanels, Tabs } from "@chakra-ui/react";
|
||||||
import { Outlet, useLoaderData, useMatches, useNavigate } from "react-router-dom";
|
import { Outlet, useLoaderData, useMatches, useNavigate } from "react-router-dom";
|
||||||
import { useUserMetadata } from "../../hooks/use-user-metadata";
|
import { useUserMetadata } from "../../hooks/use-user-metadata";
|
||||||
import { getUserDisplayName } from "../../helpers/user-metadata";
|
import { getUserDisplayName } from "../../helpers/user-metadata";
|
||||||
@@ -6,6 +6,7 @@ import { useIsMobile } from "../../hooks/use-is-mobile";
|
|||||||
import { Bech32Prefix, normalizeToBech32 } from "../../helpers/nip19";
|
import { Bech32Prefix, normalizeToBech32 } from "../../helpers/nip19";
|
||||||
import { useAppTitle } from "../../hooks/use-app-title";
|
import { useAppTitle } from "../../hooks/use-app-title";
|
||||||
import Header from "./components/header";
|
import Header from "./components/header";
|
||||||
|
import { Suspense } from "react";
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{ label: "Notes", path: "notes" },
|
{ label: "Notes", path: "notes" },
|
||||||
@@ -52,7 +53,9 @@ const UserView = () => {
|
|||||||
<TabPanels overflow={isMobile ? undefined : "auto"} height="100%">
|
<TabPanels overflow={isMobile ? undefined : "auto"} height="100%">
|
||||||
{tabs.map(({ label }) => (
|
{tabs.map(({ label }) => (
|
||||||
<TabPanel key={label} pr={0} pl={0}>
|
<TabPanel key={label} pr={0} pl={0}>
|
||||||
<Outlet context={{ pubkey }} />
|
<Suspense fallback={<Spinner />}>
|
||||||
|
<Outlet context={{ pubkey }} />
|
||||||
|
</Suspense>
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
))}
|
))}
|
||||||
</TabPanels>
|
</TabPanels>
|
||||||
|
@@ -1,14 +1,39 @@
|
|||||||
import { Button, Flex, FormControl, FormLabel, Spinner, Switch, useDisclosure } from "@chakra-ui/react";
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Flex,
|
||||||
|
FormControl,
|
||||||
|
FormLabel,
|
||||||
|
ListItem,
|
||||||
|
Popover,
|
||||||
|
PopoverArrow,
|
||||||
|
PopoverBody,
|
||||||
|
PopoverCloseButton,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
Spinner,
|
||||||
|
Switch,
|
||||||
|
UnorderedList,
|
||||||
|
useDisclosure,
|
||||||
|
} from "@chakra-ui/react";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import { useOutletContext } from "react-router-dom";
|
import { useOutletContext } from "react-router-dom";
|
||||||
|
import { RelayMode } from "../../classes/relay";
|
||||||
|
import { RelayIcon } from "../../components/icons";
|
||||||
import { Note } from "../../components/note";
|
import { Note } from "../../components/note";
|
||||||
import { isNote } from "../../helpers/nostr-event";
|
import { isNote } from "../../helpers/nostr-event";
|
||||||
import { useReadRelayUrls } from "../../hooks/use-client-relays";
|
import useMergedUserRelays from "../../hooks/use-merged-user-relays";
|
||||||
import { useTimelineLoader } from "../../hooks/use-timeline-loader";
|
import { useTimelineLoader } from "../../hooks/use-timeline-loader";
|
||||||
|
|
||||||
const UserNotesTab = () => {
|
const UserNotesTab = () => {
|
||||||
const { pubkey } = useOutletContext() as { pubkey: string };
|
const { pubkey } = useOutletContext() as { pubkey: string };
|
||||||
const relays = useReadRelayUrls();
|
const userRelays = useMergedUserRelays(pubkey);
|
||||||
|
const relays = userRelays
|
||||||
|
.filter((r) => r.mode & RelayMode.WRITE)
|
||||||
|
.map((r) => r.url)
|
||||||
|
.filter(Boolean)
|
||||||
|
.slice(0, 4) as string[];
|
||||||
|
|
||||||
const { isOpen: showReplies, onToggle: toggleReplies } = useDisclosure();
|
const { isOpen: showReplies, onToggle: toggleReplies } = useDisclosure();
|
||||||
|
|
||||||
const { events, loading, loadMore } = useTimelineLoader(
|
const { events, loading, loadMore } = useTimelineLoader(
|
||||||
@@ -26,6 +51,25 @@ const UserNotesTab = () => {
|
|||||||
Show Replies
|
Show Replies
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<Switch id="show-replies" isChecked={showReplies} onChange={toggleReplies} />
|
<Switch id="show-replies" isChecked={showReplies} onChange={toggleReplies} />
|
||||||
|
<Box flexGrow={1} />
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger>
|
||||||
|
<Button variant="link" leftIcon={<RelayIcon />}>
|
||||||
|
Using Relays
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent>
|
||||||
|
<PopoverArrow />
|
||||||
|
<PopoverCloseButton />
|
||||||
|
<PopoverBody>
|
||||||
|
<UnorderedList>
|
||||||
|
{relays.map((url) => (
|
||||||
|
<ListItem key={url}>{url}</ListItem>
|
||||||
|
))}
|
||||||
|
</UnorderedList>
|
||||||
|
</PopoverBody>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
{timeline.map((event) => (
|
{timeline.map((event) => (
|
||||||
<Note key={event.id} event={event} maxHeight={300} />
|
<Note key={event.id} event={event} maxHeight={300} />
|
||||||
|
@@ -1,38 +1,18 @@
|
|||||||
import { Text, Grid, Box, IconButton, Flex, Heading, Badge, Alert, AlertIcon } from "@chakra-ui/react";
|
import { Text, Box, IconButton, Flex, Badge } from "@chakra-ui/react";
|
||||||
import { useUserContacts } from "../../hooks/use-user-contacts";
|
|
||||||
import { useNavigate, useOutletContext } from "react-router-dom";
|
import { useNavigate, useOutletContext } from "react-router-dom";
|
||||||
import { GlobalIcon } from "../../components/icons";
|
import { GlobalIcon } from "../../components/icons";
|
||||||
import { useUserRelays } from "../../hooks/use-user-relays";
|
|
||||||
import { useMemo } from "react";
|
|
||||||
import relayScoreboardService from "../../services/relay-scoreboard";
|
import relayScoreboardService from "../../services/relay-scoreboard";
|
||||||
import { RelayConfig, RelayMode } from "../../classes/relay";
|
import { RelayMode } from "../../classes/relay";
|
||||||
|
import useMergedUserRelays from "../../hooks/use-merged-user-relays";
|
||||||
|
|
||||||
const UserRelaysTab = () => {
|
const UserRelaysTab = () => {
|
||||||
const { pubkey } = useOutletContext() as { pubkey: string };
|
const { pubkey } = useOutletContext() as { pubkey: string };
|
||||||
const contacts = useUserContacts(pubkey);
|
const userRelays = useMergedUserRelays(pubkey);
|
||||||
const userRelays = useUserRelays(pubkey);
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const relays = useMemo(() => {
|
|
||||||
let arr: RelayConfig[] = userRelays?.relays ?? [];
|
|
||||||
if (arr.length === 0 && contacts)
|
|
||||||
arr = Array.from(Object.entries(contacts.relays)).map(([url, opts]) => ({
|
|
||||||
url,
|
|
||||||
mode: (opts.write ? RelayMode.WRITE : 0) | (opts.read ? RelayMode.READ : 0),
|
|
||||||
}));
|
|
||||||
const rankedUrls = relayScoreboardService.getRankedRelays(arr.map((r) => r.url));
|
|
||||||
return rankedUrls.map((u) => arr.find((r) => r.url === u) as RelayConfig);
|
|
||||||
}, [userRelays, contacts]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex direction="column" gap="2">
|
<Flex direction="column" gap="2">
|
||||||
{!userRelays && contacts?.relays && (
|
{userRelays.map((relayConfig) => (
|
||||||
<Alert status="warning">
|
|
||||||
<AlertIcon />
|
|
||||||
Cant find new relay list
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
{relays.map((relayConfig) => (
|
|
||||||
<Box key={relayConfig.url} display="flex" gap="2" alignItems="center" pr="2" pl="2">
|
<Box key={relayConfig.url} display="flex" gap="2" alignItems="center" pr="2" pl="2">
|
||||||
<Text flex={1}>{relayConfig.url}</Text>
|
<Text flex={1}>{relayConfig.url}</Text>
|
||||||
<Text>{relayScoreboardService.getAverageResponseTime(relayConfig.url).toFixed(2)}ms</Text>
|
<Text>{relayScoreboardService.getAverageResponseTime(relayConfig.url).toFixed(2)}ms</Text>
|
||||||
|
Reference in New Issue
Block a user