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 { expect, test } from "@playwright/test";
|
||||||
|
|
||||||
import { loadHome, visitSettings } from "./utils";
|
import { loadHome } from "./utils";
|
||||||
|
|
||||||
const SIGNET_INVITE_CODE =
|
const SIGNET_INVITE_CODE =
|
||||||
"fed11qgqzc2nhwden5te0vejkg6tdd9h8gepwvejkg6tdd9h8garhduhx6at5d9h8jmn9wshxxmmd9uqqzgxg6s3evnr6m9zdxr6hxkdkukexpcs3mn7mj3g5pc5dfh63l4tj6g9zk4er";
|
"fed11qgqzc2nhwden5te0vejkg6tdd9h8gepwvejkg6tdd9h8garhduhx6at5d9h8jmn9wshxxmmd9uqqzgxg6s3evnr6m9zdxr6hxkdkukexpcs3mn7mj3g5pc5dfh63l4tj6g9zk4er";
|
||||||
@@ -11,10 +11,9 @@ test.beforeEach(async ({ page }) => {
|
|||||||
|
|
||||||
test("fedmint join, receive, send", async ({ page }) => {
|
test("fedmint join, receive, send", async ({ page }) => {
|
||||||
await loadHome(page);
|
await loadHome(page);
|
||||||
await visitSettings(page);
|
|
||||||
|
|
||||||
// Click "Manage Federations" link
|
// Click "Join a federation" cta
|
||||||
await page.click("text=Manage Federations");
|
await page.click("text=Join a federation");
|
||||||
|
|
||||||
// Fill the input with the federation code
|
// Fill the input with the federation code
|
||||||
await page.fill("input[name='federation_code']", SIGNET_INVITE_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
|
// Navigate back home
|
||||||
await page.goBack();
|
await page.goBack();
|
||||||
await page.goBack();
|
|
||||||
|
|
||||||
// Click the top left button (it's the profile button), a child of header
|
// Click the top left button (it's the profile button), a child of header
|
||||||
// TODO: better ARIA stuff
|
// TODO: better ARIA stuff
|
||||||
|
|||||||
@@ -26,8 +26,7 @@ const settingsRoutes = [
|
|||||||
"/plus",
|
"/plus",
|
||||||
"/restore",
|
"/restore",
|
||||||
"/servers",
|
"/servers",
|
||||||
"/syncnostrcontacts",
|
"/nostrkeys"
|
||||||
"/federations"
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const settingsRoutesPrefixed = settingsRoutes.map((route) => {
|
const settingsRoutesPrefixed = settingsRoutes.map((route) => {
|
||||||
@@ -96,31 +95,8 @@ test("visit each route", async ({ page }) => {
|
|||||||
await checkRoute(page, "/settings/servers", "Servers", checklist);
|
await checkRoute(page, "/settings/servers", "Servers", checklist);
|
||||||
await page.goBack();
|
await page.goBack();
|
||||||
|
|
||||||
// Connections
|
|
||||||
await checkRoute(
|
|
||||||
page,
|
|
||||||
"/settings/connections",
|
|
||||||
"Wallet Connections",
|
|
||||||
checklist
|
|
||||||
);
|
|
||||||
await page.goBack();
|
|
||||||
|
|
||||||
// Sync Nostr Contacts
|
// Sync Nostr Contacts
|
||||||
await checkRoute(
|
await checkRoute(page, "/settings/nostrkeys", "Nostr Keys", checklist);
|
||||||
page,
|
|
||||||
"/settings/syncnostrcontacts",
|
|
||||||
"Sync Nostr Contacts",
|
|
||||||
checklist
|
|
||||||
);
|
|
||||||
await page.goBack();
|
|
||||||
|
|
||||||
// Manage Federations
|
|
||||||
await checkRoute(
|
|
||||||
page,
|
|
||||||
"/settings/federations",
|
|
||||||
"Manage Federations",
|
|
||||||
checklist
|
|
||||||
);
|
|
||||||
await page.goBack();
|
await page.goBack();
|
||||||
|
|
||||||
// Emergency Kit
|
// Emergency Kit
|
||||||
|
|||||||
12
package.json
12
package.json
@@ -52,19 +52,19 @@
|
|||||||
"@capacitor/share": "^5.0.6",
|
"@capacitor/share": "^5.0.6",
|
||||||
"@capacitor/status-bar": "^5.0.6",
|
"@capacitor/status-bar": "^5.0.6",
|
||||||
"@capacitor/toast": "^5.0.6",
|
"@capacitor/toast": "^5.0.6",
|
||||||
"@kobalte/core": "^0.12.1",
|
"@kobalte/core": "^0.12.6",
|
||||||
"@kobalte/tailwindcss": "^0.9.0",
|
"@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",
|
"@modular-forms/solid": "^0.20.0",
|
||||||
"@solid-primitives/upload": "^0.0.111",
|
"@solid-primitives/upload": "^0.0.117",
|
||||||
"@solidjs/meta": "^0.29.3",
|
"@solidjs/meta": "^0.29.3",
|
||||||
"@solidjs/router": "^0.10.9",
|
"@solidjs/router": "^0.13.1",
|
||||||
"capacitor-secure-storage-plugin": "^0.9.0",
|
"capacitor-secure-storage-plugin": "^0.9.0",
|
||||||
"i18next": "^23.10.1",
|
"i18next": "^23.10.1",
|
||||||
"i18next-browser-languagedetector": "^7.1.0",
|
"i18next-browser-languagedetector": "^7.1.0",
|
||||||
"lucide-solid": "^0.330.0",
|
"lucide-solid": "^0.363.0",
|
||||||
"qr-scanner": "^1.4.2",
|
"qr-scanner": "^1.4.2",
|
||||||
"solid-js": "^1.8.12",
|
"solid-js": "^1.8.16",
|
||||||
"solid-qr-code": "^0.0.8",
|
"solid-qr-code": "^0.0.8",
|
||||||
"solid-transition-group": "^0.2.3"
|
"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",
|
"profile": "Profile",
|
||||||
"nostr_identity": "Nostr Identity",
|
"nostr_identity": "Nostr Identity",
|
||||||
"add_lightning_address": "Add Lightning Address",
|
"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": {
|
"chat": {
|
||||||
"prompt": "This is a new conversation. Try asking for money!",
|
"prompt": "This is a new conversation. Try asking for money!",
|
||||||
@@ -74,8 +82,7 @@
|
|||||||
"email_error": "That doesn't look like a lightning address",
|
"email_error": "That doesn't look like a lightning address",
|
||||||
"npub_error": "That doesn't look like a nostr npub",
|
"npub_error": "That doesn't look like a nostr npub",
|
||||||
"error_ln_address_missing": "New contacts need a lightning address",
|
"error_ln_address_missing": "New contacts need a lightning address",
|
||||||
"npub": "Nostr Npub",
|
"npub": "Nostr Npub"
|
||||||
"link_to_nostr_sync": "Import Nostr Contacts"
|
|
||||||
},
|
},
|
||||||
"redeem": {
|
"redeem": {
|
||||||
"redeem_bitcoin": "Redeem Bitcoin",
|
"redeem_bitcoin": "Redeem Bitcoin",
|
||||||
@@ -533,7 +540,7 @@
|
|||||||
"remove": "Remove",
|
"remove": "Remove",
|
||||||
"expires": "Expires",
|
"expires": "Expires",
|
||||||
"federation_id": "Federation ID",
|
"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."
|
"learn_more": "Learn more about Fedimint."
|
||||||
},
|
},
|
||||||
"gift": {
|
"gift": {
|
||||||
@@ -567,6 +574,15 @@
|
|||||||
"send_delete_confirm": "Are you sure you want to delete this gift? Is this your rugpull moment?",
|
"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.",
|
"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."
|
"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": {
|
"swap": {
|
||||||
@@ -700,4 +716,4 @@
|
|||||||
"authenticated": "Authenticated!"
|
"authenticated": "Authenticated!"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,8 +49,7 @@
|
|||||||
"email_error": "Eso no parece una dirección lightning",
|
"email_error": "Eso no parece una dirección lightning",
|
||||||
"npub_error": "Eso no parece un npub de nostr",
|
"npub_error": "Eso no parece un npub de nostr",
|
||||||
"error_ln_address_missing": "Los contactos nuevos necesitan una dirección lightning",
|
"error_ln_address_missing": "Los contactos nuevos necesitan una dirección lightning",
|
||||||
"npub": "Npub Nostr",
|
"npub": "Npub Nostr"
|
||||||
"link_to_nostr_sync": "Importar Contactos de Nostr"
|
|
||||||
},
|
},
|
||||||
"receive": {
|
"receive": {
|
||||||
"receive_bitcoin": "Recibir Bitcoin",
|
"receive_bitcoin": "Recibir Bitcoin",
|
||||||
@@ -660,5 +659,8 @@
|
|||||||
"error": "Eso no funcionó por alguna razón.",
|
"error": "Eso no funcionó por alguna razón.",
|
||||||
"authenticated": "¡Autenticado!"
|
"authenticated": "¡Autenticado!"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"profile": {
|
||||||
|
"manage_federation": "Manejar Federaciones"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -149,10 +149,12 @@ function OnchainHeader(props: { info: OnChainTx; kind?: HackActivityType }) {
|
|||||||
{props.kind === "ChannelOpen"
|
{props.kind === "ChannelOpen"
|
||||||
? i18n.t("activity.transaction_details.channel_open")
|
? i18n.t("activity.transaction_details.channel_open")
|
||||||
: props.kind === "ChannelClose"
|
: props.kind === "ChannelClose"
|
||||||
? i18n.t("activity.transaction_details.channel_close")
|
? i18n.t("activity.transaction_details.channel_close")
|
||||||
: isSend()
|
: isSend()
|
||||||
? i18n.t("activity.transaction_details.onchain_send")
|
? i18n.t("activity.transaction_details.onchain_send")
|
||||||
: i18n.t("activity.transaction_details.onchain_receive")}
|
: i18n.t(
|
||||||
|
"activity.transaction_details.onchain_receive"
|
||||||
|
)}
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match
|
<Match
|
||||||
when={
|
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 });
|
const [copy, copied] = useCopy({ copiedTimeout: 1000 });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="grid w-full grid-cols-[minmax(0,_1fr)_auto] gap-1">
|
<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
|
<button
|
||||||
class="w-[1.5rem] p-1"
|
class="w-[1.5rem] p-1"
|
||||||
classList={{ "bg-m-green rounded": copied() }}
|
classList={{ "bg-m-red rounded": copied() }}
|
||||||
onClick={() => copy(props.text)}
|
onClick={() => copy(props.text)}
|
||||||
>
|
>
|
||||||
<Copy class="h-4 w-4" />
|
<Copy class="h-4 w-4" />
|
||||||
@@ -302,8 +318,8 @@ function OnchainDetails(props: {
|
|||||||
await (state.mutiny_wallet?.list_channels() as Promise<
|
await (state.mutiny_wallet?.list_channels() as Promise<
|
||||||
MutinyChannel[]
|
MutinyChannel[]
|
||||||
>);
|
>);
|
||||||
const channel = channels.find(
|
const channel = channels.find((channel) =>
|
||||||
(channel) => channel.outpoint?.startsWith(props.info.txid)
|
channel.outpoint?.startsWith(props.info.txid)
|
||||||
);
|
);
|
||||||
return channel;
|
return channel;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -492,9 +508,8 @@ export function ActivityDetailsModal(props: {
|
|||||||
try {
|
try {
|
||||||
if (kind() === "Lightning") {
|
if (kind() === "Lightning") {
|
||||||
console.debug("reading invoice: ", id());
|
console.debug("reading invoice: ", id());
|
||||||
const invoice = await state.mutiny_wallet?.get_invoice_by_hash(
|
const invoice =
|
||||||
id()
|
await state.mutiny_wallet?.get_invoice_by_hash(id());
|
||||||
);
|
|
||||||
return invoice;
|
return invoice;
|
||||||
} else if (kind() === "ChannelClose") {
|
} else if (kind() === "ChannelClose") {
|
||||||
console.debug("reading channel close: ", id());
|
console.debug("reading channel close: ", id());
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
import { A } from "@solidjs/router";
|
import { A, useNavigate } from "@solidjs/router";
|
||||||
import { Shuffle } from "lucide-solid";
|
import { Shuffle, Users } from "lucide-solid";
|
||||||
import { Match, Show, Switch } from "solid-js";
|
import { Match, Show, Switch } from "solid-js";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AmountFiat,
|
AmountFiat,
|
||||||
AmountSats,
|
AmountSats,
|
||||||
|
ButtonCard,
|
||||||
FancyCard,
|
FancyCard,
|
||||||
Indicator,
|
Indicator,
|
||||||
InfoBox,
|
InfoBox,
|
||||||
|
MediumHeader,
|
||||||
|
NiceP,
|
||||||
VStack
|
VStack
|
||||||
} from "~/components";
|
} from "~/components";
|
||||||
import { useI18n } from "~/i18n/context";
|
import { useI18n } from "~/i18n/context";
|
||||||
@@ -45,6 +48,7 @@ const STYLE =
|
|||||||
|
|
||||||
export function BalanceBox(props: { loading?: boolean; small?: boolean }) {
|
export function BalanceBox(props: { loading?: boolean; small?: boolean }) {
|
||||||
const [state, _actions] = useMegaStore();
|
const [state, _actions] = useMegaStore();
|
||||||
|
const navigate = useNavigate();
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
|
|
||||||
const totalOnchain = () =>
|
const totalOnchain = () =>
|
||||||
@@ -57,115 +61,151 @@ export function BalanceBox(props: { loading?: boolean; small?: boolean }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<VStack>
|
<VStack>
|
||||||
<FancyCard title="Lightning">
|
<Switch>
|
||||||
<Show when={!props.loading} fallback={<LoadingShimmer />}>
|
<Match when={state.federations && state.federations.length}>
|
||||||
<Switch>
|
<div>
|
||||||
<Match when={state.safe_mode}>
|
<MediumHeader>Fedimint</MediumHeader>
|
||||||
<div class="flex flex-col gap-1">
|
<FancyCard>
|
||||||
<InfoBox accent="red">
|
<Show
|
||||||
{i18n.t("common.error_safe_mode")}
|
when={!props.loading}
|
||||||
</InfoBox>
|
fallback={<LoadingShimmer />}
|
||||||
</div>
|
>
|
||||||
</Match>
|
<div class="flex justify-between">
|
||||||
<Match when={true}>
|
<div class="flex flex-col gap-1">
|
||||||
<div class="flex flex-col gap-1">
|
<div class="text-2xl">
|
||||||
<div class="text-2xl">
|
<AmountSats
|
||||||
<AmountSats
|
amountSats={
|
||||||
amountSats={
|
state.balance?.federation ||
|
||||||
state.balance?.lightning || 0
|
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>
|
||||||
<div class="text-lg text-white/70">
|
</Show>
|
||||||
<AmountFiat
|
</FancyCard>
|
||||||
amountSats={
|
</div>
|
||||||
state.balance?.lightning || 0
|
<ButtonCard
|
||||||
}
|
onClick={() => navigate("/settings/federations")}
|
||||||
denominationSize="sm"
|
>
|
||||||
/>
|
<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>
|
||||||
</div>
|
</Match>
|
||||||
</Match>
|
<Match when={true}>
|
||||||
</Switch>
|
<div class="flex flex-col gap-1">
|
||||||
</Show>
|
<div class="text-2xl">
|
||||||
</FancyCard>
|
<AmountSats
|
||||||
<Show when={state.federations && state.federations.length}>
|
amountSats={
|
||||||
<FancyCard title="Fedimint">
|
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 />}>
|
<Show when={!props.loading} fallback={<LoadingShimmer />}>
|
||||||
<div class="flex justify-between">
|
<div class="flex justify-between">
|
||||||
<div class="flex flex-col gap-1">
|
<div class="flex flex-col gap-1">
|
||||||
<div class="text-2xl">
|
<div class="text-2xl">
|
||||||
<AmountSats
|
<AmountSats
|
||||||
amountSats={
|
amountSats={totalOnchain()}
|
||||||
state.balance?.federation || 0n
|
icon="chain"
|
||||||
}
|
|
||||||
icon="community"
|
|
||||||
denominationSize="lg"
|
denominationSize="lg"
|
||||||
isFederation
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-lg text-white/70">
|
<div class="text-lg text-white/70">
|
||||||
<AmountFiat
|
<AmountFiat
|
||||||
amountSats={
|
amountSats={totalOnchain()}
|
||||||
state.balance?.federation || 0n
|
|
||||||
}
|
|
||||||
denominationSize="sm"
|
denominationSize="sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex flex-col items-end justify-between gap-1">
|
||||||
<Show when={state.balance?.federation || 0n > 0n}>
|
<Show when={state.balance?.unconfirmed != 0n}>
|
||||||
<div class="self-end justify-self-end">
|
<Indicator>
|
||||||
<A href="/swaplightning" class={STYLE}>
|
{i18n.t("common.pending")}
|
||||||
<Shuffle class="h-6 w-6" />
|
</Indicator>
|
||||||
</A>
|
</Show>
|
||||||
</div>
|
<Show when={state.balance?.unconfirmed === 0n}>
|
||||||
</Show>
|
<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>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</FancyCard>
|
</FancyCard>
|
||||||
</Show>
|
</div>
|
||||||
<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>
|
|
||||||
</VStack>
|
</VStack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,10 @@ export function ContactButton(props: {
|
|||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<button class="flex items-center gap-2" onClick={() => props.onClick()}>
|
<button
|
||||||
|
class="flex items-center gap-2 overflow-clip"
|
||||||
|
onClick={() => props.onClick()}
|
||||||
|
>
|
||||||
<LabelCircle
|
<LabelCircle
|
||||||
name={props.contact.name}
|
name={props.contact.name}
|
||||||
image_url={props.contact.primal_image_url}
|
image_url={props.contact.primal_image_url}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { SubmitHandler } from "@modular-forms/solid";
|
import { SubmitHandler } from "@modular-forms/solid";
|
||||||
import { A } from "@solidjs/router";
|
|
||||||
import { createSignal, Match, Switch } from "solid-js";
|
import { createSignal, Match, Switch } from "solid-js";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -61,15 +60,6 @@ export function ContactEditor(props: {
|
|||||||
cta={i18n.t("contacts.create_contact")}
|
cta={i18n.t("contacts.create_contact")}
|
||||||
handleSubmit={handleSubmit}
|
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>
|
</SimpleDialog>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
|
import { createForm, email } from "@modular-forms/solid";
|
||||||
import { createFileUploader } from "@solid-primitives/upload";
|
import { createFileUploader } from "@solid-primitives/upload";
|
||||||
|
import { Pencil } from "lucide-solid";
|
||||||
import { createSignal, Match, Switch } from "solid-js";
|
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 { useMegaStore } from "~/state/megaStore";
|
||||||
import { blobToBase64 } from "~/utils";
|
import { blobToBase64 } from "~/utils";
|
||||||
|
|
||||||
export type EditableProfile = {
|
export type EditableProfile = {
|
||||||
nym?: string;
|
nym?: string;
|
||||||
|
lightningAddress?: string;
|
||||||
imageUrl?: string;
|
imageUrl?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -17,7 +21,6 @@ export function EditProfileForm(props: {
|
|||||||
cta: string;
|
cta: string;
|
||||||
}) {
|
}) {
|
||||||
const [state] = useMegaStore();
|
const [state] = useMegaStore();
|
||||||
const [nym, setNym] = createSignal(props.initialProfile?.nym || "");
|
|
||||||
const [uploading, setUploading] = createSignal(false);
|
const [uploading, setUploading] = createSignal(false);
|
||||||
|
|
||||||
const { files, selectFiles } = createFileUploader({
|
const { files, selectFiles } = createFileUploader({
|
||||||
@@ -26,14 +29,20 @@ export function EditProfileForm(props: {
|
|||||||
});
|
});
|
||||||
|
|
||||||
async function uploadFile() {
|
async function uploadFile() {
|
||||||
selectFiles(async (files) => {
|
console.log("uploadFile");
|
||||||
|
await selectFiles(async (files) => {
|
||||||
if (files.length) {
|
if (files.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onSave() {
|
const i18n = useI18n();
|
||||||
|
const [profileForm, { Form, Field }] = createForm<EditableProfile>({
|
||||||
|
initialValues: { ...props.initialProfile }
|
||||||
|
});
|
||||||
|
|
||||||
|
async function handleSubmit(profile: EditableProfile) {
|
||||||
try {
|
try {
|
||||||
let imageUrl;
|
let imageUrl;
|
||||||
if (files() && files().length) {
|
if (files() && files().length) {
|
||||||
@@ -46,7 +55,8 @@ export function EditProfileForm(props: {
|
|||||||
setUploading(false);
|
setUploading(false);
|
||||||
}
|
}
|
||||||
await props.onSave({
|
await props.onSave({
|
||||||
nym: nym(),
|
nym: profile.nym,
|
||||||
|
lightningAddress: profile.lightningAddress,
|
||||||
imageUrl: imageUrl ? imageUrl : props.initialProfile?.imageUrl
|
imageUrl: imageUrl ? imageUrl : props.initialProfile?.imageUrl
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -56,33 +66,67 @@ export function EditProfileForm(props: {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<button
|
<VStack>
|
||||||
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"
|
<button class="relative self-center" onClick={uploadFile}>
|
||||||
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>
|
||||||
<Switch>
|
<Match when={files() && files().length}>
|
||||||
<Match when={files() && files().length}>
|
<img src={files()[0].source} />
|
||||||
<img src={files()[0].source} />
|
</Match>
|
||||||
</Match>
|
<Match when={props.initialProfile?.imageUrl}>
|
||||||
<Match when={props.initialProfile?.imageUrl}>
|
<img src={props.initialProfile?.imageUrl} />
|
||||||
<img src={props.initialProfile?.imageUrl} />
|
</Match>
|
||||||
</Match>
|
</Switch>
|
||||||
<Match when={true}>+</Match>
|
</div>
|
||||||
</Switch>
|
<div class="absolute top-0 flex h-[8rem] w-[8rem] items-center justify-center bg-m-grey-975/25">
|
||||||
</button>
|
<Pencil />
|
||||||
<SimpleInput
|
</div>
|
||||||
value={nym()}
|
</button>
|
||||||
onInput={(e) => setNym(e.currentTarget.value)}
|
<VStack>
|
||||||
placeholder="Your name or nym"
|
<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" />
|
<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() {
|
export function HomeBalance() {
|
||||||
const [state, actions] = useMegaStore();
|
const [state, actions] = useMegaStore();
|
||||||
|
|
||||||
const lightningPlusFedi = () =>
|
const combinedBalance = () =>
|
||||||
(state.balance?.federation || 0n) + (state.balance?.lightning || 0n);
|
(state.balance?.federation || 0n) +
|
||||||
|
(state.balance?.lightning || 0n) +
|
||||||
|
(state.balance?.confirmed || 0n) +
|
||||||
|
(state.balance?.unconfirmed || 0n);
|
||||||
|
|
||||||
// TODO: do some sort of status indicator
|
// TODO: do some sort of status indicator
|
||||||
// const fullyReady = () => state.load_stage === "done" && state.price !== 0;
|
// const fullyReady = () => state.load_stage === "done" && state.price !== 0;
|
||||||
@@ -31,14 +34,14 @@ export function HomeBalance() {
|
|||||||
<Switch>
|
<Switch>
|
||||||
<Match when={state.balanceView === "sats"}>
|
<Match when={state.balanceView === "sats"}>
|
||||||
<AmountSats
|
<AmountSats
|
||||||
amountSats={lightningPlusFedi()}
|
amountSats={combinedBalance()}
|
||||||
icon="lightning"
|
icon="lightning"
|
||||||
denominationSize="lg"
|
denominationSize="lg"
|
||||||
/>
|
/>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={state.balanceView === "fiat"}>
|
<Match when={state.balanceView === "fiat"}>
|
||||||
<AmountFiat
|
<AmountFiat
|
||||||
amountSats={lightningPlusFedi()}
|
amountSats={combinedBalance()}
|
||||||
denominationSize="lg"
|
denominationSize="lg"
|
||||||
/>
|
/>
|
||||||
</Match>
|
</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.contact && props.name && props.name.length
|
||||||
? props.name[0]
|
? props.name[0]
|
||||||
: props.label
|
: props.label
|
||||||
? "≡"
|
? "≡"
|
||||||
: "?";
|
: "?";
|
||||||
const bg = () => (props.name && props.contact ? gradient() : "");
|
const bg = () => (props.name && props.contact ? gradient() : "");
|
||||||
|
|
||||||
const [errored, setErrored] = createSignal(false);
|
const [errored, setErrored] = createSignal(false);
|
||||||
@@ -64,7 +64,11 @@ export function LabelCircle(props: {
|
|||||||
return (
|
return (
|
||||||
<Circle
|
<Circle
|
||||||
background={props.image_url && !errored() ? "none" : bg()}
|
background={props.image_url && !errored() ? "none" : bg()}
|
||||||
onClick={() => props.onClick && props.onClick()}
|
onClick={
|
||||||
|
props.onClick
|
||||||
|
? () => props.onClick && props.onClick()
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
size={props.size}
|
size={props.size}
|
||||||
>
|
>
|
||||||
<Switch>
|
<Switch>
|
||||||
|
|||||||
@@ -113,8 +113,8 @@ export function NWCEditor(props: {
|
|||||||
const mode: "createnwa" | "createnwc" | "editnwc" = nwa()
|
const mode: "createnwa" | "createnwc" | "editnwc" = nwa()
|
||||||
? "createnwa"
|
? "createnwa"
|
||||||
: props.initialProfileIndex
|
: props.initialProfileIndex
|
||||||
? "editnwc"
|
? "editnwc"
|
||||||
: "createnwc";
|
: "createnwc";
|
||||||
return mode;
|
return mode;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -137,8 +137,8 @@ export function NostrActivity() {
|
|||||||
zap.kind === "anonymous"
|
zap.kind === "anonymous"
|
||||||
? i18n.t("activity.anonymous")
|
? i18n.t("activity.anonymous")
|
||||||
: zap.kind === "private"
|
: zap.kind === "private"
|
||||||
? i18n.t("activity.private")
|
? i18n.t("activity.private")
|
||||||
: nameFromHexpub(zap.from_hexpub)
|
: nameFromHexpub(zap.from_hexpub)
|
||||||
}
|
}
|
||||||
primaryOnClick={() => {
|
primaryOnClick={() => {
|
||||||
newContactFromHexpub(zap.from_hexpub);
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
class="flex font-mono"
|
class="flex font-mono"
|
||||||
|
|||||||
@@ -56,3 +56,4 @@ export * from "./ContactButton";
|
|||||||
export * from "./GenericItem";
|
export * from "./GenericItem";
|
||||||
export * from "./HomeBalance";
|
export * from "./HomeBalance";
|
||||||
export * from "./EditProfileForm";
|
export * from "./EditProfileForm";
|
||||||
|
export * from "./ImportNsecForm";
|
||||||
|
|||||||
@@ -33,8 +33,8 @@ export function BackPop(props: { default: string; title?: string }) {
|
|||||||
props.title !== undefined
|
props.title !== undefined
|
||||||
? props.title
|
? props.title
|
||||||
: backPath() === "/"
|
: backPath() === "/"
|
||||||
? i18n.t("common.home")
|
? i18n.t("common.home")
|
||||||
: i18n.t("common.back")
|
: i18n.t("common.back")
|
||||||
}
|
}
|
||||||
onClick={() => navigate(backPath())}
|
onClick={() => navigate(backPath())}
|
||||||
showOnDesktop
|
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<{
|
export const VStack: ParentComponent<{
|
||||||
biggap?: boolean;
|
biggap?: boolean;
|
||||||
smallgap?: boolean;
|
smallgap?: boolean;
|
||||||
|
|||||||
@@ -36,8 +36,8 @@ export function toParsedParams(
|
|||||||
const network = !params.network
|
const network = !params.network
|
||||||
? ourNetwork
|
? ourNetwork
|
||||||
: params.network === "testnet" && ourNetwork === "signet"
|
: params.network === "testnet" && ourNetwork === "signet"
|
||||||
? "signet"
|
? "signet"
|
||||||
: params.network;
|
: params.network;
|
||||||
|
|
||||||
if (network !== ourNetwork) {
|
if (network !== ourNetwork) {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -16,11 +16,11 @@ body {
|
|||||||
/* After load we need to remove the bg so the qr scanner can show through */
|
/* After load we need to remove the bg so the qr scanner can show through */
|
||||||
@apply text-white;
|
@apply text-white;
|
||||||
@apply !bg-transparent;
|
@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 {
|
#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) {
|
@media (prefers-color-scheme: light) {
|
||||||
|
|||||||
@@ -49,11 +49,11 @@ import {
|
|||||||
Gift,
|
Gift,
|
||||||
Language,
|
Language,
|
||||||
ManageFederations,
|
ManageFederations,
|
||||||
|
NostrKeys,
|
||||||
Plus,
|
Plus,
|
||||||
Restore,
|
Restore,
|
||||||
Servers,
|
Servers,
|
||||||
Settings,
|
Settings
|
||||||
SyncNostrContacts
|
|
||||||
} from "~/routes/settings";
|
} from "~/routes/settings";
|
||||||
import { Provider as MegaStoreProvider, useMegaStore } from "~/state/megaStore";
|
import { Provider as MegaStoreProvider, useMegaStore } from "~/state/megaStore";
|
||||||
|
|
||||||
@@ -187,10 +187,7 @@ export function Router() {
|
|||||||
<Route path="/plus" component={Plus} />
|
<Route path="/plus" component={Plus} />
|
||||||
<Route path="/restore" component={Restore} />
|
<Route path="/restore" component={Restore} />
|
||||||
<Route path="/servers" component={Servers} />
|
<Route path="/servers" component={Servers} />
|
||||||
<Route
|
<Route path="/nostrkeys" component={NostrKeys} />
|
||||||
path="/syncnostrcontacts"
|
|
||||||
component={SyncNostrContacts}
|
|
||||||
/>
|
|
||||||
<Route path="/federations" component={ManageFederations} />
|
<Route path="/federations" component={ManageFederations} />
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="/*all" component={NotFound} />
|
<Route path="/*all" component={NotFound} />
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
import { TagItem } from "@mutinywallet/mutiny-wasm";
|
import { TagItem } from "@mutinywallet/mutiny-wasm";
|
||||||
import { createAsync, useNavigate, useParams } from "@solidjs/router";
|
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 {
|
import {
|
||||||
createEffect,
|
createEffect,
|
||||||
createResource,
|
createResource,
|
||||||
@@ -310,6 +317,36 @@ function FixedChatHeader(props: {
|
|||||||
navigate("/search");
|
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 (
|
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="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" />
|
<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" />
|
<ArrowUpRight class="inline-block" />
|
||||||
<span>Send</span>
|
<span>Send</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<Show when={props.contact?.npub}>
|
||||||
class="flex gap-2 font-semibold text-m-blue"
|
<button
|
||||||
onClick={() => props.requestFromContact(props.contact)}
|
class="flex gap-2 font-semibold text-m-blue"
|
||||||
>
|
onClick={() =>
|
||||||
<ArrowDownLeft class="inline-block text-m-blue" />
|
props.requestFromContact(props.contact)
|
||||||
<span>Request</span>
|
}
|
||||||
</button>
|
>
|
||||||
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -363,7 +426,17 @@ export function Chat() {
|
|||||||
|
|
||||||
const i18n = useI18n();
|
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 {
|
try {
|
||||||
return state.mutiny_wallet?.get_tag_item(params.id);
|
return state.mutiny_wallet?.get_tag_item(params.id);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -417,13 +490,13 @@ export function Chat() {
|
|||||||
const a_time = isDirectMessage(a.content)
|
const a_time = isDirectMessage(a.content)
|
||||||
? a.content.date
|
? a.content.date
|
||||||
: isActivityItem(a.content)
|
: isActivityItem(a.content)
|
||||||
? a.content.last_updated
|
? a.content.last_updated
|
||||||
: 0;
|
: 0;
|
||||||
const b_time = isDirectMessage(b.content)
|
const b_time = isDirectMessage(b.content)
|
||||||
? b.content.date
|
? b.content.date
|
||||||
: isActivityItem(b.content)
|
: isActivityItem(b.content)
|
||||||
? b.content.last_updated
|
? b.content.last_updated
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
return b_time - a_time; // Descending order
|
return b_time - a_time; // Descending order
|
||||||
});
|
});
|
||||||
@@ -512,7 +585,7 @@ export function Chat() {
|
|||||||
<Show when={contact()}>
|
<Show when={contact()}>
|
||||||
<FixedChatHeader
|
<FixedChatHeader
|
||||||
contact={contact()!}
|
contact={contact()!}
|
||||||
refetch={refetch}
|
refetch={refetchContact}
|
||||||
requestFromContact={requestFromContact}
|
requestFromContact={requestFromContact}
|
||||||
sendToContact={sendToContact}
|
sendToContact={sendToContact}
|
||||||
/>
|
/>
|
||||||
@@ -540,7 +613,7 @@ export function Chat() {
|
|||||||
contact={contact()!}
|
contact={contact()!}
|
||||||
/>
|
/>
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={true}>
|
<Match when={contact() && contact()?.npub}>
|
||||||
<ButtonCard
|
<ButtonCard
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
requestFromContact(contact())
|
requestFromContact(contact())
|
||||||
@@ -561,45 +634,49 @@ export function Chat() {
|
|||||||
</Show>
|
</Show>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</div>
|
</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">
|
<Show when={contact() && contact()?.npub}>
|
||||||
<MiniFab
|
<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">
|
||||||
onScan={() => navigate("/scanner")}
|
<MiniFab
|
||||||
onSend={() => {
|
onScan={() => navigate("/scanner")}
|
||||||
sendToContact(contact());
|
onSend={() => {
|
||||||
}}
|
sendToContact(contact());
|
||||||
sendDisabled={
|
}}
|
||||||
!contact() ||
|
sendDisabled={
|
||||||
!(contact()?.ln_address || contact()?.lnurl)
|
!contact() ||
|
||||||
}
|
!(contact()?.ln_address || contact()?.lnurl)
|
||||||
onRequest={() => requestFromContact(contact())}
|
}
|
||||||
/>
|
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")}
|
|
||||||
/>
|
/>
|
||||||
</form>
|
<form
|
||||||
<div>
|
onSubmit={async (e) => {
|
||||||
<Show when={messageValue() || sending()}>
|
e.preventDefault();
|
||||||
<Button
|
await sendMessage();
|
||||||
layout="xs"
|
}}
|
||||||
intent="blue"
|
>
|
||||||
loading={sending()}
|
<SimpleInput
|
||||||
onClick={sendMessage}
|
disabled={sending()}
|
||||||
>
|
value={messageValue()}
|
||||||
Send
|
onInput={(e) =>
|
||||||
</Button>
|
setMessageValue(e.currentTarget.value)
|
||||||
</Show>
|
}
|
||||||
|
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>
|
||||||
<div class="backgrop-blur-lg z-50 bg-m-grey-975/70 safe-bottom" />
|
</Show>
|
||||||
</div>
|
|
||||||
</MutinyWalletGuard>
|
</MutinyWalletGuard>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,16 +3,13 @@ import { createMemo, createSignal, Show } from "solid-js";
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
BackLink,
|
BackLink,
|
||||||
ButtonLink,
|
|
||||||
DefaultMain,
|
DefaultMain,
|
||||||
EditableProfile,
|
EditableProfile,
|
||||||
EditProfileForm,
|
EditProfileForm,
|
||||||
LargeHeader,
|
LargeHeader,
|
||||||
MutinyWalletGuard,
|
MutinyWalletGuard,
|
||||||
NavBar,
|
NavBar
|
||||||
NiceP
|
|
||||||
} from "~/components";
|
} from "~/components";
|
||||||
// import { useI18n } from "~/i18n/context";
|
|
||||||
import { useMegaStore } from "~/state/megaStore";
|
import { useMegaStore } from "~/state/megaStore";
|
||||||
|
|
||||||
export function EditProfile() {
|
export function EditProfile() {
|
||||||
@@ -40,7 +37,7 @@ export function EditProfile() {
|
|||||||
const newProfile = await state.mutiny_wallet?.edit_nostr_profile(
|
const newProfile = await state.mutiny_wallet?.edit_nostr_profile(
|
||||||
profile.nym,
|
profile.nym,
|
||||||
profile.imageUrl,
|
profile.imageUrl,
|
||||||
originalProfile().lud16,
|
profile.lightningAddress,
|
||||||
originalProfile().nip05
|
originalProfile().nip05
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -58,16 +55,12 @@ export function EditProfile() {
|
|||||||
<DefaultMain>
|
<DefaultMain>
|
||||||
<BackLink href="/profile" title="Profile" />
|
<BackLink href="/profile" title="Profile" />
|
||||||
<LargeHeader>Edit Profile</LargeHeader>
|
<LargeHeader>Edit Profile</LargeHeader>
|
||||||
<NiceP>
|
|
||||||
Update your profile.
|
|
||||||
<br />
|
|
||||||
Your activity is private by default.
|
|
||||||
</NiceP>
|
|
||||||
<div class="flex-1" />
|
<div class="flex-1" />
|
||||||
<Show when={originalProfile()}>
|
<Show when={originalProfile()}>
|
||||||
<EditProfileForm
|
<EditProfileForm
|
||||||
initialProfile={{
|
initialProfile={{
|
||||||
nym: originalProfile().name,
|
nym: originalProfile().name,
|
||||||
|
lightningAddress: originalProfile().lud16,
|
||||||
imageUrl: originalProfile().picture
|
imageUrl: originalProfile().picture
|
||||||
}}
|
}}
|
||||||
onSave={saveProfile}
|
onSave={saveProfile}
|
||||||
@@ -75,10 +68,6 @@ export function EditProfile() {
|
|||||||
cta="Save"
|
cta="Save"
|
||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
<ButtonLink href="/importprofile" intent="text">
|
|
||||||
Import different nostr profile
|
|
||||||
</ButtonLink>
|
|
||||||
|
|
||||||
<NavBar activeTab="profile" />
|
<NavBar activeTab="profile" />
|
||||||
</DefaultMain>
|
</DefaultMain>
|
||||||
</MutinyWalletGuard>
|
</MutinyWalletGuard>
|
||||||
|
|||||||
@@ -1,21 +1,9 @@
|
|||||||
import { MutinyWallet } from "@mutinywallet/mutiny-wasm";
|
|
||||||
import { useNavigate } from "@solidjs/router";
|
import { useNavigate } from "@solidjs/router";
|
||||||
import { SecureStoragePlugin } from "capacitor-secure-storage-plugin";
|
import { Show } from "solid-js";
|
||||||
import { createSignal, Show } from "solid-js";
|
|
||||||
|
|
||||||
import {
|
import { Button, ButtonLink, DefaultMain, ImportNsecForm } from "~/components";
|
||||||
Button,
|
|
||||||
ButtonLink,
|
|
||||||
DefaultMain,
|
|
||||||
InfoBox,
|
|
||||||
SimpleInput
|
|
||||||
} from "~/components";
|
|
||||||
|
|
||||||
export function ImportProfile() {
|
export function ImportProfile() {
|
||||||
const [nsec, setNsec] = createSignal("");
|
|
||||||
const [saving, setSaving] = createSignal(false);
|
|
||||||
const [error, setError] = createSignal<string | undefined>();
|
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
function handleSkip() {
|
function handleSkip() {
|
||||||
@@ -23,25 +11,6 @@ export function ImportProfile() {
|
|||||||
navigate("/");
|
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
|
// @ts-expect-error we're checking for an extension
|
||||||
const windowHasNostr = window.nostr && window.nostr.getPublicKey;
|
const windowHasNostr = window.nostr && window.nostr.getPublicKey;
|
||||||
|
|
||||||
@@ -55,17 +24,7 @@ export function ImportProfile() {
|
|||||||
<br />
|
<br />
|
||||||
</p>
|
</p>
|
||||||
<div class="flex-1" />
|
<div class="flex-1" />
|
||||||
<SimpleInput
|
<ImportNsecForm />
|
||||||
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>
|
|
||||||
<div class="flex-1" />
|
<div class="flex-1" />
|
||||||
<div class="flex flex-col items-center">
|
<div class="flex flex-col items-center">
|
||||||
{/* Don't want them to accidentally "edit" their profile if they have one */}
|
{/* 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 }) {
|
export function WalletHeader(props: { loading: boolean }) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [state, _actions] = useMegaStore();
|
const [state, _actions] = useMegaStore();
|
||||||
const npub = () => state.mutiny_wallet?.get_npub();
|
|
||||||
|
|
||||||
async function getProfile() {
|
async function getProfile() {
|
||||||
const profile = state.mutiny_wallet?.get_nostr_profile();
|
const profile = state.mutiny_wallet?.get_nostr_profile();
|
||||||
@@ -44,7 +43,7 @@ export function WalletHeader(props: { loading: boolean }) {
|
|||||||
return profile()!.picture;
|
return profile()!.picture;
|
||||||
}
|
}
|
||||||
|
|
||||||
return `https://bitcoinfaces.xyz/api/get-image?name=${npub()}&onchain=false`;
|
return undefined;
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { useNavigate } from "@solidjs/router";
|
import { A, useNavigate } from "@solidjs/router";
|
||||||
import { createSignal } from "solid-js";
|
import { createSignal } from "solid-js";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
ButtonLink,
|
|
||||||
DefaultMain,
|
DefaultMain,
|
||||||
EditableProfile,
|
EditableProfile,
|
||||||
EditProfileForm
|
EditProfileForm
|
||||||
@@ -28,7 +27,7 @@ export function NewProfile() {
|
|||||||
const profile = await state.mutiny_wallet?.edit_nostr_profile(
|
const profile = await state.mutiny_wallet?.edit_nostr_profile(
|
||||||
p.nym ? p.nym : undefined,
|
p.nym ? p.nym : undefined,
|
||||||
p.imageUrl ? p.imageUrl : undefined,
|
p.imageUrl ? p.imageUrl : undefined,
|
||||||
undefined,
|
p.lightningAddress ? p.lightningAddress : undefined,
|
||||||
undefined
|
undefined
|
||||||
);
|
);
|
||||||
console.log("profile", profile);
|
console.log("profile", profile);
|
||||||
@@ -47,8 +46,6 @@ export function NewProfile() {
|
|||||||
<h1 class="text-3xl font-semibold">Create your profile</h1>
|
<h1 class="text-3xl font-semibold">Create your profile</h1>
|
||||||
<p class="text-center text-xl font-light text-neutral-200">
|
<p class="text-center text-xl font-light text-neutral-200">
|
||||||
Mutiny makes payments social.
|
Mutiny makes payments social.
|
||||||
<br />
|
|
||||||
Your activity is private by default.
|
|
||||||
</p>
|
</p>
|
||||||
<div class="flex-1" />
|
<div class="flex-1" />
|
||||||
<EditProfileForm
|
<EditProfileForm
|
||||||
@@ -56,14 +53,15 @@ export function NewProfile() {
|
|||||||
saving={creating()}
|
saving={creating()}
|
||||||
cta="Create"
|
cta="Create"
|
||||||
/>
|
/>
|
||||||
<div class="flex flex-col items-center">
|
<Button onClick={handleSkip} intent="text">
|
||||||
<ButtonLink href="/importprofile" intent="text">
|
Skip for now
|
||||||
Import existing nostr profile
|
</Button>
|
||||||
</ButtonLink>
|
<A
|
||||||
<Button onClick={handleSkip} intent="text">
|
class="text-base font-normal text-m-grey-400"
|
||||||
Skip for now
|
href="/importprofile"
|
||||||
</Button>
|
>
|
||||||
</div>
|
Import existing nostr profile
|
||||||
|
</A>
|
||||||
<div class="flex-1" />
|
<div class="flex-1" />
|
||||||
</div>
|
</div>
|
||||||
</DefaultMain>
|
</DefaultMain>
|
||||||
|
|||||||
@@ -1,26 +1,29 @@
|
|||||||
import { A } from "@solidjs/router";
|
import { useNavigate } from "@solidjs/router";
|
||||||
import { createMemo, Show } from "solid-js";
|
import { Copy, Edit, QrCode } from "lucide-solid";
|
||||||
|
import { createMemo, createSignal, Match, Show, Switch } from "solid-js";
|
||||||
import { QRCodeSVG } from "solid-qr-code";
|
import { QRCodeSVG } from "solid-qr-code";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
BackLink,
|
BackLink,
|
||||||
BalanceBox,
|
BalanceBox,
|
||||||
|
ButtonCard,
|
||||||
DefaultMain,
|
DefaultMain,
|
||||||
FancyCard,
|
FancyCard,
|
||||||
KeyValue,
|
|
||||||
LabelCircle,
|
LabelCircle,
|
||||||
MiniStringShower,
|
|
||||||
MutinyWalletGuard,
|
MutinyWalletGuard,
|
||||||
NavBar
|
NavBar,
|
||||||
|
NiceP,
|
||||||
|
SimpleDialog
|
||||||
} from "~/components";
|
} from "~/components";
|
||||||
import { useI18n } from "~/i18n/context";
|
import { useI18n } from "~/i18n/context";
|
||||||
import { useMegaStore } from "~/state/megaStore";
|
import { useMegaStore } from "~/state/megaStore";
|
||||||
|
import { useCopy } from "~/utils";
|
||||||
|
|
||||||
export function Profile() {
|
export function Profile() {
|
||||||
const [state, _actions] = useMegaStore();
|
const [state, _actions] = useMegaStore();
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
|
|
||||||
const npub = () => state.mutiny_wallet?.get_npub();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const profile = createMemo(() => {
|
const profile = createMemo(() => {
|
||||||
const profile = state.mutiny_wallet?.get_nostr_profile();
|
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 (
|
return (
|
||||||
<MutinyWalletGuard>
|
<MutinyWalletGuard>
|
||||||
<DefaultMain>
|
<DefaultMain>
|
||||||
@@ -43,50 +50,79 @@ export function Profile() {
|
|||||||
<LabelCircle
|
<LabelCircle
|
||||||
contact
|
contact
|
||||||
label={false}
|
label={false}
|
||||||
name={profile().name ? profile().name : "Anon"}
|
image_url={profile().picture}
|
||||||
image_url={
|
|
||||||
profile().picture
|
|
||||||
? profile().picture
|
|
||||||
: `https://bitcoinfaces.xyz/api/get-image?name=${npub()}&onchain=false`
|
|
||||||
}
|
|
||||||
size="xl"
|
size="xl"
|
||||||
/>
|
/>
|
||||||
<h1 class="text-3xl font-semibold">
|
<h1 class="text-3xl font-semibold">
|
||||||
<Show when={profile().name}>{profile().name}</Show>
|
<Show when={profile().name}>{profile().name}</Show>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<Show when={profile().lud16}>
|
<FancyCard>
|
||||||
<p class="break-all text-center font-system-mono text-base text-m-grey-350">
|
<Switch>
|
||||||
{profile().lud16}
|
<Match when={profile().lud16}>
|
||||||
</p>
|
<p class="break-all text-center font-system-mono text-base ">
|
||||||
<div class="w-[10rem] rounded bg-white p-[1rem]">
|
{profile().lud16}
|
||||||
<QRCodeSVG
|
</p>
|
||||||
value={profile().lud16}
|
<div class="flex w-full justify-center gap-8">
|
||||||
class="h-full max-h-[256px] w-full"
|
<button onClick={() => setShowQr(true)}>
|
||||||
/>
|
<QrCode class="inline-block" />
|
||||||
</div>
|
</button>
|
||||||
</Show>
|
<button
|
||||||
<Show when={!profile().lud16}>
|
class="p-1"
|
||||||
<p class="text-center text-base italic text-m-grey-350">
|
classList={{
|
||||||
Mutiny Lightning Address coming soon.
|
"bg-m-red rounded": copied()
|
||||||
</p>
|
}}
|
||||||
</Show>
|
onClick={() =>
|
||||||
|
copy(profile().lud16)
|
||||||
<A
|
}
|
||||||
href="/editprofile"
|
>
|
||||||
class="text-xl font-semibold text-m-red no-underline active:text-m-red/80"
|
<Copy class="inline-block" />
|
||||||
>
|
</button>
|
||||||
{i18n.t("profile.edit_profile")}
|
</div>{" "}
|
||||||
</A>
|
<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>
|
</Show>
|
||||||
</div>
|
</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} />
|
<BalanceBox loading={state.wallet_loading} />
|
||||||
<FancyCard title={i18n.t("profile.nostr_identity")}>
|
|
||||||
<KeyValue key="npub">
|
|
||||||
<MiniStringShower text={npub() || ""} />
|
|
||||||
</KeyValue>
|
|
||||||
</FancyCard>
|
|
||||||
<NavBar activeTab="profile" />
|
<NavBar activeTab="profile" />
|
||||||
</DefaultMain>
|
</DefaultMain>
|
||||||
</MutinyWalletGuard>
|
</MutinyWalletGuard>
|
||||||
|
|||||||
@@ -184,8 +184,8 @@ export function Receive() {
|
|||||||
? paymentTx()?.txid
|
? paymentTx()?.txid
|
||||||
: undefined
|
: undefined
|
||||||
: paymentInvoice()
|
: paymentInvoice()
|
||||||
? paymentInvoice()?.payment_hash
|
? paymentInvoice()?.payment_hash
|
||||||
: undefined;
|
: undefined;
|
||||||
const kind = paidState() === "onchain_paid" ? "OnChain" : "Lightning";
|
const kind = paidState() === "onchain_paid" ? "OnChain" : "Lightning";
|
||||||
|
|
||||||
console.log("Opening details modal: ", paymentTxId, kind);
|
console.log("Opening details modal: ", paymentTxId, kind);
|
||||||
|
|||||||
@@ -201,8 +201,8 @@ export function Send() {
|
|||||||
? sentDetails()?.txid
|
? sentDetails()?.txid
|
||||||
: undefined
|
: undefined
|
||||||
: sentDetails()
|
: sentDetails()
|
||||||
? sentDetails()?.payment_hash
|
? sentDetails()?.payment_hash
|
||||||
: undefined;
|
: undefined;
|
||||||
const kind = sentDetails()?.txid ? "OnChain" : "Lightning";
|
const kind = sentDetails()?.txid ? "OnChain" : "Lightning";
|
||||||
|
|
||||||
console.log("Opening details modal: ", paymentTxId, kind);
|
console.log("Opening details modal: ", paymentTxId, kind);
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { Title } from "@solidjs/meta";
|
import { Title } from "@solidjs/meta";
|
||||||
|
|
||||||
import { ButtonLink, LargeHeader, VStack } from "~/components";
|
import { ButtonLink, DefaultMain, LargeHeader } from "~/components";
|
||||||
import { useI18n } from "~/i18n/context";
|
import { useI18n } from "~/i18n/context";
|
||||||
|
|
||||||
export function NotFound() {
|
export function NotFound() {
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
return (
|
return (
|
||||||
<VStack>
|
<DefaultMain>
|
||||||
<Title>{i18n.t("error.not_found.title")}</Title>
|
<Title>{i18n.t("error.not_found.title")}</Title>
|
||||||
<LargeHeader>{i18n.t("error.not_found.title")}</LargeHeader>
|
<LargeHeader>{i18n.t("error.not_found.title")}</LargeHeader>
|
||||||
<p>{i18n.t("error.not_found.wtf_paul")}</p>
|
<p>{i18n.t("error.not_found.wtf_paul")}</p>
|
||||||
@@ -14,6 +14,6 @@ export function NotFound() {
|
|||||||
<ButtonLink href="/" intent="red">
|
<ButtonLink href="/" intent="red">
|
||||||
{i18n.t("common.dangit")}
|
{i18n.t("common.dangit")}
|
||||||
</ButtonLink>
|
</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")
|
? i18n.t("settings.encrypt.caption")
|
||||||
: undefined
|
: 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",
|
href: "/settings/currency",
|
||||||
text: i18n.t("settings.currency.title"),
|
text: i18n.t("settings.currency.title"),
|
||||||
@@ -119,24 +130,16 @@ export function Settings() {
|
|||||||
href: "/settings/language",
|
href: "/settings/language",
|
||||||
text: i18n.t("settings.language.title"),
|
text: i18n.t("settings.language.title"),
|
||||||
caption: i18n.t("settings.language.caption")
|
caption: i18n.t("settings.language.caption")
|
||||||
},
|
|
||||||
{
|
|
||||||
href: "/settings/servers",
|
|
||||||
text: i18n.t("settings.servers.title"),
|
|
||||||
caption: i18n.t("settings.servers.caption")
|
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
<SettingsLinkList
|
<SettingsLinkList
|
||||||
header={i18n.t("settings.experimental_features")}
|
header={i18n.t("settings.social")}
|
||||||
links={[
|
links={[
|
||||||
{
|
{
|
||||||
href: "/settings/syncnostrcontacts",
|
href: "/settings/nostrkeys",
|
||||||
text: i18n.t("settings.nostr_contacts.title")
|
text: i18n.t("settings.nostr_keys.title"),
|
||||||
},
|
caption: i18n.t("settings.nostr_keys.caption")
|
||||||
{
|
|
||||||
href: "/settings/federations",
|
|
||||||
text: i18n.t("settings.manage_federations.title")
|
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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 "./Plus";
|
||||||
export * from "./Restore";
|
export * from "./Restore";
|
||||||
export * from "./Servers";
|
export * from "./Servers";
|
||||||
export * from "./SyncNostrContacts";
|
|
||||||
export * from "./ManageFederations";
|
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