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));
|
||||
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;
|
||||
|
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 { 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);
|
||||
|
@ -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);
|
||||
|
@ -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]);
|
||||
|
||||
|
@ -67,7 +67,6 @@ export default function ProfilePage() {
|
||||
<div className="btn">Reactions</div>
|
||||
<div className="btn">Followers</div>
|
||||
<div className="btn">Follows</div>
|
||||
<div className="btn">Relays</div>
|
||||
</div>
|
||||
<Timeline pubkeys={id} />
|
||||
</>
|
||||
|
@ -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 (
|
||||
<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 (
|
||||
<div className="settings">
|
||||
<h1>Settings</h1>
|
||||
<div className="flex f-center">
|
||||
<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>
|
||||
|
||||
{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>
|
||||
);
|
||||
}
|
@ -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;
|
Loading…
x
Reference in New Issue
Block a user