Optimize subs and many other things

This commit is contained in:
Kieran 2022-12-30 23:35:02 +00:00
parent 560d827f87
commit 75a6a34900
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
23 changed files with 291 additions and 247 deletions

View File

@ -13,7 +13,6 @@
"qr-code-styling": "^1.6.0-rc.1", "qr-code-styling": "^1.6.0-rc.1",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-modal": "^3.16.1",
"react-redux": "^8.0.5", "react-redux": "^8.0.5",
"react-router-dom": "^6.5.0", "react-router-dom": "^6.5.0",
"react-scripts": "5.0.1", "react-scripts": "5.0.1",

View File

@ -8,7 +8,7 @@
<meta name="theme-color" content="#000000" /> <meta name="theme-color" content="#000000" />
<meta name="description" content="Fast nostr web ui" /> <meta name="description" content="Fast nostr web ui" />
<meta http-equiv="Content-Security-Policy" <meta http-equiv="Content-Security-Policy"
content="default-src 'self'; child-src 'none'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; connect-src wss://* 'self'; img-src *; font-src https://fonts.gstatic.com;" /> content="default-src 'self'; child-src 'none'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; connect-src wss://* 'self'; img-src * data:; font-src https://fonts.gstatic.com;" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/nostrich_512.png" /> <link rel="apple-touch-icon" href="%PUBLIC_URL%/nostrich_512.png" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />

11
src/element/Modal.css Normal file
View File

@ -0,0 +1,11 @@
.modal {
width: 100vw;
height: 100vh;
position: absolute;
top: 0;
left: 0;
background-color: rgba(0,0,0, 0.8);
display: flex;
justify-content: center;
align-items: center;
}

18
src/element/Modal.js Normal file
View File

@ -0,0 +1,18 @@
import "./Modal.css";
import { useEffect } from "react"
export default function Modal(props) {
const onClose = props.onClose || (() => {});
useEffect(() => {
window.scrollTo(0, 0);
document.body.classList.add("scroll-lock");
return () => document.body.classList.remove("scroll-lock");
}, []);
return (
<div className="modal" onClick={(e) => onClose(e)}>
{props.children}
</div>
)
}

View File

