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 {
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) {
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 <img src={url} />;
return <img key={url} src={url} />;
}
case "mp4":
case "mkv":
case "avi":
case "m4v": {
return <video src={url} controls />
return <video key={url} src={url} controls />
}
}
}
} 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 <Link to={`/p/${pref.PubKey}`}>#{pUser}</Link>;
return <Link key={pref.PubKey} to={`/p/${pref.PubKey}`}>#{pUser}</Link>;
} else {
return <pre>BROKEN REF: {match[0]}</pre>;
}
@ -100,6 +102,7 @@ export default function Note(props) {
});
return mentions;
}
return a;
});
}
@ -128,6 +131,11 @@ export default function Note(props) {
<div className="body" onClick={(e) => goToEvent(e, ev.Id)}>
{transformBody()}
</div>
<div className="footer">
<span className="pill">
👍 {(reactions?.length ?? 0)}
</span>
</div>
</div>
)
}

View File

@ -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 (
<>
<Note data={root?.ToObject()}/>
{repliesToRoot?.map(a => <Note key={a.Id} data={a.ToObject()}/>)}
<Note data={root?.ToObject()} reactions={reactions(root?.Id)}/>
{repliesToRoot?.map(a => <Note key={a.Id} data={a.ToObject()} reactions={reactions(a.Id)}/>)}
</>
);
}

View File

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

View File

@ -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<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 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 (
<Thread notes={notes}/>
)

View File

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

View File

@ -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());
}, []);

View File

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

View File

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