diff --git a/src/element/Note.css b/src/element/Note.css
index e23cd907..a3429f3d 100644
--- a/src/element/Note.css
+++ b/src/element/Note.css
@@ -37,3 +37,8 @@
.note > .header > img:hover, .note > .header > .name > .reply:hover, .note > .body:hover {
cursor: pointer;
}
+
+.note > .footer {
+ padding: 10px 0;
+ text-align: right;
+}
\ No newline at end of file
diff --git a/src/element/Note.js b/src/element/Note.js
index 86426fc4..ee99e547 100644
--- a/src/element/Note.js
+++ b/src/element/Note.js
@@ -13,6 +13,7 @@ const MentionRegex = /(#\[\d+\])/g;
export default function Note(props) {
const navigate = useNavigate();
const data = props.data;
+ const reactions = props.reactions;
const [sig, setSig] = useState(false);
const users = useSelector(s => s.users?.users);
const user = users[data?.pubkey];
@@ -74,23 +75,24 @@ export default function Note(props) {
case "png":
case "bmp":
case "webp": {
- return ;
+ return ;
}
case "mp4":
case "mkv":
case "avi":
case "m4v": {
- return
+ return
}
}
}
} else {
let mentions = a.split(MentionRegex).map((match) => {
if (match.startsWith("#")) {
- let pref = pTags[match.match(/\[(\d+)\]/)[1]];
+ let idx = parseInt(match.match(/\[(\d+)\]/)[1]) - 1;
+ let pref = pTags[idx];
if (pref) {
let pUser = users[pref.PubKey]?.name ?? pref.PubKey.substring(0, 8);
- return #{pUser};
+ return #{pUser};
} else {
return
BROKEN REF: {match[0]}
;
}
@@ -100,6 +102,7 @@ export default function Note(props) {
});
return mentions;
}
+ return a;
});
}
@@ -128,6 +131,11 @@ export default function Note(props) {
goToEvent(e, ev.Id)}>
{transformBody()}
+
+
+ 👍 {(reactions?.length ?? 0)}
+
+
)
}
\ No newline at end of file
diff --git a/src/element/Thread.js b/src/element/Thread.js
index b9d73605..76faa03a 100644
--- a/src/element/Thread.js
+++ b/src/element/Thread.js
@@ -1,4 +1,5 @@
import Event from "../nostr/Event";
+import EventKind from "../nostr/EventKind";
import Note from "./Note";
export default function Thread(props) {
@@ -11,11 +12,17 @@ export default function Thread(props) {
return null;
}
- const repliesToRoot = notes?.filter(a => a.GetThread()?.ReplyTo?.Event === root.Id);
+ function reactions(id) {
+ return notes?.filter(a => a.Kind === EventKind.Reaction && a.GetThread()?.Root?.Event === id);
+ }
+
+ const repliesToRoot = notes?.
+ filter(a => a.GetThread()?.Root?.Event === root.Id && a.Kind === EventKind.TextNote)
+ .sort((a, b) => b.CreatedAt - a.CreatedAt);
return (
<>
-
- {repliesToRoot?.map(a => )}
+
+ {repliesToRoot?.map(a => )}
>
);
}
\ No newline at end of file
diff --git a/src/index.css b/src/index.css
index 262215f4..bf889e19 100644
--- a/src/index.css
+++ b/src/index.css
@@ -63,4 +63,17 @@ input[type="text"] {
a {
color: inherit;
line-height: 1.3em;
+}
+
+span.pill {
+ display: inline-block;
+ background-color: #333;
+ padding: 0 10px;
+ border-radius: 10px;
+ user-select: none;
+ margin: 2px 10px;
+}
+
+span.pill:hover {
+ cursor: pointer;
}
\ No newline at end of file
diff --git a/src/nostr/System.js b/src/nostr/System.js
index 53d98935..2c728970 100644
--- a/src/nostr/System.js
+++ b/src/nostr/System.js
@@ -15,9 +15,9 @@ export class NostrSystem {
* @param {string} address
*/
ConnectToRelay(address) {
- if(typeof this.Sockets[address] === "undefined") {
+ if (typeof this.Sockets[address] === "undefined") {
let c = new Connection(address);
- for(let s of Object.values(this.Subscriptions)) {
+ for (let s of Object.values(this.Subscriptions)) {
c.AddSubscription(s);
}
this.Sockets[address] = c;
@@ -25,22 +25,49 @@ export class NostrSystem {
}
AddSubscription(sub) {
- for(let s of Object.values(this.Sockets)) {
+ for (let s of Object.values(this.Sockets)) {
s.AddSubscription(sub);
}
this.Subscriptions[sub.Id] = sub;
}
RemoveSubscription(subId) {
- for(let s of Object.values(this.Sockets)) {
+ for (let s of Object.values(this.Sockets)) {
s.RemoveSubscription(subId);
}
delete this.Subscriptions[subId];
}
BroadcastEvent(ev) {
- for(let s of Object.values(this.Sockets)) {
+ for (let s of Object.values(this.Sockets)) {
s.SendEvent(ev);
}
}
+
+ /**
+ * Request/Response pattern
+ * @param {Subscriptions} sub
+ * @returns {Array}
+ */
+ RequestSubscription(sub) {
+ return new Promise((resolve, reject) => {
+ let counter = 0;
+ let events = [];
+ sub.OnEvent = (ev) => {
+ if (!events.some(a => a.id === ev.id)) {
+ events.push(ev);
+ }
+ };
+ sub.OnEnd = (c) => {
+ c.RemoveSubscription(sub.Id);
+ if(--counter === 0) {
+ resolve(events);
+ }
+ };
+ for (let s of Object.values(this.Sockets)) {
+ s.AddSubscription(sub);
+ counter++;
+ }
+ });
+ }
}
\ No newline at end of file
diff --git a/src/pages/EventPage.js b/src/pages/EventPage.js
index 4dea02c7..a911d613 100644
--- a/src/pages/EventPage.js
+++ b/src/pages/EventPage.js
@@ -1,6 +1,4 @@
-import { useEffect } from "react";
import { useParams } from "react-router-dom";
-import Note from "../element/Note";
import Thread from "../element/Thread";
import useThreadFeed from "./feed/ThreadFeed";
@@ -10,7 +8,7 @@ export default function EventPage() {
const { notes } = useThreadFeed(id);
- if(notes) {
+ if(notes?.length > 0) {
return (
)
diff --git a/src/pages/Layout.js b/src/pages/Layout.js
index 60cdbeee..9d952ffa 100644
--- a/src/pages/Layout.js
+++ b/src/pages/Layout.js
@@ -8,8 +8,7 @@ export default function Layout(props) {
const system = useContext(NostrContext);
const navigate = useNavigate();
const relays = useSelector(s => s.login.relays);
-
- useUsersStore();
+ const users = useUsersStore();
useEffect(() => {
if (system && relays) {
diff --git a/src/pages/feed/ThreadFeed.js b/src/pages/feed/ThreadFeed.js
index 5dbaee96..f12b3b49 100644
--- a/src/pages/feed/ThreadFeed.js
+++ b/src/pages/feed/ThreadFeed.js
@@ -10,25 +10,33 @@ export default function useThreadFeed(id) {
const dispatch = useDispatch();
const system = useContext(NostrContext);
const notes = useSelector(s => s.thread.notes);
+ const [thisLoaded, setThisLoaded] = useState(false);
// track profiles
useEffect(() => {
+ let keys = [];
for (let n of notes) {
if (n.pubkey) {
- dispatch(addPubKey(n.pubkey));
+ keys.push(n.pubkey);
}
- for(let t of n.tags) {
- if(t[0] === "p" && t[1]) {
- dispatch(addPubKey(t[1]));
+ for (let t of n.tags) {
+ if (t[0] === "p" && t[1]) {
+ keys.push(t[1]);
}
}
}
+
+ dispatch(addPubKey(keys));
}, [notes]);
useEffect(() => {
if (system) {
let sub = new Subscriptions();
- if (notes.length === 1) {
+ let thisNote = notes.find(a => a.id === id);
+ if (thisNote && !thisLoaded) {
+ console.debug(notes);
+ setThisLoaded(true);
+
let thisNote = Event.FromObject(notes[0]);
let thread = thisNote.GetThread();
if (thread !== null) {
@@ -44,23 +52,24 @@ export default function useThreadFeed(id) {
}
} else if (notes.length === 0) {
sub.Ids.add(id);
-
- // get replies to this event
- let subRelated = new Subscriptions();
- subRelated.ETags.add(id);
- sub.AddSubscription(subRelated);
} 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);
- return () => system.RemoveSubscription(sub.Id);
}
}, [system, notes]);
useEffect(() => {
+ console.debug("use thread stream")
dispatch(reset());
}, []);
diff --git a/src/pages/feed/UsersFeed.js b/src/pages/feed/UsersFeed.js
index 037a36e2..303f5041 100644
--- a/src/pages/feed/UsersFeed.js
+++ b/src/pages/feed/UsersFeed.js
@@ -1,4 +1,4 @@
-import { useContext, useEffect } from "react";
+import { useContext, useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { NostrContext } from "../../index";
import Event from "../../nostr/Event";
@@ -10,26 +10,55 @@ export default function useUsersStore() {
const dispatch = useDispatch();
const system = useContext(NostrContext);
const pKeys = useSelector(s => s.users.pubKeys);
+ const users = useSelector(s => s.users.users);
+ const [loading, setLoading] = useState(false);
+
+ function isUserCached(id) {
+ let expire = new Date().getTime() - 60_000; // 60s expire
+ let u = users[id];
+ return u && (u.loaded || 0) < expire;
+ }
+
+ async function getUsers() {
+
+ let needProfiles = pKeys.filter(a => !isUserCached(a));
+ let sub = new Subscriptions();
+ sub.Authors = new Set(needProfiles);
+ sub.Kinds.add(EventKind.SetMetadata);
+
+ let events = await system.RequestSubscription(sub);
+
+ let loaded = new Date().getTime();
+ let profiles = events.map(a => {
+ let metaEvent = Event.FromObject(a);
+ let data = JSON.parse(metaEvent.Content);
+ return {
+ pubkey: metaEvent.PubKey,
+ fromEvent: a,
+ loaded,
+ ...data
+ };
+ });
+ let missing = needProfiles.filter(a => !events.some(b => b.pubkey === a));
+ let missingProfiles = missing.map(a => new{
+ pubkey: a,
+ loaded
+ });
+ dispatch(setUserData([
+ ...profiles,
+ ...missingProfiles
+ ]));
+ }
useEffect(() => {
- if (pKeys.length > 0) {
- const sub = new Subscriptions();
- sub.Authors = new Set(pKeys);
- sub.Kinds.add(EventKind.SetMetadata);
- sub.OnEvent = (ev) => {
- let metaEvent = Event.FromObject(ev);
- let data = JSON.parse(metaEvent.Content);
- let userData = {
- pubkey: metaEvent.PubKey,
- ...data
- };
- dispatch(setUserData(userData));
- };
+ if (system && pKeys.length > 0 && !loading) {
- if (system) {
- system.AddSubscription(sub);
- return () => system.RemoveSubscription(sub.Id);
- }
+ setLoading(true);
+ getUsers()
+ .catch(console.error)
+ .then(() => setLoading(false));
}
- }, [pKeys]);
+ }, [system, pKeys, loading]);
+
+ return { users };
}
\ No newline at end of file
diff --git a/src/state/Users.js b/src/state/Users.js
index f0168b57..4fc9963f 100644
--- a/src/state/Users.js
+++ b/src/state/Users.js
@@ -15,30 +15,39 @@ const UsersSlice = createSlice({
},
reducers: {
addPubKey: (state, action) => {
- if (!state.pubKeys.includes(action.payload)) {
- let temp = new Set(state.pubKeys);
- temp.add(action.payload);
- state.pubKeys = Array.from(temp);
+ let keys = action.payload;
+ if (!Array.isArray(keys)) {
+ keys = [keys];
}
-
- // load from cache
- let cache = window.localStorage.getItem(`user:${action.payload}`);
- if(cache) {
- let ud = JSON.parse(cache);
- state.users[ud.pubkey] = ud;
+ let temp = new Set(state.pubKeys);
+ for (let k of keys) {
+ temp.add(k);
+
+ // load from cache
+ let cache = window.localStorage.getItem(`user:${k}`);
+ if (cache) {
+ let ud = JSON.parse(cache);
+ state.users[ud.pubkey] = ud;
+ }
}
+ state.pubKeys = Array.from(temp);
},
setUserData: (state, action) => {
let ud = action.payload;
- let existing = state.users[ud.pubkey];
- if (existing) {
- ud = {
- ...existing,
- ...ud
- };
+ if (!Array.isArray(ud)) {
+ ud = [ud];
+ }
+ for (let x of ud) {
+ let existing = state.users[ud.pubkey];
+ if (existing) {
+ ud = {
+ ...existing,
+ ...ud
+ };
+ }
+ state.users[ud.pubkey] = ud;
+ window.localStorage.setItem(`user:${ud.pubkey}`, JSON.stringify(ud));
}
- state.users[ud.pubkey] = ud;
- window.localStorage.setItem(`user:${ud.pubkey}`, JSON.stringify(ud));
}
}
});