Thread loading

This commit is contained in:
Kieran 2022-12-20 23:14:13 +00:00
parent c454f5c7aa
commit ff9de60b6b
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
10 changed files with 169 additions and 65 deletions

View File

@ -37,3 +37,8 @@
.note > .header > img:hover, .note > .header > .name > .reply:hover, .note > .body:hover { .note > .header > img:hover, .note > .header > .name > .reply:hover, .note > .body:hover {
cursor: pointer; cursor: pointer;
} }
.note > .footer {
padding: 10px 0;
text-align: right;
}

View File

@ -13,6 +13,7 @@ const MentionRegex = /(#\[\d+\])/g;
export default function Note(props) { export default function Note(props) {
const navigate = useNavigate(); const navigate = useNavigate();
const data = props.data; const data = props.data;
const reactions = props.reactions;
const [sig, setSig] = useState(false); const [sig, setSig] = useState(false);
const users = useSelector(s => s.users?.users); const users = useSelector(s => s.users?.users);
const user = users[data?.pubkey]; const user = users[data?.pubkey];
@ -74,23 +75,24 @@ export default function Note(props) {
case "png": case "png":
case "bmp": case "bmp":
case "webp": { case "webp": {
return <img src={url} />; return <img key={url} src={url} />;
} }
case "mp4": case "mp4":
case "mkv": case "mkv":
case "avi": case "avi":
case "m4v": { case "m4v": {
return <video src={url} controls /> return <video key={url} src={url} controls />
} }
} }
} }
} else { } else {
let mentions = a.split(MentionRegex).map((match) => { let mentions = a.split(MentionRegex).map((match) => {
if (match.startsWith("#")) { if (match.startsWith("#")) {
let pref = pTags[match.match(/\[(\d+)\]/)[1]]; let idx = parseInt(match.match(/\[(\d+)\]/)[1]) - 1;
let pref = pTags[idx];
if (pref) { if (pref) {
let pUser = users[pref.PubKey]?.name ?? pref.PubKey.substring(0, 8); let pUser = users[pref.PubKey]?.name ?? pref.PubKey.substring(0, 8);
return <Link to={`/p/${pref.PubKey}`}>#{pUser}</Link>; return <Link key={pref.PubKey} to={`/p/${pref.PubKey}`}>#{pUser}</Link>;
} else { } else {
return <pre>BROKEN REF: {match[0]}</pre>; return <pre>BROKEN REF: {match[0]}</pre>;
} }
@ -100,6 +102,7 @@ export default function Note(props) {
}); });
return mentions; return mentions;
} }
return a;
}); });
} }
@ -128,6 +131,11 @@ export default function Note(props) {
<div className="body" onClick={(e) => goToEvent(e, ev.Id)}> <div className="body" onClick={(e) => goToEvent(e, ev.Id)}>
{transformBody()} {transformBody()}
</div> </div>
<div className="footer">
<span className="pill">
👍 {(reactions?.length ?? 0)}
</span>
</div>
</div> </div>
) )
} }

View File

@ -1,4 +1,5 @@
import Event from "../nostr/Event"; import Event from "../nostr/Event";
import EventKind from "../nostr/EventKind";
import Note from "./Note"; import Note from "./Note";
export default function Thread(props) { export default function Thread(props) {
@ -11,11 +12,17 @@ export default function Thread(props) {
return null; 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 ( return (
<> <>
<Note data={root?.ToObject()}/> <Note data={root?.ToObject()} reactions={reactions(root?.Id)}/>
{repliesToRoot?.map(a => <Note key={a.Id} data={a.ToObject()}/>)} {repliesToRoot?.map(a => <Note key={a.Id} data={a.ToObject()} reactions={reactions(a.Id)}/>)}
</> </>
); );
} }

View File

@ -63,4 +63,17 @@ input[type="text"] {
a { a {
color: inherit; color: inherit;
line-height: 1.3em; 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;
} }

View File

@ -15,9 +15,9 @@ export class NostrSystem {
* @param {string} address * @param {string} address
*/ */
ConnectToRelay(address) { ConnectToRelay(address) {
if(typeof this.Sockets[address] === "undefined") { if (typeof this.Sockets[address] === "undefined") {
let c = new Connection(address); let c = new Connection(address);
for(let s of Object.values(this.Subscriptions)) { for (let s of Object.values(this.Subscriptions)) {
c.AddSubscription(s); c.AddSubscription(s);
} }
this.Sockets[address] = c; this.Sockets[address] = c;
@ -25,22 +25,49 @@ export class NostrSystem {
} }
AddSubscription(sub) { AddSubscription(sub) {
for(let s of Object.values(this.Sockets)) { for (let s of Object.values(this.Sockets)) {
s.AddSubscription(sub); s.AddSubscription(sub);
} }
this.Subscriptions[sub.Id] = sub; this.Subscriptions[sub.Id] = sub;
} }
RemoveSubscription(subId) { RemoveSubscription(subId) {
for(let s of Object.values(this.Sockets)) { for (let s of Object.values(this.Sockets)) {
s.RemoveSubscription(subId); s.RemoveSubscription(subId);
} }
delete this.Subscriptions[subId]; delete this.Subscriptions[subId];
} }
BroadcastEvent(ev) { BroadcastEvent(ev) {
for(let s of Object.values(this.Sockets)) { for (let s of Object.values(this.Sockets)) {
s.SendEvent(ev); s.SendEvent(ev);
} }
} }
/**
* Request/Response pattern
* @param {Subscriptions} sub
* @returns {Array<any>}
*/
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++;
}
});
}
} }

