diff --git a/src/element/FollowButton.js b/src/element/FollowButton.js new file mode 100644 index 000000000..39a2e46ed --- /dev/null +++ b/src/element/FollowButton.js @@ -0,0 +1,21 @@ +import { useSelector } from "react-redux"; +import useEventPublisher from "../feed/EventPublisher"; + +export default function FollowButton(props) { + const pubkey = props.pubkey; + const className = props.className ? `btn ${props.className}` : "btn"; + const publiser = useEventPublisher(); + const follows = useSelector(s => s.login.follows); + + async function follow(pubkey) { + let ev = await publiser.addFollow(pubkey); + publiser.broadcast(ev); + } + + let isFollowing = follows?.includes(pubkey) ?? false; + return ( +
follow(pubkey)}> + {isFollowing ? "Unfollow" : "Follow"} +
+ ) +} \ No newline at end of file diff --git a/src/element/Note.js b/src/element/Note.js index 741079300..7fa2a2a58 100644 --- a/src/element/Note.js +++ b/src/element/Note.js @@ -122,7 +122,7 @@ export default function Note(props) { return (
- +
{moment(ev.CreatedAt * 1000).fromNow()}
diff --git a/src/element/NoteGhost.js b/src/element/NoteGhost.js index 2a20a0f54..19643dbed 100644 --- a/src/element/NoteGhost.js +++ b/src/element/NoteGhost.js @@ -5,7 +5,7 @@ export default function NoteGhost(props) { return (
- +
{props.text ?? "Loading..."} diff --git a/src/element/ProfileImage.js b/src/element/ProfileImage.js index cc298edf0..976d21258 100644 --- a/src/element/ProfileImage.js +++ b/src/element/ProfileImage.js @@ -4,14 +4,15 @@ import useProfile from "../feed/ProfileFeed"; import Nostrich from "../nostrich.jpg"; export default function ProfileImage(props) { - const pubKey = props.pubKey; + const pubKey = props.pubkey; const subHeader = props.subHeader; const navigate = useNavigate(); const user = useProfile(pubKey); + const hasImage = (user?.picture?.length ?? 0) > 0; return (
- navigate(`/p/${pubKey}`)} /> + navigate(`/p/${pubKey}`)} />
{user?.name ?? pubKey.substring(0, 8)} {subHeader} diff --git a/src/element/ProfilePreview.css b/src/element/ProfilePreview.css new file mode 100644 index 000000000..4e030d7c4 --- /dev/null +++ b/src/element/ProfilePreview.css @@ -0,0 +1,9 @@ +.profile-preview { + display: flex; + padding: 5px 0; + align-items: center; +} + +.profile-preview .pfp { + flex-grow: 1; +} \ No newline at end of file diff --git a/src/element/ProfilePreview.js b/src/element/ProfilePreview.js new file mode 100644 index 000000000..74f5b7bcf --- /dev/null +++ b/src/element/ProfilePreview.js @@ -0,0 +1,19 @@ +import "./ProfilePreview.css"; +import ProfileImage from "./ProfileImage"; +import { useSelector } from "react-redux"; +import FollowButton from "./FollowButton"; + +export default function ProfilePreview(props) { + const pubkey = props.pubkey; + const user = useSelector(s => s.users.users[pubkey]); + + return ( +
+ +
+ {user?.about} +
+ +
+ ) +} \ No newline at end of file diff --git a/src/feed/EventPublisher.js b/src/feed/EventPublisher.js index 2a31bf85e..ff89136bf 100644 --- a/src/feed/EventPublisher.js +++ b/src/feed/EventPublisher.js @@ -8,6 +8,8 @@ export default function useEventPublisher() { const pubKey = useSelector(s => s.login.publicKey); const privKey = useSelector(s => s.login.privateKey); const nip07 = useSelector(s => s.login.nip07); + const follows = useSelector(s => s.login.follows); + const relays = useSelector(s => s.login.relays); const hasNip07 = 'nostr' in window; /** @@ -94,6 +96,17 @@ 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); + }, + addFollow: async (pubkey) => { + let ev = Event.ForPubKey(pubKey); + ev.Kind = EventKind.ContactList; + ev.Content = JSON.stringify(relays); + for(let pk of follows) { + ev.Tags.push(new Tag(["p", pk])); + } + ev.Tags.push(new Tag(["p", pubkey])); + return await signEvent(ev, privKey); } } diff --git a/src/feed/Subscription.js b/src/feed/Subscription.js index df7baaeac..f14260070 100644 --- a/src/feed/Subscription.js +++ b/src/feed/Subscription.js @@ -32,12 +32,12 @@ export default function useSubscription(sub, opt) { useEffect(() => { if (sub) { sub.OnEvent = (e) => { + console.debug(e); dispatch(e); }; if (!options.leaveOpen) { sub.OnEnd = (c) => { - sub.OnEvent = () => {}; c.RemoveSubscription(sub.Id); if (sub.IsFinished()) { System.RemoveSubscription(sub.Id); @@ -48,7 +48,7 @@ export default function useSubscription(sub, opt) { console.debug("Adding sub: ", sub.ToObject()); System.AddSubscription(sub); return () => { - console.debug("Adding sub: ", sub.ToObject()); + console.debug("Removing sub: ", sub.ToObject()); System.RemoveSubscription(sub.Id); }; } diff --git a/src/feed/UsersFeed.js b/src/feed/UsersFeed.js index 5d8bedcf8..a928609c2 100644 --- a/src/feed/UsersFeed.js +++ b/src/feed/UsersFeed.js @@ -1,19 +1,18 @@ -import { useEffect, useState } from "react"; +import { useEffect, useMemo } from "react"; import { useDispatch, useSelector } from "react-redux"; -import { System } from ".."; import Event from "../nostr/Event"; import EventKind from "../nostr/EventKind"; import { Subscriptions } from "../nostr/Subscriptions"; import { setUserData } from "../state/Users"; +import useSubscription from "./Subscription"; export default function useUsersCache() { const dispatch = useDispatch(); const pKeys = useSelector(s => s.users.pubKeys); const users = useSelector(s => s.users.users); - const [loading, setLoading] = useState(false); function isUserCached(id) { - let expire = new Date().getTime() - (1_000 * 60 * 5); // 60s expire + let expire = new Date().getTime() - (1_000 * 60 * 5); // 5min expire let u = users[id]; return u && u.loaded > expire; } @@ -29,46 +28,25 @@ export default function useUsersCache() { }; } - async function getUsers() { + const sub = useMemo(() => { let needProfiles = pKeys.filter(a => !isUserCached(a)); if (needProfiles.length === 0) { - return; + return null; } - - let sub = new Subscriptions(); - sub.Id = "profiles"; - sub.Authors = new Set(needProfiles); - sub.Kinds.add(EventKind.SetMetadata); - sub.OnEvent = (ev) => { - dispatch(setUserData(mapEventToProfile(ev))); - }; - let events = await System.RequestSubscription(sub); - let profiles = events - .filter(a => a.kind === EventKind.SetMetadata) - .map(mapEventToProfile); - let missing = needProfiles.filter(a => !events.some(b => b.pubkey === a)); - let missingProfiles = missing.map(a => { - return { - pubkey: a, - loaded: new Date().getTime() - } - }); - dispatch(setUserData([ - ...profiles, - ...missingProfiles - ])); - } + let sub = new Subscriptions(); + sub.Id = `profiles:${sub.Id}`; + sub.Authors = new Set(needProfiles.slice(0, 20)); + sub.Kinds.add(EventKind.SetMetadata); + + return sub; + }, [pKeys]); + + const results = useSubscription(sub); useEffect(() => { - if (pKeys.length > 0 && !loading) { + dispatch(setUserData(results.notes.map(a => mapEventToProfile(a)))); + }, [results]); - setLoading(true); - getUsers() - .catch(console.error) - .then(() => setLoading(false)); - } - }, [pKeys, loading]); - - return { users }; + return results; } \ No newline at end of file diff --git a/src/index.css b/src/index.css index ce1f65b16..94809d3a7 100644 --- a/src/index.css +++ b/src/index.css @@ -85,6 +85,14 @@ input[type="text"], input[type="password"] { flex-grow: 1; } +.f-ellipsis { + min-width: 0; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + padding: 0 15px; +} + a { color: inherit; line-height: 1.3em; @@ -154,6 +162,10 @@ body.scroll-lock { margin-right: 10px; } +.ml5 { + margin-left: 5px; +} + .tabs { display: flex; margin: 10px 0; diff --git a/src/index.js b/src/index.js index 51335a198..75f27ae46 100644 --- a/src/index.js +++ b/src/index.js @@ -17,6 +17,7 @@ import ProfilePage from './pages/ProfilePage'; import RootPage from './pages/Root'; import Store from "./state/Store"; import NotificationsPage from './pages/Notifications'; +import NewUserPage from './pages/NewUserPage'; export const System = new NostrSystem(); @@ -32,6 +33,7 @@ root.render( } /> } /> } /> + } /> diff --git a/src/nostr/Connection.js b/src/nostr/Connection.js index 621cae25d..be85eb2f0 100644 --- a/src/nostr/Connection.js +++ b/src/nostr/Connection.js @@ -142,13 +142,8 @@ export default class Connection { _OnEvent(subId, ev) { if (this.Subscriptions[subId]) { - this._VerifySig(ev) - .then((e) => { - if (this.Subscriptions[subId]) { - this.Subscriptions[subId].OnEvent(e); - } - }) - .catch(console.error); + //this._VerifySig(ev); + this.Subscriptions[subId].OnEvent(ev); } else { console.warn(`No subscription for event! ${subId}`); } @@ -158,13 +153,17 @@ export default class Connection { let sub = this.Subscriptions[subId]; if (sub) { sub.Finished[this.Address] = new Date().getTime(); + let responseTime = sub.Finished[this.Address] - sub.Started[this.Address]; + if (responseTime > 10_000) { + console.warn(`[${this.Address}][${subId}] Slow response time ${(responseTime / 1000).toFixed(1)} seconds`); + } sub.OnEnd(this); } else { console.warn(`No subscription for end! ${subId}`); } } - async _VerifySig(ev) { + _VerifySig(ev) { let payload = [ 0, ev.pubkey, @@ -175,9 +174,9 @@ export default class Connection { ]; let payloadData = new TextEncoder().encode(JSON.stringify(payload)); - let data = await secp.utils.sha256(payloadData); + let data = secp.utils.sha256Sync(payloadData); let hash = secp.utils.bytesToHex(data); - if (!await secp.schnorr.verify(ev.sig, hash, ev.pubkey)) { + if (!secp.schnorr.verifySync(ev.sig, hash, ev.pubkey)) { throw "Sig verify failed"; } return ev; diff --git a/src/pages/Layout.js b/src/pages/Layout.js index 60a3d7344..59801d690 100644 --- a/src/pages/Layout.js +++ b/src/pages/Layout.js @@ -39,7 +39,7 @@ export default function Layout(props) { {notifications?.length ?? 0}
- + ) } diff --git a/src/pages/Login.js b/src/pages/Login.js index 861c34634..f8f2b7064 100644 --- a/src/pages/Login.js +++ b/src/pages/Login.js @@ -12,6 +12,12 @@ export default function LoginPage() { const publicKey = useSelector(s => s.login.publicKey); const [key, setKey] = useState(""); + useEffect(() => { + if (publicKey) { + navigate("/"); + } + }, [publicKey]); + function doLogin() { if (key.startsWith("nsec")) { let nKey = bech32.decode(key); @@ -34,6 +40,7 @@ export default function LoginPage() { async function makeRandomKey() { let newKey = secp.utils.bytesToHex(secp.utils.randomPrivateKey()); dispatch(setPrivateKey(newKey)) + navigate("/new"); } async function doNip07Login() { @@ -57,12 +64,6 @@ export default function LoginPage() { ) } - useEffect(() => { - if (publicKey) { - navigate("/"); - } - }, [publicKey]); - return ( <>

Login

diff --git a/src/pages/NewUserPage.js b/src/pages/NewUserPage.js new file mode 100644 index 000000000..b8d8794cd --- /dev/null +++ b/src/pages/NewUserPage.js @@ -0,0 +1,33 @@ +import ProfilePreview from "../element/ProfilePreview"; + +const RecommendedFollows = [ + "82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2", // jack + "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d", // fiatjaf + "020f2d21ae09bf35fcdfb65decf1478b846f5f728ab30c5eaabcd6d081a81c3e", // adam3us + "6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93", // gigi + "217e3d8b61c087b10422427e114737a4a4a4b1e15f22301fb4b07e1f33204d7c", // Kieran + "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245", // jb55 + "e33fe65f1fde44c6dc17eeb38fdad0fceaf1cae8722084332ed1e32496291d42", // wiz + "00000000827ffaa94bfea288c3dfce4422c794fbb96625b6b31e9049f729d700", // cameri + "A341F45FF9758F570A21B000C17D4E53A3A497C8397F26C0E6D61E5ACFFC7A98", // Saylor + "E88A691E98D9987C964521DFF60025F60700378A4879180DCBBB4A5027850411", // NVK + "C4EABAE1BE3CF657BC1855EE05E69DE9F059CB7A059227168B80B89761CBC4E0", // jackmallers + "85080D3BAD70CCDCD7F74C29A44F55BB85CBCD3DD0CBB957DA1D215BDB931204", // preston + "C49D52A573366792B9A6E4851587C28042FB24FA5625C6D67B8C95C8751ACA15", // holdonaut + "83E818DFBECCEA56B0F551576B3FD39A7A50E1D8159343500368FA085CCD964B", // jeffbooth + "3F770D65D3A764A9C5CB503AE123E62EC7598AD035D836E2A810F3877A745B24", // DerekRoss + "472F440F29EF996E92A186B8D320FF180C855903882E59D50DE1B8BD5669301E", // MartyBent + "1577e4599dd10c863498fe3c20bd82aafaf829a595ce83c5cf8ac3463531b09b", // yegorpetrov +]; + +export default function NewUserPage(props) { + return ( + <> +

Hmm you're not following anybody?

+

Here are some suggestions:

+ {RecommendedFollows + .sort(a => Math.random() >= 0.5 ? -1 : 1) + .map(a => )} + + ) +} \ No newline at end of file diff --git a/src/pages/ProfilePage.js b/src/pages/ProfilePage.js index 29d27d379..f25543de5 100644 --- a/src/pages/ProfilePage.js +++ b/src/pages/ProfilePage.js @@ -1,5 +1,5 @@ import "./ProfilePage.css"; -import { useEffect, useMemo, useRef, useState } from "react"; +import { useMemo, useRef, useState } from "react"; import { useDispatch, useSelector } from "react-redux"; import { bech32 } from "bech32"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; @@ -14,6 +14,7 @@ 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"; export default function ProfilePage() { const dispatch = useDispatch(); @@ -37,7 +38,7 @@ export default function ProfilePage() { useMemo(() => { if (user) { setName(user.name ?? ""); - setPicture(user.picture ?? Nostrich); + setPicture(user.picture ?? ""); setAbout(user.about ?? ""); setWebsite(user.website ?? ""); setNip05(user.nip05 ?? ""); @@ -87,6 +88,23 @@ export default function ProfilePage() { dispatch(resetProfile(id)); } + async function openFile() { + return new Promise((resolve, reject) => { + let elm = document.createElement("input"); + elm.type = "file"; + elm.onchange = (e) => { + resolve(e.target.files[0]); + }; + elm.click(); + }); + } + + async function setNewAvatar() { + let file = await openFile(); + console.log(file); + + } + function editor() { return ( <> @@ -121,7 +139,9 @@ export default function ProfilePage() {
-
+
+
dispatch(logout())}>Logout
+
saveProfile()}>Save
@@ -155,9 +175,9 @@ export default function ProfilePage() { <>
-
+
{isMe ? -
+
setNewAvatar()}>
Edit
: null diff --git a/src/pages/Root.js b/src/pages/Root.js index 4840f1cbb..3065419c1 100644 --- a/src/pages/Root.js +++ b/src/pages/Root.js @@ -3,19 +3,16 @@ import { useSelector } from "react-redux"; import Note from "../element/Note"; import useTimelineFeed from "../feed/TimelineFeed"; import { NoteCreator } from "../element/NoteCreator"; +import ProfilePreview from "../element/ProfilePreview"; export default function RootPage() { const pubKey = useSelector(s => s.login.publicKey); - const follows = useSelector(a => a.login.follows) + const follows = useSelector(a => a.login.follows); const { notes } = useTimelineFeed(follows); function followHints() { if (follows?.length === 0 && pubKey) { - return ( - <> -

Hmm you're not following anybody?

- - ); + return <>Hmm nothing here.. } } diff --git a/src/state/Login.js b/src/state/Login.js index 5a2c73dd6..603181e02 100644 --- a/src/state/Login.js +++ b/src/state/Login.js @@ -96,8 +96,12 @@ const LoginSlice = createSlice({ ]; }, logout: (state) => { - state.privateKey = null; window.localStorage.removeItem(PrivateKeyItem); + window.localStorage.removeItem(Nip07PublicKeyItem); + state.privateKey = null; + state.publicKey = null; + state.follows = []; + state.notifications = []; } } });