General improvements

This commit is contained in:
Kieran 2023-01-12 12:00:44 +00:00
parent 46df3a64c7
commit 783dbf8b68
No known key found for this signature in database
GPG Key ID: DE71CEB3925BE941
19 changed files with 642 additions and 261 deletions

View File

@ -8,6 +8,10 @@
"@fortawesome/react-fontawesome": "^0.2.0", "@fortawesome/react-fontawesome": "^0.2.0",
"@noble/secp256k1": "^1.7.0", "@noble/secp256k1": "^1.7.0",
"@reduxjs/toolkit": "^1.9.1", "@reduxjs/toolkit": "^1.9.1",
"@types/jest": "^29.2.5",
"@types/node": "^18.11.18",
"@types/react": "^18.0.26",
"@types/react-dom": "^18.0.10",
"bech32": "^2.0.0", "bech32": "^2.0.0",
"light-bolt11-decoder": "^2.1.0", "light-bolt11-decoder": "^2.1.0",
"qr-code-styling": "^1.6.0-rc.1", "qr-code-styling": "^1.6.0-rc.1",
@ -18,6 +22,7 @@
"react-router-dom": "^6.5.0", "react-router-dom": "^6.5.0",
"react-scripts": "5.0.1", "react-scripts": "5.0.1",
"react-twitter-embed": "^4.0.4", "react-twitter-embed": "^4.0.4",
"typescript": "^4.9.4",
"uuid": "^9.0.0" "uuid": "^9.0.0"
}, },
"scripts": { "scripts": {

View File

@ -2,13 +2,22 @@
/** /**
* Websocket re-connect timeout * Websocket re-connect timeout
*/ */
export const DefaultConnectTimeout = 1000; export const DefaultConnectTimeout = 2000;
/** /**
* How long profile cache should be considered valid for * How long profile cache should be considered valid for
*/ */
export const ProfileCacheExpire = (1_000 * 60 * 5); export const ProfileCacheExpire = (1_000 * 60 * 5);
/**
* Default bootstrap relays
*/
export const DefaultRelays = {
"wss://relay.snort.social": { read: true, write: true },
"wss://relay.damus.io": { read: true, write: true },
"wss://nostr-pub.wellorder.net": { read: true, write: true }
};
/** /**
* List of recommended follows for new users * List of recommended follows for new users
*/ */
@ -17,7 +26,7 @@ export const RecommendedFollows = [
"3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d", // fiatjaf "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d", // fiatjaf
"020f2d21ae09bf35fcdfb65decf1478b846f5f728ab30c5eaabcd6d081a81c3e", // adam3us "020f2d21ae09bf35fcdfb65decf1478b846f5f728ab30c5eaabcd6d081a81c3e", // adam3us
"6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93", // gigi "6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93", // gigi
"217e3d8b61c087b10422427e114737a4a4a4b1e15f22301fb4b07e1f33204d7c", // Kieran "63fe6318dc58583cfe16810f86dd09e18bfd76aabc24a0081ce2856f330504ed", // Kieran
"32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245", // jb55 "32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245", // jb55
"e33fe65f1fde44c6dc17eeb38fdad0fceaf1cae8722084332ed1e32496291d42", // wiz "e33fe65f1fde44c6dc17eeb38fdad0fceaf1cae8722084332ed1e32496291d42", // wiz
"00000000827ffaa94bfea288c3dfce4422c794fbb96625b6b31e9049f729d700", // cameri "00000000827ffaa94bfea288c3dfce4422c794fbb96625b6b31e9049f729d700", // cameri
@ -31,6 +40,7 @@ export const RecommendedFollows = [
"472F440F29EF996E92A186B8D320FF180C855903882E59D50DE1B8BD5669301E", // MartyBent "472F440F29EF996E92A186B8D320FF180C855903882E59D50DE1B8BD5669301E", // MartyBent
"1577e4599dd10c863498fe3c20bd82aafaf829a595ce83c5cf8ac3463531b09b", // yegorpetrov "1577e4599dd10c863498fe3c20bd82aafaf829a595ce83c5cf8ac3463531b09b", // yegorpetrov
"04c915daefee38317fa734444acee390a8269fe5810b2241e5e6dd343dfbecc9", // ODELL "04c915daefee38317fa734444acee390a8269fe5810b2241e5e6dd343dfbecc9", // ODELL
"7fa56f5d6962ab1e3cd424e758c3002b8665f7b0d8dcee9fe9e288d7751ac194", // verbiricha
]; ];
/** /**

View File

@ -15,7 +15,7 @@ export default function FollowListBase({ pubkeys, title}) {
<div className="f-grow">{title}</div> <div className="f-grow">{title}</div>
<div className="btn" onClick={() => followAll()}>Follow All</div> <div className="btn" onClick={() => followAll()}>Follow All</div>
</div> </div>
{pubkeys?.map(a => <ProfilePreview pubkey={a} key={a} options={{ about: false }} />)} {pubkeys?.map(a => <ProfilePreview pubkey={a} key={a} />)}
</> </>
) )
} }

View File

@ -20,7 +20,8 @@ export default function Invoice(props) {
let ret = { let ret = {
amount: !isNaN(amount) ? (amount / 1000) : 0, amount: !isNaN(amount) ? (amount / 1000) : 0,
expire: !isNaN(timestamp) && !isNaN(expire) ? timestamp + expire : null, expire: !isNaN(timestamp) && !isNaN(expire) ? timestamp + expire : null,
description description,
expired: false
}; };
if (ret.expire) { if (ret.expire) {
ret.expired = ret.expire < (new Date().getTime() / 1000); ret.expired = ret.expire < (new Date().getTime() / 1000);

View File

@ -14,9 +14,5 @@ export default function LazyImage(props) {
}) })
}, [inView]); }, [inView]);
return ( return shown ? <img {...props} /> : <div ref={ref}></div>
<div ref={ref}>
{shown ? <img {...props} /> : null}
</div>
)
} }

View File

@ -7,7 +7,7 @@ import useProfile from "../feed/ProfileFeed";
import { hexToBech32, profileLink } from "../Util"; import { hexToBech32, profileLink } from "../Util";
import LazyImage from "./LazyImage"; import LazyImage from "./LazyImage";
export default function ProfileImage({ pubkey, subHeader, showUsername = true }) { export default function ProfileImage({ pubkey, subHeader, showUsername = true, className }) {
const navigate = useNavigate(); const navigate = useNavigate();
const user = useProfile(pubkey); const user = useProfile(pubkey);
@ -23,11 +23,11 @@ export default function ProfileImage({ pubkey, subHeader, showUsername = true })
}, [user]); }, [user]);
return ( return (
<div className="pfp"> <div className={`pfp ${className ?? ""}`}>
<LazyImage src={hasImage ? user.picture : Nostrich} onClick={() => navigate(profileLink(pubkey))} /> <LazyImage src={hasImage ? user.picture : Nostrich} onClick={() => navigate(profileLink(pubkey))} />
{showUsername && (<div> {showUsername && (<div className="f-grow">
<Link key={pubkey} to={profileLink(pubkey)}>{name}</Link> <Link key={pubkey} to={profileLink(pubkey)}>{name}</Link>
{subHeader ? <div>{subHeader}</div> : null} {subHeader ? <>{subHeader}</> : null}
</div> </div>
)} )}
</div> </div>

View File

@ -7,4 +7,9 @@
.profile-preview .pfp { .profile-preview .pfp {
flex-grow: 1; flex-grow: 1;
min-width: 200px; min-width: 200px;
}
.profile-preview .about {
font-size: small;
color: var(--gray-light);
} }

View File

@ -13,10 +13,10 @@ export default function ProfilePreview(props) {
return ( return (
<div className="profile-preview"> <div className="profile-preview">
<ProfileImage pubkey={pubkey} /> <ProfileImage pubkey={pubkey} subHeader=
{options.about ? <div className="f-ellipsis"> {options.about ? <div className="f-ellipsis about">
{user?.about} {user?.about}
</div> : null} </div> : null} />
<FollowButton pubkey={pubkey} className="ml5" /> <FollowButton pubkey={pubkey} className="ml5" />
</div> </div>
) )

View File

@ -15,7 +15,7 @@ export default function useLoginFeed() {
const [pubKey, readNotifications] = useSelector(s => [s.login.publicKey, s.login.readNotifications]); const [pubKey, readNotifications] = useSelector(s => [s.login.publicKey, s.login.readNotifications]);
const sub = useMemo(() => { const sub = useMemo(() => {
if (pubKey === null) { if (!pubKey) {
return null; return null;
} }

View File

@ -7,7 +7,7 @@ export default function useProfile(pubKey) {
const user = useSelector(s => s.users.users[pubKey]); const user = useSelector(s => s.users.users[pubKey]);
useEffect(() => { useEffect(() => {
if (pubKey !== "") { if (pubKey) {
dispatch(addPubKey(pubKey)); dispatch(addPubKey(pubKey));
} }
}, [pubKey]); }, [pubKey]);

View File

@ -139,6 +139,11 @@ textarea:placeholder {
.f-grow { .f-grow {
flex-grow: 1; flex-grow: 1;
min-width: 0;
}
.f-shrink {
flex-shrink: 1;
} }
.f-ellipsis { .f-ellipsis {
@ -146,7 +151,6 @@ textarea:placeholder {
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
overflow: hidden; overflow: hidden;
padding: 0 15px;
} }
.f-col { .f-col {
@ -161,6 +165,13 @@ textarea:placeholder {
width: fill-available; width: fill-available;
} }
.w-max-w {
max-width: 100%;
max-width: -moz-available;
max-width: -webkit-fill-available;
max-width: fill-available;
}
a { a {
color: inherit; color: inherit;
line-height: 1.3em; line-height: 1.3em;

View File

@ -4,9 +4,8 @@ import React from 'react';
import ReactDOM from 'react-dom/client'; import ReactDOM from 'react-dom/client';
import { Provider } from 'react-redux' import { Provider } from 'react-redux'
import { import {
BrowserRouter as Router, createBrowserRouter,
Routes, RouterProvider,
Route,
} from "react-router-dom"; } from "react-router-dom";
import { NostrSystem } from './nostr/System'; import { NostrSystem } from './nostr/System';
@ -19,28 +18,55 @@ import Store from "./state/Store";
import NotificationsPage from './pages/Notifications'; import NotificationsPage from './pages/Notifications';
import NewUserPage from './pages/NewUserPage'; import NewUserPage from './pages/NewUserPage';
import SettingsPage from './pages/SettingsPage'; import SettingsPage from './pages/SettingsPage';
import ErrorPage from './pages/ErrorPage.tsx';
/**
* Nostr websocket managment system
*/
export const System = new NostrSystem(); export const System = new NostrSystem();
const router = createBrowserRouter([
{
element: <Layout />,
errorElement: <ErrorPage />,
children: [
{
path: "/",
element: <RootPage />
},
{
path: "/login",
element: <LoginPage />
},
{
path: "/e/:id",
element: <EventPage />
},
{
path: "/p/:id",
element: <ProfilePage />
},
{
path: "/notifications",
element: <NotificationsPage />
},
{
path: "/new",
element: <NewUserPage />
},
{
path: "/settings",
element: <SettingsPage />
}
]
}
]);
const root = ReactDOM.createRoot(document.getElementById('root')); const root = ReactDOM.createRoot(document.getElementById('root'));
root.render( root.render(
<React.StrictMode> <React.StrictMode>
<Provider store={Store}> <Provider store={Store}>
<Router> <RouterProvider router={router} />
<Layout>
<Routes>
<Route path="/" exact element={<RootPage />} />
<Route path="/login" exact element={<LoginPage />} />
<Route path="/e/:id" exact element={<EventPage />} />
<Route path="/p/:id" exact element={<ProfilePage />} />
<Route path="/notifications" exact element={<NotificationsPage />} />
<Route path="/new" exact element={<NewUserPage />} />
<Route path="/settings" exact element={<SettingsPage />}>
<Route path="/settings/relays" exact element={<h2>Relays</h2>} />
</Route>
</Routes>
</Layout>
</Router>
</Provider> </Provider>
</React.StrictMode> </React.StrictMode>
); );

18
src/pages/ErrorPage.tsx Normal file
View File

@ -0,0 +1,18 @@
import React, { FC } from "react";
import { useRouteError } from "react-router-dom";
const ErrorPage: FC = () => {
const error = useRouteError();
console.error(error);
return (
<>
<h4>{error?.message ?? "Uknown error"}</h4>
<pre>
{JSON.stringify(error)}
</pre>
</>
);
};
export default ErrorPage;

View File

@ -1,7 +1,7 @@
import "./Layout.css"; import "./Layout.css";
import { useEffect } from "react" import { useEffect } from "react"
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { useNavigate } from "react-router-dom"; import { Outlet, useNavigate } from "react-router-dom";
import { faBell } from "@fortawesome/free-solid-svg-icons"; import { faBell } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
@ -85,7 +85,7 @@ export default function Layout(props) {
</div> </div>
</div> </div>
{props.children} <Outlet/>
</div> </div>
) )
} }

