Too many changes i forgot
This commit is contained in:
parent
3910c0d67a
commit
b7b10eebbc
21
src/element/FollowButton.js
Normal file
21
src/element/FollowButton.js
Normal 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>
|
||||
)
|
||||
}
|
@ -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>
|
||||
|
@ -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..."}
|
||||
|
@ -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}
|
||||
|
9
src/element/ProfilePreview.css
Normal file
9
src/element/ProfilePreview.css
Normal file
@ -0,0 +1,9 @@
|
||||
.profile-preview {
|
||||
display: flex;
|
||||
padding: 5px 0;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.profile-preview .pfp {
|
||||
flex-grow: 1;
|
||||
}
|
19
src/element/ProfilePreview.js
Normal file
19
src/element/ProfilePreview.js
Normal 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>
|
||||
)
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
};
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
@ -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;
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
|
@ -39,7 +39,7 @@ export default function Layout(props) {
|
||||
<FontAwesomeIcon icon={faBell} size="xl" />
|
||||
{notifications?.length ?? 0}
|
||||
</div>
|
||||
<ProfileImage pubKey={key} />
|
||||
<ProfileImage pubkey={key} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
@ -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
33
src/pages/NewUserPage.js
Normal 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()} />)}
|
||||
</>
|
||||
)
|
||||
}
|
@ -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
|
||||
|
@ -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..</>
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 = [];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
Loading…
x
Reference in New Issue
Block a user