Preferences page

This commit is contained in:
Kieran 2023-08-21 14:58:57 +01:00
parent 35423cc91b
commit 976f841d0b
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
45 changed files with 484 additions and 467 deletions

View File

@ -292,6 +292,12 @@
<path d="M8.70711 3.70711C9.09763 3.31658 9.09763 2.68342 8.70711 2.29289C8.31658 1.90237 7.68342 1.90237 7.29289 2.29289L3.29289 6.29289C2.90237 6.68342 2.90237 7.31658 3.29289 7.70711L7.29289 11.7071C7.68342 12.0976 8.31658 12.0976 8.70711 11.7071C9.09763 11.3166 9.09763 10.6834 8.70711 10.2929L6.41421 8L14 8C16.7614 8 19 10.2386 19 13C19 15.7614 16.7614 18 14 18H4C3.44772 18 3 18.4477 3 19C3 19.5523 3.44772 20 4 20H14C17.866 20 21 16.866 21 13C21 9.13401 17.866 6 14 6L6.41421 6L8.70711 3.70711Z" fill="currentColor"/>
</g>
</symbol>
<symbol id="upload-01" viewBox="0 0 16 16" fill="none">
<g>
<path d="M7.52876 1.52925C7.78911 1.2689 8.21122 1.2689 8.47157 1.52925L11.8049 4.86258C12.0652 5.12293 12.0652 5.54504 11.8049 5.80539C11.5446 6.06574 11.1224 6.06574 10.8621 5.80539L8.66683 3.61013L8.66683 10.0007C8.66683 10.3688 8.36835 10.6673 8.00016 10.6673C7.63197 10.6673 7.3335 10.3688 7.3335 10.0007L7.3335 3.61013L5.13823 5.80539C4.87788 6.06574 4.45577 6.06574 4.19543 5.80539C3.93508 5.54504 3.93508 5.12293 4.19543 4.86258L7.52876 1.52925Z" fill="currentColor"/>
<path d="M2.00016 9.33398C2.36835 9.33398 2.66683 9.63246 2.66683 10.0007V10.8007C2.66683 11.3717 2.66735 11.7599 2.69186 12.06C2.71574 12.3522 2.75903 12.5017 2.81215 12.606C2.93999 12.8569 3.14396 13.0608 3.39484 13.1887C3.49911 13.2418 3.64858 13.2851 3.94086 13.3089C4.24091 13.3335 4.62911 13.334 5.20016 13.334H10.8002C11.3712 13.334 11.7594 13.3335 12.0595 13.3089C12.3517 13.2851 12.5012 13.2418 12.6055 13.1887C12.8564 13.0608 13.0603 12.8569 13.1882 12.606C13.2413 12.5017 13.2846 12.3522 13.3085 12.06C13.333 11.7599 13.3335 11.3717 13.3335 10.8007V10.0007C13.3335 9.63246 13.632 9.33398 14.0002 9.33398C14.3684 9.33398 14.6668 9.63246 14.6668 10.0007V10.8282C14.6668 11.3648 14.6668 11.8077 14.6374 12.1685C14.6067 12.5433 14.541 12.8877 14.3762 13.2113C14.1205 13.7131 13.7126 14.121 13.2108 14.3767C12.8872 14.5415 12.5428 14.6072 12.168 14.6379C11.8073 14.6673 11.3643 14.6673 10.8277 14.6673H5.17263C4.63598 14.6673 4.19308 14.6673 3.83228 14.6379C3.45755 14.6072 3.11308 14.5415 2.78952 14.3767C2.28776 14.121 1.87981 13.7131 1.62415 13.2113C1.45929 12.8877 1.39358 12.5433 1.36296 12.1685C1.33348 11.8077 1.33349 11.3648 1.3335 10.8282V10.0007C1.3335 9.63246 1.63197 9.33398 2.00016 9.33398Z" fill="currentColor"/>
</g>
</symbol>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 76 KiB

View File

