From a1d42fa9fbad08cae323591e9993fcf84d948020 Mon Sep 17 00:00:00 2001 From: Kieran Date: Sun, 5 Feb 2023 18:02:13 +0000 Subject: [PATCH] feat: new user flow Closes #44 --- src/Feed/EventPublisher.ts | 7 +-- src/Pages/Layout.tsx | 8 ++- src/Pages/NewUserPage.tsx | 82 ----------------------------- src/Pages/new/DiscoverFollows.tsx | 26 ++++++++++ src/Pages/new/ImportFollows.tsx | 67 ++++++++++++++++++++++++ src/Pages/new/NewProfile.tsx | 15 ++++++ src/Pages/new/index.tsx | 86 +++++++++++++++++++++++++++++++ src/Pages/settings/Profile.tsx | 22 ++++---- src/index.tsx | 9 ++-- 9 files changed, 220 insertions(+), 102 deletions(-) delete mode 100644 src/Pages/NewUserPage.tsx create mode 100644 src/Pages/new/DiscoverFollows.tsx create mode 100644 src/Pages/new/ImportFollows.tsx create mode 100644 src/Pages/new/NewProfile.tsx create mode 100644 src/Pages/new/index.tsx diff --git a/src/Feed/EventPublisher.ts b/src/Feed/EventPublisher.ts index 7dc88bdf..58dbdc70 100644 --- a/src/Feed/EventPublisher.ts +++ b/src/Feed/EventPublisher.ts @@ -8,6 +8,7 @@ import { RootState } from "State/Store"; import { HexKey, RawEvent, u256, UserMetadata, Lists } from "Nostr"; import { bech32ToHex } from "Util" import { DefaultRelays, HashtagRegex } from "Const"; +import { RelaySettings } from "Nostr/Connection"; declare global { interface Window { @@ -27,7 +28,7 @@ export default function useEventPublisher() { const pubKey = useSelector(s => s.login.publicKey); const privKey = useSelector(s => s.login.privateKey); const follows = useSelector(s => s.login.follows); - const relays = useSelector(s => s.login.relays); + const relays = useSelector((s: RootState) => s.login.relays); const hasNip07 = 'nostr' in window; async function signEvent(ev: NEvent): Promise { @@ -221,11 +222,11 @@ export default function useEventPublisher() { return await signEvent(ev); } }, - addFollow: async (pkAdd: HexKey | HexKey[]) => { + addFollow: async (pkAdd: HexKey | HexKey[], newRelays?: Record) => { if (pubKey) { let ev = NEvent.ForPubKey(pubKey); ev.Kind = EventKind.ContactList; - ev.Content = JSON.stringify(relays); + ev.Content = JSON.stringify(newRelays ?? relays); let temp = new Set(follows); if (Array.isArray(pkAdd)) { pkAdd.forEach(a => temp.add(a)); diff --git a/src/Pages/Layout.tsx b/src/Pages/Layout.tsx index dce0e6db..e8a5abf2 100644 --- a/src/Pages/Layout.tsx +++ b/src/Pages/Layout.tsx @@ -20,6 +20,7 @@ import { db } from "Db"; import { bech32ToHex } from "Util"; import { NoteCreator } from "Element/NoteCreator"; import Plus from "Icons/Plus"; +import { RelaySettings } from "Nostr/Connection"; export default function Layout() { @@ -101,6 +102,8 @@ export default function Layout() { }, []); async function handleNewUser() { + let newRelays: Record | undefined; + try { let rsp = await fetch("https://api.nostr.watch/v1/online"); if (rsp.ok) { @@ -108,8 +111,9 @@ export default function Layout() { let pickRandom = online.sort((a, b) => Math.random() >= 0.5 ? 1 : -1).slice(0, 4); // pick 4 random relays let relayObjects = pickRandom.map(a => [a, { read: true, write: true }]); + newRelays = Object.fromEntries(relayObjects); dispatch(setRelays({ - relays: Object.fromEntries(relayObjects), + relays: newRelays!, createdAt: 1 })); } @@ -117,7 +121,7 @@ export default function Layout() { console.warn(e); } - const ev = await pub.addFollow(bech32ToHex(SnortPubKey)); + const ev = await pub.addFollow(bech32ToHex(SnortPubKey), newRelays); pub.broadcast(ev); } diff --git a/src/Pages/NewUserPage.tsx b/src/Pages/NewUserPage.tsx deleted file mode 100644 index f7b7d13c..00000000 --- a/src/Pages/NewUserPage.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import { ApiHost, RecommendedFollows } from "Const"; -import AsyncButton from "Element/AsyncButton"; -import FollowListBase from "Element/FollowListBase"; -import ProfilePreview from "Element/ProfilePreview"; -import { HexKey } from "Nostr"; -import { useMemo, useState } from "react"; -import { useSelector } from "react-redux"; -import { RootState } from "State/Store"; -import { bech32ToHex } from "Util"; - -const TwitterFollowsApi = `${ApiHost}/api/v1/twitter/follows-for-nostr`; - -export default function NewUserPage() { - const [twitterUsername, setTwitterUsername] = useState(""); - const [follows, setFollows] = useState([]); - const currentFollows = useSelector(s => s.login.follows); - const [error, setError] = useState(""); - - const sortedReccomends = useMemo(() => { - return RecommendedFollows - .sort(a => Math.random() >= 0.5 ? -1 : 1); - }, []); - - const sortedTwitterFollows = useMemo(() => { - return follows.map(a => bech32ToHex(a)) - .sort((a, b) => currentFollows.includes(a) ? 1 : -1); - }, [follows, currentFollows]); - - async function loadFollows() { - setFollows([]); - setError(""); - try { - let rsp = await fetch(`${TwitterFollowsApi}?username=${twitterUsername}`); - let data = await rsp.json(); - if (rsp.ok) { - if (Array.isArray(data) && data.length === 0) { - setError(`No nostr users found for "${twitterUsername}"`); - } else { - setFollows(data); - } - } else if ("error" in data) { - setError(data.error); - } else { - setError("Failed to load follows, please try again later"); - } - } catch (e) { - console.warn(e); - setError("Failed to load follows, please try again later"); - } - } - - function followSomebody() { - return ( - <> -