View File

@ -1,6 +1,4 @@
import { useEffect } from "react";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import Note from "../element/Note";
import Thread from "../element/Thread"; import Thread from "../element/Thread";
import useThreadFeed from "./feed/ThreadFeed"; import useThreadFeed from "./feed/ThreadFeed";
@ -10,7 +8,7 @@ export default function EventPage() {
const { notes } = useThreadFeed(id); const { notes } = useThreadFeed(id);
if(notes) { if(notes?.length > 0) {
return ( return (
<Thread notes={notes}/> <Thread notes={notes}/>
) )

View File

@ -8,8 +8,7 @@ export default function Layout(props) {
const system = useContext(NostrContext); const system = useContext(NostrContext);
const navigate = useNavigate(); const navigate = useNavigate();
const relays = useSelector(s => s.login.relays); const relays = useSelector(s => s.login.relays);
const users = useUsersStore();
useUsersStore();
useEffect(() => { useEffect(() => {
if (system && relays) { if (system && relays) {

View File

@ -10,25 +10,33 @@ export default function useThreadFeed(id) {
const dispatch = useDispatch(); const dispatch = useDispatch();
const system = useContext(NostrContext); const system = useContext(NostrContext);
const notes = useSelector(s => s.thread.notes); const notes = useSelector(s => s.thread.notes);
const [thisLoaded, setThisLoaded] = useState(false);
// track profiles // track profiles
useEffect(() => { useEffect(() => {
let keys = [];
for (let n of notes) { for (let n of notes) {
if (n.pubkey) { if (n.pubkey) {
dispatch(addPubKey(n.pubkey)); keys.push(n.pubkey);
} }
for(let t of n.tags) { for (let t of n.tags) {
if(t[0] === "p" && t[1]) { if (t[0] === "p" && t[1]) {
dispatch(addPubKey(t[1])); keys.push(t[1]);
} }
} }
} }
dispatch(addPubKey(keys));
}, [notes]); }, [notes]);
useEffect(() => { useEffect(() => {
if (system) { if (system) {
let sub = new Subscriptions(); 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 thisNote = Event.FromObject(notes[0]);
let thread = thisNote.GetThread(); let thread = thisNote.GetThread();
if (thread !== null) { if (thread !== null) {
@ -44,23 +52,24 @@ export default function useThreadFeed(id) {
} }
} else if (notes.length === 0) { } else if (notes.length === 0) {
sub.Ids.add(id); sub.Ids.add(id);
// get replies to this event
let subRelated = new Subscriptions();
subRelated.ETags.add(id);
sub.AddSubscription(subRelated);
} else { } else {
return; return;
} }
// get replies to this event
let subRelated = new Subscriptions();
subRelated.ETags = sub.Ids;
sub.AddSubscription(subRelated);
sub.OnEvent = (e) => { sub.OnEvent = (e) => {
dispatch(addNote(e)); dispatch(addNote(e));
}; };
system.AddSubscription(sub); system.AddSubscription(sub);
return () => system.RemoveSubscription(sub.Id);
} }
}, [system, notes]); }, [system, notes]);
useEffect(() => { useEffect(() => {
console.debug("use thread stream")
dispatch(reset()); dispatch(reset());
}, []); }, []);

View File

@ -1,4 +1,4 @@
import { useContext, useEffect } from "react"; import { useContext, useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { NostrContext } from "../../index"; import { NostrContext } from "../../index";
import Event from "../../nostr/Event"; import Event from "../../nostr/Event";
@ -10,26 +10,55 @@ export default function useUsersStore() {
const dispatch = useDispatch(); const dispatch = useDispatch();
const system = useContext(NostrContext); 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 [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(() => { useEffect(() => {
if (pKeys.length > 0) { if (system && pKeys.length > 0 && !loading) {
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) { setLoading(true);
system.AddSubscription(sub); getUsers()
return () => system.RemoveSubscription(sub.Id); .catch(console.error)
} .then(() => setLoading(false));
} }
}, [pKeys]); }, [system, pKeys, loading]);
return { users };
} }

View File

@ -15,30 +15,39 @@ const UsersSlice = createSlice({
}, },
reducers: { reducers: {
addPubKey: (state, action) => { addPubKey: (state, action) => {
if (!state.pubKeys.includes(action.payload)) { let keys = action.payload;
let temp = new Set(state.pubKeys); if (!Array.isArray(keys)) {
temp.add(action.payload); keys = [keys];
state.pubKeys = Array.from(temp);
} }
let temp = new Set(state.pubKeys);
// load from cache for (let k of keys) {
let cache = window.localStorage.getItem(`user:${action.payload}`); temp.add(k);
if(cache) {
let ud = JSON.parse(cache); // load from cache
state.users[ud.pubkey] = ud; 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) => { setUserData: (state, action) => {
let ud = action.payload; let ud = action.payload;
let existing = state.users[ud.pubkey]; if (!Array.isArray(ud)) {
if (existing) { ud = [ud];
ud = { }
...existing, for (let x of ud) {
...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));
} }
} }
}); });