From d119a5f6267052ffcb36c6b836227cfe6c3300fa Mon Sep 17 00:00:00 2001 From: Kieran Date: Wed, 8 Nov 2023 14:47:35 +0000 Subject: [PATCH] feat: sign up flow v2 --- packages/app/src/Element/TrendingUsers.tsx | 6 +- .../app/src/Element/User/FollowListBase.tsx | 14 +- .../app/src/Element/User/ProfilePreview.tsx | 2 +- packages/app/src/IntlProvider.tsx | 35 +- packages/app/src/Login/Functions.ts | 11 +- packages/app/src/Pages/LoginPage.tsx | 388 ------------------ .../app/src/Pages/new/DiscoverFollows.tsx | 55 --- packages/app/src/Pages/new/GetVerified.tsx | 115 ------ packages/app/src/Pages/new/NewUserFlow.tsx | 147 ------- packages/app/src/Pages/new/ProfileSetup.tsx | 84 ---- packages/app/src/Pages/new/index.css | 179 -------- packages/app/src/Pages/new/index.tsx | 30 -- packages/app/src/Pages/new/messages.ts | 86 ---- .../app/src/Pages/onboarding/discover.tsx | 42 ++ packages/app/src/Pages/onboarding/index.css | 50 +++ packages/app/src/Pages/onboarding/index.tsx | 74 ++++ .../app/src/Pages/onboarding/moderation.tsx | 156 +++++++ packages/app/src/Pages/onboarding/profile.tsx | 47 +++ packages/app/src/Pages/onboarding/start.tsx | 151 +++++++ packages/app/src/Pages/onboarding/topics.tsx | 85 ++++ .../app/src/Pages/settings/Preferences.tsx | 6 +- packages/app/src/index.css | 4 + packages/app/src/index.tsx | 15 +- packages/app/src/lang.json | 301 +++++--------- packages/app/src/translations/en.json | 100 ++--- packages/system/package.json | 2 +- yarn.lock | 9 +- 27 files changed, 815 insertions(+), 1379 deletions(-) delete mode 100644 packages/app/src/Pages/LoginPage.tsx delete mode 100644 packages/app/src/Pages/new/DiscoverFollows.tsx delete mode 100644 packages/app/src/Pages/new/GetVerified.tsx delete mode 100644 packages/app/src/Pages/new/NewUserFlow.tsx delete mode 100644 packages/app/src/Pages/new/ProfileSetup.tsx delete mode 100644 packages/app/src/Pages/new/index.css delete mode 100644 packages/app/src/Pages/new/index.tsx delete mode 100644 packages/app/src/Pages/new/messages.ts create mode 100644 packages/app/src/Pages/onboarding/discover.tsx create mode 100644 packages/app/src/Pages/onboarding/index.css create mode 100644 packages/app/src/Pages/onboarding/index.tsx create mode 100644 packages/app/src/Pages/onboarding/moderation.tsx create mode 100644 packages/app/src/Pages/onboarding/profile.tsx create mode 100644 packages/app/src/Pages/onboarding/start.tsx create mode 100644 packages/app/src/Pages/onboarding/topics.tsx diff --git a/packages/app/src/Element/TrendingUsers.tsx b/packages/app/src/Element/TrendingUsers.tsx index a174fcf76..54cad2662 100644 --- a/packages/app/src/Element/TrendingUsers.tsx +++ b/packages/app/src/Element/TrendingUsers.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react"; +import { ReactNode, useEffect, useState } from "react"; import { HexKey } from "@snort/system"; import FollowListBase from "Element/User/FollowListBase"; @@ -6,7 +6,7 @@ import PageSpinner from "Element/PageSpinner"; import NostrBandApi from "External/NostrBand"; import { ErrorOrOffline } from "./ErrorOrOffline"; -export default function TrendingUsers() { +export default function TrendingUsers({ title }: { title?: ReactNode }) { const [userList, setUserList] = useState(); const [error, setError] = useState(); @@ -28,5 +28,5 @@ export default function TrendingUsers() { if (error) return ; if (!userList) return ; - return ; + return ; } diff --git a/packages/app/src/Element/User/FollowListBase.tsx b/packages/app/src/Element/User/FollowListBase.tsx index a4f14af69..64b57bc80 100644 --- a/packages/app/src/Element/User/FollowListBase.tsx +++ b/packages/app/src/Element/User/FollowListBase.tsx @@ -45,19 +45,21 @@ export default function FollowListBase({ } return ( -
+
{(showFollowAll ?? true) && ( -
-
{title}
+
+
{title}
{actions} followAll()} disabled={login.readonly}>
)} - {pubkeys?.map(a => ( - - ))} +
+ {pubkeys?.map(a => ( + + ))} +
); } diff --git a/packages/app/src/Element/User/ProfilePreview.tsx b/packages/app/src/Element/User/ProfilePreview.tsx index 19ba98b10..9084d03bf 100644 --- a/packages/app/src/Element/User/ProfilePreview.tsx +++ b/packages/app/src/Element/User/ProfilePreview.tsx @@ -49,7 +49,7 @@ export default function ProfilePreview(props: ProfilePreviewProps) { showProfileCard={options.profileCards} /> {props.actions ?? ( -
+
)} diff --git a/packages/app/src/IntlProvider.tsx b/packages/app/src/IntlProvider.tsx index e41459abd..5a0a45110 100644 --- a/packages/app/src/IntlProvider.tsx +++ b/packages/app/src/IntlProvider.tsx @@ -1,8 +1,9 @@ -import { useEffect, useState, type ReactNode } from "react"; +import { useEffect, useState, type ReactNode, useSyncExternalStore } from "react"; import { IntlProvider as ReactIntlProvider } from "react-intl"; import enMessages from "translations/en.json"; import useLogin from "Hooks/useLogin"; +import { ExternalStore } from "@snort/shared"; const DefaultLocale = "en-US"; @@ -80,9 +81,35 @@ const getMessages = (locale: string) => { return matchLang(locale) ?? matchLang(truncatedLocale) ?? Promise.resolve(enMessages); }; -export const IntlProvider = ({ children }: { children: ReactNode }) => { +class LangStore extends ExternalStore { + setLang(s: string) { + localStorage.setItem("lang", s); + this.notifyChange(); + } + + takeSnapshot(): string { + return localStorage.getItem("lang") ?? DefaultLocale; + } +} + +const LangOverride = new LangStore(); + +export function useLocale() { const { language } = useLogin(s => ({ language: s.preferences.language })); - const locale = language ?? getLocale(); + const loggedOutLang = useSyncExternalStore( + c => LangOverride.hook(c), + () => LangOverride.snapshot(), + ); + const locale = language ?? loggedOutLang ?? getLocale(); + return { + locale, + lang: locale.toLowerCase().split(/[_-]+/)[0], + setOverride: (s: string) => LangOverride.setLang(s), + }; +} + +export const IntlProvider = ({ children }: { children: ReactNode }) => { + const { locale } = useLocale(); const [messages, setMessages] = useState>(enMessages); useEffect(() => { @@ -93,7 +120,7 @@ export const IntlProvider = ({ children }: { children: ReactNode }) => { } }) .catch(console.error); - }, [language]); + }, [locale]); return ( diff --git a/packages/app/src/Login/Functions.ts b/packages/app/src/Login/Functions.ts index 83930ea8c..92fcad9e9 100644 --- a/packages/app/src/Login/Functions.ts +++ b/packages/app/src/Login/Functions.ts @@ -6,6 +6,7 @@ import { PrivateKeySigner, KeyStorage, SystemInterface, + UserMetadata, } from "@snort/system"; import { unixNowMs } from "@snort/shared"; import * as secp from "@noble/curves/secp256k1"; @@ -79,7 +80,11 @@ export function clearEntropy(state: LoginSession) { /** * Generate a new key and login with this generated key */ -export async function generateNewLogin(system: SystemInterface, pin: (key: string) => Promise) { +export async function generateNewLogin( + system: SystemInterface, + pin: (key: string) => Promise, + profile: UserMetadata, +) { const ent = generateBip39Entropy(); const entropy = utils.bytesToHex(ent); const privateKey = entropyToPrivateKey(ent); @@ -99,6 +104,10 @@ export async function generateNewLogin(system: SystemInterface, pin: (key: strin const ev2 = await publisher.relayList(newRelays); await system.BroadcastEvent(ev2); + // Publish new profile + const ev3 = await publisher.metadata(profile); + await system.BroadcastEvent(ev3); + LoginStore.loginWithPrivateKey(await pin(privateKey), entropy, newRelays); } diff --git a/packages/app/src/Pages/LoginPage.tsx b/packages/app/src/Pages/LoginPage.tsx deleted file mode 100644 index 01474f4dd..000000000 --- a/packages/app/src/Pages/LoginPage.tsx +++ /dev/null @@ -1,388 +0,0 @@ -import "./LoginPage.css"; - -import { CSSProperties, useEffect, useState } from "react"; -import { useNavigate } from "react-router-dom"; -import { useIntl, FormattedMessage } from "react-intl"; -import { HexKey, Nip46Signer, NotEncrypted, PinEncrypted, PrivateKeySigner } from "@snort/system"; - -import { bech32ToHex, getPublicKey, unwrap } from "SnortUtils"; -import ZapButton from "Element/Event/ZapButton"; -import useImgProxy from "Hooks/useImgProxy"; -import Icon from "Icons/Icon"; -import { generateNewLogin, LoginSessionType, LoginStore } from "Login"; -import AsyncButton from "Element/AsyncButton"; -import useLoginHandler from "Hooks/useLoginHandler"; -import { secp256k1 } from "@noble/curves/secp256k1"; -import { bytesToHex } from "@noble/curves/abstract/utils"; -import Modal from "Element/Modal"; -import QrCode from "Element/QrCode"; -import Copy from "Element/Copy"; -import { delay } from "SnortUtils"; -import { PinPrompt } from "Element/PinPrompt"; -import useEventPublisher from "Hooks/useEventPublisher"; -import { isHex } from "@snort/shared"; - -declare global { - interface Window { - plausible?: (tag: string) => void; - } -} - -interface ArtworkEntry { - name: string; - pubkey: HexKey; - link: string; -} - -const KarnageKey = bech32ToHex("npub1r0rs5q2gk0e3dk3nlc7gnu378ec6cnlenqp8a3cjhyzu6f8k5sgs4sq9ac"); - -// todo: fill more -const Artwork: Array = [ - { - name: "", - pubkey: KarnageKey, - link: "https://void.cat/d/VKhPayp9ekeXYZGzAL9CxP", - }, - { - name: "", - pubkey: KarnageKey, - link: "https://void.cat/d/3H2h8xxc3aEN6EVeobd8tw", - }, - { - name: "", - pubkey: KarnageKey, - link: "https://void.cat/d/7i9W9PXn3TV86C4RUefNC9", - }, - { - name: "", - pubkey: KarnageKey, - link: "https://void.cat/d/KtoX4ei6RYHY7HESg3Ve3k", - }, -]; - -export default function LoginPage() { - const navigate = useNavigate(); - const [key, setKey] = useState(""); - const [nip46Key, setNip46Key] = useState(""); - const [error, setError] = useState(""); - const [pin, setPin] = useState(false); - const [art, setArt] = useState(); - const [isMasking, setMasking] = useState(true); - const { formatMessage } = useIntl(); - const { proxy } = useImgProxy(); - const loginHandler = useLoginHandler(); - const hasNip7 = "nostr" in window; - const { system } = useEventPublisher(); - const hasSubtleCrypto = window.crypto.subtle !== undefined; - const [nostrConnect, setNostrConnect] = useState(""); - - useEffect(() => { - const ret = unwrap(Artwork.at(Artwork.length * Math.random())); - const url = proxy(ret.link); - setArt({ ...ret, link: url }); - }, []); - - async function makeKeyStore(key: string, pin?: string) { - if (pin) { - return await PinEncrypted.create(key, pin); - } else { - return new NotEncrypted(key); - } - } - - async function doLogin(pin?: string) { - setError(""); - try { - await loginHandler.doLogin(key, key => makeKeyStore(key, pin)); - navigate("/"); - } catch (e) { - if (e instanceof Error) { - setError(e.message); - } else { - setError( - formatMessage({ - defaultMessage: "Unknown login error", - }), - ); - } - console.error(e); - } - } - - async function makeRandomKey(pin?: string) { - try { - await generateNewLogin(system, key => makeKeyStore(key, pin)); - window.plausible?.("Generate Account"); - navigate("/new"); - } catch (e) { - if (e instanceof Error) { - setError(e.message); - } - } - } - - async function doNip07Login() { - const relays = - "getRelays" in unwrap(window.nostr) ? await unwrap(window.nostr?.getRelays).call(window.nostr) : undefined; - const pubKey = await unwrap(window.nostr).getPublicKey(); - LoginStore.loginWithPubkey(pubKey, LoginSessionType.Nip7, relays); - navigate("/"); - } - - function generateNip46() { - const meta = { - name: CONFIG.appNameCapitalized, - url: window.location.href, - }; - - const newKey = bytesToHex(secp256k1.utils.randomPrivateKey()); - const relays = ["wss://relay.damus.io"].map(a => `relay=${encodeURIComponent(a)}`); - const connectUrl = `nostrconnect://${getPublicKey(newKey)}?${[ - ...relays, - `metadata=${encodeURIComponent(JSON.stringify(meta))}`, - ].join("&")}`; - setNostrConnect(connectUrl); - setNip46Key(newKey); - } - - async function startNip46(pin?: string) { - if (!nostrConnect || !nip46Key) return; - - const signer = new Nip46Signer(nostrConnect, new PrivateKeySigner(nip46Key)); - await signer.init(); - await delay(500); - await signer.describe(); - LoginStore.loginWithPubkey( - await signer.getPubKey(), - LoginSessionType.Nip46, - undefined, - ["wss://relay.damus.io"], - await makeKeyStore(nip46Key, pin), - ); - navigate("/"); - } - - function nip46Buttons() { - return ( - <> - { - generateNip46(); - setPin(true); - }}> - - - {nostrConnect && !pin && ( - setNostrConnect("")}> - <> -

- -

-

- -

-
- - -
- -
- )} - - ); - } - - function altLogins() { - if (!hasNip7) { - return; - } - - return ( - <> - - - - {nip46Buttons()} - - ); - } - - function installExtension() { - if (hasSubtleCrypto) return; - - return ( - <> -
- -
-
-

- -

-

- -

- -

- awesome-nostr, - }} - /> -

-

- -

- {hasNip7 ? ( -
- -
- ) : ( - - - - )} - - ); - } - - return ( -
-
-
-

navigate("/")}> - {CONFIG.appName} -

