mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-04-11 21:29:26 +02:00
add user relay service
remove settings.relays
This commit is contained in:
parent
a517df5386
commit
4413b3588c
@ -48,6 +48,7 @@
|
||||
- [ ] [NIP-42](https://github.com/nostr-protocol/nips/blob/master/42.md): Authentication of clients to relays
|
||||
- [ ] [NIP-50](https://github.com/nostr-protocol/nips/blob/master/50.md): Keywords filter
|
||||
- [ ] [NIP-56](https://github.com/nostr-protocol/nips/blob/master/56.md): Reporting
|
||||
- [x] [NIP-65](https://github.com/nostr-protocol/nips/blob/master/65.md): Relay List Metadata
|
||||
|
||||
## TODO
|
||||
|
||||
|
@ -1,7 +1,9 @@
|
||||
{
|
||||
"_": "Download from https://nostr.watch/",
|
||||
"_": "Download from https://nostr.watch/relays.json",
|
||||
"relays": [
|
||||
"was://nostr.pleb.network",
|
||||
"wss://astral.ninja",
|
||||
"wss://bitcoiner.social",
|
||||
"wss://blg.nostr.sx",
|
||||
"wss://brb.io",
|
||||
"wss://btc.klendazu.com",
|
||||
@ -9,14 +11,19 @@
|
||||
"wss://deschooling.us",
|
||||
"wss://e.nos.lol",
|
||||
"wss://eden.nostr.land",
|
||||
"wss://eden.nostr.land",
|
||||
"wss://expensive-relay.fiatjaf.com",
|
||||
"wss://foolay.nostr.moe",
|
||||
"wss://freedom-relay.herokuapp.com/ws",
|
||||
"wss://freespeech.casa",
|
||||
"wss://global.relay.red",
|
||||
"wss://jiggytom.ddns.net",
|
||||
"wss://knostr.neutrine.com:8880",
|
||||
"wss://knostr.neutrine.com",
|
||||
"wss://lightningrelay.com",
|
||||
"wss://lv01.tater.ninja",
|
||||
"wss://Merrcurr.up.railway.app",
|
||||
"wss://middling.myddns.me:8080",
|
||||
"wss://mule.platanito.org",
|
||||
"wss://no-str.org",
|
||||
"wss://no.contry.xyz",
|
||||
@ -25,6 +32,7 @@
|
||||
"wss://node01.nostress.cc",
|
||||
"wss://nos.lol",
|
||||
"wss://nostr-01.bolt.observer",
|
||||
"wss://nostr-01.dorafactory.org",
|
||||
"wss://nostr-1.nbo.angani.co",
|
||||
"wss://nostr-2.orba.ca",
|
||||
"wss://nostr-2.zebedee.cloud",
|
||||
@ -46,6 +54,7 @@
|
||||
"wss://nostr-relay.j3s7m4n.com",
|
||||
"wss://nostr-relay.lnmarkets.com",
|
||||
"wss://nostr-relay.nonce.academy",
|
||||
"wss://nostr-relay.pcdkd.fyi",
|
||||
"wss://nostr-relay.schnitzel.world",
|
||||
"wss://nostr-relay.smoove.net",
|
||||
"wss://nostr-relay.texashedge.xyz",
|
||||
@ -53,28 +62,42 @@
|
||||
"wss://nostr-relay.untethr.me",
|
||||
"wss://nostr-relay.usebitcoin.space",
|
||||
"wss://nostr-relay.wolfandcrow.tech",
|
||||
"wss://nostr-sg.com",
|
||||
"wss://nostr-sg.com",
|
||||
"wss://nostr-verif.slothy.win",
|
||||
"wss://nostr-verified.wellorder.net",
|
||||
"wss://nostr.1f52b.xyz",
|
||||
"wss://nostr.21crypto.ch",
|
||||
"wss://nostr.21m.fr",
|
||||
"wss://nostr.8e23.net",
|
||||
"wss://nostr.actn.io",
|
||||
"wss://nostr.ahaspharos.de",
|
||||
"wss://nostr.aozing.com",
|
||||
"wss://nostr.app.runonflux.io",
|
||||
"wss://nostr.argdx.net",
|
||||
"wss://nostr.arguflow.gg",
|
||||
"wss://nostr.bch.ninja",
|
||||
"wss://nostr.bingtech.tk",
|
||||
"wss://nostr.bitcoin-21.org",
|
||||
"wss://nostr.bitcoin-21.org",
|
||||
"wss://nostr.bitcoin.sex",
|
||||
"wss://nostr.bitcoiner.social",
|
||||
"wss://nostr.bitcoinplebs.de",
|
||||
"wss://nostr.blockchaincaffe.it",
|
||||
"wss://nostr.blockpower.capital",
|
||||
"wss://nostr.blocs.fr",
|
||||
"wss://nostr.bongbong.com",
|
||||
"wss://nostr.bostonbtc.com",
|
||||
"wss://nostr.cercatrova.me",
|
||||
"wss://nostr.chainofimmortals.net",
|
||||
"wss://nostr.chaker.net",
|
||||
"wss://nostr.cheeserobot.org",
|
||||
"wss://nostr.cizmar.net",
|
||||
"wss://nostr.coinos.io",
|
||||
"wss://nostr.coinsamba.com.br",
|
||||
"wss://nostr.com.de",
|
||||
"wss://nostr.coollamer.com",
|
||||
"wss://nostr.corebreach.com",
|
||||
"wss://nostr.cro.social",
|
||||
"wss://nostr.d11n.net",
|
||||
"wss://nostr.datamagik.com",
|
||||
"wss://nostr.delo.software",
|
||||
@ -89,26 +112,33 @@
|
||||
"wss://nostr.fly.dev",
|
||||
"wss://nostr.fmt.wiz.biz",
|
||||
"wss://nostr.formigator.eu",
|
||||
"wss://nostr.fractalized.ovh",
|
||||
"wss://nostr.globals.fans",
|
||||
"wss://nostr.gromeul.eu",
|
||||
"wss://nostr.gruntwerk.org",
|
||||
"wss://nostr.hackerman.pro",
|
||||
"wss://nostr.handyjunky.com",
|
||||
"wss://nostr.hugo.md",
|
||||
"wss://nostr.hyperlingo.com",
|
||||
"wss://nostr.inosta.cc",
|
||||
"wss://nostr.island.network",
|
||||
"wss://nostr.itssilvestre.com",
|
||||
"wss://nostr.jiashanlu.synology.me",
|
||||
"wss://nostr.jimc.me",
|
||||
"wss://nostr.klabo.blog",
|
||||
"wss://nostr.kollider.xyz",
|
||||
"wss://nostr.lapalomilla.mx",
|
||||
"wss://nostr.leximaster.com",
|
||||
"wss://nostr.lnorb.com",
|
||||
"wss://nostr.lnprivate.network",
|
||||
"wss://nostr.localhost.re",
|
||||
"wss://nostr.lordkno.ws",
|
||||
"wss://nostr.lu.ke",
|
||||
"wss://nostr.mado.io",
|
||||
"wss://nostr.massmux.com",
|
||||
"wss://nostr.mikedilger.com",
|
||||
"wss://nostr.milou.lol",
|
||||
"wss://nostr.milou.lol",
|
||||
"wss://nostr.mom",
|
||||
"wss://nostr.mouton.dev",
|
||||
"wss://nostr.mrbits.it",
|
||||
@ -116,6 +146,7 @@
|
||||
"wss://nostr.mwmdev.com",
|
||||
"wss://nostr.namek.link",
|
||||
"wss://nostr.ncsa.illinois.edu",
|
||||
"wss://nostr.nikolaj.online",
|
||||
"wss://nostr.nodeofsven.com",
|
||||
"wss://nostr.noones.com",
|
||||
"wss://nostr.nordlysln.net",
|
||||
@ -124,12 +155,13 @@
|
||||
"wss://nostr.onsats.org",
|
||||
"wss://nostr.oooxxx.ml",
|
||||
"wss://nostr.openchain.fr",
|
||||
"wss://nostr.orangepill.dev",
|
||||
"wss://nostr.orba.ca",
|
||||
"wss://nostr.oxtr.dev",
|
||||
"wss://nostr.p2sh.co",
|
||||
"wss://nostr.pleb.network",
|
||||
"wss://nostr.plebchain.org",
|
||||
"wss://nostr.pobblelabs.org",
|
||||
"wss://nostr.radixrat.com",
|
||||
"wss://nostr.randomdevelopment.biz",
|
||||
"wss://nostr.rdfriedl.com",
|
||||
"wss://nostr.relayer.se",
|
||||
"wss://nostr.rewardsbunny.com",
|
||||
@ -137,52 +169,69 @@
|
||||
"wss://nostr.rocks",
|
||||
"wss://nostr.roundrockbitcoiners.com",
|
||||
"wss://nostr.sandwich.farm",
|
||||
"wss://nostr.satoshi.fun",
|
||||
"wss://nostr.satsophone.tk",
|
||||
"wss://nostr.screaminglife.io",
|
||||
"wss://nostr.sebastix.dev",
|
||||
"wss://nostr.sectiontwo.org",
|
||||
"wss://nostr.semisol.dev",
|
||||
"wss://nostr.sg",
|
||||
"wss://nostr.shadownode.org",
|
||||
"wss://nostr.shawnyeager.net",
|
||||
"wss://nostr.shmueli.org",
|
||||
"wss://nostr.sidnlabs.nl",
|
||||
"wss://nostr.simatime.com",
|
||||
"wss://nostr.slothy.win",
|
||||
"wss://nostr.snblago.com",
|
||||
"wss://nostr.sovbit.com",
|
||||
"wss://nostr.supremestack.xyz",
|
||||
"wss://nostr.swiss-enigma.ch",
|
||||
"wss://nostr.thesimplekid.com",
|
||||
"wss://nostr.thibautrey.fr",
|
||||
"wss://nostr.tunnelsats.com",
|
||||
"wss://nostr.unknown.place",
|
||||
"wss://nostr.up.railway.app",
|
||||
"wss://nostr.uselessshit.co",
|
||||
"wss://nostr.utxo.lol",
|
||||
"wss://nostr.v0l.io",
|
||||
"wss://nostr.vulpem.com",
|
||||
"wss://nostr.w3ird.tech",
|
||||
"wss://nostr.walletofsatoshi.com",
|
||||
"wss://nostr.whoop.ph",
|
||||
"wss://nostr.wine",
|
||||
"wss://nostr.wine",
|
||||
"wss://nostr.xpersona.net",
|
||||
"wss://nostr.yael.at",
|
||||
"wss://nostr.zaprite.io",
|
||||
"wss://nostr.zebedee.cloud",
|
||||
"wss://nostr.zenon.wtf",
|
||||
"wss://nostr.zerofeerouting.com",
|
||||
"wss://nostr.zkid.social",
|
||||
"wss://nostr.zoomout.chat",
|
||||
"wss://nostr1.starbackr.me",
|
||||
"wss://nostr1.tunnelsats.com",
|
||||
"wss://nostr2.actn.io",
|
||||
"wss://nostr2.namek.link",
|
||||
"wss://nostr21.com",
|
||||
"wss://nostr3.actn.io",
|
||||
"wss://nostrafrica.pcdkd.fyi",
|
||||
"wss://nostream.gromeul.eu",
|
||||
"wss://nostream.kinchie.snowinning.com",
|
||||
"wss://nostream.nostrly.io",
|
||||
"wss://nostrex.fly.dev",
|
||||
"wss://nostrical.com",
|
||||
"wss://nostrich.friendship.tw",
|
||||
"wss://nostrrelay.com",
|
||||
"wss://offchain.pub",
|
||||
"wss://paid.no.str.cr",
|
||||
"wss://paid.spore.ws",
|
||||
"wss://pow32.nostr.land",
|
||||
"wss://private-nostr.v0l.io",
|
||||
"wss://private.red.gb.net",
|
||||
"wss://public.nostr.swissrouting.com",
|
||||
"wss://relay-pub.deschooling.us",
|
||||
"wss://relay.21spirits.io",
|
||||
"wss://relay.bitid.nz",
|
||||
"wss://relay.bleskop.com",
|
||||
"wss://relay.boring.surf",
|
||||
"wss://relay.cryptocculture.com",
|
||||
"wss://relay.current.fyi",
|
||||
@ -192,6 +241,8 @@
|
||||
"wss://relay.farscapian.com",
|
||||
"wss://relay.futohq.com",
|
||||
"wss://relay.grunch.dev",
|
||||
"wss://relay.honk.pw",
|
||||
"wss://relay.koreus.social",
|
||||
"wss://relay.kronkltd.net",
|
||||
"wss://relay.lexingtonbitcoin.org",
|
||||
"wss://relay.minds.com/nostr/v1/ws",
|
||||
@ -199,28 +250,42 @@
|
||||
"wss://relay.n057r.club",
|
||||
"wss://relay.nosphr.com",
|
||||
"wss://relay.nostr-latam.link",
|
||||
"wss://relay.nostr.ae",
|
||||
"wss://relay.nostr.africa",
|
||||
"wss://relay.nostr.au",
|
||||
"wss://relay.nostr.band",
|
||||
"wss://relay.nostr.band/restricted",
|
||||
"wss://relay.nostr.bg",
|
||||
"wss://relay.nostr.ch",
|
||||
"wss://relay.nostr.express",
|
||||
"wss://relay.nostr.hu",
|
||||
"wss://relay.nostr.info",
|
||||
"wss://relay.nostr.lu",
|
||||
"wss://relay.nostr.moe",
|
||||
"wss://relay.nostr.nu",
|
||||
"wss://relay.nostr.or.jp",
|
||||
"wss://relay.nostr.pro",
|
||||
"wss://relay.nostr.ro",
|
||||
"wss://relay.nostr.scot",
|
||||
"wss://relay.nostr.vision",
|
||||
"wss://relay.nostr.wf",
|
||||
"wss://relay.nostr.wirednet.jp",
|
||||
"wss://relay.nostr.xyz",
|
||||
"wss://relay.nostrgraph.net",
|
||||
"wss://relay.nostrgraph.net",
|
||||
"wss://relay.nostrich.de",
|
||||
"wss://relay.nostrich.land",
|
||||
"wss://relay.nostriches.org",
|
||||
"wss://relay.nostrid.com",
|
||||
"wss://relay.nostrmoto.xyz",
|
||||
"wss://relay.nostrology.org",
|
||||
"wss://relay.nostropolis.xyz/websocket",
|
||||
"wss://relay.nostrprotocol.net",
|
||||
"wss://relay.nostrview.com",
|
||||
"wss://relay.nostrzoo.com",
|
||||
"wss://relay.nyx.ma",
|
||||
"wss://relay.oldcity-bitcoiners.info",
|
||||
"wss://relay.orangepill.dev",
|
||||
"wss://relay.plebstr.com",
|
||||
"wss://relay.r3d.red",
|
||||
"wss://relay.ryzizub.com",
|
||||
@ -229,12 +294,16 @@
|
||||
"wss://relay.sovereign-stack.org",
|
||||
"wss://relay.stoner.com",
|
||||
"wss://relay.taxi",
|
||||
"wss://relay.thes.ai",
|
||||
"wss://relay.valireum.net",
|
||||
"wss://relay.zeh.app",
|
||||
"wss://rsr.uyky.net:30443",
|
||||
"wss://rsslay.nostr.moe",
|
||||
"wss://rsslay.nostr.net",
|
||||
"wss://satstacker.cloud",
|
||||
"wss://sg.qemura.xyz",
|
||||
"wss://spleenrider.herokuapp.com",
|
||||
"wss://spore.ws",
|
||||
"wss://student.chadpolytechnic.com",
|
||||
"wss://tmp-relay.cesc.trade",
|
||||
"wss://wizards.wormrobot.org",
|
||||
|
@ -22,6 +22,7 @@ import NoteView from "./views/note";
|
||||
import { LoginStartView } from "./views/login/start";
|
||||
import { LoginNpubView } from "./views/login/npub";
|
||||
import NotificationsView from "./views/notifications";
|
||||
import { RelaysView } from "./views/relays";
|
||||
|
||||
const RequireSetup = ({ children }: { children: JSX.Element }) => {
|
||||
let location = useLocation();
|
||||
@ -85,6 +86,10 @@ const router = createBrowserRouter([
|
||||
path: "settings",
|
||||
element: <SettingsView />,
|
||||
},
|
||||
{
|
||||
path: "relays",
|
||||
element: <RelaysView />,
|
||||
},
|
||||
{
|
||||
path: "notifications",
|
||||
element: <NotificationsView />,
|
||||
|
@ -13,7 +13,7 @@ import {
|
||||
import { Relay } from "../services/relays";
|
||||
import relayPool from "../services/relays/relay-pool";
|
||||
import { useInterval } from "react-use";
|
||||
import { RelayStatus } from "../views/settings/relay-status";
|
||||
import { RelayStatus } from "./relay-status";
|
||||
import { useIsMobile } from "../hooks/use-is-mobile";
|
||||
import { RelayIcon } from "./icons";
|
||||
|
||||
|
@ -4,7 +4,7 @@ import { Bech32Prefix, normalizeToBech32 } from "../helpers/nip-19";
|
||||
import { getUserDisplayName } from "../helpers/user-metadata";
|
||||
import useSubject from "../hooks/use-subject";
|
||||
import { useUserMetadata } from "../hooks/use-user-metadata";
|
||||
import followingService from "../services/following";
|
||||
import clientFollowingService from "../services/client-following";
|
||||
import identity from "../services/identity";
|
||||
import { UserAvatar } from "./user-avatar";
|
||||
|
||||
@ -29,7 +29,7 @@ const FollowingListItem = ({ pubkey }: { pubkey: string }) => {
|
||||
|
||||
export const FollowingList = () => {
|
||||
const pubkey = useSubject(identity.pubkey);
|
||||
const following = useSubject(followingService.following);
|
||||
const following = useSubject(clientFollowingService.following);
|
||||
|
||||
if (!following) return <SkeletonText />;
|
||||
|
||||
|
@ -173,3 +173,9 @@ export const NotificationIcon = createIcon({
|
||||
d: "M5 18h14v-6.969C19 7.148 15.866 4 12 4s-7 3.148-7 7.031V18zm7-16c4.97 0 9 4.043 9 9.031V20H3v-8.969C3 6.043 7.03 2 12 2zM9.5 21h5a2.5 2.5 0 1 1-5 0z",
|
||||
defaultProps,
|
||||
});
|
||||
|
||||
export const UndoIcon = createIcon({
|
||||
displayName: "UndoIcon",
|
||||
d: "M5.828 7l2.536 2.536L6.95 10.95 2 6l4.95-4.95 1.414 1.414L5.828 5H13a8 8 0 1 1 0 16H4v-2h9a6 6 0 1 0 0-12H5.828z",
|
||||
defaultProps,
|
||||
});
|
||||
|
@ -17,22 +17,24 @@ import { NostrRequest } from "../../classes/nostr-request";
|
||||
import useSubject from "../../hooks/use-subject";
|
||||
import { getEventRelays, handleEventFromRelay } from "../../services/event-relays";
|
||||
import { relayPool } from "../../services/relays";
|
||||
import settings from "../../services/settings";
|
||||
import { NostrEvent } from "../../types/nostr-event";
|
||||
import { RelayIcon, SearchIcon } from "../icons";
|
||||
import { RelayFavicon } from "../relay-favicon";
|
||||
import { useReadRelayUrls, useWriteRelayUrls } from "../../hooks/use-client-relays";
|
||||
|
||||
export type NoteRelaysProps = Omit<IconButtonProps, "icon" | "aria-label"> & {
|
||||
event: NostrEvent;
|
||||
};
|
||||
|
||||
export const NoteRelays = memo(({ event, ...props }: NoteRelaysProps) => {
|
||||
const relays = useSubject(getEventRelays(event.id));
|
||||
const eventRelays = useSubject(getEventRelays(event.id));
|
||||
const readRelays = useReadRelayUrls();
|
||||
const writeRelays = useWriteRelayUrls();
|
||||
|
||||
const [querying, setQuerying] = useState(false);
|
||||
const queryRelays = useCallback(() => {
|
||||
setQuerying(true);
|
||||
const request = new NostrRequest(settings.relays.value);
|
||||
const request = new NostrRequest(readRelays);
|
||||
request.start({ ids: [event.id] });
|
||||
request.onEvent.subscribe({
|
||||
complete() {
|
||||
@ -43,7 +45,7 @@ export const NoteRelays = memo(({ event, ...props }: NoteRelaysProps) => {
|
||||
|
||||
const [broadcasting, setBroadcasting] = useState(false);
|
||||
const broadcast = useCallback(() => {
|
||||
const missingRelays = settings.relays.value.filter((url) => !relays.includes(url));
|
||||
const missingRelays = writeRelays.filter((url) => !eventRelays.includes(url));
|
||||
if (missingRelays.length === 0) {
|
||||
return;
|
||||
}
|
||||
@ -64,14 +66,14 @@ export const NoteRelays = memo(({ event, ...props }: NoteRelaysProps) => {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
<Popover isLazy>
|
||||
<PopoverTrigger>
|
||||
<IconButton title="Note Relays" icon={<RelayIcon />} size={props.size ?? "sm"} aria-label="Note Relays" />
|
||||
</PopoverTrigger>
|
||||
<PopoverContent>
|
||||
<PopoverArrow />
|
||||
<PopoverBody>
|
||||
{relays.map((url) => (
|
||||
{eventRelays.map((url) => (
|
||||
<Flex alignItems="center" key={url}>
|
||||
<RelayFavicon relay={url} size="2xs" mr="2" />
|
||||
<Text>{url}</Text>
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React from "react";
|
||||
import { Avatar, Button, Container, Flex, Heading, IconButton, LinkOverlay, Text, VStack } from "@chakra-ui/react";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { FeedIcon, LogoutIcon, NotificationIcon, ProfileIcon, SettingsIcon } from "./icons";
|
||||
import { FeedIcon, LogoutIcon, NotificationIcon, ProfileIcon, RelayIcon, SettingsIcon } from "./icons";
|
||||
import { ErrorBoundary } from "./error-boundary";
|
||||
import { ConnectedRelays } from "./connected-relays";
|
||||
|
||||
@ -90,6 +90,9 @@ const DesktopSideNav = () => {
|
||||
<Button onClick={() => navigate("/notifications")} leftIcon={<NotificationIcon />}>
|
||||
Notifications
|
||||
</Button>
|
||||
<Button onClick={() => navigate("/relays")} leftIcon={<RelayIcon />}>
|
||||
Relays
|
||||
</Button>
|
||||
<Button onClick={() => navigate("/settings")} leftIcon={<SettingsIcon />}>
|
||||
Settings
|
||||
</Button>
|
||||
|
@ -14,8 +14,8 @@ import React, { useState } from "react";
|
||||
import { useList } from "react-use";
|
||||
import { nostrPostAction, PostResult } from "../../classes/nostr-post-action";
|
||||
import { getReferences } from "../../helpers/nostr-event";
|
||||
import { useWriteRelayUrls } from "../../hooks/use-client-relays";
|
||||
import { useIsMobile } from "../../hooks/use-is-mobile";
|
||||
import settings from "../../services/settings";
|
||||
import { DraftNostrEvent, NostrEvent } from "../../types/nostr-event";
|
||||
import { NoteLink } from "../note-link";
|
||||
import { PostResults } from "./post-results";
|
||||
@ -39,6 +39,7 @@ export const PostModal = ({ isOpen, onClose, initialDraft }: PostModalProps) =>
|
||||
const isMobile = useIsMobile();
|
||||
const pad = isMobile ? "2" : "4";
|
||||
|
||||
const writeRelays = useWriteRelayUrls();
|
||||
const [waiting, setWaiting] = useState(false);
|
||||
const [signedEvent, setSignedEvent] = useState<NostrEvent | null>(null);
|
||||
const [results, resultsActions] = useList<PostResult>();
|
||||
@ -56,7 +57,7 @@ export const PostModal = ({ isOpen, onClose, initialDraft }: PostModalProps) =>
|
||||
setWaiting(false);
|
||||
setSignedEvent(event);
|
||||
|
||||
const postResults = nostrPostAction(settings.relays.value, event);
|
||||
const postResults = nostrPostAction(writeRelays, event);
|
||||
|
||||
postResults.subscribe({
|
||||
next(result) {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Badge, useForceUpdate } from "@chakra-ui/react";
|
||||
import { useInterval } from "react-use";
|
||||
import { Relay, relayPool } from "../../services/relays";
|
||||
import { Relay, relayPool } from "../services/relays";
|
||||
|
||||
const getStatusText = (relay: Relay) => {
|
||||
if (relay.connecting) return "Connecting...";
|
@ -1,5 +1,6 @@
|
||||
import { Input, InputProps } from "@chakra-ui/react";
|
||||
import { useAsync } from "react-use";
|
||||
import { unique } from "../helpers/array";
|
||||
|
||||
export type RelayUrlInputProps = Omit<InputProps, "type">;
|
||||
|
||||
@ -7,7 +8,7 @@ export const RelayUrlInput = ({ ...props }: RelayUrlInputProps) => {
|
||||
const { value: relaysJson, loading: loadingRelaysJson } = useAsync(async () =>
|
||||
fetch("/relays.json").then((res) => res.json() as Promise<{ relays: string[] }>)
|
||||
);
|
||||
const relaySuggestions = relaysJson?.relays ?? [];
|
||||
const relaySuggestions = relaysJson?.relays ? unique(relaysJson?.relays) : [];
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -1,26 +1,26 @@
|
||||
import { Button, ButtonProps } from "@chakra-ui/react";
|
||||
import { useReadonlyMode } from "../hooks/use-readonly-mode";
|
||||
import useSubject from "../hooks/use-subject";
|
||||
import followingService from "../services/following";
|
||||
import clientFollowingService from "../services/client-following";
|
||||
|
||||
export const UserFollowButton = ({
|
||||
pubkey,
|
||||
...props
|
||||
}: { pubkey: string } & Omit<ButtonProps, "onClick" | "isLoading" | "isDisabled">) => {
|
||||
const readonly = useReadonlyMode();
|
||||
const following = useSubject(followingService.following);
|
||||
const savingDraft = useSubject(followingService.savingDraft);
|
||||
const following = useSubject(clientFollowingService.following);
|
||||
const savingDraft = useSubject(clientFollowingService.savingDraft);
|
||||
|
||||
const isFollowing = following.some((t) => t[1] === pubkey);
|
||||
|
||||
const toggleFollow = async () => {
|
||||
if (isFollowing) {
|
||||
followingService.removeContact(pubkey);
|
||||
clientFollowingService.removeContact(pubkey);
|
||||
} else {
|
||||
followingService.addContact(pubkey);
|
||||
clientFollowingService.addContact(pubkey);
|
||||
}
|
||||
|
||||
await followingService.savePendingDraft();
|
||||
await clientFollowingService.savePending();
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -1,6 +1,7 @@
|
||||
import moment from "moment";
|
||||
import { getEventRelays } from "../services/event-relays";
|
||||
import { DraftNostrEvent, isETag, isPTag, NostrEvent } from "../types/nostr-event";
|
||||
import { DraftNostrEvent, isETag, isPTag, NostrEvent, RTag } from "../types/nostr-event";
|
||||
import { RelayConfig, RelayMode } from "../services/relays/relay";
|
||||
|
||||
export function isReply(event: NostrEvent | DraftNostrEvent) {
|
||||
return !!event.tags.find((tag) => isETag(tag) && tag[3] !== "mention");
|
||||
@ -83,3 +84,14 @@ export function buildReply(event: NostrEvent): DraftNostrEvent {
|
||||
created_at: moment().unix(),
|
||||
};
|
||||
}
|
||||
|
||||
export function parseRTag(tag: RTag): RelayConfig {
|
||||
switch (tag[2]) {
|
||||
case "write":
|
||||
return { url: tag[1], mode: RelayMode.WRITE };
|
||||
case "read":
|
||||
return { url: tag[1], mode: RelayMode.READ };
|
||||
default:
|
||||
return { url: tag[1], mode: RelayMode.ALL };
|
||||
}
|
||||
}
|
||||
|
29
src/hooks/use-client-relays.ts
Normal file
29
src/hooks/use-client-relays.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { unique } from "../helpers/array";
|
||||
import clientRelaysService from "../services/client-relays";
|
||||
import { RelayMode } from "../services/relays/relay";
|
||||
import useSubject from "./use-subject";
|
||||
|
||||
export function useClientRelays(mode: RelayMode = RelayMode.READ) {
|
||||
const relays = useSubject(clientRelaysService.relays);
|
||||
|
||||
return relays.filter((r) => r.mode & mode);
|
||||
}
|
||||
export function useReadRelayUrls(additional: string[] = []) {
|
||||
const relays = useClientRelays(RelayMode.READ);
|
||||
|
||||
const urls = relays.map((r) => r.url);
|
||||
if (additional) {
|
||||
return unique([...urls, ...additional]);
|
||||
}
|
||||
return urls;
|
||||
}
|
||||
|
||||
export function useWriteRelayUrls(additional: string[] = []) {
|
||||
const relays = useClientRelays(RelayMode.WRITE);
|
||||
|
||||
const urls = relays.map((r) => r.url);
|
||||
if (additional) {
|
||||
return unique([...urls, ...additional]);
|
||||
}
|
||||
return urls;
|
||||
}
|
@ -1,17 +1,17 @@
|
||||
import { useRef } from "react";
|
||||
import { useDeepCompareEffect, useUnmount } from "react-use";
|
||||
import { NostrSubscription } from "../classes/nostr-subscription";
|
||||
import settings from "../services/settings";
|
||||
import { NostrQuery } from "../types/nostr-query";
|
||||
import useSubject from "./use-subject";
|
||||
import { useReadRelayUrls } from "./use-client-relays";
|
||||
|
||||
type Options = {
|
||||
name?: string;
|
||||
enabled?: boolean;
|
||||
};
|
||||
|
||||
/** @deprecated */
|
||||
export function useSubscription(query: NostrQuery, opts?: Options) {
|
||||
const relays = useSubject(settings.relays);
|
||||
const relays = useReadRelayUrls();
|
||||
const sub = useRef<NostrSubscription | null>(null);
|
||||
sub.current = sub.current || new NostrSubscription(relays, undefined, opts?.name);
|
||||
|
||||
|
@ -2,7 +2,7 @@ import { useEffect, useMemo, useRef } from "react";
|
||||
import { useUnmount } from "react-use";
|
||||
import { ThreadLoader } from "../classes/thread-loader";
|
||||
import { linkEvents } from "../helpers/thread";
|
||||
import settings from "../services/settings";
|
||||
import { useReadRelayUrls } from "./use-client-relays";
|
||||
import useSubject from "./use-subject";
|
||||
|
||||
type Options = {
|
||||
@ -10,7 +10,7 @@ type Options = {
|
||||
};
|
||||
|
||||
export function useThreadLoader(eventId: string, opts?: Options) {
|
||||
const relays = useSubject(settings.relays);
|
||||
const relays = useReadRelayUrls();
|
||||
|
||||
const ref = useRef<ThreadLoader | null>(null);
|
||||
const loader = (ref.current = ref.current || new ThreadLoader(relays, eventId));
|
||||
|
@ -2,9 +2,9 @@ import { useMemo } from "react";
|
||||
import userContactsService from "../services/user-contacts";
|
||||
import useSubject from "./use-subject";
|
||||
|
||||
export function useUserContacts(pubkey: string, relays: string[] = [], alwaysRequest = false) {
|
||||
export function useUserContacts(pubkey: string, additionalRelays: string[] = [], alwaysRequest = false) {
|
||||
const observable = useMemo(
|
||||
() => userContactsService.requestContacts(pubkey, relays, alwaysRequest),
|
||||
() => userContactsService.requestContacts(pubkey, additionalRelays, alwaysRequest),
|
||||
[pubkey, alwaysRequest]
|
||||
);
|
||||
const contacts = useSubject(observable) ?? undefined;
|
||||
|
13
src/hooks/use-user-relays.ts
Normal file
13
src/hooks/use-user-relays.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { useMemo } from "react";
|
||||
import userRelaysService from "../services/user-relays";
|
||||
import useSubject from "./use-subject";
|
||||
|
||||
export function useUserRelays(pubkey: string, additionalRelays: string[] = [], alwaysRequest = false) {
|
||||
const observable = useMemo(
|
||||
() => userRelaysService.requestRelays(pubkey, additionalRelays, alwaysRequest),
|
||||
[pubkey, alwaysRequest]
|
||||
);
|
||||
const contacts = useSubject(observable) ?? undefined;
|
||||
|
||||
return contacts;
|
||||
}
|
@ -2,14 +2,13 @@ import moment from "moment";
|
||||
import { BehaviorSubject, lastValueFrom } from "rxjs";
|
||||
import { nostrPostAction } from "../classes/nostr-post-action";
|
||||
import { DraftNostrEvent, PTag } from "../types/nostr-event";
|
||||
import clientRelaysService from "./client-relays";
|
||||
import identity from "./identity";
|
||||
import settings from "./settings";
|
||||
import userContactsService, { UserContacts } from "./user-contacts";
|
||||
import userContactsService from "./user-contacts";
|
||||
|
||||
export type RelayDirectory = Record<string, { read: boolean; write: boolean }>;
|
||||
|
||||
const following = new BehaviorSubject<PTag[]>([]);
|
||||
// const relays = new BehaviorSubject<RelayDirectory>({});
|
||||
const pendingDraft = new BehaviorSubject<DraftNostrEvent | null>(null);
|
||||
const savingDraft = new BehaviorSubject(false);
|
||||
|
||||
@ -18,7 +17,7 @@ identity.pubkey.subscribe((pubkey) => {
|
||||
// clear the following list until a new one can be fetched
|
||||
following.next([]);
|
||||
|
||||
sub = userContactsService.requestContacts(pubkey, settings.relays.value, true).subscribe((userContacts) => {
|
||||
sub = userContactsService.requestContacts(pubkey, [], true).subscribe((userContacts) => {
|
||||
if (!userContacts) return;
|
||||
|
||||
following.next(
|
||||
@ -50,7 +49,7 @@ function getDraftEvent(): DraftNostrEvent {
|
||||
};
|
||||
}
|
||||
|
||||
async function savePendingDraft() {
|
||||
async function savePending() {
|
||||
const draft = pendingDraft.value;
|
||||
if (!draft) return;
|
||||
|
||||
@ -58,7 +57,7 @@ async function savePendingDraft() {
|
||||
savingDraft.next(true);
|
||||
const event = await window.nostr.signEvent(draft);
|
||||
|
||||
const results = nostrPostAction(settings.relays.value, event);
|
||||
const results = nostrPostAction(clientRelaysService.getWriteUrls(), event);
|
||||
await lastValueFrom(results);
|
||||
|
||||
savingDraft.next(false);
|
||||
@ -92,18 +91,18 @@ function removeContact(pubkey: string) {
|
||||
}
|
||||
}
|
||||
|
||||
const followingService = {
|
||||
const clientFollowingService = {
|
||||
following: following,
|
||||
isFollowing,
|
||||
savingDraft,
|
||||
savePendingDraft,
|
||||
savePending,
|
||||
addContact,
|
||||
removeContact,
|
||||
};
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
// @ts-ignore
|
||||
window.followingService = followingService;
|
||||
window.clientFollowingService = clientFollowingService;
|
||||
}
|
||||
|
||||
export default followingService;
|
||||
export default clientFollowingService;
|
96
src/services/client-relays.ts
Normal file
96
src/services/client-relays.ts
Normal file
@ -0,0 +1,96 @@
|
||||
import moment from "moment";
|
||||
import { BehaviorSubject, lastValueFrom, Subscription } from "rxjs";
|
||||
import { nostrPostAction } from "../classes/nostr-post-action";
|
||||
import { unique } from "../helpers/array";
|
||||
import { DraftNostrEvent, RTag } from "../types/nostr-event";
|
||||
import identity from "./identity";
|
||||
import { RelayConfig, RelayMode } from "./relays/relay";
|
||||
import userRelaysService from "./user-relays";
|
||||
|
||||
export type RelayDirectory = Record<string, { read: boolean; write: boolean }>;
|
||||
|
||||
class ClientRelayService {
|
||||
bootstrapRelays = new Set<string>();
|
||||
relays = new BehaviorSubject<RelayConfig[]>([]);
|
||||
writeRelays = new BehaviorSubject<RelayConfig[]>([]);
|
||||
readRelays = new BehaviorSubject<RelayConfig[]>([]);
|
||||
|
||||
constructor() {
|
||||
let sub: Subscription;
|
||||
identity.pubkey.subscribe((pubkey) => {
|
||||
// clear the relay list until a new one can be fetched
|
||||
this.relays.next([]);
|
||||
|
||||
if (sub) sub.unsubscribe();
|
||||
|
||||
sub = userRelaysService.requestRelays(pubkey, Array.from(this.bootstrapRelays), true).subscribe((userRelays) => {
|
||||
if (!userRelays) return;
|
||||
|
||||
this.relays.next(userRelays.relays);
|
||||
});
|
||||
});
|
||||
|
||||
// add preset relays fromm nip07 extension to bootstrap list
|
||||
identity.relays.subscribe((presetRelays) => {
|
||||
for (const [url, opts] of Object.entries(presetRelays)) {
|
||||
if (opts.read) {
|
||||
clientRelaysService.bootstrapRelays.add(url);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.relays.subscribe((relays) => this.writeRelays.next(relays.filter((r) => r.mode & RelayMode.WRITE)));
|
||||
this.relays.subscribe((relays) => this.readRelays.next(relays.filter((r) => r.mode & RelayMode.READ)));
|
||||
}
|
||||
|
||||
async postUpdatedRelays(newRelays: RelayConfig[]) {
|
||||
const rTags: RTag[] = newRelays.map((r) => {
|
||||
switch (r.mode) {
|
||||
case RelayMode.READ:
|
||||
return ["r", r.url, "read"];
|
||||
case RelayMode.WRITE:
|
||||
return ["r", r.url, "write"];
|
||||
case RelayMode.ALL:
|
||||
default:
|
||||
return ["r", r.url];
|
||||
}
|
||||
});
|
||||
|
||||
const draft: DraftNostrEvent = {
|
||||
kind: 10002,
|
||||
tags: rTags,
|
||||
content: "",
|
||||
created_at: moment().unix(),
|
||||
};
|
||||
|
||||
const newRelayUrls = newRelays.filter((r) => r.mode & RelayMode.WRITE).map((r) => r.url);
|
||||
const oldRelayUrls = this.relays.value.filter((r) => r.mode & RelayMode.WRITE).map((r) => r.url);
|
||||
const writeUrls = unique([...oldRelayUrls, ...newRelayUrls]);
|
||||
|
||||
if (window.nostr) {
|
||||
const event = await window.nostr.signEvent(draft);
|
||||
|
||||
const results = nostrPostAction(writeUrls, event);
|
||||
await lastValueFrom(results);
|
||||
|
||||
// pass new event to the user relay service
|
||||
userRelaysService.receiveEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
getWriteUrls() {
|
||||
return this.relays.value.filter((r) => r.mode & RelayMode.WRITE).map((r) => r.url);
|
||||
}
|
||||
getReadUrls() {
|
||||
return this.relays.value.filter((r) => r.mode & RelayMode.READ).map((r) => r.url);
|
||||
}
|
||||
}
|
||||
|
||||
const clientRelaysService = new ClientRelayService();
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
// @ts-ignore
|
||||
window.clientRelaysService = clientRelaysService;
|
||||
}
|
||||
|
||||
export default clientRelaysService;
|
@ -12,9 +12,15 @@ type MigrationFunction = (
|
||||
const MIGRATIONS: MigrationFunction[] = [
|
||||
// 0 -> 1
|
||||
function (db, transaction, event) {
|
||||
const metadata = db.createObjectStore("userMetadata", {
|
||||
const userMetadata = db.createObjectStore("userMetadata", {
|
||||
keyPath: "pubkey",
|
||||
});
|
||||
userMetadata.createIndex("created_at", "created_at");
|
||||
|
||||
const userRelays = db.createObjectStore("userRelays", {
|
||||
keyPath: "pubkey",
|
||||
});
|
||||
userRelays.createIndex("created_at", "created_at");
|
||||
|
||||
const contacts = db.createObjectStore("userContacts", {
|
||||
keyPath: "pubkey",
|
||||
|
@ -1,10 +1,12 @@
|
||||
import { DBSchema } from "idb";
|
||||
import { NostrEvent } from "../../types/nostr-event";
|
||||
import { RelayConfig } from "../relays/relay";
|
||||
|
||||
export interface CustomSchema extends DBSchema {
|
||||
userMetadata: {
|
||||
key: string;
|
||||
value: NostrEvent;
|
||||
indexes: { created_at: number };
|
||||
};
|
||||
userContacts: {
|
||||
key: string;
|
||||
@ -17,6 +19,11 @@ export interface CustomSchema extends DBSchema {
|
||||
};
|
||||
indexes: { created_at: number; contacts: string };
|
||||
};
|
||||
userRelays: {
|
||||
key: string;
|
||||
value: { pubkey: string; relays: {url: string, mode: number}[]; created_at: number };
|
||||
indexes: { created_at: number };
|
||||
};
|
||||
dnsIdentifiers: {
|
||||
key: string;
|
||||
value: { name: string; domain: string; pubkey: string; relays: string[]; updated: number };
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
import { unique } from "../helpers/array";
|
||||
import settings from "./settings";
|
||||
|
||||
export type PresetRelays = Record<string, { read: boolean; write: boolean }>;
|
||||
@ -15,7 +14,7 @@ class IdentityService {
|
||||
setup = new BehaviorSubject(false);
|
||||
pubkey = new BehaviorSubject("");
|
||||
readonly = new BehaviorSubject(false);
|
||||
// TODO: remove this when there is a service to manage user relays
|
||||
// directory of relays provided by nip07 extension
|
||||
relays = new BehaviorSubject<PresetRelays>({});
|
||||
private useExtension: boolean = false;
|
||||
private secKey: string | undefined = undefined;
|
||||
@ -48,21 +47,22 @@ class IdentityService {
|
||||
useExtension: true,
|
||||
});
|
||||
|
||||
// disabled because I dont want to load the preset relays yet (ably dose not support changing them)
|
||||
// const relays = await window.nostr.getRelays();
|
||||
// if (Array.isArray(relays)) {
|
||||
// this.relays.next(relays.reduce<PresetRelays>((d, r) => ({ ...d, [r]: { read: true, write: true } }), {}));
|
||||
// } else this.relays.next(relays);
|
||||
const relays = await window.nostr.getRelays();
|
||||
if (Array.isArray(relays)) {
|
||||
this.relays.next(relays.reduce<PresetRelays>((d, r) => ({ ...d, [r]: { read: true, write: true } }), {}));
|
||||
} else {
|
||||
this.relays.next(relays);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
loginWithSecKey(secKey: string) {
|
||||
// const pubkey =
|
||||
// settings.identity.next({
|
||||
// pubkey,
|
||||
// useExtension: true,
|
||||
// });
|
||||
}
|
||||
// loginWithSecKey(secKey: string) {
|
||||
// const pubkey =
|
||||
// settings.identity.next({
|
||||
// pubkey,
|
||||
// useExtension: true,
|
||||
// });
|
||||
// }
|
||||
|
||||
loginWithPubkey(pubkey: string) {
|
||||
this.readonly.next(true);
|
||||
@ -83,9 +83,4 @@ if (import.meta.env.DEV) {
|
||||
window.identity = identity;
|
||||
}
|
||||
|
||||
// TODO: create a service to manage user relays (download latest and handle conflicts)
|
||||
identity.relays.subscribe((presetRelays) => {
|
||||
settings.relays.next(unique([...settings.relays.value, ...Object.keys(presetRelays)]));
|
||||
});
|
||||
|
||||
export default identity;
|
||||
|
@ -29,9 +29,6 @@ export class RelayPool {
|
||||
}
|
||||
return relay;
|
||||
}
|
||||
requestRelays(urls: string[], connect = true) {
|
||||
return urls.map((url) => this.requestRelay(url, connect));
|
||||
}
|
||||
|
||||
pruneRelays() {
|
||||
for (const [url, relay] of this.relays.entries()) {
|
||||
|
@ -23,12 +23,13 @@ export type IncomingCommandResult = {
|
||||
message?: string;
|
||||
};
|
||||
|
||||
export enum Permission {
|
||||
export enum RelayMode {
|
||||
NONE = 0,
|
||||
READ = 1,
|
||||
WRITE = 2,
|
||||
ALL = 1 | 2,
|
||||
}
|
||||
export type RelayConfig = { url: string; mode: RelayMode };
|
||||
|
||||
export class Relay {
|
||||
url: string;
|
||||
@ -39,13 +40,13 @@ export class Relay {
|
||||
onEndOfStoredEvents = new Subject<IncomingEOSE>();
|
||||
onCommandResult = new Subject<IncomingCommandResult>();
|
||||
ws?: WebSocket;
|
||||
permission: Permission = Permission.ALL;
|
||||
mode: RelayMode = RelayMode.ALL;
|
||||
|
||||
private queue: NostrOutgoingMessage[] = [];
|
||||
|
||||
constructor(url: string, permission: Permission = Permission.ALL) {
|
||||
constructor(url: string, mode: RelayMode = RelayMode.ALL) {
|
||||
this.url = url;
|
||||
this.permission = permission;
|
||||
this.mode = mode;
|
||||
}
|
||||
|
||||
open() {
|
||||
@ -71,7 +72,7 @@ export class Relay {
|
||||
this.ws.onmessage = this.handleMessage.bind(this);
|
||||
}
|
||||
send(json: NostrOutgoingMessage) {
|
||||
if (this.permission & Permission.WRITE) {
|
||||
if (this.mode & RelayMode.WRITE) {
|
||||
if (this.connected) {
|
||||
this.ws?.send(JSON.stringify(json));
|
||||
} else this.queue.push(json);
|
||||
@ -116,7 +117,7 @@ export class Relay {
|
||||
// skip empty events
|
||||
if (!event.data) return;
|
||||
|
||||
if (!(this.permission & Permission.READ)) return;
|
||||
if (!(this.mode & RelayMode.READ)) return;
|
||||
|
||||
try {
|
||||
const data: RawIncomingNostrEvent = JSON.parse(event.data);
|
||||
|
@ -3,7 +3,6 @@ import db from "./db";
|
||||
import { SavedIdentity } from "./identity";
|
||||
|
||||
const settings = {
|
||||
relays: new BehaviorSubject<string[]>([]),
|
||||
identity: new BehaviorSubject<SavedIdentity | null>(null),
|
||||
blurImages: new BehaviorSubject(true),
|
||||
autoShowMedia: new BehaviorSubject(true),
|
||||
|
@ -7,6 +7,7 @@ import db from "./db";
|
||||
import settings from "./settings";
|
||||
import userFollowersService from "./user-followers";
|
||||
import pubkeyRelayWeightsService from "./pubkey-relay-weights";
|
||||
import clientRelaysService from "./client-relays";
|
||||
|
||||
const subscription = new NostrSubscription([], undefined, "user-contacts");
|
||||
const subjects = new PubkeySubjectCache<UserContacts>();
|
||||
@ -39,10 +40,10 @@ function parseContacts(event: NostrEvent): UserContacts {
|
||||
};
|
||||
}
|
||||
|
||||
function requestContacts(pubkey: string, relays: string[] = [], alwaysRequest = false) {
|
||||
function requestContacts(pubkey: string, additionalRelays: string[] = [], alwaysRequest = false) {
|
||||
let subject = subjects.getSubject(pubkey);
|
||||
|
||||
if (relays.length) subjects.addRelays(pubkey, relays);
|
||||
if (additionalRelays.length) subjects.addRelays(pubkey, additionalRelays);
|
||||
|
||||
if (alwaysRequest) forceRequestedKeys.add(pubkey);
|
||||
|
||||
@ -59,20 +60,20 @@ function flushRequests() {
|
||||
if (!subjects.dirty) return;
|
||||
|
||||
const pubkeys = new Set<string>();
|
||||
const relays = new Set<string>();
|
||||
const relayUrls = new Set<string>();
|
||||
|
||||
const pending = subjects.getAllPubkeysMissingData(Array.from(forceRequestedKeys));
|
||||
for (const key of pending.pubkeys) pubkeys.add(key);
|
||||
for (const url of pending.relays) relays.add(url);
|
||||
for (const url of pending.relays) relayUrls.add(url);
|
||||
|
||||
if (pubkeys.size === 0) return;
|
||||
|
||||
const systemRelays = settings.relays.getValue();
|
||||
for (const url of systemRelays) relays.add(url);
|
||||
const clientRelays = clientRelaysService.getReadUrls();
|
||||
for (const url of clientRelays) relayUrls.add(url);
|
||||
|
||||
const query: NostrQuery = { authors: Array.from(pubkeys), kinds: [3] };
|
||||
|
||||
subscription.setRelays(Array.from(relays));
|
||||
subscription.setRelays(Array.from(relayUrls));
|
||||
subscription.setQuery(query);
|
||||
if (subscription.state !== NostrSubscription.OPEN) {
|
||||
subscription.open();
|
||||
|
@ -3,10 +3,10 @@ import { NostrQuery } from "../types/nostr-query";
|
||||
import { PubkeySubjectCache } from "../classes/pubkey-subject-cache";
|
||||
import { NostrSubscription } from "../classes/nostr-subscription";
|
||||
import db from "./db";
|
||||
import settings from "./settings";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
import { getReferences } from "../helpers/nostr-event";
|
||||
import userContactsService from "./user-contacts";
|
||||
import clientRelaysService from "./client-relays";
|
||||
|
||||
const subscription = new NostrSubscription([], undefined, "user-followers");
|
||||
const subjects = new PubkeySubjectCache<string[]>();
|
||||
@ -23,10 +23,10 @@ function mergeNext(subject: BehaviorSubject<string[] | null>, next: string[]) {
|
||||
subject.next(arr);
|
||||
}
|
||||
|
||||
function requestFollowers(pubkey: string, relays: string[] = [], alwaysRequest = false) {
|
||||
function requestFollowers(pubkey: string, additionalRelays: string[] = [], alwaysRequest = false) {
|
||||
let subject = subjects.getSubject(pubkey);
|
||||
|
||||
if (relays.length) subjects.addRelays(pubkey, relays);
|
||||
if (additionalRelays.length) subjects.addRelays(pubkey, additionalRelays);
|
||||
|
||||
db.getAllKeysFromIndex("userContacts", "contacts", pubkey).then((cached) => {
|
||||
mergeNext(subject, cached);
|
||||
@ -41,20 +41,20 @@ function flushRequests() {
|
||||
if (!subjects.dirty) return;
|
||||
|
||||
const pubkeys = new Set<string>();
|
||||
const relays = new Set<string>();
|
||||
const relayUrls = new Set<string>();
|
||||
|
||||
const pending = subjects.getAllPubkeysMissingData(Array.from(forceRequestedKeys));
|
||||
for (const key of pending.pubkeys) pubkeys.add(key);
|
||||
for (const url of pending.relays) relays.add(url);
|
||||
for (const url of pending.relays) relayUrls.add(url);
|
||||
|
||||
if (pubkeys.size === 0) return;
|
||||
|
||||
const systemRelays = settings.relays.getValue();
|
||||
for (const url of systemRelays) relays.add(url);
|
||||
const clientRelays = clientRelaysService.getReadUrls();
|
||||
for (const url of clientRelays) relayUrls.add(url);
|
||||
|
||||
const query: NostrQuery = { kinds: [3], "#p": Array.from(pubkeys) };
|
||||
|
||||
subscription.setRelays(Array.from(relays));
|
||||
subscription.setRelays(Array.from(relayUrls));
|
||||
subscription.setQuery(query);
|
||||
if (subscription.state !== NostrSubscription.OPEN) {
|
||||
subscription.open();
|
||||
|
@ -1,9 +1,9 @@
|
||||
import db from "./db";
|
||||
import settings from "./settings";
|
||||
import { NostrSubscription } from "../classes/nostr-subscription";
|
||||
import { PubkeySubjectCache } from "../classes/pubkey-subject-cache";
|
||||
import { Kind0ParsedContent, NostrEvent } from "../types/nostr-event";
|
||||
import { NostrQuery } from "../types/nostr-query";
|
||||
import clientRelaysService from "./client-relays";
|
||||
|
||||
type Metadata = Kind0ParsedContent & { created_at: number };
|
||||
|
||||
@ -11,10 +11,10 @@ const subscription = new NostrSubscription([], undefined, "user-metadata");
|
||||
const subjects = new PubkeySubjectCache<Metadata>();
|
||||
const forceRequestedKeys = new Set<string>();
|
||||
|
||||
function requestMetadata(pubkey: string, relays: string[], alwaysRequest = false) {
|
||||
function requestMetadata(pubkey: string, additionalRelays: string[], alwaysRequest = false) {
|
||||
let subject = subjects.getSubject(pubkey);
|
||||
|
||||
if (relays.length) subjects.addRelays(pubkey, relays);
|
||||
if (additionalRelays.length) subjects.addRelays(pubkey, additionalRelays);
|
||||
|
||||
if (alwaysRequest) {
|
||||
forceRequestedKeys.add(pubkey);
|
||||
@ -42,20 +42,20 @@ function flushRequests() {
|
||||
if (!subjects.dirty) return;
|
||||
|
||||
const pubkeys = new Set<string>();
|
||||
const relays = new Set<string>();
|
||||
const relayUrls = new Set<string>();
|
||||
|
||||
const pending = subjects.getAllPubkeysMissingData(Array.from(forceRequestedKeys));
|
||||
for (const key of pending.pubkeys) pubkeys.add(key);
|
||||
for (const url of pending.relays) relays.add(url);
|
||||
for (const url of pending.relays) relayUrls.add(url);
|
||||
|
||||
if (pubkeys.size === 0) return;
|
||||
|
||||
const systemRelays = settings.relays.getValue();
|
||||
for (const url of systemRelays) relays.add(url);
|
||||
const clientRelays = clientRelaysService.getReadUrls();
|
||||
for (const url of clientRelays) relayUrls.add(url);
|
||||
|
||||
const query: NostrQuery = { authors: Array.from(pubkeys), kinds: [0] };
|
||||
|
||||
subscription.setRelays(Array.from(relays));
|
||||
subscription.setRelays(Array.from(relayUrls));
|
||||
subscription.setQuery(query);
|
||||
if (subscription.state !== NostrSubscription.OPEN) {
|
||||
subscription.open();
|
||||
|
94
src/services/user-relays.ts
Normal file
94
src/services/user-relays.ts
Normal file
@ -0,0 +1,94 @@
|
||||
import db from "./db";
|
||||
import { NostrSubscription } from "../classes/nostr-subscription";
|
||||
import { PubkeySubjectCache } from "../classes/pubkey-subject-cache";
|
||||
import { isRTag, NostrEvent } from "../types/nostr-event";
|
||||
import { NostrQuery } from "../types/nostr-query";
|
||||
import { RelayConfig } from "./relays/relay";
|
||||
import { parseRTag } from "../helpers/nostr-event";
|
||||
import clientRelaysService from "./client-relays";
|
||||
|
||||
export type UserRelays = {
|
||||
pubkey: string;
|
||||
relays: RelayConfig[];
|
||||
created_at: number;
|
||||
};
|
||||
|
||||
const subscription = new NostrSubscription([], undefined, "user-relays");
|
||||
const subjects = new PubkeySubjectCache<UserRelays>();
|
||||
const forceRequestedKeys = new Set<string>();
|
||||
|
||||
function requestRelays(pubkey: string, relays: string[], alwaysRequest = false) {
|
||||
let subject = subjects.getSubject(pubkey);
|
||||
|
||||
if (relays.length) subjects.addRelays(pubkey, relays);
|
||||
|
||||
if (alwaysRequest) forceRequestedKeys.add(pubkey);
|
||||
|
||||
if (!subject.value) {
|
||||
db.get("userRelays", pubkey).then((cached) => {
|
||||
if (cached) {
|
||||
subject.next(cached);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return subject;
|
||||
}
|
||||
|
||||
function flushRequests() {
|
||||
if (!subjects.dirty) return;
|
||||
|
||||
const pubkeys = new Set<string>();
|
||||
const relayUrls = new Set<string>();
|
||||
|
||||
const pending = subjects.getAllPubkeysMissingData(Array.from(forceRequestedKeys));
|
||||
for (const key of pending.pubkeys) pubkeys.add(key);
|
||||
for (const url of pending.relays) relayUrls.add(url);
|
||||
|
||||
if (pubkeys.size === 0) return;
|
||||
|
||||
const clientRelays = clientRelaysService.readRelays.value;
|
||||
for (const relay of clientRelays) relayUrls.add(relay.url);
|
||||
|
||||
const query: NostrQuery = { authors: Array.from(pubkeys), kinds: [10002] };
|
||||
|
||||
subscription.setRelays(Array.from(relayUrls));
|
||||
subscription.setQuery(query);
|
||||
if (subscription.state !== NostrSubscription.OPEN) {
|
||||
subscription.open();
|
||||
}
|
||||
subjects.dirty = false;
|
||||
}
|
||||
|
||||
function receiveEvent(event: NostrEvent) {
|
||||
const subject = subjects.getSubject(event.pubkey);
|
||||
const latest = subject.getValue();
|
||||
if (!latest || event.created_at > latest.created_at) {
|
||||
const userRelays = {
|
||||
pubkey: event.pubkey,
|
||||
relays: event.tags.filter(isRTag).map(parseRTag),
|
||||
created_at: event.created_at,
|
||||
};
|
||||
|
||||
subject.next(userRelays);
|
||||
db.put("userRelays", userRelays);
|
||||
forceRequestedKeys.delete(event.pubkey);
|
||||
}
|
||||
}
|
||||
|
||||
subscription.onEvent.subscribe(receiveEvent);
|
||||
|
||||
// flush requests every second
|
||||
setInterval(() => {
|
||||
subjects.prune();
|
||||
flushRequests();
|
||||
}, 1000 * 2);
|
||||
|
||||
const userRelaysService = { requestRelays, flushRequests, subjects, receiveEvent };
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
// @ts-ignore
|
||||
window.userRelaysService = userRelaysService;
|
||||
}
|
||||
|
||||
export default userRelaysService;
|
@ -1,12 +1,14 @@
|
||||
export type ETag = ["e", string] | ["e", string, string] | ["e", string, string, string];
|
||||
export type PTag = ["p", string] | ["p", string, string];
|
||||
export type RTag = ["r", string] | ["r", string, string];
|
||||
export type Tag =
|
||||
| [string]
|
||||
| [string, string]
|
||||
| [string, string, string]
|
||||
| [string, string, string, string]
|
||||
| ETag
|
||||
| PTag;
|
||||
| PTag
|
||||
| RTag;
|
||||
|
||||
export type NostrEvent = {
|
||||
id: string;
|
||||
@ -44,3 +46,6 @@ export function isETag(tag: Tag): tag is ETag {
|
||||
export function isPTag(tag: Tag): tag is PTag {
|
||||
return tag[0] === "p" && tag[1] !== undefined;
|
||||
}
|
||||
export function isRTag(tag: Tag): tag is RTag {
|
||||
return tag[0] === "r" && tag[1] !== undefined;
|
||||
}
|
||||
|
@ -1,145 +0,0 @@
|
||||
import {
|
||||
Accordion,
|
||||
AccordionButton,
|
||||
AccordionIcon,
|
||||
AccordionItem,
|
||||
AccordionPanel,
|
||||
Box,
|
||||
Button,
|
||||
Flex,
|
||||
FormLabel,
|
||||
Input,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalCloseButton,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
ModalHeader,
|
||||
ModalOverlay,
|
||||
ModalProps,
|
||||
Switch,
|
||||
} from "@chakra-ui/react";
|
||||
import { useState } from "react";
|
||||
import { useList } from "react-use";
|
||||
import { RelayUrlInput } from "../../../components/relay-url-input";
|
||||
import { unique } from "../../../helpers/array";
|
||||
import useSubject from "../../../hooks/use-subject";
|
||||
import settings from "../../../services/settings";
|
||||
|
||||
const CustomRelayForm = ({ onSubmit }: { onSubmit: (url: string) => void }) => {
|
||||
const [customRelay, setCustomRelay] = useState("");
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
onSubmit(customRelay);
|
||||
setCustomRelay("");
|
||||
}}
|
||||
>
|
||||
<Flex gap="2">
|
||||
<RelayUrlInput
|
||||
size="sm"
|
||||
placeholder="wss://relay.example.com"
|
||||
value={customRelay}
|
||||
onChange={(e) => setCustomRelay(e.target.value)}
|
||||
/>
|
||||
<Button size="sm" type="submit">
|
||||
Add
|
||||
</Button>
|
||||
</Flex>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export type FilterValues = {
|
||||
relays: string[];
|
||||
};
|
||||
|
||||
export type FeedFiltersProps = {
|
||||
isOpen: boolean;
|
||||
onClose: ModalProps["onClose"];
|
||||
values: FilterValues;
|
||||
onSave: (values: FilterValues) => void;
|
||||
};
|
||||
|
||||
export const FeedFilters = ({ isOpen, onClose, values }: FeedFiltersProps) => {
|
||||
const defaultRelays = useSubject(settings.relays);
|
||||
|
||||
const [selectedRelays, relayActions] = useList(values.relays);
|
||||
const availableRelays = unique([...defaultRelays, ...selectedRelays]);
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose} size="4xl">
|
||||
<ModalOverlay />
|
||||
<ModalContent>
|
||||
<ModalHeader>Filters</ModalHeader>
|
||||
<ModalCloseButton />
|
||||
<ModalBody>
|
||||
<Accordion allowToggle>
|
||||
<AccordionItem>
|
||||
<h2>
|
||||
<AccordionButton>
|
||||
<Box as="span" flex="1" textAlign="left">
|
||||
Relays
|
||||
</Box>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
</h2>
|
||||
<AccordionPanel pb={4}>
|
||||
{availableRelays.map((url) => (
|
||||
<Box key={url}>
|
||||
<FormLabel>
|
||||
<Switch
|
||||
size="sm"
|
||||
mr="2"
|
||||
isChecked={selectedRelays.includes(url)}
|
||||
onChange={() =>
|
||||
selectedRelays.includes(url)
|
||||
? relayActions.removeAt(selectedRelays.indexOf(url))
|
||||
: relayActions.push(url)
|
||||
}
|
||||
/>
|
||||
{url}
|
||||
</FormLabel>
|
||||
</Box>
|
||||
))}
|
||||
<Flex gap="2">
|
||||
<Button size="sm" onClick={() => relayActions.set(defaultRelays)}>
|
||||
Select All
|
||||
</Button>
|
||||
<CustomRelayForm onSubmit={(url) => relayActions.push(url)} />
|
||||
</Flex>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem>
|
||||
<h2>
|
||||
<AccordionButton>
|
||||
<Box as="span" flex="1" textAlign="left">
|
||||
Add Custom
|
||||
</Box>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
</h2>
|
||||
<AccordionPanel pb={4}>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et
|
||||
dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip
|
||||
ex ea commodo consequat.
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button mr={3} onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button colorScheme="brand" variant="solid">
|
||||
Save
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
@ -9,8 +9,8 @@ import identity from "../../services/identity";
|
||||
import userContactsService from "../../services/user-contacts";
|
||||
import { useTimelineLoader } from "../../hooks/use-timeline-loader";
|
||||
import { isNote } from "../../helpers/nostr-event";
|
||||
import settings from "../../services/settings";
|
||||
import { useAppTitle } from "../../hooks/use-app-title";
|
||||
import { useReadRelayUrls } from "../../hooks/use-client-relays";
|
||||
|
||||
function useExtendedContacts(pubkey: string) {
|
||||
useAppTitle("discover");
|
||||
@ -42,7 +42,7 @@ function useExtendedContacts(pubkey: string) {
|
||||
|
||||
export const DiscoverTab = () => {
|
||||
const pubkey = useSubject(identity.pubkey);
|
||||
const relays = useSubject(settings.relays);
|
||||
const relays = useReadRelayUrls();
|
||||
|
||||
const contactsOfContacts = useExtendedContacts(pubkey);
|
||||
const { events, loading, loadMore } = useTimelineLoader(
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Button, Flex, FormControl, FormLabel, Spinner, Switch, useDisclosure } from "@chakra-ui/react";
|
||||
import { Button, Flex, FormControl, FormLabel, Spinner, Switch } from "@chakra-ui/react";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import moment from "moment";
|
||||
import { Note } from "../../components/note";
|
||||
@ -7,16 +7,16 @@ import useSubject from "../../hooks/use-subject";
|
||||
import { useTimelineLoader } from "../../hooks/use-timeline-loader";
|
||||
import { useUserContacts } from "../../hooks/use-user-contacts";
|
||||
import identity from "../../services/identity";
|
||||
import settings from "../../services/settings";
|
||||
import { AddIcon } from "@chakra-ui/icons";
|
||||
import { useContext } from "react";
|
||||
import { PostModalContext } from "../../providers/post-modal-provider";
|
||||
import { useReadonlyMode } from "../../hooks/use-readonly-mode";
|
||||
import { useReadRelayUrls } from "../../hooks/use-client-relays";
|
||||
|
||||
export const FollowingTab = () => {
|
||||
const readonly = useReadonlyMode();
|
||||
const pubkey = useSubject(identity.pubkey);
|
||||
const relays = useSubject(settings.relays);
|
||||
const relays = useReadRelayUrls();
|
||||
const { openModal } = useContext(PostModalContext);
|
||||
const contacts = useUserContacts(pubkey);
|
||||
const [search, setSearch] = useSearchParams();
|
||||
|
@ -5,13 +5,12 @@ import { Note } from "../../components/note";
|
||||
import { unique } from "../../helpers/array";
|
||||
import { isNote } from "../../helpers/nostr-event";
|
||||
import { useAppTitle } from "../../hooks/use-app-title";
|
||||
import useSubject from "../../hooks/use-subject";
|
||||
import { useReadRelayUrls } from "../../hooks/use-client-relays";
|
||||
import { useTimelineLoader } from "../../hooks/use-timeline-loader";
|
||||
import settings from "../../services/settings";
|
||||
|
||||
export const GlobalTab = () => {
|
||||
useAppTitle("global");
|
||||
const defaultRelays = useSubject(settings.relays);
|
||||
const defaultRelays = useReadRelayUrls();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const selectedRelay = searchParams.get("relay") ?? "";
|
||||
const setSelectedRelay = (url: string) => {
|
||||
|
@ -4,13 +4,13 @@ import { useNavigate } from "react-router-dom";
|
||||
import { RelayUrlInput } from "../../components/relay-url-input";
|
||||
import { normalizeToHex } from "../../helpers/nip-19";
|
||||
import identity from "../../services/identity";
|
||||
import settings from "../../services/settings";
|
||||
import clientRelaysService from "../../services/client-relays";
|
||||
|
||||
export const LoginNpubView = () => {
|
||||
const navigate = useNavigate();
|
||||
const toast = useToast();
|
||||
const [npub, setNpub] = useState("");
|
||||
const [relay, setRelay] = useState("");
|
||||
const [relayUrl, setRelayUrl] = useState("");
|
||||
|
||||
const handleSubmit: React.FormEventHandler<HTMLDivElement> = (e) => {
|
||||
e.preventDefault();
|
||||
@ -21,10 +21,8 @@ export const LoginNpubView = () => {
|
||||
}
|
||||
|
||||
identity.loginWithPubkey(pubkey);
|
||||
// TODO: the settings service should not be in charge of the relays
|
||||
if (!settings.relays.value.includes(relay)) {
|
||||
settings.relays.next([...settings.relays.value, relay]);
|
||||
}
|
||||
|
||||
clientRelaysService.bootstrapRelays.add(relayUrl);
|
||||
};
|
||||
|
||||
return (
|
||||
@ -47,8 +45,8 @@ export const LoginNpubView = () => {
|
||||
<RelayUrlInput
|
||||
placeholder="wss://nostr.example.com"
|
||||
isRequired
|
||||
value={relay}
|
||||
onChange={(e) => setRelay(e.target.value)}
|
||||
value={relayUrl}
|
||||
onChange={(e) => setRelayUrl(e.target.value)}
|
||||
/>
|
||||
<FormHelperText>The first relay to connect to.</FormHelperText>
|
||||
</FormControl>
|
||||
|
@ -2,14 +2,13 @@ import { Button, Card, CardBody, Flex, Spinner, Text } from "@chakra-ui/react";
|
||||
import moment from "moment";
|
||||
import { memo } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { NoteContents } from "../../components/note/note-contents";
|
||||
import { UserAvatar } from "../../components/user-avatar";
|
||||
import { UserLink } from "../../components/user-link";
|
||||
import { convertTimestampToDate } from "../../helpers/date";
|
||||
import { useReadRelayUrls } from "../../hooks/use-client-relays";
|
||||
import useSubject from "../../hooks/use-subject";
|
||||
import { useTimelineLoader } from "../../hooks/use-timeline-loader";
|
||||
import identity from "../../services/identity";
|
||||
import settings from "../../services/settings";
|
||||
import { NostrEvent } from "../../types/nostr-event";
|
||||
|
||||
const Kind1Notification = ({ event }: { event: NostrEvent }) => {
|
||||
@ -39,11 +38,11 @@ const NotificationItem = memo(({ event }: { event: NostrEvent }) => {
|
||||
});
|
||||
|
||||
const NotificationsView = () => {
|
||||
const relays = useSubject(settings.relays);
|
||||
const readRelays = useReadRelayUrls();
|
||||
const pubkey = useSubject(identity.pubkey);
|
||||
const { events, loading, loadMore } = useTimelineLoader(
|
||||
"notifications",
|
||||
relays,
|
||||
readRelays,
|
||||
{
|
||||
"#p": [pubkey],
|
||||
kinds: [1],
|
||||
|
139
src/views/relays/index.tsx
Normal file
139
src/views/relays/index.tsx
Normal file
@ -0,0 +1,139 @@
|
||||
import {
|
||||
Button,
|
||||
Flex,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
Table,
|
||||
Thead,
|
||||
Tbody,
|
||||
Tr,
|
||||
Th,
|
||||
Td,
|
||||
TableContainer,
|
||||
IconButton,
|
||||
Text,
|
||||
Badge,
|
||||
} from "@chakra-ui/react";
|
||||
import { SyntheticEvent, useEffect, useState } from "react";
|
||||
import { TrashIcon, UndoIcon } from "../../components/icons";
|
||||
import useSubject from "../../hooks/use-subject";
|
||||
import { RelayFavicon } from "../../components/relay-favicon";
|
||||
import clientRelaysService from "../../services/client-relays";
|
||||
import { RelayConfig, RelayMode } from "../../services/relays/relay";
|
||||
import { useList } from "react-use";
|
||||
import { RelayUrlInput } from "../../components/relay-url-input";
|
||||
|
||||
export const RelaysView = () => {
|
||||
const relays = useSubject(clientRelaysService.relays);
|
||||
|
||||
const [pendingAdd, addActions] = useList<RelayConfig>([]);
|
||||
const [pendingRemove, removeActions] = useList<RelayConfig>([]);
|
||||
|
||||
useEffect(() => {
|
||||
addActions.clear();
|
||||
removeActions.clear();
|
||||
}, [relays, addActions, removeActions]);
|
||||
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [relayInputValue, setRelayInputValue] = useState("");
|
||||
|
||||
const handleRemoveRelay = (relay: RelayConfig) => {
|
||||
if (pendingAdd.includes(relay)) {
|
||||
addActions.filter((r) => r !== relay);
|
||||
} else if (pendingRemove.includes(relay)) {
|
||||
removeActions.filter((r) => r !== relay);
|
||||
} else {
|
||||
removeActions.push(relay);
|
||||
}
|
||||
};
|
||||
const handleAddRelay = (event: SyntheticEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
setRelayInputValue("");
|
||||
|
||||
const url = relayInputValue;
|
||||
if (!relays.some((r) => r.url === url) && !pendingAdd.some((r) => r.url === url)) {
|
||||
addActions.push({ url, mode: RelayMode.ALL });
|
||||
}
|
||||
};
|
||||
const savePending = async () => {
|
||||
setSaving(true);
|
||||
const newRelays = relays.concat(pendingAdd).filter((r) => !pendingRemove.includes(r));
|
||||
await clientRelaysService.postUpdatedRelays(newRelays);
|
||||
setSaving(false);
|
||||
};
|
||||
|
||||
const hasPending = pendingAdd.length > 0 || pendingRemove.length > 0;
|
||||
|
||||
return (
|
||||
<Flex direction="column" pt="2" pb="2" overflow="auto">
|
||||
<TableContainer mb="4">
|
||||
<Table variant="simple" size="sm">
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>Url</Th>
|
||||
<Th></Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{[...relays, ...pendingAdd].map((relay, i) => (
|
||||
<Tr key={relay.url + i}>
|
||||
<Td>
|
||||
<Flex alignItems="center">
|
||||
<RelayFavicon size="xs" relay={relay.url} mr="2" />
|
||||
<Text>{relay.url}</Text>
|
||||
</Flex>
|
||||
</Td>
|
||||
<Td isNumeric>
|
||||
{pendingAdd.includes(relay) && (
|
||||
<Badge colorScheme="green" mr="2">
|
||||
Add
|
||||
</Badge>
|
||||
)}
|
||||
{pendingRemove.includes(relay) && (
|
||||
<Badge colorScheme="red" mr="2">
|
||||
Remove
|
||||
</Badge>
|
||||
)}
|
||||
<IconButton
|
||||
icon={pendingRemove.includes(relay) ? <UndoIcon /> : <TrashIcon />}
|
||||
title="Toggle Remove"
|
||||
aria-label="Toggle Remove"
|
||||
size="sm"
|
||||
onClick={() => handleRemoveRelay(relay)}
|
||||
isDisabled={saving}
|
||||
/>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
<form onSubmit={handleAddRelay}>
|
||||
<FormControl>
|
||||
<FormLabel htmlFor="relay-url-input">Add Relay</FormLabel>
|
||||
<Flex gap="2">
|
||||
<RelayUrlInput
|
||||
id="relay-url-input"
|
||||
value={relayInputValue}
|
||||
onChange={(e) => setRelayInputValue(e.target.value)}
|
||||
isRequired
|
||||
/>
|
||||
<Button type="submit" isDisabled={saving}>
|
||||
Add
|
||||
</Button>
|
||||
</Flex>
|
||||
</FormControl>
|
||||
</form>
|
||||
|
||||
<Flex justifyContent="flex-end" gap="2">
|
||||
<Button type="submit" onClick={savePending} isDisabled={saving || !hasPending}>
|
||||
Reset
|
||||
</Button>
|
||||
<Button type="submit" isLoading={saving} onClick={savePending} isDisabled={!hasPending}>
|
||||
Save Changes
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
};
|
@ -5,14 +5,6 @@ import {
|
||||
FormLabel,
|
||||
Switch,
|
||||
useColorMode,
|
||||
Table,
|
||||
Thead,
|
||||
Tbody,
|
||||
Tr,
|
||||
Th,
|
||||
Td,
|
||||
TableContainer,
|
||||
IconButton,
|
||||
AccordionItem,
|
||||
Accordion,
|
||||
AccordionPanel,
|
||||
@ -21,41 +13,20 @@ import {
|
||||
AccordionIcon,
|
||||
ButtonGroup,
|
||||
FormHelperText,
|
||||
Text,
|
||||
} from "@chakra-ui/react";
|
||||
import { SyntheticEvent, useState } from "react";
|
||||
import { GlobalIcon, RelayIcon, TrashIcon } from "../../components/icons";
|
||||
import { RelayStatus } from "./relay-status";
|
||||
import { useState } from "react";
|
||||
import useSubject from "../../hooks/use-subject";
|
||||
import settings from "../../services/settings";
|
||||
import { clearCacheData, deleteDatabase } from "../../services/db";
|
||||
import { RelayUrlInput } from "../../components/relay-url-input";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import identity from "../../services/identity";
|
||||
import { RelayFavicon } from "../../components/relay-favicon";
|
||||
|
||||
export const SettingsView = () => {
|
||||
const navigate = useNavigate();
|
||||
const relays = useSubject(settings.relays);
|
||||
const blurImages = useSubject(settings.blurImages);
|
||||
const autoShowMedia = useSubject(settings.autoShowMedia);
|
||||
const proxyUserMedia = useSubject(settings.proxyUserMedia);
|
||||
const [relayInputValue, setRelayInputValue] = useState("");
|
||||
|
||||
const { colorMode, setColorMode } = useColorMode();
|
||||
|
||||
const handleRemoveRelay = (url: string) => {
|
||||
settings.relays.next(relays.filter((v) => v !== url));
|
||||
};
|
||||
const handleAddRelay = (event: SyntheticEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (!relays.includes(relayInputValue)) {
|
||||
settings.relays.next([...relays, relayInputValue]);
|
||||
setRelayInputValue("");
|
||||
}
|
||||
};
|
||||
|
||||
const [clearing, setClearing] = useState(false);
|
||||
const handleClearData = async () => {
|
||||
setClearing(true);
|
||||
@ -73,75 +44,6 @@ export const SettingsView = () => {
|
||||
return (
|
||||
<Flex direction="column" pt="2" pb="2" overflow="auto">
|
||||
<Accordion defaultIndex={[0]} allowMultiple>
|
||||
<AccordionItem>
|
||||
<h2>
|
||||
<AccordionButton>
|
||||
<Box as="span" flex="1" textAlign="left">
|
||||
Relays
|
||||
</Box>
|
||||
<AccordionIcon />
|
||||
</AccordionButton>
|
||||
</h2>
|
||||
<AccordionPanel>
|
||||
<TableContainer mb="4">
|
||||
<Table variant="simple" size="sm">
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>Url</Th>
|
||||
<Th>Status</Th>
|
||||
<Th></Th>
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{relays.map((url) => (
|
||||
<Tr key={url}>
|
||||
<Td>
|
||||
<Flex alignItems="center">
|
||||
<RelayFavicon size="xs" relay={url} mr="2" />
|
||||
<Text>{url}</Text>
|
||||
</Flex>
|
||||
</Td>
|
||||
<Td>
|
||||
<RelayStatus url={url} />
|
||||
</Td>
|
||||
<Td isNumeric>
|
||||
<IconButton
|
||||
icon={<GlobalIcon />}
|
||||
onClick={() => navigate("/global?relay=" + url)}
|
||||
size="sm"
|
||||
aria-label="Global Feed"
|
||||
mr="2"
|
||||
/>
|
||||
<IconButton
|
||||
icon={<TrashIcon />}
|
||||
title="Remove Relay"
|
||||
aria-label="Remove Relay"
|
||||
size="sm"
|
||||
onClick={() => handleRemoveRelay(url)}
|
||||
/>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<form onSubmit={handleAddRelay}>
|
||||
<FormControl>
|
||||
<FormLabel htmlFor="relay-url-input">Add Relay</FormLabel>
|
||||
<Flex gap="2">
|
||||
<RelayUrlInput
|
||||
id="relay-url-input"
|
||||
value={relayInputValue}
|
||||
onChange={(e) => setRelayInputValue(e.target.value)}
|
||||
isRequired
|
||||
/>
|
||||
<Button type="submit">Add</Button>
|
||||
</Flex>
|
||||
</FormControl>
|
||||
</form>
|
||||
</AccordionPanel>
|
||||
</AccordionItem>
|
||||
|
||||
<AccordionItem>
|
||||
<h2>
|
||||
<AccordionButton>
|
||||
|
@ -1,5 +1,5 @@
|
||||
import moment from "moment";
|
||||
import { Flex, Grid, SkeletonText, Text } from "@chakra-ui/react";
|
||||
import { Flex, Grid, SkeletonText } from "@chakra-ui/react";
|
||||
|
||||
import { UserCard } from "./components/user-card";
|
||||
import { useUserContacts } from "../../hooks/use-user-contacts";
|
||||
|
@ -3,13 +3,12 @@ import moment from "moment";
|
||||
import { useOutletContext } from "react-router-dom";
|
||||
import { Note } from "../../components/note";
|
||||
import { isNote } from "../../helpers/nostr-event";
|
||||
import useSubject from "../../hooks/use-subject";
|
||||
import { useReadRelayUrls } from "../../hooks/use-client-relays";
|
||||
import { useTimelineLoader } from "../../hooks/use-timeline-loader";
|
||||
import settings from "../../services/settings";
|
||||
|
||||
const UserNotesTab = () => {
|
||||
const { pubkey } = useOutletContext() as { pubkey: string };
|
||||
const relays = useSubject(settings.relays);
|
||||
const relays = useReadRelayUrls();
|
||||
|
||||
const { events, loading, loadMore } = useTimelineLoader(
|
||||
`${pubkey} notes`,
|
||||
|
@ -1,49 +1,51 @@
|
||||
import { useCallback } from "react";
|
||||
import { SkeletonText, Text, Grid, Box, IconButton } from "@chakra-ui/react";
|
||||
import settings from "../../services/settings";
|
||||
import useSubject from "../../hooks/use-subject";
|
||||
import { Text, Grid, Box, IconButton, Flex, Heading } from "@chakra-ui/react";
|
||||
import { useUserContacts } from "../../hooks/use-user-contacts";
|
||||
import { useNavigate, useOutletContext } from "react-router-dom";
|
||||
import { AddIcon, GlobalIcon } from "../../components/icons";
|
||||
import { GlobalIcon } from "../../components/icons";
|
||||
import { useUserRelays } from "../../hooks/use-user-relays";
|
||||
|
||||
const UserRelaysTab = () => {
|
||||
const { pubkey } = useOutletContext() as { pubkey: string };
|
||||
const contacts = useUserContacts(pubkey);
|
||||
const userRelays = useUserRelays(pubkey);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const relays = useSubject(settings.relays);
|
||||
const addRelay = useCallback(
|
||||
(url: string) => {
|
||||
settings.relays.next([...relays, url]);
|
||||
},
|
||||
[relays]
|
||||
);
|
||||
|
||||
if (!contacts) {
|
||||
return <SkeletonText />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Grid templateColumns={{ base: "1fr", xl: "repeat(2, 1fr)" }} gap="2">
|
||||
{Object.entries(contacts.relays).map(([url, opts]) => (
|
||||
<Box key={url} display="flex" gap="2" alignItems="center" pr="2" pl="2">
|
||||
<Text flex={1}>{url}</Text>
|
||||
<IconButton
|
||||
icon={<GlobalIcon />}
|
||||
onClick={() => navigate("/global?relay=" + url)}
|
||||
size="sm"
|
||||
aria-label="Global Feed"
|
||||
/>
|
||||
<IconButton
|
||||
icon={<AddIcon />}
|
||||
onClick={() => addRelay(url)}
|
||||
isDisabled={relays.includes(url)}
|
||||
size="sm"
|
||||
aria-label="Add Relay"
|
||||
/>
|
||||
</Box>
|
||||
))}
|
||||
</Grid>
|
||||
<Flex direction="column">
|
||||
{userRelays && (
|
||||
<Grid templateColumns={{ base: "1fr", xl: "repeat(2, 1fr)" }} gap="2">
|
||||
{userRelays.relays.map((relayConfig) => (
|
||||
<Box key={relayConfig.url} display="flex" gap="2" alignItems="center" pr="2" pl="2">
|
||||
<Text flex={1}>{relayConfig.url}</Text>
|
||||
<IconButton
|
||||
icon={<GlobalIcon />}
|
||||
onClick={() => navigate("/global?relay=" + relayConfig.url)}
|
||||
size="sm"
|
||||
aria-label="Global Feed"
|
||||
/>
|
||||
</Box>
|
||||
))}
|
||||
</Grid>
|
||||
)}
|
||||
{contacts && (
|
||||
<>
|
||||
<Heading size="md">Relays from contact list (old)</Heading>
|
||||
<Grid templateColumns={{ base: "1fr", xl: "repeat(2, 1fr)" }} gap="2">
|
||||
{Object.entries(contacts.relays).map(([url, opts]) => (
|
||||
<Box key={url} display="flex" gap="2" alignItems="center" pr="2" pl="2">
|
||||
<Text flex={1}>{url}</Text>
|
||||
<IconButton
|
||||
icon={<GlobalIcon />}
|
||||
onClick={() => navigate("/global?relay=" + url)}
|
||||
size="sm"
|
||||
aria-label="Global Feed"
|
||||
/>
|
||||
</Box>
|
||||
))}
|
||||
</Grid>
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -3,13 +3,12 @@ import moment from "moment";
|
||||
import { useOutletContext } from "react-router-dom";
|
||||
import { Note } from "../../components/note";
|
||||
import { isReply } from "../../helpers/nostr-event";
|
||||
import useSubject from "../../hooks/use-subject";
|
||||
import { useReadRelayUrls } from "../../hooks/use-client-relays";
|
||||
import { useTimelineLoader } from "../../hooks/use-timeline-loader";
|
||||
import settings from "../../services/settings";
|
||||
|
||||
const UserRepliesTab = () => {
|
||||
const { pubkey } = useOutletContext() as { pubkey: string };
|
||||
const relays = useSubject(settings.relays);
|
||||
const relays = useReadRelayUrls();
|
||||
|
||||
const { events, loading, loadMore } = useTimelineLoader(
|
||||
`${pubkey} replies`,
|
||||
|
Loading…
x
Reference in New Issue
Block a user