Relay managment
This commit is contained in:
parent
276a4cbcd1
commit
fd7e00c8d4
10
src/element/Relay.css
Normal file
10
src/element/Relay.css
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
.relay {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
background-color: #222;
|
||||||
|
border-radius: 5px;
|
||||||
|
text-align: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.relay > div {
|
||||||
|
padding: 5px;
|
||||||
|
}
|
50
src/element/Relay.js
Normal file
50
src/element/Relay.js
Normal file
@ -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 (
|
||||||
|
<>
|
||||||
|
<div className="flex relay w-max">
|
||||||
|
<div>
|
||||||
|
<FontAwesomeIcon icon={faPlug} color={state?.connected ? "green" : "red"} />
|
||||||
|
</div>
|
||||||
|
<div className="f-grow f-col">
|
||||||
|
<b>{name}</b>
|
||||||
|
<div>
|
||||||
|
Write
|
||||||
|
<span className="pill" onClick={() => configure({ write: !relaySettings.write, read: relaySettings.read })}>
|
||||||
|
<FontAwesomeIcon icon={relaySettings.write ? faSquareCheck : faSquareXmark} />
|
||||||
|
</span>
|
||||||
|
Read
|
||||||
|
<span className="pill" onClick={() => configure({ write: relaySettings.write, read: !relaySettings.read })}>
|
||||||
|
<FontAwesomeIcon icon={relaySettings.read ? faSquareCheck : faSquareXmark} />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="pill">
|
||||||
|
<FontAwesomeIcon icon={faTrash} onClick={() => dispatch(removeRelay(props.addr))} />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
@ -89,6 +89,16 @@ export default function useEventPublisher() {
|
|||||||
ev.Tags.push(new Tag(["p", evRef.PubKey], 1));
|
ev.Tags.push(new Tag(["p", evRef.PubKey], 1));
|
||||||
return await signEvent(ev);
|
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) => {
|
addFollow: async (pkAdd) => {
|
||||||
let ev = Event.ForPubKey(pubKey);
|
let ev = Event.ForPubKey(pubKey);
|
||||||
ev.Kind = EventKind.ContactList;
|
ev.Kind = EventKind.ContactList;
|
||||||
|
9
src/feed/RelayState.js
Normal file
9
src/feed/RelayState.js
Normal file
@ -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);
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
import * as secp from "@noble/secp256k1";
|
import * as secp from "@noble/secp256k1";
|
||||||
|
import { v4 as uuid } from "uuid";
|
||||||
|
|
||||||
import { Subscriptions } from "./Subscriptions";
|
import { Subscriptions } from "./Subscriptions";
|
||||||
import Event from "./Event";
|
import Event from "./Event";
|
||||||
@ -24,10 +25,19 @@ export default class Connection {
|
|||||||
this.Write = options?.write || true;
|
this.Write = options?.write || true;
|
||||||
this.ConnectTimeout = DefaultConnectTimeout;
|
this.ConnectTimeout = DefaultConnectTimeout;
|
||||||
this.Stats = new ConnectionStats();
|
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();
|
this.Connect();
|
||||||
}
|
}
|
||||||
|
|
||||||
Connect() {
|
Connect() {
|
||||||
|
this.IsClosed = false;
|
||||||
this.Socket = new WebSocket(this.Address);
|
this.Socket = new WebSocket(this.Address);
|
||||||
this.Socket.onopen = (e) => this.OnOpen(e);
|
this.Socket.onopen = (e) => this.OnOpen(e);
|
||||||
this.Socket.onmessage = (e) => this.OnMessage(e);
|
this.Socket.onmessage = (e) => this.OnMessage(e);
|
||||||
@ -35,6 +45,16 @@ export default class Connection {
|
|||||||
this.Socket.onclose = (e) => this.OnClose(e);
|
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) {
|
OnOpen(e) {
|
||||||
this.ConnectTimeout = DefaultConnectTimeout;
|
this.ConnectTimeout = DefaultConnectTimeout;
|
||||||
console.log(`[${this.Address}] Open!`);
|
console.log(`[${this.Address}] Open!`);
|
||||||
@ -43,14 +63,22 @@ export default class Connection {
|
|||||||
for (let p of this.Pending) {
|
for (let p of this.Pending) {
|
||||||
this._SendJson(p);
|
this._SendJson(p);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this._UpdateState();
|
||||||
}
|
}
|
||||||
|
|
||||||
OnClose(e) {
|
OnClose(e) {
|
||||||
this.ConnectTimeout = this.ConnectTimeout * 2;
|
if (!this.IsClosed) {
|
||||||
console.log(`[${this.Address}] Closed (${e.reason}), trying again in ${(this.ConnectTimeout / 1000).toFixed(0).toLocaleString()} sec`);
|
this.ConnectTimeout = this.ConnectTimeout * 2;
|
||||||
setTimeout(() => {
|
console.log(`[${this.Address}] Closed (${e.reason}), trying again in ${(this.ConnectTimeout / 1000).toFixed(0).toLocaleString()} sec`);
|
||||||
this.Connect();
|
this.ReconnectTimer = setTimeout(() => {
|
||||||
}, this.ConnectTimeout);
|
this.Connect();
|
||||||
|
}, this.ConnectTimeout);
|
||||||
|
} else {
|
||||||
|
console.log(`[${this.Address}] Closed!`);
|
||||||
|
this.ReconnectTimer = null;
|
||||||
|
}
|
||||||
|
this._UpdateState();
|
||||||
}
|
}
|
||||||
|
|
||||||
OnMessage(e) {
|
OnMessage(e) {
|
||||||
@ -84,7 +112,8 @@ export default class Connection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
OnError(e) {
|
OnError(e) {
|
||||||
console.log(e);
|
console.error(e);
|
||||||
|
this._UpdateState();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -144,6 +173,43 @@ export default class Connection {
|
|||||||
return false;
|
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) {
|
_SendJson(obj) {
|
||||||
if (this.Socket?.readyState !== WebSocket.OPEN) {
|
if (this.Socket?.readyState !== WebSocket.OPEN) {
|
||||||
this.Pending.push(obj);
|
this.Pending.push(obj);
|
||||||
|
@ -28,6 +28,14 @@ export class NostrSystem {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DisconnectRelay(address) {
|
||||||
|
let c = this.Sockets[address];
|
||||||
|
delete this.Sockets[address];
|
||||||
|
if (c) {
|
||||||
|
c.Close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
AddSubscription(sub) {
|
AddSubscription(sub) {
|
||||||
for (let s of Object.values(this.Sockets)) {
|
for (let s of Object.values(this.Sockets)) {
|
||||||
s.AddSubscription(sub);
|
s.AddSubscription(sub);
|
||||||
|
@ -27,6 +27,11 @@ export default function Layout(props) {
|
|||||||
for (let [k, v] of Object.entries(relays)) {
|
for (let [k, v] of Object.entries(relays)) {
|
||||||
System.ConnectToRelay(k, v);
|
System.ConnectToRelay(k, v);
|
||||||
}
|
}
|
||||||
|
for (let [k, v] of Object.entries(System.Sockets)) {
|
||||||
|
if (!relays[k]) {
|
||||||
|
System.DisconnectRelay(k);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [relays]);
|
}, [relays]);
|
||||||
|
|
||||||
|
@ -67,7 +67,6 @@ export default function ProfilePage() {
|
|||||||
<div className="btn">Reactions</div>
|
<div className="btn">Reactions</div>
|
||||||
<div className="btn">Followers</div>
|
<div className="btn">Followers</div>
|
||||||
<div className="btn">Follows</div>
|
<div className="btn">Follows</div>
|
||||||
<div className="btn">Relays</div>
|
|
||||||
</div>
|
</div>
|
||||||
<Timeline pubkeys={id} />
|
<Timeline pubkeys={id} />
|
||||||
</>
|
</>
|
||||||
|
@ -7,12 +7,14 @@ import { useDispatch, useSelector } from "react-redux";
|
|||||||
import useEventPublisher from "../feed/EventPublisher";
|
import useEventPublisher from "../feed/EventPublisher";
|
||||||
import useProfile from "../feed/ProfileFeed";
|
import useProfile from "../feed/ProfileFeed";
|
||||||
import VoidUpload from "../feed/VoidUpload";
|
import VoidUpload from "../feed/VoidUpload";
|
||||||
import { logout } from "../state/Login";
|
import { logout, setRelays } from "../state/Login";
|
||||||
import { resetProfile } from "../state/Users";
|
import { resetProfile } from "../state/Users";
|
||||||
import { openFile } from "../Util";
|
import { openFile } from "../Util";
|
||||||
|
import Relay from "../element/Relay";
|
||||||
|
|
||||||
export default function SettingsPage(props) {
|
export default function SettingsPage(props) {
|
||||||
const id = useSelector(s => s.login.publicKey);
|
const id = useSelector(s => s.login.publicKey);
|
||||||
|
const relays = useSelector(s => s.login.relays);
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const user = useProfile(id);
|
const user = useProfile(id);
|
||||||
const publisher = useEventPublisher();
|
const publisher = useEventPublisher();
|
||||||
@ -24,6 +26,7 @@ export default function SettingsPage(props) {
|
|||||||
const [nip05, setNip05] = useState("");
|
const [nip05, setNip05] = useState("");
|
||||||
const [lud06, setLud06] = useState("");
|
const [lud06, setLud06] = useState("");
|
||||||
const [lud16, setLud16] = useState("");
|
const [lud16, setLud16] = useState("");
|
||||||
|
const [newRelay, setNewRelay] = useState("");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (user) {
|
if (user) {
|
||||||
@ -85,6 +88,11 @@ export default function SettingsPage(props) {
|
|||||||
setPicture(rsp.metadata.url ?? `https://void.cat/d/${rsp.id}`)
|
setPicture(rsp.metadata.url ?? `https://void.cat/d/${rsp.id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function saveRelays() {
|
||||||
|
let ev = await publisher.saveRelays();
|
||||||
|
publisher.broadcast(ev);
|
||||||
|
}
|
||||||
|
|
||||||
function editor() {
|
function editor() {
|
||||||
return (
|
return (
|
||||||
<div className="editor">
|
<div className="editor">
|
||||||
@ -130,16 +138,36 @@ export default function SettingsPage(props) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function addRelay() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h4>Add Relays</h4>
|
||||||
|
<div className="flex mb10">
|
||||||
|
<input type="text" className="f-grow" placeholder="wss://my-relay.com" value={newRelay} onChange={(e) => setNewRelay(e.target.value)} />
|
||||||
|
</div>
|
||||||
|
<div className="btn mb10" onClick={() => dispatch(setRelays({ [newRelay]: { read: false, write: false } }))}>Add</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="settings">
|
<div className="settings">
|
||||||
<h1>Settings</h1>
|
<h1>Settings</h1>
|
||||||
<div className="flex f-center">
|
<div className="flex f-center">
|
||||||
<div style={{ backgroundImage: `url(${picture.length === 0 ? Nostrich : picture})` }} className="avatar">
|
<div style={{ backgroundImage: `url(${picture.length === 0 ? Nostrich : picture})` }} className="avatar">
|
||||||
<div className="edit">Edit</div>
|
<div className="edit" onClick={() => setNewAvatar()}>Edit</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{editor()}
|
{editor()}
|
||||||
|
<h4>Relays</h4>
|
||||||
|
<div className="flex f-col">
|
||||||
|
{Object.keys(relays || {}).map(a => <Relay addr={a} key={a} />)}
|
||||||
|
</div>
|
||||||
|
<div className="flex">
|
||||||
|
<div className="f-grow"></div>
|
||||||
|
<div className="btn" onClick={() => saveRelays()}>Save</div>
|
||||||
|
</div>
|
||||||
|
{addRelay()}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
@ -86,11 +86,15 @@ const LoginSlice = createSlice({
|
|||||||
},
|
},
|
||||||
setRelays: (state, action) => {
|
setRelays: (state, action) => {
|
||||||
// filter out non-websocket urls
|
// 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://"));
|
.filter(a => a[0].startsWith("ws://") || a[0].startsWith("wss://"));
|
||||||
|
|
||||||
state.relays = Object.fromEntries(filtered);
|
state.relays = Object.fromEntries(filtered);
|
||||||
},
|
},
|
||||||
|
removeRelay: (state, action) => {
|
||||||
|
delete state.relays[action.payload];
|
||||||
|
state.relays = { ...state.relays };
|
||||||
|
},
|
||||||
setFollows: (state, action) => {
|
setFollows: (state, action) => {
|
||||||
let existing = new Set(state.follows);
|
let existing = new Set(state.follows);
|
||||||
let update = Array.isArray(action.payload) ? action.payload : [action.payload];
|
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;
|
export const reducer = LoginSlice.reducer;
|
Loading…
Reference in New Issue
Block a user