mirror of
https://github.com/aljazceru/mutiny-web.git
synced 2025-12-17 22:34:23 +01:00
rc5
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
import { loadHome, visitSettings } from "./utils";
|
||||
import { loadHome } from "./utils";
|
||||
|
||||
const SIGNET_INVITE_CODE =
|
||||
"fed11qgqzc2nhwden5te0vejkg6tdd9h8gepwvejkg6tdd9h8garhduhx6at5d9h8jmn9wshxxmmd9uqqzgxg6s3evnr6m9zdxr6hxkdkukexpcs3mn7mj3g5pc5dfh63l4tj6g9zk4er";
|
||||
@@ -11,10 +11,9 @@ test.beforeEach(async ({ page }) => {
|
||||
|
||||
test("fedmint join, receive, send", async ({ page }) => {
|
||||
await loadHome(page);
|
||||
await visitSettings(page);
|
||||
|
||||
// Click "Manage Federations" link
|
||||
await page.click("text=Manage Federations");
|
||||
// Click "Join a federation" cta
|
||||
await page.click("text=Join a federation");
|
||||
|
||||
// Fill the input with the federation code
|
||||
await page.fill("input[name='federation_code']", SIGNET_INVITE_CODE);
|
||||
@@ -29,7 +28,6 @@ test("fedmint join, receive, send", async ({ page }) => {
|
||||
|
||||
// Navigate back home
|
||||
await page.goBack();
|
||||
await page.goBack();
|
||||
|
||||
// Click the top left button (it's the profile button), a child of header
|
||||
// TODO: better ARIA stuff
|
||||
|
||||
@@ -26,8 +26,7 @@ const settingsRoutes = [
|
||||
"/plus",
|
||||
"/restore",
|
||||
"/servers",
|
||||
"/syncnostrcontacts",
|
||||
"/federations"
|
||||
"/nostrkeys"
|
||||
];
|
||||
|
||||
const settingsRoutesPrefixed = settingsRoutes.map((route) => {
|
||||
@@ -96,31 +95,8 @@ test("visit each route", async ({ page }) => {
|
||||
await checkRoute(page, "/settings/servers", "Servers", checklist);
|
||||
await page.goBack();
|
||||
|
||||
// Connections
|
||||
await checkRoute(
|
||||
page,
|
||||
"/settings/connections",
|
||||
"Wallet Connections",
|
||||
checklist
|
||||
);
|
||||
await page.goBack();
|
||||
|
||||
// Sync Nostr Contacts
|
||||
await checkRoute(
|
||||
page,
|
||||
"/settings/syncnostrcontacts",
|
||||
"Sync Nostr Contacts",
|
||||
checklist
|
||||
);
|
||||
await page.goBack();
|
||||
|
||||
// Manage Federations
|
||||
await checkRoute(
|
||||
page,
|
||||
"/settings/federations",
|
||||
"Manage Federations",
|
||||
checklist
|
||||
);
|
||||
await checkRoute(page, "/settings/nostrkeys", "Nostr Keys", checklist);
|
||||
await page.goBack();
|
||||
|
||||
// Emergency Kit
|
||||
|
||||
12
package.json
12
package.json
@@ -52,19 +52,19 @@
|
||||
"@capacitor/share": "^5.0.6",
|
||||
"@capacitor/status-bar": "^5.0.6",
|
||||
"@capacitor/toast": "^5.0.6",
|
||||
"@kobalte/core": "^0.12.1",
|
||||
"@kobalte/core": "^0.12.6",
|
||||
"@kobalte/tailwindcss": "^0.9.0",
|
||||
"@mutinywallet/mutiny-wasm": "0.6.0-rc3",
|
||||
"@mutinywallet/mutiny-wasm": "0.6.0-rc5",
|
||||
"@modular-forms/solid": "^0.20.0",
|
||||
"@solid-primitives/upload": "^0.0.111",
|
||||
"@solid-primitives/upload": "^0.0.117",
|
||||
"@solidjs/meta": "^0.29.3",
|
||||
"@solidjs/router": "^0.10.9",
|
||||
"@solidjs/router": "^0.13.1",
|
||||
"capacitor-secure-storage-plugin": "^0.9.0",
|
||||
"i18next": "^23.10.1",
|
||||
"i18next-browser-languagedetector": "^7.1.0",
|
||||
"lucide-solid": "^0.330.0",
|
||||
"lucide-solid": "^0.363.0",
|
||||
"qr-scanner": "^1.4.2",
|
||||
"solid-js": "^1.8.12",
|
||||
"solid-js": "^1.8.16",
|
||||
"solid-qr-code": "^0.0.8",
|
||||
"solid-transition-group": "^0.2.3"
|
||||
},
|
||||
|
||||
3278
pnpm-lock.yaml
generated
3278
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -45,7 +45,15 @@
|
||||
"profile": "Profile",
|
||||
"nostr_identity": "Nostr Identity",
|
||||
"add_lightning_address": "Add Lightning Address",
|
||||
"edit_profile": "Edit Profile"
|
||||
"edit_profile": "Edit Profile",
|
||||
"join_federation": "Join a federation",
|
||||
"manage_federation": "Manage Federations",
|
||||
"federated_custody": "Federated Custody",
|
||||
"self_custody": "Self Custody",
|
||||
"social": "Social",
|
||||
"edit": {
|
||||
"nym": "Nym"
|
||||
}
|
||||
},
|
||||
"chat": {
|
||||
"prompt": "This is a new conversation. Try asking for money!",
|
||||
@@ -74,8 +82,7 @@
|
||||
"email_error": "That doesn't look like a lightning address",
|
||||
"npub_error": "That doesn't look like a nostr npub",
|
||||
"error_ln_address_missing": "New contacts need a lightning address",
|
||||
"npub": "Nostr Npub",
|
||||
"link_to_nostr_sync": "Import Nostr Contacts"
|
||||
"npub": "Nostr Npub"
|
||||
},
|
||||
"redeem": {
|
||||
"redeem_bitcoin": "Redeem Bitcoin",
|
||||
@@ -533,7 +540,7 @@
|
||||
"remove": "Remove",
|
||||
"expires": "Expires",
|
||||
"federation_id": "Federation ID",
|
||||
"description": "Mutiny has experimental support for the Fedimint protocol. You'll need a federation invite code to use this feature. These funds are currently not backed up remotely. Store funds in a federation at your own risk!",
|
||||
"description": "Mutiny has experimental support for the Fedimint protocol. You'll need a federation invite code to use this feature. Store funds in a federation at your own risk!",
|
||||
"learn_more": "Learn more about Fedimint."
|
||||
},
|
||||
"gift": {
|
||||
@@ -567,6 +574,15 @@
|
||||
"send_delete_confirm": "Are you sure you want to delete this gift? Is this your rugpull moment?",
|
||||
"send_tip": "Your copy of Mutiny Wallet needs to be open for the gift to be redeemed.",
|
||||
"need_plus": "Upgrade to Mutiny+ to enable gifting. Gifting allows you to create a Mutiny gift URL that can be claimed by anyone with a web browser."
|
||||
},
|
||||
"appearance": "Appearance",
|
||||
"social": "Social",
|
||||
"nostr_keys": {
|
||||
"title": "Nostr Keys",
|
||||
"caption": "Not your keys not your notes",
|
||||
"description": "You can use the same nostr profile across multiple apps.",
|
||||
"warning": "Be careful where you share your private nostr key!",
|
||||
"learn_more": "Learn more about nostr"
|
||||
}
|
||||
},
|
||||
"swap": {
|
||||
@@ -700,4 +716,4 @@
|
||||
"authenticated": "Authenticated!"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,8 +49,7 @@
|
||||
"email_error": "Eso no parece una dirección lightning",
|
||||
"npub_error": "Eso no parece un npub de nostr",
|
||||
"error_ln_address_missing": "Los contactos nuevos necesitan una dirección lightning",
|
||||
"npub": "Npub Nostr",
|
||||
"link_to_nostr_sync": "Importar Contactos de Nostr"
|
||||
"npub": "Npub Nostr"
|
||||
},
|
||||
"receive": {
|
||||
"receive_bitcoin": "Recibir Bitcoin",
|
||||
@@ -660,5 +659,8 @@
|
||||
"error": "Eso no funcionó por alguna razón.",
|
||||
"authenticated": "¡Autenticado!"
|
||||
}
|
||||
},
|
||||
"profile": {
|
||||
"manage_federation": "Manejar Federaciones"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,10 +149,12 @@ function OnchainHeader(props: { info: OnChainTx; kind?: HackActivityType }) {
|
||||
{props.kind === "ChannelOpen"
|
||||
? i18n.t("activity.transaction_details.channel_open")
|
||||
: props.kind === "ChannelClose"
|
||||
? i18n.t("activity.transaction_details.channel_close")
|
||||
: isSend()
|
||||
? i18n.t("activity.transaction_details.onchain_send")
|
||||
: i18n.t("activity.transaction_details.onchain_receive")}
|
||||
? i18n.t("activity.transaction_details.channel_close")
|
||||
: isSend()
|
||||
? i18n.t("activity.transaction_details.onchain_send")
|
||||
: i18n.t(
|
||||
"activity.transaction_details.onchain_receive"
|
||||
)}
|
||||
<Switch>
|
||||
<Match
|
||||
when={
|
||||
@@ -191,15 +193,29 @@ function OnchainHeader(props: { info: OnChainTx; kind?: HackActivityType }) {
|
||||
);
|
||||
}
|
||||
|
||||
export function MiniStringShower(props: { text: string }) {
|
||||
export function MiniStringShower(props: { text: string; hide?: boolean }) {
|
||||
const [copy, copied] = useCopy({ copiedTimeout: 1000 });
|
||||
|
||||
return (
|
||||
<div class="grid w-full grid-cols-[minmax(0,_1fr)_auto] gap-1">
|
||||
<TruncateMiddle text={props.text} />
|
||||
<Switch>
|
||||
<Match when={props.hide}>
|
||||
<input
|
||||
type="password"
|
||||
value={props.text}
|
||||
class="flex bg-transparent font-mono"
|
||||
readonly
|
||||
disabled
|
||||
/>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<TruncateMiddle text={props.text} />
|
||||
</Match>
|
||||
</Switch>
|
||||
|
||||
<button
|
||||
class="w-[1.5rem] p-1"
|
||||
classList={{ "bg-m-green rounded": copied() }}
|
||||
classList={{ "bg-m-red rounded": copied() }}
|
||||
onClick={() => copy(props.text)}
|
||||
>
|
||||
<Copy class="h-4 w-4" />
|
||||
@@ -302,8 +318,8 @@ function OnchainDetails(props: {
|
||||
await (state.mutiny_wallet?.list_channels() as Promise<
|
||||
MutinyChannel[]
|
||||
>);
|
||||
const channel = channels.find(
|
||||
(channel) => channel.outpoint?.startsWith(props.info.txid)
|
||||
const channel = channels.find((channel) =>
|
||||
channel.outpoint?.startsWith(props.info.txid)
|
||||
);
|
||||
return channel;
|
||||
} catch (e) {
|
||||
@@ -492,9 +508,8 @@ export function ActivityDetailsModal(props: {
|
||||
try {
|
||||
if (kind() === "Lightning") {
|
||||
console.debug("reading invoice: ", id());
|
||||
const invoice = await state.mutiny_wallet?.get_invoice_by_hash(
|
||||
id()
|
||||
);
|
||||
const invoice =
|
||||
await state.mutiny_wallet?.get_invoice_by_hash(id());
|
||||
return invoice;
|
||||
} else if (kind() === "ChannelClose") {
|
||||
console.debug("reading channel close: ", id());
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import { A } from "@solidjs/router";
|
||||
import { Shuffle } from "lucide-solid";
|
||||
import { A, useNavigate } from "@solidjs/router";
|
||||
import { Shuffle, Users } from "lucide-solid";
|
||||
import { Match, Show, Switch } from "solid-js";
|
||||
|
||||
import {
|
||||
AmountFiat,
|
||||
AmountSats,
|
||||
ButtonCard,
|
||||
FancyCard,
|
||||
Indicator,
|
||||
InfoBox,
|
||||
MediumHeader,
|
||||
NiceP,
|
||||
VStack
|
||||
} from "~/components";
|
||||
import { useI18n } from "~/i18n/context";
|
||||
@@ -45,6 +48,7 @@ const STYLE =
|
||||
|
||||
export function BalanceBox(props: { loading?: boolean; small?: boolean }) {
|
||||
const [state, _actions] = useMegaStore();
|
||||
const navigate = useNavigate();
|
||||
const i18n = useI18n();
|
||||
|
||||
const totalOnchain = () =>
|
||||
@@ -57,115 +61,151 @@ export function BalanceBox(props: { loading?: boolean; small?: boolean }) {
|
||||
|
||||
return (
|
||||
<VStack>
|
||||
<FancyCard title="Lightning">
|
||||
<Show when={!props.loading} fallback={<LoadingShimmer />}>
|
||||
<Switch>
|
||||
<Match when={state.safe_mode}>
|
||||
<div class="flex flex-col gap-1">
|
||||
<InfoBox accent="red">
|
||||
{i18n.t("common.error_safe_mode")}
|
||||
</InfoBox>
|
||||
</div>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="text-2xl">
|
||||
<AmountSats
|
||||
amountSats={
|
||||
state.balance?.lightning || 0
|
||||
<Switch>
|
||||
<Match when={state.federations && state.federations.length}>
|
||||
<div>
|
||||
<MediumHeader>Fedimint</MediumHeader>
|
||||
<FancyCard>
|
||||
<Show
|
||||
when={!props.loading}
|
||||
fallback={<LoadingShimmer />}
|
||||
>
|
||||
<div class="flex justify-between">
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="text-2xl">
|
||||
<AmountSats
|
||||
amountSats={
|
||||
state.balance?.federation ||
|
||||
0n
|
||||
}
|
||||
icon="community"
|
||||
denominationSize="lg"
|
||||
isFederation
|
||||
/>
|
||||
</div>
|
||||
<div class="text-lg text-white/70">
|
||||
<AmountFiat
|
||||
amountSats={
|
||||
state.balance?.federation ||
|
||||
0n
|
||||
}
|
||||
denominationSize="sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Show
|
||||
when={
|
||||
state.balance?.federation || 0n > 0n
|
||||
}
|
||||
icon="lightning"
|
||||
denominationSize="lg"
|
||||
/>
|
||||
>
|
||||
<div class="self-end justify-self-end">
|
||||
<A
|
||||
href="/swaplightning"
|
||||
class={STYLE}
|
||||
>
|
||||
<Shuffle class="h-6 w-6" />
|
||||
</A>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
<div class="text-lg text-white/70">
|
||||
<AmountFiat
|
||||
amountSats={
|
||||
state.balance?.lightning || 0
|
||||
}
|
||||
denominationSize="sm"
|
||||
/>
|
||||
</Show>
|
||||
</FancyCard>
|
||||
</div>
|
||||
<ButtonCard
|
||||
onClick={() => navigate("/settings/federations")}
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<Users class="inline-block text-m-red" />
|
||||
<NiceP>{i18n.t("profile.manage_federation")}</NiceP>
|
||||
</div>
|
||||
</ButtonCard>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<ButtonCard
|
||||
onClick={() => navigate("/settings/federations")}
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<Users class="inline-block text-m-red" />
|
||||
<NiceP>{i18n.t("profile.join_federation")}</NiceP>
|
||||
</div>
|
||||
</ButtonCard>
|
||||
</Match>
|
||||
</Switch>
|
||||
<div>
|
||||
<MediumHeader>{i18n.t("profile.self_custody")}</MediumHeader>
|
||||
<FancyCard>
|
||||
<Show when={!props.loading} fallback={<LoadingShimmer />}>
|
||||
<Switch>
|
||||
<Match when={state.safe_mode}>
|
||||
<div class="flex flex-col gap-1">
|
||||
<InfoBox accent="red">
|
||||
{i18n.t("common.error_safe_mode")}
|
||||
</InfoBox>
|
||||
</div>
|
||||
</div>
|
||||
</Match>
|
||||
</Switch>
|
||||
</Show>
|
||||
</FancyCard>
|
||||
<Show when={state.federations && state.federations.length}>
|
||||
<FancyCard title="Fedimint">
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="text-2xl">
|
||||
<AmountSats
|
||||
amountSats={
|
||||
state.balance?.lightning || 0
|
||||
}
|
||||
icon="lightning"
|
||||
denominationSize="lg"
|
||||
/>
|
||||
</div>
|
||||
<div class="text-lg text-white/70">
|
||||
<AmountFiat
|
||||
amountSats={
|
||||
state.balance?.lightning || 0
|
||||
}
|
||||
denominationSize="sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Match>
|
||||
</Switch>
|
||||
</Show>
|
||||
<hr class="my-2 border-m-grey-750" />
|
||||
<Show when={!props.loading} fallback={<LoadingShimmer />}>
|
||||
<div class="flex justify-between">
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="text-2xl">
|
||||
<AmountSats
|
||||
amountSats={
|
||||
state.balance?.federation || 0n
|
||||
}
|
||||
icon="community"
|
||||
amountSats={totalOnchain()}
|
||||
icon="chain"
|
||||
denominationSize="lg"
|
||||
isFederation
|
||||
/>
|
||||
</div>
|
||||
<div class="text-lg text-white/70">
|
||||
<AmountFiat
|
||||
amountSats={
|
||||
state.balance?.federation || 0n
|
||||
}
|
||||
amountSats={totalOnchain()}
|
||||
denominationSize="sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Show when={state.balance?.federation || 0n > 0n}>
|
||||
<div class="self-end justify-self-end">
|
||||
<A href="/swaplightning" class={STYLE}>
|
||||
<Shuffle class="h-6 w-6" />
|
||||
</A>
|
||||
</div>
|
||||
</Show>
|
||||
<div class="flex flex-col items-end justify-between gap-1">
|
||||
<Show when={state.balance?.unconfirmed != 0n}>
|
||||
<Indicator>
|
||||
{i18n.t("common.pending")}
|
||||
</Indicator>
|
||||
</Show>
|
||||
<Show when={state.balance?.unconfirmed === 0n}>
|
||||
<div />
|
||||
</Show>
|
||||
<Show when={usableOnchain() > 0n}>
|
||||
<div class="self-end justify-self-end">
|
||||
<A href="/swap" class={STYLE}>
|
||||
<Shuffle class="h-6 w-6" />
|
||||
</A>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</FancyCard>
|
||||
</Show>
|
||||
<FancyCard title="On-chain">
|
||||
{/* <hr class="my-2 border-m-grey-750" /> */}
|
||||
<Show when={!props.loading} fallback={<LoadingShimmer />}>
|
||||
<div class="flex justify-between">
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="text-2xl">
|
||||
<AmountSats
|
||||
amountSats={totalOnchain()}
|
||||
icon="chain"
|
||||
denominationSize="lg"
|
||||
/>
|
||||
</div>
|
||||
<div class="text-lg text-white/70">
|
||||
<AmountFiat
|
||||
amountSats={totalOnchain()}
|
||||
denominationSize="sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col items-end justify-between gap-1">
|
||||
<Show when={state.balance?.unconfirmed != 0n}>
|
||||
<Indicator>
|
||||
{i18n.t("common.pending")}
|
||||
</Indicator>
|
||||
</Show>
|
||||
<Show when={state.balance?.unconfirmed === 0n}>
|
||||
<div />
|
||||
</Show>
|
||||
<Show when={usableOnchain() > 0n}>
|
||||
<div class="self-end justify-self-end">
|
||||
<A href="/swap" class={STYLE}>
|
||||
<Shuffle class="h-6 w-6" />
|
||||
</A>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</FancyCard>
|
||||
</div>
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,7 +8,10 @@ export function ContactButton(props: {
|
||||
onClick: () => void;
|
||||
}) {
|
||||
return (
|
||||
<button class="flex items-center gap-2" onClick={() => props.onClick()}>
|
||||
<button
|
||||
class="flex items-center gap-2 overflow-clip"
|
||||
onClick={() => props.onClick()}
|
||||
>
|
||||
<LabelCircle
|
||||
name={props.contact.name}
|
||||
image_url={props.contact.primal_image_url}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { SubmitHandler } from "@modular-forms/solid";
|
||||
import { A } from "@solidjs/router";
|
||||
import { createSignal, Match, Switch } from "solid-js";
|
||||
|
||||
import {
|
||||
@@ -61,15 +60,6 @@ export function ContactEditor(props: {
|
||||
cta={i18n.t("contacts.create_contact")}
|
||||
handleSubmit={handleSubmit}
|
||||
/>
|
||||
<A
|
||||
href="/settings/syncnostrcontacts"
|
||||
class="self-center font-semibold text-m-red no-underline active:text-m-red/80"
|
||||
state={{
|
||||
previous: location.pathname
|
||||
}}
|
||||
>
|
||||
{i18n.t("contacts.link_to_nostr_sync")}
|
||||
</A>
|
||||
</SimpleDialog>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import { createForm, email } from "@modular-forms/solid";
|
||||
import { createFileUploader } from "@solid-primitives/upload";
|
||||
import { Pencil } from "lucide-solid";
|
||||
import { createSignal, Match, Switch } from "solid-js";
|
||||
|
||||
import { Button, SimpleInput } from "~/components";
|
||||
import { Button, TextField, VStack } from "~/components";
|
||||
import { useI18n } from "~/i18n/context";
|
||||
import { useMegaStore } from "~/state/megaStore";
|
||||
import { blobToBase64 } from "~/utils";
|
||||
|
||||
export type EditableProfile = {
|
||||
nym?: string;
|
||||
lightningAddress?: string;
|
||||
imageUrl?: string;
|
||||
};
|
||||
|
||||
@@ -17,7 +21,6 @@ export function EditProfileForm(props: {
|
||||
cta: string;
|
||||
}) {
|
||||
const [state] = useMegaStore();
|
||||
const [nym, setNym] = createSignal(props.initialProfile?.nym || "");
|
||||
const [uploading, setUploading] = createSignal(false);
|
||||
|
||||
const { files, selectFiles } = createFileUploader({
|
||||
@@ -26,14 +29,20 @@ export function EditProfileForm(props: {
|
||||
});
|
||||
|
||||
async function uploadFile() {
|
||||
selectFiles(async (files) => {
|
||||
console.log("uploadFile");
|
||||
await selectFiles(async (files) => {
|
||||
if (files.length) {
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function onSave() {
|
||||
const i18n = useI18n();
|
||||
const [profileForm, { Form, Field }] = createForm<EditableProfile>({
|
||||
initialValues: { ...props.initialProfile }
|
||||
});
|
||||
|
||||
async function handleSubmit(profile: EditableProfile) {
|
||||
try {
|
||||
let imageUrl;
|
||||
if (files() && files().length) {
|
||||
@@ -46,7 +55,8 @@ export function EditProfileForm(props: {
|
||||
setUploading(false);
|
||||
}
|
||||
await props.onSave({
|
||||
nym: nym(),
|
||||
nym: profile.nym,
|
||||
lightningAddress: profile.lightningAddress,
|
||||
imageUrl: imageUrl ? imageUrl : props.initialProfile?.imageUrl
|
||||
});
|
||||
} catch (e) {
|
||||
@@ -56,33 +66,67 @@ export function EditProfileForm(props: {
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
class="shiny-button flex h-[8rem] w-[8rem] flex-none items-center justify-center self-center overflow-clip rounded-full bg-m-grey-800 text-5xl uppercase"
|
||||
onClick={uploadFile}
|
||||
>
|
||||
<Switch>
|
||||
<Match when={files() && files().length}>
|
||||
<img src={files()[0].source} />
|
||||
</Match>
|
||||
<Match when={props.initialProfile?.imageUrl}>
|
||||
<img src={props.initialProfile?.imageUrl} />
|
||||
</Match>
|
||||
<Match when={true}>+</Match>
|
||||
</Switch>
|
||||
</button>
|
||||
<SimpleInput
|
||||
value={nym()}
|
||||
onInput={(e) => setNym(e.currentTarget.value)}
|
||||
placeholder="Your name or nym"
|
||||
/>
|
||||
<VStack>
|
||||
<button class="relative self-center" onClick={uploadFile}>
|
||||
<div class="shiny-button flex h-[8rem] w-[8rem] flex-none items-center justify-center self-center overflow-clip rounded-full bg-m-grey-800 text-5xl uppercase">
|
||||
<Switch>
|
||||
<Match when={files() && files().length}>
|
||||
<img src={files()[0].source} />
|
||||
</Match>
|
||||
<Match when={props.initialProfile?.imageUrl}>
|
||||
<img src={props.initialProfile?.imageUrl} />
|
||||
</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
<div class="absolute top-0 flex h-[8rem] w-[8rem] items-center justify-center bg-m-grey-975/25">
|
||||
<Pencil />
|
||||
</div>
|
||||
</button>
|
||||
<VStack>
|
||||
<Form
|
||||
onSubmit={handleSubmit}
|
||||
class="mx-auto flex w-full flex-1 flex-col justify-around gap-4"
|
||||
>
|
||||
<Field name="nym">
|
||||
{(field, props) => (
|
||||
<TextField
|
||||
{...props}
|
||||
placeholder={i18n.t("contacts.placeholder")}
|
||||
value={field.value}
|
||||
error={field.error}
|
||||
label={i18n.t("profile.edit.nym")}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
<Field
|
||||
name="lightningAddress"
|
||||
validate={[email(i18n.t("contacts.email_error"))]}
|
||||
>
|
||||
{(field, props) => (
|
||||
<TextField
|
||||
{...props}
|
||||
placeholder="example@example.com"
|
||||
value={field.value}
|
||||
error={field.error}
|
||||
label={i18n.t("contacts.ln_address")}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
<Button
|
||||
layout="full"
|
||||
type="submit"
|
||||
loading={
|
||||
props.saving ||
|
||||
uploading() ||
|
||||
profileForm.submitting
|
||||
}
|
||||
>
|
||||
{props.cta}
|
||||
</Button>
|
||||
</Form>
|
||||
</VStack>
|
||||
</VStack>
|
||||
<div class="flex-1" />
|
||||
<Button
|
||||
layout="full"
|
||||
onClick={onSave}
|
||||
loading={props.saving || uploading()}
|
||||
>
|
||||
{props.cta}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,8 +6,11 @@ import { useMegaStore } from "~/state/megaStore";
|
||||
export function HomeBalance() {
|
||||
const [state, actions] = useMegaStore();
|
||||
|
||||
const lightningPlusFedi = () =>
|
||||
(state.balance?.federation || 0n) + (state.balance?.lightning || 0n);
|
||||
const combinedBalance = () =>
|
||||
(state.balance?.federation || 0n) +
|
||||
(state.balance?.lightning || 0n) +
|
||||
(state.balance?.confirmed || 0n) +
|
||||
(state.balance?.unconfirmed || 0n);
|
||||
|
||||
// TODO: do some sort of status indicator
|
||||
// const fullyReady = () => state.load_stage === "done" && state.price !== 0;
|
||||
@@ -31,14 +34,14 @@ export function HomeBalance() {
|
||||
<Switch>
|
||||
<Match when={state.balanceView === "sats"}>
|
||||
<AmountSats
|
||||
amountSats={lightningPlusFedi()}
|
||||
amountSats={combinedBalance()}
|
||||
icon="lightning"
|
||||
denominationSize="lg"
|
||||
/>
|
||||
</Match>
|
||||
<Match when={state.balanceView === "fiat"}>
|
||||
<AmountFiat
|
||||
amountSats={lightningPlusFedi()}
|
||||
amountSats={combinedBalance()}
|
||||
denominationSize="lg"
|
||||
/>
|
||||
</Match>
|
||||
|
||||
46
src/components/ImportNsecForm.tsx
Normal file
46
src/components/ImportNsecForm.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import { MutinyWallet } from "@mutinywallet/mutiny-wasm";
|
||||
import { SecureStoragePlugin } from "capacitor-secure-storage-plugin";
|
||||
import { createSignal, Show } from "solid-js";
|
||||
|
||||
import { Button, InfoBox, SimpleInput } from "~/components";
|
||||
|
||||
export function ImportNsecForm() {
|
||||
const [nsec, setNsec] = createSignal("");
|
||||
const [saving, setSaving] = createSignal(false);
|
||||
const [error, setError] = createSignal<string | undefined>();
|
||||
|
||||
async function saveNsec() {
|
||||
setSaving(true);
|
||||
setError(undefined);
|
||||
const trimmedNsec = nsec().trim();
|
||||
try {
|
||||
const npub = await MutinyWallet.nsec_to_npub(trimmedNsec);
|
||||
if (!npub) {
|
||||
throw new Error("Invalid nsec");
|
||||
}
|
||||
await SecureStoragePlugin.set({ key: "nsec", value: trimmedNsec });
|
||||
// TODO: right now we need a reload to set the nsec
|
||||
window.location.href = "/";
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setError("Invalid nsec");
|
||||
}
|
||||
setSaving(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<SimpleInput
|
||||
value={nsec()}
|
||||
onInput={(e) => setNsec(e.currentTarget.value)}
|
||||
placeholder={`Nostr private key (starts with "nsec")`}
|
||||
/>
|
||||
<Button layout="full" onClick={saveNsec} loading={saving()}>
|
||||
Import
|
||||
</Button>
|
||||
<Show when={error()}>
|
||||
<InfoBox accent="red">{error()}</InfoBox>
|
||||
</Show>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -55,8 +55,8 @@ export function LabelCircle(props: {
|
||||
props.contact && props.name && props.name.length
|
||||
? props.name[0]
|
||||
: props.label
|
||||
? "≡"
|
||||
: "?";
|
||||
? "≡"
|
||||
: "?";
|
||||
const bg = () => (props.name && props.contact ? gradient() : "");
|
||||
|
||||
const [errored, setErrored] = createSignal(false);
|
||||
@@ -64,7 +64,11 @@ export function LabelCircle(props: {
|
||||
return (
|
||||
<Circle
|
||||
background={props.image_url && !errored() ? "none" : bg()}
|
||||
onClick={() => props.onClick && props.onClick()}
|
||||
onClick={
|
||||
props.onClick
|
||||
? () => props.onClick && props.onClick()
|
||||
: undefined
|
||||
}
|
||||
size={props.size}
|
||||
>
|
||||
<Switch>
|
||||
|
||||
@@ -113,8 +113,8 @@ export function NWCEditor(props: {
|
||||
const mode: "createnwa" | "createnwc" | "editnwc" = nwa()
|
||||
? "createnwa"
|
||||
: props.initialProfileIndex
|
||||
? "editnwc"
|
||||
: "createnwc";
|
||||
? "editnwc"
|
||||
: "createnwc";
|
||||
return mode;
|
||||
});
|
||||
|
||||
|
||||
@@ -137,8 +137,8 @@ export function NostrActivity() {
|
||||
zap.kind === "anonymous"
|
||||
? i18n.t("activity.anonymous")
|
||||
: zap.kind === "private"
|
||||
? i18n.t("activity.private")
|
||||
: nameFromHexpub(zap.from_hexpub)
|
||||
? i18n.t("activity.private")
|
||||
: nameFromHexpub(zap.from_hexpub)
|
||||
}
|
||||
primaryOnClick={() => {
|
||||
newContactFromHexpub(zap.from_hexpub);
|
||||
|
||||
@@ -34,7 +34,11 @@ function ShareButton(props: { receiveString: string; whiteBg?: boolean }) {
|
||||
);
|
||||
}
|
||||
|
||||
export function TruncateMiddle(props: { text: string; whiteBg?: boolean }) {
|
||||
export function TruncateMiddle(props: {
|
||||
text: string;
|
||||
whiteBg?: boolean;
|
||||
hidden?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
class="flex font-mono"
|
||||
|
||||
@@ -56,3 +56,4 @@ export * from "./ContactButton";
|
||||
export * from "./GenericItem";
|
||||
export * from "./HomeBalance";
|
||||
export * from "./EditProfileForm";
|
||||
export * from "./ImportNsecForm";
|
||||
|
||||
@@ -33,8 +33,8 @@ export function BackPop(props: { default: string; title?: string }) {
|
||||
props.title !== undefined
|
||||
? props.title
|
||||
: backPath() === "/"
|
||||
? i18n.t("common.home")
|
||||
: i18n.t("common.back")
|
||||
? i18n.t("common.home")
|
||||
: i18n.t("common.back")
|
||||
}
|
||||
onClick={() => navigate(backPath())}
|
||||
showOnDesktop
|
||||
|
||||
@@ -228,6 +228,16 @@ export const LargeHeader: ParentComponent<{
|
||||
);
|
||||
};
|
||||
|
||||
export const MediumHeader: ParentComponent = (props) => {
|
||||
return (
|
||||
<header class="mt-2">
|
||||
<h2 class="text-xl font-semibold text-m-grey-350">
|
||||
{props.children}
|
||||
</h2>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
export const VStack: ParentComponent<{
|
||||
biggap?: boolean;
|
||||
smallgap?: boolean;
|
||||
|
||||
@@ -36,8 +36,8 @@ export function toParsedParams(
|
||||
const network = !params.network
|
||||
? ourNetwork
|
||||
: params.network === "testnet" && ourNetwork === "signet"
|
||||
? "signet"
|
||||
: params.network;
|
||||
? "signet"
|
||||
: params.network;
|
||||
|
||||
if (network !== ourNetwork) {
|
||||
return {
|
||||
|
||||
@@ -16,11 +16,11 @@ body {
|
||||
/* After load we need to remove the bg so the qr scanner can show through */
|
||||
@apply text-white;
|
||||
@apply !bg-transparent;
|
||||
@apply mx-auto flex w-full max-w-[600px] flex-1 flex-col safe-top safe-left safe-right safe-bottom min-h-device h-device;
|
||||
@apply flex w-full flex-1 flex-col safe-top safe-left safe-right safe-bottom min-h-device h-device;
|
||||
}
|
||||
|
||||
#root {
|
||||
@apply flex flex-1 flex-col;
|
||||
@apply mx-auto flex w-full max-w-[600px] flex-1 flex-col;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
|
||||
@@ -49,11 +49,11 @@ import {
|
||||
Gift,
|
||||
Language,
|
||||
ManageFederations,
|
||||
NostrKeys,
|
||||
Plus,
|
||||
Restore,
|
||||
Servers,
|
||||
Settings,
|
||||
SyncNostrContacts
|
||||
Settings
|
||||
} from "~/routes/settings";
|
||||
import { Provider as MegaStoreProvider, useMegaStore } from "~/state/megaStore";
|
||||
|
||||
@@ -187,10 +187,7 @@ export function Router() {
|
||||
<Route path="/plus" component={Plus} />
|
||||
<Route path="/restore" component={Restore} />
|
||||
<Route path="/servers" component={Servers} />
|
||||
<Route
|
||||
path="/syncnostrcontacts"
|
||||
component={SyncNostrContacts}
|
||||
/>
|
||||
<Route path="/nostrkeys" component={NostrKeys} />
|
||||
<Route path="/federations" component={ManageFederations} />
|
||||
</Route>
|
||||
<Route path="/*all" component={NotFound} />
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import { TagItem } from "@mutinywallet/mutiny-wasm";
|
||||
import { createAsync, useNavigate, useParams } from "@solidjs/router";
|
||||
import { ArrowDownLeft, ArrowUpRight, MessagesSquare, Zap } from "lucide-solid";
|
||||
import {
|
||||
ArrowDownLeft,
|
||||
ArrowUpRight,
|
||||
Check,
|
||||
MessagesSquare,
|
||||
X,
|
||||
Zap
|
||||
} from "lucide-solid";
|
||||
import {
|
||||
createEffect,
|
||||
createResource,
|
||||
@@ -310,6 +317,36 @@ function FixedChatHeader(props: {
|
||||
navigate("/search");
|
||||
}
|
||||
|
||||
const [updatingFollowStatus, setUpdatingFollowStatus] = createSignal(false);
|
||||
|
||||
async function followContact() {
|
||||
setUpdatingFollowStatus(true);
|
||||
|
||||
try {
|
||||
if (!props.contact.npub) throw new Error("No npub");
|
||||
await state.mutiny_wallet?.follow_npub(props.contact.npub);
|
||||
props.refetch();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showToast(eify(e));
|
||||
}
|
||||
setUpdatingFollowStatus(false);
|
||||
}
|
||||
|
||||
async function unfollowContact() {
|
||||
setUpdatingFollowStatus(true);
|
||||
|
||||
try {
|
||||
if (!props.contact.npub) throw new Error("No npub");
|
||||
await state.mutiny_wallet?.unfollow_npub(props.contact.npub);
|
||||
props.refetch();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showToast(eify(e));
|
||||
}
|
||||
setUpdatingFollowStatus(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="fixed top-0 z-50 flex w-full max-w-[600px] flex-col gap-2 bg-m-grey-975/70 px-4 py-4 backdrop-blur-lg">
|
||||
<div class="backgrop-blur-lg z-50 bg-m-grey-975/70 safe-top" />
|
||||
@@ -341,13 +378,39 @@ function FixedChatHeader(props: {
|
||||
<ArrowUpRight class="inline-block" />
|
||||
<span>Send</span>
|
||||
</button>
|
||||
<button
|
||||
class="flex gap-2 font-semibold text-m-blue"
|
||||
onClick={() => props.requestFromContact(props.contact)}
|
||||
>
|
||||
<ArrowDownLeft class="inline-block text-m-blue" />
|
||||
<span>Request</span>
|
||||
</button>
|
||||
<Show when={props.contact?.npub}>
|
||||
<button
|
||||
class="flex gap-2 font-semibold text-m-blue"
|
||||
onClick={() =>
|
||||
props.requestFromContact(props.contact)
|
||||
}
|
||||
>
|
||||
<ArrowDownLeft class="inline-block text-m-blue" />
|
||||
<span>Request</span>
|
||||
</button>
|
||||
<Switch>
|
||||
<Match when={props.contact?.is_followed}>
|
||||
<button
|
||||
class="flex gap-2 font-semibold text-m-red disabled:text-m-grey-350 disabled:opacity-50"
|
||||
onClick={unfollowContact}
|
||||
disabled={updatingFollowStatus()}
|
||||
>
|
||||
<X class="inline-block text-m-red" />
|
||||
<span>Unfollow</span>
|
||||
</button>
|
||||
</Match>
|
||||
<Match when={!props.contact?.is_followed}>
|
||||
<button
|
||||
class="flex gap-2 font-semibold text-white disabled:text-m-grey-350 disabled:opacity-50"
|
||||
onClick={followContact}
|
||||
disabled={updatingFollowStatus()}
|
||||
>
|
||||
<Check class="inline-block text-white" />
|
||||
<span>Follow</span>
|
||||
</button>
|
||||
</Match>
|
||||
</Switch>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -363,7 +426,17 @@ export function Chat() {
|
||||
|
||||
const i18n = useI18n();
|
||||
|
||||
const contact = createAsync(async () => {
|
||||
// const contact = createAsync(async () => {
|
||||
// try {
|
||||
// return state.mutiny_wallet?.get_tag_item(params.id);
|
||||
// } catch (e) {
|
||||
// console.error("couldn't find contact");
|
||||
// console.error(e);
|
||||
// return undefined;
|
||||
// }
|
||||
// });
|
||||
|
||||
const [contact, { refetch: refetchContact }] = createResource(async () => {
|
||||
try {
|
||||
return state.mutiny_wallet?.get_tag_item(params.id);
|
||||
} catch (e) {
|
||||
@@ -417,13 +490,13 @@ export function Chat() {
|
||||
const a_time = isDirectMessage(a.content)
|
||||
? a.content.date
|
||||
: isActivityItem(a.content)
|
||||
? a.content.last_updated
|
||||
: 0;
|
||||
? a.content.last_updated
|
||||
: 0;
|
||||
const b_time = isDirectMessage(b.content)
|
||||
? b.content.date
|
||||
: isActivityItem(b.content)
|
||||
? b.content.last_updated
|
||||
: 0;
|
||||
? b.content.last_updated
|
||||
: 0;
|
||||
|
||||
return b_time - a_time; // Descending order
|
||||
});
|
||||
@@ -512,7 +585,7 @@ export function Chat() {
|
||||
<Show when={contact()}>
|
||||
<FixedChatHeader
|
||||
contact={contact()!}
|
||||
refetch={refetch}
|
||||
refetch={refetchContact}
|
||||
requestFromContact={requestFromContact}
|
||||
sendToContact={sendToContact}
|
||||
/>
|
||||
@@ -540,7 +613,7 @@ export function Chat() {
|
||||
contact={contact()!}
|
||||
/>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<Match when={contact() && contact()?.npub}>
|
||||
<ButtonCard
|
||||
onClick={() =>
|
||||
requestFromContact(contact())
|
||||
@@ -561,45 +634,49 @@ export function Chat() {
|
||||
</Show>
|
||||
</Suspense>
|
||||
</div>
|
||||
<div class="fixed bottom-0 grid w-full max-w-[600px] grid-cols-[auto_1fr_auto] grid-rows-1 items-center gap-2 bg-m-grey-975/70 px-4 py-2 backdrop-blur-lg">
|
||||
<MiniFab
|
||||
onScan={() => navigate("/scanner")}
|
||||
onSend={() => {
|
||||
sendToContact(contact());
|
||||
}}
|
||||
sendDisabled={
|
||||
!contact() ||
|
||||
!(contact()?.ln_address || contact()?.lnurl)
|
||||
}
|
||||
onRequest={() => requestFromContact(contact())}
|
||||
/>
|
||||
<form
|
||||
onSubmit={async (e) => {
|
||||
e.preventDefault();
|
||||
await sendMessage();
|
||||
}}
|
||||
>
|
||||
<SimpleInput
|
||||
disabled={sending()}
|
||||
value={messageValue()}
|
||||
onInput={(e) => setMessageValue(e.currentTarget.value)}
|
||||
placeholder={i18n.t("chat.placeholder")}
|
||||
<Show when={contact() && contact()?.npub}>
|
||||
<div class="fixed bottom-0 grid w-full max-w-[600px] grid-cols-[auto_1fr_auto] grid-rows-1 items-center gap-2 bg-m-grey-975/70 px-4 py-2 backdrop-blur-lg">
|
||||
<MiniFab
|
||||
onScan={() => navigate("/scanner")}
|
||||
onSend={() => {
|
||||
sendToContact(contact());
|
||||
}}
|
||||
sendDisabled={
|
||||
!contact() ||
|
||||
!(contact()?.ln_address || contact()?.lnurl)
|
||||
}
|
||||
onRequest={() => requestFromContact(contact())}
|
||||
/>
|
||||
</form>
|
||||
<div>
|
||||
<Show when={messageValue() || sending()}>
|
||||
<Button
|
||||
layout="xs"
|
||||
intent="blue"
|
||||
loading={sending()}
|
||||
onClick={sendMessage}
|
||||
>
|
||||
Send
|
||||
</Button>
|
||||
</Show>
|
||||
<form
|
||||
onSubmit={async (e) => {
|
||||
e.preventDefault();
|
||||
await sendMessage();
|
||||
}}
|
||||
>
|
||||
<SimpleInput
|
||||
disabled={sending()}
|
||||
value={messageValue()}
|
||||
onInput={(e) =>
|
||||
setMessageValue(e.currentTarget.value)
|
||||
}
|
||||
placeholder={i18n.t("chat.placeholder")}
|
||||
/>
|
||||
</form>
|
||||
<div>
|
||||
<Show when={messageValue() || sending()}>
|
||||
<Button
|
||||
layout="xs"
|
||||
intent="blue"
|
||||
loading={sending()}
|
||||
onClick={sendMessage}
|
||||
>
|
||||
Send
|
||||
</Button>
|
||||
</Show>
|
||||
</div>
|
||||
<div class="backgrop-blur-lg z-50 bg-m-grey-975/70 safe-bottom" />
|
||||
</div>
|
||||
<div class="backgrop-blur-lg z-50 bg-m-grey-975/70 safe-bottom" />
|
||||
</div>
|
||||
</Show>
|
||||
</MutinyWalletGuard>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,16 +3,13 @@ import { createMemo, createSignal, Show } from "solid-js";
|
||||
|
||||
import {
|
||||
BackLink,
|
||||
ButtonLink,
|
||||
DefaultMain,
|
||||
EditableProfile,
|
||||
EditProfileForm,
|
||||
LargeHeader,
|
||||
MutinyWalletGuard,
|
||||
NavBar,
|
||||
NiceP
|
||||
NavBar
|
||||
} from "~/components";
|
||||
// import { useI18n } from "~/i18n/context";
|
||||
import { useMegaStore } from "~/state/megaStore";
|
||||
|
||||
export function EditProfile() {
|
||||
@@ -40,7 +37,7 @@ export function EditProfile() {
|
||||
const newProfile = await state.mutiny_wallet?.edit_nostr_profile(
|
||||
profile.nym,
|
||||
profile.imageUrl,
|
||||
originalProfile().lud16,
|
||||
profile.lightningAddress,
|
||||
originalProfile().nip05
|
||||
);
|
||||
|
||||
@@ -58,16 +55,12 @@ export function EditProfile() {
|
||||
<DefaultMain>
|
||||
<BackLink href="/profile" title="Profile" />
|
||||
<LargeHeader>Edit Profile</LargeHeader>
|
||||
<NiceP>
|
||||
Update your profile.
|
||||
<br />
|
||||
Your activity is private by default.
|
||||
</NiceP>
|
||||
<div class="flex-1" />
|
||||
<Show when={originalProfile()}>
|
||||
<EditProfileForm
|
||||
initialProfile={{
|
||||
nym: originalProfile().name,
|
||||
lightningAddress: originalProfile().lud16,
|
||||
imageUrl: originalProfile().picture
|
||||
}}
|
||||
onSave={saveProfile}
|
||||
@@ -75,10 +68,6 @@ export function EditProfile() {
|
||||
cta="Save"
|
||||
/>
|
||||
</Show>
|
||||
<ButtonLink href="/importprofile" intent="text">
|
||||
Import different nostr profile
|
||||
</ButtonLink>
|
||||
|
||||
<NavBar activeTab="profile" />
|
||||
</DefaultMain>
|
||||
</MutinyWalletGuard>
|
||||
|
||||
@@ -1,21 +1,9 @@
|
||||
import { MutinyWallet } from "@mutinywallet/mutiny-wasm";
|
||||
import { useNavigate } from "@solidjs/router";
|
||||
import { SecureStoragePlugin } from "capacitor-secure-storage-plugin";
|
||||
import { createSignal, Show } from "solid-js";
|
||||
import { Show } from "solid-js";
|
||||
|
||||
import {
|
||||
Button,
|
||||
ButtonLink,
|
||||
DefaultMain,
|
||||
InfoBox,
|
||||
SimpleInput
|
||||
} from "~/components";
|
||||
import { Button, ButtonLink, DefaultMain, ImportNsecForm } from "~/components";
|
||||
|
||||
export function ImportProfile() {
|
||||
const [nsec, setNsec] = createSignal("");
|
||||
const [saving, setSaving] = createSignal(false);
|
||||
const [error, setError] = createSignal<string | undefined>();
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
function handleSkip() {
|
||||
@@ -23,25 +11,6 @@ export function ImportProfile() {
|
||||
navigate("/");
|
||||
}
|
||||
|
||||
async function saveNsec() {
|
||||
setSaving(true);
|
||||
setError(undefined);
|
||||
const trimmedNsec = nsec().trim();
|
||||
try {
|
||||
const npub = await MutinyWallet.nsec_to_npub(trimmedNsec);
|
||||
if (!npub) {
|
||||
throw new Error("Invalid nsec");
|
||||
}
|
||||
await SecureStoragePlugin.set({ key: "nsec", value: trimmedNsec });
|
||||
// TODO: right now we need a reload to set the nsec
|
||||
window.location.href = "/";
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setError("Invalid nsec");
|
||||
}
|
||||
setSaving(false);
|
||||
}
|
||||
|
||||
// @ts-expect-error we're checking for an extension
|
||||
const windowHasNostr = window.nostr && window.nostr.getPublicKey;
|
||||
|
||||
@@ -55,17 +24,7 @@ export function ImportProfile() {
|
||||
<br />
|
||||
</p>
|
||||
<div class="flex-1" />
|
||||
<SimpleInput
|
||||
value={nsec()}
|
||||
onInput={(e) => setNsec(e.currentTarget.value)}
|
||||
placeholder={`Nostr private key (starts with "nsec")`}
|
||||
/>
|
||||
<Button layout="full" onClick={saveNsec} loading={saving()}>
|
||||
Import
|
||||
</Button>
|
||||
<Show when={error()}>
|
||||
<InfoBox accent="red">{error()}</InfoBox>
|
||||
</Show>
|
||||
<ImportNsecForm />
|
||||
<div class="flex-1" />
|
||||
<div class="flex flex-col items-center">
|
||||
{/* Don't want them to accidentally "edit" their profile if they have one */}
|
||||
|
||||
@@ -20,7 +20,6 @@ import { useMegaStore } from "~/state/megaStore";
|
||||
export function WalletHeader(props: { loading: boolean }) {
|
||||
const navigate = useNavigate();
|
||||
const [state, _actions] = useMegaStore();
|
||||
const npub = () => state.mutiny_wallet?.get_npub();
|
||||
|
||||
async function getProfile() {
|
||||
const profile = state.mutiny_wallet?.get_nostr_profile();
|
||||
@@ -44,7 +43,7 @@ export function WalletHeader(props: { loading: boolean }) {
|
||||
return profile()!.picture;
|
||||
}
|
||||
|
||||
return `https://bitcoinfaces.xyz/api/get-image?name=${npub()}&onchain=false`;
|
||||
return undefined;
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { useNavigate } from "@solidjs/router";
|
||||
import { A, useNavigate } from "@solidjs/router";
|
||||
import { createSignal } from "solid-js";
|
||||
|
||||
import {
|
||||
Button,
|
||||
ButtonLink,
|
||||
DefaultMain,
|
||||
EditableProfile,
|
||||
EditProfileForm
|
||||
@@ -28,7 +27,7 @@ export function NewProfile() {
|
||||
const profile = await state.mutiny_wallet?.edit_nostr_profile(
|
||||
p.nym ? p.nym : undefined,
|
||||
p.imageUrl ? p.imageUrl : undefined,
|
||||
undefined,
|
||||
p.lightningAddress ? p.lightningAddress : undefined,
|
||||
undefined
|
||||
);
|
||||
console.log("profile", profile);
|
||||
@@ -47,8 +46,6 @@ export function NewProfile() {
|
||||
<h1 class="text-3xl font-semibold">Create your profile</h1>
|
||||
<p class="text-center text-xl font-light text-neutral-200">
|
||||
Mutiny makes payments social.
|
||||
<br />
|
||||
Your activity is private by default.
|
||||
</p>
|
||||
<div class="flex-1" />
|
||||
<EditProfileForm
|
||||
@@ -56,14 +53,15 @@ export function NewProfile() {
|
||||
saving={creating()}
|
||||
cta="Create"
|
||||
/>
|
||||
<div class="flex flex-col items-center">
|
||||
<ButtonLink href="/importprofile" intent="text">
|
||||
Import existing nostr profile
|
||||
</ButtonLink>
|
||||
<Button onClick={handleSkip} intent="text">
|
||||
Skip for now
|
||||
</Button>
|
||||
</div>
|
||||
<Button onClick={handleSkip} intent="text">
|
||||
Skip for now
|
||||
</Button>
|
||||
<A
|
||||
class="text-base font-normal text-m-grey-400"
|
||||
href="/importprofile"
|
||||
>
|
||||
Import existing nostr profile
|
||||
</A>
|
||||
<div class="flex-1" />
|
||||
</div>
|
||||
</DefaultMain>
|
||||
|
||||
@@ -1,26 +1,29 @@
|
||||
import { A } from "@solidjs/router";
|
||||
import { createMemo, Show } from "solid-js";
|
||||
import { useNavigate } from "@solidjs/router";
|
||||
import { Copy, Edit, QrCode } from "lucide-solid";
|
||||
import { createMemo, createSignal, Match, Show, Switch } from "solid-js";
|
||||
import { QRCodeSVG } from "solid-qr-code";
|
||||
|
||||
import {
|
||||
BackLink,
|
||||
BalanceBox,
|
||||
ButtonCard,
|
||||
DefaultMain,
|
||||
FancyCard,
|
||||
KeyValue,
|
||||
LabelCircle,
|
||||
MiniStringShower,
|
||||
MutinyWalletGuard,
|
||||
NavBar
|
||||
NavBar,
|
||||
NiceP,
|
||||
SimpleDialog
|
||||
} from "~/components";
|
||||
import { useI18n } from "~/i18n/context";
|
||||
import { useMegaStore } from "~/state/megaStore";
|
||||
import { useCopy } from "~/utils";
|
||||
|
||||
export function Profile() {
|
||||
const [state, _actions] = useMegaStore();
|
||||
const i18n = useI18n();
|
||||
|
||||
const npub = () => state.mutiny_wallet?.get_npub();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const profile = createMemo(() => {
|
||||
const profile = state.mutiny_wallet?.get_nostr_profile();
|
||||
@@ -34,6 +37,10 @@ export function Profile() {
|
||||
};
|
||||
});
|
||||
|
||||
const [showQr, setShowQr] = createSignal(false);
|
||||
|
||||
const [copy, copied] = useCopy({ copiedTimeout: 1000 });
|
||||
|
||||
return (
|
||||
<MutinyWalletGuard>
|
||||
<DefaultMain>
|
||||
@@ -43,50 +50,79 @@ export function Profile() {
|
||||
<LabelCircle
|
||||
contact
|
||||
label={false}
|
||||
name={profile().name ? profile().name : "Anon"}
|
||||
image_url={
|
||||
profile().picture
|
||||
? profile().picture
|
||||
: `https://bitcoinfaces.xyz/api/get-image?name=${npub()}&onchain=false`
|
||||
}
|
||||
image_url={profile().picture}
|
||||
size="xl"
|
||||
/>
|
||||
<h1 class="text-3xl font-semibold">
|
||||
<Show when={profile().name}>{profile().name}</Show>
|
||||
</h1>
|
||||
|
||||
<Show when={profile().lud16}>
|
||||
<p class="break-all text-center font-system-mono text-base text-m-grey-350">
|
||||
{profile().lud16}
|
||||
</p>
|
||||
<div class="w-[10rem] rounded bg-white p-[1rem]">
|
||||
<QRCodeSVG
|
||||
value={profile().lud16}
|
||||
class="h-full max-h-[256px] w-full"
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={!profile().lud16}>
|
||||
<p class="text-center text-base italic text-m-grey-350">
|
||||
Mutiny Lightning Address coming soon.
|
||||
</p>
|
||||
</Show>
|
||||
|
||||
<A
|
||||
href="/editprofile"
|
||||
class="text-xl font-semibold text-m-red no-underline active:text-m-red/80"
|
||||
>
|
||||
{i18n.t("profile.edit_profile")}
|
||||
</A>
|
||||
<FancyCard>
|
||||
<Switch>
|
||||
<Match when={profile().lud16}>
|
||||
<p class="break-all text-center font-system-mono text-base ">
|
||||
{profile().lud16}
|
||||
</p>
|
||||
<div class="flex w-full justify-center gap-8">
|
||||
<button onClick={() => setShowQr(true)}>
|
||||
<QrCode class="inline-block" />
|
||||
</button>
|
||||
<button
|
||||
class="p-1"
|
||||
classList={{
|
||||
"bg-m-red rounded": copied()
|
||||
}}
|
||||
onClick={() =>
|
||||
copy(profile().lud16)
|
||||
}
|
||||
>
|
||||
<Copy class="inline-block" />
|
||||
</button>
|
||||
</div>{" "}
|
||||
<SimpleDialog
|
||||
open={showQr()}
|
||||
setOpen={(open) => {
|
||||
setShowQr(open);
|
||||
}}
|
||||
title={"Lightning Address"}
|
||||
>
|
||||
<div class="w-[10rem] self-center rounded bg-white p-[1rem]">
|
||||
<QRCodeSVG
|
||||
value={profile().lud16 || ""}
|
||||
class="h-full max-h-[256px] w-full"
|
||||
/>
|
||||
</div>
|
||||
</SimpleDialog>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<p class="text-center text-base italic text-m-grey-350">
|
||||
No Lightning Address set
|
||||
</p>
|
||||
</Match>
|
||||
</Switch>
|
||||
</FancyCard>
|
||||
</Show>
|
||||
</div>
|
||||
{/* <LargeHeader>Accounts</LargeHeader> */}
|
||||
{/* <div>
|
||||
<MediumHeader>{i18n.t("profile.social")}</MediumHeader>
|
||||
<FancyCard>
|
||||
<KeyValue key="LN Address">
|
||||
<MiniStringShower text={profile().lud16 || ""} />
|
||||
</KeyValue>
|
||||
<KeyValue key="npub">
|
||||
<MiniStringShower text={npub() || ""} />
|
||||
</KeyValue>
|
||||
</FancyCard>
|
||||
</div> */}
|
||||
<ButtonCard onClick={() => navigate("/editprofile")}>
|
||||
<div class="flex items-center gap-2">
|
||||
{/* <Users class="inline-block text-m-red" /> */}
|
||||
<Edit class="inline-block text-m-red" />
|
||||
<NiceP>{i18n.t("profile.edit_profile")}</NiceP>
|
||||
</div>
|
||||
</ButtonCard>
|
||||
<BalanceBox loading={state.wallet_loading} />
|
||||
<FancyCard title={i18n.t("profile.nostr_identity")}>
|
||||
<KeyValue key="npub">
|
||||
<MiniStringShower text={npub() || ""} />
|
||||
</KeyValue>
|
||||
</FancyCard>
|
||||
|
||||
<NavBar activeTab="profile" />
|
||||
</DefaultMain>
|
||||
</MutinyWalletGuard>
|
||||
|
||||
@@ -184,8 +184,8 @@ export function Receive() {
|
||||
? paymentTx()?.txid
|
||||
: undefined
|
||||
: paymentInvoice()
|
||||
? paymentInvoice()?.payment_hash
|
||||
: undefined;
|
||||
? paymentInvoice()?.payment_hash
|
||||
: undefined;
|
||||
const kind = paidState() === "onchain_paid" ? "OnChain" : "Lightning";
|
||||
|
||||
console.log("Opening details modal: ", paymentTxId, kind);
|
||||
|
||||
@@ -201,8 +201,8 @@ export function Send() {
|
||||
? sentDetails()?.txid
|
||||
: undefined
|
||||
: sentDetails()
|
||||
? sentDetails()?.payment_hash
|
||||
: undefined;
|
||||
? sentDetails()?.payment_hash
|
||||
: undefined;
|
||||
const kind = sentDetails()?.txid ? "OnChain" : "Lightning";
|
||||
|
||||
console.log("Opening details modal: ", paymentTxId, kind);
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Title } from "@solidjs/meta";
|
||||
|
||||
import { ButtonLink, LargeHeader, VStack } from "~/components";
|
||||
import { ButtonLink, DefaultMain, LargeHeader } from "~/components";
|
||||
import { useI18n } from "~/i18n/context";
|
||||
|
||||
export function NotFound() {
|
||||
const i18n = useI18n();
|
||||
return (
|
||||
<VStack>
|
||||
<DefaultMain>
|
||||
<Title>{i18n.t("error.not_found.title")}</Title>
|
||||
<LargeHeader>{i18n.t("error.not_found.title")}</LargeHeader>
|
||||
<p>{i18n.t("error.not_found.wtf_paul")}</p>
|
||||
@@ -14,6 +14,6 @@ export function NotFound() {
|
||||
<ButtonLink href="/" intent="red">
|
||||
{i18n.t("common.dangit")}
|
||||
</ButtonLink>
|
||||
</VStack>
|
||||
</DefaultMain>
|
||||
);
|
||||
}
|
||||
|
||||
70
src/routes/settings/NostrKeys.tsx
Normal file
70
src/routes/settings/NostrKeys.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import { A } from "@solidjs/router";
|
||||
import { Show } from "solid-js";
|
||||
import { QRCodeSVG } from "solid-qr-code";
|
||||
|
||||
import {
|
||||
BackPop,
|
||||
DefaultMain,
|
||||
ExternalLink,
|
||||
FancyCard,
|
||||
KeyValue,
|
||||
LargeHeader,
|
||||
MiniStringShower,
|
||||
MutinyWalletGuard,
|
||||
NavBar,
|
||||
NiceP,
|
||||
VStack
|
||||
} from "~/components";
|
||||
import { useI18n } from "~/i18n/context";
|
||||
import { useMegaStore } from "~/state/megaStore";
|
||||
|
||||
export function NostrKeys() {
|
||||
const i18n = useI18n();
|
||||
const [state, _actions] = useMegaStore();
|
||||
|
||||
const npub = () => state.mutiny_wallet?.get_npub();
|
||||
const nsec = () => state.mutiny_wallet?.export_nsec();
|
||||
|
||||
return (
|
||||
<MutinyWalletGuard>
|
||||
<DefaultMain>
|
||||
<BackPop default="/settings" />
|
||||
<LargeHeader>{i18n.t("settings.nostr_keys.title")}</LargeHeader>
|
||||
<NiceP>{i18n.t("settings.nostr_keys.description")}</NiceP>
|
||||
<NiceP>
|
||||
<ExternalLink href="https://nostr.com/">
|
||||
{i18n.t("settings.nostr_keys.learn_more")}
|
||||
</ExternalLink>
|
||||
</NiceP>
|
||||
<FancyCard>
|
||||
<VStack>
|
||||
<div class="w-[10rem] self-center rounded bg-white p-[1rem]">
|
||||
<QRCodeSVG
|
||||
value={npub() || ""}
|
||||
class="h-full max-h-[256px] w-full"
|
||||
/>
|
||||
</div>
|
||||
<KeyValue key="Public Key">
|
||||
<MiniStringShower text={npub() || ""} />
|
||||
</KeyValue>
|
||||
<Show when={nsec()}>
|
||||
<KeyValue key="Private Key">
|
||||
<MiniStringShower text={nsec() || ""} hide />
|
||||
</KeyValue>
|
||||
<p class="text-base italic text-m-grey-350">
|
||||
{i18n.t("settings.nostr_keys.warning")}
|
||||
</p>
|
||||
</Show>
|
||||
</VStack>
|
||||
</FancyCard>
|
||||
<A
|
||||
href="/importprofile"
|
||||
class="self-center text-base font-normal text-m-grey-400"
|
||||
>
|
||||
Import different nostr profile
|
||||
</A>
|
||||
</DefaultMain>
|
||||
<NavBar activeTab="settings" />
|
||||
</MutinyWalletGuard>
|
||||
);
|
||||
}
|
||||
@@ -110,6 +110,17 @@ export function Settings() {
|
||||
? i18n.t("settings.encrypt.caption")
|
||||
: undefined
|
||||
},
|
||||
|
||||
{
|
||||
href: "/settings/servers",
|
||||
text: i18n.t("settings.servers.title"),
|
||||
caption: i18n.t("settings.servers.caption")
|
||||
}
|
||||
]}
|
||||
/>
|
||||
<SettingsLinkList
|
||||
header={i18n.t("settings.appearance")}
|
||||
links={[
|
||||
{
|
||||
href: "/settings/currency",
|
||||
text: i18n.t("settings.currency.title"),
|
||||
@@ -119,24 +130,16 @@ export function Settings() {
|
||||
href: "/settings/language",
|
||||
text: i18n.t("settings.language.title"),
|
||||
caption: i18n.t("settings.language.caption")
|
||||
},
|
||||
{
|
||||
href: "/settings/servers",
|
||||
text: i18n.t("settings.servers.title"),
|
||||
caption: i18n.t("settings.servers.caption")
|
||||
}
|
||||
]}
|
||||
/>
|
||||
<SettingsLinkList
|
||||
header={i18n.t("settings.experimental_features")}
|
||||
header={i18n.t("settings.social")}
|
||||
links={[
|
||||
{
|
||||
href: "/settings/syncnostrcontacts",
|
||||
text: i18n.t("settings.nostr_contacts.title")
|
||||
},
|
||||
{
|
||||
href: "/settings/federations",
|
||||
text: i18n.t("settings.manage_federations.title")
|
||||
href: "/settings/nostrkeys",
|
||||
text: i18n.t("settings.nostr_keys.title"),
|
||||
caption: i18n.t("settings.nostr_keys.caption")
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
@@ -1,170 +0,0 @@
|
||||
import { Capacitor } from "@capacitor/core";
|
||||
import { createForm, required, SubmitHandler } from "@modular-forms/solid";
|
||||
import { SecureStoragePlugin } from "capacitor-secure-storage-plugin";
|
||||
import { createSignal, Match, Show, Switch } from "solid-js";
|
||||
|
||||
import {
|
||||
BackPop,
|
||||
Button,
|
||||
DefaultMain,
|
||||
FancyCard,
|
||||
InfoBox,
|
||||
KeyValue,
|
||||
LargeHeader,
|
||||
MiniStringShower,
|
||||
MutinyWalletGuard,
|
||||
NavBar,
|
||||
TextField,
|
||||
VStack
|
||||
} from "~/components";
|
||||
import { useI18n } from "~/i18n/context";
|
||||
import { useMegaStore } from "~/state/megaStore";
|
||||
import { eify } from "~/utils";
|
||||
|
||||
type NostrContactsForm = {
|
||||
npub: string;
|
||||
};
|
||||
|
||||
function SyncContactsForm() {
|
||||
const i18n = useI18n();
|
||||
const [state, actions] = useMegaStore();
|
||||
const [error, setError] = createSignal<Error>();
|
||||
|
||||
const allowNsec = Capacitor.isNativePlatform();
|
||||
|
||||
const [feedbackForm, { Form, Field }] = createForm<NostrContactsForm>({
|
||||
initialValues: {
|
||||
npub: ""
|
||||
}
|
||||
});
|
||||
|
||||
const handleSubmit: SubmitHandler<NostrContactsForm> = async (
|
||||
f: NostrContactsForm
|
||||
) => {
|
||||
try {
|
||||
const npub = f.npub.trim();
|
||||
await state.mutiny_wallet?.sync_nostr_contacts(npub);
|
||||
actions.saveNpub(npub);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setError(eify(e));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<VStack>
|
||||
<Field
|
||||
name="npub"
|
||||
validate={[
|
||||
required(
|
||||
i18n.t("settings.nostr_contacts.npub_required")
|
||||
)
|
||||
]}
|
||||
>
|
||||
{(field, props) => (
|
||||
<TextField
|
||||
{...props}
|
||||
value={field.value}
|
||||
error={field.error}
|
||||
label={i18n.t("settings.nostr_contacts.npub_label")}
|
||||
placeholder={allowNsec ? "npub/nsec..." : "npub..."}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
<Show when={error()}>
|
||||
<InfoBox accent="red">{error()?.message}</InfoBox>
|
||||
</Show>
|
||||
<Button
|
||||
loading={feedbackForm.submitting}
|
||||
disabled={
|
||||
!feedbackForm.dirty ||
|
||||
feedbackForm.submitting ||
|
||||
feedbackForm.invalid
|
||||
}
|
||||
intent="blue"
|
||||
type="submit"
|
||||
>
|
||||
{i18n.t("settings.nostr_contacts.sync")}
|
||||
</Button>
|
||||
</VStack>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
export function SyncNostrContacts() {
|
||||
const i18n = useI18n();
|
||||
const [state, actions] = useMegaStore();
|
||||
const [loading, setLoading] = createSignal(false);
|
||||
const [error, setError] = createSignal<Error>();
|
||||
|
||||
async function clearNpub() {
|
||||
actions.saveNpub("");
|
||||
if (Capacitor.isNativePlatform()) {
|
||||
await SecureStoragePlugin.remove({ key: "nsec" });
|
||||
}
|
||||
}
|
||||
|
||||
async function resync() {
|
||||
setError(undefined);
|
||||
setLoading(true);
|
||||
try {
|
||||
await state.mutiny_wallet?.sync_nostr_contacts(
|
||||
// We can only see the resync button if there's an npub set
|
||||
state.npub!
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<MutinyWalletGuard>
|
||||
<DefaultMain>
|
||||
<BackPop default="/settings" />
|
||||
<LargeHeader>
|
||||
{i18n.t("settings.nostr_contacts.title")}
|
||||
</LargeHeader>
|
||||
<Switch>
|
||||
<Match when={state.npub}>
|
||||
<VStack>
|
||||
<Show when={error()}>
|
||||
<InfoBox accent="red">
|
||||
{error()?.message}
|
||||
</InfoBox>
|
||||
</Show>
|
||||
<FancyCard>
|
||||
<VStack>
|
||||
<KeyValue key="Npub">
|
||||
<MiniStringShower
|
||||
text={state.npub || ""}
|
||||
/>
|
||||
</KeyValue>
|
||||
<Button
|
||||
intent="blue"
|
||||
onClick={resync}
|
||||
loading={loading()}
|
||||
>
|
||||
{i18n.t(
|
||||
"settings.nostr_contacts.resync"
|
||||
)}
|
||||
</Button>
|
||||
<Button intent="red" onClick={clearNpub}>
|
||||
{i18n.t(
|
||||
"settings.nostr_contacts.remove"
|
||||
)}
|
||||
</Button>
|
||||
</VStack>
|
||||
</FancyCard>
|
||||
</VStack>
|
||||
</Match>
|
||||
<Match when={!state.npub}>
|
||||
<SyncContactsForm />
|
||||
</Match>
|
||||
</Switch>
|
||||
</DefaultMain>
|
||||
<NavBar activeTab="settings" />
|
||||
</MutinyWalletGuard>
|
||||
);
|
||||
}
|
||||
@@ -11,5 +11,5 @@ export * from "./Gift";
|
||||
export * from "./Plus";
|
||||
export * from "./Restore";
|
||||
export * from "./Servers";
|
||||
export * from "./SyncNostrContacts";
|
||||
export * from "./ManageFederations";
|
||||
export * from "./NostrKeys";
|
||||
|
||||
5
types/index.d.ts
vendored
5
types/index.d.ts
vendored
@@ -1,5 +0,0 @@
|
||||
declare module "*.svg" {
|
||||
const content: any;
|
||||
export default content;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user