mirror of
https://github.com/aljazceru/mutiny-web.git
synced 2026-01-27 10:04:29 +01:00
feat: third pass at translations
This commit is contained in:
@@ -177,7 +177,9 @@ export function ActivityItem(props: {
|
||||
</Switch>
|
||||
<Switch>
|
||||
<Match when={props.date && props.date > 2147483647}>
|
||||
<time class="text-sm text-neutral-500">Pending</time>
|
||||
<time class="text-sm text-neutral-500">
|
||||
{i18n.t("common.pending")}
|
||||
</time>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<time class="text-sm text-neutral-500">
|
||||
|
||||
@@ -81,6 +81,7 @@ export function AmountCard(props: {
|
||||
exitRoute?: string;
|
||||
maxAmountSats?: bigint;
|
||||
}) {
|
||||
const i18n = useI18n();
|
||||
// Normally we want to add the fee to the amount, but for max amount we just show the max
|
||||
const totalOrTotalLessFee = () => {
|
||||
if (
|
||||
@@ -99,7 +100,7 @@ export function AmountCard(props: {
|
||||
<Switch>
|
||||
<Match when={props.fee}>
|
||||
<div class="flex flex-col gap-1">
|
||||
<KeyValue key="Amount">
|
||||
<KeyValue key={i18n.t("receive.amount")}>
|
||||
<Show
|
||||
when={props.isAmountEditable}
|
||||
fallback={
|
||||
@@ -123,13 +124,13 @@ export function AmountCard(props: {
|
||||
/>
|
||||
</Show>
|
||||
</KeyValue>
|
||||
<KeyValue gray key="+ Fee">
|
||||
<KeyValue gray key={i18n.t("receive.fee")}>
|
||||
<InlineAmount amount={props.fee || "0"} />
|
||||
</KeyValue>
|
||||
</div>
|
||||
<hr class="border-white/20" />
|
||||
<div class="flex flex-col gap-1">
|
||||
<KeyValue key="Total">
|
||||
<KeyValue key={i18n.t("receive.total")}>
|
||||
<InlineAmount amount={totalOrTotalLessFee()} />
|
||||
</KeyValue>
|
||||
<USDShower
|
||||
@@ -140,7 +141,7 @@ export function AmountCard(props: {
|
||||
</Match>
|
||||
<Match when={props.reserve}>
|
||||
<div class="flex flex-col gap-1">
|
||||
<KeyValue key="Channel size">
|
||||
<KeyValue key={i18n.t("receive.channel_size")}>
|
||||
<InlineAmount
|
||||
amount={add(
|
||||
props.amountSats,
|
||||
@@ -148,13 +149,16 @@ export function AmountCard(props: {
|
||||
).toString()}
|
||||
/>
|
||||
</KeyValue>
|
||||
<KeyValue gray key="- Channel Reserve">
|
||||
<KeyValue
|
||||
gray
|
||||
key={i18n.t("receive.channel_reserve")}
|
||||
>
|
||||
<InlineAmount amount={props.reserve || "0"} />
|
||||
</KeyValue>
|
||||
</div>
|
||||
<hr class="border-white/20" />
|
||||
<div class="flex flex-col gap-1">
|
||||
<KeyValue key="Spendable">
|
||||
<KeyValue key={i18n.t("receive.spendable")}>
|
||||
<InlineAmount amount={props.amountSats} />
|
||||
</KeyValue>
|
||||
<USDShower
|
||||
@@ -165,7 +169,7 @@ export function AmountCard(props: {
|
||||
</Match>
|
||||
<Match when={!props.fee && !props.reserve}>
|
||||
<div class="flex flex-col gap-1">
|
||||
<KeyValue key="Amount">
|
||||
<KeyValue key={i18n.t("receive.amount")}>
|
||||
<Show
|
||||
when={props.isAmountEditable}
|
||||
fallback={
|
||||
|
||||
@@ -24,18 +24,6 @@ import { FeesModal } from "./MoreInfoModal";
|
||||
import { useI18n } from "~/i18n/context";
|
||||
import { useNavigate } from "solid-start";
|
||||
|
||||
const FIXED_AMOUNTS_SATS = [
|
||||
{ label: "10k", amount: "10000" },
|
||||
{ label: "100k", amount: "100000" },
|
||||
{ label: "1m", amount: "1000000" }
|
||||
];
|
||||
|
||||
const FIXED_AMOUNTS_USD = [
|
||||
{ label: "$1", amount: "1" },
|
||||
{ label: "$10", amount: "10" },
|
||||
{ label: "$100", amount: "100" }
|
||||
];
|
||||
|
||||
function fiatInputSanitizer(input: string): string {
|
||||
// Make sure only numbers and a single decimal point are allowed
|
||||
const numeric = input.replace(/[^0-9.]/g, "").replace(/(\..*)\./g, "$1");
|
||||
@@ -76,7 +64,7 @@ function SingleDigitButton(props: {
|
||||
function onHold() {
|
||||
if (
|
||||
props.character === "DEL" ||
|
||||
props.character === i18n.t("char.del")
|
||||
props.character === i18n.t("receive.amount_editable.del")
|
||||
) {
|
||||
holdTimer = setTimeout(() => {
|
||||
props.onClear();
|
||||
@@ -148,7 +136,7 @@ function SmallSubtleAmount(props: { text: string; fiat: boolean }) {
|
||||
<h2 class="flex flex-row items-end text-xl font-light text-neutral-400">
|
||||
~{props.text}
|
||||
<span class="text-base">
|
||||
{props.fiat ? i18n.t("common.usd") : `${i18n.t("common.sats")}`}
|
||||
{props.fiat ? i18n.t("common.usd") : i18n.t("common.sats")}
|
||||
</span>
|
||||
<img
|
||||
class={"pl-[4px] pb-[4px] hover:cursor-pointer"}
|
||||
@@ -208,6 +196,28 @@ export const AmountEditable: ParentComponent<{
|
||||
false
|
||||
)
|
||||
);
|
||||
|
||||
const FIXED_AMOUNTS_SATS = [
|
||||
{
|
||||
label: i18n.t("receive.amount_editable.fix_amounts.ten_k"),
|
||||
amount: "10000"
|
||||
},
|
||||
{
|
||||
label: i18n.t("receive.amount_editable.fix_amounts.one_hundred_k"),
|
||||
amount: "100000"
|
||||
},
|
||||
{
|
||||
label: i18n.t("receive.amount_editable.fix_amounts.one_million"),
|
||||
amount: "1000000"
|
||||
}
|
||||
];
|
||||
|
||||
const FIXED_AMOUNTS_USD = [
|
||||
{ label: "$1", amount: "1" },
|
||||
{ label: "$10", amount: "10" },
|
||||
{ label: "$100", amount: "100" }
|
||||
];
|
||||
|
||||
const CHARACTERS = [
|
||||
"1",
|
||||
"2",
|
||||
@@ -220,7 +230,7 @@ export const AmountEditable: ParentComponent<{
|
||||
"9",
|
||||
".",
|
||||
"0",
|
||||
i18n.t("char.del")
|
||||
i18n.t("receive.amount_editable.del")
|
||||
];
|
||||
|
||||
const displaySats = () => toDisplayHandleNaN(localSats(), false);
|
||||
@@ -295,7 +305,10 @@ export const AmountEditable: ParentComponent<{
|
||||
|
||||
let sane;
|
||||
|
||||
if (character === "DEL" || character === i18n.t("char.del")) {
|
||||
if (
|
||||
character === "DEL" ||
|
||||
character === i18n.t("receive.amount_editable.del")
|
||||
) {
|
||||
if (localValue().length <= 1) {
|
||||
sane = "0";
|
||||
} else {
|
||||
|
||||
@@ -15,8 +15,10 @@ import { PendingNwc } from "./PendingNwc";
|
||||
import { DecryptDialog } from "./DecryptDialog";
|
||||
import { LoadingIndicator } from "./LoadingIndicator";
|
||||
import { FeedbackLink } from "~/routes/Feedback";
|
||||
import { useI18n } from "~/i18n/context";
|
||||
|
||||
export default function App() {
|
||||
const i18n = useI18n();
|
||||
const [state, _actions] = useMegaStore();
|
||||
|
||||
return (
|
||||
@@ -71,7 +73,7 @@ export default function App() {
|
||||
<PendingNwc />
|
||||
</Show>
|
||||
</Suspense>
|
||||
<Card title="Activity">
|
||||
<Card title={i18n.t("activity.title")}>
|
||||
<div class="p-1" />
|
||||
<VStack>
|
||||
<Suspense>
|
||||
|
||||
@@ -79,7 +79,9 @@ export default function BalanceBox(props: { loading?: boolean }) {
|
||||
</div>
|
||||
<div class="flex flex-col items-end gap-1 justify-between">
|
||||
<Show when={state.balance?.unconfirmed != 0n}>
|
||||
<Indicator>Pending</Indicator>
|
||||
<Indicator>
|
||||
{i18n.t("common.pending")}
|
||||
</Indicator>
|
||||
</Show>
|
||||
<Show when={state.balance?.unconfirmed === 0n}>
|
||||
<div />
|
||||
|
||||
@@ -4,28 +4,26 @@ import { DIALOG_CONTENT, DIALOG_POSITIONER, OVERLAY } from "./DetailsModal";
|
||||
import { ModalCloseButton, SmallHeader } from "./layout";
|
||||
import { ExternalLink } from "./layout/ExternalLink";
|
||||
import { getExistingSettings } from "~/logic/mutinyWalletSetup";
|
||||
import { useI18n } from "~/i18n/context";
|
||||
|
||||
export function BetaWarningModal() {
|
||||
const i18n = useI18n();
|
||||
return (
|
||||
<WarningModal title="Warning: beta software" linkText="Why?">
|
||||
<p>
|
||||
We're so glad you're here. But we do want to warn you: Mutiny
|
||||
Wallet is in beta, and there are still bugs and rough edges.
|
||||
</p>
|
||||
<p>
|
||||
Please be careful and don't put more money into Mutiny than
|
||||
you're willing to lose.
|
||||
</p>
|
||||
<WarningModal
|
||||
title={i18n.t("modals.beta_warning.title")}
|
||||
linkText={i18n.t("common.why")}
|
||||
>
|
||||
<p>{i18n.t("translations:modals.beta_warning.beta_warning")}</p>
|
||||
<p>{i18n.t("modals.beta_warning.be_careful")}</p>
|
||||
<p>
|
||||
<ExternalLink href="https://github.com/MutinyWallet/mutiny-web/wiki/Mutiny-Beta-Readme">
|
||||
Learn more about the beta
|
||||
{i18n.t("modals.beta_warning.beta_link")}
|
||||
</ExternalLink>
|
||||
</p>
|
||||
<p class="small text-neutral-400">
|
||||
If you want to use pretend money to test out Mutiny without
|
||||
risk,{" "}
|
||||
{i18n.t("modals.beta_warning.pretend_money")}{" "}
|
||||
<ExternalLink href="https://blog.mutinywallet.com/mutiny-wallet-signet-release/">
|
||||
check out our Signet version.
|
||||
{i18n.t("modals.beta_warning.signet_link")}
|
||||
</ExternalLink>
|
||||
</p>
|
||||
</WarningModal>
|
||||
|
||||
@@ -6,11 +6,13 @@ import { SubmitHandler } from "@modular-forms/solid";
|
||||
import { ContactForm } from "./ContactForm";
|
||||
import { ContactFormValues } from "./ContactViewer";
|
||||
import { DIALOG_CONTENT, DIALOG_POSITIONER } from "~/styles/dialogs";
|
||||
import { useI18n } from "~/i18n/context";
|
||||
|
||||
export function ContactEditor(props: {
|
||||
createContact: (contact: ContactFormValues) => void;
|
||||
list?: boolean;
|
||||
}) {
|
||||
const i18n = useI18n();
|
||||
const [isOpen, setIsOpen] = createSignal(false);
|
||||
|
||||
// What we're all here for in the first place: returning a value
|
||||
@@ -32,12 +34,14 @@ export function ContactEditor(props: {
|
||||
<div class="bg-neutral-500 flex-none h-16 w-16 rounded-full flex items-center justify-center text-4xl uppercase ">
|
||||
<span class="leading-[4rem]">+</span>
|
||||
</div>
|
||||
<SmallHeader class="overflow-ellipsis">new</SmallHeader>
|
||||
<SmallHeader class="overflow-ellipsis">
|
||||
{i18n.t("contacts.new")}
|
||||
</SmallHeader>
|
||||
</button>
|
||||
</Match>
|
||||
<Match when={!props.list}>
|
||||
<TinyButton onClick={() => setIsOpen(true)}>
|
||||
+ Add Contact
|
||||
+ {i18n.t("contacts.add_contact")}
|
||||
</TinyButton>
|
||||
</Match>
|
||||
</Switch>
|
||||
@@ -57,8 +61,8 @@ export function ContactEditor(props: {
|
||||
</button>
|
||||
</div>
|
||||
<ContactForm
|
||||
title="New contact"
|
||||
cta="Create contact"
|
||||
title={i18n.t("contacts.new_contact")}
|
||||
cta={i18n.t("contacts.create_contact")}
|
||||
handleSubmit={handleSubmit}
|
||||
/>
|
||||
</Dialog.Content>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { SubmitHandler, createForm, required } from "@modular-forms/solid";
|
||||
import { Button, LargeHeader, VStack } from "~/components/layout";
|
||||
import { TextField } from "~/components/layout/TextField";
|
||||
import { ContactFormValues } from "./ContactViewer";
|
||||
import { useI18n } from "~/i18n/context";
|
||||
|
||||
export function ContactForm(props: {
|
||||
handleSubmit: SubmitHandler<ContactFormValues>;
|
||||
@@ -9,6 +10,7 @@ export function ContactForm(props: {
|
||||
title: string;
|
||||
cta: string;
|
||||
}) {
|
||||
const i18n = useI18n();
|
||||
const [_contactForm, { Form, Field }] = createForm<ContactFormValues>({
|
||||
initialValues: props.initialValues
|
||||
});
|
||||
@@ -23,15 +25,15 @@ export function ContactForm(props: {
|
||||
<VStack>
|
||||
<Field
|
||||
name="name"
|
||||
validate={[required("We at least need a name")]}
|
||||
validate={[required(i18n.t("contacts.error_name"))]}
|
||||
>
|
||||
{(field, props) => (
|
||||
<TextField
|
||||
{...props}
|
||||
placeholder="Satoshi"
|
||||
placeholder={i18n.t("contacts.placeholder")}
|
||||
value={field.value}
|
||||
error={field.error}
|
||||
label="Name"
|
||||
label={i18n.t("contacts.name")}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
|
||||
@@ -7,6 +7,7 @@ import { ContactForm } from "./ContactForm";
|
||||
import { showToast } from "./Toaster";
|
||||
import { Contact } from "@mutinywallet/mutiny-wasm";
|
||||
import { DIALOG_CONTENT, DIALOG_POSITIONER } from "~/styles/dialogs";
|
||||
import { useI18n } from "~/i18n/context";
|
||||
|
||||
export type ContactFormValues = {
|
||||
name: string;
|
||||
@@ -18,6 +19,7 @@ export function ContactViewer(props: {
|
||||
gradient: string;
|
||||
saveContact: (contact: Contact) => void;
|
||||
}) {
|
||||
const i18n = useI18n();
|
||||
const [isOpen, setIsOpen] = createSignal(false);
|
||||
const [isEditing, setIsEditing] = createSignal(false);
|
||||
|
||||
@@ -71,8 +73,8 @@ export function ContactViewer(props: {
|
||||
<Switch>
|
||||
<Match when={isEditing()}>
|
||||
<ContactForm
|
||||
title="Edit contact"
|
||||
cta="Save contact"
|
||||
title={i18n.t("contacts.edit_contact")}
|
||||
cta={i18n.t("contacts.save_contact")}
|
||||
handleSubmit={handleSubmit}
|
||||
initialValues={props.contact}
|
||||
/>
|
||||
@@ -91,9 +93,13 @@ export function ContactViewer(props: {
|
||||
<h1 class="text-2xl font-semibold uppercase mt-2 mb-4">
|
||||
{props.contact.name}
|
||||
</h1>
|
||||
<Card title="Payment history">
|
||||
<Card
|
||||
title={i18n.t(
|
||||
"contacts.payment_history"
|
||||
)}
|
||||
>
|
||||
<NiceP>
|
||||
No payments yet with{" "}
|
||||
{i18n.t("contacts.no_payments")}{" "}
|
||||
<span class="font-semibold">
|
||||
{props.contact.name}
|
||||
</span>
|
||||
@@ -107,19 +113,22 @@ export function ContactViewer(props: {
|
||||
intent="green"
|
||||
onClick={() => setIsEditing(true)}
|
||||
>
|
||||
Edit
|
||||
{i18n.t("contacts.edit")}
|
||||
</Button>
|
||||
<Button
|
||||
intent="blue"
|
||||
onClick={() => {
|
||||
showToast({
|
||||
title: "Unimplemented",
|
||||
description:
|
||||
"We don't do that yet"
|
||||
title: i18n.t(
|
||||
"contacts.unimplemented"
|
||||
),
|
||||
description: i18n.t(
|
||||
"contacts.not_available"
|
||||
)
|
||||
});
|
||||
}}
|
||||
>
|
||||
Pay
|
||||
{i18n.t("contacts.pay")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { Show } from "solid-js";
|
||||
import { QRCodeSVG } from "solid-qr-code";
|
||||
import { useI18n } from "~/i18n/context";
|
||||
import { useCopy } from "~/utils/useCopy";
|
||||
|
||||
export function CopyableQR(props: { value: string }) {
|
||||
const i18n = useI18n();
|
||||
const [copy, copied] = useCopy({ copiedTimeout: 1000 });
|
||||
return (
|
||||
<div
|
||||
@@ -12,7 +14,7 @@ export function CopyableQR(props: { value: string }) {
|
||||
>
|
||||
<Show when={copied()}>
|
||||
<div class="absolute w-full h-full bg-neutral-900/60 z-50 rounded-xl flex flex-col items-center justify-center transition-all">
|
||||
<p class="text-xl font-bold">Copied</p>
|
||||
<p class="text-xl font-bold">{i18n.t("common.copied")}</p>
|
||||
</div>
|
||||
</Show>
|
||||
<QRCodeSVG
|
||||
|
||||
@@ -5,8 +5,10 @@ import { InfoBox } from "~/components/InfoBox";
|
||||
import { useMegaStore } from "~/state/megaStore";
|
||||
import eify from "~/utils/eify";
|
||||
import { A } from "solid-start";
|
||||
import { useI18n } from "~/i18n/context";
|
||||
|
||||
export function DecryptDialog() {
|
||||
const i18n = useI18n();
|
||||
const [state, actions] = useMegaStore();
|
||||
|
||||
const [password, setPassword] = createSignal("");
|
||||
@@ -27,7 +29,7 @@ export function DecryptDialog() {
|
||||
const err = eify(e);
|
||||
console.error(e);
|
||||
if (err.message === "wrong") {
|
||||
setError("Invalid password");
|
||||
setError(i18n.t("settings.decrypt.error_wrong_password"));
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
@@ -42,7 +44,7 @@ export function DecryptDialog() {
|
||||
|
||||
return (
|
||||
<SimpleDialog
|
||||
title="Enter your password"
|
||||
title={i18n.t("settings.decrypt.title")}
|
||||
// Only show the dialog if we need a password and there's no setup error
|
||||
open={state.needs_password && !state.setup_error}
|
||||
>
|
||||
@@ -62,12 +64,12 @@ export function DecryptDialog() {
|
||||
<InfoBox accent="red">{error()}</InfoBox>
|
||||
</Show>
|
||||
<Button intent="blue" loading={loading()} onClick={decrypt}>
|
||||
Decrypt Wallet
|
||||
{i18n.t("settings.decrypt.decrypt_wallet")}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
<A class="self-end text-m-grey-400" href="/settings/restore">
|
||||
Forgot Password?
|
||||
{i18n.t("settings.decrypt.forgot_password_link")}
|
||||
</A>
|
||||
</SimpleDialog>
|
||||
);
|
||||
|
||||
@@ -30,6 +30,7 @@ import { Network } from "~/logic/mutinyWalletSetup";
|
||||
import { AmountSmall } from "./Amount";
|
||||
import { ExternalLink } from "./layout/ExternalLink";
|
||||
import { InfoBox } from "./InfoBox";
|
||||
import { useI18n } from "~/i18n/context";
|
||||
|
||||
type ChannelClosure = {
|
||||
channel_id: string;
|
||||
@@ -48,6 +49,7 @@ function LightningHeader(props: {
|
||||
info: MutinyInvoice;
|
||||
tags: MutinyTagItem[];
|
||||
}) {
|
||||
const i18n = useI18n();
|
||||
const [state, _actions] = useMegaStore();
|
||||
|
||||
return (
|
||||
@@ -56,7 +58,9 @@ function LightningHeader(props: {
|
||||
<img src={bolt} alt="lightning bolt" class="w-8 h-8" />
|
||||
</div>
|
||||
<h1 class="uppercase font-semibold">
|
||||
{props.info.inbound ? "Lightning receive" : "Lightning send"}
|
||||
{props.info.inbound
|
||||
? i18n.t("modals.transaction_details.lightning_receive")
|
||||
: i18n.t("modals.transaction_details.lightning_send")}
|
||||
</h1>
|
||||
<ActivityAmount
|
||||
center
|
||||
@@ -85,6 +89,7 @@ function OnchainHeader(props: {
|
||||
tags: MutinyTagItem[];
|
||||
kind?: HackActivityType;
|
||||
}) {
|
||||
const i18n = useI18n();
|
||||
const [state, _actions] = useMegaStore();
|
||||
|
||||
const isSend = () => {
|
||||
@@ -118,12 +123,12 @@ function OnchainHeader(props: {
|
||||
</div>
|
||||
<h1 class="uppercase font-semibold">
|
||||
{props.kind === "ChannelOpen"
|
||||
? "Channel Open"
|
||||
? i18n.t("modals.transaction_details.channel_open")
|
||||
: props.kind === "ChannelClose"
|
||||
? "Channel Close"
|
||||
? i18n.t("modals.transaction_details.channel_close")
|
||||
: isSend()
|
||||
? "On-chain send"
|
||||
: "On-chain receive"}
|
||||
? i18n.t("modals.transaction_details.onchain_send")
|
||||
: i18n.t("modals.transaction_details.onchain_receive")}
|
||||
</h1>
|
||||
<Show when={props.kind !== "ChannelClose"}>
|
||||
<ActivityAmount
|
||||
@@ -179,38 +184,45 @@ export function MiniStringShower(props: { text: string }) {
|
||||
}
|
||||
|
||||
function LightningDetails(props: { info: MutinyInvoice }) {
|
||||
const i18n = useI18n();
|
||||
return (
|
||||
<VStack>
|
||||
<ul class="flex flex-col gap-4">
|
||||
<KeyValue key="Status">
|
||||
<KeyValue key={i18n.t("modals.transaction_details.status")}>
|
||||
<span class="text-neutral-300">
|
||||
{props.info.paid ? "Paid" : "Unpaid"}
|
||||
{props.info.paid
|
||||
? i18n.t("modals.transaction_details.paid")
|
||||
: i18n.t("modals.transaction_details.unpaid")}
|
||||
</span>
|
||||
</KeyValue>
|
||||
<KeyValue key="When">
|
||||
<KeyValue key={i18n.t("modals.transaction_details.when")}>
|
||||
<span class="text-neutral-300">
|
||||
{prettyPrintTime(Number(props.info.last_updated))}
|
||||
</span>
|
||||
</KeyValue>
|
||||
<Show when={props.info.description}>
|
||||
<KeyValue key="Description">
|
||||
<KeyValue
|
||||
key={i18n.t("modals.transaction_details.description")}
|
||||
>
|
||||
<span class="text-neutral-300 truncate">
|
||||
{props.info.description}
|
||||
</span>
|
||||
</KeyValue>
|
||||
</Show>
|
||||
<KeyValue key="Fees">
|
||||
<KeyValue key={i18n.t("modals.transaction_details.fees")}>
|
||||
<span class="text-neutral-300">
|
||||
<AmountSmall amountSats={props.info.fees_paid} />
|
||||
</span>
|
||||
</KeyValue>
|
||||
<KeyValue key="Bolt11">
|
||||
<KeyValue key={i18n.t("modals.transaction_details.bolt11")}>
|
||||
<MiniStringShower text={props.info.bolt11 ?? ""} />
|
||||
</KeyValue>
|
||||
<KeyValue key="Payment Hash">
|
||||
<KeyValue
|
||||
key={i18n.t("modals.transaction_details.payment_hash")}
|
||||
>
|
||||
<MiniStringShower text={props.info.payment_hash ?? ""} />
|
||||
</KeyValue>
|
||||
<KeyValue key="Preimage">
|
||||
<KeyValue key={i18n.t("modals.transaction_details.preimage")}>
|
||||
<MiniStringShower text={props.info.preimage ?? ""} />
|
||||
</KeyValue>
|
||||
</ul>
|
||||
@@ -219,6 +231,7 @@ function LightningDetails(props: { info: MutinyInvoice }) {
|
||||
}
|
||||
|
||||
function OnchainDetails(props: { info: OnChainTx; kind?: HackActivityType }) {
|
||||
const i18n = useI18n();
|
||||
const [state, _actions] = useMegaStore();
|
||||
|
||||
const confirmationTime = () => {
|
||||
@@ -251,13 +264,15 @@ function OnchainDetails(props: { info: OnChainTx; kind?: HackActivityType }) {
|
||||
<VStack>
|
||||
{/* <pre>{JSON.stringify(channelInfo() || "", null, 2)}</pre> */}
|
||||
<ul class="flex flex-col gap-4">
|
||||
<KeyValue key="Status">
|
||||
<KeyValue key={i18n.t("modals.transaction_details.status")}>
|
||||
<span class="text-neutral-300">
|
||||
{confirmationTime() ? "Confirmed" : "Unconfirmed"}
|
||||
{confirmationTime()
|
||||
? i18n.t("modals.transaction_details.confirmed")
|
||||
: i18n.t("modals.transaction_details.unconfirmed")}
|
||||
</span>
|
||||
</KeyValue>
|
||||
<Show when={confirmationTime()}>
|
||||
<KeyValue key="When">
|
||||
<KeyValue key={i18n.t("modals.transaction_details.when")}>
|
||||
<span class="text-neutral-300">
|
||||
{confirmationTime()
|
||||
? prettyPrintTime(Number(confirmationTime()))
|
||||
@@ -266,32 +281,38 @@ function OnchainDetails(props: { info: OnChainTx; kind?: HackActivityType }) {
|
||||
</KeyValue>
|
||||
</Show>
|
||||
<Show when={props.info.fee && props.info.fee > 0}>
|
||||
<KeyValue key="Fee">
|
||||
<KeyValue key={i18n.t("modals.transaction_details.fee")}>
|
||||
<span class="text-neutral-300">
|
||||
<AmountSmall amountSats={props.info.fee} />
|
||||
</span>
|
||||
</KeyValue>
|
||||
</Show>
|
||||
<KeyValue key="Txid">
|
||||
<KeyValue key={i18n.t("modals.transaction_details.txid")}>
|
||||
<MiniStringShower text={props.info.txid ?? ""} />
|
||||
</KeyValue>
|
||||
<Switch>
|
||||
<Match when={props.kind === "ChannelOpen" && channelInfo()}>
|
||||
<KeyValue key="Balance">
|
||||
<KeyValue
|
||||
key={i18n.t("modals.transaction_details.balance")}
|
||||
>
|
||||
<span class="text-neutral-300">
|
||||
<AmountSmall
|
||||
amountSats={channelInfo()?.balance}
|
||||
/>
|
||||
</span>
|
||||
</KeyValue>
|
||||
<KeyValue key="Reserve">
|
||||
<KeyValue
|
||||
key={i18n.t("modals.transaction_details.reserve")}
|
||||
>
|
||||
<span class="text-neutral-300">
|
||||
<AmountSmall
|
||||
amountSats={channelInfo()?.reserve}
|
||||
/>
|
||||
</span>
|
||||
</KeyValue>
|
||||
<KeyValue key="Peer">
|
||||
<KeyValue
|
||||
key={i18n.t("modals.transaction_details.peer")}
|
||||
>
|
||||
<span class="text-neutral-300">
|
||||
<MiniStringShower
|
||||
text={channelInfo()?.peer ?? ""}
|
||||
@@ -301,15 +322,14 @@ function OnchainDetails(props: { info: OnChainTx; kind?: HackActivityType }) {
|
||||
</Match>
|
||||
<Match when={props.kind === "ChannelOpen"}>
|
||||
<InfoBox accent="blue">
|
||||
No channel details found, which means this channel
|
||||
has likely been closed.
|
||||
{i18n.t("modals.transaction_details.no_details")}
|
||||
</InfoBox>
|
||||
</Match>
|
||||
</Switch>
|
||||
</ul>
|
||||
<div class="text-center">
|
||||
<ExternalLink href={mempoolTxUrl(props.info.txid, network)}>
|
||||
View Transaction
|
||||
{i18n.t("common.view_transaction")}
|
||||
</ExternalLink>
|
||||
</div>
|
||||
</VStack>
|
||||
@@ -317,23 +337,24 @@ function OnchainDetails(props: { info: OnChainTx; kind?: HackActivityType }) {
|
||||
}
|
||||
|
||||
function ChannelCloseDetails(props: { info: ChannelClosure }) {
|
||||
const i18n = useI18n();
|
||||
return (
|
||||
<VStack>
|
||||
{/* <pre>{JSON.stringify(props.info.value, null, 2)}</pre> */}
|
||||
<ul class="flex flex-col gap-4">
|
||||
<KeyValue key="Channel ID">
|
||||
<KeyValue key={i18n.t("modals.transaction_details.channel_id")}>
|
||||
<MiniStringShower text={props.info.channel_id ?? ""} />
|
||||
</KeyValue>
|
||||
<Show when={props.info.timestamp}>
|
||||
<KeyValue key="When">
|
||||
<KeyValue key={i18n.t("modals.transaction_details.when")}>
|
||||
<span class="text-neutral-300">
|
||||
{props.info.timestamp
|
||||
? prettyPrintTime(Number(props.info.timestamp))
|
||||
: "Pending"}
|
||||
: i18n.t("common.pending")}
|
||||
</span>
|
||||
</KeyValue>
|
||||
</Show>
|
||||
<KeyValue key="Reason">
|
||||
<KeyValue key={i18n.t("modals.transaction_details.reason")}>
|
||||
<p class="text-neutral-300 text-right">
|
||||
{props.info.reason ?? ""}
|
||||
</p>
|
||||
@@ -349,6 +370,7 @@ export function DetailsIdModal(props: {
|
||||
id: string;
|
||||
setOpen: (open: boolean) => void;
|
||||
}) {
|
||||
const i18n = useI18n();
|
||||
const [state, _actions] = useMegaStore();
|
||||
|
||||
const id = () => props.id;
|
||||
@@ -473,7 +495,7 @@ export function DetailsIdModal(props: {
|
||||
<Show when={props.kind !== "ChannelClose"}>
|
||||
<div class="flex justify-center">
|
||||
<CopyButton
|
||||
title="Copy"
|
||||
title={i18n.t("common.copy")}
|
||||
text={json()}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Dialog } from "@kobalte/core";
|
||||
import { ParentComponent } from "solid-js";
|
||||
import { Button, SmallHeader } from "./layout";
|
||||
import { useI18n } from "~/i18n/context";
|
||||
|
||||
const OVERLAY = "fixed inset-0 z-50 bg-black/50 backdrop-blur-sm";
|
||||
const DIALOG_POSITIONER = "fixed inset-0 z-50 flex items-center justify-center";
|
||||
@@ -14,6 +15,7 @@ export const ConfirmDialog: ParentComponent<{
|
||||
onCancel: () => void;
|
||||
onConfirm: () => void;
|
||||
}> = (props) => {
|
||||
const i18n = useI18n();
|
||||
return (
|
||||
<Dialog.Root open={props.open} onOpenChange={props.onCancel}>
|
||||
<Dialog.Portal>
|
||||
@@ -22,20 +24,26 @@ export const ConfirmDialog: ParentComponent<{
|
||||
<Dialog.Content class={DIALOG_CONTENT}>
|
||||
<div class="flex justify-between mb-2">
|
||||
<Dialog.Title>
|
||||
<SmallHeader>Are you sure?</SmallHeader>
|
||||
<SmallHeader>
|
||||
{i18n.t(
|
||||
"modals.confirm_dialog.are_you_sure"
|
||||
)}
|
||||
</SmallHeader>
|
||||
</Dialog.Title>
|
||||
</div>
|
||||
<Dialog.Description class="flex flex-col gap-4">
|
||||
{props.children}
|
||||
<div class="flex gap-4 w-full justify-end">
|
||||
<Button onClick={props.onCancel}>Cancel</Button>
|
||||
<Button onClick={props.onCancel}>
|
||||
{i18n.t("modals.confirm_dialog.cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
intent="red"
|
||||
onClick={props.onConfirm}
|
||||
loading={props.loading}
|
||||
disabled={props.loading}
|
||||
>
|
||||
Confirm
|
||||
{i18n.t("modals.confirm_dialog.confirm")}
|
||||
</Button>
|
||||
</div>
|
||||
</Dialog.Description>
|
||||
|
||||
@@ -8,35 +8,38 @@ import {
|
||||
SmallHeader
|
||||
} from "~/components/layout";
|
||||
import { ExternalLink } from "./layout/ExternalLink";
|
||||
import { useI18n } from "~/i18n/context";
|
||||
|
||||
export default function ErrorDisplay(props: { error: Error }) {
|
||||
const i18n = useI18n();
|
||||
return (
|
||||
<SafeArea>
|
||||
<Title>Oh no!</Title>
|
||||
<Title>{i18n.t("error.general.oh_no")}</Title>
|
||||
<DefaultMain>
|
||||
<LargeHeader>Error</LargeHeader>
|
||||
<SmallHeader>This never should've happened</SmallHeader>
|
||||
<LargeHeader>{i18n.t("error.title")}</LargeHeader>
|
||||
<SmallHeader>
|
||||
{i18n.t("error.general.never_should_happen")}
|
||||
</SmallHeader>
|
||||
<p class="bg-white/10 rounded-xl p-4 font-mono">
|
||||
<span class="font-bold">{props.error.name}</span>:{" "}
|
||||
{props.error.message}
|
||||
</p>
|
||||
<NiceP>
|
||||
Try reloading this page or clicking the "Dangit" button. If
|
||||
you keep having problems,{" "}
|
||||
{i18n.t("error.general.try_reloading")}{" "}
|
||||
<ExternalLink href="https://matrix.to/#/#mutiny-community:lightninghackers.com">
|
||||
reach out to us for support.
|
||||
{i18n.t("error.general.support_link")}
|
||||
</ExternalLink>
|
||||
</NiceP>
|
||||
<NiceP>
|
||||
Getting desperate? Try the{" "}
|
||||
<A href="/emergencykit">emergency kit.</A>
|
||||
{i18n.t("error.general.getting_desperate")}{" "}
|
||||
<A href="/emergencykit">{i18n.t("error.emergency_link")}</A>
|
||||
</NiceP>
|
||||
<div class="h-full" />
|
||||
<Button
|
||||
onClick={() => (window.location.href = "/")}
|
||||
intent="red"
|
||||
>
|
||||
Dangit
|
||||
{i18n.t("common.dangit")}
|
||||
</Button>
|
||||
</DefaultMain>
|
||||
</SafeArea>
|
||||
|
||||
@@ -8,23 +8,31 @@ import copyBlack from "~/assets/icons/copy-black.svg";
|
||||
import shareBlack from "~/assets/icons/share-black.svg";
|
||||
import chainBlack from "~/assets/icons/chain-black.svg";
|
||||
import boltBlack from "~/assets/icons/bolt-black.svg";
|
||||
import { useI18n } from "~/i18n/context";
|
||||
|
||||
function KindIndicator(props: { kind: ReceiveFlavor }) {
|
||||
const i18n = useI18n();
|
||||
return (
|
||||
<div class="text-black flex flex-col items-end">
|
||||
<Switch>
|
||||
<Match when={props.kind === "onchain"}>
|
||||
<h3 class="font-semibold">On-chain</h3>
|
||||
<h3 class="font-semibold">
|
||||
{i18n.t("receive.integrated_qr.onchain")}
|
||||
</h3>
|
||||
<img src={chainBlack} alt="chain" />
|
||||
</Match>
|
||||
|
||||
<Match when={props.kind === "lightning"}>
|
||||
<h3 class="font-semibold">Lightning</h3>
|
||||
<h3 class="font-semibold">
|
||||
{i18n.t("receive.integrated_qr.lightning")}
|
||||
</h3>
|
||||
<img src={boltBlack} alt="bolt" />
|
||||
</Match>
|
||||
|
||||
<Match when={props.kind === "unified"}>
|
||||
<h3 class="font-semibold">Unified</h3>
|
||||
<h3 class="font-semibold">
|
||||
{i18n.t("receive.integrated_qr.unified")}
|
||||
</h3>
|
||||
<div class="flex gap-1">
|
||||
<img src={chainBlack} alt="chain" />
|
||||
<img src={boltBlack} alt="bolt" />
|
||||
@@ -56,6 +64,7 @@ export function IntegratedQr(props: {
|
||||
amountSats: string;
|
||||
kind: ReceiveFlavor;
|
||||
}) {
|
||||
const i18n = useI18n();
|
||||
const [copy, copied] = useCopy({ copiedTimeout: 1000 });
|
||||
return (
|
||||
<div
|
||||
@@ -65,7 +74,7 @@ export function IntegratedQr(props: {
|
||||
>
|
||||
<Show when={copied()}>
|
||||
<div class="absolute w-full h-full bg-neutral-900/60 z-50 rounded-xl flex flex-col items-center justify-center transition-all">
|
||||
<p class="text-xl font-bold">Copied</p>
|
||||
<p class="text-xl font-bold">{i18n.t("common.copied")}</p>
|
||||
</div>
|
||||
</Show>
|
||||
<div
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
OVERLAY
|
||||
} from "~/components/DetailsModal";
|
||||
import { CopyButton } from "./ShareCard";
|
||||
import { useI18n } from "~/i18n/context";
|
||||
|
||||
export function JsonModal(props: {
|
||||
title: string;
|
||||
@@ -16,6 +17,7 @@ export function JsonModal(props: {
|
||||
setOpen: (open: boolean) => void;
|
||||
children?: JSX.Element;
|
||||
}) {
|
||||
const i18n = useI18n();
|
||||
const json = createMemo(() =>
|
||||
props.plaintext ? props.plaintext : JSON.stringify(props.data, null, 2)
|
||||
);
|
||||
@@ -41,7 +43,10 @@ export function JsonModal(props: {
|
||||
</pre>
|
||||
</div>
|
||||
{props.children}
|
||||
<CopyButton title="Copy" text={json()} />
|
||||
<CopyButton
|
||||
title={i18n.t("common.copy")}
|
||||
text={json()}
|
||||
/>
|
||||
</Dialog.Description>
|
||||
</Dialog.Content>
|
||||
</div>
|
||||
|
||||
@@ -21,6 +21,7 @@ import { Restart } from "./Restart";
|
||||
import { ResyncOnchain } from "./ResyncOnchain";
|
||||
import { ResetRouter } from "./ResetRouter";
|
||||
import { MiniStringShower } from "./DetailsModal";
|
||||
import { useI18n } from "~/i18n/context";
|
||||
|
||||
// TODO: hopefully I don't have to maintain this type forever but I don't know how to pass it around otherwise
|
||||
type RefetchPeersType = (
|
||||
@@ -28,6 +29,7 @@ type RefetchPeersType = (
|
||||
) => MutinyPeer[] | Promise<MutinyPeer[] | undefined> | null | undefined;
|
||||
|
||||
function PeerItem(props: { peer: MutinyPeer }) {
|
||||
const i18n = useI18n();
|
||||
const [state, _] = useMegaStore();
|
||||
|
||||
const handleDisconnectPeer = async () => {
|
||||
@@ -65,7 +67,7 @@ function PeerItem(props: { peer: MutinyPeer }) {
|
||||
layout="xs"
|
||||
onClick={handleDisconnectPeer}
|
||||
>
|
||||
Disconnect
|
||||
{i18n.t("settings.admin.kitchen_sink.disconnect")}
|
||||
</Button>
|
||||
</VStack>
|
||||
</Collapsible.Content>
|
||||
@@ -74,6 +76,7 @@ function PeerItem(props: { peer: MutinyPeer }) {
|
||||
}
|
||||
|
||||
function PeersList() {
|
||||
const i18n = useI18n();
|
||||
const [state, _] = useMegaStore();
|
||||
|
||||
const getPeers = async () => {
|
||||
@@ -86,20 +89,26 @@ function PeersList() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<InnerCard title="Peers">
|
||||
<InnerCard title={i18n.t("settings.admin.kitchen_sink.peers")}>
|
||||
{/* By wrapping this in a suspense I don't cause the page to jump to the top */}
|
||||
<Suspense>
|
||||
<VStack>
|
||||
<For
|
||||
each={peers.latest}
|
||||
fallback={<code>No peers</code>}
|
||||
fallback={
|
||||
<code>
|
||||
{i18n.t(
|
||||
"settings.admin.kitchen_sink.no_peers"
|
||||
)}
|
||||
</code>
|
||||
}
|
||||
>
|
||||
{(peer) => <PeerItem peer={peer} />}
|
||||
</For>
|
||||
</VStack>
|
||||
</Suspense>
|
||||
<Button layout="small" onClick={refetch}>
|
||||
Refresh Peers
|
||||
{i18n.t("settings.admin.kitchen_sink.refresh_peers")}
|
||||
</Button>
|
||||
</InnerCard>
|
||||
<ConnectPeer refetchPeers={refetch} />
|
||||
@@ -108,6 +117,7 @@ function PeersList() {
|
||||
}
|
||||
|
||||
function ConnectPeer(props: { refetchPeers: RefetchPeersType }) {
|
||||
const i18n = useI18n();
|
||||
const [state, _] = useMegaStore();
|
||||
|
||||
const [value, setValue] = createSignal("");
|
||||
@@ -139,18 +149,18 @@ function ConnectPeer(props: { refetchPeers: RefetchPeersType }) {
|
||||
class="flex flex-col gap-4"
|
||||
>
|
||||
<TextField.Label class="text-sm font-semibold uppercase">
|
||||
Connect Peer
|
||||
{i18n.t("settings.admin.kitchen_sink.connect_peer")}
|
||||
</TextField.Label>
|
||||
<TextField.Input
|
||||
class="w-full p-2 rounded-lg text-black"
|
||||
placeholder="028241..."
|
||||
/>
|
||||
<TextField.ErrorMessage class="text-red-500">
|
||||
Expecting a value...
|
||||
{i18n.t("settings.admin.kitchen_sink.expect_a_value")}
|
||||
</TextField.ErrorMessage>
|
||||
</TextField.Root>
|
||||
<Button layout="small" type="submit">
|
||||
Connect
|
||||
{i18n.t("settings.admin.kitchen_sink.connect")}
|
||||
</Button>
|
||||
</form>
|
||||
</InnerCard>
|
||||
@@ -164,6 +174,7 @@ type RefetchChannelsListType = (
|
||||
type PendingChannelAction = "close" | "force_close" | "abandon";
|
||||
|
||||
function ChannelItem(props: { channel: MutinyChannel; network?: Network }) {
|
||||
const i18n = useI18n();
|
||||
const [state, _] = useMegaStore();
|
||||
|
||||
const [pendingChannelAction, setPendingChannelAction] =
|
||||
@@ -206,28 +217,28 @@ function ChannelItem(props: { channel: MutinyChannel; network?: Network }) {
|
||||
props.network
|
||||
)}
|
||||
>
|
||||
View Transaction
|
||||
{i18n.t("common.view_transaction")}
|
||||
</ExternalLink>
|
||||
<Button
|
||||
intent="glowy"
|
||||
layout="xs"
|
||||
onClick={() => setPendingChannelAction("close")}
|
||||
>
|
||||
Close Channel
|
||||
{i18n.t("settings.admin.kitchen_sink.close_channel")}
|
||||
</Button>
|
||||
<Button
|
||||
intent="glowy"
|
||||
layout="xs"
|
||||
onClick={() => setPendingChannelAction("force_close")}
|
||||
>
|
||||
Force close Channel
|
||||
{i18n.t("settings.admin.kitchen_sink.force_close")}
|
||||
</Button>
|
||||
<Button
|
||||
intent="glowy"
|
||||
layout="xs"
|
||||
onClick={() => setPendingChannelAction("abandon")}
|
||||
>
|
||||
Abandon Channel
|
||||
{i18n.t("settings.admin.kitchen_sink.abandon_channel")}
|
||||
</Button>
|
||||
</VStack>
|
||||
<ConfirmDialog
|
||||
@@ -238,21 +249,24 @@ function ChannelItem(props: { channel: MutinyChannel; network?: Network }) {
|
||||
>
|
||||
<Switch>
|
||||
<Match when={pendingChannelAction() === "close"}>
|
||||
<p>Are you sure you want to close this channel?</p>
|
||||
<p>
|
||||
{i18n.t(
|
||||
"settings.admin.kitchen_sink.confirm_close_channel"
|
||||
)}
|
||||
</p>
|
||||
</Match>
|
||||
<Match when={pendingChannelAction() === "force_close"}>
|
||||
<p>
|
||||
Are you sure you want to force close this
|
||||
channel? Your funds will take a few days to
|
||||
redeem on chain.
|
||||
{i18n.t(
|
||||
"settings.admin.kitchen_sink.confirm_force_close"
|
||||
)}
|
||||
</p>
|
||||
</Match>
|
||||
<Match when={pendingChannelAction() === "abandon"}>
|
||||
<p>
|
||||
Are you sure you want to abandon this channel?
|
||||
Typically only do this if the opening
|
||||
transaction will never confirm. Otherwise, you
|
||||
will lose funds.
|
||||
{i18n.t(
|
||||
"settings.admin.kitchen_sink.confirm_abandon_channel"
|
||||
)}
|
||||
</p>
|
||||
</Match>
|
||||
</Switch>
|
||||
@@ -263,6 +277,7 @@ function ChannelItem(props: { channel: MutinyChannel; network?: Network }) {
|
||||
}
|
||||
|
||||
function ChannelsList() {
|
||||
const i18n = useI18n();
|
||||
const [state, _] = useMegaStore();
|
||||
|
||||
const getChannels = async () => {
|
||||
@@ -277,10 +292,19 @@ function ChannelsList() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<InnerCard title="Channels">
|
||||
<InnerCard title={i18n.t("settings.admin.kitchen_sink.channels")}>
|
||||
{/* By wrapping this in a suspense I don't cause the page to jump to the top */}
|
||||
<Suspense>
|
||||
<For each={channels()} fallback={<code>No channels</code>}>
|
||||
<For
|
||||
each={channels()}
|
||||
fallback={
|
||||
<code>
|
||||
{i18n.t(
|
||||
"settings.admin.kitchen_sink.no_channels"
|
||||
)}
|
||||
</code>
|
||||
}
|
||||
>
|
||||
{(channel) => (
|
||||
<ChannelItem channel={channel} network={network} />
|
||||
)}
|
||||
@@ -294,7 +318,7 @@ function ChannelsList() {
|
||||
refetch();
|
||||
}}
|
||||
>
|
||||
Refresh Channels
|
||||
{i18n.t("settings.admin.kitchen_sink.refresh_channels")}
|
||||
</Button>
|
||||
</InnerCard>
|
||||
<OpenChannel refetchChannels={refetch} />
|
||||
@@ -303,6 +327,7 @@ function ChannelsList() {
|
||||
}
|
||||
|
||||
function OpenChannel(props: { refetchChannels: RefetchChannelsListType }) {
|
||||
const i18n = useI18n();
|
||||
const [state, _] = useMegaStore();
|
||||
|
||||
const [creationError, setCreationError] = createSignal<Error>();
|
||||
@@ -354,7 +379,7 @@ function OpenChannel(props: { refetchChannels: RefetchChannelsListType }) {
|
||||
class="flex flex-col gap-2"
|
||||
>
|
||||
<TextField.Label class="text-sm font-semibold uppercase">
|
||||
Pubkey
|
||||
{i18n.t("settings.admin.kitchen_sink.pubkey")}
|
||||
</TextField.Label>
|
||||
<TextField.Input class="w-full p-2 rounded-lg text-black" />
|
||||
</TextField.Root>
|
||||
@@ -364,7 +389,7 @@ function OpenChannel(props: { refetchChannels: RefetchChannelsListType }) {
|
||||
class="flex flex-col gap-2"
|
||||
>
|
||||
<TextField.Label class="text-sm font-semibold uppercase">
|
||||
Amount
|
||||
{i18n.t("settings.admin.kitchen_sink.amount")}
|
||||
</TextField.Label>
|
||||
<TextField.Input
|
||||
type="number"
|
||||
@@ -372,7 +397,7 @@ function OpenChannel(props: { refetchChannels: RefetchChannelsListType }) {
|
||||
/>
|
||||
</TextField.Root>
|
||||
<Button layout="small" type="submit">
|
||||
Open Channel
|
||||
{i18n.t("settings.admin.kitchen_sink.open_channel")}
|
||||
</Button>
|
||||
</form>
|
||||
</InnerCard>
|
||||
@@ -387,7 +412,7 @@ function OpenChannel(props: { refetchChannels: RefetchChannelsListType }) {
|
||||
network
|
||||
)}
|
||||
>
|
||||
View Transaction
|
||||
{i18n.t("common.view_transaction")}
|
||||
</ExternalLink>
|
||||
</Show>
|
||||
<Show when={creationError()}>
|
||||
@@ -398,6 +423,7 @@ function OpenChannel(props: { refetchChannels: RefetchChannelsListType }) {
|
||||
}
|
||||
|
||||
function ListNodes() {
|
||||
const i18n = useI18n();
|
||||
const [state, _] = useMegaStore();
|
||||
|
||||
const getNodeIds = async () => {
|
||||
@@ -408,9 +434,16 @@ function ListNodes() {
|
||||
const [nodeIds] = createResource(getNodeIds);
|
||||
|
||||
return (
|
||||
<InnerCard title="Nodes">
|
||||
<InnerCard title={i18n.t("settings.admin.kitchen_sink.nodes")}>
|
||||
<Suspense>
|
||||
<For each={nodeIds()} fallback={<code>No nodes</code>}>
|
||||
<For
|
||||
each={nodeIds()}
|
||||
fallback={
|
||||
<code>
|
||||
{i18n.t("settings.admin.kitchen_sink.no_nodes")}
|
||||
</code>
|
||||
}
|
||||
>
|
||||
{(nodeId) => <MiniStringShower text={nodeId} />}
|
||||
</For>
|
||||
</Suspense>
|
||||
|
||||
@@ -1,22 +1,24 @@
|
||||
import { Progress } from "@kobalte/core";
|
||||
import { Show } from "solid-js";
|
||||
import { useI18n } from "~/i18n/context";
|
||||
import { useMegaStore } from "~/state/megaStore";
|
||||
|
||||
export function LoadingBar(props: { value: number; max: number }) {
|
||||
const i18n = useI18n();
|
||||
function valueToStage(value: number) {
|
||||
switch (value) {
|
||||
case 0:
|
||||
return "Just getting started";
|
||||
return i18n.t("modals.loading.default");
|
||||
case 1:
|
||||
return "Double checking something";
|
||||
return i18n.t("modals.loading.double_checking");
|
||||
case 2:
|
||||
return "Downloading";
|
||||
return i18n.t("modals.loading.downloading");
|
||||
case 3:
|
||||
return "Setup";
|
||||
return i18n.t("modals.loading.setup");
|
||||
case 4:
|
||||
return "Done";
|
||||
return i18n.t("modals.loading.done");
|
||||
default:
|
||||
return "Just getting started";
|
||||
return i18n.t("modals.loading.default");
|
||||
}
|
||||
}
|
||||
return (
|
||||
@@ -24,7 +26,9 @@ export function LoadingBar(props: { value: number; max: number }) {
|
||||
value={props.value}
|
||||
minValue={0}
|
||||
maxValue={props.max}
|
||||
getValueLabel={({ value }) => `Loading: ${valueToStage(value)}`}
|
||||
getValueLabel={({ value }) =>
|
||||
i18n.t("modals.loading.loading", { stage: valueToStage(value) })
|
||||
}
|
||||
class="w-full flex flex-col gap-2"
|
||||
>
|
||||
<Progress.ValueLabel class="text-sm text-m-grey-400" />
|
||||
|
||||
@@ -10,20 +10,20 @@ export function FeesModal(props: { icon?: boolean }) {
|
||||
const i18n = useI18n();
|
||||
return (
|
||||
<MoreInfoModal
|
||||
title={i18n.t("whats_with_the_fees")}
|
||||
title={i18n.t("modals.more_info.whats_with_the_fees")}
|
||||
linkText={
|
||||
props.icon ? (
|
||||
<img src={help} alt="help" class="w-4 h-4 cursor-pointer" />
|
||||
) : (
|
||||
i18n.t("why")
|
||||
i18n.t("common.why")
|
||||
)
|
||||
}
|
||||
>
|
||||
<p>{i18n.t("more_info_modal_p1")}</p>
|
||||
<p>{i18n.t("more_info_modal_p2")}</p>
|
||||
<p>{i18n.t("modals.more_info.self_custodial")}</p>
|
||||
<p>{i18n.t("modals.more_info.future_payments")}</p>
|
||||
<p>
|
||||
<ExternalLink href="https://github.com/MutinyWallet/mutiny-web/wiki/Understanding-liquidity">
|
||||
{i18n.t("learn_more_about_liquidity")}
|
||||
{i18n.t("modals.more_info.liquidity")}
|
||||
</ExternalLink>
|
||||
</p>
|
||||
</MoreInfoModal>
|
||||
|
||||
@@ -5,8 +5,10 @@ import { showToast } from "./Toaster";
|
||||
import save from "~/assets/icons/save.svg";
|
||||
import close from "~/assets/icons/close.svg";
|
||||
import restore from "~/assets/icons/upload.svg";
|
||||
import { useI18n } from "~/i18n/context";
|
||||
|
||||
export function OnboardWarning() {
|
||||
const i18n = useI18n();
|
||||
const [state, actions] = useMegaStore();
|
||||
const [dismissedBackup, setDismissedBackup] = createSignal(
|
||||
sessionStorage.getItem("dismissed_backup") ?? false
|
||||
@@ -31,11 +33,13 @@ export function OnboardWarning() {
|
||||
</div>
|
||||
<div class="flex md:flex-row flex-col items-center gap-4">
|
||||
<div class="flex flex-col">
|
||||
<SmallHeader>Welcome!</SmallHeader>
|
||||
<SmallHeader>
|
||||
{i18n.t("modals.onboarding.welcome")}
|
||||
</SmallHeader>
|
||||
<p class="text-base font-light">
|
||||
If you've used Mutiny before you can restore
|
||||
from a backup. Otherwise you can skip this and
|
||||
enjoy your new wallet!
|
||||
{i18n.t(
|
||||
"modals.onboarding.restore_from_backup"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
@@ -44,12 +48,14 @@ export function OnboardWarning() {
|
||||
class="self-start md:self-auto"
|
||||
onClick={() => {
|
||||
showToast({
|
||||
title: "Unimplemented",
|
||||
description: "We don't do that yet"
|
||||
title: i18n.t("common.error_unimplemented"),
|
||||
description: i18n.t(
|
||||
"modals.onboarding.not_available"
|
||||
)
|
||||
});
|
||||
}}
|
||||
>
|
||||
Restore
|
||||
{i18n.t("settings.restore.title")}
|
||||
</Button>
|
||||
</div>
|
||||
<button
|
||||
@@ -72,10 +78,11 @@ export function OnboardWarning() {
|
||||
</div>
|
||||
<div class="flex flex-row max-md:items-center justify-between gap-4">
|
||||
<div class="flex flex-col">
|
||||
<SmallHeader>Secure your funds</SmallHeader>
|
||||
<SmallHeader>
|
||||
{i18n.t("modals.onboarding.secure_your_funds")}
|
||||
</SmallHeader>
|
||||
<p class="text-base font-light max-md:hidden">
|
||||
You have money stored in this browser. Let's
|
||||
make sure you have a backup.
|
||||
{i18n.t("modals.onboarding.make_backup")}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
@@ -85,7 +92,7 @@ export function OnboardWarning() {
|
||||
class="self-auto"
|
||||
href="/settings/backup"
|
||||
>
|
||||
Backup
|
||||
{i18n.t("settings.backup.title")}
|
||||
</ButtonLink>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -20,6 +20,7 @@ import { InfoBox } from "./InfoBox";
|
||||
import eify from "~/utils/eify";
|
||||
import { A } from "solid-start";
|
||||
import { createDeepSignal } from "~/utils/deepSignal";
|
||||
import { useI18n } from "~/i18n/context";
|
||||
|
||||
type PendingItem = {
|
||||
id: string;
|
||||
@@ -29,6 +30,7 @@ type PendingItem = {
|
||||
};
|
||||
|
||||
export function PendingNwc() {
|
||||
const i18n = useI18n();
|
||||
const [state, _actions] = useMegaStore();
|
||||
|
||||
const [error, setError] = createSignal<Error>();
|
||||
@@ -109,7 +111,7 @@ export function PendingNwc() {
|
||||
|
||||
return (
|
||||
<Show when={pendingRequests() && pendingRequests()!.length > 0}>
|
||||
<Card title="Pending Requests">
|
||||
<Card title={i18n.t("settings.connections.pending_nwc.title")}>
|
||||
<div class="p-1" />
|
||||
<VStack>
|
||||
<Show when={error()}>
|
||||
@@ -183,7 +185,7 @@ export function PendingNwc() {
|
||||
href="/settings/connections"
|
||||
class="text-m-red active:text-m-red/80 font-semibold no-underline self-center"
|
||||
>
|
||||
Configure
|
||||
{i18n.t("settings.connections.pending_nwc.configure_link")}
|
||||
</A>
|
||||
</Card>
|
||||
</Show>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { Button, InnerCard, NiceP, VStack } from "~/components/layout";
|
||||
import { useMegaStore } from "~/state/megaStore";
|
||||
import { useI18n } from "~/i18n/context";
|
||||
|
||||
export function ResetRouter() {
|
||||
const i18n = useI18n();
|
||||
const [state, _] = useMegaStore();
|
||||
|
||||
async function reset() {
|
||||
@@ -15,12 +17,9 @@ export function ResetRouter() {
|
||||
return (
|
||||
<InnerCard>
|
||||
<VStack>
|
||||
<NiceP>
|
||||
Failing to make payments? Try resetting the lightning
|
||||
router.
|
||||
</NiceP>
|
||||
<NiceP>{i18n.t("error.reset_router.payments_failing")}</NiceP>
|
||||
<Button intent="red" onClick={reset}>
|
||||
Reset Router
|
||||
{i18n.t("error.reset_router.reset_router")}
|
||||
</Button>
|
||||
</VStack>
|
||||
</InnerCard>
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { createSignal } from "solid-js";
|
||||
import { Button, InnerCard, NiceP, VStack } from "~/components/layout";
|
||||
import { useI18n } from "~/i18n/context";
|
||||
import { useMegaStore } from "~/state/megaStore";
|
||||
|
||||
export function Restart() {
|
||||
const i18n = useI18n();
|
||||
const [state, _] = useMegaStore();
|
||||
const [hasStopped, setHasStopped] = createSignal(false);
|
||||
|
||||
@@ -23,14 +25,14 @@ export function Restart() {
|
||||
return (
|
||||
<InnerCard>
|
||||
<VStack>
|
||||
<NiceP>
|
||||
Something *extra* screwy going on? Stop the nodes!
|
||||
</NiceP>
|
||||
<NiceP>{i18n.t("error.restart.title")}</NiceP>
|
||||
<Button
|
||||
intent={hasStopped() ? "green" : "red"}
|
||||
onClick={toggle}
|
||||
>
|
||||
{hasStopped() ? "Start" : "Stop"}
|
||||
{hasStopped()
|
||||
? i18n.t("error.restart.start")
|
||||
: i18n.t("error.restart.stop")}
|
||||
</Button>
|
||||
</VStack>
|
||||
</InnerCard>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { Button, InnerCard, NiceP, VStack } from "~/components/layout";
|
||||
import { useI18n } from "~/i18n/context";
|
||||
import { useMegaStore } from "~/state/megaStore";
|
||||
|
||||
export function ResyncOnchain() {
|
||||
const i18n = useI18n();
|
||||
const [state, _] = useMegaStore();
|
||||
|
||||
async function reset() {
|
||||
@@ -15,12 +17,9 @@ export function ResyncOnchain() {
|
||||
return (
|
||||
<InnerCard>
|
||||
<VStack>
|
||||
<NiceP>
|
||||
On-chain balance seems incorrect? Try re-syncing the
|
||||
on-chain wallet.
|
||||
</NiceP>
|
||||
<NiceP>{i18n.t("error.resync.incorrect_balance")}</NiceP>
|
||||
<Button intent="red" onClick={reset}>
|
||||
Resync wallet
|
||||
{i18n.t("error.resync.resync_wallet")}
|
||||
</Button>
|
||||
</VStack>
|
||||
</InnerCard>
|
||||
|
||||
@@ -12,6 +12,7 @@ import { ImportExport } from "./ImportExport";
|
||||
import { Logs } from "./Logs";
|
||||
import { DeleteEverything } from "./DeleteEverything";
|
||||
import { FeedbackLink } from "~/routes/Feedback";
|
||||
import { useI18n } from "~/i18n/context";
|
||||
|
||||
function ErrorFooter() {
|
||||
return (
|
||||
@@ -26,86 +27,100 @@ function ErrorFooter() {
|
||||
|
||||
export default function SetupErrorDisplay(props: { initialError: Error }) {
|
||||
// Error shouldn't be reactive, so we assign to it so it just gets rendered with the first value
|
||||
const i18n = useI18n();
|
||||
const error = props.initialError;
|
||||
|
||||
return (
|
||||
<SafeArea>
|
||||
<Switch>
|
||||
<Match when={error.message.startsWith("Existing tab")}>
|
||||
<Title>Multiple tabs detected</Title>
|
||||
<Title>{i18n.t("error.on_boot.existing_tab.title")}</Title>
|
||||
<DefaultMain>
|
||||
<LargeHeader>Multiple tabs detected</LargeHeader>
|
||||
<LargeHeader>
|
||||
{i18n.t("error.on_boot.existing_tab.title")}
|
||||
</LargeHeader>
|
||||
<p class="bg-white/10 rounded-xl p-4 font-mono">
|
||||
<span class="font-bold">{error.name}</span>:{" "}
|
||||
{error.message}
|
||||
</p>
|
||||
<NiceP>
|
||||
Mutiny currently only supports use in one tab at a
|
||||
time. It looks like you have another tab open with
|
||||
Mutiny running. Please close that tab and refresh
|
||||
this page, or close this tab and refresh the other
|
||||
one.
|
||||
{i18n.t("error.on_boot.existing_tab.description")}
|
||||
</NiceP>
|
||||
<ErrorFooter />
|
||||
</DefaultMain>
|
||||
</Match>
|
||||
<Match when={error.message.startsWith("Browser error")}>
|
||||
<Title>Incompatible browser</Title>
|
||||
<Title>
|
||||
{i18n.t("error.on_boot.incompatible_browser.title")}
|
||||
</Title>
|
||||
<DefaultMain>
|
||||
<LargeHeader>Incompatible browser detected</LargeHeader>
|
||||
<LargeHeader>
|
||||
{i18n.t(
|
||||
"error.on_boot.incompatible_browser.header"
|
||||
)}
|
||||
</LargeHeader>
|
||||
<p class="bg-white/10 rounded-xl p-4 font-mono">
|
||||
<span class="font-bold">{error.name}</span>:{" "}
|
||||
{error.message}
|
||||
</p>
|
||||
<NiceP>
|
||||
Mutiny requires a modern browser that supports
|
||||
WebAssembly, LocalStorage, and IndexedDB. Some
|
||||
browsers disable these features in private mode.
|
||||
{i18n.t(
|
||||
"error.on_boot.incompatible_browser.description"
|
||||
)}
|
||||
</NiceP>
|
||||
<NiceP>
|
||||
Please make sure your browser supports all these
|
||||
features, or consider trying another browser. You
|
||||
might also try disabling certain extensions or
|
||||
"shields" that block these features.
|
||||
{i18n.t(
|
||||
"error.on_boot.incompatible_browser.try_different_browser"
|
||||
)}
|
||||
</NiceP>
|
||||
<NiceP>
|
||||
(We'd love to support more private browsers, but we
|
||||
have to save your wallet data to browser storage or
|
||||
else you will lose funds.)
|
||||
{i18n.t(
|
||||
"error.on_boot.incompatible_browser.browser_storage"
|
||||
)}
|
||||
</NiceP>
|
||||
<ExternalLink href="https://github.com/MutinyWallet/mutiny-web/wiki/Browser-Compatibility">
|
||||
Supported Browsers
|
||||
{i18n.t(
|
||||
"error.on_boot.incompatible_browser.browsers_link"
|
||||
)}
|
||||
</ExternalLink>
|
||||
|
||||
<ErrorFooter />
|
||||
</DefaultMain>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<Title>Failed to load</Title>
|
||||
<Title>
|
||||
{i18n.t("error.on_boot.loading_failed.title")}
|
||||
</Title>
|
||||
<DefaultMain>
|
||||
<LargeHeader>Failed to load Mutiny</LargeHeader>
|
||||
<LargeHeader>
|
||||
{i18n.t("error.on_boot.loading_failed.header")}
|
||||
</LargeHeader>
|
||||
<p class="bg-white/10 rounded-xl p-4 font-mono">
|
||||
<span class="font-bold">{error.name}</span>:{" "}
|
||||
{error.message}
|
||||
</p>
|
||||
<NiceP>
|
||||
Something went wrong while booting up Mutiny Wallet.
|
||||
{i18n.t("error.on_boot.loading_failed.description")}
|
||||
</NiceP>
|
||||
<NiceP>
|
||||
If your wallet seems broken, here are some tools to
|
||||
try to debug and repair it.
|
||||
{i18n.t(
|
||||
"error.on_boot.loading_failed.repair_options"
|
||||
)}
|
||||
</NiceP>
|
||||
<NiceP>
|
||||
If you have any questions on what these buttons do,
|
||||
please{" "}
|
||||
{i18n.t("error.on_boot.loading_failed.questions")}{" "}
|
||||
<ExternalLink href="https://matrix.to/#/#mutiny-community:lightninghackers.com">
|
||||
reach out to us for support.
|
||||
{i18n.t(
|
||||
"error.on_boot.loading_failed.support_link"
|
||||
)}
|
||||
</ExternalLink>
|
||||
</NiceP>
|
||||
<ImportExport emergency />
|
||||
<Logs />
|
||||
<div class="rounded-xl p-4 flex flex-col gap-2 bg-m-red">
|
||||
<SmallHeader>Danger zone</SmallHeader>
|
||||
<SmallHeader>
|
||||
{i18n.t("settings.danger_zone")}
|
||||
</SmallHeader>
|
||||
<DeleteEverything emergency />
|
||||
</div>
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import shareBlack from "~/assets/icons/share-black.svg";
|
||||
import eyeIcon from "~/assets/icons/eye.svg";
|
||||
import { Show, createSignal } from "solid-js";
|
||||
import { JsonModal } from "./JsonModal";
|
||||
import { useI18n } from "~/i18n/context";
|
||||
|
||||
const STYLE =
|
||||
"px-4 py-2 rounded-xl border-2 border-white flex gap-2 items-center font-semibold hover:text-m-blue transition-colors";
|
||||
@@ -15,13 +16,14 @@ export function ShareButton(props: {
|
||||
receiveString: string;
|
||||
whiteBg?: boolean;
|
||||
}) {
|
||||
const i18n = useI18n();
|
||||
async function share(receiveString: string) {
|
||||
// If the browser doesn't support share we can just copy the address
|
||||
if (!navigator.share) {
|
||||
console.error("Share not supported");
|
||||
}
|
||||
const shareData: ShareData = {
|
||||
title: "Mutiny Wallet",
|
||||
title: i18n.t("common.title"),
|
||||
text: receiveString
|
||||
};
|
||||
try {
|
||||
@@ -33,7 +35,7 @@ export function ShareButton(props: {
|
||||
|
||||
return (
|
||||
<button class={STYLE} onClick={(_) => share(props.receiveString)}>
|
||||
<span>Share</span>
|
||||
<span>{i18n.t("modals.share")}</span>
|
||||
<img src={props.whiteBg ? shareBlack : shareIcon} alt="share" />
|
||||
</button>
|
||||
);
|
||||
@@ -57,13 +59,14 @@ export function TruncateMiddle(props: { text: string; whiteBg?: boolean }) {
|
||||
}
|
||||
|
||||
export function StringShower(props: { text: string }) {
|
||||
const i18n = useI18n();
|
||||
const [open, setOpen] = createSignal(false);
|
||||
return (
|
||||
<>
|
||||
<JsonModal
|
||||
open={open()}
|
||||
plaintext={props.text}
|
||||
title="Details"
|
||||
title={i18n.t("modals.details")}
|
||||
setOpen={setOpen}
|
||||
/>
|
||||
<div class="w-full grid grid-cols-[minmax(0,_1fr)_auto]">
|
||||
@@ -81,6 +84,7 @@ export function CopyButton(props: {
|
||||
title?: string;
|
||||
whiteBg?: boolean;
|
||||
}) {
|
||||
const i18n = useI18n();
|
||||
const [copy, copied] = useCopy({ copiedTimeout: 1000 });
|
||||
|
||||
function handleCopy() {
|
||||
@@ -89,7 +93,9 @@ export function CopyButton(props: {
|
||||
|
||||
return (
|
||||
<button class={STYLE} onClick={handleCopy}>
|
||||
{copied() ? "Copied" : props.title ?? "Copy"}
|
||||
{copied()
|
||||
? i18n.t("common.copied")
|
||||
: props.title ?? i18n.t("common.copy")}
|
||||
<img src={props.whiteBg ? copyBlack : copyIcon} alt="copy" />
|
||||
</button>
|
||||
);
|
||||
|
||||
@@ -153,7 +153,7 @@ export const FullscreenLoader = () => {
|
||||
<p class="max-w-[20rem] text-neutral-400">
|
||||
{i18n.t("error.load_time.stuck")}{" "}
|
||||
<A class="text-white" href="/emergencykit">
|
||||
{i18n.t("error.load_time.emergency_link")}
|
||||
{i18n.t("error.emergency_link")}
|
||||
</A>
|
||||
</p>
|
||||
</Show>
|
||||
|
||||
Reference in New Issue
Block a user