From c9432501343d4d6de2729d55d22e13aa5ded2f66 Mon Sep 17 00:00:00 2001 From: hzrd149 Date: Tue, 7 Feb 2023 17:04:17 -0600 Subject: [PATCH] add simple subscription --- index.html | 2 +- package.json | 6 + src/app.jsx | 38 + src/components/error-boundary.jsx | 24 + src/components/wait-for-relays.jsx | 18 + src/helpers/signal.js | 18 +- src/hooks/use-signal.js | 9 + src/index.jsx | 15 +- src/services/{db.js => db/index.js} | 4 - src/services/{ => db}/migrations/index.js | 3 + src/services/{relays.js => relays/index.js} | 48 +- src/services/relays/relay.js | 66 + src/services/subscriptions.js | 44 + src/views/home.jsx | 25 + src/views/settings.jsx | 54 + src/views/user/index.jsx | 46 + src/views/user/posts.jsx | 40 + src/views/user/relays.jsx | 41 + yarn.lock | 1798 ++++++++++++++++++- 19 files changed, 2224 insertions(+), 75 deletions(-) create mode 100644 src/app.jsx create mode 100644 src/components/error-boundary.jsx create mode 100644 src/components/wait-for-relays.jsx create mode 100644 src/hooks/use-signal.js rename src/services/{db.js => db/index.js} (72%) rename src/services/{ => db}/migrations/index.js (89%) rename src/services/{relays.js => relays/index.js} (51%) create mode 100644 src/services/relays/relay.js create mode 100644 src/services/subscriptions.js create mode 100644 src/views/home.jsx create mode 100644 src/views/settings.jsx create mode 100644 src/views/user/index.jsx create mode 100644 src/views/user/posts.jsx create mode 100644 src/views/user/relays.jsx diff --git a/index.html b/index.html index c43df682b..772288820 100644 --- a/index.html +++ b/index.html @@ -4,7 +4,7 @@ - nostr-client + personal-nostr-client
diff --git a/package.json b/package.json index 605fe7dc0..086c01532 100644 --- a/package.json +++ b/package.json @@ -13,10 +13,16 @@ "vite-plugin-pwa": "^0.14.0" }, "dependencies": { + "@chakra-ui/react": "^2.4.4", + "@emotion/react": "^11.10.5", + "@emotion/styled": "^11.10.5", + "framer-motion": "^7.10.3", "idb": "^7.1.1", "noble-secp256k1": "^1.2.14", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-error-boundary": "^3.1.4", + "react-markdown": "^8.0.4", "react-router-dom": "^6.5.0" } } diff --git a/src/app.jsx b/src/app.jsx new file mode 100644 index 000000000..aa97e6dbd --- /dev/null +++ b/src/app.jsx @@ -0,0 +1,38 @@ +import React, { useEffect } from "react"; +import { ChakraProvider } from "@chakra-ui/react"; +import { HashRouter, Route, Routes } from "react-router-dom"; +import { connectToRelays } from "./services/relays"; +import { HomeView } from "./views/home"; +import { UserView } from "./views/user"; +import { ErrorBoundary } from "./components/error-boundary"; +import { SettingsView } from "./views/settings"; +import { WaitForRelays } from "./components/wait-for-relays"; + +export const App = () => { + useEffect(() => { + connectToRelays(); + }, []); + + return ( + + + + + + + + + } + /> + } /> + } /> + + + + + + ); +}; diff --git a/src/components/error-boundary.jsx b/src/components/error-boundary.jsx new file mode 100644 index 000000000..27cc71e49 --- /dev/null +++ b/src/components/error-boundary.jsx @@ -0,0 +1,24 @@ +import React from "react"; +import { ErrorBoundary as ErrorBoundaryHelper } from "react-error-boundary"; +import { + Alert, + AlertIcon, + AlertTitle, + AlertDescription, +} from "@chakra-ui/react"; + +export function ErrorFallback({ error, resetErrorBoundary }) { + return ( + + + Something went wrong + {error.message} + + ); +} + +export const ErrorBoundary = ({ children, ...props }) => ( + + {children} + +); diff --git a/src/components/wait-for-relays.jsx b/src/components/wait-for-relays.jsx new file mode 100644 index 000000000..4f9ee6b61 --- /dev/null +++ b/src/components/wait-for-relays.jsx @@ -0,0 +1,18 @@ +import { Spinner } from "@chakra-ui/react"; +import React, { useEffect, useState } from "react"; +import { getAllActive } from "../services/relays"; + +export const WaitForRelays = ({ min, children }) => { + const [hide, setHide] = useState(true); + + useEffect(() => { + const i = setInterval(async () => { + if ((await getAllActive()).length > 0) { + setHide(false); + clearInterval(i); + } + }, 1000); + }, [setHide]); + + return hide ? : <>{children}; +}; diff --git a/src/helpers/signal.js b/src/helpers/signal.js index 2391c5a4e..c5ddfd07a 100644 --- a/src/helpers/signal.js +++ b/src/helpers/signal.js @@ -1,21 +1,25 @@ export class Signal { - listeners = new Set(); + listeners = []; connections = new Set(); emit(event) { - for (const fn of this.listeners) { - fn(event); + for (const [fn, ctx] of this.listeners) { + if (ctx) { + fn.apply(ctx, [event]); + } else fn(event); } for (const signal of this.connections) { signal.emit(event); } } - addListener(fn) { - this.listeners.add(fn); + addListener(fn, ctx) { + this.listeners.push([fn, ctx]); } - removeListener(fn) { - this.listeners.delete(fn); + removeListener(fn, ctx) { + this.listeners = this.listeners.filter( + (listener) => listener.fn !== fn && listener.ctx !== ctx + ); } addConnection(signal) { this.connections.add(signal); diff --git a/src/hooks/use-signal.js b/src/hooks/use-signal.js new file mode 100644 index 000000000..f789f3e3d --- /dev/null +++ b/src/hooks/use-signal.js @@ -0,0 +1,9 @@ +import { useCallback, useEffect } from "react"; + +export function useSignal(signal, fn, watch = []) { + const listener = useCallback(fn, watch); + useEffect(() => { + signal.addListener(listener); + return () => signal.removeListener(listener); + }, [listener]); +} diff --git a/src/index.jsx b/src/index.jsx index 939c02479..155cd8d75 100644 --- a/src/index.jsx +++ b/src/index.jsx @@ -1,17 +1,6 @@ import React from "react"; import { createRoot } from "react-dom/client"; - -import { connectToRelays, subscribeToAuthor, onEvent } from "./services/relays"; +import { App } from "./app"; const root = createRoot(document.getElementById("root")); -root.render(

Hello, world!

); - -await connectToRelays(); - -const self = "266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5"; - -subscribeToAuthor(self); - -onEvent.addListener((event) => { - console.log(event); -}); +root.render(); diff --git a/src/services/db.js b/src/services/db/index.js similarity index 72% rename from src/services/db.js rename to src/services/db/index.js index 6cf9a5cd8..e8c506758 100644 --- a/src/services/db.js +++ b/src/services/db/index.js @@ -6,8 +6,4 @@ const db = await openDB("storage", version, { upgrade, }); -export async function getUsername(id) { - db.get("users", id); -} - export default db; diff --git a/src/services/migrations/index.js b/src/services/db/migrations/index.js similarity index 89% rename from src/services/migrations/index.js rename to src/services/db/migrations/index.js index f05249582..4c5208999 100644 --- a/src/services/migrations/index.js +++ b/src/services/db/migrations/index.js @@ -4,6 +4,9 @@ const MIGRATIONS = [ db.createObjectStore("users", { keyPath: "pubkey", }); + db.createObjectStore("contacts", { + keyPath: "pubkey", + }); db.createObjectStore("settings"); // setup data diff --git a/src/services/relays.js b/src/services/relays/index.js similarity index 51% rename from src/services/relays.js rename to src/services/relays/index.js index e90354bb0..f3f0b2a7b 100644 --- a/src/services/relays.js +++ b/src/services/relays/index.js @@ -1,46 +1,6 @@ -import { Signal } from "../helpers/signal"; -import { getRelays } from "./settings"; - -class RelayConnection { - constructor(url) { - this.ws = new WebSocket(url); - this.url = url; - - this.onEvent = new Signal(); - this.onNotice = new Signal(); - - this.ws.onclose = this.handleClose.bind(this); - this.ws.onmessage = this.handleMessage.bind(this); - } - - send(json) { - this.ws.send(JSON.stringify(json)); - } - - get connected() { - return this.ws.readyState === WebSocket.OPEN; - } - get state() { - return this.ws.readyState; - } - - handleMessage(event) { - const data = JSON.parse(event.data); - const type = data[0]; - - switch (type) { - case "EVENT": - this.onEvent.emit({ subId: data[1], body: data[2] }); - break; - case "NOTICE": - this.onEvent.emit({ message: data[1] }); - break; - } - } - handleClose() { - console.log(this.url, "closed"); - } -} +import { Signal } from "../../helpers/signal"; +import { getRelays } from "../settings"; +import { Relay } from "./relay"; const connections = new Map(); export const onEvent = new Signal(); @@ -51,7 +11,7 @@ export function getAllActive() { export async function connectToRelay(url) { if (!connections.has(url)) { - const relay = new RelayConnection(url); + const relay = new Relay(url); connections.set(url, relay); // send all onEvent events to the main onEvent signal diff --git a/src/services/relays/relay.js b/src/services/relays/relay.js new file mode 100644 index 000000000..e5fe19aa6 --- /dev/null +++ b/src/services/relays/relay.js @@ -0,0 +1,66 @@ +import { Signal } from "../../helpers/signal"; + +export class Relay { + constructor(url) { + this.url = url; + + this.onOpen = new Signal(); + this.onClose = new Signal(); + this.onEvent = new Signal(); + this.onNotice = new Signal(); + + this.connect(); + } + + connect() { + if (this.connected || this.connecting) return; + this.ws = new WebSocket(this.url); + + this.ws.onopen = this.handleOpen.bind(this); + this.ws.onclose = this.handleClose.bind(this); + this.ws.onmessage = this.handleMessage.bind(this); + } + + send(json) { + if (this.connected) { + this.ws.send(JSON.stringify(json)); + } + } + + get connected() { + return this.ws?.readyState === WebSocket.OPEN; + } + get connecting() { + return this.ws?.readyState === WebSocket.CONNECTING; + } + get state() { + return this.ws?.readyState; + } + + handleMessage(event) { + const data = JSON.parse(event.data); + const type = data[0]; + + switch (type) { + case "EVENT": + this.onEvent.emit({ subId: data[1], body: data[2] }); + break; + case "NOTICE": + this.onEvent.emit({ message: data[1] }); + break; + } + } + handleOpen() { + console.log(this.url, "connected"); + + this.onOpen.emit(); + } + handleClose() { + console.log(this.url, "reconnecting in 10s"); + + this.onClose.emit(); + setTimeout(() => { + this.connect(); + }, 1000 * 10); + } +} diff --git a/src/services/subscriptions.js b/src/services/subscriptions.js new file mode 100644 index 000000000..1ba33c826 --- /dev/null +++ b/src/services/subscriptions.js @@ -0,0 +1,44 @@ +import { Signal } from "../helpers/signal"; +import { getAllActive } from "./relays"; + +export class Subscription { + static OPEN = "open"; + static CLOSED = "closed"; + + constructor(relays, query) { + this.id = String(Math.floor(Math.random() * 1000000)); + this.relays = relays; + this.status = Subscription.OPEN; + + this.onEvent = new Signal(); + + for (const relay of this.relays) { + relay.onEvent.addListener(this.handleEvent, this); + } + + this.send(["REQ", this.id, query]); + } + handleEvent(event) { + if (event.subId === this.id) { + this.onEvent.emit(event.body); + } + } + send(message) { + for (const relay of this.relays) { + relay.send(message); + } + } + close() { + this.status = Subscription.CLOSED; + this.send(["CLOSE", this.id]); + + for (const relay of this.relays) { + relay.onEvent.removeListener(this.handleEvent, this); + } + } +} + +export function createSubscription(query) { + const relays = getAllActive(); + return new Subscription(relays, query); +} diff --git a/src/views/home.jsx b/src/views/home.jsx new file mode 100644 index 000000000..28a321d31 --- /dev/null +++ b/src/views/home.jsx @@ -0,0 +1,25 @@ +import React from "react"; +import { VStack, Heading } from "@chakra-ui/react"; +import { Link } from "react-router-dom"; + +export const HomeView = () => { + return ( + + personal nostr client + + There are many benefits to a joint design and development system. Not + only does it bring benefits to the design team, but it also brings + benefits to engineering teams. It makes sure that our experiences have a + consistent look and feel, not just in our design specs, but in + production. + + Settings + + self + + + gigi + + + ); +}; diff --git a/src/views/settings.jsx b/src/views/settings.jsx new file mode 100644 index 000000000..0951832bd --- /dev/null +++ b/src/views/settings.jsx @@ -0,0 +1,54 @@ +import { + Button, + FormControl, + FormHelperText, + FormLabel, + Stack, + Textarea, +} from "@chakra-ui/react"; +import React, { useEffect, useState } from "react"; +import * as settings from "../services/settings"; + +export const SettingsView = () => { + const [relayUrls, setRelayUrls] = useState(""); + + const handleSubmit = (event) => { + event.preventDefault(); + const relays = relayUrls + .split("\n") + .filter(Boolean) + .map((url) => url.trim()); + if (relays.length > 0) { + settings.setRelays(relays); + } + }; + const resetForm = async () => { + const urls = await settings.getRelays(); + setRelayUrls(urls.join("\n")); + }; + + useEffect(() => { + resetForm(); + }, []); + + return ( +
+ + Relays +