feat: new wallet design

This commit is contained in:
Kieran 2024-01-02 18:11:44 +00:00
parent 0d10122394
commit bc7ec4d77f
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
16 changed files with 252 additions and 130 deletions

View File

@ -408,7 +408,7 @@ export function NoteCreator() {
<div className="flex flex-col g8">
{[...(note.zapSplits ?? [])].map((v, i, arr) => (
<div className="flex items-center g8">
<div className="flex flex-col f-4 g4">
<div className="flex flex-col flex-4 g4">
<h4>
<FormattedMessage defaultMessage="Recipient" id="8Rkoyb" />
</h4>
@ -423,7 +423,7 @@ export function NoteCreator() {
placeholder={formatMessage({ defaultMessage: "npub / nprofile / nostr address", id: "WvGmZT" })}
/>
</div>
<div className="flex flex-col f-1 g4">
<div className="flex flex-col flex-1 g4">
<h4>
<FormattedMessage defaultMessage="Weight" id="zCb8fX" />
</h4>

View File

@ -0,0 +1,34 @@
import { bech32ToHex } from "@snort/shared";
import { EventKind, ReplaceableNoteStore, RequestBuilder } from "@snort/system";
import { useRequestBuilder } from "@snort/system-react";
import { useMemo } from "react";
// Snort backend publishes rates
const SnortPubkey = "npub1sn0rtcjcf543gj4wsg7fa59s700d5ztys5ctj0g69g2x6802npjqhjjtws";
export function useRates(symbol: string, leaveOpen = true) {
const sub = useMemo(() => {
const rb = new RequestBuilder(`rates:${symbol}`);
rb.withOptions({
leaveOpen,
});
rb.withFilter()
.kinds([1009 as EventKind])
.authors([bech32ToHex(SnortPubkey)])
.tag("d", [symbol])
.limit(1);
return rb;
}, [symbol]);
const data = useRequestBuilder(ReplaceableNoteStore, sub);
const tag = data?.data?.tags.find(a => a[0] === "d" && a[1] === symbol);
if (!tag) return undefined;
return {
time: data.data?.created_at,
ask: Number(tag[2]),
bid: Number(tag[3]),
low: Number(tag[4]),
hight: Number(tag[5]),
};
}

View File

@ -71,7 +71,7 @@ export function SnortDeckLayout() {
id="IOu4Xh"
values={{
app: CONFIG.appNameCapitalized,
tier: mapPlanName(CONFIG.deckSubKind),
tier: mapPlanName(CONFIG.deckSubKind ?? -1),
}}
/>
</div>
@ -122,7 +122,11 @@ export function SnortDeckLayout() {
)}
{deckState.article && (
<>
<Modal onClose={() => setDeckState({})} className="long-form" onClick={() => setDeckState({})}>
<Modal
id="deck-article"
onClose={() => setDeckState({})}
className="long-form"
onClick={() => setDeckState({})}>
<div onClick={e => e.stopPropagation()}>
<LongFormText ev={deckState.article} isPreview={false} related={[]} />
</div>
@ -140,11 +144,11 @@ function NotesCol() {
return (
<div>
<div className="deck-col-header flex">
<div className="flex f-1 g8">
<div className="flex flex-1 g8">
<Icon name="rows-01" size={24} />
<FormattedMessage defaultMessage="Notes" id="7+Domh" />
</div>
<div className="f-1">
<div className="flex-1">
<RootTabs base="/deck" />
</div>
</div>

View File

@ -1,18 +1,21 @@
import { LogoHeader } from "./LogoHeader";
import { useNavigate } from "react-router-dom";
import { Link, useNavigate } from "react-router-dom";
import Icon from "@/Icons/Icon";
import { ProfileLink } from "../../Element/User/ProfileLink";
import Avatar from "../../Element/User/Avatar";
import useLogin from "../../Hooks/useLogin";
import { useUserProfile } from "@snort/system-react";
import { NoteCreatorButton } from "../../Element/Event/Create/NoteCreatorButton";
import { FormattedMessage, useIntl } from "react-intl";
import { FormattedMessage, FormattedNumber, useIntl } from "react-intl";
import classNames from "classnames";
import { getCurrentSubscription } from "@/Subscription";
import { HasNotificationsMarker } from "@/Pages/Layout/HasNotificationsMarker";
import NavLink from "@/Element/Button/NavLink";
import { subscribeToNotifications } from "@/Notifications";
import useEventPublisher from "@/Hooks/useEventPublisher";
import { Sats, useWallet } from "@/Wallet";
import { useEffect, useState } from "react";
import { useRates } from "@/Hooks/useRates";
const MENU_ITEMS = [
{
@ -72,6 +75,44 @@ const getNavLinkClass = (isActive: boolean, narrow: boolean) => {
});
};
const WalletBalance = () => {
const [balance, setBalance] = useState<Sats>();
const wallet = useWallet();
const rates = useRates("BTCUSD");
useEffect(() => {
setBalance(undefined);
if (wallet.wallet && wallet.wallet.canGetBalance()) {
wallet.wallet.getBalance().then(setBalance);
}
}, [wallet]);
return (
<div className="w-max flex flex-col max-xl:hidden">
<div className="grow flex items-center justify-between">
<div className="flex gap-1 items-center">
<Icon name="sats" size={24} />
<FormattedNumber value={balance ?? 0} />
</div>
<Link to="/wallet">
<Icon name="dots" className="text-secondary" />
</Link>
</div>
<div className="text-secondary text-sm">
<FormattedMessage
defaultMessage="~{amount}"
id="3QwfJR"
values={{
amount: (
<FormattedNumber style="currency" currency="USD" value={(rates?.ask ?? 0) * (balance ?? 0) * 1e-8} />
),
}}
/>
</div>
</div>
);
};
export default function NavSidebar({ narrow = false }) {
const { publicKey, subscriptions, readonly } = useLogin(s => ({
publicKey: s.publicKey,
@ -106,6 +147,7 @@ export default function NavSidebar({ narrow = false }) {
{ "xl:items-start": !narrow, "xl:gap-2": !narrow },
"gap-1 flex flex-col items-center text-lg font-bold",
)}>
<WalletBalance narrow={narrow} />
{MENU_ITEMS.filter(a => {
if ((CONFIG.hideFromNavbar ?? []).includes(a.link)) {
return false;
@ -122,7 +164,7 @@ export default function NavSidebar({ narrow = false }) {
return "";
}
const onClick = () => {
if (item.label === "Notifications") {
if (item.label === "Notifications" && publisher) {
subscribeToNotifications(publisher);
}
};

View File

@ -5,31 +5,35 @@ import { useNavigate } from "react-router-dom";
import { FormattedMessage, FormattedNumber, useIntl } from "react-intl";
import NoteTime from "@/Element/Event/NoteTime";
import { WalletInvoice, Sats, WalletInfo, WalletInvoiceState, useWallet, LNWallet, Wallets } from "@/Wallet";
import { WalletInvoice, Sats, useWallet, LNWallet, Wallets } from "@/Wallet";
import AsyncButton from "@/Element/Button/AsyncButton";
import { unwrap } from "@/SnortUtils";
import Icon from "@/Icons/Icon";
import { useRates } from "@/Hooks/useRates";
import { AsyncIcon } from "@/Element/Button/AsyncIcon";
import classNames from "classnames";
export default function WalletPage() {
export default function WalletPage(props: { showHistory: boolean }) {
const navigate = useNavigate();
const { formatMessage } = useIntl();
const [info, setInfo] = useState<WalletInfo>();
const [balance, setBalance] = useState<Sats>();
const [history, setHistory] = useState<WalletInvoice[]>();
const [walletPassword, setWalletPassword] = useState<string>();
const [error, setError] = useState<string>();
const walletState = useWallet();
const wallet = walletState.wallet;
const rates = useRates("BTCUSD");
async function loadWallet(wallet: LNWallet) {
try {
const i = await wallet.getInfo();
setInfo(i);
setError(undefined);
setBalance(0);
setHistory(undefined);
if (wallet.canGetBalance()) {
const b = await wallet.getBalance();
setBalance(b as Sats);
}
if (wallet.canGetInvoices()) {
if (wallet.canGetInvoices() && (props.showHistory ?? true)) {
const h = await wallet.getInvoices();
setHistory((h as WalletInvoice[]).sort((a, b) => b.timestamp - a.timestamp));
}
@ -43,29 +47,11 @@ export default function WalletPage() {
}
useEffect(() => {
if (wallet) {
if (wallet.isReady()) {
loadWallet(wallet).catch(console.warn);
} else if (wallet.canAutoLogin()) {
wallet
.login()
.then(async () => await loadWallet(wallet))
.catch(console.warn);
}
if (wallet && wallet.isReady()) {
loadWallet(wallet).catch(console.warn);
}
}, [wallet]);
function stateIcon(s: WalletInvoiceState) {
switch (s) {
case WalletInvoiceState.Pending:
return <Icon name="clock" size={15} />;
case WalletInvoiceState.Paid:
return <Icon name="check" size={15} />;
case WalletInvoiceState.Expired:
return <Icon name="close" size={15} />;
}
}
async function loginWallet(pw: string) {
if (wallet) {
await wallet.login(pw);
@ -112,11 +98,11 @@ export default function WalletPage() {
);
}
return (
<div className="flex w-max">
<h4 className="f-1">
<div className="flex items-center">
<h4 className="grow">
<FormattedMessage defaultMessage="Select Wallet" id="G1BGCg" />
</h4>
<div className="f-1">
<div>
<select className="w-max" onChange={e => Wallets.switch(e.target.value)} value={walletState.config?.id}>
{Wallets.list().map(a => {
return <option value={a.id}>{a.info.alias}</option>;
@ -128,79 +114,108 @@ export default function WalletPage() {
}
function walletHistory() {
if (!wallet?.canGetInvoices()) return;
if (!wallet?.canGetInvoices() || !(props.showHistory ?? true)) return;
return (
<>
<div className="flex flex-col gap-1">
<h3>
<FormattedMessage defaultMessage="History" id="d6CyG5" description="Wallet transation history" />
<FormattedMessage defaultMessage="Payments" id="pukxg/" description="Wallet transation history" />
</h3>
{history?.map(a => (
<div className="card flex wallet-history-item" key={a.timestamp}>
<div className="grow">
<NoteTime from={a.timestamp * 1000} fallback={formatMessage({ defaultMessage: "now", id: "kaaf1E" })} />
<div>{(a.memo ?? "").length === 0 ? <>&nbsp;</> : a.memo}</div>
</div>
<div
className={`flex gap-2 items-center ${(() => {
switch (a.state) {
case WalletInvoiceState.Paid:
return "success";
case WalletInvoiceState.Expired:
return "expired";
case WalletInvoiceState.Failed:
return "failed";
default:
return "pending";
}
})()}`}>
<div>{stateIcon(a.state)}</div>
{history?.map(a => {
const dirClassname = {
"text-[--success]": a.direction === "in",
"text-[--error]": a.direction === "out",
};
return (
<div className="flex gap-4 p-2 hover:bg-[--gray-superdark] rounded-xl items-center" key={a.timestamp}>
<div>
<FormattedMessage
defaultMessage="{amount} sats"
id="vrTOHJ"
values={{
amount: <FormattedNumber value={a.amount / 1e3} />,
}}
/>
<div className="rounded-full aspect-square p-2 bg-[--gray-dark]">
<Icon
name="arrow-up-right"
className={classNames(dirClassname, {
"rotate-180": a.direction === "in",
})}
/>
</div>
</div>
<div className="grow flex justify-between">
<div className="flex flex-col gap-1">
<div>{a.memo?.length === 0 ? CONFIG.appNameCapitalized : a.memo}</div>
<div className="text-secondary text-sm">
<NoteTime
from={a.timestamp * 1000}
fallback={formatMessage({ defaultMessage: "now", id: "kaaf1E" })}
/>
</div>
</div>
<div className="flex flex-col gap-1 text-right">
<div className={classNames(dirClassname)}>
<FormattedMessage
defaultMessage="{sign} {amount} sats"
id="tj6kdX"
values={{
sign: a.direction === "in" ? "+" : "-",
amount: <FormattedNumber value={a.amount / 1e3} />,
}}
/>
</div>
<div className="text-secondary text-sm">
<FormattedMessage
defaultMessage="~{amount}"
id="3QwfJR"
values={{
amount: (
<FormattedNumber
style="currency"
currency="USD"
value={(rates?.ask ?? 0) * a.amount * 1e-11}
/>
),
}}
/>
</div>
</div>
</div>
</div>
</div>
))}
</>
);
})}
</div>
);
}
function walletBalance() {
if (!wallet?.canGetBalance()) return;
return (
<small>
<div className="flex items-center gap-2">
<FormattedMessage
defaultMessage="Balance: {amount} sats"
id="VN0+Fz"
defaultMessage="<big>{amount}</big> <small>sats</small>"
id="E5ZIPD"
values={{
big: c => <span className="text-3xl font-bold">{c}</span>,
small: c => <span className="text-secondary">{c}</span>,
amount: <FormattedNumber value={balance ?? 0} />,
}}
/>
</small>
<AsyncIcon size={20} className="text-secondary cursor-pointer" iconName="closedeye" />
</div>
);
}
function walletInfo() {
if (!wallet?.isReady()) return;
return (
<>
<div className="p br b flex justify-between">
<div>
<div>{info?.alias}</div>
{walletBalance()}
</div>
<div>
{walletState.config?.id && (
<AsyncButton onClick={() => Wallets.remove(unwrap(walletState.config?.id))}>
<FormattedMessage defaultMessage="Remove" id="G/yZLu" />
</AsyncButton>
)}
<div className="flex flex-col items-center px-6 py-4 bg-[--gray-superdark] rounded-2xl gap-1">
{walletBalance()}
<div className="text-secondary">
<FormattedMessage
defaultMessage="~{amount}"
id="3QwfJR"
values={{
amount: (
<FormattedNumber style="currency" currency="USD" value={(rates?.ask ?? 0) * (balance ?? 0) * 1e-8} />
),
}}
/>
</div>
</div>
{walletHistory()}
@ -209,7 +224,7 @@ export default function WalletPage() {
}
return (
<div className="main-content p">
<div className="main-content">
{walletList()}
{error && <b className="warning">{error}</b>}
{unlockWallet()}

View File

@ -22,7 +22,7 @@ export default function AccountsPage() {
about: false,
}}
actions={
<div className="f-1">
<div className="flex-1">
<button className="mb10" onClick={() => LoginStore.switchAccount(a.id)}>
<FormattedMessage defaultMessage="Switch" id="n1Whvj" />
</button>

View File

@ -16,7 +16,7 @@ const WalletSettings = () => {
const navigate = useNavigate();
return (
<>
<WalletPage />
<WalletPage showHistory={false} />
<h3>
<FormattedMessage defaultMessage="Connect Wallet" id="cg1VJ2" />
</h3>

View File

@ -47,7 +47,7 @@ export default function SubscriptionCard({ sub }: { sub: Subscription }) {
{mapPlanName(sub.type)}
</div>
<div className="flex">
<p className="f-1">
<p className="flex-1">
<FormattedMessage defaultMessage="Created" id="ORGv1Q" />
:&nbsp;
<time dateTime={created.toISOString()}>
@ -55,7 +55,7 @@ export default function SubscriptionCard({ sub }: { sub: Subscription }) {
</time>
</p>
{daysToExpire >= 1 && (
<p className="f-1">
<p className="flex-1">
<FormattedMessage defaultMessage="Expires" id="xhQMeQ" />
:&nbsp;
<time dateTime={expires.toISOString()}>
@ -70,7 +70,7 @@ export default function SubscriptionCard({ sub }: { sub: Subscription }) {
</p>
)}
{daysToExpire >= 0 && daysToExpire < 1 && (
<p className="f-1">
<p className="flex-1">
<FormattedMessage defaultMessage="Expires" id="xhQMeQ" />
:&nbsp;
<time dateTime={expires.toISOString()}>
@ -85,12 +85,12 @@ export default function SubscriptionCard({ sub }: { sub: Subscription }) {
</p>
)}
{isExpired && (
<p className="f-1 error">
<p className="flex-1 error">
<FormattedMessage defaultMessage="Expired" id="RahCRH" />
</p>
)}
{isNew && (
<p className="f-1">
<p className="flex-1">
<FormattedMessage defaultMessage="Unpaid" id="6uMqL1" />
</p>
)}

View File

@ -23,7 +23,10 @@ export default class LNDHubWallet implements LNWallet {
password: string;
auth?: AuthResponse;
constructor(url: string) {
constructor(
url: string,
readonly changed: () => void,
) {
if (url.startsWith("lndhub://")) {
const regex = /^lndhub:\/\/([\S-]+):([\S-]+)@(.*)$/i;
const parsedUrl = url.match(regex);
@ -60,25 +63,31 @@ export default class LNDHubWallet implements LNWallet {
}
async getInfo() {
await this.login();
return await this.getJson<WalletInfo>("GET", "/getinfo");
}
async login() {
if (this.auth) return true;
const rsp = await this.getJson<AuthResponse>("POST", "/auth?type=auth", {
login: this.user,
password: this.password,
});
this.auth = rsp as AuthResponse;
this.changed();
return true;
}
async getBalance(): Promise<Sats> {
await this.login();
const rsp = await this.getJson<GetBalanceResponse>("GET", "/balance");
const bal = Math.floor((rsp as GetBalanceResponse).BTC.AvailableBalance);
return bal as Sats;
}
async createInvoice(req: InvoiceRequest) {
await this.login();
const rsp = await this.getJson<UserInvoicesResponse>("POST", "/addinvoice", {
amt: req.amount,
memo: req.memo,
@ -95,6 +104,7 @@ export default class LNDHubWallet implements LNWallet {
}
async payInvoice(pr: string) {
await this.login();
const rsp = await this.getJson<PayInvoiceResponse>("POST", "/payinvoice", {
invoice: pr,
});
@ -113,6 +123,7 @@ export default class LNDHubWallet implements LNWallet {
}
async getInvoices(): Promise<WalletInvoice[]> {
await this.login();
const rsp = await this.getJson<UserInvoicesResponse[]>("GET", "/getuserinvoices");
return (rsp as UserInvoicesResponse[])
.sort((a, b) => (a.timestamp > b.timestamp ? -1 : 1))

View File

@ -81,7 +81,10 @@ export class NostrConnectWallet implements LNWallet {
#info?: WalletInfo;
#supported_methods: Array<string> = DefaultSupported;
constructor(cfg: string) {
constructor(
cfg: string,
readonly changed: () => void,
) {
this.#config = NostrConnectWallet.parseConfigUrl(cfg);
this.#commandQueue = new Map();
}
@ -147,9 +150,9 @@ export class NostrConnectWallet implements LNWallet {
async login() {
if (this.#conn) return true;
return await new Promise<boolean>(resolve => {
await new Promise<void>(resolve => {
this.#conn = new Connection(this.#config.relayUrl, { read: true, write: true });
this.#conn.on("connected", () => resolve(true));
this.#conn.on("connected", () => resolve());
this.#conn.on("auth", async (c, r, cb) => {
const eb = new EventBuilder();
eb.kind(EventKind.Auth).tag(["relay", r]).tag(["challenge", c]);
@ -161,6 +164,9 @@ export class NostrConnectWallet implements LNWallet {
});
this.#conn.Connect();
});
await this.getInfo();
this.changed();
return true;
}
async close() {
@ -232,6 +238,7 @@ export class NostrConnectWallet implements LNWallet {
timestamp: typeof a.created_at === "string" ? new Date(a.created_at).getTime() / 1000 : a.created_at,
preimage: a.preimage,
state: WalletInvoiceState.Paid,
direction: a.type === "incoming" ? "in" : "out",
}) as WalletInvoice,
) ?? []
);

View File

@ -80,6 +80,7 @@ export interface WalletInvoice {
timestamp: number;
preimage?: string;
state: WalletInvoiceState;
direction: "in" | "out";
}
export function prToWalletInvoice(pr: string) {
@ -92,6 +93,7 @@ export function prToWalletInvoice(pr: string) {
timestamp: parsedInvoice.timestamp ?? 0,
state: parsedInvoice.expired ? WalletInvoiceState.Expired : WalletInvoiceState.Pending,
pr,
direction: "in",
} as WalletInvoice;
}
}
@ -163,6 +165,9 @@ export class WalletStore extends ExternalStore<WalletStoreSnapshot> {
this.notifyChange();
});
return undefined;
} else {
this.#instance.set(activeConfig.id, w);
this.notifyChange();
}
return w;
} else {
@ -230,10 +235,10 @@ export class WalletStore extends ExternalStore<WalletStoreSnapshot> {
return new WebLNWallet();
}
case WalletKind.LNDHub: {
return new LNDHubWallet(unwrap(cfg.data));
return new LNDHubWallet(unwrap(cfg.data), () => this.notifyChange());
}
case WalletKind.NWC: {
return new NostrConnectWallet(unwrap(cfg.data));
return new NostrConnectWallet(unwrap(cfg.data), () => this.notifyChange());
}
}
}

View File

@ -454,5 +454,12 @@
<symbol id="medical-cross" viewBox="0 0 24 24" fill="none">
<path d="M15 4.6C15 4.03995 15 3.75992 14.891 3.54601C14.7951 3.35785 14.6422 3.20487 14.454 3.10899C14.2401 3 13.9601 3 13.4 3H10.6C10.0399 3 9.75992 3 9.54601 3.10899C9.35785 3.20487 9.20487 3.35785 9.10899 3.54601C9 3.75992 9 4.03995 9 4.6V7.4C9 7.96005 9 8.24008 8.89101 8.45399C8.79513 8.64215 8.64215 8.79513 8.45399 8.89101C8.24008 9 7.96005 9 7.4 9H4.6C4.03995 9 3.75992 9 3.54601 9.10899C3.35785 9.20487 3.20487 9.35785 3.10899 9.54601C3 9.75992 3 10.0399 3 10.6V13.4C3 13.9601 3 14.2401 3.10899 14.454C3.20487 14.6422 3.35785 14.7951 3.54601 14.891C3.75992 15 4.03995 15 4.6 15H7.4C7.96005 15 8.24008 15 8.45399 15.109C8.64215 15.2049 8.79513 15.3578 8.89101 15.546C9 15.7599 9 16.0399 9 16.6V19.4C9 19.9601 9 20.2401 9.10899 20.454C9.20487 20.6422 9.35785 20.7951 9.54601 20.891C9.75992 21 10.0399 21 10.6 21H13.4C13.9601 21 14.2401 21 14.454 20.891C14.6422 20.7951 14.7951 20.6422 14.891 20.454C15 20.2401 15 19.9601 15 19.4V16.6C15 16.0399 15 15.7599 15.109 15.546C15.2049 15.3578 15.3578 15.2049 15.546 15.109C15.7599 15 16.0399 15 16.6 15H19.4C19.9601 15 20.2401 15 20.454 14.891C20.6422 14.7951 20.7951 14.6422 20.891 14.454C21 14.2401 21 13.9601 21 13.4V10.6C21 10.0399 21 9.75992 20.891 9.54601C20.7951 9.35785 20.6422 9.20487 20.454 9.10899C20.2401 9 19.9601 9 19.4 9L16.6 9C16.0399 9 15.7599 9 15.546 8.89101C15.3578 8.79513 15.2049 8.64215 15.109 8.45399C15 8.24008 15 7.96005 15 7.4V4.6Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</symbol>
<symbol id="arrow-up-right" viewBox="0 0 24 24" fill="none">
<path d="M6 18L18 6M18 6H10M18 6V14" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</symbol>
<symbol id="sats" viewBox="0 0 24 25" fill="none">
<path fill-rule="evenodd" clip-rule="evenodd" d="M21 12.5C21 13.6819 20.7672 14.8522 20.3149 15.9442C19.8626 17.0361 19.1997 18.0282 18.364 18.864C17.5282 19.6997 16.5361 20.3626 15.4442 20.8149C14.3522 21.2672 13.1819 21.5 12 21.5C10.8181 21.5 9.64778 21.2672 8.55585 20.8149C7.46392 20.3626 6.47177 19.6997 5.63604 18.864C4.80031 18.0282 4.13738 17.0361 3.68508 15.9442C3.23279 14.8522 3 13.6819 3 12.5C3 10.1131 3.94821 7.82387 5.63604 6.13604C7.32387 4.44821 9.61305 3.5 12 3.5C14.3869 3.5 16.6761 4.44821 18.364 6.13604C20.0518 7.82387 21 10.1131 21 12.5ZM8.693 9.242L16.33 11.305L16.667 9.843L9.029 7.78L8.693 9.242ZM14.219 6.192L13.813 7.966L12.365 7.574L12.772 5.8L14.219 6.192ZM11.227 19.2L11.635 17.426L10.187 17.035L9.779 18.809L11.227 19.2ZM15.648 14.266L8.011 12.2L8.347 10.738L15.984 12.804L15.648 14.266ZM7.332 15.156L14.97 17.22L15.306 15.758L7.668 13.694L7.332 15.156Z" fill="currentColor"/>
</symbol>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 128 KiB

After

Width:  |  Height:  |  Size: 129 KiB

View File

@ -480,22 +480,6 @@ input:disabled {
cursor: not-allowed;
}
.f-1 {
flex: 1;
}
.f-2 {
flex: 2;
}
.f-3 {
flex: 3;
}
.f-4 {
flex: 4;
}
.f-ellipsis {
min-width: 0;
white-space: nowrap;

View File

@ -64,6 +64,7 @@ import { setupWebLNWalletConfig } from "@/Wallet/WebLN";
import { Wallets } from "@/Wallet";
import Fuse from "fuse.js";
import NetworkGraph from "@/Pages/NetworkGraph";
import WalletPage from "./Pages/WalletPage";
declare global {
interface Window {
@ -279,6 +280,14 @@ const mainRoutes = [
path: "/graph",
element: <NetworkGraph />,
},
{
path: "/wallet",
element: (
<div className="p">
<WalletPage showHistory={true} />
</div>
),
},
...OnboardingRoutes,
...SettingsRoutes,
] as Array<RouteObject>;

View File

@ -156,6 +156,9 @@
"3KNMbJ": {
"defaultMessage": "Articles"
},
"3QwfJR": {
"defaultMessage": "~{amount}"
},
"3cc4Ct": {
"defaultMessage": "Light"
},
@ -455,6 +458,9 @@
"Dx4ey3": {
"defaultMessage": "Toggle all"
},
"E5ZIPD": {
"defaultMessage": "<big>{amount}</big> <small>sats</small>"
},
"EJbFi7": {
"defaultMessage": "Search notes"
},
@ -945,9 +951,6 @@
"VL900k": {
"defaultMessage": "Recommended Relays"
},
"VN0+Fz": {
"defaultMessage": "Balance: {amount} sats"
},
"VOjC1i": {
"defaultMessage": "Pick which upload service you want to upload attachments to"
},
@ -1130,10 +1133,6 @@
"d2ebEu": {
"defaultMessage": "Not Subscribed to Push"
},
"d6CyG5": {
"defaultMessage": "History",
"description": "Wallet transation history"
},
"d7d0/x": {
"defaultMessage": "LN Address"
},
@ -1468,6 +1467,10 @@
"puLNUJ": {
"defaultMessage": "Pin"
},
"pukxg/": {
"defaultMessage": "Payments",
"description": "Wallet transation history"
},
"pzTOmv": {
"defaultMessage": "Followers"
},
@ -1558,6 +1561,9 @@
"thnRpU": {
"defaultMessage": "Getting NIP-05 verified can help:"
},
"tj6kdX": {
"defaultMessage": "{sign} {amount} sats"
},
"tjpYlr": {
"defaultMessage": "Relay Metrics"
},
@ -1618,9 +1624,6 @@
"voxBKC": {
"defaultMessage": "Followed by friends"
},
"vrTOHJ": {
"defaultMessage": "{amount} sats"
},
"vxwnbh": {
"defaultMessage": "Amount of work to apply to all published events"
},

View File

@ -51,6 +51,7 @@
"2zJXeA": "Profiles",
"39AHJm": "Sign Up",
"3KNMbJ": "Articles",
"3QwfJR": "~{amount}",
"3cc4Ct": "Light",
"3gOsZq": "Translators",
"3qnJlS": "You are voting with {amount} sats",
@ -150,6 +151,7 @@
"DrZqav": "About must be less than {limit} characters",
"DtYelJ": "Transfer",
"Dx4ey3": "Toggle all",
"E5ZIPD": "<big>{amount}</big> <small>sats</small>",
"EJbFi7": "Search notes",
"ELbg9p": "Data Providers",
"EQKRE4": "Show badges on profile pages",
@ -311,7 +313,6 @@
"UrKTqQ": "You have an active iris.to account",
"UxgyeY": "Your referral code is {code}",
"VL900k": "Recommended Relays",
"VN0+Fz": "Balance: {amount} sats",
"VOjC1i": "Pick which upload service you want to upload attachments to",
"VR5eHw": "Public key (npub/nprofile)",
"VcwrfF": "Yes please",
@ -372,7 +373,6 @@
"cyR7Kh": "Back",
"d+6YsV": "Lists to mute:",
"d2ebEu": "Not Subscribed to Push",
"d6CyG5": "History",
"d7d0/x": "LN Address",
"d8gpCh": "Try to use less than 5 hashtags to stay on topic 🙏",
"dOQCL8": "Display name",
@ -484,6 +484,7 @@
"pI+77w": "Downloadable backups from Snort relay",
"pRess9": "ZapPool",
"puLNUJ": "Pin",
"pukxg/": "Payments",
"pzTOmv": "Followers",
"qD9EUF": "Email <> DM bridge for your Snort nostr address",
"qDwvZ4": "Unknown error",
@ -514,6 +515,7 @@
"tOdNiY": "Dark",
"th5lxp": "Send note to a subset of your write relays",
"thnRpU": "Getting NIP-05 verified can help:",
"tj6kdX": "{sign} {amount} sats",
"tjpYlr": "Relay Metrics",
"ttxS0b": "Supporter Badge",
"u+LyXc": "Interactions",
@ -534,7 +536,6 @@
"vhlWFg": "Poll Options",
"vlbWtt": "Get a free one",
"voxBKC": "Followed by friends",
"vrTOHJ": "{amount} sats",
"vxwnbh": "Amount of work to apply to all published events",
"w1Fanr": "Business",
"w6qrwX": "NSFW",