feat: new user flow

Closes #44
This commit is contained in:
Kieran 2023-02-05 18:02:13 +00:00
parent efa765ea84
commit a1d42fa9fb
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
9 changed files with 220 additions and 102 deletions

View File

@ -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<RootState, HexKey | undefined>(s => s.login.publicKey);
const privKey = useSelector<RootState, HexKey | undefined>(s => s.login.privateKey);
const follows = useSelector<RootState, HexKey[]>(s => s.login.follows);
const relays = useSelector<RootState>(s => s.login.relays);
const relays = useSelector((s: RootState) => s.login.relays);
const hasNip07 = 'nostr' in window;
async function signEvent(ev: NEvent): Promise<NEvent> {
@ -221,11 +222,11 @@ export default function useEventPublisher() {
return await signEvent(ev);
}
},
addFollow: async (pkAdd: HexKey | HexKey[]) => {
addFollow: async (pkAdd: HexKey | HexKey[], newRelays?: Record<string, RelaySettings>) => {
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));

View File

@ -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<string, RelaySettings> | 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);
}

View File

@ -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<string>("");
const [follows, setFollows] = useState<string[]>([]);
const currentFollows = useSelector<RootState, HexKey[]>(s => s.login.follows);
const [error, setError] = useState<string>("");
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 (
<>
<h2>Follow some popular accounts</h2>
{sortedReccomends.map(a => <ProfilePreview key={a} pubkey={a.toLowerCase()} />)}
</>
)
}
function importTwitterFollows() {
return (
<>
<h2>Import twitter follows</h2>
<p>Find your twitter follows on nostr (Data provided by <a href="https://nostr.directory" target="_blank" rel="noreferrer">nostr.directory</a>)</p>
<div className="flex">
<input type="text" placeholder="Twitter username.." className="f-grow mr10" value={twitterUsername} onChange={e => setTwitterUsername(e.target.value)} />
<AsyncButton onClick={loadFollows}>Check</AsyncButton>
</div>
{error.length > 0 && <b className="error">{error}</b>}
{sortedTwitterFollows.length > 0 && (<FollowListBase pubkeys={sortedTwitterFollows} />)}
</>
)
}
return (
<div className="main-content">
{importTwitterFollows()}
{followSomebody()}
</div>
);
}

View File

@ -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 (
<>
<h2>Follow some popular accounts</h2>
<button onClick={() => navigate("/")}>
Skip
</button>
{sortedReccomends.length > 0 && (<FollowListBase pubkeys={sortedReccomends} />)}
<button onClick={() => navigate("/")}>
Done!
</button>
</>
)
}

View File

@ -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<string>("");
const [follows, setFollows] = useState<string[]>([]);
const [error, setError] = useState<string>("");
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 (
<>
<h2>Import Twitter Follows</h2>
<p>
Find your twitter follows on nostr (Data provided by <a href="https://nostr.directory" target="_blank" rel="noreferrer">nostr.directory</a>)
</p>
<div className="flex">
<input type="text" placeholder="Twitter username.." className="f-grow mr10" value={twitterUsername} onChange={e => setTwitterUsername(e.target.value)} />
<AsyncButton onClick={loadFollows}>Check</AsyncButton>
</div>
{error.length > 0 && <b className="error">{error}</b>}
{sortedTwitterFollows.length > 0 && (<FollowListBase pubkeys={sortedTwitterFollows} />)}
<button onClick={() => navigate("/new/discover")}>
Next
</button>
</>
)
}

View File

@ -0,0 +1,15 @@
import ProfileSettings from "Pages/settings/Profile";
import { useNavigate } from "react-router-dom";
export default function NewUserProfile() {
const navigate = useNavigate();
return (
<>
<h1>Setup your Profile</h1>
<ProfileSettings privateKey={false} banner={false} />
<button onClick={() => navigate("/new/import")}>
Next
</button>
</>
)
}

86
src/Pages/new/index.tsx Normal file
View File

@ -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: <NewUserFlow />
},
{
path: "/new/profile",
element: <NewUserProfile />
},
{
path: "/new/import",
element: <ImportFollows />
},
{
path: "/new/discover",
element: <DiscoverFollows />
}
];
export default function NewUserFlow() {
const { privateKey } = useSelector((s: RootState) => s.login);
const navigate = useNavigate();
return (
<>
<h1>Welcome to Snort!</h1>
<p>
Snort is a Nostr UI, nostr is a decentralised protocol for saving and distributing "notes".
</p>
<p>
Notes hold text content, the most popular usage of these notes is to store "tweet like" messages.
</p>
<p>
Snort is designed to have a similar experience to Twitter.
</p>
<h2>Keys</h2>
<p>
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.
</p>
<p>
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.
</p>
<p>
This is the same technology which is used by Bitcoin and has been proven to be extremely secure.
</p>
<h2>Your Key</h2>
<p>
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.
</p>
<p>
Please now copy your private key and save it somewhere secure:
</p>
<div className="card">
<Copy text={hexToBech32("nsec", privateKey ?? "")} />
</div>
<p>
It is also recommended to use one of the following browser extensions if you are on a desktop computer to secure your key:
</p>
<ul>
<li><a href="https://getalby.com/" target="_blank" rel="noreferrer">Alby</a></li>
<li><a href="https://github.com/fiatjaf/nos2x" target="_blank" rel="noreferrer">nos2x</a></li>
</ul>
<p>
You can also use these extensions to login to most Nostr sites.
</p>
<button onClick={() => navigate("/new/profile")}>
Next
</button>
</>
)
}

View File

@ -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<RootState, HexKey | undefined>(s => s.login.publicKey);
const privKey = useSelector<RootState, HexKey | undefined>(s => s.login.privateKey);
@ -145,7 +150,6 @@ export default function ProfileSettings() {
</div>
<div className="form-group">
<div>
<LogoutButton />
</div>
<div>
<button type="button" onClick={() => saveProfile()}>Save</button>
@ -160,18 +164,18 @@ export default function ProfileSettings() {
return (
<>
<div className="flex f-center image-settings">
<div>
{(props.avatar ?? true) && (<div>
<h2>Avatar</h2>
<div style={{ backgroundImage: `url(${avatarPicture})` }} className="avatar">
<div className="edit" onClick={() => setNewAvatar()}>Edit</div>
</div>
</div>
<div>
</div>)}
{(props.banner ?? true) && (<div>
<h2>Header</h2>
<div style={{ backgroundImage: `url(${(banner?.length ?? 0) === 0 ? Nostrich : banner})` }} className="banner">
<div className="edit" onClick={() => setNewBanner()}>Edit</div>
</div>
</div>
</div>)}
</div>
{editor()}
</>
@ -180,9 +184,9 @@ export default function ProfileSettings() {
return (
<div className="settings">
<h3>Profile</h3>
<h3>Edit Profile</h3>
{settings()}
{privKey && (<div className="flex f-col bg-grey">
{privKey && (props.privateKey ?? true) && (<div className="flex f-col bg-grey">
<div>
<h4>Your Private Key Is (do not share this with anyone):</h4>
</div>

View File

@ -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: <NotificationsPage />
},
{
path: "/new",
element: <NewUserPage />
},
{
path: "/settings",
element: <SettingsPage />,
@ -98,7 +94,8 @@ export const router = createBrowserRouter([
{
path: "/search/:keyword?",
element: <SearchPage />
}
},
...NewUserRoutes
]
}
]);