Track follows and relays
This commit is contained in:
parent
99410dd8c2
commit
57418b6355
@ -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 ?
|
||||
<NoteGhost text={`Loading... (${notes.length} events loaded)`}/>
|
||||
: <Note data-ev={root} reactions={reactions(root?.Id)} />}
|
||||
{thisNote ? <Note data-ev={thisNote} reactions={reactions(thisNote.Id)}/> : null}
|
||||
{thisNote && !thisIsRootNote ? <Note data-ev={thisNote} reactions={reactions(thisNote.Id)}/> : null}
|
||||
<h4>Other Replies</h4>
|
||||
{repliesToRoot?.map(a => <Note key={a.Id} data-ev={a} reactions={reactions(a.Id)} />)}
|
||||
</>
|
||||
|
@ -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];
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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]);
|
||||
|
@ -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 (
|
||||
<div className="profile">
|
||||
<div>
|
||||
<div style={{ backgroundImage: `url(${picture})` }} className="avatar">
|
||||
<div className="edit">
|
||||
<div>Edit</div>
|
||||
function details() {
|
||||
return (
|
||||
<>
|
||||
<div className="form-group">
|
||||
<div>Name:</div>
|
||||
<div>
|
||||
{name}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
{isMe ? editor() : null}
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<div>About:</div>
|
||||
<div>
|
||||
{about}
|
||||
</div>
|
||||
</div>
|
||||
{website ?
|
||||
<div className="form-group">
|
||||
<div>Website:</div>
|
||||
<div>
|
||||
{website}
|
||||
</div>
|
||||
</div> : null}
|
||||
<div className="form-group">
|
||||
<div>NIP-05:</div>
|
||||
<div>
|
||||
{nip05}
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<div>Lightning Address:</div>
|
||||
<div>
|
||||
{lud16}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
</div>
|
||||
return (
|
||||
<>
|
||||
<div className="profile">
|
||||
<div>
|
||||
<div style={{ backgroundImage: `url(${picture})` }} className="avatar">
|
||||
{isMe ?
|
||||
<div className="edit">
|
||||
<div>Edit</div>
|
||||
</div>
|
||||
: null
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
{isMe ? editor() : details()}
|
||||
</div>
|
||||
</div>
|
||||
<h4>Notes</h4>
|
||||
{notes?.sort((a, b) => b.created_at - a.created_at).map(a => <Note key={a.id} data={a} />)}
|
||||
</>
|
||||
)
|
||||
}
|
@ -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 (
|
||||
<div className="timeline">
|
||||
{sorted.map(e => <Note key={e.id} data={e}/>)}
|
||||
{notes?.sort((a, b) => b.created_at - a.created_at).map(e => <Note key={e.id} data={e} />)}
|
||||
</div>
|
||||
);
|
||||
}
|
37
src/pages/feed/LoginFeed.js
Normal file
37
src/pages/feed/LoginFeed.js
Normal file
@ -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 {};
|
||||
}
|
@ -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 };
|
||||
}
|
@ -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);
|
||||
|
@ -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;
|
@ -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
|
||||
|
@ -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;
|
Loading…
x
Reference in New Issue
Block a user