Follow some popular accounts

- {sortedReccomends.map(a => )} - - ) - } - - function importTwitterFollows() { - return ( - <> -

Import twitter follows

-

Find your twitter follows on nostr (Data provided by nostr.directory)

-
- setTwitterUsername(e.target.value)} /> - Check -
- {error.length > 0 && {error}} - {sortedTwitterFollows.length > 0 && ()} - - ) - } - - return ( -
- {importTwitterFollows()} - {followSomebody()} -
- ); -} \ No newline at end of file diff --git a/src/Pages/new/DiscoverFollows.tsx b/src/Pages/new/DiscoverFollows.tsx new file mode 100644 index 00000000..e9ad10a0 --- /dev/null +++ b/src/Pages/new/DiscoverFollows.tsx @@ -0,0 +1,26 @@ +import { RecommendedFollows } from "Const"; +import FollowListBase from "Element/FollowListBase"; +import { useMemo } from "react"; +import { useNavigate } from "react-router-dom"; + +export default function DiscoverFollows() { + const navigate = useNavigate(); + + const sortedReccomends = useMemo(() => { + return RecommendedFollows + .sort(a => Math.random() >= 0.5 ? -1 : 1); + }, []); + + return ( + <> +

Follow some popular accounts

+ + {sortedReccomends.length > 0 && ()} + + + ) +} \ No newline at end of file diff --git a/src/Pages/new/ImportFollows.tsx b/src/Pages/new/ImportFollows.tsx new file mode 100644 index 00000000..60288397 --- /dev/null +++ b/src/Pages/new/ImportFollows.tsx @@ -0,0 +1,67 @@ +import { useMemo, useState } from "react"; +import { useSelector } from "react-redux"; + +import { ApiHost } from "Const"; +import AsyncButton from "Element/AsyncButton"; +import FollowListBase from "Element/FollowListBase"; +import { RootState } from "State/Store"; +import { bech32ToHex } from "Util"; +import { useNavigate } from "react-router-dom"; + +const TwitterFollowsApi = `${ApiHost}/api/v1/twitter/follows-for-nostr`; + +export default function ImportFollows() { + const navigate = useNavigate(); + const currentFollows = useSelector((s: RootState) => s.login.follows); + const [twitterUsername, setTwitterUsername] = useState(""); + const [follows, setFollows] = useState([]); + const [error, setError] = useState(""); + + + const sortedTwitterFollows = useMemo(() => { + return follows.map(a => bech32ToHex(a)) + .sort((a, b) => currentFollows.includes(a) ? 1 : -1); + }, [follows, currentFollows]); + + async function loadFollows() { + setFollows([]); + setError(""); + try { + let rsp = await fetch(`${TwitterFollowsApi}?username=${twitterUsername}`); + let data = await rsp.json(); + if (rsp.ok) { + if (Array.isArray(data) && data.length === 0) { + setError(`No nostr users found for "${twitterUsername}"`); + } else { + setFollows(data); + } + } else if ("error" in data) { + setError(data.error); + } else { + setError("Failed to load follows, please try again later"); + } + } catch (e) { + console.warn(e); + setError("Failed to load follows, please try again later"); + } + } + + return ( + <> +

Import Twitter Follows

+

+ Find your twitter follows on nostr (Data provided by nostr.directory) +

+
+ setTwitterUsername(e.target.value)} /> + Check +
+ {error.length > 0 && {error}} + {sortedTwitterFollows.length > 0 && ()} + + + + ) +} \ No newline at end of file diff --git a/src/Pages/new/NewProfile.tsx b/src/Pages/new/NewProfile.tsx new file mode 100644 index 00000000..9c3d9393 --- /dev/null +++ b/src/Pages/new/NewProfile.tsx @@ -0,0 +1,15 @@ +import ProfileSettings from "Pages/settings/Profile"; +import { useNavigate } from "react-router-dom"; + +export default function NewUserProfile() { + const navigate = useNavigate(); + return ( + <> +

Setup your Profile

+ + + + ) +} \ No newline at end of file diff --git a/src/Pages/new/index.tsx b/src/Pages/new/index.tsx new file mode 100644 index 00000000..9361f5a9 --- /dev/null +++ b/src/Pages/new/index.tsx @@ -0,0 +1,86 @@ +import { useSelector } from "react-redux"; +import { RouteObject, useNavigate } from "react-router-dom"; + +import Copy from "Element/Copy"; +import { RootState } from "State/Store"; +import { hexToBech32 } from "Util"; +import NewUserProfile from "Pages/new//NewProfile"; +import ImportFollows from "Pages/new/ImportFollows"; +import DiscoverFollows from "Pages/new/DiscoverFollows"; + +export const NewUserRoutes: RouteObject[] = [ + { + path: "/new", + element: + }, + { + path: "/new/profile", + element: + }, + { + path: "/new/import", + element: + }, + { + path: "/new/discover", + element: + } +]; + +export default function NewUserFlow() { + const { privateKey } = useSelector((s: RootState) => s.login); + const navigate = useNavigate(); + + return ( + <> +

Welcome to Snort!

+

+ Snort is a Nostr UI, nostr is a decentralised protocol for saving and distributing "notes". +

+

+ Notes hold text content, the most popular usage of these notes is to store "tweet like" messages. +

+

+ Snort is designed to have a similar experience to Twitter. +

+ +

Keys

+

+ Nostr uses digital signature technology to provide tamper proof notes which can safely + be replicated to many relays to provide redundant storage of your content. +

+

+ This means that nobody can modify notes which you have created + and everybody can easily verify that the notes they are reading are created by you. +

+

+ This is the same technology which is used by Bitcoin and has been proven to be extremely secure. +

+ +

Your Key

+

+ When you want to author new notes you need to sign them with your private key, + as with Bitcoin private keys these need to be kept secure. +

+

+ Please now copy your private key and save it somewhere secure: +

+
+ +
+

+ It is also recommended to use one of the following browser extensions if you are on a desktop computer to secure your key: +

+ +

+ You can also use these extensions to login to most Nostr sites. +

+ + + ) +} \ No newline at end of file diff --git a/src/Pages/settings/Profile.tsx b/src/Pages/settings/Profile.tsx index 9d5bc517..55409b86 100644 --- a/src/Pages/settings/Profile.tsx +++ b/src/Pages/settings/Profile.tsx @@ -9,14 +9,19 @@ import { faShop } from "@fortawesome/free-solid-svg-icons"; import useEventPublisher from "Feed/EventPublisher"; import { useUserProfile } from "Feed/ProfileFeed"; -import LogoutButton from "Element/LogoutButton"; import { hexToBech32, openFile } from "Util"; import Copy from "Element/Copy"; import { RootState } from "State/Store"; import { HexKey } from "Nostr"; import useFileUpload from "Upload"; -export default function ProfileSettings() { +export interface ProfileSettingsProps { + avatar?: boolean, + banner?: boolean, + privateKey?: boolean +} + +export default function ProfileSettings(props: ProfileSettingsProps) { const navigate = useNavigate(); const id = useSelector(s => s.login.publicKey); const privKey = useSelector(s => s.login.privateKey); @@ -145,7 +150,6 @@ export default function ProfileSettings() {
-
@@ -160,18 +164,18 @@ export default function ProfileSettings() { return ( <>
-
+ {(props.avatar ?? true) && (

Avatar

setNewAvatar()}>Edit
-
-
+
)} + {(props.banner ?? true) && (

Header

setNewBanner()}>Edit
-
+
)}
{editor()} @@ -180,9 +184,9 @@ export default function ProfileSettings() { return (
-

Profile

+

Edit Profile

{settings()} - {privKey && (
+ {privKey && (props.privateKey ?? true) && (

Your Private Key Is (do not share this with anyone):

diff --git a/src/index.tsx b/src/index.tsx index 9aaf80ab..b87a701e 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -19,7 +19,6 @@ import LoginPage from 'Pages/Login'; import ProfilePage from 'Pages/ProfilePage'; import RootPage from 'Pages/Root'; import NotificationsPage from 'Pages/Notifications'; -import NewUserPage from 'Pages/NewUserPage'; import SettingsPage, { SettingsRoutes } from 'Pages/SettingsPage'; import ErrorPage from 'Pages/ErrorPage'; import VerificationPage from 'Pages/Verification'; @@ -29,6 +28,7 @@ import DonatePage from 'Pages/DonatePage'; import HashTagsPage from 'Pages/HashTagsPage'; import SearchPage from 'Pages/SearchPage'; import HelpPage from 'Pages/HelpPage'; +import { NewUserRoutes } from 'Pages/new'; /** * HTTP query provider @@ -66,10 +66,6 @@ export const router = createBrowserRouter([ path: "/notifications", element: }, - { - path: "/new", - element: - }, { path: "/settings", element: , @@ -98,7 +94,8 @@ export const router = createBrowserRouter([ { path: "/search/:keyword?", element: - } + }, + ...NewUserRoutes ] } ]);