@ -1,5 +1,5 @@
import "./Note.css"; import "./Note.css";
import { useEffect, useState } from "react"; import { useState } from "react";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import moment from "moment"; import moment from "moment";
import { Link, useNavigate } from "react-router-dom"; import { Link, useNavigate } from "react-router-dom";
@ -11,9 +11,9 @@ import ProfileImage from "./ProfileImage";
import useEventPublisher from "../feed/EventPublisher"; import useEventPublisher from "../feed/EventPublisher";
import { NoteCreator } from "./NoteCreator"; import { NoteCreator } from "./NoteCreator";
const UrlRegex = /((?:http|ftp|https):\/\/(?:[\w+?\.\w+])+(?:[a-zA-Z0-9\~\!\@\#\$\%\^\&\*\(\)_\-\=\+\\\/\?\.\:\;\'\,]*)?)/; const UrlRegex = /((?:http|ftp|https):\/\/(?:[\w+?\.\w+])+(?:[a-zA-Z0-9\~\!\@\#\$\%\^\&\*\(\)_\-\=\+\\\/\?\.\:\;\'\,]*)?)/i;
const FileExtensionRegex = /\.([\w]+)$/; const FileExtensionRegex = /\.([\w]+)$/i;
const MentionRegex = /(#\[\d+\])/g; const MentionRegex = /(#\[\d+\])/gi;
export default function Note(props) { export default function Note(props) {
const navigate = useNavigate(); const navigate = useNavigate();
@ -21,22 +21,10 @@ export default function Note(props) {
const dataEvent = props["data-ev"]; const dataEvent = props["data-ev"];
const reactions = props.reactions; const reactions = props.reactions;
const publisher = useEventPublisher(); const publisher = useEventPublisher();
const [sig, setSig] = useState(false);
const [showReply, setShowReply] = useState(false); const [showReply, setShowReply] = useState(false);
const users = useSelector(s => s.users?.users); const users = useSelector(s => s.users?.users);
const ev = dataEvent ?? Event.FromObject(data); const ev = dataEvent ?? Event.FromObject(data);
useEffect(() => {
if (sig === false) {
verifyEvent();
}
}, []);
async function verifyEvent() {
let res = await ev.Verify();
setSig(res);
}
function goToEvent(e, id) { function goToEvent(e, id) {
if (!window.location.pathname.startsWith("/e/")) { if (!window.location.pathname.startsWith("/e/")) {
e.stopPropagation(); e.stopPropagation();
@ -66,7 +54,7 @@ export default function Note(props) {
return urlBody.map(a => { return urlBody.map(a => {
if (a.startsWith("http")) { if (a.startsWith("http")) {
let url = new URL(a); let url = new URL(a);
let ext = url.pathname.match(FileExtensionRegex); let ext = url.pathname.toLowerCase().match(FileExtensionRegex);
if (ext) { if (ext) {
switch (ext[1]) { switch (ext[1]) {
case "gif": case "gif":

View File

@ -1,12 +1,10 @@
import { useContext } from "react";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
import { NostrContext } from ".."; import { System } from "..";
import Event from "../nostr/Event"; import Event from "../nostr/Event";
import EventKind from "../nostr/EventKind"; import EventKind from "../nostr/EventKind";
import Tag from "../nostr/Tag"; import Tag from "../nostr/Tag";
export default function useEventPublisher() { export default function useEventPublisher() {
const system = useContext(NostrContext);
const pubKey = useSelector(s => s.login.publicKey); const pubKey = useSelector(s => s.login.publicKey);
const privKey = useSelector(s => s.login.privateKey); const privKey = useSelector(s => s.login.privateKey);
const nip07 = useSelector(s => s.login.nip07); const nip07 = useSelector(s => s.login.nip07);
@ -33,7 +31,7 @@ export default function useEventPublisher() {
return { return {
broadcast: (ev) => { broadcast: (ev) => {
console.debug("Sending event: ", ev); console.debug("Sending event: ", ev);
system.BroadcastEvent(ev); System.BroadcastEvent(ev);
}, },
metadata: async (obj) => { metadata: async (obj) => {
let ev = Event.ForPubKey(pubKey); let ev = Event.ForPubKey(pubKey);

View File

@ -1,37 +1,53 @@
import { useContext, useEffect } from "react"; import { useContext, useEffect } from "react";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { NostrContext } from ".."; import { System } from "..";
import Event from "../nostr/Event"; import Event from "../nostr/Event";
import EventKind from "../nostr/EventKind"; import EventKind from "../nostr/EventKind";
import { Subscriptions } from "../nostr/Subscriptions"; import { Subscriptions } from "../nostr/Subscriptions";
import { setFollows, setRelays } from "../state/Login"; import { addNotifications, setFollows, setRelays } from "../state/Login";
/** /**
* Managed loading data for the current logged in user * Managed loading data for the current logged in user
*/ */
export default function useLoginFeed() { export default function useLoginFeed() {
const dispatch = useDispatch(); const dispatch = useDispatch();
const system = useContext(NostrContext);
const pubKey = useSelector(s => s.login.publicKey); const pubKey = useSelector(s => s.login.publicKey);
useEffect(() => { useEffect(() => {
if (system && pubKey) { if (pubKey) {
let sub = new Subscriptions(); let sub = new Subscriptions();
sub.Id = "login";
sub.Authors.add(pubKey); sub.Authors.add(pubKey);
sub.Kinds.add(EventKind.ContactList); sub.Kinds.add(EventKind.ContactList);
let notifications = new Subscriptions();
notifications.Kinds.add(EventKind.TextNote);
notifications.Kinds.add(EventKind.Reaction);
notifications.PTags.add(pubKey);
sub.AddSubscription(notifications);
sub.OnEvent = (e) => { sub.OnEvent = (e) => {
let ev = Event.FromObject(e); let ev = Event.FromObject(e);
if (ev.Content !== "") { switch (ev.Kind) {
let relays = JSON.parse(ev.Content); case EventKind.ContactList: {
dispatch(setRelays(relays)); 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));
break;
}
default: {
dispatch(addNotifications(ev.ToObject()));
break;
}
} }
let pTags = ev.Tags.filter(a => a.Key === "p").map(a => a.PubKey);
dispatch(setFollows(pTags));
} }
system.AddSubscription(sub); System.AddSubscription(sub);
return () => system.RemoveSubscription(sub.Id); return () => System.RemoveSubscription(sub.Id);
} }
}, [system, pubKey]); }, [pubKey]);
return {}; return {};
} }

View File

@ -1,19 +1,17 @@
import { useContext, useEffect } from "react"; import { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { NostrContext } from "..";
import { addPubKey } from "../state/Users"; import { addPubKey } from "../state/Users";
export default function useProfile(pubKey) { export default function useProfile(pubKey) {
const dispatch = useDispatch(); const dispatch = useDispatch();
const system = useContext(NostrContext);
const user = useSelector(s => s.users.users[pubKey]); const user = useSelector(s => s.users.users[pubKey]);
const pubKeys = useSelector(s => s.users.pubKeys); const pubKeys = useSelector(s => s.users.pubKeys);
useEffect(() => { useEffect(() => {
if (system && pubKey !== "" && !pubKeys.includes(pubKey)) { if (pubKey !== "" && !pubKeys.includes(pubKey)) {
dispatch(addPubKey(pubKey)); dispatch(addPubKey(pubKey));
} }
}, [system]); }, [pubKey]);
return user; return user;
} }

51
src/feed/Subscription.js Normal file
View File

@ -0,0 +1,51 @@
import { useEffect, useMemo, useState } from "react";
import { System } from "..";
import { Subscriptions } from "../nostr/Subscriptions";
/**
*
* @param {Subscriptions} sub
* @param {any} opt
* @returns
*/
export default function useSubscription(sub, opt) {
const [notes, setNotes] = useState([]);
const options = {
leaveOpen: false,
...opt
};
useEffect(() => {
if (sub) {
sub.OnEvent = (e) => {
setNotes(n => {
if (Array.isArray(n) && !n.some(a => a.id === e.id)) {
return [
...n,
e
]
} else {
return n;
}
});
};
if (!options.leaveOpen) {
sub.OnEnd = (c) => {
c.RemoveSubscription(sub.Id);
if (sub.IsFinished()) {
System.RemoveSubscription(sub.Id);
}
};
}
System.AddSubscription(sub);
return () => {
System.RemoveSubscription(sub.Id);
};
}
}, [sub]);
return { notes, sub };
}

View File

@ -1,79 +1,56 @@
import { useContext, useEffect, useState } from "react"; import { useMemo } from "react";
import { useDispatch, useSelector } from "react-redux"; import EventKind from "../nostr/EventKind";
import { NostrContext } from "..";
import Event from "../nostr/Event";
import { Subscriptions } from "../nostr/Subscriptions"; import { Subscriptions } from "../nostr/Subscriptions";
import { addNote, reset } from "../state/Thread"; import useSubscription from "./Subscription";
import { addPubKey } from "../state/Users";
export default function useThreadFeed(id) { export default function useThreadFeed(id) {
const dispatch = useDispatch(); const sub = useMemo(() => {
const system = useContext(NostrContext); const thisSub = new Subscriptions();
const notes = useSelector(s => s.thread.notes); thisSub.Id = "thread";
const [thisLoaded, setThisLoaded] = useState(false); thisSub.Ids.add(id);
// track profiles // get replies to this event
useEffect(() => { const subRelated = new Subscriptions();
let keys = []; subRelated.Kinds.add(EventKind.Reaction);
for (let n of notes) { subRelated.Kinds.add(EventKind.TextNote);
if (n.pubkey) { subRelated.ETags = thisSub.Ids;
keys.push(n.pubkey); thisSub.AddSubscription(subRelated);
return thisSub;
}, [id]);
const main = useSubscription(sub, { leaveOpen: true });
const relatedThisSub = useMemo(() => {
let thisNote = main.notes.find(a => a.id === id);
if (thisNote) {
let otherSubs = new Subscriptions();
otherSubs.Id = "thread-related";
for (let e of thisNote.tags.filter(a => a[0] === "e")) {
otherSubs.Ids.add(e[1]);
} }
for (let t of n.tags) { // no #e skip related
if (t[0] === "p" && t[1]) { if (otherSubs.Ids.size === 0) {
keys.push(t[1]); return null;
}
} }
let relatedSubs = new Subscriptions();
relatedSubs.Kinds.add(EventKind.Reaction);
relatedSubs.Kinds.add(EventKind.TextNote);
relatedSubs.ETags = otherSubs.Ids;
otherSubs.AddSubscription(relatedSubs);
return otherSubs;
} }
}, [main.notes]);
dispatch(addPubKey(keys)); const others = useSubscription(relatedThisSub, { leaveOpen: true });
}, [notes]);
useEffect(() => { return {
if (system) { notes: [
let sub = new Subscriptions(); ...main.notes,
let thisNote = notes.find(a => a.id === id); ...others.notes
if (thisNote && !thisLoaded) { ]
console.debug(notes); };
setThisLoaded(true);
let thisNote = Event.FromObject(notes[0]);
let thread = thisNote.GetThread();
if (thread !== null) {
if (thread.ReplyTo) {
sub.Ids.add(thread.ReplyTo.Event);
}
if (thread.Root) {
sub.Ids.add(thread.Root.Event);
}
for (let m of thread.Mentions) {
sub.Ids.add(m.Event);
}
} else {
// this event is a root note, no other notes need to be loaded
return;
}
} else if (notes.length === 0) {
sub.Ids.add(id);
} else {
return;
}
// get replies to this event
let subRelated = new Subscriptions();
subRelated.ETags = sub.Ids;
sub.AddSubscription(subRelated);
sub.OnEvent = (e) => {
dispatch(addNote(e));
};
system.AddSubscription(sub);
}
}, [system, notes]);
useEffect(() => {
dispatch(reset());
}, []);
return { notes };
} }

View File

@ -1,38 +1,21 @@
import { useContext, useEffect, useState } from "react"; import { useCallback, useMemo } from "react";
import { NostrContext } from "..";
import EventKind from "../nostr/EventKind"; import EventKind from "../nostr/EventKind";
import { Subscriptions } from "../nostr/Subscriptions"; import { Subscriptions } from "../nostr/Subscriptions";
import useSubscription from "./Subscription";
export default function useTimelineFeed(pubKeys) { export default function useTimelineFeed(pubKeys) {
const system = useContext(NostrContext); const sub = useMemo(() => {
const [notes, setNotes] = useState([]); let sub = new Subscriptions();
sub.Id = "timeline";
sub.Authors = new Set(pubKeys);
sub.Kinds.add(EventKind.TextNote);
sub.Limit = 10;
useEffect(() => { return sub;
if (system && pubKeys.length > 0) { }, [pubKeys]);
const sub = new Subscriptions();
sub.Authors = new Set(pubKeys);
sub.Kinds.add(EventKind.TextNote);
sub.Limit = 10;
sub.OnEvent = (e) => { const { notes } = useSubscription(sub, { leaveOpen: true });
setNotes(n => {
if (Array.isArray(n) && !n.some(a => a.id === e.id)) {
return [
...n,
e
]
} else {
return n;
}
});
};
system.AddSubscription(sub);
return () => {
system.RemoveSubscription(sub.Id);
};
}
}, [system, pubKeys]);
return { notes }; return { notes };
} }

View File

@ -1,6 +1,6 @@
import { useContext, useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { NostrContext } from ".."; import { System } from "..";
import Event from "../nostr/Event"; import Event from "../nostr/Event";
import EventKind from "../nostr/EventKind"; import EventKind from "../nostr/EventKind";
import { Subscriptions } from "../nostr/Subscriptions"; import { Subscriptions } from "../nostr/Subscriptions";
@ -8,7 +8,6 @@ import { setUserData } from "../state/Users";
export default function useUsersCache() { export default function useUsersCache() {
const dispatch = useDispatch(); const dispatch = useDispatch();
const system = useContext(NostrContext);
const pKeys = useSelector(s => s.users.pubKeys); const pKeys = useSelector(s => s.users.pubKeys);
const users = useSelector(s => s.users.users); const users = useSelector(s => s.users.users);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
@ -35,15 +34,16 @@ export default function useUsersCache() {
if (needProfiles.length === 0) { if (needProfiles.length === 0) {
return; return;
} }
console.debug("Need profiles: ", needProfiles);
let sub = new Subscriptions(); let sub = new Subscriptions();
sub.Id = "profiles";
sub.Authors = new Set(needProfiles); sub.Authors = new Set(needProfiles);
sub.Kinds.add(EventKind.SetMetadata); sub.Kinds.add(EventKind.SetMetadata);
sub.OnEvent = (ev) => { sub.OnEvent = (ev) => {
dispatch(setUserData(mapEventToProfile(ev))); dispatch(setUserData(mapEventToProfile(ev)));
}; };
let events = await system.RequestSubscription(sub); let events = await System.RequestSubscription(sub);
let profiles = events let profiles = events
.filter(a => a.kind === EventKind.SetMetadata) .filter(a => a.kind === EventKind.SetMetadata)
.map(mapEventToProfile); .map(mapEventToProfile);
@ -61,14 +61,14 @@ export default function useUsersCache() {
} }
useEffect(() => { useEffect(() => {
if (system && pKeys.length > 0 && !loading) { if (pKeys.length > 0 && !loading) {
setLoading(true); setLoading(true);
getUsers() getUsers()
.catch(console.error) .catch(console.error)
.then(() => setLoading(false)); .then(() => setLoading(false));
} }
}, [system, pKeys, loading]); }, [pKeys, loading]);
return { users }; return { users };
} }

View File

@ -145,8 +145,9 @@ div.form-group > div:first-child {
margin-top: 5vh; margin-top: 5vh;
} }
.ReactModal__Body--open { body.scroll-lock {
overflow: hidden; overflow: hidden;
height: 100vh;
} }
.mr10 { .mr10 {

View File

@ -1,4 +1,3 @@
import './index.css'; import './index.css';
import React from 'react'; import React from 'react';
@ -17,26 +16,25 @@ import LoginPage from './pages/Login';
import ProfilePage from './pages/ProfilePage'; import ProfilePage from './pages/ProfilePage';
import RootPage from './pages/Root'; import RootPage from './pages/Root';
import Store from "./state/Store"; import Store from "./state/Store";
import NotificationsPage from './pages/Notifications';
const System = new NostrSystem(); export const System = new NostrSystem();
export const NostrContext = React.createContext();
const root = ReactDOM.createRoot(document.getElementById('root')); const root = ReactDOM.createRoot(document.getElementById('root'));
root.render( root.render(
<React.StrictMode> <React.StrictMode>
<NostrContext.Provider value={System}> <Provider store={Store}>
<Provider store={Store}> <Router>
<Router> <Layout>
<Layout> <Routes>
<Routes> <Route path="/" exact element={<RootPage />} />
<Route path="/" exact element={<RootPage/>} /> <Route path="/login" exact element={<LoginPage />} />
<Route path="/login" exact element={<LoginPage />} /> <Route path="/e/:id" exact element={<EventPage />} />
<Route path="/e/:id" exact element={<EventPage />} /> <Route path="/p/:id" exact element={<ProfilePage />} />
<Route path="/p/:id" exact element={<ProfilePage />} /> <Route path="/notifications" exact element={<NotificationsPage />} />
</Routes> </Routes>
</Layout> </Layout>
</Router> </Router>
</Provider> </Provider>
</NostrContext.Provider>
</React.StrictMode> </React.StrictMode>
); );

View File

@ -1,5 +1,6 @@
import { Subscriptions } from "./Subscriptions"; import { Subscriptions } from "./Subscriptions";
import Event from "./Event"; import Event from "./Event";
import * as secp from "@noble/secp256k1";
const DefaultConnectTimeout = 1000; const DefaultConnectTimeout = 1000;
@ -16,15 +17,11 @@ export default class Connection {
} }
Connect() { Connect() {
try { this.Socket = new WebSocket(this.Address);
this.Socket = new WebSocket(this.Address); this.Socket.onopen = (e) => this.OnOpen(e);
this.Socket.onopen = (e) => this.OnOpen(e); this.Socket.onmessage = (e) => this.OnMessage(e);
this.Socket.onmessage = (e) => this.OnMessage(e); this.Socket.onerror = (e) => this.OnError(e);
this.Socket.onerror = (e) => this.OnError(e); this.Socket.onclose = (e) => this.OnClose(e);
this.Socket.onclose = (e) => this.OnClose(e);
} catch (e) {
console.warn(`[${this.Address}] Connect failed!`);
}
} }
OnOpen(e) { OnOpen(e) {
@ -82,6 +79,9 @@ export default class Connection {
* @param {Event} e * @param {Event} e
*/ */
SendEvent(e) { SendEvent(e) {
if (!this.Write) {
return;
}
let req = ["EVENT", e.ToObject()]; let req = ["EVENT", e.ToObject()];
this._SendJson(req); this._SendJson(req);
} }
@ -91,11 +91,20 @@ export default class Connection {
* @param {Subscriptions | Array<Subscriptions>} sub Subscriptions object * @param {Subscriptions | Array<Subscriptions>} sub Subscriptions object
*/ */
AddSubscription(sub) { AddSubscription(sub) {
if (!this.Read) {
return;
}
let subObj = sub.ToObject(); let subObj = sub.ToObject();
if (Object.keys(subObj).length === 0) { if (Object.keys(subObj).length === 0) {
debugger; debugger;
throw "CANNOT SEND EMPTY SUB - FIX ME"; throw "CANNOT SEND EMPTY SUB - FIX ME";
} }
if (this.Subscriptions[sub.Id]) {
return;
}
let req = ["REQ", sub.Id, subObj]; let req = ["REQ", sub.Id, subObj];
if (sub.OrSubs.length > 0) { if (sub.OrSubs.length > 0) {
req = [ req = [
@ -133,7 +142,13 @@ export default class Connection {
_OnEvent(subId, ev) { _OnEvent(subId, ev) {
if (this.Subscriptions[subId]) { if (this.Subscriptions[subId]) {
this.Subscriptions[subId].OnEvent(ev); this._VerifySig(ev)
.then((e) => {
if (this.Subscriptions[subId]) {
this.Subscriptions[subId].OnEvent(e);
}
})
.catch(console.error);
} else { } else {
console.warn(`No subscription for event! ${subId}`); console.warn(`No subscription for event! ${subId}`);
} }
@ -148,4 +163,23 @@ export default class Connection {
console.warn(`No subscription for end! ${subId}`); console.warn(`No subscription for end! ${subId}`);
} }
} }
async _VerifySig(ev) {
let payload = [
0,
ev.pubkey,
ev.created_at,
ev.kind,
ev.tags,
ev.content
];
let payloadData = new TextEncoder().encode(JSON.stringify(payload));
let data = await secp.utils.sha256(payloadData);
let hash = secp.utils.bytesToHex(data);
if (!await secp.schnorr.verify(ev.sig, hash, ev.pubkey)) {
throw "Sig verify failed";
}
return ev;
}
} }

View File

@ -5,7 +5,7 @@ import useThreadFeed from "../feed/ThreadFeed";
export default function EventPage() { export default function EventPage() {
const params = useParams(); const params = useParams();
const id = params.id; const id = params.id;
const { notes } = useThreadFeed(id); const { notes } = useThreadFeed(id);
return <Thread notes={notes} this={id}/>; return <Thread notes={notes} this={id}/>;
} }

View File

@ -1,10 +1,10 @@
import "./Layout.css"; import "./Layout.css";
import { useContext, useEffect } from "react" import { useEffect } from "react"
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { faBell } from "@fortawesome/free-solid-svg-icons"; import { faBell } from "@fortawesome/free-solid-svg-icons";
import { NostrContext } from ".." import { System } from ".."
import ProfileImage from "../element/ProfileImage"; import ProfileImage from "../element/ProfileImage";
import { init } from "../state/Login"; import { init } from "../state/Login";
import useLoginFeed from "../feed/LoginFeed"; import useLoginFeed from "../feed/LoginFeed";
@ -13,20 +13,20 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
export default function Layout(props) { export default function Layout(props) {
const dispatch = useDispatch(); const dispatch = useDispatch();
const system = useContext(NostrContext);
const navigate = useNavigate(); const navigate = useNavigate();
const key = useSelector(s => s.login.publicKey); const key = useSelector(s => s.login.publicKey);
const relays = useSelector(s => s.login.relays); const relays = useSelector(s => s.login.relays);
const notifications = useSelector(s => s.login.notifications);
useUsersCache(); useUsersCache();
useLoginFeed(); useLoginFeed();
useEffect(() => { useEffect(() => {
if (system && relays) { if (relays) {
for (let [k, v] of Object.entries(relays)) { for (let [k, v] of Object.entries(relays)) {
system.ConnectToRelay(k, v); System.ConnectToRelay(k, v);
} }
} }
}, [relays, system]); }, [relays]);
useEffect(() => { useEffect(() => {
dispatch(init()); dispatch(init());
@ -35,8 +35,9 @@ export default function Layout(props) {
function accountHeader() { function accountHeader() {
return ( return (
<> <>
<div className="btn btn-rnd notifications"> <div className="btn btn-rnd notifications" onClick={() => navigate("/notifications")}>
<FontAwesomeIcon icon={faBell} size="xl" /> <FontAwesomeIcon icon={faBell} size="xl" />
{notifications?.length ?? 0}
</div> </div>
<ProfileImage pubKey={key} /> <ProfileImage pubKey={key} />
</> </>

View File

@ -0,0 +1,6 @@
export default function NotificationsPage() {
return (
<>
</>
)
}

View File

@ -13,7 +13,7 @@ import useEventPublisher from "../feed/EventPublisher";
import useTimelineFeed from "../feed/TimelineFeed"; import useTimelineFeed from "../feed/TimelineFeed";
import Note from "../element/Note"; import Note from "../element/Note";
import QRCodeStyling from "qr-code-styling"; import QRCodeStyling from "qr-code-styling";
import ReactModal from "react-modal"; import Modal from "../element/Modal";
export default function ProfilePage() { export default function ProfilePage() {
const dispatch = useDispatch(); const dispatch = useDispatch();
@ -65,7 +65,7 @@ export default function ProfilePage() {
useEffect(() => { useEffect(() => {
if (qrRef.current && showLnQr) { if (qrRef.current && showLnQr) {
let qr = new QRCodeStyling({ let qr = new QRCodeStyling({
data: "", data: {lud16},
type: "canvas" type: "canvas"
}); });
qrRef.current.innerHTML = ""; qrRef.current.innerHTML = "";
@ -143,9 +143,10 @@ export default function ProfilePage() {
<div>&nbsp; {lud16}</div> <div>&nbsp; {lud16}</div>
</div> : null} </div> : null}
{showLnQr === true ? {showLnQr === true ?
<ReactModal isOpen={showLnQr} onRequestClose={() => setShowLnQr(false)} overlayClassName="modal" className="modal-content" preventScroll={true}> <Modal onClose={() => setShowLnQr(false)}>
<div ref={qrRef}>QR</div> <h4>{lud16}</h4>
</ReactModal> : null} <div ref={qrRef}></div>
</Modal> : null}
</> </>
) )
} }

View File

@ -31,6 +31,11 @@ const LoginSlice = createSlice({
* Login keys are managed by extension * Login keys are managed by extension
*/ */
nip07: false, nip07: false,
/**
* Notifications for this login session
*/
notifications: []
}, },
reducers: { reducers: {
init: (state) => { init: (state) => {
@ -40,7 +45,6 @@ const LoginSlice = createSlice({
state.publicKey = secp.utils.bytesToHex(secp.schnorr.getPublicKey(state.privateKey, true)); state.publicKey = secp.utils.bytesToHex(secp.schnorr.getPublicKey(state.privateKey, true));
} }
state.relays = { state.relays = {
"wss://beta.nostr.v0l.io": { read: true, write: true },
"wss://nostr.v0l.io": { read: true, write: true }, "wss://nostr.v0l.io": { read: true, write: true },
"wss://relay.damus.io": { read: true, write: true }, "wss://relay.damus.io": { read: true, write: true },
"wss://nostr-pub.wellorder.net": { read: true, write: true } "wss://nostr-pub.wellorder.net": { read: true, write: true }
@ -48,7 +52,7 @@ const LoginSlice = createSlice({
// check nip07 pub key // check nip07 pub key
let nip07PubKey = window.localStorage.getItem(Nip07PublicKeyItem); let nip07PubKey = window.localStorage.getItem(Nip07PublicKeyItem);
if(nip07PubKey && !state.privateKey) { if (nip07PubKey && !state.privateKey) {
state.publicKey = nip07PubKey; state.publicKey = nip07PubKey;
state.nip07 = true; state.nip07 = true;
} }
@ -72,6 +76,21 @@ const LoginSlice = createSlice({
setFollows: (state, action) => { setFollows: (state, action) => {
state.follows = action.payload; state.follows = action.payload;
}, },
addNotifications: (state, action) => {
let n = action.payload;
if (!Array.isArray(n)) {
n = [n];
}
for (let x in n) {
if (!state.notifications.some(a => a.id === x.id)) {
state.notifications.push(x);
}
}
state.notifications = [
...state.notifications
];
},
logout: (state) => { logout: (state) => {
state.privateKey = null; state.privateKey = null;
window.localStorage.removeItem(PrivateKeyItem); window.localStorage.removeItem(PrivateKeyItem);
@ -79,5 +98,5 @@ const LoginSlice = createSlice({
} }
}); });
export const { init, setPrivateKey, setPublicKey, setNip07PubKey, setRelays, setFollows, logout } = LoginSlice.actions; export const { init, setPrivateKey, setPublicKey, setNip07PubKey, setRelays, setFollows, addNotifications, logout } = LoginSlice.actions;
export const reducer = LoginSlice.reducer; export const reducer = LoginSlice.reducer;

View File

@ -1,13 +1,11 @@
import { configureStore } from "@reduxjs/toolkit"; import { configureStore } from "@reduxjs/toolkit";
import { reducer as UsersReducer } from "./Users"; import { reducer as UsersReducer } from "./Users";
import { reducer as LoginReducer } from "./Login"; import { reducer as LoginReducer } from "./Login";
import { reducer as ThreadReducer } from "./Thread";
const Store = configureStore({ const Store = configureStore({
reducer: { reducer: {
users: UsersReducer, users: UsersReducer,
login: LoginReducer, login: LoginReducer
thread: ThreadReducer
} }
}); });

View File

@ -1,26 +0,0 @@
import { createSlice } from '@reduxjs/toolkit'
const ThreadSlice = createSlice({
name: "Thread",
initialState: {
notes: [],
},
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);
}
},
reset: (state) => {
state.notes = [];
}
}
});
export const { setNotes, addNote, reset } = ThreadSlice.actions;
export const reducer = ThreadSlice.reducer;

View File

@ -4124,11 +4124,6 @@ execa@^5.0.0:
signal-exit "^3.0.3" signal-exit "^3.0.3"
strip-final-newline "^2.0.0" strip-final-newline "^2.0.0"
exenv@^1.2.0:
version "1.2.2"
resolved "https://registry.yarnpkg.com/exenv/-/exenv-1.2.2.tgz#2ae78e85d9894158670b03d47bec1f03bd91bb9d"
integrity sha512-Z+ktTxTwv9ILfgKCk32OX3n/doe+OcLTRtqK9pcL+JsP3J1/VW8Uvl4ZjLlKqeW4rzK4oesDOGMEMRIZqtP4Iw==
exit@^0.1.2: exit@^0.1.2:
version "0.1.2" version "0.1.2"
resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c"
@ -5843,7 +5838,7 @@ lodash@^4.17.20, lodash@^4.17.21, lodash@^4.7.0:
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0: loose-envify@^1.1.0, loose-envify@^1.4.0:
version "1.4.0" version "1.4.0"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
@ -7035,7 +7030,7 @@ prompts@^2.0.1, prompts@^2.4.2:
kleur "^3.0.3" kleur "^3.0.3"
sisteransi "^1.0.5" sisteransi "^1.0.5"
prop-types@^15.7.2, prop-types@^15.8.1: prop-types@^15.8.1:
version "15.8.1" version "15.8.1"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
@ -7200,21 +7195,6 @@ react-is@^18.0.0:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b"
integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==
react-lifecycles-compat@^3.0.0:
version "3.0.4"
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
react-modal@^3.16.1:
version "3.16.1"
resolved "https://registry.yarnpkg.com/react-modal/-/react-modal-3.16.1.tgz#34018528fc206561b1a5467fc3beeaddafb39b2b"
integrity sha512-VStHgI3BVcGo7OXczvnJN7yT2TWHJPDXZWyI/a0ssFNhGZWsPmB8cF0z33ewDXq4VfYMO1vXgiv/g8Nj9NDyWg==
dependencies:
exenv "^1.2.0"
prop-types "^15.7.2"
react-lifecycles-compat "^3.0.0"
warning "^4.0.3"
react-redux@^8.0.5: react-redux@^8.0.5:
version "8.0.5" version "8.0.5"
resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-8.0.5.tgz#e5fb8331993a019b8aaf2e167a93d10af469c7bd" resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-8.0.5.tgz#e5fb8331993a019b8aaf2e167a93d10af469c7bd"
@ -8511,13 +8491,6 @@ walker@^1.0.7:
dependencies: dependencies:
makeerror "1.0.12" makeerror "1.0.12"
warning@^4.0.3:
version "4.0.3"
resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3"
integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==
dependencies:
loose-envify "^1.0.0"
watchpack@^2.4.0: watchpack@^2.4.0:
version "2.4.0" version "2.4.0"
resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d" resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d"