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 { 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 { 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 { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
import Event from "../nostr/Event";
|
import Event from "../nostr/Event";
|
||||||
@ -19,10 +19,13 @@ export default function Note(props) {
|
|||||||
const opt = props.options;
|
const opt = props.options;
|
||||||
const dataEvent = props["data-ev"];
|
const dataEvent = props["data-ev"];
|
||||||
const reactions = props.reactions;
|
const reactions = props.reactions;
|
||||||
|
const deletion = props.deletion;
|
||||||
const publisher = useEventPublisher();
|
const publisher = useEventPublisher();
|
||||||
const [showReply, setShowReply] = useState(false);
|
const [showReply, setShowReply] = useState(false);
|
||||||
const users = useSelector(s => s.users?.users);
|
const users = useSelector(s => s.users?.users);
|
||||||
|
const login = useSelector(s => s.login.publicKey);
|
||||||
const ev = dataEvent ?? Event.FromObject(data);
|
const ev = dataEvent ?? Event.FromObject(data);
|
||||||
|
const isMine = ev.PubKey === login;
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
showHeader: true,
|
showHeader: true,
|
||||||
@ -36,8 +39,16 @@ export default function Note(props) {
|
|||||||
|
|
||||||
let fragments = extractLinks([body]);
|
let fragments = extractLinks([body]);
|
||||||
fragments = extractMentions(fragments);
|
fragments = extractMentions(fragments);
|
||||||
return extractInvoices(fragments);
|
fragments = extractInvoices(fragments);
|
||||||
}, [data, dataEvent]);
|
if (deletion?.length > 0) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<b className="error">Deleted</b>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return fragments;
|
||||||
|
}, [data, dataEvent, reactions, deletion]);
|
||||||
|
|
||||||
function goToEvent(e, id) {
|
function goToEvent(e, id) {
|
||||||
if (!window.location.pathname.startsWith("/e/")) {
|
if (!window.location.pathname.startsWith("/e/")) {
|
||||||
@ -150,6 +161,13 @@ export default function Note(props) {
|
|||||||
publisher.broadcast(evLike);
|
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()) {
|
if (!ev.IsContent()) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -175,6 +193,9 @@ export default function Note(props) {
|
|||||||
</div>
|
</div>
|
||||||
{options.showFooter ?
|
{options.showFooter ?
|
||||||
<div className="footer">
|
<div className="footer">
|
||||||
|
{isMine ? <span className="pill">
|
||||||
|
<FontAwesomeIcon icon={faTrash} onClick={() => deleteEvent()} />
|
||||||
|
</span> : null}
|
||||||
<span className="pill" onClick={() => setShowReply(!showReply)}>
|
<span className="pill" onClick={() => setShowReply(!showReply)}>
|
||||||
<FontAwesomeIcon icon={faReply} />
|
<FontAwesomeIcon icon={faReply} />
|
||||||
</span>
|
</span>
|
||||||
|
@ -12,8 +12,8 @@ export default function Thread(props) {
|
|||||||
// root note has no thread info
|
// root note has no thread info
|
||||||
const root = notes.find(a => a.GetThread() === null);
|
const root = notes.find(a => a.GetThread() === null);
|
||||||
|
|
||||||
function reactions(id) {
|
function reactions(id, kind = EventKind.Reaction) {
|
||||||
return notes?.filter(a => a.Kind === EventKind.Reaction && a.Tags.find(a => a.Key === "e").Event === id);
|
return notes?.filter(a => a.Kind === kind && a.Tags.find(a => a.Key === "e" && a.Event === id));
|
||||||
}
|
}
|
||||||
|
|
||||||
const repliesToRoot = notes?.
|
const repliesToRoot = notes?.
|
||||||
@ -26,9 +26,9 @@ export default function Thread(props) {
|
|||||||
{root === undefined ?
|
{root === undefined ?
|
||||||
<NoteGhost text={`Loading... (${notes.length} events loaded)`}/>
|
<NoteGhost text={`Loading... (${notes.length} events loaded)`}/>
|
||||||
: <Note data-ev={root} reactions={reactions(root?.Id)} />}
|
: <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>
|
<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
|
* @param {*} privKey
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
async function signEvent(ev, privKey) {
|
async function signEvent(ev) {
|
||||||
if (hasNip07 && !privKey) {
|
if (hasNip07 && !privKey) {
|
||||||
ev.Id = await ev.CreateId();
|
ev.Id = await ev.CreateId();
|
||||||
let tmpEv = await window.nostr.signEvent(ev.ToObject());
|
let tmpEv = await window.nostr.signEvent(ev.ToObject());
|
||||||
@ -46,7 +46,7 @@ export default function useEventPublisher() {
|
|||||||
let ev = Event.ForPubKey(pubKey);
|
let ev = Event.ForPubKey(pubKey);
|
||||||
ev.Kind = EventKind.TextNote;
|
ev.Kind = EventKind.TextNote;
|
||||||
ev.Content = msg;
|
ev.Content = msg;
|
||||||
return await signEvent(ev, privKey);
|
return await signEvent(ev);
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* Reply to a note
|
* 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(["e", replyTo.Id, "", "reply"], 0));
|
||||||
ev.Tags.push(new Tag(["p", replyTo.PubKey], 1));
|
ev.Tags.push(new Tag(["p", replyTo.PubKey], 1));
|
||||||
}
|
}
|
||||||
return await signEvent(ev, privKey);
|
return await signEvent(ev);
|
||||||
},
|
},
|
||||||
like: async (evRef) => {
|
like: async (evRef) => {
|
||||||
let ev = Event.ForPubKey(pubKey);
|
let ev = Event.ForPubKey(pubKey);
|
||||||
@ -86,7 +86,7 @@ export default function useEventPublisher() {
|
|||||||
ev.Content = "+";
|
ev.Content = "+";
|
||||||
ev.Tags.push(new Tag(["e", evRef.Id], 0));
|
ev.Tags.push(new Tag(["e", evRef.Id], 0));
|
||||||
ev.Tags.push(new Tag(["p", evRef.PubKey], 1));
|
ev.Tags.push(new Tag(["p", evRef.PubKey], 1));
|
||||||
return await signEvent(ev, privKey);
|
return await signEvent(ev);
|
||||||
},
|
},
|
||||||
dislike: async (evRef) => {
|
dislike: async (evRef) => {
|
||||||
let ev = Event.ForPubKey(pubKey);
|
let ev = Event.ForPubKey(pubKey);
|
||||||
@ -94,31 +94,38 @@ export default function useEventPublisher() {
|
|||||||
ev.Content = "-";
|
ev.Content = "-";
|
||||||
ev.Tags.push(new Tag(["e", evRef.Id], 0));
|
ev.Tags.push(new Tag(["e", evRef.Id], 0));
|
||||||
ev.Tags.push(new Tag(["p", evRef.PubKey], 1));
|
ev.Tags.push(new Tag(["p", evRef.PubKey], 1));
|
||||||
return await signEvent(ev, privKey);
|
return await signEvent(ev);
|
||||||
},
|
},
|
||||||
addFollow: async (pkAdd) => {
|
addFollow: async (pkAdd) => {
|
||||||
let ev = Event.ForPubKey(pubKey);
|
let ev = Event.ForPubKey(pubKey);
|
||||||
ev.Kind = EventKind.ContactList;
|
ev.Kind = EventKind.ContactList;
|
||||||
ev.Content = JSON.stringify(relays);
|
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", pk]));
|
||||||
}
|
}
|
||||||
ev.Tags.push(new Tag(["p", pkAdd]));
|
ev.Tags.push(new Tag(["p", pkAdd]));
|
||||||
|
|
||||||
return await signEvent(ev, privKey);
|
return await signEvent(ev);
|
||||||
},
|
},
|
||||||
removeFollow: async (pkRemove) => {
|
removeFollow: async (pkRemove) => {
|
||||||
let ev = Event.ForPubKey(pubKey);
|
let ev = Event.ForPubKey(pubKey);
|
||||||
ev.Kind = EventKind.ContactList;
|
ev.Kind = EventKind.ContactList;
|
||||||
ev.Content = JSON.stringify(relays);
|
ev.Content = JSON.stringify(relays);
|
||||||
for(let pk of follows) {
|
for (let pk of follows) {
|
||||||
if(pk === pkRemove) {
|
if (pk === pkRemove) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
ev.Tags.push(new Tag(["p", pk]));
|
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.Authors.add(pubKey);
|
||||||
sub.Kinds.add(EventKind.ContactList);
|
sub.Kinds.add(EventKind.ContactList);
|
||||||
sub.Kinds.add(EventKind.SetMetadata);
|
sub.Kinds.add(EventKind.SetMetadata);
|
||||||
|
sub.Kinds.add(EventKind.Deletion);
|
||||||
|
|
||||||
let notifications = new Subscriptions();
|
let notifications = new Subscriptions();
|
||||||
notifications.Kinds.add(EventKind.TextNote);
|
notifications.Kinds.add(EventKind.TextNote);
|
||||||
|
@ -22,6 +22,21 @@ export default function useTimelineFeed(pubKeys, global = false) {
|
|||||||
return sub;
|
return sub;
|
||||||
}, [pubKeys]);
|
}, [pubKeys]);
|
||||||
|
|
||||||
const { notes } = useSubscription(sub, { leaveOpen: true });
|
const main = useSubscription(sub, { leaveOpen: true });
|
||||||
return { notes };
|
|
||||||
|
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 {
|
span.pill {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
background-color: #333;
|
background-color: #333;
|
||||||
padding: 0 10px;
|
padding: 2px 10px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
margin: 2px 5px;
|
margin: 2px 5px;
|
||||||
|
@ -147,7 +147,8 @@ export default class Connection {
|
|||||||
//this._VerifySig(ev);
|
//this._VerifySig(ev);
|
||||||
this.Subscriptions[subId].OnEvent(ev);
|
this.Subscriptions[subId].OnEvent(ev);
|
||||||
} else {
|
} 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);
|
sub.OnEnd(this);
|
||||||
} else {
|
} 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,
|
RecommendServer: 2,
|
||||||
ContactList: 3, // NIP-02
|
ContactList: 3, // NIP-02
|
||||||
DirectMessage: 4, // NIP-04
|
DirectMessage: 4, // NIP-04
|
||||||
|
Deletion: 5,
|
||||||
Reaction: 7 // NIP-25
|
Reaction: 7 // NIP-25
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -10,14 +10,13 @@ import useProfile from "../feed/ProfileFeed";
|
|||||||
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";
|
import useEventPublisher from "../feed/EventPublisher";
|
||||||
import useTimelineFeed from "../feed/TimelineFeed";
|
|
||||||
import Note from "../element/Note";
|
|
||||||
import QRCodeStyling from "qr-code-styling";
|
import QRCodeStyling from "qr-code-styling";
|
||||||
import Modal from "../element/Modal";
|
import Modal from "../element/Modal";
|
||||||
import { logout } from "../state/Login";
|
import { logout } from "../state/Login";
|
||||||
import FollowButton from "../element/FollowButton";
|
import FollowButton from "../element/FollowButton";
|
||||||
import VoidUpload from "../feed/VoidUpload";
|
import VoidUpload from "../feed/VoidUpload";
|
||||||
import { openFile } from "../Util";
|
import { openFile } from "../Util";
|
||||||
|
import Timeline from "../element/Timeline";
|
||||||
|
|
||||||
export default function ProfilePage() {
|
export default function ProfilePage() {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
@ -25,7 +24,6 @@ export default function ProfilePage() {
|
|||||||
const id = params.id;
|
const id = params.id;
|
||||||
const user = useProfile(id);
|
const user = useProfile(id);
|
||||||
const publisher = useEventPublisher();
|
const publisher = useEventPublisher();
|
||||||
const { notes } = useTimelineFeed(id);
|
|
||||||
const loginPubKey = useSelector(s => s.login.publicKey);
|
const loginPubKey = useSelector(s => s.login.publicKey);
|
||||||
const isMe = loginPubKey === id;
|
const isMe = loginPubKey === id;
|
||||||
const qrRef = useRef();
|
const qrRef = useRef();
|
||||||
@ -110,6 +108,7 @@ export default function ProfilePage() {
|
|||||||
|
|
||||||
let ev = await publisher.metadata(userCopy);
|
let ev = await publisher.metadata(userCopy);
|
||||||
console.debug(ev);
|
console.debug(ev);
|
||||||
|
dispatch(resetProfile(id));
|
||||||
publisher.broadcast(ev);
|
publisher.broadcast(ev);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -220,7 +219,7 @@ export default function ProfilePage() {
|
|||||||
<div className="btn">Follows</div>
|
<div className="btn">Follows</div>
|
||||||
<div className="btn">Relays</div>
|
<div className="btn">Relays</div>
|
||||||
</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 { useSelector } from "react-redux";
|
||||||
import Note from "../element/Note";
|
import { Link } from "react-router-dom";
|
||||||
import useTimelineFeed from "../feed/TimelineFeed";
|
|
||||||
import { NoteCreator } from "../element/NoteCreator";
|
import { NoteCreator } from "../element/NoteCreator";
|
||||||
|
import Timeline from "../element/Timeline";
|
||||||
|
|
||||||
export default function RootPage() {
|
export default function RootPage() {
|
||||||
const [loggedOut, pubKey, follows] = useSelector(s => [s.login.loggedOut, s.login.publicKey, s.login.follows]);
|
const [loggedOut, pubKey, follows] = useSelector(s => [s.login.loggedOut, s.login.publicKey, s.login.follows]);
|
||||||
|
|
||||||
const { notes } = useTimelineFeed(follows, loggedOut === true);
|
|
||||||
|
|
||||||
function followHints() {
|
function followHints() {
|
||||||
if (follows?.length === 0 && pubKey) {
|
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}
|
{pubKey ? <NoteCreator /> : null}
|
||||||
{followHints()}
|
{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