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() {
@@ -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 => )}
+
+
+ {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