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 {
|
.note > .header > img:hover, .note > .header > .name > .reply:hover, .note > .body:hover {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.note > .footer {
|
||||||
|
padding: 10px 0;
|
||||||
|
text-align: right;
|
||||||
|
}
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
@ -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)}/>)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
@ -64,3 +64,16 @@ 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;
|
||||||
|
}
|
@ -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++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
@ -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}/>
|
||||||
)
|
)
|
||||||
|
@ -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) {
|
||||||
|
@ -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());
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
@ -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 };
|
||||||
}
|
}
|
@ -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);
|
||||||
|
for (let k of keys) {
|
||||||
|
temp.add(k);
|
||||||
|
|
||||||
// load from cache
|
// load from cache
|
||||||
let cache = window.localStorage.getItem(`user:${action.payload}`);
|
let cache = window.localStorage.getItem(`user:${k}`);
|
||||||
if(cache) {
|
if (cache) {
|
||||||
let ud = JSON.parse(cache);
|
let ud = JSON.parse(cache);
|
||||||
state.users[ud.pubkey] = ud;
|
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));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user