mirror of
https://github.com/PrimalHQ/primal-web-app.git
synced 2024-09-30 00:41:09 +00:00
Add Account settings
This commit is contained in:
parent
feff1269c4
commit
f05149cf53
@ -32,6 +32,7 @@ const Mutelist = lazy(() => import('./pages/Mutelist'));
|
||||
const CreateAccount = lazy(() => import('./pages/CreateAccount'));
|
||||
|
||||
const NotifSettings = lazy(() => import('./pages/Settings/Notifications'));
|
||||
const Account = lazy(() => import('./pages/Settings/Account'));
|
||||
const Appearance = lazy(() => import('./pages/Settings/Appearance'));
|
||||
const HomeFeeds = lazy(() => import('./pages/Settings/HomeFeeds'));
|
||||
const ZapSettings = lazy(() => import('./pages/Settings/Zaps'));
|
||||
@ -104,6 +105,7 @@ const Router: Component = () => {
|
||||
<Route path="/download" element={<Navigate href='/downloads' />} />;
|
||||
<Route path="/settings" component={Settings}>
|
||||
<Route path="/" component={Menu} />
|
||||
<Route path="/account" component={Account} />
|
||||
<Route path="/appearance" component={Appearance} />
|
||||
<Route path="/feeds" component={HomeFeeds} />
|
||||
<Route path="/notifications" component={NotifSettings} />
|
||||
|
@ -2,7 +2,8 @@
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
margin: 0px 8px;
|
||||
padding: 0px;
|
||||
padding-inline: 10px;
|
||||
padding-block: 6px;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
font-weight: 700;
|
||||
|
@ -53,6 +53,7 @@ const NavMenu: Component< { id?: string } > = (props) => {
|
||||
label: intl.formatMessage(t.settings),
|
||||
icon: 'settingsIcon',
|
||||
hiddenOnSmallScreens: true,
|
||||
bubble: () => account?.sec ? 1 : 0,
|
||||
},
|
||||
{
|
||||
to: '/help',
|
||||
|
@ -25,7 +25,7 @@ import { sendContacts, sendLike, sendMuteList, triggerImportEvents } from "../li
|
||||
import { generatePrivateKey, Relay, getPublicKey as nostrGetPubkey, nip19 } from "nostr-tools";
|
||||
import { APP_ID } from "../App";
|
||||
import { getLikes, getFilterlists, getProfileContactList, getProfileMuteList, getUserProfiles, sendFilterlists, getAllowlist, sendAllowList } from "../lib/profile";
|
||||
import { getStorage, readSecFromStorage, saveFollowing, saveLikes, saveMuted, saveMuteList, saveRelaySettings, storeSec } from "../lib/localStore";
|
||||
import { clearSec, getStorage, readSecFromStorage, saveFollowing, saveLikes, saveMuted, saveMuteList, saveRelaySettings, storeSec } from "../lib/localStore";
|
||||
import { connectRelays, connectToRelay, getDefaultRelays, getPreConfiguredRelays } from "../lib/relays";
|
||||
import { getPublicKey } from "../lib/nostrAPI";
|
||||
import { generateKeys } from "../lib/PrimalNostr";
|
||||
@ -79,6 +79,7 @@ export type AccountContextStore = {
|
||||
addToAllowlist: (pubkey: string | undefined, then?: () => void) => void,
|
||||
removeFromAllowlist: (pubkey: string | undefined) => void,
|
||||
setSec: (sec: string | undefined) => void,
|
||||
logout: () => void,
|
||||
},
|
||||
}
|
||||
|
||||
@ -120,6 +121,12 @@ export function AccountProvider(props: { children: JSXElement }) {
|
||||
|
||||
let connectedRelaysCopy: Relay[] = [];
|
||||
|
||||
const logout = () => {
|
||||
updateStore('sec', () => undefined);
|
||||
updateStore('publicKey', () => undefined);
|
||||
clearSec();
|
||||
};
|
||||
|
||||
const setSec = (sec: string | undefined) => {
|
||||
const decoded = nip19.decode(sec);
|
||||
|
||||
@ -1215,6 +1222,7 @@ const [store, updateStore] = createStore<AccountContextStore>({
|
||||
addToAllowlist,
|
||||
removeFromAllowlist,
|
||||
setSec,
|
||||
logout,
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { createStore } from "solid-js/store";
|
||||
import { createStore, reconcile } from "solid-js/store";
|
||||
import { Kind } from "../constants";
|
||||
import {
|
||||
createContext,
|
||||
@ -31,7 +31,7 @@ import {
|
||||
UserRelation,
|
||||
} from "../types/primal";
|
||||
import { APP_ID } from "../App";
|
||||
import { getMessageCounts, getNewMessages, getOldMessages, markAllAsRead, resetMessageCount, subscribeToMessagesStats } from "../lib/messages";
|
||||
import { getMessageCounts, getNewMessages, getOldMessages, markAllAsRead, resetMessageCount, subscribeToMessagesStats, unsubscribeToMessagesStats } from "../lib/messages";
|
||||
import { useAccountContext } from "./AccountContext";
|
||||
import { convertToUser } from "../stores/profile";
|
||||
import { getUserProfiles } from "../lib/profile";
|
||||
@ -742,6 +742,14 @@ export const MessagesProvider = (props: { children: ContextChildren }) => {
|
||||
}
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
if (!account?.sec) {
|
||||
unsubscribeToMessagesStats(subidMsgCount);
|
||||
updateStore('messageCount', () => 0);
|
||||
updateStore('messageCountPerSender', reconcile({}));
|
||||
}
|
||||
});
|
||||
|
||||
onCleanup(() => {
|
||||
removeSocketListeners(
|
||||
socket(),
|
||||
|
@ -18,7 +18,7 @@ import {
|
||||
NostrEvent,
|
||||
} from "../types/primal";
|
||||
import { APP_ID } from "../App";
|
||||
import { subscribeToNotificationStats } from "../lib/notifications";
|
||||
import { subscribeToNotificationStats, unsubscribeToNotificationStats } from "../lib/notifications";
|
||||
import { useAccountContext } from "./AccountContext";
|
||||
|
||||
export type NotificationsContextStore = {
|
||||
@ -124,7 +124,12 @@ export const NotificationsProvider = (props: { children: ContextChildren }) => {
|
||||
}
|
||||
});
|
||||
|
||||
createEffect(() => {});
|
||||
createEffect(() => {
|
||||
if (!account?.sec) {
|
||||
unsubscribeToNotificationStats(subid);
|
||||
updateStore('notificationCount', () => 0);
|
||||
}
|
||||
});
|
||||
|
||||
onCleanup(() => {
|
||||
removeSocketListeners(
|
||||
|
@ -155,3 +155,7 @@ export const storeSec = (sec: string | undefined) => {
|
||||
localStorage.setItem('primalSec', sec);
|
||||
|
||||
};
|
||||
|
||||
export const clearSec = () => {
|
||||
localStorage.removeItem('primalSec');
|
||||
};
|
||||
|
@ -12,6 +12,14 @@ export const subscribeToMessagesStats = (pubkey: string, subid: string) => {
|
||||
]));
|
||||
}
|
||||
|
||||
export const unsubscribeToMessagesStats = (subid: string) => {
|
||||
sendMessage(JSON.stringify([
|
||||
"CLOSE",
|
||||
subid,
|
||||
{cache: ["directmsg_count"]},
|
||||
]));
|
||||
}
|
||||
|
||||
export const resetMessageCount = async (sender: string, subid: string) => {
|
||||
const event = {
|
||||
content: `{ "description": "reset messages from '${sender}'"}`,
|
||||
|
@ -101,6 +101,14 @@ export const subscribeToNotificationStats = (pubkey: string, subid: string) => {
|
||||
]));
|
||||
}
|
||||
|
||||
export const unsubscribeToNotificationStats = (subid: string) => {
|
||||
sendMessage(JSON.stringify([
|
||||
"CLOSE",
|
||||
subid,
|
||||
{cache: ["notification_counts", { subid }]},
|
||||
]));
|
||||
}
|
||||
|
||||
export const truncateNumber = (amount: number, from?: 1 | 2 | 3 | 4) => {
|
||||
const t = 1_000;
|
||||
const s = from || 1;
|
||||
|
110
src/pages/Settings/Account.tsx
Normal file
110
src/pages/Settings/Account.tsx
Normal file
@ -0,0 +1,110 @@
|
||||
import { Component, createSignal, Show } from 'solid-js';
|
||||
import styles from './Settings.module.scss';
|
||||
|
||||
import ThemeChooser from '../../components/ThemeChooser/ThemeChooser';
|
||||
import { useIntl } from '@cookbook/solid-intl';
|
||||
import { settings as t, actions as tActions } from '../../translations';
|
||||
import PageCaption from '../../components/PageCaption/PageCaption';
|
||||
import { Link } from '@solidjs/router';
|
||||
import PageTitle from '../../components/PageTitle/PageTitle';
|
||||
import { useAccountContext } from '../../contexts/AccountContext';
|
||||
import Avatar from '../../components/Avatar/Avatar';
|
||||
|
||||
const Account: Component = () => {
|
||||
|
||||
const intl = useIntl();
|
||||
const account = useAccountContext();
|
||||
|
||||
const [isCoppied, setIsCoppied] = createSignal('');
|
||||
|
||||
const onCopy = (text: string) => {
|
||||
navigator.clipboard.writeText(text);
|
||||
setIsCoppied(text);
|
||||
|
||||
setTimeout(() => setIsCoppied(''), 2_000);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PageTitle title={`${intl.formatMessage(t.account.title)} ${intl.formatMessage(t.title)}`} />
|
||||
|
||||
<PageCaption>
|
||||
<Link href='/settings' >{intl.formatMessage(t.index.title)}</Link>:
|
||||
<div>{intl.formatMessage(t.account.title)}</div>
|
||||
</PageCaption>
|
||||
|
||||
<div class={styles.securityWarning}>
|
||||
<div class={styles.securityIcon}></div>
|
||||
<div class={styles.securityMessage}>
|
||||
{intl.formatMessage(t.account.description, {
|
||||
link: <a href="https://getalby.com" target='_blank'>Alby</a>,
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class={styles.settingsAccountCaption}>
|
||||
<div class={styles.caption}>
|
||||
{intl.formatMessage(t.account.pubkey)}
|
||||
</div>
|
||||
<button
|
||||
class={styles.copy}
|
||||
onClick={() => onCopy(account?.activeUser?.npub || '')}
|
||||
>
|
||||
<Show when={isCoppied() === account?.activeUser?.npub}>
|
||||
<div class={styles.checkIcon}></div>
|
||||
</Show>
|
||||
{intl.formatMessage(tActions.copyPubkey)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class={styles.settingsKeyArea}>
|
||||
<Avatar
|
||||
src={account?.activeUser?.picture}
|
||||
size="vs"
|
||||
/>
|
||||
<div class={styles.key}>
|
||||
{account?.activeUser?.npub}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class={styles.settingsDescription}>
|
||||
{intl.formatMessage(t.account.pubkeyDesc)}
|
||||
</div>
|
||||
|
||||
|
||||
<div class={styles.settingsAccountCaption}>
|
||||
<div class={styles.caption}>
|
||||
{intl.formatMessage(t.account.privkey)}
|
||||
</div>
|
||||
<button
|
||||
class={styles.copy}
|
||||
onClick={() => onCopy(account?.sec || '')}
|
||||
>
|
||||
<Show when={isCoppied() === account?.sec}>
|
||||
<div class={styles.checkIcon}></div>
|
||||
</Show>
|
||||
{intl.formatMessage(tActions.copyPrivkey)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class={styles.settingsKeyArea}>
|
||||
<div class={styles.icon}>
|
||||
<div class={styles.keyIcon}></div>
|
||||
</div>
|
||||
<input
|
||||
class={styles.key}
|
||||
value={account?.sec || ''}
|
||||
readOnly={true}
|
||||
type="password"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class={styles.settingsDescription}>
|
||||
{intl.formatMessage(t.account.privkeyDesc)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Account;
|
@ -2,10 +2,11 @@ import { Component, Show } from 'solid-js';
|
||||
import styles from './Settings.module.scss';
|
||||
|
||||
import { useIntl } from '@cookbook/solid-intl';
|
||||
import { settings as t } from '../../translations';
|
||||
import { settings as t, actions as tActions } from '../../translations';
|
||||
import PageCaption from '../../components/PageCaption/PageCaption';
|
||||
import { Link } from '@solidjs/router';
|
||||
import { useAccountContext } from '../../contexts/AccountContext';
|
||||
import ButtonPrimary from '../../components/Buttons/ButtonPrimary';
|
||||
|
||||
const Menu: Component = () => {
|
||||
|
||||
@ -19,6 +20,18 @@ const Menu: Component = () => {
|
||||
<PageCaption title={intl.formatMessage(t.title)} />
|
||||
|
||||
<div class={styles.subpageLinks}>
|
||||
<Show when={account?.sec != undefined}>
|
||||
<Link href="/settings/account">
|
||||
<div class={styles.caption}>
|
||||
{intl.formatMessage(t.account.title)}
|
||||
<div class={styles.bubble}>
|
||||
<div>{1}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class={styles.chevron}></div>
|
||||
</Link>
|
||||
</Show>
|
||||
|
||||
<Link href="/settings/appearance">
|
||||
{intl.formatMessage(t.appearance.title)}
|
||||
<div class={styles.chevron}></div>
|
||||
@ -57,6 +70,14 @@ const Menu: Component = () => {
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<Show when={account?.sec}>
|
||||
<div class={styles.webVersion}>
|
||||
<ButtonPrimary onClick={account?.actions.logout}>
|
||||
{intl.formatMessage(tActions.logout)}
|
||||
</ButtonPrimary>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<div class={styles.webVersion}>
|
||||
<div class={styles.title}>version</div>
|
||||
<div class={styles.value}>{version}</div>
|
||||
|
@ -50,6 +50,95 @@
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.settingsDescription {
|
||||
color: var(--text-tertiary);
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 17px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.settingsAccountCaption {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
margin-top: 46px;
|
||||
|
||||
.caption {
|
||||
color: var(--text-primary);
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
line-height: 20px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.copy {
|
||||
color: var(--accent-1);
|
||||
background: none;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
width: fit-content;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
|
||||
&:focus {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.checkIcon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: inline-block;
|
||||
margin-inline: 2px;
|
||||
background-color: var(--success-color);
|
||||
-webkit-mask: url(../../assets/icons/check.svg) no-repeat 0 / 80%;
|
||||
mask: url(../../assets/icons/check.svg) no-repeat 0 / 80%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.settingsKeyArea {
|
||||
background-color: var(--background-input);
|
||||
padding-inline: 12px;
|
||||
padding-block: 10px;
|
||||
border-radius: 12px;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.key {
|
||||
color: var(--text-secondary);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 20px;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
margin-left: 12px;
|
||||
border: none;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin: 7px;
|
||||
}
|
||||
|
||||
.keyIcon {
|
||||
min-width: 28px;
|
||||
height: 28px;
|
||||
display: block;
|
||||
margin: 0px;
|
||||
background-color: var(--text-secondary);
|
||||
-webkit-mask: url(../../assets/icons/key.svg) no-repeat 0 0 / 28px 28px;
|
||||
mask: url(../../assets/icons/key.svg) no-repeat 0 0 / 28px 28px;
|
||||
}
|
||||
}
|
||||
|
||||
.secondCaption {
|
||||
margin-top: 24px;
|
||||
}
|
||||
@ -108,7 +197,31 @@
|
||||
line-height: 28px;
|
||||
text-decoration: none;
|
||||
|
||||
>div {
|
||||
.caption {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
|
||||
.bubble {
|
||||
text-align: center;
|
||||
padding-top: 2px;
|
||||
padding-inline: 4px;
|
||||
margin-inline: 8px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 8px;
|
||||
font-weight: 500;
|
||||
font-size: 12px;
|
||||
line-height: 12px;
|
||||
|
||||
background: var(--brand-gradient);
|
||||
border: 1px solid var(--background-site);
|
||||
|
||||
color: white;
|
||||
text-shadow: 0.5px 0.5px 0px black;
|
||||
}
|
||||
}
|
||||
|
||||
.chevron {
|
||||
width: 12px;
|
||||
height: 20px;
|
||||
background-color: var(--text-tertiary);
|
||||
@ -117,7 +230,7 @@
|
||||
}
|
||||
|
||||
&:hover {
|
||||
>div {
|
||||
.chevron {
|
||||
background-color: var(--text-primary);
|
||||
}
|
||||
}
|
||||
@ -864,3 +977,28 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.securityWarning {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
|
||||
.securityMessage {
|
||||
display: inline-block;
|
||||
color: var(--text-secondary);
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 17px;
|
||||
margin-left: 22px;
|
||||
}
|
||||
|
||||
.securityIcon {
|
||||
min-width: 54px;
|
||||
height: 54px;
|
||||
display: inline-block;
|
||||
margin: 0px;
|
||||
background-color: var(--text-secondary);
|
||||
-webkit-mask: url(../../assets/icons/report.svg) no-repeat 0 0 / 54px 54px;
|
||||
mask: url(../../assets/icons/report.svg) no-repeat 0 0 / 54px 54px;
|
||||
}
|
||||
}
|
||||
|
@ -1,15 +1,11 @@
|
||||
import { Component } from 'solid-js';
|
||||
import Branding from '../../components/Branding/Branding';
|
||||
import styles from './Settings.module.scss';
|
||||
|
||||
import Wormhole from '../../components/Wormhole/Wormhole';
|
||||
import { useIntl } from '@cookbook/solid-intl';
|
||||
import Search from '../../components/Search/Search';
|
||||
import { settings as t } from '../../translations';
|
||||
import { useSettingsContext } from '../../contexts/SettingsContext';
|
||||
import PageCaption from '../../components/PageCaption/PageCaption';
|
||||
import { Link, Outlet } from '@solidjs/router';
|
||||
import HomeSidebar from '../../components/HomeSidebar/HomeSidebar';
|
||||
import { Outlet } from '@solidjs/router';
|
||||
import StickySidebar from '../../components/StickySidebar/StickySidebar';
|
||||
import SettingsSidebar from '../../components/SettingsSidebar/SettingsSidebar';
|
||||
import PageTitle from '../../components/PageTitle/PageTitle';
|
||||
|
@ -108,6 +108,11 @@ export const actions = {
|
||||
defaultMessage: 'Login now',
|
||||
description: 'Login Now action, button label',
|
||||
},
|
||||
logout: {
|
||||
id: 'actions.logout',
|
||||
defaultMessage: 'Logout',
|
||||
description: 'Logout action, button label',
|
||||
},
|
||||
getStarted: {
|
||||
id: 'actions.getStarted',
|
||||
defaultMessage: 'Get Started',
|
||||
@ -123,6 +128,16 @@ export const actions = {
|
||||
defaultMessage: 'copy',
|
||||
description: 'Copy action, button label',
|
||||
},
|
||||
copyPubkey: {
|
||||
id: 'actions.copyPubkey',
|
||||
defaultMessage: 'copy public key',
|
||||
description: 'Copy pubkey action, button label',
|
||||
},
|
||||
copyPrivkey: {
|
||||
id: 'actions.copyPrivkey',
|
||||
defaultMessage: 'copy private key',
|
||||
description: 'Copy private key action, button label',
|
||||
},
|
||||
addFeedToHome: {
|
||||
id: 'actions.addFeedToHome',
|
||||
defaultMessage: 'add this feed to my home page',
|
||||
@ -1046,6 +1061,38 @@ export const settings = {
|
||||
description: 'Title of the settings page',
|
||||
},
|
||||
},
|
||||
account: {
|
||||
title: {
|
||||
id: 'settings.account.title',
|
||||
defaultMessage: 'Account',
|
||||
description: 'Title of the account settings sub-page',
|
||||
},
|
||||
description: {
|
||||
id: 'settings.account.description',
|
||||
defaultMessage: "You can improve your account security by installing a Nostr browser extension, like {link}. By storing your Nostr private key within a browser extension, you will be able to securely sign into any Nostr web app, including Primal.",
|
||||
description: 'Warning about account security',
|
||||
},
|
||||
pubkey: {
|
||||
id: 'settings.account.pubkey',
|
||||
defaultMessage: 'Your Public Key',
|
||||
description: 'Your public key section caption',
|
||||
},
|
||||
pubkeyDesc: {
|
||||
id: 'settings.account.pubkeyDesc',
|
||||
defaultMessage: 'Anyone on Nostr can find you via your public key. Feel free to share anywhere.',
|
||||
description: 'Label describing the public key',
|
||||
},
|
||||
privkey: {
|
||||
id: 'settings.account.privkey',
|
||||
defaultMessage: 'Your Private Key',
|
||||
description: 'Your private key section caption',
|
||||
},
|
||||
privkeyDesc: {
|
||||
id: 'settings.account.privkeyDesc',
|
||||
defaultMessage: 'This key fully controls your Nostr account. Don’t share it with anyone. Only copy this key to store it securely or to login to another Nostr app.',
|
||||
description: 'Label describing the private key',
|
||||
},
|
||||
},
|
||||
appearance: {
|
||||
title: {
|
||||
id: 'settings.appearance.title',
|
||||
|
Loading…
Reference in New Issue
Block a user