Threading improvments
This commit is contained in:
parent
b26f3a9b95
commit
99410dd8c2
@ -1,10 +1,11 @@
|
|||||||
import "./Note.css";
|
import "./Note.css";
|
||||||
import Event from "../nostr/Event";
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import { Link, useNavigate } from "react-router-dom";
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
|
import Event from "../nostr/Event";
|
||||||
import ProfileImage from "./ProfileImage";
|
import ProfileImage from "./ProfileImage";
|
||||||
|
import useEventPublisher from "../pages/feed/EventPublisher";
|
||||||
|
|
||||||
const UrlRegex = /((?:http|ftp|https):\/\/(?:[\w+?\.\w+])+(?:[a-zA-Z0-9\~\!\@\#\$\%\^\&\*\(\)_\-\=\+\\\/\?\.\:\;\'\,]*)?)/;
|
const UrlRegex = /((?:http|ftp|https):\/\/(?:[\w+?\.\w+])+(?:[a-zA-Z0-9\~\!\@\#\$\%\^\&\*\(\)_\-\=\+\\\/\?\.\:\;\'\,]*)?)/;
|
||||||
const FileExtensionRegex = /\.([\w]+)$/;
|
const FileExtensionRegex = /\.([\w]+)$/;
|
||||||
@ -13,11 +14,12 @@ 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 dataEvent = props["data-ev"];
|
||||||
const reactions = props.reactions;
|
const reactions = props.reactions;
|
||||||
|
const publisher = useEventPublisher();
|
||||||
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 ev = dataEvent ?? Event.FromObject(data);
|
||||||
const ev = Event.FromObject(data);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (sig === false) {
|
if (sig === false) {
|
||||||
@ -109,6 +111,11 @@ export default function Note(props) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function like() {
|
||||||
|
let evLike = await publisher.like(ev);
|
||||||
|
publisher.broadcast(evLike);
|
||||||
|
}
|
||||||
|
|
||||||
if (!ev.IsContent()) {
|
if (!ev.IsContent()) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -131,7 +138,7 @@ export default function Note(props) {
|
|||||||
{transformBody()}
|
{transformBody()}
|
||||||
</div>
|
</div>
|
||||||
<div className="footer">
|
<div className="footer">
|
||||||
<span className="pill">
|
<span className="pill" onClick={() => like()}>
|
||||||
👍 {(reactions?.length ?? 0)}
|
👍 {(reactions?.length ?? 0)}
|
||||||
</span>
|
</span>
|
||||||
<span className="pill" onClick={() => console.debug(ev)}>
|
<span className="pill" onClick={() => console.debug(ev)}>
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import "./Note.css";
|
import "./Note.css";
|
||||||
import moment from "moment";
|
|
||||||
import ProfileImage from "./ProfileImage";
|
import ProfileImage from "./ProfileImage";
|
||||||
|
|
||||||
export default function NoteGhost(props) {
|
export default function NoteGhost(props) {
|
||||||
@ -7,12 +6,9 @@ export default function NoteGhost(props) {
|
|||||||
<div className="note">
|
<div className="note">
|
||||||
<div className="header">
|
<div className="header">
|
||||||
<ProfileImage pubKey="" />
|
<ProfileImage pubKey="" />
|
||||||
<div className="info">
|
|
||||||
{moment().fromNow()}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="body">
|
<div className="body">
|
||||||
Loading...
|
{props.text ?? "Loading..."}
|
||||||
</div>
|
</div>
|
||||||
<div className="footer">
|
<div className="footer">
|
||||||
</div>
|
</div>
|
||||||
|
@ -4,6 +4,8 @@ import Note from "./Note";
|
|||||||
import NoteGhost from "./NoteGhost";
|
import NoteGhost from "./NoteGhost";
|
||||||
|
|
||||||
export default function Thread(props) {
|
export default function Thread(props) {
|
||||||
|
const thisEvent = props.this;
|
||||||
|
|
||||||
/** @type {Array<Event>} */
|
/** @type {Array<Event>} */
|
||||||
const notes = props.notes?.map(a => Event.FromObject(a));
|
const notes = props.notes?.map(a => Event.FromObject(a));
|
||||||
|
|
||||||
@ -11,18 +13,21 @@ export default function Thread(props) {
|
|||||||
const root = notes.find(a => a.GetThread() === null);
|
const root = notes.find(a => a.GetThread() === null);
|
||||||
|
|
||||||
function reactions(id) {
|
function reactions(id) {
|
||||||
return notes?.filter(a => a.Kind === EventKind.Reaction && a.GetThread()?.Root?.Event === id);
|
return notes?.filter(a => a.Kind === EventKind.Reaction && a.Tags.find(a => a.Key === "e").Event === id);
|
||||||
}
|
}
|
||||||
|
|
||||||
const repliesToRoot = notes?.
|
const repliesToRoot = notes?.
|
||||||
filter(a => a.GetThread()?.Root?.Event === root?.Id && a.Kind === EventKind.TextNote)
|
filter(a => a.GetThread()?.Root !== null && a.Kind === EventKind.TextNote && a.Id !== thisEvent)
|
||||||
.sort((a, b) => b.CreatedAt - a.CreatedAt);
|
.sort((a, b) => a.CreatedAt - b.CreatedAt);
|
||||||
|
const thisNote = notes?.find(a => a.Id === thisEvent);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{root === undefined ?
|
{root === undefined ?
|
||||||
<NoteGhost />
|
<NoteGhost text={`Loading... (${notes.length} events loaded)`}/>
|
||||||
: <Note data={root?.ToObject()} reactions={reactions(root?.Id)} />}
|
: <Note data-ev={root} reactions={reactions(root?.Id)} />}
|
||||||
{repliesToRoot?.map(a => <Note key={a.Id} data={a.ToObject()} reactions={reactions(a.Id)} />)}
|
{thisNote ? <Note data-ev={thisNote} reactions={reactions(thisNote.Id)}/> : null}
|
||||||
|
<h4>Other Replies</h4>
|
||||||
|
{repliesToRoot?.map(a => <Note key={a.Id} data-ev={a} reactions={reactions(a.Id)} />)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
@ -149,30 +149,4 @@ export default class Event {
|
|||||||
ev.PubKey = pubKey;
|
ev.PubKey = pubKey;
|
||||||
return ev;
|
return ev;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Create new SetMetadata event
|
|
||||||
* @param {String} pubKey Pubkey of the creator of this event
|
|
||||||
* @param {any} obj Metadata content
|
|
||||||
* @returns {Event}
|
|
||||||
*/
|
|
||||||
static SetMetadata(pubKey, obj) {
|
|
||||||
let ev = Event.ForPubKey(pubKey);
|
|
||||||
ev.Kind = EventKind.SetMetadata;
|
|
||||||
ev.Content = JSON.stringify(obj);
|
|
||||||
return ev;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new TextNote event
|
|
||||||
* @param {String} pubKey
|
|
||||||
* @param {String} message
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
static NewNote(pubKey, message) {
|
|
||||||
let ev = Event.ForPubKey(pubKey);
|
|
||||||
ev.Kind = EventKind.TextNote;
|
|
||||||
ev.Content = message;
|
|
||||||
return ev;
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -7,5 +7,5 @@ export default function EventPage() {
|
|||||||
const id = params.id;
|
const id = params.id;
|
||||||
|
|
||||||
const { notes } = useThreadFeed(id);
|
const { notes } = useThreadFeed(id);
|
||||||
return <Thread notes={notes}/>;
|
return <Thread notes={notes} this={id}/>;
|
||||||
}
|
}
|
@ -3,19 +3,17 @@ import { useDispatch, useSelector } from "react-redux";
|
|||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
import useProfile from "./feed/ProfileFeed";
|
import useProfile from "./feed/ProfileFeed";
|
||||||
import { useContext, useEffect, useState } from "react";
|
import { useContext, useEffect, useState } from "react";
|
||||||
import Event from "../nostr/Event";
|
|
||||||
import { NostrContext } from "..";
|
|
||||||
import { resetProfile } from "../state/Users";
|
import { resetProfile } from "../state/Users";
|
||||||
import Nostrich from "../nostrich.jpg";
|
import Nostrich from "../nostrich.jpg";
|
||||||
|
import useEventPublisher from "./feed/EventPublisher";
|
||||||
|
|
||||||
export default function ProfilePage() {
|
export default function ProfilePage() {
|
||||||
const system = useContext(NostrContext);
|
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const id = params.id;
|
const id = params.id;
|
||||||
const user = useProfile(id);
|
const user = useProfile(id);
|
||||||
|
const publisher = useEventPublisher();
|
||||||
const loginPubKey = useSelector(s => s.login.publicKey);
|
const loginPubKey = useSelector(s => s.login.publicKey);
|
||||||
const privKey = useSelector(s => s.login.privateKey);
|
|
||||||
const isMe = loginPubKey === id;
|
const isMe = loginPubKey === id;
|
||||||
|
|
||||||
let [name, setName] = useState("");
|
let [name, setName] = useState("");
|
||||||
@ -37,7 +35,7 @@ export default function ProfilePage() {
|
|||||||
}, [user]);
|
}, [user]);
|
||||||
|
|
||||||
async function saveProfile() {
|
async function saveProfile() {
|
||||||
let ev = Event.SetMetadata(id, {
|
let ev = await publisher.metadata({
|
||||||
name,
|
name,
|
||||||
about,
|
about,
|
||||||
picture,
|
picture,
|
||||||
@ -45,10 +43,8 @@ export default function ProfilePage() {
|
|||||||
nip05,
|
nip05,
|
||||||
lud16
|
lud16
|
||||||
});
|
});
|
||||||
await ev.Sign(privKey);
|
|
||||||
|
|
||||||
console.debug(ev);
|
console.debug(ev);
|
||||||
system.BroadcastEvent(ev);
|
publisher.broadcast(ev);
|
||||||
dispatch(resetProfile(id));
|
dispatch(resetProfile(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,22 +1,20 @@
|
|||||||
import "./Root.css";
|
import "./Root.css";
|
||||||
import Timeline from "./Timeline";
|
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
import { useContext, useState } from "react";
|
import { useState } from "react";
|
||||||
import Event from "../nostr/Event";
|
import Timeline from "./Timeline";
|
||||||
import { NostrContext } from "..";
|
import useEventPublisher from "./feed/EventPublisher";
|
||||||
|
|
||||||
export default function RootPage() {
|
export default function RootPage() {
|
||||||
const system = useContext(NostrContext);
|
const publisher = useEventPublisher();
|
||||||
const pubKey = useSelector(s => s.login.publicKey);
|
const pubKey = useSelector(s => s.login.publicKey);
|
||||||
const privKey = useSelector(s => s.login.privateKey);
|
|
||||||
const [note, setNote] = useState("");
|
const [note, setNote] = useState("");
|
||||||
|
|
||||||
async function sendNote() {
|
async function sendNote() {
|
||||||
let ev = Event.NewNote(pubKey, note);
|
let ev = await publisher.note(note);
|
||||||
await ev.Sign(privKey);
|
|
||||||
|
|
||||||
console.debug("Sending note: ", ev);
|
console.debug("Sending note: ", ev);
|
||||||
system.BroadcastEvent(ev);
|
publisher.broadcast(ev);
|
||||||
setNote("");
|
setNote("");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
54
src/pages/feed/EventPublisher.js
Normal file
54
src/pages/feed/EventPublisher.js
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
import { useContext } from "react";
|
||||||
|
import { useSelector } from "react-redux";
|
||||||
|
import { NostrContext } from "../..";
|
||||||
|
import Event from "../../nostr/Event";
|
||||||
|
import EventKind from "../../nostr/EventKind";
|
||||||
|
import Tag from "../../nostr/Tag";
|
||||||
|
|
||||||
|
export default function useEventPublisher() {
|
||||||
|
const system = useContext(NostrContext);
|
||||||
|
const pubKey = useSelector(s => s.login.publicKey);
|
||||||
|
const privKey = useSelector(s => s.login.privateKey);
|
||||||
|
|
||||||
|
return {
|
||||||
|
broadcast: (ev) => {
|
||||||
|
console.debug("Sending event: ", ev);
|
||||||
|
system.BroadcastEvent(ev);
|
||||||
|
},
|
||||||
|
metadata: async (obj) => {
|
||||||
|
let ev = Event.ForPubKey(pubKey);
|
||||||
|
ev.Kind = EventKind.SetMetadata;
|
||||||
|
ev.Content = JSON.stringify(obj);
|
||||||
|
await ev.Sign(privKey);
|
||||||
|
return ev;
|
||||||
|
},
|
||||||
|
note: async (msg) => {
|
||||||
|
if(typeof msg !== "string") {
|
||||||
|
throw "Must be text!";
|
||||||
|
}
|
||||||
|
let ev = Event.ForPubKey(pubKey);
|
||||||
|
ev.Kind = EventKind.TextNote;
|
||||||
|
ev.Content = msg;
|
||||||
|
await ev.Sign(privKey);
|
||||||
|
return ev;
|
||||||
|
},
|
||||||
|
like: async (evRef) => {
|
||||||
|
let ev = Event.ForPubKey(pubKey);
|
||||||
|
ev.Kind = EventKind.Reaction;
|
||||||
|
ev.Content = "+";
|
||||||
|
ev.Tags.push(new Tag(["e", evRef.Id], 0));
|
||||||
|
ev.Tags.push(new Tag(["p", evRef.PubKey], 1));
|
||||||
|
await ev.Sign(privKey);
|
||||||
|
return ev;
|
||||||
|
},
|
||||||
|
dislike: async (evRef) => {
|
||||||
|
let ev = Event.ForPubKey(pubKey);
|
||||||
|
ev.Kind = EventKind.Reaction;
|
||||||
|
ev.Content = "-";
|
||||||
|
ev.Tags.push(new Tag(["e", evRef.Id], 0));
|
||||||
|
ev.Tags.push(new Tag(["p", evRef.PubKey], 1));
|
||||||
|
await ev.Sign(privKey);
|
||||||
|
return ev;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user