From fd7e00c8d4f2f6ef76266142de3766cf6dc0f3f6 Mon Sep 17 00:00:00 2001 From: Kieran Date: Mon, 9 Jan 2023 12:40:10 +0000 Subject: [PATCH] Relay managment --- src/element/Relay.css | 10 +++++ src/element/Relay.js | 50 ++++++++++++++++++++++++ src/feed/EventPublisher.js | 10 +++++ src/feed/RelayState.js | 9 +++++ src/nostr/Connection.js | 78 +++++++++++++++++++++++++++++++++++--- src/nostr/System.js | 8 ++++ src/pages/Layout.js | 5 +++ src/pages/ProfilePage.js | 1 - src/pages/SettingsPage.js | 34 +++++++++++++++-- src/state/Login.js | 8 +++- 10 files changed, 201 insertions(+), 12 deletions(-) create mode 100644 src/element/Relay.css create mode 100644 src/element/Relay.js create mode 100644 src/feed/RelayState.js diff --git a/src/element/Relay.css b/src/element/Relay.css new file mode 100644 index 00000000..93e1f796 --- /dev/null +++ b/src/element/Relay.css @@ -0,0 +1,10 @@ +.relay { + margin-bottom: 10px; + background-color: #222; + border-radius: 5px; + text-align: start; +} + +.relay > div { + padding: 5px; +} \ No newline at end of file diff --git a/src/element/Relay.js b/src/element/Relay.js new file mode 100644 index 00000000..2d3ac597 --- /dev/null +++ b/src/element/Relay.js @@ -0,0 +1,50 @@ +import "./Relay.css" + +import { faPlug, faTrash, faSquareCheck, faSquareXmark } from "@fortawesome/free-solid-svg-icons"; +import useRelayState from "../feed/RelayState"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { useMemo } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { removeRelay, setRelays } from "../state/Login"; + + +export default function Relay(props) { + const dispatch = useDispatch(); + const relaySettings = useSelector(s => s.login.relays[props.addr]); + const state = useRelayState(props.addr); + const name = useMemo(() => new URL(props.addr).host, [props.addr]); + + function configure(o) { + dispatch(setRelays({ + [props.addr]: o + })); + } + + return ( + <> +
+
+ +
+
+ {name} +
+ Write + configure({ write: !relaySettings.write, read: relaySettings.read })}> + + + Read + configure({ write: relaySettings.write, read: !relaySettings.read })}> + + +
+
+
+ + dispatch(removeRelay(props.addr))} /> + +
+
+ + ) +} \ No newline at end of file diff --git a/src/feed/EventPublisher.js b/src/feed/EventPublisher.js index 0fc85939..2f7a9fe8 100644 --- a/src/feed/EventPublisher.js +++ b/src/feed/EventPublisher.js @@ -89,6 +89,16 @@ export default function useEventPublisher() { ev.Tags.push(new Tag(["p", evRef.PubKey], 1)); return await signEvent(ev); }, + saveRelays: async () => { + let ev = Event.ForPubKey(pubKey); + ev.Kind = EventKind.ContactList; + ev.Content = JSON.stringify(relays); + for (let pk of follows) { + ev.Tags.push(new Tag(["p", pk])); + } + + return await signEvent(ev); + }, addFollow: async (pkAdd) => { let ev = Event.ForPubKey(pubKey); ev.Kind = EventKind.ContactList; diff --git a/src/feed/RelayState.js b/src/feed/RelayState.js new file mode 100644 index 00000000..3d82d938 --- /dev/null +++ b/src/feed/RelayState.js @@ -0,0 +1,9 @@ +import { useSyncExternalStore } from "react"; +import { System } from ".."; + +const noop = () => {}; + +export default function useRelayState(addr) { + let c = System.Sockets[addr]; + return useSyncExternalStore(c?.StatusHook.bind(c) ?? noop, c?.GetState.bind(c) ?? noop); +} \ No newline at end of file diff --git a/src/nostr/Connection.js b/src/nostr/Connection.js index 839528b2..5ab9536b 100644 --- a/src/nostr/Connection.js +++ b/src/nostr/Connection.js @@ -1,4 +1,5 @@ import * as secp from "@noble/secp256k1"; +import { v4 as uuid } from "uuid"; import { Subscriptions } from "./Subscriptions"; import Event from "./Event"; @@ -24,10 +25,19 @@ export default class Connection { this.Write = options?.write || true; this.ConnectTimeout = DefaultConnectTimeout; this.Stats = new ConnectionStats(); + this.StateHooks = {}; + this.HasStateChange = true; + this.CurrentState = { + connected: false + }; + this.LastState = Object.freeze({ ...this.CurrentState }); + this.IsClosed = false; + this.ReconnectTimer = null; this.Connect(); } Connect() { + this.IsClosed = false; this.Socket = new WebSocket(this.Address); this.Socket.onopen = (e) => this.OnOpen(e); this.Socket.onmessage = (e) => this.OnMessage(e); @@ -35,6 +45,16 @@ export default class Connection { this.Socket.onclose = (e) => this.OnClose(e); } + Close() { + this.IsClosed = true; + if(this.ReconnectTimer !== null) { + clearTimeout(this.ReconnectTimer); + this.ReconnectTimer = null; + } + this.Socket.close(); + this._UpdateState(); + } + OnOpen(e) { this.ConnectTimeout = DefaultConnectTimeout; console.log(`[${this.Address}] Open!`); @@ -43,14 +63,22 @@ export default class Connection { for (let p of this.Pending) { this._SendJson(p); } + + this._UpdateState(); } OnClose(e) { - this.ConnectTimeout = this.ConnectTimeout * 2; - console.log(`[${this.Address}] Closed (${e.reason}), trying again in ${(this.ConnectTimeout / 1000).toFixed(0).toLocaleString()} sec`); - setTimeout(() => { - this.Connect(); - }, this.ConnectTimeout); + if (!this.IsClosed) { + this.ConnectTimeout = this.ConnectTimeout * 2; + console.log(`[${this.Address}] Closed (${e.reason}), trying again in ${(this.ConnectTimeout / 1000).toFixed(0).toLocaleString()} sec`); + this.ReconnectTimer = setTimeout(() => { + this.Connect(); + }, this.ConnectTimeout); + } else { + console.log(`[${this.Address}] Closed!`); + this.ReconnectTimer = null; + } + this._UpdateState(); } OnMessage(e) { @@ -84,7 +112,8 @@ export default class Connection { } OnError(e) { - console.log(e); + console.error(e); + this._UpdateState(); } /** @@ -144,6 +173,43 @@ export default class Connection { return false; } + /** + * Hook status for connection + * @param {function} fnHook Subscription hook + */ + StatusHook(fnHook) { + let id = uuid(); + this.StateHooks[id] = fnHook; + return () => { + delete this.StateHooks[id]; + }; + } + + /** + * Returns the current state of this connection + * @returns {any} + */ + GetState() { + if (this.HasStateChange) { + this.LastState = Object.freeze({ ...this.CurrentState }); + this.HasStateChange = false; + } + return this.LastState; + } + + _UpdateState() { + this.CurrentState.connected = this.Socket?.readyState === WebSocket.OPEN; + this.HasStateChange = true; + this._NotifyState(); + } + + _NotifyState() { + let state = this.GetState(); + for (let h of Object.values(this.StateHooks)) { + h(state); + } + } + _SendJson(obj) { if (this.Socket?.readyState !== WebSocket.OPEN) { this.Pending.push(obj); diff --git a/src/nostr/System.js b/src/nostr/System.js index 486da5cd..0469deda 100644 --- a/src/nostr/System.js +++ b/src/nostr/System.js @@ -28,6 +28,14 @@ export class NostrSystem { } } + DisconnectRelay(address) { + let c = this.Sockets[address]; + delete this.Sockets[address]; + if (c) { + c.Close(); + } + } + AddSubscription(sub) { for (let s of Object.values(this.Sockets)) { s.AddSubscription(sub); diff --git a/src/pages/Layout.js b/src/pages/Layout.js index d41101fd..21eb8222 100644 --- a/src/pages/Layout.js +++ b/src/pages/Layout.js @@ -27,6 +27,11 @@ export default function Layout(props) { for (let [k, v] of Object.entries(relays)) { System.ConnectToRelay(k, v); } + for (let [k, v] of Object.entries(System.Sockets)) { + if (!relays[k]) { + System.DisconnectRelay(k); + } + } } }, [relays]); diff --git a/src/pages/ProfilePage.js b/src/pages/ProfilePage.js index 53f74d70..da90a732 100644 --- a/src/pages/ProfilePage.js +++ b/src/pages/ProfilePage.js @@ -67,7 +67,6 @@ export default function ProfilePage() {
Reactions
Followers
Follows
-
Relays
diff --git a/src/pages/SettingsPage.js b/src/pages/SettingsPage.js index 7187a557..2e9630fa 100644 --- a/src/pages/SettingsPage.js +++ b/src/pages/SettingsPage.js @@ -7,12 +7,14 @@ import { useDispatch, useSelector } from "react-redux"; import useEventPublisher from "../feed/EventPublisher"; import useProfile from "../feed/ProfileFeed"; import VoidUpload from "../feed/VoidUpload"; -import { logout } from "../state/Login"; +import { logout, setRelays } from "../state/Login"; import { resetProfile } from "../state/Users"; import { openFile } from "../Util"; +import Relay from "../element/Relay"; export default function SettingsPage(props) { const id = useSelector(s => s.login.publicKey); + const relays = useSelector(s => s.login.relays); const dispatch = useDispatch(); const user = useProfile(id); const publisher = useEventPublisher(); @@ -24,6 +26,7 @@ export default function SettingsPage(props) { const [nip05, setNip05] = useState(""); const [lud06, setLud06] = useState(""); const [lud16, setLud16] = useState(""); + const [newRelay, setNewRelay] = useState(""); useEffect(() => { if (user) { @@ -85,6 +88,11 @@ export default function SettingsPage(props) { setPicture(rsp.metadata.url ?? `https://void.cat/d/${rsp.id}`) } + async function saveRelays() { + let ev = await publisher.saveRelays(); + publisher.broadcast(ev); + } + function editor() { return (
@@ -130,16 +138,36 @@ export default function SettingsPage(props) { ) } + function addRelay() { + return ( + <> +

Add Relays

+
+ setNewRelay(e.target.value)} /> +
+
dispatch(setRelays({ [newRelay]: { read: false, write: false } }))}>Add
+ + ) + } + return (

Settings

-
Edit
+
setNewAvatar()}>Edit
- {editor()} +

Relays

+
+ {Object.keys(relays || {}).map(a => )} +
+
+
+
saveRelays()}>Save
+
+ {addRelay()}
); } \ No newline at end of file diff --git a/src/state/Login.js b/src/state/Login.js index cd4df895..a51baf34 100644 --- a/src/state/Login.js +++ b/src/state/Login.js @@ -86,11 +86,15 @@ const LoginSlice = createSlice({ }, setRelays: (state, action) => { // filter out non-websocket urls - let filtered = Object.entries(action.payload) + let filtered = Object.entries({ ...state.relays, ...action.payload }) .filter(a => a[0].startsWith("ws://") || a[0].startsWith("wss://")); state.relays = Object.fromEntries(filtered); }, + removeRelay: (state, action) => { + delete state.relays[action.payload]; + state.relays = { ...state.relays }; + }, setFollows: (state, action) => { let existing = new Set(state.follows); let update = Array.isArray(action.payload) ? action.payload : [action.payload]; @@ -143,5 +147,5 @@ const LoginSlice = createSlice({ } }); -export const { init, setPrivateKey, setPublicKey, setRelays, setFollows, addNotifications, logout, markNotificationsRead } = LoginSlice.actions; +export const { init, setPrivateKey, setPublicKey, setRelays, removeRelay, setFollows, addNotifications, logout, markNotificationsRead } = LoginSlice.actions; export const reducer = LoginSlice.reducer; \ No newline at end of file