diff --git a/src/element/Note.js b/src/element/Note.js index 37644fd0..3e5829e4 100644 --- a/src/element/Note.js +++ b/src/element/Note.js @@ -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 ( + <> + Deleted + + ); + } + 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) { {options.showFooter ?
+ {isMine ? + deleteEvent()} /> + : null} setShowReply(!showReply)}> diff --git a/src/element/Thread.js b/src/element/Thread.js index 41539f03..610743d1 100644 --- a/src/element/Thread.js +++ b/src/element/Thread.js @@ -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 ? : } - {thisNote && !thisIsRootNote ? : null} + {thisNote && !thisIsRootNote ? : null}

Other Replies

- {repliesToRoot?.map(a => )} + {repliesToRoot?.map(a => )} ); } \ No newline at end of file diff --git a/src/element/Timeline.js b/src/element/Timeline.js new file mode 100644 index 00000000..0052bcdf --- /dev/null +++ b/src/element/Timeline.js @@ -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 => )} + + ) +} \ No newline at end of file diff --git a/src/feed/EventPublisher.js b/src/feed/EventPublisher.js index c3bf0ee4..5a1016b4 100644 --- a/src/feed/EventPublisher.js +++ b/src/feed/EventPublisher.js @@ -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); } } } \ No newline at end of file diff --git a/src/feed/LoginFeed.js b/src/feed/LoginFeed.js index 504f2570..657164a5 100644 --- a/src/feed/LoginFeed.js +++ b/src/feed/LoginFeed.js @@ -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); diff --git a/src/feed/TimelineFeed.js b/src/feed/TimelineFeed.js index a1e38edb..ac6d7bbe 100644 --- a/src/feed/TimelineFeed.js +++ b/src/feed/TimelineFeed.js @@ -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 }; } \ No newline at end of file diff --git a/src/index.css b/src/index.css index b7210321..f3bc91c7 100644 --- a/src/index.css +++ b/src/index.css @@ -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; diff --git a/src/nostr/Connection.js b/src/nostr/Connection.js index 74ea4c3a..16e96e77 100644 --- a/src/nostr/Connection.js +++ b/src/nostr/Connection.js @@ -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 } } diff --git a/src/nostr/EventKind.js b/src/nostr/EventKind.js index fa8c880c..e6acbd6e 100644 --- a/src/nostr/EventKind.js +++ b/src/nostr/EventKind.js @@ -5,6 +5,7 @@ const EventKind = { RecommendServer: 2, ContactList: 3, // NIP-02 DirectMessage: 4, // NIP-04 + Deletion: 5, Reaction: 7 // NIP-25 }; diff --git a/src/pages/ProfilePage.js b/src/pages/ProfilePage.js index 7163afe7..39a70988 100644 --- a/src/pages/ProfilePage.js +++ b/src/pages/ProfilePage.js @@ -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() {
Follows
Relays
- {notes?.sort((a, b) => b.created_at - a.created_at).map(a => )} + ) } \ No newline at end of file diff --git a/src/pages/Root.js b/src/pages/Root.js index 2659d26f..ec63bd37 100644 --- a/src/pages/Root.js +++ b/src/pages/Root.js @@ -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 New users page to follow some recommended nostrich's! + } } @@ -18,7 +18,7 @@ export default function RootPage() { <> {pubKey ? : null} {followHints()} - {notes?.sort((a, b) => b.created_at - a.created_at).map(e => )} + ); } \ No newline at end of file