feat: account switching
This commit is contained in:
@ -2,13 +2,13 @@ import "./ChatPage.css";
|
||||
import { KeyboardEvent, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { RawEvent, TaggedRawEvent } from "@snort/nostr";
|
||||
import { TaggedRawEvent } from "@snort/nostr";
|
||||
|
||||
import ProfileImage from "Element/ProfileImage";
|
||||
import { bech32ToHex } from "Util";
|
||||
import useEventPublisher from "Feed/EventPublisher";
|
||||
import DM from "Element/DM";
|
||||
import { dmsInChat, isToSelf } from "Pages/MessagesPage";
|
||||
import { dmsForLogin, dmsInChat, isToSelf } from "Pages/MessagesPage";
|
||||
import NoteToSelf from "Element/NoteToSelf";
|
||||
import { useDmCache } from "Hooks/useDmsCache";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
@ -24,15 +24,17 @@ export default function ChatPage() {
|
||||
const pubKey = useLogin().publicKey;
|
||||
const [content, setContent] = useState<string>();
|
||||
const dmListRef = useRef<HTMLDivElement>(null);
|
||||
const dms = filterDms(useDmCache());
|
||||
|
||||
function filterDms(dms: readonly RawEvent[]) {
|
||||
return dmsInChat(id === pubKey ? dms.filter(d => isToSelf(d, pubKey)) : dms, id);
|
||||
}
|
||||
const dms = useDmCache();
|
||||
|
||||
const sortedDms = useMemo(() => {
|
||||
return [...dms].sort((a, b) => a.created_at - b.created_at);
|
||||
}, [dms]);
|
||||
if (pubKey) {
|
||||
const myDms = dmsForLogin(dms, pubKey);
|
||||
// filter dms in this chat, or dms to self
|
||||
const thisDms = id === pubKey ? myDms.filter(d => isToSelf(d, pubKey)) : myDms;
|
||||
return [...dmsInChat(thisDms, id)].sort((a, b) => a.created_at - b.created_at);
|
||||
}
|
||||
return [];
|
||||
}, [dms, pubKey]);
|
||||
|
||||
useEffect(() => {
|
||||
if (dmListRef.current) {
|
||||
|
@ -24,6 +24,7 @@ import useLogin from "Hooks/useLogin";
|
||||
import Avatar from "Element/Avatar";
|
||||
import { useUserProfile } from "Hooks/useUserProfile";
|
||||
import { profileLink } from "Util";
|
||||
import { getCurrentSubscription } from "Subscription";
|
||||
|
||||
export default function Layout() {
|
||||
const location = useLocation();
|
||||
@ -32,7 +33,8 @@ export default function Layout() {
|
||||
const isReplyNoteCreatorShowing = replyTo && isNoteCreatorShowing;
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
const { publicKey, relays, preferences, currentSubscription } = useLogin();
|
||||
const { publicKey, relays, preferences, subscriptions } = useLogin();
|
||||
const currentSubscription = getCurrentSubscription(subscriptions);
|
||||
const [pageClass, setPageClass] = useState("page");
|
||||
const pub = useEventPublisher();
|
||||
useLoginFeed();
|
||||
|
@ -2,21 +2,17 @@ import "./Login.css";
|
||||
|
||||
import { CSSProperties, useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import * as secp from "@noble/secp256k1";
|
||||
import { useIntl, FormattedMessage } from "react-intl";
|
||||
import { HexKey } from "@snort/nostr";
|
||||
|
||||
import { EmailRegex, MnemonicRegex } from "Const";
|
||||
import { bech32ToHex, unwrap } from "Util";
|
||||
import { generateBip39Entropy, entropyToPrivateKey } 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 AsyncButton from "Element/AsyncButton";
|
||||
|
||||
import messages from "./messages";
|
||||
import useLoginHandler from "Hooks/useLoginHandler";
|
||||
|
||||
interface ArtworkEntry {
|
||||
name: string;
|
||||
@ -74,6 +70,7 @@ export default function LoginPage() {
|
||||
const [isMasking, setMasking] = useState(true);
|
||||
const { formatMessage } = useIntl();
|
||||
const { proxy } = useImgProxy();
|
||||
const loginHandler = useLoginHandler();
|
||||
const hasNip7 = "nostr" in window;
|
||||
const hasSubtleCrypto = window.crypto.subtle !== undefined;
|
||||
|
||||
@ -90,42 +87,8 @@ export default function LoginPage() {
|
||||
}, []);
|
||||
|
||||
async function doLogin() {
|
||||
const insecureMsg = formatMessage({
|
||||
defaultMessage:
|
||||
"Can't login with private key on an insecure connection, please use a Nostr key manager extension instead",
|
||||
});
|
||||
try {
|
||||
if (key.startsWith("nsec")) {
|
||||
if (!hasSubtleCrypto) {
|
||||
throw new Error(insecureMsg);
|
||||
}
|
||||
const hexKey = bech32ToHex(key);
|
||||
if (secp.utils.isValidPrivateKey(hexKey)) {
|
||||
LoginStore.loginWithPrivateKey(hexKey);
|
||||
} else {
|
||||
throw new Error("INVALID PRIVATE KEY");
|
||||
}
|
||||
} else if (key.startsWith("npub")) {
|
||||
const hexKey = bech32ToHex(key);
|
||||
LoginStore.loginWithPubkey(hexKey);
|
||||
} else if (key.match(EmailRegex)) {
|
||||
const hexKey = await getNip05PubKey(key);
|
||||
LoginStore.loginWithPubkey(hexKey);
|
||||
} else if (key.match(MnemonicRegex)) {
|
||||
if (!hasSubtleCrypto) {
|
||||
throw new Error(insecureMsg);
|
||||
}
|
||||
const ent = generateBip39Entropy(key);
|
||||
const keyHex = entropyToPrivateKey(ent);
|
||||
LoginStore.loginWithPrivateKey(keyHex);
|
||||
} else if (secp.utils.isValidPrivateKey(key)) {
|
||||
if (!hasSubtleCrypto) {
|
||||
throw new Error(insecureMsg);
|
||||
}
|
||||
LoginStore.loginWithPrivateKey(key);
|
||||
} else {
|
||||
throw new Error("INVALID PRIVATE KEY");
|
||||
}
|
||||
await loginHandler.doLogin(key);
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
setError(e.message);
|
||||
@ -242,7 +205,9 @@ export default function LoginPage() {
|
||||
<input
|
||||
dir="auto"
|
||||
type={isMasking ? "password" : "text"}
|
||||
placeholder={formatMessage(messages.KeyPlaceholder)}
|
||||
placeholder={formatMessage({
|
||||
defaultMessage: "nsec, npub, nip-05, hex, mnemonic",
|
||||
})}
|
||||
className="f-grow"
|
||||
onChange={e => setKey(e.target.value)}
|
||||
/>
|
||||
|
@ -4,7 +4,7 @@ import { HexKey, RawEvent } from "@snort/nostr";
|
||||
|
||||
import UnreadCount from "Element/UnreadCount";
|
||||
import ProfileImage from "Element/ProfileImage";
|
||||
import { hexToBech32 } from "Util";
|
||||
import { dedupe, hexToBech32, unwrap } from "Util";
|
||||
import NoteToSelf from "Element/NoteToSelf";
|
||||
import useModeration from "Hooks/useModeration";
|
||||
import { useDmCache } from "Hooks/useDmsCache";
|
||||
@ -31,7 +31,7 @@ export default function MessagesPage() {
|
||||
);
|
||||
}
|
||||
return [];
|
||||
}, [dms, login]);
|
||||
}, [dms, login.publicKey]);
|
||||
|
||||
const unreadCount = useMemo(() => chats.reduce((p, c) => p + c.unreadMessages, 0), [chats]);
|
||||
|
||||
@ -105,7 +105,7 @@ export function setLastReadDm(pk: HexKey) {
|
||||
|
||||
export function dmTo(e: RawEvent) {
|
||||
const firstP = e.tags.find(b => b[0] === "p");
|
||||
return firstP ? firstP[1] : "";
|
||||
return unwrap(firstP?.[1]);
|
||||
}
|
||||
|
||||
export function isToSelf(e: Readonly<RawEvent>, pk: HexKey) {
|
||||
@ -137,14 +137,19 @@ function newestMessage(dms: RawEvent[], myPubKey: HexKey, pk: HexKey) {
|
||||
return dmsInChat(dms, pk).reduce((acc, v) => (acc = v.created_at > acc ? v.created_at : acc), 0);
|
||||
}
|
||||
|
||||
export function dmsForLogin(dms: readonly RawEvent[], myPubKey: HexKey) {
|
||||
return dms.filter(a => a.pubkey === myPubKey || (a.pubkey !== myPubKey && dmTo(a) === myPubKey));
|
||||
}
|
||||
|
||||
export function extractChats(dms: RawEvent[], myPubKey: HexKey) {
|
||||
const keys = dms.map(a => [a.pubkey, dmTo(a)]).flat();
|
||||
const filteredKeys = Array.from(new Set<string>(keys));
|
||||
const myDms = dmsForLogin(dms, myPubKey);
|
||||
const keys = myDms.map(a => [a.pubkey, dmTo(a)]).flat();
|
||||
const filteredKeys = dedupe(keys);
|
||||
return filteredKeys.map(a => {
|
||||
return {
|
||||
pubkey: a,
|
||||
unreadMessages: unreadDms(dms, myPubKey, a),
|
||||
newestMessage: newestMessage(dms, myPubKey, a),
|
||||
unreadMessages: unreadDms(myDms, myPubKey, a),
|
||||
newestMessage: newestMessage(myDms, myPubKey, a),
|
||||
} as DmChat;
|
||||
});
|
||||
}
|
||||
|
@ -185,8 +185,12 @@ const GlobalTab = () => {
|
||||
};
|
||||
|
||||
const PostsTab = () => {
|
||||
const { follows } = useLogin();
|
||||
const subject: TimelineSubject = { type: "pubkey", items: follows.item, discriminator: "follows" };
|
||||
const { follows, publicKey } = useLogin();
|
||||
const subject: TimelineSubject = {
|
||||
type: "pubkey",
|
||||
items: follows.item,
|
||||
discriminator: `follows:${publicKey?.slice(0, 12)}`,
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -197,8 +201,12 @@ const PostsTab = () => {
|
||||
};
|
||||
|
||||
const ConversationsTab = () => {
|
||||
const { follows } = useLogin();
|
||||
const subject: TimelineSubject = { type: "pubkey", items: follows.item, discriminator: "follows" };
|
||||
const { follows, publicKey } = useLogin();
|
||||
const subject: TimelineSubject = {
|
||||
type: "pubkey",
|
||||
items: follows.item,
|
||||
discriminator: `follows:${publicKey?.slice(0, 12)}`,
|
||||
};
|
||||
|
||||
return <Timeline subject={subject} postsOnly={false} method={"TIME_RANGE"} window={undefined} relay={undefined} />;
|
||||
};
|
||||
|
@ -5,6 +5,7 @@ import Profile from "Pages/settings/Profile";
|
||||
import Relay from "Pages/settings/Relays";
|
||||
import Preferences from "Pages/settings/Preferences";
|
||||
import RelayInfo from "Pages/settings/RelayInfo";
|
||||
import AccountsPage from "Pages/settings/Accounts";
|
||||
import { WalletSettingsRoutes } from "Pages/settings/WalletSettings";
|
||||
import { ManageHandleRoutes } from "Pages/settings/handle";
|
||||
|
||||
@ -44,6 +45,10 @@ export const SettingsRoutes: RouteObject[] = [
|
||||
path: "preferences",
|
||||
element: <Preferences />,
|
||||
},
|
||||
{
|
||||
path: "accounts",
|
||||
element: <AccountsPage />,
|
||||
},
|
||||
...ManageHandleRoutes,
|
||||
...WalletSettingsRoutes,
|
||||
];
|
||||
|
@ -46,5 +46,4 @@ export default defineMessages({
|
||||
},
|
||||
Bookmarks: { defaultMessage: "Bookmarks" },
|
||||
BookmarksCount: { defaultMessage: "{n} Bookmarks" },
|
||||
KeyPlaceholder: { defaultMessage: "nsec, npub, nip-05, hex, mnemonic" },
|
||||
});
|
||||
|
87
packages/app/src/Pages/settings/Accounts.tsx
Normal file
87
packages/app/src/Pages/settings/Accounts.tsx
Normal file
@ -0,0 +1,87 @@
|
||||
import { useState } from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
|
||||
import ProfilePreview from "Element/ProfilePreview";
|
||||
import { LoginStore } from "Login";
|
||||
import useLoginHandler from "Hooks/useLoginHandler";
|
||||
import AsyncButton from "Element/AsyncButton";
|
||||
import { getActiveSubscriptions } from "Subscription";
|
||||
|
||||
export default function AccountsPage() {
|
||||
const { formatMessage } = useIntl();
|
||||
const [key, setKey] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
const loginHandler = useLoginHandler();
|
||||
const logins = LoginStore.getSessions();
|
||||
const sub = getActiveSubscriptions(LoginStore.allSubscriptions());
|
||||
|
||||
async function doLogin() {
|
||||
try {
|
||||
setError("");
|
||||
await loginHandler.doLogin(key);
|
||||
setKey("");
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
setError(e.message);
|
||||
} else {
|
||||
setError(
|
||||
formatMessage({
|
||||
defaultMessage: "Unknown login error",
|
||||
})
|
||||
);
|
||||
}
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<h3>
|
||||
<FormattedMessage defaultMessage="Logins" />
|
||||
</h3>
|
||||
{logins.map(a => (
|
||||
<div className="card flex" key={a}>
|
||||
<ProfilePreview
|
||||
pubkey={a}
|
||||
options={{
|
||||
about: false,
|
||||
}}
|
||||
actions={
|
||||
<div className="f-1">
|
||||
<button className="mb10" onClick={() => LoginStore.switchAccount(a)}>
|
||||
<FormattedMessage defaultMessage="Switch" />
|
||||
</button>
|
||||
<button onClick={() => LoginStore.removeSession(a)}>
|
||||
<FormattedMessage defaultMessage="Logout" />
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{sub && (
|
||||
<>
|
||||
<h3>
|
||||
<FormattedMessage defaultMessage="Add Account" />
|
||||
</h3>
|
||||
<div className="flex">
|
||||
<input
|
||||
dir="auto"
|
||||
type="text"
|
||||
placeholder={formatMessage({
|
||||
defaultMessage: "nsec, npub, nip-05, hex, mnemonic",
|
||||
})}
|
||||
className="f-grow mr10"
|
||||
onChange={e => setKey(e.target.value)}
|
||||
/>
|
||||
<AsyncButton onClick={() => doLogin()}>
|
||||
<FormattedMessage defaultMessage="Login" />
|
||||
</AsyncButton>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{error && <b className="error">{error}</b>}
|
||||
</>
|
||||
);
|
||||
}
|
@ -2,15 +2,17 @@ import "./Index.css";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import Icon from "Icons/Icon";
|
||||
import { logout } from "Login";
|
||||
import { LoginStore, logout } from "Login";
|
||||
import useLogin from "Hooks/useLogin";
|
||||
import { unwrap } from "Util";
|
||||
import { getCurrentSubscription } from "Subscription";
|
||||
|
||||
import messages from "./messages";
|
||||
|
||||
const SettingsIndex = () => {
|
||||
const login = useLogin();
|
||||
const navigate = useNavigate();
|
||||
const sub = getCurrentSubscription(LoginStore.allSubscriptions());
|
||||
|
||||
function handleLogout() {
|
||||
logout(unwrap(login.publicKey));
|
||||
@ -55,6 +57,13 @@ const SettingsIndex = () => {
|
||||
<FormattedMessage defaultMessage="Snort Subscription" />
|
||||
<Icon name="arrowFront" />
|
||||
</div>
|
||||
{sub && (
|
||||
<div className="settings-row" onClick={() => navigate("accounts")}>
|
||||
<Icon name="code-circle" />
|
||||
<FormattedMessage defaultMessage="Accounts" />
|
||||
<Icon name="arrowFront" />
|
||||
</div>
|
||||
)}
|
||||
<div className="settings-row" onClick={handleLogout}>
|
||||
<Icon name="logout" />
|
||||
<FormattedMessage {...messages.LogOut} />
|
||||
|
Reference in New Issue
Block a user