diff --git a/src/element/Thread.js b/src/element/Thread.js index 5bc27830..41539f03 100644 --- a/src/element/Thread.js +++ b/src/element/Thread.js @@ -17,15 +17,16 @@ export default function Thread(props) { } const repliesToRoot = notes?. - filter(a => a.GetThread()?.Root !== null && a.Kind === EventKind.TextNote && a.Id !== thisEvent) + filter(a => a.GetThread()?.Root !== null && a.Kind === EventKind.TextNote && a.Id !== thisEvent && a.Id !== root?.Id) .sort((a, b) => a.CreatedAt - b.CreatedAt); const thisNote = notes?.find(a => a.Id === thisEvent); + const thisIsRootNote = thisNote?.Id === root?.Id; return ( <> {root === undefined ? : } - {thisNote ? : null} + {thisNote && !thisIsRootNote ? : null}

Other Replies

{repliesToRoot?.map(a => )} diff --git a/src/nostr/Connection.js b/src/nostr/Connection.js index fc90c440..7a1dfec4 100644 --- a/src/nostr/Connection.js +++ b/src/nostr/Connection.js @@ -1,26 +1,35 @@ import { Subscriptions } from "./Subscriptions"; import Event from "./Event"; +const DefaultConnectTimeout = 1000; + export default class Connection { - constructor(addr) { + constructor(addr, options) { this.Address = addr; this.Socket = null; this.Pending = []; this.Subscriptions = {}; + this.Read = options?.read || true; + this.Write = options?.write || true; + this.ConnectTimeout = DefaultConnectTimeout; this.Connect(); } Connect() { - this.Socket = new WebSocket(this.Address); - this.Socket.onopen = (e) => this.OnOpen(e); - this.Socket.onmessage = (e) => this.OnMessage(e); - this.Socket.onerror = (e) => this.OnError(e); - this.Socket.onclose = (e) => this.OnClose(e); + try { + this.Socket = new WebSocket(this.Address); + this.Socket.onopen = (e) => this.OnOpen(e); + this.Socket.onmessage = (e) => this.OnMessage(e); + this.Socket.onerror = (e) => this.OnError(e); + this.Socket.onclose = (e) => this.OnClose(e); + } catch (e) { + console.warn(`[${this.Address}] Connect failed!`); + } } OnOpen(e) { - console.log(`Opened connection to: ${this.Address}`); - console.log(e); + this.ConnectTimeout = DefaultConnectTimeout; + console.log(`[${this.Address}] Open!`); // send pending for (let p of this.Pending) { @@ -29,10 +38,11 @@ export default class Connection { } OnClose(e) { - console.log(`[${this.Address}] Closed: `, 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(); - }, 500); + }, this.ConnectTimeout); } OnMessage(e) { @@ -82,7 +92,7 @@ export default class Connection { */ AddSubscription(sub) { let subObj = sub.ToObject(); - if(Object.keys(subObj).length === 0) { + if (Object.keys(subObj).length === 0) { throw "CANNOT SEND EMPTY SUB - FIX ME"; } let req = ["REQ", sub.Id, subObj]; diff --git a/src/nostr/System.js b/src/nostr/System.js index c56ee950..f9787a5b 100644 --- a/src/nostr/System.js +++ b/src/nostr/System.js @@ -14,9 +14,9 @@ export class NostrSystem { * Connect to a NOSTR relay if not already connected * @param {string} address */ - ConnectToRelay(address) { + ConnectToRelay(address, options) { if (typeof this.Sockets[address] === "undefined") { - let c = new Connection(address); + let c = new Connection(address, options); for (let s of Object.values(this.Subscriptions)) { c.AddSubscription(s); } diff --git a/src/pages/Layout.js b/src/pages/Layout.js index ce1551af..2617209e 100644 --- a/src/pages/Layout.js +++ b/src/pages/Layout.js @@ -4,7 +4,8 @@ import { useNavigate } from "react-router-dom"; import { NostrContext } from ".." import ProfileImage from "../element/ProfileImage"; import { init } from "../state/Login"; -import useUsersStore from "./feed/UsersFeed"; +import useLoginFeed from "./feed/LoginFeed"; +import useUsersCache from "./feed/UsersFeed"; export default function Layout(props) { const dispatch = useDispatch(); @@ -12,12 +13,13 @@ export default function Layout(props) { const navigate = useNavigate(); const key = useSelector(s => s.login.publicKey); const relays = useSelector(s => s.login.relays); - const users = useUsersStore(); + useUsersCache(); + useLoginFeed(); useEffect(() => { if (system && relays) { - for (let r of relays) { - system.ConnectToRelay(r); + for (let [k, v] of Object.entries(relays)) { + system.ConnectToRelay(k, v); } } }, [relays, system]); diff --git a/src/pages/ProfilePage.js b/src/pages/ProfilePage.js index 37f9a9ba..4bb7c54c 100644 --- a/src/pages/ProfilePage.js +++ b/src/pages/ProfilePage.js @@ -2,10 +2,12 @@ import "./ProfilePage.css"; import { useDispatch, useSelector } from "react-redux"; import { useParams } from "react-router-dom"; import useProfile from "./feed/ProfileFeed"; -import { useContext, useEffect, useState } from "react"; +import { useEffect, useState } from "react"; import { resetProfile } from "../state/Users"; import Nostrich from "../nostrich.jpg"; import useEventPublisher from "./feed/EventPublisher"; +import useTimelineFeed from "./feed/TimelineFeed"; +import Note from "../element/Note"; export default function ProfilePage() { const dispatch = useDispatch(); @@ -13,6 +15,7 @@ export default function ProfilePage() { const id = params.id; const user = useProfile(id); const publisher = useEventPublisher(); + const { notes } = useTimelineFeed([id]); const loginPubKey = useSelector(s => s.login.publicKey); const isMe = loginPubKey === id; @@ -91,19 +94,63 @@ export default function ProfilePage() { ) } - return ( -
-
-
-
-
Edit
+ function details() { + return ( + <> +
+
Name:
+
+ {name}
-
-
- {isMe ? editor() : null} -
+
+
About:
+
+ {about} +
+
+ {website ? +
+
Website:
+
+ {website} +
+
: null} +
+
NIP-05:
+
+ {nip05} +
+
+
+
Lightning Address:
+
+ {lud16} +
+
+ + ) + } -
+ return ( + <> +
+
+
+ {isMe ? +
+
Edit
+
+ : null + } +
+
+
+ {isMe ? editor() : details()} +
+
+

Notes

+ {notes?.sort((a, b) => b.created_at - a.created_at).map(a => )} + ) } \ No newline at end of file diff --git a/src/pages/Timeline.js b/src/pages/Timeline.js index 9d4360b3..645923cb 100644 --- a/src/pages/Timeline.js +++ b/src/pages/Timeline.js @@ -1,16 +1,14 @@ +import { useSelector } from "react-redux"; import Note from "../element/Note"; import useTimelineFeed from "./feed/TimelineFeed"; export default function Timeline() { - const { notes } = useTimelineFeed(); - - const sorted = [ - ...(notes || []) - ].sort((a, b) => b.created_at - a.created_at); + const follows = useSelector(a => a.login.follows) + const { notes } = useTimelineFeed(follows); return (
- {sorted.map(e => )} + {notes?.sort((a, b) => b.created_at - a.created_at).map(e => )}
); } \ No newline at end of file diff --git a/src/pages/feed/LoginFeed.js b/src/pages/feed/LoginFeed.js new file mode 100644 index 00000000..5ac3f604 --- /dev/null +++ b/src/pages/feed/LoginFeed.js @@ -0,0 +1,37 @@ +import { useContext, useEffect } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { NostrContext } from "../.."; +import Event from "../../nostr/Event"; +import EventKind from "../../nostr/EventKind"; +import { Subscriptions } from "../../nostr/Subscriptions"; +import { setFollows, setRelays } from "../../state/Login"; + +/** + * Managed loading data for the current logged in user + */ +export default function useLoginFeed() { + const dispatch = useDispatch(); + const system = useContext(NostrContext); + const pubKey = useSelector(s => s.login.publicKey); + + useEffect(() => { + if (system && pubKey) { + let sub = new Subscriptions(); + sub.Authors.add(pubKey); + sub.Kinds.add(EventKind.ContactList); + sub.OnEvent = (e) => { + let ev = Event.FromObject(e); + if (ev.Content !== "") { + let relays = JSON.parse(ev.Content); + dispatch(setRelays(relays)); + } + let pTags = ev.Tags.filter(a => a.Key === "p").map(a => a.PubKey); + dispatch(setFollows(pTags)); + } + system.AddSubscription(sub); + return () => system.RemoveSubscription(sub.Id); + } + }, [system, pubKey]); + + return {}; +} \ No newline at end of file diff --git a/src/pages/feed/TimelineFeed.js b/src/pages/feed/TimelineFeed.js index 96c6c5ae..21556890 100644 --- a/src/pages/feed/TimelineFeed.js +++ b/src/pages/feed/TimelineFeed.js @@ -1,55 +1,38 @@ -import { useContext, useEffect } from "react"; -import { useDispatch, useSelector } from "react-redux"; +import { useContext, useEffect, useState } from "react"; import { NostrContext } from "../../index"; import EventKind from "../../nostr/EventKind"; import { Subscriptions } from "../../nostr/Subscriptions"; -import { addNote } from "../../state/Timeline"; -import { addPubKey } from "../../state/Users"; -export default function useTimelineFeed(opt) { +export default function useTimelineFeed(pubKeys) { const system = useContext(NostrContext); - const dispatch = useDispatch(); - const follows = useSelector(s => s.timeline?.follows); - const notes = useSelector(s => s.timeline?.notes); - const pubKeys = useSelector(s => s.users.pubKeys); - - const options = { - - ...opt - }; - - function trackPubKeys(keys) { - for (let pk of keys) { - if (!pubKeys.includes(pk)) { - dispatch(addPubKey(pk)); - } - } - } + const [notes, setNotes] = useState([]); useEffect(() => { - if (follows.length > 0) { + if (system && pubKeys.length > 0) { const sub = new Subscriptions(); - sub.Authors = new Set(follows); + sub.Authors = new Set(pubKeys); sub.Kinds.add(EventKind.TextNote); sub.Limit = 10; sub.OnEvent = (e) => { - dispatch(addNote(e)); + setNotes(n => { + if (Array.isArray(n) && !n.some(a => a.id === e.id)) { + return [ + ...n, + e + ] + } else { + return n; + } + }); }; - trackPubKeys(follows); - if (system) { - system.AddSubscription(sub); - return () => system.RemoveSubscription(sub.Id); - } + system.AddSubscription(sub); + return () => { + system.RemoveSubscription(sub.Id); + }; } - }, [follows]); + }, [system, pubKeys]); - useEffect(() => { - for (let n of notes) { - - } - }, [notes]); - - return { notes, follows }; + return { notes }; } \ No newline at end of file diff --git a/src/pages/feed/UsersFeed.js b/src/pages/feed/UsersFeed.js index 7995b178..f2d0a37b 100644 --- a/src/pages/feed/UsersFeed.js +++ b/src/pages/feed/UsersFeed.js @@ -6,7 +6,7 @@ import EventKind from "../../nostr/EventKind"; import { Subscriptions } from "../../nostr/Subscriptions"; import { setUserData } from "../../state/Users"; -export default function useUsersStore() { +export default function useUsersCache() { const dispatch = useDispatch(); const system = useContext(NostrContext); const pKeys = useSelector(s => s.users.pubKeys); diff --git a/src/state/Login.js b/src/state/Login.js index 4aebf750..e272589c 100644 --- a/src/state/Login.js +++ b/src/state/Login.js @@ -2,12 +2,6 @@ import { createSlice } from '@reduxjs/toolkit' import * as secp from '@noble/secp256k1'; const PrivateKeyItem = "secret"; -const RelayList = "relays"; -const DefaultRelays = JSON.stringify([ - "wss://nostr-pub.wellorder.net", - "wss://relay.damus.io", - "wss://beta.nostr.v0l.io" -]); const LoginSlice = createSlice({ name: "Login", @@ -25,21 +19,35 @@ const LoginSlice = createSlice({ /** * Configured relays for this user */ - relays: [] + relays: {}, + + /** + * A list of pubkeys this user follows + */ + follows: [] }, reducers: { init: (state) => { state.privateKey = window.localStorage.getItem(PrivateKeyItem); - if(state.privateKey) { + if (state.privateKey) { state.publicKey = secp.utils.bytesToHex(secp.schnorr.getPublicKey(state.privateKey, true)); } - state.relays = JSON.parse(window.localStorage.getItem(RelayList) || DefaultRelays); + state.relays = { + "wss://beta.nostr.v0l.io": { read: true, write: true }, + "wss://nostr.v0l.io": { read: true, write: true } + }; }, setPrivateKey: (state, action) => { state.privateKey = action.payload; window.localStorage.setItem(PrivateKeyItem, action.payload); state.publicKey = secp.utils.bytesToHex(secp.schnorr.getPublicKey(action.payload, true)); }, + setRelays: (state, action) => { + state.relays = action.payload; + }, + setFollows: (state, action) => { + state.follows = action.payload; + }, logout: (state) => { state.privateKey = null; window.localStorage.removeItem(PrivateKeyItem); @@ -47,5 +55,5 @@ const LoginSlice = createSlice({ } }); -export const { init, setPrivateKey, logout } = LoginSlice.actions; +export const { init, setPrivateKey, setRelays, setFollows, logout } = LoginSlice.actions; export const reducer = LoginSlice.reducer; \ No newline at end of file diff --git a/src/state/Store.js b/src/state/Store.js index 9046a36e..64bab662 100644 --- a/src/state/Store.js +++ b/src/state/Store.js @@ -1,12 +1,10 @@ import { configureStore } from "@reduxjs/toolkit"; -import { reducer as TimelineReducer } from "./Timeline"; import { reducer as UsersReducer } from "./Users"; import { reducer as LoginReducer } from "./Login"; import { reducer as ThreadReducer } from "./Thread"; const Store = configureStore({ reducer: { - timeline: TimelineReducer, users: UsersReducer, login: LoginReducer, thread: ThreadReducer diff --git a/src/state/Timeline.js b/src/state/Timeline.js deleted file mode 100644 index ed77519b..00000000 --- a/src/state/Timeline.js +++ /dev/null @@ -1,32 +0,0 @@ -import { createSlice } from '@reduxjs/toolkit' - -const TimelineSlice = createSlice({ - name: "Timeline", - initialState: { - notes: [], - follows: ["217e3d8b61c087b10422427e114737a4a4a4b1e15f22301fb4b07e1f33204d7c", "82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2", "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245", "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d"] - }, - reducers: { - setNotes: (state, action) => { - state.notes = action.payload; - }, - addNote: (state, action) => { - if (!state.notes.some(n => n.id === action.payload.id)) { - let tmp = new Set(state.notes); - tmp.add(action.payload); - state.notes = Array.from(tmp); - } - }, - setFollowers: (state, action) => { - state.follows = action.payload; - }, - addFollower: (state, action) => { - let tmp = new Set(state.follows); - tmp.add(action.payload); - state.follows = Array.from(tmp); - } - } -}); - -export const { setNotes, addNote, setFollowers, addFollower } = TimelineSlice.actions; -export const reducer = TimelineSlice.reducer; \ No newline at end of file