forked from Kieran/snort
Use timeline element (Deletions and reactions)
This commit is contained in:
parent
75cf4dedf0
commit
b8a6ad3f0c
@ -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>
|
||||
|
@ -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
23
src/element/Timeline.js
Normal 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)} />)}
|
||||
</>
|
||||
)
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -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 };
|
||||
}
|
@ -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;
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -5,6 +5,7 @@ const EventKind = {
|
||||
RecommendServer: 2,
|
||||
ContactList: 3, // NIP-02
|
||||
DirectMessage: 4, // NIP-04
|
||||
Deletion: 5,
|
||||
Reaction: 7 // NIP-25
|
||||
};
|
||||
|
||||
|
@ -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} />
|
||||
</>
|
||||
)
|
||||
}
|
@ -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} />
|
||||
</>
|
||||
);
|
||||
}
|
Loading…
Reference in New Issue
Block a user