Thread loading
This commit is contained in:
parent
c454f5c7aa
commit
ff9de60b6b
@ -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;
|
||||
}
|
@ -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>
|
||||
)
|
||||
}
|
@ -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)}/>)}
|
||||
</>
|
||||
);
|
||||
}
|
@ -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;
|
||||
}
|
@ -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++;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -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}/>
|
||||
)
|
||||
|
@ -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) {
|
||||
|
@ -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());
|
||||
}, []);
|
||||
|
||||
|
@ -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 };
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user