@ -11,19 +11,21 @@ interface AvatarProps {
user?: UserMetadata;
onClick?: () => void;
size?: number;
image?: string;
}
const Avatar = ({ user, size, onClick }: AvatarProps) => {
const Avatar = ({ user, size, onClick, image }: AvatarProps) => {
const [url, setUrl] = useState<string>(Nostrich);
const { proxy } = useImgProxy();
useEffect(() => {
if (user?.picture) {
const url = proxy(user.picture, size ?? 120);
setUrl(url);
const url = image ?? user?.picture;
if (url) {
const proxyUrl = proxy(url, size ?? 120);
setUrl(proxyUrl);
} else {
setUrl(Nostrich);
}
}, [user]);
}, [user, image]);
const backgroundImage = `url(${url})`;
const style = { "--img-url": backgroundImage } as CSSProperties;

View File

@ -0,0 +1,20 @@
.avatar .edit,
.banner .edit {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
background-color: var(--bg-color);
cursor: pointer;
opacity: 0;
border-radius: 100%;
}
.avatar .edit.new {
opacity: 0.5;
}
.avatar .edit:hover {
opacity: 0.5;
}

View File

@ -1,7 +1,9 @@
import "./AvatarEditor.css";
import Icon from "Icons/Icon";
import { useState } from "react";
import useFileUpload from "Upload";
import { openFile, unwrap } from "SnortUtils";
import Spinner from "Icons/Spinner";
interface AvatarEditorProps {
picture?: string;
@ -11,9 +13,11 @@ interface AvatarEditorProps {
export default function AvatarEditor({ picture, onPictureChange }: AvatarEditorProps) {
const uploader = useFileUpload();
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
async function uploadFile() {
setError("");
setLoading(true);
try {
const f = await openFile();
if (f) {
@ -32,6 +36,7 @@ export default function AvatarEditor({ picture, onPictureChange }: AvatarEditorP
setError(`Upload failed`);
}
}
setLoading(false);
}
return (
@ -39,7 +44,7 @@ export default function AvatarEditor({ picture, onPictureChange }: AvatarEditorP
<div className="flex f-center">
<div style={{ backgroundImage: `url(${picture})` }} className="avatar">
<div className={`edit${picture ? "" : " new"}`} onClick={() => uploadFile().catch(console.error)}>
<Icon name={picture ? "edit" : "camera-plus"} />
{loading ? <Spinner /> : <Icon name={picture ? "edit" : "camera-plus"} />}
</div>
</div>
</div>

View File

@ -41,6 +41,8 @@ export function updatePreferences(state: LoginSession, p: UserPreferences) {
export function logout(k: HexKey) {
LoginStore.removeSession(k);
//TODO: delete giftwarps for:k
//TODO: delete notifications for:k
}
export function markNotificationsRead(state: LoginSession) {

View File

@ -158,6 +158,7 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
const pk = unwrap(s.publicKey);
if (this.#accounts.has(pk)) {
this.#accounts.set(pk, s);
console.debug("SET SESSION", s);
this.#save();
}
}
@ -175,7 +176,7 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
const s = this.#activeAccount ? this.#accounts.get(this.#activeAccount) : undefined;
if (!s) return LoggedOut;
return s;
return { ...s };
}
#createPublisher(l: LoginSession) {

View File

@ -74,7 +74,7 @@ const DonatePage = () => {
}
return (
<div className="main-content m5">
<div className="main-content p">
<h2>
<FormattedMessage defaultMessage="Help fund the development of Snort" />
</h2>

View File

@ -5,8 +5,6 @@ import Nip5Service from "Element/Nip5Service";
import messages from "./messages";
import "./Verification.css";
export const SnortNostrAddressService = {
name: "Snort",
service: `${ApiHost}/api/v1/n5sp`,
@ -26,11 +24,11 @@ export const Nip5Services = [
},
];
export default function VerificationPage() {
export default function NostrAddressPage() {
return (
<div className="main-content verification">
<div className="main-content p">
<h2>
<FormattedMessage {...messages.GetVerified} />
<FormattedMessage defaultMessage="Buy nostr address" />
</h2>
<p>
<FormattedMessage {...messages.Nip05} />

View File

@ -86,7 +86,7 @@ export default function NotificationsPage() {
const timeGrouped = useMemo(() => {
return orderDescending([...notifications])
.filter(a => !isMuted(a.pubkey))
.filter(a => !isMuted(a.pubkey) && findTag(a, "p") === login.publicKey)
.reduce((acc, v) => {
const key = `${timeKey(v)}:${notificationContext(v as TaggedRawEvent)?.encode()}:${v.kind}`;
if (acc.has(key)) {

View File

@ -333,11 +333,19 @@ export const RootRoutes = [
},
{
path: "trending/people",
element: <TrendingUsers />,
element: (
<div className="p">
<TrendingUsers />
</div>
),
},
{
path: "suggested",
element: <SuggestedProfiles />,
element: (
<div className="p">
<SuggestedProfiles />
</div>
),
},
{
path: "/t/:tag",

View File

@ -29,31 +29,33 @@ export const SettingsRoutes: RouteObject[] = [
{
path: "",
element: <SettingsIndex />,
children: [
{
path: "profile",
element: <Profile />,
},
{
path: "relays",
element: <Relay />,
},
{
path: "relays/:id",
element: <RelayInfo />,
},
{
path: "preferences",
element: <Preferences />,
},
{
path: "accounts",
element: <AccountsPage />,
},
{
path: "keys",
element: <ExportKeys />,
},
...ManageHandleRoutes,
...WalletSettingsRoutes,
],
},
{
path: "profile",
element: <Profile />,
},
{
path: "relays",
element: <Relay />,
},
{
path: "relays/:id",
element: <RelayInfo />,
},
{
path: "preferences",
element: <Preferences />,
},
{
path: "accounts",
element: <AccountsPage />,
},
{
path: "keys",
element: <ExportKeys />,
},
...ManageHandleRoutes,
...WalletSettingsRoutes,
];

View File

@ -1,3 +0,0 @@
.verification a {
color: var(--highlight);
}

View File

@ -204,15 +204,11 @@ export default function WalletPage() {
}
return (
<div className="main-content">
<div className="main-content p">
{error && <b className="error">{error}</b>}
{walletList()}
{unlockWallet()}
{walletInfo()}
<button onClick={() => Wallets.remove(unwrap(walletState.config).id)}>
<FormattedMessage defaultMessage="Delete Account" />
</button>
</div>
);
}

View File

@ -1,3 +1,7 @@
.zap-pool input[type="range"] {
width: 200px;
}
.zap-pool h4 {
margin: 0;
}

View File

@ -92,7 +92,7 @@ export default function ZapPoolPage() {
const sumPending = zapPool.reduce((acc, v) => acc + v.sum, 0);
return (
<div className="zap-pool main-content">
<div className="zap-pool main-content p">
<h1>
<FormattedMessage defaultMessage="Zap Pool" />
</h1>
@ -150,7 +150,7 @@ export default function ZapPoolPage() {
</AsyncButton>
)}
</p>
<div className="card">
<div>
<ZapTarget
target={
zapPool.find(b => b.pubkey === bech32ToHex(SnortPubKey) && b.type === ZapPoolRecipientType.Generic) ?? {
@ -166,7 +166,7 @@ export default function ZapPoolPage() {
<FormattedMessage defaultMessage="Relays" />
</h3>
{relayConnections.map(a => (
<div className="card">
<div>
<h4>{getRelayName(a.address)}</h4>
<ZapTarget
target={
@ -184,7 +184,7 @@ export default function ZapPoolPage() {
<FormattedMessage defaultMessage="File hosts" />
</h3>
{UploaderServices.map(a => (
<div className="card">
<div>
<h4>{a.name}</h4>
<ZapTarget
target={
@ -202,7 +202,7 @@ export default function ZapPoolPage() {
<FormattedMessage defaultMessage="Data Providers" />
</h3>
{DataProviders.map(a => (
<div className="card">
<div>
<h4>{a.name}</h4>
<ZapTarget
target={

View File

@ -25,7 +25,7 @@ export default function DiscoverFollows() {
}
return (
<div className="main-content new-user" dir="auto">
<div className="main-content new-user p" dir="auto">
<Logo />
<div className="progress-bar">
<div className="progress"></div>
@ -45,6 +45,9 @@ export default function DiscoverFollows() {
<FormattedMessage {...messages.PopularAccounts} />
</h3>
{sortedReccomends.length > 0 && <FollowListBase pubkeys={sortedReccomends} showAbout={true} />}
<h3>
<FormattedMessage defaultMessage="Trending Users" />
</h3>
<TrendingUsers />
</div>
);

View File

@ -4,7 +4,7 @@ import { useNavigate } from "react-router-dom";
import { useUserProfile } from "@snort/system-react";
import Logo from "Element/Logo";
import { Nip5Services } from "Pages/Verification";
import { Nip5Services } from "Pages/NostrAddressPage";
import Nip5Service from "Element/Nip5Service";
import ProfileImage from "Element/ProfileImage";
import useLogin from "Hooks/useLogin";

View File

@ -90,7 +90,7 @@ export default function NewUserFlow() {
const navigate = useNavigate();
return (
<div className="main-content new-user" dir="auto">
<div className="main-content new-user p" dir="auto">
<Logo />
<div className="progress-bar">
<div className="progress progress-first"></div>
@ -98,31 +98,25 @@ export default function NewUserFlow() {
<h1>
<FormattedMessage {...messages.SaveKeys} />
</h1>
<div className="card flex">
<div className="flex f-col f-grow">
<div>
<FormattedMessage defaultMessage="Language" />
</div>
</div>
<div>
<select
value={login.preferences.language || DefaultPreferences.language}
onChange={e =>
updatePreferences(login, {
...login.preferences,
language: e.target.value,
})
}
style={{ textTransform: "capitalize" }}>
{AllLanguageCodes.sort().map(a => (
<option value={a}>
{new Intl.DisplayNames([a], {
type: "language",
}).of(a)}
</option>
))}
</select>
</div>
<div className="flex f-space">
<FormattedMessage defaultMessage="Language" />
<select
value={login.preferences.language || DefaultPreferences.language}
onChange={e =>
updatePreferences(login, {
...login.preferences,
language: e.target.value,
})
}
style={{ textTransform: "capitalize" }}>
{AllLanguageCodes.sort().map(a => (
<option value={a}>
{new Intl.DisplayNames([a], {
type: "language",
}).of(a)}
</option>
))}
</select>
</div>
<p>
<FormattedMessage {...messages.SaveKeysHelp} />

View File

@ -47,7 +47,7 @@ export default function ProfileSetup() {
};
return (
<div className="main-content new-user" dir="auto">
<div className="main-content new-user p" dir="auto">
<Logo />
<div className="progress-bar">
<div className="progress progress-second"></div>

View File

@ -1,4 +1,4 @@
.export-keys > .copy {
.copy.dashed {
padding: 12px 16px;
border: 2px dashed #222222;
border-radius: 16px;

View File

@ -10,18 +10,18 @@ import { hexToBech32 } from "SnortUtils";
export default function ExportKeys() {
const { publicKey, privateKey, generatedEntropy } = useLogin();
return (
<div className="export-keys">
<div className="flex-column g12">
<h3>
<FormattedMessage defaultMessage="Public Key" />
</h3>
<Copy text={hexToBech32("npub", publicKey ?? "")} maxSize={48} className="mb10" />
<Copy text={encodeTLV(NostrPrefix.Profile, publicKey ?? "")} maxSize={48} />
<Copy text={hexToBech32("npub", publicKey ?? "")} className="dashed" />
<Copy text={encodeTLV(NostrPrefix.Profile, publicKey ?? "")} className="dashed" />
{privateKey && (
<>
<h3>
<FormattedMessage defaultMessage="Private Key" />
</h3>
<Copy text={hexToBech32("nsec", privateKey)} maxSize={48} />
<Copy text={hexToBech32("nsec", privateKey)} className="dashed" />
</>
)}
{generatedEntropy && (
@ -29,7 +29,7 @@ export default function ExportKeys() {
<h3>
<FormattedMessage defaultMessage="Mnemonic" />
</h3>
<Copy text={hexToMnemonic(generatedEntropy ?? "")} maxSize={48} />
<Copy text={hexToMnemonic(generatedEntropy ?? "")} className="dashed" />
</>
)}
</div>

View File

@ -1,8 +1,14 @@
.preferences small {
margin-top: 0.5em;
color: var(--font-secondary-color);
}
.preferences select {
min-width: 100px;
}
.preferences h4 {
margin: 0;
font-size: 16px;
font-weight: 600;
line-height: 22px; /* 137.5% */
}

View File

@ -32,6 +32,7 @@ export const AllLanguageCodes = [
const PreferencesPage = () => {
const { formatMessage } = useIntl();
const login = useLogin();
console.debug(login);
const perf = login.preferences;
const [emoji, setEmoji] = useState<Array<{ name: string; char: string }>>([]);
@ -42,17 +43,15 @@ const PreferencesPage = () => {
}, []);
return (
<div className="preferences">
<div className="preferences flex-column g24">
<h3>
<FormattedMessage {...messages.Preferences} />
</h3>
<div className="card flex">
<div className="flex f-col f-grow">
<div>
<FormattedMessage defaultMessage="Language" />
</div>
</div>
<div className="flex f-space w-max">
<h4>
<FormattedMessage defaultMessage="Language" />
</h4>
<div>
<select
value={perf.language || DefaultPreferences.language}
@ -73,12 +72,10 @@ const PreferencesPage = () => {
</select>
</div>
</div>
<div className="card flex">
<div className="flex f-col f-grow">
<div>
<FormattedMessage {...messages.Theme} />
</div>
</div>
<div className="flex f-space w-max">
<h4>
<FormattedMessage {...messages.Theme} />
</h4>
<div>
<select
value={perf.theme}
@ -100,12 +97,10 @@ const PreferencesPage = () => {
</select>
</div>
</div>
<div className="card flex">
<div className="flex f-col f-grow">
<div>
<FormattedMessage {...messages.DefaultRootTab} />
</div>
</div>
<div className="flex f-space w-max">
<h4>
<FormattedMessage {...messages.DefaultRootTab} />
</h4>
<div>
<select
value={perf.defaultRootTab}
@ -127,16 +122,17 @@ const PreferencesPage = () => {
</select>
</div>
</div>
<div className="card flex">
<div className="flex f-col f-grow">
<div>
<div className="flex w-max">
<div className="flex-column g8">
<h4>
<FormattedMessage {...messages.AutoloadMedia} />
</div>
</h4>
<small>
<FormattedMessage {...messages.AutoloadMediaHelp} />
</small>
<div className="mt10">
<div className="w-max">
<select
className="w-max"
value={perf.autoLoadMedia}
onChange={e =>
updatePreferences(login, {
@ -157,11 +153,11 @@ const PreferencesPage = () => {
</div>
</div>
</div>
<div className="card flex">
<div className="flex f-col f-grow">
<div>
<div className="flex f-space w-max">
<div className="flex-column g8">
<h4>
<FormattedMessage defaultMessage="Proof of Work" />
</div>
</h4>
<small>
<FormattedMessage defaultMessage="Amount of work to apply to all published events" />
</small>
@ -175,12 +171,10 @@ const PreferencesPage = () => {
/>
</div>
</div>
<div className="card flex">
<div className="flex f-col f-grow">
<div>
<FormattedMessage defaultMessage="Default Zap amount" />
</div>
</div>
<div className="flex f-space w-max">
<h4>
<FormattedMessage defaultMessage="Default Zap amount" />
</h4>
<div>
<input
type="number"
@ -190,11 +184,11 @@ const PreferencesPage = () => {
/>
</div>
</div>
<div className="card flex">
<div className="flex f-col f-grow">
<div>
<div className="flex f-space w-max">
<div className="flex-column g8">
<h4>
<FormattedMessage defaultMessage="Auto Zap" />
</div>
</h4>
<small>
<FormattedMessage defaultMessage="Automatically zap every note when loaded" />
</small>
@ -207,12 +201,12 @@ const PreferencesPage = () => {
/>
</div>
</div>
<div className="card flex f-col">
<div className="flex w-max">
<div className="flex f-col f-grow">
<div>
<div className="flex-column">
<div className="flex f-space">
<div className="flex-column g8">
<h4>
<FormattedMessage {...messages.ImgProxy} />
</div>
</h4>
<small>
<FormattedMessage {...messages.ImgProxyHelp} />
</small>
@ -231,7 +225,7 @@ const PreferencesPage = () => {
</div>
</div>
{perf.imgProxyConfig && (
<div className="w-max mt10 form">
<div className="w-max form">
<div className="form-group">
<div>
<FormattedMessage {...messages.ServiceUrl} />
@ -307,11 +301,11 @@ const PreferencesPage = () => {
</div>
)}
</div>
<div className="card flex">
<div className="flex f-col f-grow">
<div>
<div className="flex f-space w-max">
<div className="flex-column g8">
<h4>
<FormattedMessage {...messages.EnableReactions} />
</div>
</h4>
<small>
<FormattedMessage {...messages.EnableReactionsHelp} />
</small>
@ -324,43 +318,39 @@ const PreferencesPage = () => {
/>
</div>
</div>
<div className="card flex">
<div className="flex f-col f-grow">
<div>
<FormattedMessage {...messages.ReactionEmoji} />
</div>
<small>
<FormattedMessage {...messages.ReactionEmojiHelp} />
</small>
<div className="mt10">
<select
className="emoji-selector"
value={perf.reactionEmoji}
onChange={e =>
updatePreferences(login, {
...perf,
reactionEmoji: e.target.value,
})
}>
<option value="+">
+ <FormattedMessage {...messages.Default} />
<div className="flex-column g8">
<h4>
<FormattedMessage {...messages.ReactionEmoji} />
</h4>
<small>
<FormattedMessage {...messages.ReactionEmojiHelp} />
</small>
<select
className="emoji-selector"
value={perf.reactionEmoji}
onChange={e =>
updatePreferences(login, {
...perf,
reactionEmoji: e.target.value,
})
}>
<option value="+">
+ <FormattedMessage {...messages.Default} />
</option>
{emoji.map(({ name, char }) => {
return (
<option value={char}>
{name} {char}
</option>
{emoji.map(({ name, char }) => {
return (
<option value={char}>
{name} {char}
</option>
);
})}
</select>
</div>
</div>
);
})}
</select>
</div>
<div className="card flex">
<div className="flex f-col f-grow">
<div>
<div className="flex f-space">
<div className="flex-column g8">
<h4>
<FormattedMessage {...messages.ConfirmReposts} />
</div>
</h4>
<small>
<FormattedMessage {...messages.ConfirmRepostsHelp} />
</small>
@ -373,11 +363,11 @@ const PreferencesPage = () => {
/>
</div>
</div>
<div className="card flex">
<div className="flex f-col f-grow">
<div>
<div className="flex f-space">
<div className="flex-column g8">
<h4>
<FormattedMessage {...messages.ShowLatest} />
</div>
</h4>
<small>
<FormattedMessage {...messages.ShowLatestHelp} />
</small>
@ -390,37 +380,33 @@ const PreferencesPage = () => {
/>
</div>
</div>
<div className="card flex">
<div className="flex f-col f-grow">
<div>
<FormattedMessage {...messages.FileUpload} />
</div>
<small>
<FormattedMessage {...messages.FileUploadHelp} />
</small>
<div className="mt10">
<select
value={perf.fileUploader}
onChange={e =>
updatePreferences(login, {
...perf,
fileUploader: e.target.value,
} as UserPreferences)
}>
<option value="void.cat">
void.cat <FormattedMessage {...messages.Default} />
</option>
<option value="nostr.build">nostr.build</option>
<option value="nostrimg.com">nostrimg.com</option>
</select>
</div>
</div>
<div className="flex-column g8">
<h4>
<FormattedMessage {...messages.FileUpload} />
</h4>
<small>
<FormattedMessage {...messages.FileUploadHelp} />
</small>
<select
value={perf.fileUploader}
onChange={e =>
updatePreferences(login, {
...perf,
fileUploader: e.target.value,
} as UserPreferences)
}>
<option value="void.cat">
void.cat <FormattedMessage {...messages.Default} />
</option>
<option value="nostr.build">nostr.build</option>
<option value="nostrimg.com">nostrimg.com</option>
</select>
</div>
<div className="card flex">
<div className="flex f-col f-grow">
<div>
<div className="flex f-space">
<div className="flex-column g8">
<h4>
<FormattedMessage {...messages.DebugMenus} />
</div>
</h4>
<small>
<FormattedMessage {...messages.DebugMenusHelp} />
</small>

View File

@ -1,53 +1,51 @@
.settings h4 {
font-size: 16px;
font-weight: 600;
margin: 0;
}
.settings .avatar {
width: 256px;
height: 256px;
width: 64px;
height: 64px;
background-size: cover;
border-radius: 100%;
cursor: pointer;
margin-bottom: 20px;
}
.settings .banner {
width: 300px;
height: 150px;
width: 100%;
height: 100%;
background-size: cover;
margin-bottom: 20px;
border-radius: 12px;
}
.settings .image-settings {
display: block;
align-items: center;
justify-content: center;
}
.settings .image-setting {
display: flex;
justify-content: space-between;
}
.settings .image-setting > div:first-child {
align-self: center;
}
.avatar .edit,
.banner .edit {
display: flex;
align-items: center;
justify-content: center;
position: relative;
width: 100%;
height: 100%;
background-color: var(--bg-color);
cursor: pointer;
opacity: 0;
border-radius: 100%;
height: 145px;
margin-block-end: 45px;
}
.avatar .edit.new {
opacity: 0.5;
.settings .image-settings > div {
position: absolute;
}
.avatar .edit:hover {
opacity: 0.5;
.settings .image-settings .avatar-stack {
bottom: -32px;
}
.settings .image-settings .avatar-stack .btn-rnd {
position: absolute;
padding: 0;
width: 32px;
height: 32px;
right: -10px;
bottom: -10px;
}
.settings .image-settings .banner button {
right: 16px;
top: 12px;
position: absolute;
}
.settings .editor textarea {
@ -59,3 +57,8 @@
.settings .actions {
margin-top: 16px;
}
.settings small {
font-size: 14px;
color: var(--font-secondary-color);
}

View File

@ -13,10 +13,8 @@ import useFileUpload from "Upload";
import AsyncButton from "Element/AsyncButton";
import { UserCache } from "Cache";
import useLogin from "Hooks/useLogin";
import AvatarEditor from "Element/AvatarEditor";
import Icon from "Icons/Icon";
import messages from "./messages";
import Avatar from "Element/Avatar";
export interface ProfileSettingsProps {
avatar?: boolean;
@ -31,7 +29,6 @@ export default function ProfileSettings(props: ProfileSettingsProps) {
const uploader = useFileUpload();
const [name, setName] = useState<string>();
const [displayName, setDisplayName] = useState<string>();
const [picture, setPicture] = useState<string>();
const [banner, setBanner] = useState<string>();
const [about, setAbout] = useState<string>();
@ -39,12 +36,9 @@ export default function ProfileSettings(props: ProfileSettingsProps) {
const [nip05, setNip05] = useState<string>();
const [lud16, setLud16] = useState<string>();
const avatarPicture = (picture?.length ?? 0) === 0 ? Nostrich : picture;
useEffect(() => {
if (user) {
setName(user.name);
setDisplayName(user.display_name);
setPicture(user.picture);
setBanner(user.banner);
setAbout(user.about);
@ -59,7 +53,6 @@ export default function ProfileSettings(props: ProfileSettingsProps) {
const userCopy = {
...user,
name,
display_name: displayName,
about,
picture,
banner,
@ -107,69 +100,62 @@ export default function ProfileSettings(props: ProfileSettingsProps) {
}
}
async function setNewAvatar() {
const rsp = await uploadFile();
if (rsp) {
setPicture(rsp);
}
}
function editor() {
return (
<div className="editor form">
<div className="form-group card">
<div>
<FormattedMessage {...messages.Name} />:
</div>
<div>
<input type="text" value={name} onChange={e => setName(e.target.value)} />
<div className="flex f-col g24">
<div className="flex f-col w-max g8">
<h4>
<FormattedMessage defaultMessage="Name" />
</h4>
<input className="w-max" type="text" value={name} onChange={e => setName(e.target.value)} />
</div>
<div className="flex f-col w-max g8">
<h4>
<FormattedMessage defaultMessage="About" />
</h4>
<textarea className="w-max" onChange={e => setAbout(e.target.value)} value={about}></textarea>
</div>
<div className="flex f-col w-max g8">
<h4>
<FormattedMessage defaultMessage="Website" />
</h4>
<input className="w-max" type="text" value={website} onChange={e => setWebsite(e.target.value)} />
</div>
<div className="flex f-col w-max g8">
<h4>
<FormattedMessage defaultMessage="Nostr Address" />
</h4>
<div className="flex f-col g8 w-max">
<input type="text" className="w-max" value={nip05} onChange={e => setNip05(e.target.value)} />
<small>
<FormattedMessage defaultMessage="Usernames are not unique on Nostr. The nostr address is your unique human-readable address that is unique to you upon registration." />
</small>
<div className="flex g12">
<button className="flex f-center" type="button" onClick={() => navigate("/nostr-address")}>
<FormattedMessage defaultMessage="Buy nostr address" />
</button>
<button className="flex f-center secondary" type="button" onClick={() => navigate("/nostr-address")}>
<FormattedMessage defaultMessage="Get a free one" />
</button>
</div>
</div>
</div>
<div className="form-group card">
<div>
<FormattedMessage {...messages.DisplayName} />:
</div>
<div>
<input type="text" value={displayName} onChange={e => setDisplayName(e.target.value)} />
</div>
</div>
<div className="form-group card">
<div>
<FormattedMessage {...messages.About} />:
</div>
<div className="w-max">
<textarea className="w-max" onChange={e => setAbout(e.target.value)} value={about}></textarea>
</div>
</div>
<div className="form-group card">
<div>
<FormattedMessage {...messages.Website} />:
</div>
<div>
<input type="text" value={website} onChange={e => setWebsite(e.target.value)} />
</div>
</div>
<div className="form-group card">
<div>
<FormattedMessage {...messages.Nip05} />:
</div>
<div>
<input type="text" className="mr10" value={nip05} onChange={e => setNip05(e.target.value)} />
<button type="button" onClick={() => navigate("/verification")}>
<Icon name="shopping-bag" />
&nbsp; <FormattedMessage {...messages.Buy} />
</button>
</div>
</div>
<div className="form-group card">
<div>
<FormattedMessage {...messages.LnAddress} />:
</div>
<div>
<input type="text" value={lud16} onChange={e => setLud16(e.target.value)} />
</div>
</div>
<div className="form-group card">
<div></div>
<div>
<AsyncButton onClick={() => saveProfile()}>
<FormattedMessage {...messages.Save} />
</AsyncButton>
</div>
<div className="flex f-col w-max g8">
<h4>
<FormattedMessage defaultMessage="Lightning Address" />
</h4>
<input className="w-max" type="text" value={lud16} onChange={e => setLud16(e.target.value)} />
</div>
<AsyncButton onClick={() => saveProfile()}>
<FormattedMessage defaultMessage="Save" />
</AsyncButton>
</div>
);
}
@ -179,28 +165,23 @@ export default function ProfileSettings(props: ProfileSettingsProps) {
return (
<>
<div className="flex f-center image-settings">
{(props.avatar ?? true) && (
<div className="image-setting card">
<div>
<FormattedMessage {...messages.Avatar} />:
</div>
<AvatarEditor picture={avatarPicture} onPictureChange={p => setPicture(p)} />
{(props.banner ?? true) && (
<div
style={{
backgroundImage: `url(${(banner?.length ?? 0) === 0 ? Nostrich : banner})`,
}}
className="banner">
<AsyncButton type="button" onClick={() => setNewBanner()}>
<FormattedMessage defaultMessage="Upload" />
</AsyncButton>
</div>
)}
{(props.banner ?? true) && (
<div className="image-setting card">
<div>
<FormattedMessage {...messages.Banner} />:
</div>
<div
style={{
backgroundImage: `url(${(banner?.length ?? 0) === 0 ? Nostrich : banner})`,
}}
className="banner">
<div className="edit" onClick={() => setNewBanner()}>
<FormattedMessage {...messages.Edit} />
</div>
</div>
{(props.avatar ?? true) && (
<div className="avatar-stack">
<Avatar user={user} image={picture} />
<AsyncButton type="button" className="btn-rnd" onClick={() => setNewAvatar()}>
<Icon name="upload-01" />
</AsyncButton>
</div>
)}
</div>
@ -209,12 +190,5 @@ export default function ProfileSettings(props: ProfileSettingsProps) {
);
}
return (
<div className="settings">
<h3>
<FormattedMessage {...messages.EditProfile} />
</h3>
{settings()}
</div>
);
return <div className="settings">{settings()}</div>;
}

View File

@ -22,7 +22,7 @@ const RelayInfo = () => {
<h3 className="pointer" onClick={() => navigate("/settings/relays")}>
<FormattedMessage {...messages.Relays} />
</h3>
<div className="card">
<div>
<h3>{stats?.info?.name}</h3>
<p>{stats?.info?.description}</p>

View File

@ -1,19 +1,29 @@
.settings-nav {
display: grid;
grid-template-columns: 237px auto;
}
.settings-nav > div {
border: 1px solid var(--gray-superdark);
}
.settings-nav > div:nth-child(2) {
padding: 12px 16px;
}
.settings-nav .card {
cursor: pointer;
}
.settings-row {
display: grid;
grid-template-columns: 22px 1fr 8px;
grid-template-columns: 24px 1fr 24px;
align-items: center;
font-weight: 600;
font-size: 16px;
padding: 0.8em 1em;
background: var(--note-bg);
border-radius: 10px;
cursor: pointer;
gap: 10px;
margin-bottom: 5px;
padding: 12px 16px;
gap: 8px;
font-size: 16px;
font-weight: 600;
}
.settings-row.inner {
@ -24,12 +34,12 @@
}
.settings-group-header {
font-weight: 600;
align-items: center;
cursor: pointer;
padding: 12px 16px;
gap: 8px;
font-size: 16px;
padding: 0.8em 1em;
background-color: var(--note-bg);
border-radius: 10px;
margin-bottom: 5px;
font-weight: 600;
}
.settings-row:hover,
@ -41,11 +51,6 @@
margin-left: auto;
}
.settings-row svg {
width: 100%;
height: 100%;
}
.settings-group-header .collapse-icon > svg {
width: 8px;
}

View File

@ -1,12 +1,11 @@
import "./Root.css";
import { FormattedMessage } from "react-intl";
import { useNavigate } from "react-router-dom";
import { Outlet, useNavigate } from "react-router-dom";
import Icon from "Icons/Icon";
import { LoginStore, logout } from "Login";
import useLogin from "Hooks/useLogin";
import { unwrap } from "SnortUtils";
import { getCurrentSubscription } from "Subscription";
import { CollapsedSection } from "Element/Collapsed";
import messages from "./messages";
@ -21,80 +20,72 @@ const SettingsIndex = () => {
}
return (
<>
<div className="settings-nav">
<CollapsedSection
title={
<div className="flex">
<Icon name="user" className="mr10" />
<FormattedMessage defaultMessage="Account" />
</div>
}
className="settings-group-header">
<div className="card">
<div className="settings-row inner" onClick={() => navigate("profile")}>
<Icon name="profile" />
<FormattedMessage {...messages.Profile} />
<Icon name="arrowFront" />
</div>
<div className="settings-row inner" onClick={() => navigate("relays")}>
<Icon name="relay" />
<FormattedMessage {...messages.Relays} />
<Icon name="arrowFront" />
</div>
<div className="settings-row inner" onClick={() => navigate("keys")}>
<Icon name="key" />
<FormattedMessage defaultMessage="Export Keys" />
<Icon name="arrowFront" />
</div>
<div className="settings-row inner" onClick={() => navigate("handle")}>
<Icon name="badge" />
<FormattedMessage defaultMessage="Nostr Adddress" />
<Icon name="arrowFront" />
</div>
<div className="settings-row inner" onClick={() => navigate("/subscribe/manage")}>
<Icon name="diamond" />
<FormattedMessage defaultMessage="Subscription" />
<Icon name="arrowFront" />
</div>
{sub && (
<div className="settings-row inner" onClick={() => navigate("accounts")}>
<Icon name="code-circle" />
<FormattedMessage defaultMessage="Account Switcher" />
<Icon name="arrowFront" />
</div>
)}
<div className="settings-nav">
<div>
<div className="settings-row" onClick={() => navigate("profile")}>
<Icon name="profile" size={24} />
<FormattedMessage {...messages.Profile} />
<Icon name="arrowFront" size={16} />
</div>
<div className="settings-row" onClick={() => navigate("relays")}>
<Icon name="relay" size={24} />
<FormattedMessage {...messages.Relays} />
<Icon name="arrowFront" size={16} />
</div>
<div className="settings-row" onClick={() => navigate("keys")}>
<Icon name="key" size={24} />
<FormattedMessage defaultMessage="Export Keys" />
<Icon name="arrowFront" size={16} />
</div>
<div className="settings-row" onClick={() => navigate("handle")}>
<Icon name="badge" size={24} />
<FormattedMessage defaultMessage="Nostr Adddress" />
<Icon name="arrowFront" size={16} />
</div>
<div className="settings-row" onClick={() => navigate("/subscribe/manage")}>
<Icon name="diamond" size={24} />
<FormattedMessage defaultMessage="Subscription" />
<Icon name="arrowFront" size={16} />
</div>
{sub && (
<div className="settings-row" onClick={() => navigate("accounts")}>
<Icon name="code-circle" size={24} />
<FormattedMessage defaultMessage="Account Switcher" />
<Icon name="arrowFront" size={16} />
</div>
</CollapsedSection>
)}
<div className="settings-row" onClick={() => navigate("preferences")}>
<Icon name="gear" />
<Icon name="gear" size={24} />
<FormattedMessage {...messages.Preferences} />
<Icon name="arrowFront" />
<Icon name="arrowFront" size={16} />
</div>
<div className="settings-row" onClick={() => navigate("wallet")}>
<Icon name="wallet" />
<Icon name="wallet" size={24} />
<FormattedMessage defaultMessage="Wallet" />
<Icon name="arrowFront" />
<Icon name="arrowFront" size={16} />
</div>
<div className="settings-row" onClick={() => navigate("/donate")}>
<Icon name="heart" />
<Icon name="heart" size={24} />
<FormattedMessage {...messages.Donate} />
<Icon name="arrowFront" />
<Icon name="arrowFront" size={16} />
</div>
<div className="settings-row" onClick={() => navigate("/zap-pool")}>
<Icon name="piggy-bank" />
<Icon name="piggy-bank" size={24} />
<FormattedMessage defaultMessage="Zap Pool" />
<Icon name="arrowFront" />
<Icon name="arrowFront" size={16} />
</div>
<div className="settings-row" onClick={handleLogout}>
<Icon name="logout" />
<Icon name="logout" size={24} />
<FormattedMessage {...messages.LogOut} />
<Icon name="arrowFront" />
<Icon name="arrowFront" size={16} />
</div>
</div>
</>
<div>
<Outlet />
</div>
</div>
);
};

View File

@ -5,10 +5,10 @@
grid-gap: 10px;
}
.wallet-grid .card {
.wallet-grid > div {
cursor: pointer;
display: flex;
flex-direction: column;
justify-content: flex-end;
align-items: center;
justify-content: space-between;
}

View File

@ -1,7 +1,7 @@
import "./WalletSettings.css";
import LndLogo from "lnd-logo.png";
import { FormattedMessage } from "react-intl";
import { RouteObject, useNavigate } from "react-router-dom";
import { Link, RouteObject, useNavigate } from "react-router-dom";
import BlueWallet from "Icons/BlueWallet";
import ConnectLNC from "Pages/settings/wallet/LNC";
@ -15,21 +15,26 @@ const WalletSettings = () => {
const navigate = useNavigate();
return (
<>
<Link to="/wallet">
<button type="button">
<FormattedMessage defaultMessage="View Wallets" />
</button>
</Link>
<h3>
<FormattedMessage defaultMessage="Connect Wallet" />
</h3>
<div className="wallet-grid">
<div className="card" onClick={() => navigate("/settings/wallet/lnc")}>
<div onClick={() => navigate("/settings/wallet/lnc")}>
<img src={LndLogo} width={100} />
<h3 className="f-end">LND with LNC</h3>
<h3>LND with LNC</h3>
</div>
<div className="card" onClick={() => navigate("/settings/wallet/lndhub")}>
<div onClick={() => navigate("/settings/wallet/lndhub")}>
<BlueWallet width={100} height={100} />
<h3 className="f-end">LNDHub</h3>
<h3>LNDHub</h3>
</div>
<div className="card" onClick={() => navigate("/settings/wallet/nwc")}>
<div onClick={() => navigate("/settings/wallet/nwc")}>
<NostrIcon width={100} height={100} />
<h3 className="f-end">Nostr Wallet Connect</h3>
<h3>Nostr Wallet Connect</h3>
</div>
</div>
</>

View File

@ -42,7 +42,7 @@ export default function LNForwardAddress({ handle }: { handle: ManageHandle }) {
}
return (
<div className="card">
<div>
<h4>
<FormattedMessage defaultMessage="Update Lightning Address" />
</h4>

View File

@ -29,37 +29,33 @@ export default function ListHandles() {
defaultMessage="It looks like you dont have any, check {link} to buy one!"
values={{
link: (
<Link to="/verification">
<FormattedMessage defaultMessage="Verification" />
<Link to="/nostr-address">
<FormattedMessage defaultMessage="Buy Handle" />
</Link>
),
}}
/>
)}
{handles.map(a => (
<div className="card flex" key={a.id}>
<div className="f-grow">
<h4 className="nip05">
{a.handle}@
<span className="domain" data-domain={a.domain?.toLowerCase()}>
{a.domain}
</span>
</h4>
</div>
<div>
<button
onClick={() =>
navigate("manage", {
state: a,
})
}>
<FormattedMessage defaultMessage="Manage" />
</button>
</div>
<div className="flex f-space" key={a.id}>
<h4 className="nip05">
{a.handle}@
<span className="domain" data-domain={a.domain?.toLowerCase()}>
{a.domain}
</span>
</h4>
<button
onClick={() =>
navigate("manage", {
state: a,
})
}>
<FormattedMessage defaultMessage="Manage" />
</button>
</div>
))}
{handles.length > 0 && (
<button onClick={() => navigate("/verification")}>
<button onClick={() => navigate("/nostr-address")}>
<FormattedMessage defaultMessage="Buy Handle" />
</button>
)}

View File

@ -28,7 +28,7 @@ export default function TransferHandle({ handle }: { handle: ManageHandle }) {
}
return (
<div className="card">
<div>
<h4>
<FormattedMessage defaultMessage="Transfer to Pubkey" />
</h4>

View File

@ -31,7 +31,7 @@ const ConnectCashu = () => {
data: mintUrl,
} as WalletConfig;
Wallets.add(newWallet);
navigate("/wallet");
navigate("/settings/wallet");
} catch (e) {
if (e instanceof Error) {
setError((e as Error).message);

View File

@ -46,7 +46,7 @@ const ConnectLNC = () => {
active: true,
info: unwrap(walletInfo),
});
navigate("/wallet");
navigate("/settings/wallet");
}
function flowConnect() {

View File

@ -29,7 +29,7 @@ const ConnectLNDHub = () => {
} as WalletConfig;
Wallets.add(newWallet);
navigate("/wallet");
navigate("/settings/wallet");
} catch (e) {
if (e instanceof Error) {
setError((e as Error).message);

View File

@ -29,7 +29,7 @@ const ConnectNostrWallet = () => {
} as WalletConfig;
Wallets.add(newWallet);
navigate("/wallet");
navigate("/settings/wallet");
} catch (e) {
if (e instanceof Error) {
setError((e as Error).message);

View File

@ -33,7 +33,7 @@ export default function ManageSubscriptionPage() {
return <PageSpinner />;
}
return (
<>
<div className="main-content p flex-column g24">
<h2>
<FormattedMessage defaultMessage="Subscriptions" />
</h2>
@ -60,6 +60,6 @@ export default function ManageSubscriptionPage() {
</p>
)}
{error && <b className="error">{mapSubscriptionErrorCode(error)}</b>}
</>
</div>
);
}

View File

@ -8,7 +8,7 @@ import Icon from "Icons/Icon";
import useEventPublisher from "Feed/EventPublisher";
import SendSats from "Element/SendSats";
import Nip5Service from "Element/Nip5Service";
import { SnortNostrAddressService } from "Pages/Verification";
import { SnortNostrAddressService } from "Pages/NostrAddressPage";
import Nip05 from "Element/Nip05";
export default function SubscriptionCard({ sub }: { sub: Subscription }) {
@ -62,7 +62,7 @@ export default function SubscriptionCard({ sub }: { sub: Subscription }) {
return (
<>
<div className="card">
<div className="p">
<div className="flex card-title">
<Icon name="badge" className="mr5" size={25} />
{mapPlanName(sub.type)}

View File

@ -1,4 +1,4 @@
.subscribe-page > div.card {
.subscribe-page > div {
margin: 5px;
min-height: 400px;
user-select: none;
@ -18,7 +18,7 @@
flex-direction: column;
}
.subscribe-page > div.card {
.subscribe-page > div {
flex: unset;
}
}

View File

@ -77,11 +77,11 @@ export function SubscribePage() {
return (
<>
<div className="flex subscribe-page">
<div className="flex subscribe-page main-content">
{Plans.map(a => {
const lower = Plans.filter(b => b.id < a.id);
return (
<div className={`card flex f-col${a.disabled ? " disabled" : ""}`}>
<div className={`p flex-column${a.disabled ? " disabled" : ""}`}>
<div className="f-grow">
<h2>{mapPlanName(a.id)}</h2>
<p>

View File

@ -27,14 +27,18 @@ export async function openFile(): Promise<File | undefined> {
return new Promise(resolve => {
const elm = document.createElement("input");
elm.type = "file";
elm.onchange = (e: Event) => {
const handleInput = (e: Event) => {
console.debug(e);
const elm = e.target as HTMLInputElement;
if (elm.files) {
resolve(elm.files[0]);
if ((elm.files?.length ?? 0) > 0) {
resolve(elm.files![0]);
} else {
resolve(undefined);
}
};
elm.onchange = e => handleInput(e);
elm.onblur = e => handleInput(e);
elm.click();
});
}

View File

@ -14,11 +14,11 @@ export class Nip5Task extends BaseUITask {
return (
<p>
<FormattedMessage
defaultMessage="Hey, it looks like you dont have a NIP-05 handle yet, you should get one! Check out {link}"
defaultMessage="Hey, it looks like you dont have a Nostr Address yet, you should get one! Check out {link}"
values={{
link: (
<Link to="/verification">
<FormattedMessage defaultMessage="NIP-05 Shop" />
<Link to="/nostr-address">
<FormattedMessage defaultMessage="Buy nostr address" />
</Link>
),
}}

View File

@ -125,6 +125,10 @@ body #root > div:not(.page) header {
}
}
.p {
padding: 12px 16px;
}
.card {
padding: 12px 16px;
border-bottom: 1px solid var(--gray-superdark);
@ -158,7 +162,6 @@ button {
padding: 6px 12px;
font-weight: 600;
color: white;
min-height: 35px;
font-size: var(--font-size);
background-color: var(--highlight);
border: none;
@ -296,12 +299,13 @@ input[type="password"],
input[type="number"],
select,
textarea {
padding: 12px;
padding: 12px 16px;
color: var(--font-color);
background: transparent;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
outline: none;
line-height: 24px; /* 150% */
}
.light input[type="text"],
@ -338,6 +342,11 @@ input:disabled {
min-width: 0;
}
.flex-column {
display: flex;
flex-direction: column;
}
.f-center {
justify-content: center;
}

View File

@ -19,7 +19,7 @@ import { RootRoutes } from "Pages/Root";
import NotificationsPage from "Pages/Notifications";
import SettingsPage, { SettingsRoutes } from "Pages/SettingsPage";
import ErrorPage from "Pages/ErrorPage";
import VerificationPage from "Pages/Verification";
import NostrAddressPage from "Pages/NostrAddressPage";
import MessagesPage from "Pages/MessagesPage";
import DonatePage from "Pages/DonatePage";
import SearchPage from "Pages/SearchPage";
@ -125,8 +125,8 @@ export const router = createBrowserRouter([
children: SettingsRoutes,
},
{
path: "/verification",
element: <VerificationPage />,
path: "/nostr-address",
element: <NostrAddressPage />,
},
{
path: "/messages/:id?",