Too many changes i forgot

This commit is contained in:
Kieran 2023-01-01 19:57:27 +00:00
parent 3910c0d67a
commit b7b10eebbc
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
18 changed files with 183 additions and 74 deletions

View File

@ -0,0 +1,21 @@
import { useSelector } from "react-redux";
import useEventPublisher from "../feed/EventPublisher";
export default function FollowButton(props) {
const pubkey = props.pubkey;
const className = props.className ? `btn ${props.className}` : "btn";
const publiser = useEventPublisher();
const follows = useSelector(s => s.login.follows);
async function follow(pubkey) {
let ev = await publiser.addFollow(pubkey);
publiser.broadcast(ev);
}
let isFollowing = follows?.includes(pubkey) ?? false;
return (
<div className={className} onClick={() => follow(pubkey)}>
{isFollowing ? "Unfollow" : "Follow"}
</div>
)
}

View File

@ -122,7 +122,7 @@ export default function Note(props) {
return (
<div className="note">
<div className="header">
<ProfileImage pubKey={ev.PubKey} subHeader={replyTag()} />
<ProfileImage pubkey={ev.PubKey} subHeader={replyTag()} />
<div className="info">
{moment(ev.CreatedAt * 1000).fromNow()}
</div>

View File

@ -5,7 +5,7 @@ export default function NoteGhost(props) {
return (
<div className="note">
<div className="header">
<ProfileImage pubKey="" />
<ProfileImage pubkey="" />
</div>
<div className="body">
{props.text ?? "Loading..."}

View File

@ -4,14 +4,15 @@ import useProfile from "../feed/ProfileFeed";
import Nostrich from "../nostrich.jpg";
export default function ProfileImage(props) {
const pubKey = props.pubKey;
const pubKey = props.pubkey;
const subHeader = props.subHeader;
const navigate = useNavigate();
const user = useProfile(pubKey);
const hasImage = (user?.picture?.length ?? 0) > 0;
return (
<div className="pfp">
<img src={user?.picture ?? Nostrich} onClick={() => navigate(`/p/${pubKey}`)} />
<img src={hasImage ? user.picture : Nostrich} onClick={() => navigate(`/p/${pubKey}`)} />
<div>
{user?.name ?? pubKey.substring(0, 8)}
{subHeader}

View File

@ -0,0 +1,9 @@
.profile-preview {
display: flex;
padding: 5px 0;
align-items: center;
}
.profile-preview .pfp {
flex-grow: 1;
}

View File

@ -0,0 +1,19 @@
import "./ProfilePreview.css";
import ProfileImage from "./ProfileImage";
import { useSelector } from "react-redux";
import FollowButton from "./FollowButton";
export default function ProfilePreview(props) {
const pubkey = props.pubkey;
const user = useSelector(s => s.users.users[pubkey]);
return (
<div className="profile-preview">
<ProfileImage pubkey={pubkey}/>
<div className="f-ellipsis">
{user?.about}
</div>
<FollowButton pubkey={pubkey} className="ml5"/>
</div>
)
}

View File

@ -8,6 +8,8 @@ export default function useEventPublisher() {
const pubKey = useSelector(s => s.login.publicKey);
const privKey = useSelector(s => s.login.privateKey);
const nip07 = useSelector(s => s.login.nip07);
const follows = useSelector(s => s.login.follows);
const relays = useSelector(s => s.login.relays);
const hasNip07 = 'nostr' in window;
/**
@ -94,6 +96,17 @@ export default function useEventPublisher() {
ev.Content = "-";
ev.Tags.push(new Tag(["e", evRef.Id], 0));
ev.Tags.push(new Tag(["p", evRef.PubKey], 1));
return await signEvent(ev, privKey);
},
addFollow: async (pubkey) => {
let ev = Event.ForPubKey(pubKey);
ev.Kind = EventKind.ContactList;
ev.Content = JSON.stringify(relays);
for(let pk of follows) {
ev.Tags.push(new Tag(["p", pk]));
}
ev.Tags.push(new Tag(["p", pubkey]));
return await signEvent(ev, privKey);
}
}

View File

@ -32,12 +32,12 @@ export default function useSubscription(sub, opt) {
useEffect(() => {
if (sub) {
sub.OnEvent = (e) => {
console.debug(e);
dispatch(e);
};
if (!options.leaveOpen) {
sub.OnEnd = (c) => {
sub.OnEvent = () => {};
c.RemoveSubscription(sub.Id);
if (sub.IsFinished()) {
System.RemoveSubscription(sub.Id);
@ -48,7 +48,7 @@ export default function useSubscription(sub, opt) {
console.debug("Adding sub: ", sub.ToObject());
System.AddSubscription(sub);
return () => {
console.debug("Adding sub: ", sub.ToObject());
console.debug("Removing sub: ", sub.ToObject());
System.RemoveSubscription(sub.Id);
};
}

View File

@ -1,19 +1,18 @@
import { useEffect, useState } from "react";
import { useEffect, useMemo } from "react";
import { useDispatch, useSelector } from "react-redux";
import { System } from "..";
import Event from "../nostr/Event";
import EventKind from "../nostr/EventKind";
import { Subscriptions } from "../nostr/Subscriptions";
import { setUserData } from "../state/Users";
import useSubscription from "./Subscription";
export default function useUsersCache() {
const dispatch = useDispatch();
const pKeys = useSelector(s => s.users.pubKeys);
const users = useSelector(s => s.users.users);
const [loading, setLoading] = useState(false);
function isUserCached(id) {
let expire = new Date().getTime() - (1_000 * 60 * 5); // 60s expire
let expire = new Date().getTime() - (1_000 * 60 * 5); // 5min expire
let u = users[id];
return u && u.loaded > expire;
}
@ -29,46 +28,25 @@ export default function useUsersCache() {
};
}
async function getUsers() {
const sub = useMemo(() => {
let needProfiles = pKeys.filter(a => !isUserCached(a));
if (needProfiles.length === 0) {
return;
return null;
}
let sub = new Subscriptions();
sub.Id = "profiles";
sub.Authors = new Set(needProfiles);
sub.Kinds.add(EventKind.SetMetadata);
sub.OnEvent = (ev) => {
dispatch(setUserData(mapEventToProfile(ev)));
};
let events = await System.RequestSubscription(sub);
let profiles = events
.filter(a => a.kind === EventKind.SetMetadata)
.map(mapEventToProfile);
let missing = needProfiles.filter(a => !events.some(b => b.pubkey === a));
let missingProfiles = missing.map(a => {
return {
pubkey: a,
loaded: new Date().getTime()
}
});
dispatch(setUserData([
...profiles,
...missingProfiles
]));
}
let sub = new Subscriptions();
sub.Id = `profiles:${sub.Id}`;
sub.Authors = new Set(needProfiles.slice(0, 20));
sub.Kinds.add(EventKind.SetMetadata);
return sub;
}, [pKeys]);
const results = useSubscription(sub);
useEffect(() => {
if (pKeys.length > 0 && !loading) {
dispatch(setUserData(results.notes.map(a => mapEventToProfile(a))));
}, [results]);
setLoading(true);
getUsers()
.catch(console.error)
.then(() => setLoading(false));
}
}, [pKeys, loading]);
return { users };
return results;
}

View File

@ -85,6 +85,14 @@ input[type="text"], input[type="password"] {
flex-grow: 1;
}
.f-ellipsis {
min-width: 0;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
padding: 0 15px;
}
a {
color: inherit;
line-height: 1.3em;
@ -154,6 +162,10 @@ body.scroll-lock {
margin-right: 10px;
}
.ml5 {
margin-left: 5px;
}
.tabs {
display: flex;
margin: 10px 0;

View File

@ -17,6 +17,7 @@ import ProfilePage from './pages/ProfilePage';
import RootPage from './pages/Root';
import Store from "./state/Store";
import NotificationsPage from './pages/Notifications';
import NewUserPage from './pages/NewUserPage';
export const System = new NostrSystem();
@ -32,6 +33,7 @@ root.render(
<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 />} />
</Routes>
</Layout>
</Router>

View File

@ -142,13 +142,8 @@ export default class Connection {
_OnEvent(subId, ev) {
if (this.Subscriptions[subId]) {
this._VerifySig(ev)
.then((e) => {
if (this.Subscriptions[subId]) {
this.Subscriptions[subId].OnEvent(e);
}
})
.catch(console.error);
//this._VerifySig(ev);
this.Subscriptions[subId].OnEvent(ev);
} else {
console.warn(`No subscription for event! ${subId}`);
}
@ -158,13 +153,17 @@ export default class Connection {
let sub = this.Subscriptions[subId];
if (sub) {
sub.Finished[this.Address] = new Date().getTime();
let responseTime = sub.Finished[this.Address] - sub.Started[this.Address];
if (responseTime > 10_000) {
console.warn(`[${this.Address}][${subId}] Slow response time ${(responseTime / 1000).toFixed(1)} seconds`);
}
sub.OnEnd(this);
} else {
console.warn(`No subscription for end! ${subId}`);
}
}
async _VerifySig(ev) {
_VerifySig(ev) {
let payload = [
0,
ev.pubkey,
@ -175,9 +174,9 @@ export default class Connection {
];
let payloadData = new TextEncoder().encode(JSON.stringify(payload));
let data = await secp.utils.sha256(payloadData);
let data = secp.utils.sha256Sync(payloadData);
let hash = secp.utils.bytesToHex(data);
if (!await secp.schnorr.verify(ev.sig, hash, ev.pubkey)) {
if (!secp.schnorr.verifySync(ev.sig, hash, ev.pubkey)) {
throw "Sig verify failed";
}
return ev;

View File

@ -39,7 +39,7 @@ export default function Layout(props) {
<FontAwesomeIcon icon={faBell} size="xl" />
{notifications?.length ?? 0}
</div>
<ProfileImage pubKey={key} />
<ProfileImage pubkey={key} />
</>
)
}

View File

@ -12,6 +12,12 @@ export default function LoginPage() {
const publicKey = useSelector(s => s.login.publicKey);
const [key, setKey] = useState("");
useEffect(() => {
if (publicKey) {
navigate("/");
}
}, [publicKey]);
function doLogin() {
if (key.startsWith("nsec")) {
let nKey = bech32.decode(key);
@ -34,6 +40,7 @@ export default function LoginPage() {
async function makeRandomKey() {
let newKey = secp.utils.bytesToHex(secp.utils.randomPrivateKey());
dispatch(setPrivateKey(newKey))
navigate("/new");
}
async function doNip07Login() {
@ -57,12 +64,6 @@ export default function LoginPage() {
)
}
useEffect(() => {
if (publicKey) {
navigate("/");
}
}, [publicKey]);
return (
<>
<h1>Login</h1>

33
src/pages/NewUserPage.js Normal file
View File

@ -0,0 +1,33 @@
import ProfilePreview from "../element/ProfilePreview";
const RecommendedFollows = [
"82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2", // jack
"3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d", // fiatjaf
"020f2d21ae09bf35fcdfb65decf1478b846f5f728ab30c5eaabcd6d081a81c3e", // adam3us
"6e468422dfb74a5738702a8823b9b28168abab8655faacb6853cd0ee15deee93", // gigi
"217e3d8b61c087b10422427e114737a4a4a4b1e15f22301fb4b07e1f33204d7c", // Kieran
"32e1827635450ebb3c5a7d12c1f8e7b2b514439ac10a67eef3d9fd9c5c68e245", // jb55
"e33fe65f1fde44c6dc17eeb38fdad0fceaf1cae8722084332ed1e32496291d42", // wiz
"00000000827ffaa94bfea288c3dfce4422c794fbb96625b6b31e9049f729d700", // cameri
"A341F45FF9758F570A21B000C17D4E53A3A497C8397F26C0E6D61E5ACFFC7A98", // Saylor
"E88A691E98D9987C964521DFF60025F60700378A4879180DCBBB4A5027850411", // NVK
"C4EABAE1BE3CF657BC1855EE05E69DE9F059CB7A059227168B80B89761CBC4E0", // jackmallers
"85080D3BAD70CCDCD7F74C29A44F55BB85CBCD3DD0CBB957DA1D215BDB931204", // preston
"C49D52A573366792B9A6E4851587C28042FB24FA5625C6D67B8C95C8751ACA15", // holdonaut
"83E818DFBECCEA56B0F551576B3FD39A7A50E1D8159343500368FA085CCD964B", // jeffbooth
"3F770D65D3A764A9C5CB503AE123E62EC7598AD035D836E2A810F3877A745B24", // DerekRoss
"472F440F29EF996E92A186B8D320FF180C855903882E59D50DE1B8BD5669301E", // MartyBent
"1577e4599dd10c863498fe3c20bd82aafaf829a595ce83c5cf8ac3463531b09b", // yegorpetrov
];
export default function NewUserPage(props) {
return (
<>
<h2>Hmm you're not following anybody?</h2>
<h4>Here are some suggestions:</h4>
{RecommendedFollows
.sort(a => Math.random() >= 0.5 ? -1 : 1)
.map(a => <ProfilePreview key={a} pubkey={a.toLowerCase()} />)}
</>
)
}

View File

@ -1,5 +1,5 @@
import "./ProfilePage.css";
import { useEffect, useMemo, useRef, useState } from "react";
import { useMemo, useRef, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { bech32 } from "bech32";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
@ -14,6 +14,7 @@ import useTimelineFeed from "../feed/TimelineFeed";
import Note from "../element/Note";
import QRCodeStyling from "qr-code-styling";
import Modal from "../element/Modal";
import { logout } from "../state/Login";
export default function ProfilePage() {
const dispatch = useDispatch();
@ -37,7 +38,7 @@ export default function ProfilePage() {
useMemo(() => {
if (user) {
setName(user.name ?? "");
setPicture(user.picture ?? Nostrich);
setPicture(user.picture ?? "");
setAbout(user.about ?? "");
setWebsite(user.website ?? "");
setNip05(user.nip05 ?? "");
@ -87,6 +88,23 @@ export default function ProfilePage() {
dispatch(resetProfile(id));
}
async function openFile() {
return new Promise((resolve, reject) => {
let elm = document.createElement("input");
elm.type = "file";
elm.onchange = (e) => {
resolve(e.target.files[0]);
};
elm.click();
});
}
async function setNewAvatar() {
let file = await openFile();
console.log(file);
}
function editor() {
return (
<>
@ -121,7 +139,9 @@ export default function ProfilePage() {
</div>
</div>
<div className="form-group">
<div></div>
<div>
<div className="btn" onClick={() => dispatch(logout())}>Logout</div>
</div>
<div>
<div className="btn" onClick={() => saveProfile()}>Save</div>
</div>
@ -155,9 +175,9 @@ export default function ProfilePage() {
<>
<div className="profile">
<div>
<div style={{ backgroundImage: `url(${picture})` }} className="avatar">
<div style={{ backgroundImage: `url(${picture.length === 0 ? Nostrich : picture})` }} className="avatar">
{isMe ?
<div className="edit">
<div className="edit" onClick={() => setNewAvatar()}>
<div>Edit</div>
</div>
: null

View File

@ -3,19 +3,16 @@ import { useSelector } from "react-redux";
import Note from "../element/Note";
import useTimelineFeed from "../feed/TimelineFeed";
import { NoteCreator } from "../element/NoteCreator";
import ProfilePreview from "../element/ProfilePreview";
export default function RootPage() {
const pubKey = useSelector(s => s.login.publicKey);
const follows = useSelector(a => a.login.follows)
const follows = useSelector(a => a.login.follows);
const { notes } = useTimelineFeed(follows);
function followHints() {
if (follows?.length === 0 && pubKey) {
return (
<>
<h3>Hmm you're not following anybody?</h3>
</>
);
return <>Hmm nothing here..</>
}
}

View File

@ -96,8 +96,12 @@ const LoginSlice = createSlice({
];
},
logout: (state) => {
state.privateKey = null;
window.localStorage.removeItem(PrivateKeyItem);
window.localStorage.removeItem(Nip07PublicKeyItem);
state.privateKey = null;
state.publicKey = null;
state.follows = [];
state.notifications = [];
}
}
});