Use timeline element (Deletions and reactions)

This commit is contained in:
Kieran 2023-01-04 13:23:05 +00:00
parent 75cf4dedf0
commit b8a6ad3f0c
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
11 changed files with 101 additions and 32 deletions

View File

@ -3,7 +3,7 @@ import { useCallback, useState } from "react";
import { useSelector } from "react-redux";
import moment from "moment";
import { Link, useNavigate } from "react-router-dom";
import { faHeart, faReply, faInfo } from "@fortawesome/free-solid-svg-icons";
import { faHeart, faReply, faInfo, faTrash } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import Event from "../nostr/Event";
@ -19,10 +19,13 @@ export default function Note(props) {
const opt = props.options;
const dataEvent = props["data-ev"];
const reactions = props.reactions;
const deletion = props.deletion;
const publisher = useEventPublisher();
const [showReply, setShowReply] = useState(false);
const users = useSelector(s => s.users?.users);
const login = useSelector(s => s.login.publicKey);
const ev = dataEvent ?? Event.FromObject(data);
const isMine = ev.PubKey === login;
const options = {
showHeader: true,
@ -36,8 +39,16 @@ export default function Note(props) {
let fragments = extractLinks([body]);
fragments = extractMentions(fragments);
return extractInvoices(fragments);
}, [data, dataEvent]);
fragments = extractInvoices(fragments);
if (deletion?.length > 0) {
return (
<>
<b className="error">Deleted</b>
</>
);
}
return fragments;
}, [data, dataEvent, reactions, deletion]);
function goToEvent(e, id) {
if (!window.location.pathname.startsWith("/e/")) {
@ -150,6 +161,13 @@ export default function Note(props) {
publisher.broadcast(evLike);
}
async function deleteEvent() {
if (window.confirm(`Are you sure you want to delete ${ev.Id.substring(0, 8)}?`)) {
let evDelete = await publisher.delete(ev.Id);
publisher.broadcast(evDelete);
}
}
if (!ev.IsContent()) {
return (
<>
@ -175,6 +193,9 @@ export default function Note(props) {
</div>
{options.showFooter ?
<div className="footer">
{isMine ? <span className="pill">
<FontAwesomeIcon icon={faTrash} onClick={() => deleteEvent()} />
</span> : null}
<span className="pill" onClick={() => setShowReply(!showReply)}>
<FontAwesomeIcon icon={faReply} />
</span>

View File

@ -12,8 +12,8 @@ export default function Thread(props) {
// root note has no thread info
const root = notes.find(a => a.GetThread() === null);
function reactions(id) {
return notes?.filter(a => a.Kind === EventKind.Reaction && a.Tags.find(a => a.Key === "e").Event === id);
function reactions(id, kind = EventKind.Reaction) {
return notes?.filter(a => a.Kind === kind && a.Tags.find(a => a.Key === "e" && a.Event === id));
}
const repliesToRoot = notes?.
@ -26,9 +26,9 @@ export default function Thread(props) {
{root === undefined ?
<NoteGhost text={`Loading... (${notes.length} events loaded)`}/>
: <Note data-ev={root} reactions={reactions(root?.Id)} />}
{thisNote && !thisIsRootNote ? <Note data-ev={thisNote} reactions={reactions(thisNote.Id)}/> : null}
{thisNote && !thisIsRootNote ? <Note data-ev={thisNote} reactions={reactions(thisNote.Id)} deletion={reactions(thisNote.Id, EventKind.Deletion)}/> : null}
<h4>Other Replies</h4>
{repliesToRoot?.map(a => <Note key={a.Id} data-ev={a} reactions={reactions(a.Id)} />)}
{repliesToRoot?.map(a => <Note key={a.Id} data-ev={a} reactions={reactions(a.Id)} deletion={reactions(a.Id, EventKind.Deletion)}/>)}
</>
);
}

23
src/element/Timeline.js Normal file
View File

@ -0,0 +1,23 @@
import useTimelineFeed from "../feed/TimelineFeed";
import EventKind from "../nostr/EventKind";
import Note from "./Note";
/**
* A list of notes by pubkeys
*/
export default function Timeline(props) {
const pubkeys = props.pubkeys;
const global = props.global;
const feed = useTimelineFeed(pubkeys, global ?? false);
function reaction(id, kind = EventKind.Reaction) {
return feed?.others?.filter(a => a.kind === kind && a.tags.some(b => b[0] === "e" && b[1] === id));
}
return (
<>
{feed.main?.sort((a, b) => b.created_at - a.created_at)
.map(a => <Note key={a.id} data={a} reactions={reaction(a.id)} deletion={reaction(a.id, EventKind.Deletion)} />)}
</>
)
}

View File

@ -17,7 +17,7 @@ export default function useEventPublisher() {
* @param {*} privKey
* @returns
*/
async function signEvent(ev, privKey) {
async function signEvent(ev) {
if (hasNip07 && !privKey) {
ev.Id = await ev.CreateId();
let tmpEv = await window.nostr.signEvent(ev.ToObject());
@ -46,7 +46,7 @@ export default function useEventPublisher() {
let ev = Event.ForPubKey(pubKey);
ev.Kind = EventKind.TextNote;
ev.Content = msg;
return await signEvent(ev, privKey);
return await signEvent(ev);
},
/**
* Reply to a note
@ -78,7 +78,7 @@ export default function useEventPublisher() {
ev.Tags.push(new Tag(["e", replyTo.Id, "", "reply"], 0));
ev.Tags.push(new Tag(["p", replyTo.PubKey], 1));
}
return await signEvent(ev, privKey);
return await signEvent(ev);
},
like: async (evRef) => {
let ev = Event.ForPubKey(pubKey);
@ -86,7 +86,7 @@ export default function useEventPublisher() {
ev.Content = "+";
ev.Tags.push(new Tag(["e", evRef.Id], 0));
ev.Tags.push(new Tag(["p", evRef.PubKey], 1));
return await signEvent(ev, privKey);
return await signEvent(ev);
},
dislike: async (evRef) => {
let ev = Event.ForPubKey(pubKey);
@ -94,31 +94,38 @@ export default function useEventPublisher() {
ev.Content = "-";
ev.Tags.push(new Tag(["e", evRef.Id], 0));
ev.Tags.push(new Tag(["p", evRef.PubKey], 1));
return await signEvent(ev, privKey);
return await signEvent(ev);
},
addFollow: async (pkAdd) => {
let ev = Event.ForPubKey(pubKey);
ev.Kind = EventKind.ContactList;
ev.Content = JSON.stringify(relays);
for(let pk of follows) {
for (let pk of follows) {
ev.Tags.push(new Tag(["p", pk]));
}
ev.Tags.push(new Tag(["p", pkAdd]));
return await signEvent(ev, privKey);
return await signEvent(ev);
},
removeFollow: async (pkRemove) => {
let ev = Event.ForPubKey(pubKey);
ev.Kind = EventKind.ContactList;
ev.Content = JSON.stringify(relays);
for(let pk of follows) {
if(pk === pkRemove) {
for (let pk of follows) {
if (pk === pkRemove) {
continue;
}
ev.Tags.push(new Tag(["p", pk]));
}
return await signEvent(ev, privKey);
return await signEvent(ev);
},
delete: async (id) => {
let ev = Event.ForPubKey(pubKey);
ev.Kind = EventKind.Deletion;
ev.Content = "";
ev.Tags.push(new Tag(["e", id]));
return await signEvent(ev);
}
}
}

View File

@ -25,6 +25,7 @@ export default function useLoginFeed() {
sub.Authors.add(pubKey);
sub.Kinds.add(EventKind.ContactList);
sub.Kinds.add(EventKind.SetMetadata);
sub.Kinds.add(EventKind.Deletion);
let notifications = new Subscriptions();
notifications.Kinds.add(EventKind.TextNote);

View File

@ -22,6 +22,21 @@ export default function useTimelineFeed(pubKeys, global = false) {
return sub;
}, [pubKeys]);
const { notes } = useSubscription(sub, { leaveOpen: true });
return { notes };
const main = useSubscription(sub, { leaveOpen: true });
const subNext = useMemo(() => {
if (main.notes.length > 0) {
let sub = new Subscriptions();
sub.Id = `timeline-related:${sub.Id}`;
sub.Kinds.add(EventKind.Reaction);
sub.Kinds.add(EventKind.Deletion);
sub.ETags = new Set(main.notes.map(a => a.id));
return sub;
}
}, [main]);
const others = useSubscription(subNext, { leaveOpen: true });
return { main: main.notes, others: others.notes };
}

View File

@ -108,7 +108,7 @@ a {
span.pill {
display: inline-block;
background-color: #333;
padding: 0 10px;
padding: 2px 10px;
border-radius: 10px;
user-select: none;
margin: 2px 5px;

View File

@ -147,7 +147,8 @@ export default class Connection {
//this._VerifySig(ev);
this.Subscriptions[subId].OnEvent(ev);
} else {
console.warn(`No subscription for event! ${subId}`);
// console.warn(`No subscription for event! ${subId}`);
// ignored for now, track as "dropped event" with connection stats
}
}
@ -161,7 +162,8 @@ export default class Connection {
}
sub.OnEnd(this);
} else {
console.warn(`No subscription for end! ${subId}`);
// console.warn(`No subscription for end! ${subId}`);
// ignored for now, track as "dropped event" with connection stats
}
}

View File

@ -5,6 +5,7 @@ const EventKind = {
RecommendServer: 2,
ContactList: 3, // NIP-02
DirectMessage: 4, // NIP-04
Deletion: 5,
Reaction: 7 // NIP-25
};

View File

@ -10,14 +10,13 @@ import useProfile from "../feed/ProfileFeed";
import { resetProfile } from "../state/Users";
import Nostrich from "../nostrich.jpg";
import useEventPublisher from "../feed/EventPublisher";
import useTimelineFeed from "../feed/TimelineFeed";
import Note from "../element/Note";
import QRCodeStyling from "qr-code-styling";
import Modal from "../element/Modal";
import { logout } from "../state/Login";
import FollowButton from "../element/FollowButton";
import VoidUpload from "../feed/VoidUpload";
import { openFile } from "../Util";
import Timeline from "../element/Timeline";
export default function ProfilePage() {
const dispatch = useDispatch();
@ -25,7 +24,6 @@ export default function ProfilePage() {
const id = params.id;
const user = useProfile(id);
const publisher = useEventPublisher();
const { notes } = useTimelineFeed(id);
const loginPubKey = useSelector(s => s.login.publicKey);
const isMe = loginPubKey === id;
const qrRef = useRef();
@ -110,6 +108,7 @@ export default function ProfilePage() {
let ev = await publisher.metadata(userCopy);
console.debug(ev);
dispatch(resetProfile(id));
publisher.broadcast(ev);
}
@ -220,7 +219,7 @@ export default function ProfilePage() {
<div className="btn">Follows</div>
<div className="btn">Relays</div>
</div>
{notes?.sort((a, b) => b.created_at - a.created_at).map(a => <Note key={a.id} data={a} />)}
<Timeline pubkeys={id} />
</>
)
}

View File

@ -1,16 +1,16 @@
import { useSelector } from "react-redux";
import Note from "../element/Note";
import useTimelineFeed from "../feed/TimelineFeed";
import { Link } from "react-router-dom";
import { NoteCreator } from "../element/NoteCreator";
import Timeline from "../element/Timeline";
export default function RootPage() {
const [loggedOut, pubKey, follows] = useSelector(s => [s.login.loggedOut, s.login.publicKey, s.login.follows]);
const { notes } = useTimelineFeed(follows, loggedOut === true);
function followHints() {
if (follows?.length === 0 && pubKey) {
return <>Hmm nothing here..</>
return <>
Hmm nothing here.. Checkout <Link to={"/new"}>New users page</Link> to follow some recommended nostrich's!
</>
}
}
@ -18,7 +18,7 @@ export default function RootPage() {
<>
{pubKey ? <NoteCreator /> : null}
{followHints()}
{notes?.sort((a, b) => b.created_at - a.created_at).map(e => <Note key={e.id} data={e} />)}
<Timeline pubkeys={follows} global={loggedOut === true} />
</>
);
}