feat: multi-account system

This commit is contained in:
2023-04-14 12:33:19 +01:00
parent 723589fec7
commit fe788853c9
58 changed files with 966 additions and 1080 deletions

View File

@ -1,19 +1,17 @@
import "./ChatPage.css";
import { KeyboardEvent, useEffect, useMemo, useRef, useState } from "react";
import { useSelector } from "react-redux";
import { useParams } from "react-router-dom";
import { FormattedMessage } from "react-intl";
import { RawEvent, TaggedRawEvent } from "@snort/nostr";
import ProfileImage from "Element/ProfileImage";
import { bech32ToHex } from "Util";
import useEventPublisher from "Feed/EventPublisher";
import DM from "Element/DM";
import { RawEvent, TaggedRawEvent } from "@snort/nostr";
import { dmsInChat, isToSelf } from "Pages/MessagesPage";
import NoteToSelf from "Element/NoteToSelf";
import { RootState } from "State/Store";
import { FormattedMessage } from "react-intl";
import { useDmCache } from "Hooks/useDmsCache";
import useLogin from "Hooks/useLogin";
type RouterParams = {
id: string;
@ -23,7 +21,7 @@ export default function ChatPage() {
const params = useParams<RouterParams>();
const publisher = useEventPublisher();
const id = bech32ToHex(params.id ?? "");
const pubKey = useSelector((s: RootState) => s.login.publicKey);
const pubKey = useLogin().publicKey;
const [content, setContent] = useState<string>();
const dmListRef = useRef<HTMLDivElement>(null);
const dms = filterDms(useDmCache());

View File

@ -1,30 +1,27 @@
import { useMemo } from "react";
import { useParams } from "react-router-dom";
import { FormattedMessage } from "react-intl";
import { useSelector, useDispatch } from "react-redux";
import Timeline from "Element/Timeline";
import useEventPublisher from "Feed/EventPublisher";
import { setTags } from "State/Login";
import type { RootState } from "State/Store";
import useLogin from "Hooks/useLogin";
import { setTags } from "Login";
const HashTagsPage = () => {
const params = useParams();
const tag = (params.tag ?? "").toLowerCase();
const dispatch = useDispatch();
const { tags } = useSelector((s: RootState) => s.login);
const login = useLogin();
const isFollowing = useMemo(() => {
return tags.includes(tag);
}, [tags, tag]);
return login.tags.item.includes(tag);
}, [login, tag]);
const publisher = useEventPublisher();
function followTags(ts: string[]) {
dispatch(
setTags({
tags: ts,
createdAt: new Date().getTime(),
})
);
publisher.tags(ts).then(ev => publisher.broadcast(ev));
async function followTags(ts: string[]) {
const ev = await publisher.tags(ts);
if (ev) {
publisher.broadcast(ev);
setTags(login, ts, ev.created_at * 1000);
}
}
return (
@ -33,11 +30,14 @@ const HashTagsPage = () => {
<div className="action-heading">
<h2>#{tag}</h2>
{isFollowing ? (
<button type="button" className="secondary" onClick={() => followTags(tags.filter(t => t !== tag))}>
<button
type="button"
className="secondary"
onClick={() => followTags(login.tags.item.filter(t => t !== tag))}>
<FormattedMessage defaultMessage="Unfollow" />
</button>
) : (
<button type="button" onClick={() => followTags(tags.concat([tag]))}>
<button type="button" onClick={() => followTags(login.tags.item.concat([tag]))}>
<FormattedMessage defaultMessage="Follow" />
</button>
)}

View File

@ -4,13 +4,10 @@ import { useDispatch, useSelector } from "react-redux";
import { Outlet, useLocation, useNavigate } from "react-router-dom";
import { FormattedMessage } from "react-intl";
import { RelaySettings } from "@snort/nostr";
import messages from "./messages";
import { bech32ToHex, randomSample, unixNowMs, unwrap } from "Util";
import Icon from "Icons/Icon";
import { RootState } from "State/Store";
import { init, setRelays } from "State/Login";
import { setShow, reset } from "State/NoteCreator";
import { System } from "System";
import ProfileImage from "Element/ProfileImage";
@ -20,11 +17,11 @@ import useModeration from "Hooks/useModeration";
import { NoteCreator } from "Element/NoteCreator";
import { db } from "Db";
import useEventPublisher from "Feed/EventPublisher";
import { DefaultRelays, SnortPubKey } from "Const";
import SubDebug from "Element/SubDebug";
import { preload } from "Cache";
import { useDmCache } from "Hooks/useDmsCache";
import { mapPlanName } from "./subscribe";
import useLogin from "Hooks/useLogin";
export default function Layout() {
const location = useLocation();
@ -33,9 +30,7 @@ export default function Layout() {
const isReplyNoteCreatorShowing = replyTo && isNoteCreatorShowing;
const dispatch = useDispatch();
const navigate = useNavigate();
const { loggedOut, publicKey, relays, preferences, newUserKey, subscription } = useSelector(
(s: RootState) => s.login
);
const { publicKey, relays, preferences, currentSubscription } = useLogin();
const [pageClass, setPageClass] = useState("page");
const pub = useEventPublisher();
useLoginFeed();
@ -72,11 +67,11 @@ export default function Layout() {
useEffect(() => {
if (relays) {
(async () => {
for (const [k, v] of Object.entries(relays)) {
for (const [k, v] of Object.entries(relays.item)) {
await System.ConnectToRelay(k, v);
}
for (const [k, c] of System.Sockets) {
if (!relays[k] && !c.Ephemeral) {
if (!relays.item[k] && !c.Ephemeral) {
System.DisconnectRelay(k);
}
}
@ -117,7 +112,6 @@ export default function Layout() {
await preload();
}
console.debug(`Using db: ${a ? "IndexedDB" : "In-Memory"}`);
dispatch(init());
try {
if ("registerProtocolHandler" in window.navigator) {
@ -133,53 +127,16 @@ export default function Layout() {
});
}, []);
async function handleNewUser() {
let newRelays: Record<string, RelaySettings> = {};
try {
const rsp = await fetch("https://api.nostr.watch/v1/online");
if (rsp.ok) {
const online: string[] = await rsp.json();
const pickRandom = randomSample(online, 4);
const relayObjects = pickRandom.map(a => [a, { read: true, write: true }]);
newRelays = {
...Object.fromEntries(relayObjects),
...Object.fromEntries(DefaultRelays.entries()),
};
dispatch(
setRelays({
relays: newRelays,
createdAt: unixNowMs(),
})
);
}
} catch (e) {
console.warn(e);
}
const ev = await pub.addFollow([bech32ToHex(SnortPubKey), unwrap(publicKey)], newRelays);
pub.broadcast(ev);
}
useEffect(() => {
if (newUserKey === true) {
handleNewUser().catch(console.warn);
}
}, [newUserKey]);
if (typeof loggedOut !== "boolean") {
return null;
}
return (
<div className={pageClass}>
{!shouldHideHeader && (
<header>
<div className="logo" onClick={() => navigate("/")}>
<h1>Snort</h1>
{subscription && (
{currentSubscription && (
<small className="flex">
<Icon name="diamond" size={10} className="mr5" />
{mapPlanName(subscription.type)}
{mapPlanName(currentSubscription.type)}
</small>
)}
</div>
@ -214,7 +171,7 @@ const AccountHeader = () => {
const navigate = useNavigate();
const { isMuted } = useModeration();
const { publicKey, latestNotification, readNotifications } = useSelector((s: RootState) => s.login);
const { publicKey, latestNotification, readNotifications } = useLogin();
const dms = useDmCache();
const hasNotifications = useMemo(

View File

@ -1,22 +1,23 @@
import "./Login.css";
import { CSSProperties, useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { useNavigate } from "react-router-dom";
import * as secp from "@noble/secp256k1";
import { useIntl, FormattedMessage } from "react-intl";
import { HexKey } from "@snort/nostr";
import { RootState } from "State/Store";
import { setPrivateKey, setPublicKey, setRelays, setGeneratedPrivateKey } from "State/Login";
import { DefaultRelays, EmailRegex, MnemonicRegex } from "Const";
import { EmailRegex, MnemonicRegex } from "Const";
import { bech32ToHex, unwrap } from "Util";
import { generateBip39Entropy, entropyToDerivedKey } from "nip6";
import ZapButton from "Element/ZapButton";
import useImgProxy from "Hooks/useImgProxy";
import Icon from "Icons/Icon";
import useLogin from "Hooks/useLogin";
import { generateNewLogin, LoginStore } from "Login";
import useEventPublisher from "Feed/EventPublisher";
import AsyncButton from "Element/AsyncButton";
import messages from "./messages";
import Icon from "Icons/Icon";
interface ArtworkEntry {
name: string;
@ -24,26 +25,28 @@ interface ArtworkEntry {
link: string;
}
const KarnageKey = bech32ToHex("npub1r0rs5q2gk0e3dk3nlc7gnu378ec6cnlenqp8a3cjhyzu6f8k5sgs4sq9ac");
// todo: fill more
const Artwork: Array<ArtworkEntry> = [
{
name: "",
pubkey: bech32ToHex("npub1r0rs5q2gk0e3dk3nlc7gnu378ec6cnlenqp8a3cjhyzu6f8k5sgs4sq9ac"),
pubkey: KarnageKey,
link: "https://void.cat/d/VKhPayp9ekeXYZGzAL9CxP",
},
{
name: "",
pubkey: bech32ToHex("npub1r0rs5q2gk0e3dk3nlc7gnu378ec6cnlenqp8a3cjhyzu6f8k5sgs4sq9ac"),
pubkey: KarnageKey,
link: "https://void.cat/d/3H2h8xxc3aEN6EVeobd8tw",
},
{
name: "",
pubkey: bech32ToHex("npub1r0rs5q2gk0e3dk3nlc7gnu378ec6cnlenqp8a3cjhyzu6f8k5sgs4sq9ac"),
pubkey: KarnageKey,
link: "https://void.cat/d/7i9W9PXn3TV86C4RUefNC9",
},
{
name: "",
pubkey: bech32ToHex("npub1r0rs5q2gk0e3dk3nlc7gnu378ec6cnlenqp8a3cjhyzu6f8k5sgs4sq9ac"),
pubkey: KarnageKey,
link: "https://void.cat/d/KtoX4ei6RYHY7HESg3Ve3k",
},
];
@ -64,9 +67,9 @@ export async function getNip05PubKey(addr: string): Promise<string> {
}
export default function LoginPage() {
const dispatch = useDispatch();
const navigate = useNavigate();
const publicKey = useSelector<RootState, HexKey | undefined>(s => s.login.publicKey);
const publisher = useEventPublisher();
const login = useLogin();
const [key, setKey] = useState("");
const [error, setError] = useState("");
const [art, setArt] = useState<ArtworkEntry>();
@ -77,10 +80,10 @@ export default function LoginPage() {
const hasSubtleCrypto = window.crypto.subtle !== undefined;
useEffect(() => {
if (publicKey) {
if (login.publicKey) {
navigate("/");
}
}, [publicKey, navigate]);
}, [login, navigate]);
useEffect(() => {
const ret = unwrap(Artwork.at(Artwork.length * Math.random()));
@ -99,28 +102,28 @@ export default function LoginPage() {
}
const hexKey = bech32ToHex(key);
if (secp.utils.isValidPrivateKey(hexKey)) {
dispatch(setPrivateKey(hexKey));
LoginStore.loginWithPrivateKey(hexKey);
} else {
throw new Error("INVALID PRIVATE KEY");
}
} else if (key.startsWith("npub")) {
const hexKey = bech32ToHex(key);
dispatch(setPublicKey(hexKey));
LoginStore.loginWithPubkey(hexKey);
} else if (key.match(EmailRegex)) {
const hexKey = await getNip05PubKey(key);
dispatch(setPublicKey(hexKey));
LoginStore.loginWithPubkey(hexKey);
} else if (key.match(MnemonicRegex)) {
if (!hasSubtleCrypto) {
throw new Error(insecureMsg);
}
const ent = generateBip39Entropy(key);
const keyHex = entropyToDerivedKey(ent);
dispatch(setPrivateKey(keyHex));
LoginStore.loginWithPrivateKey(keyHex);
} else if (secp.utils.isValidPrivateKey(key)) {
if (!hasSubtleCrypto) {
throw new Error(insecureMsg);
}
dispatch(setPrivateKey(key));
LoginStore.loginWithPrivateKey(key);
} else {
throw new Error("INVALID PRIVATE KEY");
}
@ -139,29 +142,14 @@ export default function LoginPage() {
}
async function makeRandomKey() {
const ent = generateBip39Entropy();
const entHex = secp.utils.bytesToHex(ent);
const newKeyHex = entropyToDerivedKey(ent);
dispatch(setGeneratedPrivateKey({ key: newKeyHex, entropy: entHex }));
await generateNewLogin(publisher);
navigate("/new");
}
async function doNip07Login() {
const relays = "getRelays" in window.nostr ? await window.nostr.getRelays() : undefined;
const pubKey = await window.nostr.getPublicKey();
dispatch(setPublicKey(pubKey));
if ("getRelays" in window.nostr) {
const relays = await window.nostr.getRelays();
dispatch(
setRelays({
relays: {
...relays,
...Object.fromEntries(DefaultRelays.entries()),
},
createdAt: 1,
})
);
}
LoginStore.loginWithPubkey(pubKey, relays);
}
function altLogins() {
@ -198,9 +186,9 @@ export default function LoginPage() {
/>
</p>
<div className="login-actions">
<button type="button" onClick={() => makeRandomKey()}>
<AsyncButton onClick={() => makeRandomKey()}>
<FormattedMessage defaultMessage="Generate Key" description="Button: Generate a new key" />
</button>
</AsyncButton>
</div>
</>
);

View File

@ -1,18 +1,16 @@
import { useMemo } from "react";
import { FormattedMessage } from "react-intl";
import { useDispatch, useSelector } from "react-redux";
import { HexKey, RawEvent } from "@snort/nostr";
import UnreadCount from "Element/UnreadCount";
import ProfileImage from "Element/ProfileImage";
import { hexToBech32 } from "../Util";
import { incDmInteraction } from "State/Login";
import { RootState } from "State/Store";
import { hexToBech32 } from "Util";
import NoteToSelf from "Element/NoteToSelf";
import useModeration from "Hooks/useModeration";
import { useDmCache } from "Hooks/useDmsCache";
import useLogin from "Hooks/useLogin";
import messages from "./messages";
import { useDmCache } from "Hooks/useDmsCache";
type DmChat = {
pubkey: HexKey;
@ -21,18 +19,19 @@ type DmChat = {
};
export default function MessagesPage() {
const dispatch = useDispatch();
const myPubKey = useSelector<RootState, HexKey | undefined>(s => s.login.publicKey);
const dmInteraction = useSelector<RootState, number>(s => s.login.dmInteraction);
const login = useLogin();
const { isMuted } = useModeration();
const dms = useDmCache();
const chats = useMemo(() => {
return extractChats(
dms.filter(a => !isMuted(a.pubkey)),
myPubKey ?? ""
);
}, [dms, myPubKey, dmInteraction]);
if (login.publicKey) {
return extractChats(
dms.filter(a => !isMuted(a.pubkey)),
login.publicKey
);
}
return [];
}, [dms, login]);
const unreadCount = useMemo(() => chats.reduce((p, c) => p + c.unreadMessages, 0), [chats]);
@ -50,7 +49,7 @@ export default function MessagesPage() {
}
function person(chat: DmChat) {
if (chat.pubkey === myPubKey) return noteToSelf(chat);
if (chat.pubkey === login.publicKey) return noteToSelf(chat);
return (
<div className="flex mb10" key={chat.pubkey}>
<ProfileImage pubkey={chat.pubkey} className="f-grow" link={`/messages/${hexToBech32("npub", chat.pubkey)}`} />
@ -63,7 +62,6 @@ export default function MessagesPage() {
for (const c of chats) {
setLastReadDm(c.pubkey);
}
dispatch(incDmInteraction());
}
return (
@ -78,7 +76,11 @@ export default function MessagesPage() {
</div>
{chats
.sort((a, b) => {
return a.pubkey === myPubKey ? -1 : b.pubkey === myPubKey ? 1 : b.newestMessage - a.newestMessage;
return a.pubkey === login.publicKey
? -1
: b.pubkey === login.publicKey
? 1
: b.newestMessage - a.newestMessage;
})
.map(person)}
</div>

View File

@ -1,32 +1,25 @@
import { NostrPrefix } from "@snort/nostr";
import { useEffect, useState } from "react";
import { FormattedMessage } from "react-intl";
import { useDispatch } from "react-redux";
import { useNavigate, useParams } from "react-router-dom";
import Spinner from "Icons/Spinner";
import { setRelays } from "State/Login";
import { parseNostrLink, profileLink, unixNowMs, unwrap } from "Util";
import { parseNostrLink, profileLink } from "Util";
import { getNip05PubKey } from "Pages/Login";
import { System } from "System";
export default function NostrLinkHandler() {
const params = useParams();
const [loading, setLoading] = useState(true);
const dispatch = useDispatch();
const navigate = useNavigate();
const [loading, setLoading] = useState(true);
const link = decodeURIComponent(params["*"] ?? "").toLowerCase();
async function handleLink(link: string) {
const nav = parseNostrLink(link);
if (nav) {
if ((nav.relays?.length ?? 0) > 0) {
// todo: add as ephemerial connection
dispatch(
setRelays({
relays: Object.fromEntries(unwrap(nav.relays).map(a => [a, { read: true, write: false }])),
createdAt: unixNowMs(),
})
);
nav.relays?.map(a => System.ConnectEphemeralRelay(a));
}
if (nav.type === NostrPrefix.Event || nav.type === NostrPrefix.Note || nav.type === NostrPrefix.Address) {
navigate(`/e/${nav.encode()}`);

View File

@ -1,17 +1,15 @@
import { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { HexKey } from "@snort/nostr";
import { markNotificationsRead } from "State/Login";
import { RootState } from "State/Store";
import Timeline from "Element/Timeline";
import { TaskList } from "Tasks/TaskList";
import useLogin from "Hooks/useLogin";
import { markNotificationsRead } from "Login";
export default function NotificationsPage() {
const dispatch = useDispatch();
const pubkey = useSelector<RootState, HexKey | undefined>(s => s.login.publicKey);
const login = useLogin();
useEffect(() => {
dispatch(markNotificationsRead());
markNotificationsRead(login);
}, []);
return (
@ -19,12 +17,12 @@ export default function NotificationsPage() {
<div className="main-content">
<TaskList />
</div>
{pubkey && (
{login.publicKey && (
<Timeline
subject={{
type: "ptag",
items: [pubkey],
discriminator: pubkey.slice(0, 12),
items: [login.publicKey],
discriminator: login.publicKey.slice(0, 12),
}}
postsOnly={false}
method={"TIME_RANGE"}

View File

@ -1,7 +1,6 @@
import "./ProfilePage.css";
import { useEffect, useState } from "react";
import { useIntl, FormattedMessage } from "react-intl";
import { useSelector } from "react-redux";
import { useNavigate, useParams } from "react-router-dom";
import { encodeTLV, EventKind, HexKey, NostrPrefix } from "@snort/nostr";
@ -36,7 +35,6 @@ import BlockList from "Element/BlockList";
import MutedList from "Element/MutedList";
import FollowsList from "Element/FollowListBase";
import IconButton from "Element/IconButton";
import { RootState } from "State/Store";
import FollowsYou from "Element/FollowsYou";
import QrCode from "Element/QrCode";
import Modal from "Element/Modal";
@ -46,6 +44,7 @@ import useHorizontalScroll from "Hooks/useHorizontalScroll";
import { EmailRegex } from "Const";
import { getNip05PubKey } from "Pages/Login";
import { LNURL } from "LNURL";
import useLogin from "Hooks/useLogin";
import messages from "./messages";
@ -106,7 +105,7 @@ export default function ProfilePage() {
const navigate = useNavigate();
const [id, setId] = useState<string>();
const user = useUserProfile(id);
const loginPubKey = useSelector((s: RootState) => s.login.publicKey);
const loginPubKey = useLogin().publicKey;
const isMe = loginPubKey === id;
const [showLnQr, setShowLnQr] = useState<boolean>(false);
const [showProfileQr, setShowProfileQr] = useState<boolean>(false);

View File

@ -12,6 +12,7 @@ import { TimelineSubject } from "Feed/TimelineFeed";
import { debounce, getRelayName, sha256, unixNow, unwrap } from "Util";
import messages from "./messages";
import useLogin from "Hooks/useLogin";
interface RelayOption {
url: string;
@ -22,7 +23,7 @@ export default function RootPage() {
const { formatMessage } = useIntl();
const navigate = useNavigate();
const location = useLocation();
const { publicKey: pubKey, tags, preferences } = useSelector((s: RootState) => s.login);
const { publicKey: pubKey, tags, preferences } = useLogin();
const RootTab: Record<string, Tab> = {
Posts: {
@ -65,7 +66,7 @@ export default function RootPage() {
}
}, [location]);
const tagTabs = tags.map((t, idx) => {
const tagTabs = tags.item.map((t, idx) => {
return { text: `#${t}`, value: idx + 3, data: `/tag/${t}` };
});
const tabs = [RootTab.Posts, RootTab.PostsAndReplies, RootTab.Global, ...tagTabs];
@ -81,8 +82,8 @@ export default function RootPage() {
}
const FollowsHint = () => {
const { publicKey: pubKey, follows } = useSelector((s: RootState) => s.login);
if (follows?.length === 0 && pubKey) {
const { publicKey: pubKey, follows } = useLogin();
if (follows.item?.length === 0 && pubKey) {
return (
<FormattedMessage
{...messages.NoFollows}
@ -100,7 +101,7 @@ const FollowsHint = () => {
};
const GlobalTab = () => {
const { relays } = useSelector((s: RootState) => s.login);
const { relays } = useLogin();
const [relay, setRelay] = useState<RelayOption>();
const [allRelays, setAllRelays] = useState<RelayOption[]>();
const [now] = useState(unixNow());
@ -177,8 +178,8 @@ const GlobalTab = () => {
};
const PostsTab = () => {
const follows = useSelector((s: RootState) => s.login.follows);
const subject: TimelineSubject = { type: "pubkey", items: follows, discriminator: "follows" };
const { follows } = useLogin();
const subject: TimelineSubject = { type: "pubkey", items: follows.item, discriminator: "follows" };
return (
<>
@ -189,8 +190,8 @@ const PostsTab = () => {
};
const ConversationsTab = () => {
const { follows } = useSelector((s: RootState) => s.login);
const subject: TimelineSubject = { type: "pubkey", items: follows, discriminator: "follows" };
const { follows } = useLogin();
const subject: TimelineSubject = { type: "pubkey", items: follows.item, discriminator: "follows" };
return <Timeline subject={subject} postsOnly={false} method={"TIME_RANGE"} window={undefined} relay={undefined} />;
};

View File

@ -1,24 +1,25 @@
import { useMemo } from "react";
import { useIntl, FormattedMessage } from "react-intl";
import { useDispatch } from "react-redux";
import { useNavigate, Link } from "react-router-dom";
import { RecommendedFollows } from "Const";
import Logo from "Element/Logo";
import FollowListBase from "Element/FollowListBase";
import { useMemo } from "react";
import { clearEntropy } from "State/Login";
import { clearEntropy } from "Login";
import useLogin from "Hooks/useLogin";
import messages from "./messages";
export default function DiscoverFollows() {
const { formatMessage } = useIntl();
const dispatch = useDispatch();
const login = useLogin();
const navigate = useNavigate();
const sortedReccomends = useMemo(() => {
return RecommendedFollows.sort(() => (Math.random() >= 0.5 ? -1 : 1)).map(a => a.toLowerCase());
}, []);
async function clearEntropyAndGo() {
dispatch(clearEntropy());
clearEntropy(login);
navigate("/");
}

View File

@ -1,20 +1,19 @@
import { useState } from "react";
import { FormattedMessage } from "react-intl";
import { useNavigate } from "react-router-dom";
import { useSelector } from "react-redux";
import Logo from "Element/Logo";
import { services } from "Pages/Verification";
import Nip5Service from "Element/Nip5Service";
import ProfileImage from "Element/ProfileImage";
import type { RootState } from "State/Store";
import { useUserProfile } from "Hooks/useUserProfile";
import useLogin from "Hooks/useLogin";
import messages from "./messages";
export default function GetVerified() {
const navigate = useNavigate();
const { publicKey } = useSelector((s: RootState) => s.login);
const { publicKey } = useLogin();
const user = useUserProfile(publicKey);
const [isVerified, setIsVerified] = useState(false);
const name = user?.name || "nostrich";

View File

@ -1,5 +1,4 @@
import { useMemo, useState } from "react";
import { useSelector } from "react-redux";
import { useIntl, FormattedMessage } from "react-intl";
import { useNavigate } from "react-router-dom";
@ -7,15 +6,15 @@ import { ApiHost } from "Const";
import Logo from "Element/Logo";
import AsyncButton from "Element/AsyncButton";
import FollowListBase from "Element/FollowListBase";
import { RootState } from "State/Store";
import { bech32ToHex } from "Util";
import SnortApi from "SnortApi";
import useLogin from "Hooks/useLogin";
import messages from "./messages";
export default function ImportFollows() {
const navigate = useNavigate();
const currentFollows = useSelector((s: RootState) => s.login.follows);
const currentFollows = useLogin().follows;
const { formatMessage } = useIntl();
const [twitterUsername, setTwitterUsername] = useState<string>("");
const [follows, setFollows] = useState<string[]>([]);
@ -23,7 +22,7 @@ export default function ImportFollows() {
const api = new SnortApi(ApiHost);
const sortedTwitterFollows = useMemo(() => {
return follows.map(a => bech32ToHex(a)).sort(a => (currentFollows.includes(a) ? 1 : -1));
return follows.map(a => bech32ToHex(a)).sort(a => (currentFollows.item.includes(a) ? 1 : -1));
}, [follows, currentFollows]);
async function loadFollows() {

View File

@ -1,13 +1,12 @@
import { useSelector } from "react-redux";
import { FormattedMessage } from "react-intl";
import { useNavigate } from "react-router-dom";
import Logo from "Element/Logo";
import { CollapsedSection } from "Element/Collapsed";
import Copy from "Element/Copy";
import { RootState } from "State/Store";
import { hexToBech32 } from "Util";
import { hexToMnemonic } from "nip6";
import useLogin from "Hooks/useLogin";
import messages from "./messages";
@ -69,7 +68,7 @@ const Extensions = () => {
};
export default function NewUserFlow() {
const { publicKey, privateKey, generatedEntropy } = useSelector((s: RootState) => s.login);
const { publicKey, privateKey, generatedEntropy } = useLogin();
const navigate = useNavigate();
return (

View File

@ -1,22 +1,20 @@
import "./Index.css";
import { FormattedMessage } from "react-intl";
import { useDispatch } from "react-redux";
import { useNavigate } from "react-router-dom";
import Icon from "Icons/Icon";
import { logout } from "State/Login";
import { logout } from "Login";
import useLogin from "Hooks/useLogin";
import { unwrap } from "Util";
import messages from "./messages";
const SettingsIndex = () => {
const dispatch = useDispatch();
const login = useLogin();
const navigate = useNavigate();
function handleLogout() {
dispatch(
logout(() => {
navigate("/");
})
);
logout(unwrap(login.publicKey));
navigate("/");
}
return (

View File

@ -1,20 +1,20 @@
import "./Preferences.css";
import { useDispatch, useSelector } from "react-redux";
import { FormattedMessage, useIntl } from "react-intl";
import { Link } from "react-router-dom";
import emoji from "@jukben/emoji-search";
import { DefaultImgProxy, setPreferences, UserPreferences } from "State/Login";
import { RootState } from "State/Store";
import useLogin from "Hooks/useLogin";
import { updatePreferences, UserPreferences } from "Login";
import { DefaultImgProxy } from "Const";
import { unwrap } from "Util";
import messages from "./messages";
const PreferencesPage = () => {
const dispatch = useDispatch();
const { formatMessage } = useIntl();
const perf = useSelector<RootState, UserPreferences>(s => s.login.preferences);
const login = useLogin();
const perf = login.preferences;
return (
<div className="preferences">
@ -32,12 +32,10 @@ const PreferencesPage = () => {
<select
value={perf.language}
onChange={e =>
dispatch(
setPreferences({
...perf,
language: e.target.value,
} as UserPreferences)
)
updatePreferences(login, {
...perf,
language: e.target.value,
})
}
style={{ textTransform: "capitalize" }}>
{["en", "ja", "es", "hu", "zh-CN", "zh-TW", "fr", "ar", "it", "id", "de", "ru", "sv", "hr"]
@ -62,12 +60,10 @@ const PreferencesPage = () => {
<select
value={perf.theme}
onChange={e =>
dispatch(
setPreferences({
...perf,
theme: e.target.value,
} as UserPreferences)
)
updatePreferences(login, {
...perf,
theme: e.target.value,
} as UserPreferences)
}>
<option value="system">
<FormattedMessage {...messages.System} />
@ -91,12 +87,10 @@ const PreferencesPage = () => {
<select
value={perf.defaultRootTab}
onChange={e =>
dispatch(
setPreferences({
...perf,
defaultRootTab: e.target.value,
} as UserPreferences)
)
updatePreferences(login, {
...perf,
defaultRootTab: e.target.value,
} as UserPreferences)
}>
<option value="posts">
<FormattedMessage {...messages.Posts} />
@ -122,12 +116,10 @@ const PreferencesPage = () => {
<select
value={perf.autoLoadMedia}
onChange={e =>
dispatch(
setPreferences({
...perf,
autoLoadMedia: e.target.value,
} as UserPreferences)
)
updatePreferences(login, {
...perf,
autoLoadMedia: e.target.value,
} as UserPreferences)
}>
<option value="none">
<FormattedMessage {...messages.None} />
@ -153,7 +145,7 @@ const PreferencesPage = () => {
type="number"
defaultValue={perf.defaultZapAmount}
min={1}
onChange={e => dispatch(setPreferences({ ...perf, defaultZapAmount: parseInt(e.target.value || "0") }))}
onChange={e => updatePreferences(login, { ...perf, defaultZapAmount: parseInt(e.target.value || "0") })}
/>
</div>
</div>
@ -189,7 +181,7 @@ const PreferencesPage = () => {
defaultValue={perf.fastZapDonate * 100}
min={0}
max={100}
onChange={e => dispatch(setPreferences({ ...perf, fastZapDonate: parseInt(e.target.value || "0") / 100 }))}
onChange={e => updatePreferences(login, { ...perf, fastZapDonate: parseInt(e.target.value || "0") / 100 })}
/>
</div>
</div>
@ -206,7 +198,7 @@ const PreferencesPage = () => {
<input
type="checkbox"
checked={perf.autoZap}
onChange={e => dispatch(setPreferences({ ...perf, autoZap: e.target.checked }))}
onChange={e => updatePreferences(login, { ...perf, autoZap: e.target.checked })}
/>
</div>
</div>
@ -225,12 +217,10 @@ const PreferencesPage = () => {
type="checkbox"
checked={perf.imgProxyConfig !== null}
onChange={e =>
dispatch(
setPreferences({
...perf,
imgProxyConfig: e.target.checked ? DefaultImgProxy : null,
})
)
updatePreferences(login, {
...perf,
imgProxyConfig: e.target.checked ? DefaultImgProxy : null,
})
}
/>
</div>
@ -250,15 +240,13 @@ const PreferencesPage = () => {
description: "Placeholder text for imgproxy url textbox",
})}
onChange={e =>
dispatch(
setPreferences({
...perf,
imgProxyConfig: {
...unwrap(perf.imgProxyConfig),
url: e.target.value,
},
})
)
updatePreferences(login, {
...perf,
imgProxyConfig: {
...unwrap(perf.imgProxyConfig),
url: e.target.value,
},
})
}
/>
</div>
@ -276,15 +264,13 @@ const PreferencesPage = () => {
description: "Hexidecimal 'key' input for improxy",
})}
onChange={e =>
dispatch(
setPreferences({
...perf,
imgProxyConfig: {
...unwrap(perf.imgProxyConfig),
key: e.target.value,
},
})
)
updatePreferences(login, {
...perf,
imgProxyConfig: {
...unwrap(perf.imgProxyConfig),
key: e.target.value,
},
})
}
/>
</div>
@ -302,15 +288,13 @@ const PreferencesPage = () => {
description: "Hexidecimal 'salt' input for imgproxy",
})}
onChange={e =>
dispatch(
setPreferences({
...perf,
imgProxyConfig: {
...unwrap(perf.imgProxyConfig),
salt: e.target.value,
},
})
)
updatePreferences(login, {
...perf,
imgProxyConfig: {
...unwrap(perf.imgProxyConfig),
salt: e.target.value,
},
})
}
/>
</div>
@ -331,7 +315,7 @@ const PreferencesPage = () => {
<input
type="checkbox"
checked={perf.enableReactions}
onChange={e => dispatch(setPreferences({ ...perf, enableReactions: e.target.checked }))}
onChange={e => updatePreferences(login, { ...perf, enableReactions: e.target.checked })}
/>
</div>
</div>
@ -348,12 +332,10 @@ const PreferencesPage = () => {
className="emoji-selector"
value={perf.reactionEmoji}
onChange={e =>
dispatch(
setPreferences({
...perf,
reactionEmoji: e.target.value,
} as UserPreferences)
)
updatePreferences(login, {
...perf,
reactionEmoji: e.target.value,
})
}>
<option value="+">
+ <FormattedMessage {...messages.Default} />
@ -382,7 +364,7 @@ const PreferencesPage = () => {
<input
type="checkbox"
checked={perf.confirmReposts}
onChange={e => dispatch(setPreferences({ ...perf, confirmReposts: e.target.checked }))}
onChange={e => updatePreferences(login, { ...perf, confirmReposts: e.target.checked })}
/>
</div>
</div>
@ -399,7 +381,7 @@ const PreferencesPage = () => {
<input
type="checkbox"
checked={perf.autoShowLatest}
onChange={e => dispatch(setPreferences({ ...perf, autoShowLatest: e.target.checked }))}
onChange={e => updatePreferences(login, { ...perf, autoShowLatest: e.target.checked })}
/>
</div>
</div>
@ -415,12 +397,10 @@ const PreferencesPage = () => {
<select
value={perf.fileUploader}
onChange={e =>
dispatch(
setPreferences({
...perf,
fileUploader: e.target.value,
} as UserPreferences)
)
updatePreferences(login, {
...perf,
fileUploader: e.target.value,
} as UserPreferences)
}>
<option value="void.cat">
void.cat <FormattedMessage {...messages.Default} />
@ -444,7 +424,7 @@ const PreferencesPage = () => {
<input
type="checkbox"
checked={perf.showDebugMenus}
onChange={e => dispatch(setPreferences({ ...perf, showDebugMenus: e.target.checked }))}
onChange={e => updatePreferences(login, { ...perf, showDebugMenus: e.target.checked })}
/>
</div>
</div>

View File

@ -2,22 +2,21 @@ import "./Profile.css";
import Nostrich from "nostrich.webp";
import { useEffect, useState } from "react";
import { FormattedMessage } from "react-intl";
import { useSelector } from "react-redux";
import { useNavigate } from "react-router-dom";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faShop } from "@fortawesome/free-solid-svg-icons";
import { HexKey, TaggedRawEvent } from "@snort/nostr";
import { TaggedRawEvent } from "@snort/nostr";
import useEventPublisher from "Feed/EventPublisher";
import { useUserProfile } from "Hooks/useUserProfile";
import { hexToBech32, openFile } from "Util";
import Copy from "Element/Copy";
import { RootState } from "State/Store";
import useFileUpload from "Upload";
import messages from "./messages";
import AsyncButton from "Element/AsyncButton";
import { mapEventToProfile, UserCache } from "Cache";
import useLogin from "Hooks/useLogin";
import messages from "./messages";
export interface ProfileSettingsProps {
avatar?: boolean;
@ -27,8 +26,7 @@ export interface ProfileSettingsProps {
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);
const { publicKey: id, privateKey: privKey } = useLogin();
const user = useUserProfile(id ?? "");
const publisher = useEventPublisher();
const uploader = useFileUpload();

View File

@ -1,18 +1,18 @@
import { FormattedMessage } from "react-intl";
import ProfilePreview from "Element/ProfilePreview";
import useRelayState from "Feed/RelayState";
import { useDispatch } from "react-redux";
import { useNavigate, useParams } from "react-router-dom";
import { removeRelay } from "State/Login";
import { parseId, unwrap } from "Util";
import { System } from "System";
import { removeRelay } from "Login";
import useLogin from "Hooks/useLogin";
import messages from "./messages";
const RelayInfo = () => {
const params = useParams();
const navigate = useNavigate();
const dispatch = useDispatch();
const login = useLogin();
const conn = Array.from(System.Sockets.values()).find(a => a.Id === params.id);
const stats = useRelayState(conn?.Address ?? "");
@ -105,7 +105,7 @@ const RelayInfo = () => {
<div
className="btn error"
onClick={() => {
dispatch(removeRelay(unwrap(conn).Address));
removeRelay(login, unwrap(conn).Address);
navigate("/settings/relays");
}}>
<FormattedMessage {...messages.Remove} />

View File

@ -1,24 +1,23 @@
import { useMemo, useState } from "react";
import { FormattedMessage } from "react-intl";
import { useDispatch, useSelector } from "react-redux";
import { randomSample } from "Util";
import { randomSample, unixNowMs } from "Util";
import Relay from "Element/Relay";
import useEventPublisher from "Feed/EventPublisher";
import { RootState } from "State/Store";
import { setRelays } from "State/Login";
import { System } from "System";
import messages from "./messages";
import useLogin from "Hooks/useLogin";
import { setRelays } from "Login";
const RelaySettingsPage = () => {
const dispatch = useDispatch();
const publisher = useEventPublisher();
const relays = useSelector((s: RootState) => s.login.relays);
const login = useLogin();
const relays = login.relays;
const [newRelay, setNewRelay] = useState<string>();
const otherConnections = useMemo(() => {
return [...System.Sockets.keys()].filter(a => relays[a] === undefined);
return [...System.Sockets.keys()].filter(a => relays.item[a] === undefined);
}, [relays]);
async function saveRelays() {
@ -69,13 +68,10 @@ const RelaySettingsPage = () => {
if ((newRelay?.length ?? 0) > 0) {
const parsed = new URL(newRelay ?? "");
const payload = {
relays: {
...relays,
[parsed.toString()]: { read: false, write: false },
},
createdAt: Math.floor(new Date().getTime() / 1000),
...relays.item,
[parsed.toString()]: { read: true, write: true },
};
dispatch(setRelays(payload));
setRelays(login, payload, unixNowMs());
}
}
@ -85,7 +81,7 @@ const RelaySettingsPage = () => {
<FormattedMessage {...messages.Relays} />
</h3>
<div className="flex f-col mb10">
{Object.keys(relays || {}).map(a => (
{Object.keys(relays.item || {}).map(a => (
<Relay addr={a} key={a} />
))}
</div>