feat: account switching
This commit is contained in:
@ -154,7 +154,7 @@ export default abstract class FeedCache<TCached> {
|
||||
|
||||
protected notifyChange(keys: Array<string>) {
|
||||
this.#changed = true;
|
||||
this.#hooks.filter(a => keys.includes(a.key)).forEach(h => h.fn());
|
||||
this.#hooks.filter(a => keys.includes(a.key) || a.key === "*").forEach(h => h.fn());
|
||||
}
|
||||
|
||||
abstract key(of: TCached): string;
|
||||
|
@ -31,6 +31,9 @@ import messages from "./messages";
|
||||
import { ClipboardEventHandler, useState } from "react";
|
||||
import Spinner from "Icons/Spinner";
|
||||
import { EventBuilder } from "System";
|
||||
import { Menu, MenuItem } from "@szhsin/react-menu";
|
||||
import { LoginStore } from "Login";
|
||||
import { getCurrentSubscription } from "Subscription";
|
||||
|
||||
interface NotePreviewProps {
|
||||
note: TaggedRawEvent;
|
||||
@ -56,6 +59,7 @@ export function NoteCreator() {
|
||||
useSelector((s: RootState) => s.noteCreator);
|
||||
const [uploadInProgress, setUploadInProgress] = useState(false);
|
||||
const dispatch = useDispatch();
|
||||
const sub = getCurrentSubscription(LoginStore.allSubscriptions());
|
||||
|
||||
async function sendNote() {
|
||||
if (note && publisher) {
|
||||
@ -229,6 +233,18 @@ export function NoteCreator() {
|
||||
}
|
||||
}
|
||||
|
||||
function listAccounts() {
|
||||
return LoginStore.getSessions().map(a => (
|
||||
<MenuItem
|
||||
onClick={ev => {
|
||||
ev.stopPropagation = true;
|
||||
LoginStore.switchAccount(a);
|
||||
}}>
|
||||
<ProfileImage pubkey={a} linkToProfile={false} />
|
||||
</MenuItem>
|
||||
));
|
||||
}
|
||||
|
||||
const handlePaste: ClipboardEventHandler<HTMLDivElement> = evt => {
|
||||
if (evt.clipboardData) {
|
||||
const clipboardItems = evt.clipboardData.items;
|
||||
@ -273,12 +289,23 @@ export function NoteCreator() {
|
||||
/>
|
||||
{renderPollOptions()}
|
||||
<div className="insert">
|
||||
{sub && (
|
||||
<Menu
|
||||
menuButton={
|
||||
<button>
|
||||
<Icon name="code-circle" />
|
||||
</button>
|
||||
}
|
||||
menuClassName="ctx-menu">
|
||||
{listAccounts()}
|
||||
</Menu>
|
||||
)}
|
||||
{pollOptions === undefined && !replyTo && (
|
||||
<button type="button" onClick={() => dispatch(setPollOptions(["A", "B"]))}>
|
||||
<button onClick={() => dispatch(setPollOptions(["A", "B"]))}>
|
||||
<Icon name="pie-chart" />
|
||||
</button>
|
||||
)}
|
||||
<button type="button" onClick={attachFile}>
|
||||
<button onClick={attachFile}>
|
||||
<Icon name="attachment" />
|
||||
</button>
|
||||
</div>
|
||||
@ -288,19 +315,19 @@ export function NoteCreator() {
|
||||
)}
|
||||
<div className="note-creator-actions">
|
||||
{uploadInProgress && <Spinner />}
|
||||
<button className="secondary" type="button" onClick={() => dispatch(setShowAdvanced(!showAdvanced))}>
|
||||
<button className="secondary" onClick={() => dispatch(setShowAdvanced(!showAdvanced))}>
|
||||
<FormattedMessage defaultMessage="Advanced" />
|
||||
</button>
|
||||
<button className="secondary" type="button" onClick={cancel}>
|
||||
<button className="secondary" onClick={cancel}>
|
||||
<FormattedMessage {...messages.Cancel} />
|
||||
</button>
|
||||
<button type="button" onClick={onSubmit}>
|
||||
<button onClick={onSubmit}>
|
||||
{replyTo ? <FormattedMessage {...messages.Reply} /> : <FormattedMessage {...messages.Send} />}
|
||||
</button>
|
||||
</div>
|
||||
{showAdvanced && (
|
||||
<div>
|
||||
<button className="secondary" type="button" onClick={loadPreview}>
|
||||
<button className="secondary" onClick={loadPreview}>
|
||||
<FormattedMessage defaultMessage="Toggle Preview" />
|
||||
</button>
|
||||
<h4>
|
||||
|
@ -1,12 +1,13 @@
|
||||
import "./ProfileImage.css";
|
||||
|
||||
import { useMemo } from "react";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { HexKey, NostrPrefix } from "@snort/nostr";
|
||||
|
||||
import { useUserProfile } from "Hooks/useUserProfile";
|
||||
import { hexToBech32, profileLink } from "Util";
|
||||
import Avatar from "Element/Avatar";
|
||||
import Nip05 from "Element/Nip05";
|
||||
import { HexKey, NostrPrefix } from "@snort/nostr";
|
||||
import { MetadataCache } from "Cache";
|
||||
import usePageWidth from "Hooks/usePageWidth";
|
||||
|
||||
@ -55,17 +56,17 @@ export default function ProfileImage({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`pfp f-ellipsis${className ? ` ${className}` : ""}`}>
|
||||
<div className={`pfp f-ellipsis${className ? ` ${className}` : ""}`} onClick={onAvatarClick}>
|
||||
<div className="avatar-wrapper">
|
||||
<Avatar user={user} onClick={onAvatarClick} />
|
||||
<Avatar user={user} />
|
||||
</div>
|
||||
{showUsername && (
|
||||
<div className="profile-name">
|
||||
<div className="username">
|
||||
<Link className="display-name" key={pubkey} to={link ?? profileLink(pubkey)}>
|
||||
<div className="display-name">
|
||||
<div>{name.trim()}</div>
|
||||
{nip05 && <Nip05 nip05={nip05} pubkey={pubkey} verifyNip={verifyNip} />}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div className="subheader" style={{ width: autoWidth ? width - 190 : "" }}>
|
||||
{subHeader}
|
||||
|
@ -26,7 +26,7 @@ export default function useLoginFeed() {
|
||||
const subLogin = useMemo(() => {
|
||||
if (!pubKey) return null;
|
||||
|
||||
const b = new RequestBuilder("login");
|
||||
const b = new RequestBuilder(`login:${pubKey.slice(0, 12)}`);
|
||||
b.withOptions({
|
||||
leaveOpen: true,
|
||||
});
|
||||
@ -46,7 +46,7 @@ export default function useLoginFeed() {
|
||||
|
||||
const subLists = useMemo(() => {
|
||||
if (!pubKey) return null;
|
||||
const b = new RequestBuilder("login:lists");
|
||||
const b = new RequestBuilder(`login:${pubKey.slice(0, 12)}:lists`);
|
||||
b.withOptions({
|
||||
leaveOpen: true,
|
||||
});
|
||||
|
@ -3,7 +3,7 @@ import { useSyncExternalStore } from "react";
|
||||
|
||||
export function useDmCache() {
|
||||
return useSyncExternalStore(
|
||||
c => DmCache.hook(c, undefined),
|
||||
c => DmCache.hook(c, "*"),
|
||||
() => DmCache.snapshot()
|
||||
);
|
||||
}
|
||||
|
55
packages/app/src/Hooks/useLoginHandler.tsx
Normal file
55
packages/app/src/Hooks/useLoginHandler.tsx
Normal file
@ -0,0 +1,55 @@
|
||||
import { useIntl } from "react-intl";
|
||||
import * as secp from "@noble/secp256k1";
|
||||
|
||||
import { EmailRegex, MnemonicRegex } from "Const";
|
||||
import { LoginStore } from "Login";
|
||||
import { generateBip39Entropy, entropyToPrivateKey } from "nip6";
|
||||
import { getNip05PubKey } from "Pages/Login";
|
||||
import { bech32ToHex } from "Util";
|
||||
|
||||
export default function useLoginHandler() {
|
||||
const { formatMessage } = useIntl();
|
||||
const hasSubtleCrypto = window.crypto.subtle !== undefined;
|
||||
|
||||
async function doLogin(key: string) {
|
||||
const insecureMsg = formatMessage({
|
||||
defaultMessage:
|
||||
"Can't login with private key on an insecure connection, please use a Nostr key manager extension instead",
|
||||
});
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
doLogin,
|
||||
};
|
||||
}
|
@ -5,7 +5,7 @@ import { DefaultRelays, SnortPubKey } from "Const";
|
||||
import { LoginStore, UserPreferences, LoginSession } from "Login";
|
||||
import { generateBip39Entropy, entropyToPrivateKey } from "nip6";
|
||||
import { bech32ToHex, dedupeById, randomSample, sanitizeRelayUrl, unixNowMs, unwrap } from "Util";
|
||||
import { getCurrentSubscription, SubscriptionEvent } from "Subscription";
|
||||
import { SubscriptionEvent } from "Subscription";
|
||||
import { EventPublisher } from "System/EventPublisher";
|
||||
|
||||
export function setRelays(state: LoginSession, relays: Record<string, RelaySettings>, createdAt: number) {
|
||||
@ -150,7 +150,7 @@ export function setBookmarked(state: LoginSession, bookmarked: Array<string>, ts
|
||||
export function addSubscription(state: LoginSession, ...subs: SubscriptionEvent[]) {
|
||||
const newSubs = dedupeById([...(state.subscriptions || []), ...subs]);
|
||||
if (newSubs.length !== state.subscriptions.length) {
|
||||
state.currentSubscription = getCurrentSubscription(state.subscriptions);
|
||||
state.subscriptions = newSubs;
|
||||
LoginStore.updateSession(state);
|
||||
}
|
||||
}
|
||||
|
@ -80,9 +80,4 @@ export interface LoginSession {
|
||||
* Snort subscriptions licences
|
||||
*/
|
||||
subscriptions: Array<SubscriptionEvent>;
|
||||
|
||||
/**
|
||||
* Current active subscription
|
||||
*/
|
||||
currentSubscription?: SubscriptionEvent;
|
||||
}
|
||||
|
@ -73,6 +73,17 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
|
||||
return [...this.#accounts.keys()];
|
||||
}
|
||||
|
||||
allSubscriptions() {
|
||||
return [...this.#accounts.values()].map(a => a.subscriptions).flat();
|
||||
}
|
||||
|
||||
switchAccount(pk: string) {
|
||||
if (this.#accounts.has(pk)) {
|
||||
this.#activeAccount = pk;
|
||||
this.#save();
|
||||
}
|
||||
}
|
||||
|
||||
loginWithPubkey(key: HexKey, relays?: Record<string, RelaySettings>) {
|
||||
if (this.#accounts.has(key)) {
|
||||
throw new Error("Already logged in with this pubkey");
|
||||
@ -127,6 +138,9 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
|
||||
|
||||
removeSession(k: string) {
|
||||
if (this.#accounts.delete(k)) {
|
||||
if (this.#activeAccount === k) {
|
||||
this.#activeAccount = undefined;
|
||||
}
|
||||
this.#save();
|
||||
}
|
||||
}
|
||||
|
@ -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