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