parent
efa765ea84
commit
a1d42fa9fb
@ -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));
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
26
src/Pages/new/DiscoverFollows.tsx
Normal file
26
src/Pages/new/DiscoverFollows.tsx
Normal 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>
|
||||
</>
|
||||
)
|
||||
}
|
67
src/Pages/new/ImportFollows.tsx
Normal file
67
src/Pages/new/ImportFollows.tsx
Normal 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>
|
||||
</>
|
||||
)
|
||||
}
|
15
src/Pages/new/NewProfile.tsx
Normal file
15
src/Pages/new/NewProfile.tsx
Normal 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
86
src/Pages/new/index.tsx
Normal 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>
|
||||
</>
|
||||
)
|
||||
}
|
@ -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>
|
||||
|
@ -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
|
||||
]
|
||||
}
|
||||
]);
|
||||
|
Loading…
x
Reference in New Issue
Block a user