-

- -

-

- -

-
- setKey(e.target.value)} - /> - setMasking(!isMasking)} - /> -
- {error.length > 0 ? {error} : null} -

- -

-
- { - if (key.startsWith("nsec") || (key.length === 64 && isHex(key))) { - setPin(true); - } else { - await doLogin(); - } - }}> - - - setPin(true)}> - - - {pin && ( - -

- -

-

- -

-

- -

- - } - onResult={async pin => { - setPin(false); - if (key) { - await doLogin(pin); - } else if (nostrConnect) { - await startNip46(pin); - } else { - await makeRandomKey(pin); - } - }} - onCancel={async () => { - setPin(false); - if (key) { - await doLogin(); - } else if (nostrConnect) { - await startNip46(); - } else { - await makeRandomKey(); - } - }} - /> - )} - {altLogins()} -
- {installExtension()} -
-
-
-
-
- Karnage, - }} - /> - -
-
-
-
- ); -} diff --git a/packages/app/src/Pages/new/DiscoverFollows.tsx b/packages/app/src/Pages/new/DiscoverFollows.tsx deleted file mode 100644 index 5e3c1f07f..000000000 --- a/packages/app/src/Pages/new/DiscoverFollows.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { useIntl, FormattedMessage } from "react-intl"; -import { useNavigate, Link } from "react-router-dom"; - -import { DeveloperAccounts } from "Const"; -import Logo from "Element/Logo"; -import FollowListBase from "Element/User/FollowListBase"; -import { clearEntropy } from "Login"; -import useLogin from "Hooks/useLogin"; -import TrendingUsers from "Element/TrendingUsers"; - -import messages from "./messages"; - -export default function DiscoverFollows() { - const { formatMessage } = useIntl(); - const login = useLogin(); - const navigate = useNavigate(); - - async function clearEntropyAndGo() { - clearEntropy(login); - navigate("/"); - } - - return ( -
- -
-
-
-

- -

-

- {formatMessage(messages.World)} }} /> -

