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 ( return (
<div className="note"> <div className="note">
<div className="header"> <div className="header">
<ProfileImage pubKey={ev.PubKey} subHeader={replyTag()} /> <ProfileImage pubkey={ev.PubKey} subHeader={replyTag()} />
<div className="info"> <div className="info">
{moment(ev.CreatedAt * 1000).fromNow()} {moment(ev.CreatedAt * 1000).fromNow()}
</div> </div>

View File

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

View File

@ -4,14 +4,15 @@ import useProfile from "../feed/ProfileFeed";
import Nostrich from "../nostrich.jpg"; import Nostrich from "../nostrich.jpg";
export default function ProfileImage(props) { export default function ProfileImage(props) {
const pubKey = props.pubKey; const pubKey = props.pubkey;
const subHeader = props.subHeader; const subHeader = props.subHeader;
const navigate = useNavigate(); const navigate = useNavigate();
const user = useProfile(pubKey); const user = useProfile(pubKey);
const hasImage = (user?.picture?.length ?? 0) > 0;
return ( return (
<div className="pfp"> <div className="pfp">
<img src={user?.picture ?? Nostrich} onClick={() => navigate(`/p/${pubKey}`)} /> <img src={hasImage ? user.picture : Nostrich} onClick={() => navigate(`/p/${pubKey}`)} />
<div> <div>
{user?.name ?? pubKey.substring(0, 8)} {user?.name ?? pubKey.substring(0, 8)}
{subHeader} {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 pubKey = useSelector(s => s.login.publicKey);
const privKey = useSelector(s => s.login.privateKey); const privKey = useSelector(s => s.login.privateKey);
const nip07 = useSelector(s => s.login.nip07); 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; const hasNip07 = 'nostr' in window;
/** /**
@ -94,6 +96,17 @@ export default function useEventPublisher() {
ev.Content = "-"; ev.Content = "-";
ev.Tags.push(new Tag(["e", evRef.Id], 0)); ev.Tags.push(new Tag(["e", evRef.Id], 0));
ev.Tags.push(new Tag(["p", evRef.PubKey], 1)); 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); return await signEvent(ev, privKey);
} }
} }

View File

@ -32,12 +32,12 @@ export default function useSubscription(sub, opt) {
useEffect(() => { useEffect(() => {
if (sub) { if (sub) {
sub.OnEvent = (e) => { sub.OnEvent = (e) => {
console.debug(e);
dispatch(e); dispatch(e);
}; };
if (!options.leaveOpen) { if (!options.leaveOpen) {
sub.OnEnd = (c) => { sub.OnEnd = (c) => {
sub.OnEvent = () => {};
c.RemoveSubscription(sub.Id); c.RemoveSubscription(sub.Id);
if (sub.IsFinished()) { if (sub.IsFinished()) {
System.RemoveSubscription(sub.Id); System.RemoveSubscription(sub.Id);
@ -48,7 +48,7 @@ export default function useSubscription(sub, opt) {
console.debug("Adding sub: ", sub.ToObject()); console.debug("Adding sub: ", sub.ToObject());
System.AddSubscription(sub); System.AddSubscription(sub);
return () => { return () => {
console.debug("Adding sub: ", sub.ToObject()); console.debug("Removing sub: ", sub.ToObject());
System.RemoveSubscription(sub.Id); 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 { useDispatch, useSelector } from "react-redux";
import { System } from "..";
import Event from "../nostr/Event"; import Event from "../nostr/Event";
import EventKind from "../nostr/EventKind"; import EventKind from "../nostr/EventKind";
import { Subscriptions } from "../nostr/Subscriptions"; import { Subscriptions } from "../nostr/Subscriptions";
import { setUserData } from "../state/Users"; import { setUserData } from "../state/Users";
import useSubscription from "./Subscription";
export default function useUsersCache() { export default function useUsersCache() {
const dispatch = useDispatch(); const dispatch = useDispatch();
const pKeys = useSelector(s => s.users.pubKeys); const pKeys = useSelector(s => s.users.pubKeys);
const users = useSelector(s => s.users.users); const users = useSelector(s => s.users.users);
const [loading, setLoading] = useState(false);
function isUserCached(id) { 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]; let u = users[id];
return u && u.loaded > expire; 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)); let needProfiles = pKeys.filter(a => !isUserCached(a));
if (needProfiles.length === 0) { if (needProfiles.length === 0) {
return; return null;
} }
let sub = new Subscriptions(); let sub = new Subscriptions();
sub.Id = "profiles"; sub.Id = `profiles:${sub.Id}`;
sub.Authors = new Set(needProfiles); sub.Authors = new Set(needProfiles.slice(0, 20));
sub.Kinds.add(EventKind.SetMetadata); sub.Kinds.add(EventKind.SetMetadata);
sub.OnEvent = (ev) => {
dispatch(setUserData(mapEventToProfile(ev)));
};
let events = await System.RequestSubscription(sub); return sub;
let profiles = events }, [pKeys]);
.filter(a => a.kind === EventKind.SetMetadata)
.map(mapEventToProfile); const results = useSubscription(sub);
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
]));
}
useEffect(() => { useEffect(() => {
if (pKeys.length > 0 && !loading) { dispatch(setUserData(results.notes.map(a => mapEventToProfile(a))));
}, [results]);
setLoading(true); return results;
getUsers()
.catch(console.error)
.then(() => setLoading(false));
}
}, [pKeys, loading]);
return { users };
} }

View File

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

View File

@ -17,6 +17,7 @@ import ProfilePage from './pages/ProfilePage';
import RootPage from './pages/Root'; import RootPage from './pages/Root';
import Store from "./state/Store"; import Store from "./state/Store";
import NotificationsPage from './pages/Notifications'; import NotificationsPage from './pages/Notifications';
import NewUserPage from './pages/NewUserPage';
export const System = new NostrSystem(); export const System = new NostrSystem();
@ -32,6 +33,7 @@ root.render(
<Route path="/e/:id" exact element={<EventPage />} /> <Route path="/e/:id" exact element={<EventPage />} />
<Route path="/p/:id" exact element={<ProfilePage />} /> <Route path="/p/:id" exact element={<ProfilePage />} />
<Route path="/notifications" exact element={<NotificationsPage />} /> <Route path="/notifications" exact element={<NotificationsPage />} />
<Route path="/new" exact element={<NewUserPage />} />
</Routes> </Routes>
</Layout> </Layout>
</Router> </Router>

View File

@ -142,13 +142,8 @@ export default class Connection {
_OnEvent(subId, ev) { _OnEvent(subId, ev) {
if (this.Subscriptions[subId]) { if (this.Subscriptions[subId]) {
this._VerifySig(ev) //this._VerifySig(ev);
.then((e) => { this.Subscriptions[subId].OnEvent(ev);
if (this.Subscriptions[subId]) {
this.Subscriptions[subId].OnEvent(e);
}
})
.catch(console.error);
} else { } else {
console.warn(`No subscription for event! ${subId}`); console.warn(`No subscription for event! ${subId}`);
} }
@ -158,13 +153,17 @@ export default class Connection {
let sub = this.Subscriptions[subId]; let sub = this.Subscriptions[subId];
if (sub) { if (sub) {
sub.Finished[this.Address] = new Date().getTime(); 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); sub.OnEnd(this);
} else { } else {
console.warn(`No subscription for end! ${subId}`); console.warn(`No subscription for end! ${subId}`);
} }
} }
async _VerifySig(ev) { _VerifySig(ev) {
let payload = [ let payload = [
0, 0,
ev.pubkey, ev.pubkey,
@ -175,9 +174,9 @@ export default class Connection {
]; ];
let payloadData = new TextEncoder().encode(JSON.stringify(payload)); 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); 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"; throw "Sig verify failed";
} }
return ev; return ev;

View File

@ -39,7 +39,7 @@ export default function Layout(props) {
<FontAwesomeIcon icon={faBell} size="xl" /> <FontAwesomeIcon icon={faBell} size="xl" />
{notifications?.length ?? 0} {notifications?.length ?? 0}
</div> </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 publicKey = useSelector(s => s.login.publicKey);
const [key, setKey] = useState(""); const [key, setKey] = useState("");
useEffect(() => {
if (publicKey) {
navigate("/");
}
}, [publicKey]);
function doLogin() { function doLogin() {
if (key.startsWith("nsec")) { if (key.startsWith("nsec")) {
let nKey = bech32.decode(key); let nKey = bech32.decode(key);
@ -34,6 +40,7 @@ export default function LoginPage() {
async function makeRandomKey() { async function makeRandomKey() {
let newKey = secp.utils.bytesToHex(secp.utils.randomPrivateKey()); let newKey = secp.utils.bytesToHex(secp.utils.randomPrivateKey());
dispatch(setPrivateKey(newKey)) dispatch(setPrivateKey(newKey))
navigate("/new");
} }
async function doNip07Login() { async function doNip07Login() {
@ -57,12 +64,6 @@ export default function LoginPage() {
) )
} }
useEffect(() => {
if (publicKey) {
navigate("/");
}
}, [publicKey]);
return ( return (
<> <>
<h1>Login</h1> <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 "./ProfilePage.css";
import { useEffect, useMemo, useRef, useState } from "react"; import { useMemo, useRef, useState } from "react";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import { bech32 } from "bech32"; import { bech32 } from "bech32";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
@ -14,6 +14,7 @@ import useTimelineFeed from "../feed/TimelineFeed";
import Note from "../element/Note"; import Note from "../element/Note";
import QRCodeStyling from "qr-code-styling"; import QRCodeStyling from "qr-code-styling";
import Modal from "../element/Modal"; import Modal from "../element/Modal";
import { logout } from "../state/Login";
export default function ProfilePage() { export default function ProfilePage() {
const dispatch = useDispatch(); const dispatch = useDispatch();
@ -37,7 +38,7 @@ export default function ProfilePage() {
useMemo(() => { useMemo(() => {
if (user) { if (user) {
setName(user.name ?? ""); setName(user.name ?? "");
setPicture(user.picture ?? Nostrich); setPicture(user.picture ?? "");
setAbout(user.about ?? ""); setAbout(user.about ?? "");
setWebsite(user.website ?? ""); setWebsite(user.website ?? "");
setNip05(user.nip05 ?? ""); setNip05(user.nip05 ?? "");
@ -87,6 +88,23 @@ export default function ProfilePage() {
dispatch(resetProfile(id)); 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() { function editor() {
return ( return (
<> <>
@ -121,7 +139,9 @@ export default function ProfilePage() {
</div> </div>
</div> </div>
<div className="form-group"> <div className="form-group">
<div></div> <div>
<div className="btn" onClick={() => dispatch(logout())}>Logout</div>
</div>
<div> <div>
<div className="btn" onClick={() => saveProfile()}>Save</div> <div className="btn" onClick={() => saveProfile()}>Save</div>
</div> </div>
@ -155,9 +175,9 @@ export default function ProfilePage() {
<> <>
<div className="profile"> <div className="profile">
<div> <div>
<div style={{ backgroundImage: `url(${picture})` }} className="avatar"> <div style={{ backgroundImage: `url(${picture.length === 0 ? Nostrich : picture})` }} className="avatar">
{isMe ? {isMe ?
<div className="edit"> <div className="edit" onClick={() => setNewAvatar()}>
<div>Edit</div> <div>Edit</div>
</div> </div>
: null : null

View File

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

View File

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