View File

@ -1,14 +1,19 @@
import { Outlet } from "react-router-dom";
import { RecommendedFollows } from "../Const"; import { RecommendedFollows } from "../Const";
import ProfilePreview from "../element/ProfilePreview"; import ProfilePreview from "../element/ProfilePreview";
export default function NewUserPage(props) { export default function NewUserPage(props) {
return (
<> function followSomebody() {
<h2>Hmm you're not following anybody?</h2> return (
<h4>Here are some suggestions:</h4> <>
{RecommendedFollows <h2>Follow some popular accounts</h2>
.sort(a => Math.random() >= 0.5 ? -1 : 1) <h4>Here are some suggestions:</h4>
.map(a => <ProfilePreview key={a} pubkey={a.toLowerCase()} />)} {RecommendedFollows
</> .sort(a => Math.random() >= 0.5 ? -1 : 1)
) .map(a => <ProfilePreview key={a} pubkey={a.toLowerCase()} />)}
</>
)
}
return followSomebody()
} }

View File

@ -17,7 +17,7 @@ export default function RootPage() {
const [eop] = useScroll(); const [eop] = useScroll();
function followHints() { function followHints() {
if (follows?.length === 0 && pubKey) { if (follows?.length === 0 && pubKey && tab !== RootTab.Global) {
return <> return <>
Hmm nothing here.. Checkout <Link to={"/new"}>New users page</Link> to follow some recommended nostrich's! Hmm nothing here.. Checkout <Link to={"/new"}>New users page</Link> to follow some recommended nostrich's!
</> </>

View File

@ -3,6 +3,7 @@ import Nostrich from "../nostrich.jpg";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { useNavigate } from "react-router-dom";
import useEventPublisher from "../feed/EventPublisher"; import useEventPublisher from "../feed/EventPublisher";
import useProfile from "../feed/ProfileFeed"; import useProfile from "../feed/ProfileFeed";
@ -13,6 +14,7 @@ import { openFile } from "../Util";
import Relay from "../element/Relay"; import Relay from "../element/Relay";
export default function SettingsPage(props) { export default function SettingsPage(props) {
const navigate = useNavigate();
const id = useSelector(s => s.login.publicKey); const id = useSelector(s => s.login.publicKey);
const relays = useSelector(s => s.login.relays); const relays = useSelector(s => s.login.relays);
const dispatch = useDispatch(); const dispatch = useDispatch();
@ -137,7 +139,7 @@ export default function SettingsPage(props) {
</div> </div>
<div className="form-group"> <div className="form-group">
<div> <div>
<div className="btn" onClick={() => dispatch(logout())}>Logout</div> <div className="btn" onClick={() => { dispatch(logout()); navigate("/"); }}>Logout</div>
</div> </div>
<div> <div>
<div className="btn" onClick={() => saveProfile()}>Save</div> <div className="btn" onClick={() => saveProfile()}>Save</div>
@ -159,15 +161,24 @@ export default function SettingsPage(props) {
) )
} }
function settings() {
if (!id) return null;
return (
<>
<h1>Settings</h1>
<div className="flex f-center">
<div style={{ backgroundImage: `url(${picture.length === 0 ? Nostrich : picture})` }} className="avatar">
<div className="edit" onClick={() => setNewAvatar()}>Edit</div>
</div>
</div>
{editor()}
</>
)
}
return ( return (
<div className="settings"> <div className="settings">
<h1>Settings</h1> {settings()}
<div className="flex f-center">
<div style={{ backgroundImage: `url(${picture.length === 0 ? Nostrich : picture})` }} className="avatar">
<div className="edit" onClick={() => setNewAvatar()}>Edit</div>
</div>
</div>
{editor()}
<h4>Relays</h4> <h4>Relays</h4>
<div className="flex f-col"> <div className="flex f-col">
{Object.keys(relays || {}).map(a => <Relay addr={a} key={a} />)} {Object.keys(relays || {}).map(a => <Relay addr={a} key={a} />)}

View File

@ -1,5 +1,6 @@
import { createSlice } from '@reduxjs/toolkit' import { createSlice } from '@reduxjs/toolkit'
import * as secp from '@noble/secp256k1'; import * as secp from '@noble/secp256k1';
import { DefaultRelays } from '../Const';
const PrivateKeyItem = "secret"; const PrivateKeyItem = "secret";
const PublicKeyItem = "pubkey"; const PublicKeyItem = "pubkey";
@ -54,11 +55,7 @@ const LoginSlice = createSlice({
state.loggedOut = true; state.loggedOut = true;
} }
state.relays = { state.relays = DefaultRelays;
"wss://nostr.v0l.io": { read: true, write: true },
"wss://relay.damus.io": { read: true, write: true },
"wss://nostr-pub.wellorder.net": { read: true, write: true }
};
// check pub key only // check pub key only
let pubKey = window.localStorage.getItem(PublicKeyItem); let pubKey = window.localStorage.getItem(PublicKeyItem);

692
yarn.lock

File diff suppressed because it is too large Load Diff