-
- -
-

- -

- {DeveloperAccounts.length > 0 && } -

- -

- -
- ); -} diff --git a/packages/app/src/Pages/new/GetVerified.tsx b/packages/app/src/Pages/new/GetVerified.tsx deleted file mode 100644 index 4507f116f..000000000 --- a/packages/app/src/Pages/new/GetVerified.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import { useState } from "react"; -import { FormattedMessage } from "react-intl"; -import { useNavigate } from "react-router-dom"; -import { useUserProfile } from "@snort/system-react"; - -import Logo from "Element/Logo"; -import { Nip5Services } from "Pages/NostrAddressPage"; -import Nip5Service from "Element/Nip5Service"; -import ProfileImage from "Element/User/ProfileImage"; -import useLogin from "Hooks/useLogin"; - -import messages from "./messages"; - -export default function GetVerified() { - const navigate = useNavigate(); - const { publicKey } = useLogin(); - const user = useUserProfile(publicKey); - const [isVerified, setIsVerified] = useState(false); - const name = user?.name || "nostrich"; - const [nip05, setNip05] = useState(`${name}@snort.social`); - - const onNext = async () => { - navigate("/new/import"); - }; - - return ( -
- -
-
-
-

- -

-
- -
-

- -

-
- {publicKey && } -
-

- -

-
    -
  • - -
  • -
  • - -
  • -
  • - -
  • -
-

- -

- {!isVerified && ( - <> -

- -

-

- -

-
- setIsVerified(true)} - /> -
- - )} - {!isVerified && ( - <> -

- -

-

- -

-
- setIsVerified(true)} - /> -
- - )} -
- {!isVerified && ( - - )} - {isVerified && ( - - )} -
-
- ); -} diff --git a/packages/app/src/Pages/new/NewUserFlow.tsx b/packages/app/src/Pages/new/NewUserFlow.tsx deleted file mode 100644 index e5bc7501c..000000000 --- a/packages/app/src/Pages/new/NewUserFlow.tsx +++ /dev/null @@ -1,147 +0,0 @@ -import { FormattedMessage } from "react-intl"; -import { useNavigate } from "react-router-dom"; - -import Logo from "Element/Logo"; -import { CollapsedSection } from "Element/Collapsed"; -import useLogin from "Hooks/useLogin"; -import { PROFILE } from "."; -import { DefaultPreferences, LoginStore, updatePreferences } from "Login"; -import { AllLanguageCodes } from "Pages/settings/Preferences"; - -import messages from "./messages"; -import ExportKeys from "Pages/settings/Keys"; - -const WhatIsSnort = () => { - return ( - - - - }> -

- -

-

- -

-

- -

-
- ); -}; - -const HowDoKeysWork = () => { - return ( - - - - }> -

- -

-

- -

-

- -

-
- ); -}; - -const Extensions = () => { - const { preferences } = useLogin(); - return ( - - - - }> -

- -

- -

- -

-
- ); -}; - -export default function NewUserFlow() { - const login = useLogin(); - const navigate = useNavigate(); - - return ( -
- -
-
-
-

- -

-
- - -
-

- -

- -
- -
- - - -
- ); -} diff --git a/packages/app/src/Pages/new/ProfileSetup.tsx b/packages/app/src/Pages/new/ProfileSetup.tsx deleted file mode 100644 index 4fe4c1603..000000000 --- a/packages/app/src/Pages/new/ProfileSetup.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import { useEffect, useState } from "react"; -import { useIntl, FormattedMessage } from "react-intl"; -import { useNavigate } from "react-router-dom"; -import { mapEventToProfile } from "@snort/system"; -import { useUserProfile } from "@snort/system-react"; - -import Logo from "Element/Logo"; -import useEventPublisher from "Hooks/useEventPublisher"; -import useLogin from "Hooks/useLogin"; -import { UserCache } from "Cache"; -import AvatarEditor from "Element/User/AvatarEditor"; -import { DISCOVER } from "."; - -import messages from "./messages"; - -export default function ProfileSetup() { - const login = useLogin(); - const myProfile = useUserProfile(login.publicKey); - const [username, setUsername] = useState(""); - const [picture, setPicture] = useState(""); - const { formatMessage } = useIntl(); - const { publisher, system } = useEventPublisher(); - const navigate = useNavigate(); - - useEffect(() => { - if (myProfile) { - setUsername(myProfile.name ?? ""); - setPicture(myProfile.picture ?? ""); - } - }, [myProfile]); - - const onNext = async () => { - if ((username.length > 0 || picture.length > 0) && publisher) { - const ev = await publisher.metadata({ - ...myProfile, - name: username, - picture, - }); - system.BroadcastEvent(ev); - const profile = mapEventToProfile(ev); - if (profile) { - UserCache.set(profile); - } - } - navigate(DISCOVER); - }; - - return ( -
- -
-
-
-

- -

-

- -

- setPicture(p)} /> -

- -

- setUsername(ev.target.value)} - /> -
- -
-
- - -
-
- ); -} diff --git a/packages/app/src/Pages/new/index.css b/packages/app/src/Pages/new/index.css deleted file mode 100644 index 7bd1e9701..000000000 --- a/packages/app/src/Pages/new/index.css +++ /dev/null @@ -1,179 +0,0 @@ -.new-user { - color: var(--font-secondary-color); -} - -.new-user input { - font-size: 16px; -} - -.new-user p { - font-weight: 400; - font-size: 16px; - line-height: 24px; -} - -.new-user p > a { - color: var(--highlight); -} - -.new-user li { - line-height: 24px; -} - -.new-user li > a { - color: var(--highlight); -} - -.new-user .nip-handle { - max-width: 120px; -} - -.new-user h1 { - color: var(--font-color); - font-weight: 700; - font-size: 32px; - line-height: 39px; -} - -.new-user h2 { - margin-top: 24px; - margin-bottom: 16px; - color: var(--font-color); - font-weight: 600; - font-size: 16px; - line-height: 19px; -} - -.new-user h3 { - color: var(--font-color); - font-weight: 700; - font-size: 21px; - line-height: 25px; -} - -.new-user h4 { - color: var(--font-secondary-color); - font-weight: 600; - font-size: 12px; - line-height: 19px; - letter-spacing: 0.08em; - text-transform: uppercase; -} - -.progress-bar { - width: 100%; - height: 7px; - background: var(--gray-secondary); - border-radius: 53px; -} - -.progress-bar .progress { - height: 7px; - background: var(--snort-gradient); - border-radius: 53px; -} - -.progress.progress-first { - width: 20%; -} - -.progress.progress-second { - width: 50%; -} - -.progress.progress-third { - width: 75%; -} - -.progress.progress-last { - width: 95%; -} - -.new-user .next-actions { - margin-top: 32px; - margin-bottom: 64px; - width: 100%; - display: flex; - justify-content: flex-end; -} - -.new-user .next-actions button:not(:last-child) { - margin-right: 12px; -} - -.new-user .next-actions.continue-actions { - margin-bottom: 0; -} - -.new-user > .copy { - padding: 12px 16px; - border: 2px dashed #222222; - border-radius: 16px; -} - -.light .new-user > .copy { - border: 2px dashed #aaaaaa; -} - -.new-user > .copy .body { - font-size: 16px; -} -@media (max-width: 520px) { - .new-user > .copy .body { - font-size: 12px; - } -} - -.new-user > .copy .icon { - margin-left: auto; -} - -.new-user > .copy .icon svg { - width: 16px; - height: 16px; -} - -.new-user input { - width: 100%; - max-width: 568px; - background: #222; - border: none; -} - -@media (max-width: 720px) { - .new-user input { - width: calc(100vw - 40px); - } -} - -.light .new-user input { - background: none; -} - -.new-user .warning { - font-weight: 400; - font-size: 14px; - line-height: 19px; - color: #fc6e1e; -} - -.profile-preview-nip { - padding: 12px 16px; - border: 1px solid rgba(255, 255, 255, 0.1); - border-radius: 16px; -} - -.light .profile-preview-nip { - border: 1px solid rgba(0, 0, 0, 0.1); -} - -.new-user .nip-container input[type="text"] { - width: 166px; -} - -.new-user .help-text { - margin-top: 6px; - font-weight: 400; - font-size: 14px; - line-height: 24px; -} diff --git a/packages/app/src/Pages/new/index.tsx b/packages/app/src/Pages/new/index.tsx deleted file mode 100644 index 29fa7887b..000000000 --- a/packages/app/src/Pages/new/index.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import "./index.css"; -import { RouteObject } from "react-router-dom"; - -import GetVerified from "Pages/new/GetVerified"; -import ProfileSetup from "Pages/new/ProfileSetup"; -import NewUserFlow from "Pages/new/NewUserFlow"; -import DiscoverFollows from "Pages/new/DiscoverFollows"; - -export const PROFILE = "/new/profile"; -export const DISCOVER = "/new/discover"; -export const VERIFY = "/new/verify"; - -export const NewUserRoutes: RouteObject[] = [ - { - path: "/new", - element: , - }, - { - path: PROFILE, - element: , - }, - { - path: VERIFY, - element: , - }, - { - path: DISCOVER, - element: , - }, -]; diff --git a/packages/app/src/Pages/new/messages.ts b/packages/app/src/Pages/new/messages.ts deleted file mode 100644 index 2a86b565e..000000000 --- a/packages/app/src/Pages/new/messages.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { defineMessages } from "react-intl"; - -export default defineMessages({ - SaveKeys: { - defaultMessage: "Save your keys!", - }, - SaveKeysHelp: { - defaultMessage: - "Your private key is your password. If you lose this key, you will lose access to your account! Copy it and keep it in a safe place. There is no way to reset your private key.", - }, - YourPubkey: { defaultMessage: "Your public key" }, - YourPrivkey: { defaultMessage: "Your private key" }, - YourMnemonic: { defaultMessage: "Your mnemonic phrase" }, - KeysSaved: { defaultMessage: "I have saved my keys, continue" }, - WhatIsSnort: { - defaultMessage: "What is {site} and how does it work?", - values: { site: CONFIG.appNameCapitalized }, - }, - WhatIsSnortIntro: { - defaultMessage: `{site} is a Nostr UI, nostr is a decentralised protocol for saving and distributing "notes".`, - values: { site: CONFIG.appNameCapitalized }, - }, - WhatIsSnortNotes: { - defaultMessage: `Notes hold text content, the most popular usage of these notes is to store "tweet like" messages.`, - }, - - WhatIsSnortExperience: { - defaultMessage: "{site} is designed to have a similar experience to Twitter.", - values: { site: CONFIG.appNameCapitalized }, - }, - HowKeysWork: { defaultMessage: "How do keys work?" }, - DigitalSignatures: { - defaultMessage: `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.`, - }, - TamperProof: { - defaultMessage: `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.`, - }, - Bitcoin: { - defaultMessage: `This is the same technology which is used by Bitcoin and has been proven to be extremely secure.`, - }, - Extensions: { - defaultMessage: `It is recommended to use one of the following browser extensions if you are on a desktop computer to secure your key:`, - }, - ExtensionsNostr: { defaultMessage: `You can also use these extensions to login to most Nostr sites.` }, - ImproveSecurity: { defaultMessage: "Improve login security with browser extensions" }, - PickUsername: { defaultMessage: "Pick a username" }, - Username: { defaultMessage: "Username" }, - UsernamePlaceholder: { defaultMessage: "e.g. Jack" }, - PopularAccounts: { defaultMessage: "Follow some popular accounts" }, - Skip: { defaultMessage: "Skip" }, - Done: { defaultMessage: "Done!" }, - ImportTwitter: { defaultMessage: "Import Twitter Follows" }, - TwitterPlaceholder: { defaultMessage: "Twitter username..." }, - FindYourFollows: { defaultMessage: "Find your twitter follows on nostr (Data provided by {provider})" }, - TwitterUsername: { defaultMessage: "Twitter username" }, - FollowsOnNostr: { defaultMessage: "{username}'s Follows on Nostr" }, - NoUsersFound: { defaultMessage: "No nostr users found for {twitterUsername}" }, - FailedToLoad: { defaultMessage: "Failed to load follows, please try again later" }, - Check: { defaultMessage: "Check" }, - Next: { defaultMessage: "Next" }, - SetupProfile: { defaultMessage: "Setup your Profile" }, - Identifier: { defaultMessage: "Get an identifier" }, - IdentifierHelp: { - defaultMessage: - "Getting an identifier helps confirm the real you to people who know you. Many people can have a username @jack, but there is only one jack@cash.app.", - }, - PreventFakes: { defaultMessage: "Prevent fake accounts from imitating you" }, - EasierToFind: { defaultMessage: "Make your profile easier to find and share" }, - Funding: { defaultMessage: "Fund developers and platforms providing NIP-05 verification services" }, - NameSquatting: { - defaultMessage: - "Name-squatting and impersonation is not allowed. {site} and our partners reserve the right to terminate your handle (not your account - nobody can take that away) for violating this rule.", - values: { site: CONFIG.appNameCapitalized }, - }, - PreviewOnSnort: { defaultMessage: "Preview on {site}", values: { site: CONFIG.appNameCapitalized } }, - GetSnortId: { defaultMessage: "Get a Snort identifier" }, - GetSnortIdHelp: { - defaultMessage: - "Only Snort and our integration partner identifier gives you a colorful domain name, but you are welcome to use other services too.", - }, - GetPartnerId: { defaultMessage: "Get a partner identifier" }, - GetPartnerIdHelp: { defaultMessage: "We have also partnered with nostrplebs.com to give you more options" }, - Ready: { defaultMessage: "You're ready!" }, - Share: { defaultMessage: "Share your thoughts with {link}" }, - World: { defaultMessage: "the world" }, -}); diff --git a/packages/app/src/Pages/onboarding/discover.tsx b/packages/app/src/Pages/onboarding/discover.tsx new file mode 100644 index 000000000..21e1b1191 --- /dev/null +++ b/packages/app/src/Pages/onboarding/discover.tsx @@ -0,0 +1,42 @@ +import { FormattedMessage } from "react-intl"; +import { useLocation, useNavigate } from "react-router-dom"; +import AsyncButton from "Element/AsyncButton"; +import { NewUserState } from "."; +import TrendingUsers from "Element/TrendingUsers"; + +export function Discover() { + const location = useLocation(); + const navigate = useNavigate(); + const state = location.state as NewUserState; + + return ( +
+

+ +

+
+ + + + } + /> +
+ + navigate("/login/sign-up/moderation", { + state, + }) + }> + + +
+ ); +} diff --git a/packages/app/src/Pages/onboarding/index.css b/packages/app/src/Pages/onboarding/index.css new file mode 100644 index 000000000..4829727d8 --- /dev/null +++ b/packages/app/src/Pages/onboarding/index.css @@ -0,0 +1,50 @@ +.onboarding-modal { + clear: both; + margin-top: 10vh; +} + +@media (min-width: 640px) { + .onboarding-modal { + padding: 40px 48px; + border-radius: 24px; + background-color: var(--gray-superdark); + margin-left: auto; + margin-right: auto; + margin-top: 10vh; + width: 460px; + } +} + +.onboarding-modal h1, +.onboarding-modal h2, +.onboarding-modal h3, +.onboarding-modal h4, +.onboarding-modal h5 { + margin: 0; +} + +.onboarding-modal button.secondary:hover { + background-color: var(--gray-medium); +} + +.onboarding-modal { + --border-color: #3a3a3a; +} + +.new-username { + padding: 10px 16px !important; + align-self: stretch; + border-radius: 100px !important; + background-color: var(--gray-superlight) !important; + box-shadow: 0px 0px 0px 4px transparent !important; + color: var(--gray-ultradark) !important; +} + +.new-username:focus { + box-shadow: 0px 0px 0px 4px rgba(172, 136, 255, 0.8) !important; +} + +.new-trending { + max-height: 30vh; + overflow-y: scroll; +} diff --git a/packages/app/src/Pages/onboarding/index.tsx b/packages/app/src/Pages/onboarding/index.tsx new file mode 100644 index 000000000..4f82523b4 --- /dev/null +++ b/packages/app/src/Pages/onboarding/index.tsx @@ -0,0 +1,74 @@ +import "./index.css"; +import { Outlet, RouteObject } from "react-router-dom"; +import { SignIn, SignUp } from "./start"; +import { AllLanguageCodes } from "Pages/settings/Preferences"; +import Icon from "Icons/Icon"; +import { Profile } from "./profile"; +import { Topics } from "./topics"; +import { Discover } from "./discover"; +import { useLocale } from "IntlProvider"; +import { Moderation } from "./moderation"; + +export interface NewUserState { + name?: string; + picture?: string; + topics?: Array; + muteLists?: Array; +} + +function OnboardingLayout() { + const { lang, setOverride } = useLocale(); + + return ( +
+
+ + +
+
+ +
+
+ ); +} + +export const OnboardingRoutes = [ + { + path: "/login", + element: , + children: [ + { + path: "", + element: , + }, + { + path: "sign-up", + element: , + }, + { + path: "sign-up/profile", + element: , + }, + { + path: "sign-up/topics", + element: , + }, + { + path: "sign-up/discover", + element: , + }, + { + path: "sign-up/moderation", + element: , + }, + ], + }, +] as Array; diff --git a/packages/app/src/Pages/onboarding/moderation.tsx b/packages/app/src/Pages/onboarding/moderation.tsx new file mode 100644 index 000000000..e7017608f --- /dev/null +++ b/packages/app/src/Pages/onboarding/moderation.tsx @@ -0,0 +1,156 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { ReactNode, useState } from "react"; +import { FormattedMessage } from "react-intl"; +import { useNavigate } from "react-router-dom"; +import AsyncButton from "Element/AsyncButton"; +import classNames from "classnames"; +import { appendDedupe } from "SnortUtils"; +import useEventPublisher from "Hooks/useEventPublisher"; +import { setMuted } from "Login"; +import { ToggleSwitch } from "Icons/Toggle"; + +export const FixedModeration = { + hateSpeech: { + title: , + words: [], + canEdit: false, + }, + derogatory: { + title: , + words: [], + canEdit: false, + }, + nsfw: { + title: , + words: [ + "adult content", + "explicit", + "mature audiences", + "18+", + "sensitive content", + "graphic content", + "age-restricted", + "explicit material", + "adult material", + "nsfw", + "explicit images", + "adult film", + "adult video", + "mature themes", + "sexual content", + "graphic violence", + "strong language", + "explicit language", + "adult-only", + "mature language", + ], + canEdit: false, + }, + crypto: { + title: , + words: [ + "bitcoin", + "btc", + "satoshi", + "crypto", + "blockchain", + "mining", + "wallet", + "exchange", + "halving", + "hash rate", + "ledger", + "crypto trading", + "digital currency", + "virtual currency", + "cryptocurrency investment", + "altcoin", + "decentralized finance", + "defi", + "token", + "ico", + "crypto wallet", + "satoshi nakamoto", + ], + canEdit: true, + }, + politics: { + title: , + words: [], + canEdit: true, + }, +}; + +export function Moderation() { + const { publisher, system } = useEventPublisher(); + const [topics, setTopics] = useState>(Object.keys(FixedModeration)); + const navigate = useNavigate(); + + return ( +
+
+

+ +

+ +
+
+
+ + + + + + + + topics.length === Object.keys(FixedModeration).length + ? setTopics([]) + : setTopics(Object.keys(FixedModeration)) + } + className={topics.length === Object.keys(FixedModeration).length ? "active" : ""} + /> +
+ {Object.entries(FixedModeration).map(([k, v]) => ( +
+
{v.title}
+ {v.canEdit && ( +
+ +
+ )} + setTopics(s => (topics.includes(k) ? s.filter(a => a !== k) : appendDedupe(s, [k])))} + /> +
+ ))} +
+
+ + + + + + + +
+ { + const words = Object.entries(FixedModeration) + .filter(([k]) => topics.includes(k)) + .map(([, v]) => v.words) + .flat(); + if (words.length > 0) { + // no + } + navigate("/"); + }}> + + +
+ ); +} diff --git a/packages/app/src/Pages/onboarding/profile.tsx b/packages/app/src/Pages/onboarding/profile.tsx new file mode 100644 index 000000000..240a51566 --- /dev/null +++ b/packages/app/src/Pages/onboarding/profile.tsx @@ -0,0 +1,47 @@ +import AsyncButton from "Element/AsyncButton"; +import AvatarEditor from "Element/User/AvatarEditor"; +import { useContext, useState } from "react"; +import { FormattedMessage } from "react-intl"; +import { useLocation, useNavigate } from "react-router-dom"; +import { generateNewLogin } from "Login"; +import { SnortContext } from "@snort/system-react"; +import { NotEncrypted } from "@snort/system"; +import { NewUserState } from "."; + +export function Profile() { + const system = useContext(SnortContext); + const [picture, setPicture] = useState(); + const [error, setError] = useState(""); + const navigate = useNavigate(); + const location = useLocation(); + const state = location.state as NewUserState; + + async function makeRandomKey() { + try { + setError(""); + await generateNewLogin(system, key => Promise.resolve(new NotEncrypted(key)), { + name: state.name, + picture, + }); + window.plausible?.("Generate Account"); + navigate("/login/sign-up/topics"); + } catch (e) { + if (e instanceof Error) { + setError(e.message); + } + } + } + + return ( +
+

+ +

+ setPicture(p)} /> + makeRandomKey()}> + + + {error && {error}} +
+ ); +} diff --git a/packages/app/src/Pages/onboarding/start.tsx b/packages/app/src/Pages/onboarding/start.tsx new file mode 100644 index 000000000..412f502bf --- /dev/null +++ b/packages/app/src/Pages/onboarding/start.tsx @@ -0,0 +1,151 @@ +import { FormattedMessage, useIntl } from "react-intl"; +import { useState } from "react"; +import { Link, useNavigate } from "react-router-dom"; +import { unwrap } from "@snort/shared"; + +import AsyncButton from "Element/AsyncButton"; +import Icon from "Icons/Icon"; +import { NewUserState } from "."; +import { LoginSessionType, LoginStore } from "Login"; +import useLoginHandler from "Hooks/useLoginHandler"; +import { NotEncrypted } from "@snort/system"; +import classNames from "classnames"; + +export function SignIn() { + const navigate = useNavigate(); + const { formatMessage } = useIntl(); + const [key, setKey] = useState(""); + const [error, setError] = useState(""); + const [useKey, setUseKey] = useState(false); + const loginHandler = useLoginHandler(); + + const hasNip7 = "nostr" in window; + async function doNip07Login() { + const relays = + "getRelays" in unwrap(window.nostr) ? await unwrap(window.nostr?.getRelays).call(window.nostr) : undefined; + const pubKey = await unwrap(window.nostr).getPublicKey(); + LoginStore.loginWithPubkey(pubKey, LoginSessionType.Nip7, relays); + navigate("/"); + } + + async function doLogin() { + setError(""); + try { + await loginHandler.doLogin(key, key => Promise.resolve(new NotEncrypted(key))); + navigate("/"); + } catch (e) { + if (e instanceof Error) { + setError(e.message); + } else { + setError( + formatMessage({ + defaultMessage: "Unknown login error", + }), + ); + } + console.error(e); + } + } + + const nip7Login = hasNip7 && !useKey; + return ( +
+ +
+

+ +

+ {nip7Login && } +
+
+ {hasNip7 && !useKey && ( + <> + +
+ +
+ +
+ + + + setUseKey(true)}> + + + + )} + {(!hasNip7 || useKey) && ( + <> + setKey(e.target.value)} + className="new-username" + /> + {error && {error}} + + + + + )} +
+
+ + navigate("/login/sign-up")}> + + +
+
+ ); +} + +export function SignUp() { + const { formatMessage } = useIntl(); + const navigate = useNavigate(); + const [name, setName] = useState(""); + + return ( +
+ +
+

+ +

+ +
+
+ setName(e.target.value)} + className="new-username" + /> + + navigate("/login/sign-up/profile", { + state: { + name: name, + } as NewUserState, + }) + }> + + +
+
+ + navigate("/login")}> + + +
+
+ ); +} diff --git a/packages/app/src/Pages/onboarding/topics.tsx b/packages/app/src/Pages/onboarding/topics.tsx new file mode 100644 index 000000000..37e31f107 --- /dev/null +++ b/packages/app/src/Pages/onboarding/topics.tsx @@ -0,0 +1,85 @@ +import { ReactNode, useState } from "react"; +import { FormattedMessage } from "react-intl"; +import { useNavigate } from "react-router-dom"; +import AsyncButton from "Element/AsyncButton"; +import classNames from "classnames"; +import { appendDedupe } from "SnortUtils"; +import useEventPublisher from "Hooks/useEventPublisher"; + +export const FixedTopics = { + life: { + text: , + tags: ["life"], + }, + science: { + text: , + tags: ["science"], + }, + nature: { + text: , + tags: ["nature"], + }, + business: { + text: , + tags: ["business"], + }, + game: { + text: , + tags: ["game", "gaming"], + }, + sport: { + text: , + tags: ["sport"], + }, + photography: { + text: , + tags: ["photography"], + }, + bitcoin: { + text: , + tags: ["bitcoin"], + }, +}; + +export function Topics() { + const { publisher, system } = useEventPublisher(); + const [topics, setTopics] = useState>([]); + const navigate = useNavigate(); + + function tab(name: string, text: ReactNode) { + const active = topics.includes(name); + return ( +
setTopics(s => (active ? s.filter(a => a !== name) : appendDedupe(s, [name])))}> + {text} +
+ ); + } + + return ( +
+

+ +

+
{Object.entries(FixedTopics).map(([k, v]) => tab(k, v.text))}
+ { + const tags = Object.entries(FixedTopics) + .filter(([k]) => topics.includes(k)) + .map(([, v]) => v.tags) + .flat(); + if (tags.length > 0) { + const ev = await publisher?.tags(tags); + if (ev) { + await system.BroadcastEvent(ev); + } + } + navigate("/login/sign-up/discover"); + }}> + + +
+ ); +} diff --git a/packages/app/src/Pages/settings/Preferences.tsx b/packages/app/src/Pages/settings/Preferences.tsx index 3d1ebd296..b2ffbedb8 100644 --- a/packages/app/src/Pages/settings/Preferences.tsx +++ b/packages/app/src/Pages/settings/Preferences.tsx @@ -2,9 +2,10 @@ import "./Preferences.css"; import { FormattedMessage, useIntl } from "react-intl"; import useLogin from "Hooks/useLogin"; -import { DefaultPreferences, updatePreferences, UserPreferences } from "Login"; +import { updatePreferences, UserPreferences } from "Login"; import { DefaultImgProxy } from "Const"; import { unwrap } from "SnortUtils"; +import { useLocale } from "IntlProvider"; import messages from "./messages"; @@ -36,6 +37,7 @@ const PreferencesPage = () => { const { formatMessage } = useIntl(); const login = useLogin(); const perf = login.preferences; + const { lang } = useLocale(); return (
@@ -49,7 +51,7 @@ const PreferencesPage = () => {