Track follows and relays

This commit is contained in:
Kieran 2022-12-28 23:28:28 +00:00
parent 99410dd8c2
commit 57418b6355
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
12 changed files with 172 additions and 120 deletions

View File

@ -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)} />)}
</>

View File

@ -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];

View File

@ -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);
}

View File

@ -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]);

View File

@ -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} />)}
</>
)
}

View File

@ -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>
);
}

View 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 {};
}

View File

@ -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 };
}

View File

@ -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);

View File

@ -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;

View File

@ -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

View File

@ -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;