From 00b0cecf6c69b726ec645b105ba6f9f61666f8d0 Mon Sep 17 00:00:00 2001 From: Kieran Date: Wed, 28 Dec 2022 14:51:33 +0000 Subject: [PATCH] Profile editor --- src/element/Note.js | 2 +- src/nostr/Connection.js | 6 ++- src/nostr/Event.js | 27 +++++++++++++ src/nostr/Subscriptions.js | 8 ++++ src/nostr/System.js | 20 +++++----- src/pages/ProfilePage.css | 18 +++++++-- src/pages/ProfilePage.js | 71 +++++++++++++++++++++++++++++++---- src/pages/feed/ProfileFeed.js | 4 +- src/state/Users.js | 10 ++++- 9 files changed, 140 insertions(+), 26 deletions(-) diff --git a/src/element/Note.js b/src/element/Note.js index 0ccb2bd..773c587 100644 --- a/src/element/Note.js +++ b/src/element/Note.js @@ -83,7 +83,7 @@ export default function Note(props) { } else { let mentions = a.split(MentionRegex).map((match) => { if (match.startsWith("#")) { - let idx = parseInt(match.match(/\[(\d+)\]/)[1]) - 1; + let idx = parseInt(match.match(/\[(\d+)\]/)[1]); let pref = pTags[idx]; if (pref) { let pUser = users[pref.PubKey]?.name ?? pref.PubKey.substring(0, 8); diff --git a/src/nostr/Connection.js b/src/nostr/Connection.js index 05cefdd..e191e7d 100644 --- a/src/nostr/Connection.js +++ b/src/nostr/Connection.js @@ -45,8 +45,12 @@ export default class Connection { console.log(e); } + /** + * Send event on this connection + * @param {Event} e + */ SendEvent(e) { - let req = ["EVENT", e]; + let req = ["EVENT", e.ToObject()]; this._SendJson(req); } diff --git a/src/nostr/Event.js b/src/nostr/Event.js index 2f04684..faf3b0f 100644 --- a/src/nostr/Event.js +++ b/src/nostr/Event.js @@ -57,6 +57,9 @@ export default class Event { let sig = await secp.schnorr.sign(this.Id, key); this.Signature = secp.utils.bytesToHex(sig); + if(!await this.Verify()) { + throw "Signing failed"; + } } /** @@ -135,4 +138,28 @@ export default class Event { sig: this.Signature }; } + + /** + * Create a new event for a specific pubkey + * @param {String} pubKey + */ + static ForPubKey(pubKey) { + let ev = new Event(); + ev.CreatedAt = parseInt(new Date().getTime() / 1000); + ev.PubKey = pubKey; + return ev; + } + + /** + * Create new SetMetadata event + * @param {String} pubKey Pubkey of the creator of this event + * @param {any} obj Metadata content + * @returns {Event} + */ + static SetMetadata(pubKey, obj) { + let ev = Event.ForPubKey(pubKey); + ev.Kind = EventKind.SetMetadata; + ev.Content = JSON.stringify(obj); + return ev; + } } \ No newline at end of file diff --git a/src/nostr/Subscriptions.js b/src/nostr/Subscriptions.js index 95f39af..06bd028 100644 --- a/src/nostr/Subscriptions.js +++ b/src/nostr/Subscriptions.js @@ -83,6 +83,14 @@ export class Subscriptions { this.OrSubs.push(sub); } + /** + * If all relays have responded with EOSE + * @returns {boolean} + */ + IsFinished() { + return Object.keys(this.Started).length === Object.keys(this.Finished).length; + } + static FromObject(obj) { let ret = new Subscriptions(); ret.Ids = new Set(obj.ids); diff --git a/src/nostr/System.js b/src/nostr/System.js index 1a32647..f5e913d 100644 --- a/src/nostr/System.js +++ b/src/nostr/System.js @@ -8,6 +8,7 @@ export class NostrSystem { constructor() { this.Sockets = {}; this.Subscriptions = {}; + this.PendingSubscriptions = []; } /** @@ -38,6 +39,10 @@ export class NostrSystem { delete this.Subscriptions[subId]; } + /** + * Send events to writable relays + * @param {Event} ev + */ BroadcastEvent(ev) { for (let s of Object.values(this.Sockets)) { s.SendEvent(ev); @@ -51,15 +56,11 @@ export class NostrSystem { */ RequestSubscription(sub) { return new Promise((resolve, reject) => { - let counter = 0; let events = []; // force timeout returning current results let timeout = setTimeout(() => { - for (let s of Object.values(this.Sockets)) { - s.RemoveSubscription(sub.Id); - counter++; - } + this.RemoveSubscription(sub.Id); resolve(events); }, 10_000); @@ -74,16 +75,13 @@ export class NostrSystem { }; sub.OnEnd = (c) => { c.RemoveSubscription(sub.Id); - console.debug(counter); - if (counter-- <= 0) { + if (sub.IsFinished()) { clearInterval(timeout); + console.debug(`[${sub.Id}] Finished`); resolve(events); } }; - for (let s of Object.values(this.Sockets)) { - s.AddSubscription(sub); - counter++; - } + this.AddSubscription(sub); }); } } \ No newline at end of file diff --git a/src/pages/ProfilePage.css b/src/pages/ProfilePage.css index 35985dd..fcc84d7 100644 --- a/src/pages/ProfilePage.css +++ b/src/pages/ProfilePage.css @@ -7,11 +7,23 @@ margin-left: 10px; } -.profile img.avatar { +.profile .avatar { width: 256px; height: 256px; + background-size: contain; + cursor: pointer; } -.profile img.avatar:hover { - cursor: pointer; +.profile .avatar .edit { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + opacity: 0; + background-color: black; +} + +.profile .avatar .edit:hover { + opacity: 0.5; } \ No newline at end of file diff --git a/src/pages/ProfilePage.js b/src/pages/ProfilePage.js index 0f7615f..5398e75 100644 --- a/src/pages/ProfilePage.js +++ b/src/pages/ProfilePage.js @@ -1,20 +1,55 @@ import "./ProfilePage.css"; -import { useSelector } from "react-redux"; +import { useDispatch, useSelector } from "react-redux"; import { useParams } from "react-router-dom"; import useProfile from "./feed/ProfileFeed"; -import useProfileFeed from "./feed/ProfileFeed"; -import { useState } from "react"; +import { useContext, useEffect, useState } from "react"; +import Event from "../nostr/Event"; +import { NostrContext } from ".."; +import { resetProfile } from "../state/Users"; export default function ProfilePage() { + const system = useContext(NostrContext); + const dispatch = useDispatch(); const params = useParams(); const id = params.id; const user = useProfile(id); const loginPubKey = useSelector(s => s.login.publicKey); + const privKey = useSelector(s => s.login.privateKey); const isMe = loginPubKey === id; - let [name, setName] = useState(user?.name); - let [about, setAbout] = useState(user?.amount); - let [website, setWebsite] = useState(user?.website); + let [name, setName] = useState(""); + let [picture, setPicture] = useState(""); + let [about, setAbout] = useState(""); + let [website, setWebsite] = useState(""); + let [nip05, setNip05] = useState(""); + let [lud16, setLud16] = useState(""); + + useEffect(() => { + if (user) { + setName(user.name ?? ""); + setPicture(user.picture ?? ""); + setAbout(user.about ?? ""); + setWebsite(user.website ?? ""); + setNip05(user.nip05 ?? ""); + setLud16(user.lud16 ?? ""); + } + }, [user]); + + async function saveProfile() { + let ev = Event.SetMetadata(id, { + name, + about, + picture, + website, + nip05, + lud16 + }); + await ev.Sign(privKey); + + console.debug(ev); + system.BroadcastEvent(ev); + dispatch(resetProfile(id)); + } function editor() { return ( @@ -37,6 +72,24 @@ export default function ProfilePage() { setWebsite(e.target.value)} /> +
+
NIP-05:
+
+ setNip05(e.target.value)} /> +
+
+
+
Lightning Address:
+
+ setLud16(e.target.value)} /> +
+
+
+
+
+
saveProfile()}>Save
+
+
) } @@ -44,7 +97,11 @@ export default function ProfilePage() { return (
- +
+
+
Edit
+
+
{isMe ? editor() : null} diff --git a/src/pages/feed/ProfileFeed.js b/src/pages/feed/ProfileFeed.js index d64fb29..3589a90 100644 --- a/src/pages/feed/ProfileFeed.js +++ b/src/pages/feed/ProfileFeed.js @@ -10,10 +10,10 @@ export default function useProfile(pubKey) { const pubKeys = useSelector(s => s.users.pubKeys); useEffect(() => { - if (!pubKeys.includes(pubKey)) { + if (system && !pubKeys.includes(pubKey)) { dispatch(addPubKey(pubKey)); } - }, []); + }, [system]); return user; } \ No newline at end of file diff --git a/src/state/Users.js b/src/state/Users.js index ac40228..b7ca0c5 100644 --- a/src/state/Users.js +++ b/src/state/Users.js @@ -52,6 +52,14 @@ const UsersSlice = createSlice({ state.users[x.pubkey] = x; window.localStorage.setItem(`user:${x.pubkey}`, JSON.stringify(x)); + state.users = { + ...state.users + }; + } + }, + resetProfile: (state, action) => { + if(state.users[action.payload]) { + delete state.users[action.payload]; state.users = { ...state.users }; @@ -60,5 +68,5 @@ const UsersSlice = createSlice({ } }); -export const { addPubKey, setUserData } = UsersSlice.actions; +export const { addPubKey, setUserData, resetProfile } = UsersSlice.actions; export const reducer = UsersSlice.reducer; \ No newline at end of file