working activity refactor!

This commit is contained in:
Paul Miller
2023-06-01 11:36:00 -05:00
parent 2dfbcf96ee
commit 5671c04df5
5 changed files with 435 additions and 441 deletions

View File

@@ -1,204 +1,133 @@
import { LoadingSpinner, NiceP } from "./layout"
import {
For,
Match,
Switch,
createEffect,
createMemo,
createResource,
createSignal,
} from "solid-js"
import { useMegaStore } from "~/state/megaStore"
import { MutinyInvoice } from "@mutinywallet/mutiny-wasm"
import { ActivityItem } from "./ActivityItem"
import { MutinyTagItem } from "~/utils/tags"
import { Network } from "~/logic/mutinyWalletSetup"
import { DetailsModal } from "./DetailsModal"
import { For, Match, Show, Switch, createEffect, createResource, createSignal } from "solid-js";
import { useMegaStore } from "~/state/megaStore";
import { MutinyInvoice, ActivityItem as MutinyActivity } from "@mutinywallet/mutiny-wasm";
import { ActivityItem, HackActivityType } from "./ActivityItem";
import { MutinyTagItem } from "~/utils/tags";
import { DetailsIdModal } from "./DetailsModal";
export const THREE_COLUMNS =
"grid grid-cols-[auto,1fr,auto] gap-4 py-2 px-2 border-b border-neutral-800 last:border-b-0"
export const CENTER_COLUMN = "min-w-0 overflow-hidden max-w-full"
export const MISSING_LABEL =
"py-1 px-2 bg-white/10 rounded inline-block text-sm"
export const REDSHIFT_LABEL =
"py-1 px-2 bg-white text-m-red rounded inline-block text-sm"
export const RIGHT_COLUMN = "flex flex-col items-right text-right max-w-[8rem]"
"grid grid-cols-[auto,1fr,auto] gap-4 py-2 px-2 border-b border-neutral-800 last:border-b-0";
export const CENTER_COLUMN = "min-w-0 overflow-hidden max-w-full";
export const MISSING_LABEL = "py-1 px-2 bg-white/10 rounded inline-block text-sm";
export const REDSHIFT_LABEL = "py-1 px-2 bg-white text-m-red rounded inline-block text-sm";
export const RIGHT_COLUMN = "flex flex-col items-right text-right max-w-[8rem]";
export type OnChainTx = {
txid: string
received: number
sent: number
fee?: number
txid: string;
received: number;
sent: number;
fee?: number;
confirmation_time?: {
Confirmed?: {
height: number
time: number
}
}
labels: string[]
}
height: number;
time: number;
};
};
labels: string[];
};
export type UtxoItem = {
outpoint: string
outpoint: string;
txout: {
value: number
script_pubkey: string
}
keychain: string
is_spent: boolean
redshifted?: boolean
}
value: number;
script_pubkey: string;
};
keychain: string;
is_spent: boolean;
redshifted?: boolean;
};
function OnChainItem(props: {
item: OnChainTx
labels: MutinyTagItem[]
network: Network
function UnifiedActivityItem(props: {
item: MutinyActivity;
onClick: (id: string, kind: HackActivityType) => void;
}) {
const isReceive = () => props.item.received > props.item.sent
const [open, setOpen] = createSignal(false)
const click = () => {
props.onClick(props.item.id, props.item.kind as unknown as HackActivityType);
};
return (
<>
<DetailsModal open={open()} data={props.item} setOpen={setOpen} />
<ActivityItem
kind={"onchain"}
labels={props.labels}
// FIXME: is this something we can put into node logic?
amount={
isReceive()
? props.item.received - props.item.sent
: props.item.sent - props.item.received
}
date={props.item.confirmation_time?.Confirmed?.time}
positive={isReceive()}
onClick={() => setOpen(o => !o)}
/>
</>
)
}
function InvoiceItem(props: { item: MutinyInvoice; labels: MutinyTagItem[] }) {
const isSend = createMemo(() => !props.item.inbound);
const [open, setOpen] = createSignal(false)
return (
<>
<DetailsModal open={open()} data={props.item} setOpen={setOpen} />
<ActivityItem
kind={"lightning"}
labels={props.labels}
amount={props.item.amount_sats || 0n}
date={props.item.last_updated}
positive={!isSend()}
onClick={() => setOpen(o => !o)}
/>
</>
)
<ActivityItem
// This is actually the ActivityType enum but wasm is hard
kind={props.item.kind as unknown as HackActivityType}
labels={props.item.labels}
contacts={props.item.contacts}
// FIXME: is this something we can put into node logic?
amount={props.item.amount_sats || 0}
date={props.item.last_updated}
positive={props.item.inbound}
onClick={click}
/>
);
}
type ActivityItem = {
type: "onchain" | "lightning"
item: OnChainTx | MutinyInvoice
time: number
labels: MutinyTagItem[]
}
function sortByTime(a: ActivityItem, b: ActivityItem) {
return b.time - a.time
}
type: "onchain" | "lightning";
item: OnChainTx | MutinyInvoice;
time: number;
labels: MutinyTagItem[];
};
export function CombinedActivity(props: { limit?: number }) {
const [state, actions] = useMegaStore()
const [state, _actions] = useMegaStore();
const getAllActivity = async () => {
console.log("Getting all activity")
const txs = (await state.mutiny_wallet?.list_onchain()) as OnChainTx[]
const invoices =
(await state.mutiny_wallet?.list_invoices()) as MutinyInvoice[]
const tags = await actions.listTags()
let activity: ActivityItem[] = []
for (let i = 0; i < txs.length; i++) {
activity.push({
type: "onchain",
item: txs[i],
time: txs[i].confirmation_time?.Confirmed?.time || Date.now(),
labels: [],
})
}
for (let i = 0; i < invoices.length; i++) {
if (invoices[i].paid) {
activity.push({
type: "lightning",
item: invoices[i],
time: Number(invoices[i].last_updated),
labels: [],
})
}
}
if (props.limit) {
activity = activity.sort(sortByTime).slice(0, props.limit)
const [activity, { refetch }] = createResource(async () => {
console.log("Getting all activity");
const allActivity = await state.mutiny_wallet?.get_activity();
// return allActivity.reverse().filter((a: MutinyActivity) => a.kind as unknown as HackActivityType === "Lightning" && !a.paid);
if (props.limit && allActivity.length > props.limit) {
return allActivity.slice(0, props.limit);
} else {
activity.sort(sortByTime)
return allActivity;
}
for (let i = 0; i < activity.length; i++) {
// filter the tags to only include the ones that have an id matching one of the labels
activity[i].labels = tags.filter((tag) =>
activity[i].item.labels.includes(tag.id)
)
}
return activity
}
const [activity, { refetch }] = createResource(getAllActivity)
const network = state.mutiny_wallet?.get_network() as Network
});
createEffect(() => {
// After every sync we should refetch the activity
if (!state.is_syncing) {
refetch()
refetch();
}
})
});
const [detailsOpen, setDetailsOpen] = createSignal(false);
const [detailsKind, setDetailsKind] = createSignal<HackActivityType>();
const [detailsId, setDetailsId] = createSignal("");
function openDetailsModal(id: string, kind: HackActivityType) {
console.log("Opening details modal: ", id, kind);
setDetailsId(id);
setDetailsKind(kind);
setDetailsOpen(true);
}
return (
<Switch>
<Match when={activity.loading}>
<LoadingSpinner wide />
</Match>
<Match when={activity.state === "ready" && activity().length === 0}>
<div class="w-full text-center">
<NiceP>Receive some sats to get started</NiceP>
</div>
</Match>
<Match when={activity.state === "ready" && activity().length >= 0}>
<For each={activity.latest}>
{(activityItem) => (
<Switch>
<Match when={activityItem.type === "onchain"}>
<OnChainItem
item={activityItem.item as OnChainTx}
labels={activityItem.labels}
network={network}
/>
</Match>
<Match when={activityItem.type === "lightning"}>
<InvoiceItem
item={activityItem.item as MutinyInvoice}
labels={activityItem.labels}
/>
</Match>
</Switch>
)}
</For>
</Match>
</Switch>
<>
<Show when={detailsId() && detailsKind()}>
<DetailsIdModal
open={detailsOpen()}
kind={detailsKind()}
id={detailsId()}
setOpen={setDetailsOpen}
/>
</Show>
<Switch>
<Match when={activity.loading}>
<LoadingSpinner wide />
</Match>
<Match when={activity.state === "ready" && activity().length === 0}>
<div class="w-full text-center">
<NiceP>Receive some sats to get started</NiceP>
</div>
</Match>
<Match when={activity.state === "ready" && activity().length >= 0}>
<For each={activity.latest}>
{(activityItem) => (
<UnifiedActivityItem item={activityItem} onClick={openDetailsModal} />
)}
</For>
</Match>
</Switch>
</>
);
}

View File

@@ -1,102 +1,138 @@
import { ParentComponent, createMemo, createResource } from "solid-js";
import { Match, ParentComponent, Switch, createMemo, createResource } from "solid-js";
import { satsToUsd } from "~/utils/conversions";
import bolt from "~/assets/icons/bolt.svg"
import chain from "~/assets/icons/chain.svg"
import bolt from "~/assets/icons/bolt.svg";
import chain from "~/assets/icons/chain.svg";
import shuffle from "~/assets/icons/shuffle.svg";
import { timeAgo } from "~/utils/prettyPrintTime";
import { MutinyTagItem } from "~/utils/tags";
import { generateGradient } from "~/utils/gradientHash";
import { useMegaStore } from "~/state/megaStore";
import { Contact } from "@mutinywallet/mutiny-wasm";
export const ActivityAmount: ParentComponent<{ amount: string, price: number, positive?: boolean, center?: boolean }> = (props) => {
const amountInUsd = createMemo(() => {
const parsed = Number(props.amount);
if (isNaN(parsed)) {
return props.amount;
} else {
return satsToUsd(props.price, parsed, true);
}
})
export const ActivityAmount: ParentComponent<{
amount: string;
price: number;
positive?: boolean;
center?: boolean;
}> = (props) => {
const amountInUsd = createMemo(() => {
const parsed = Number(props.amount);
if (isNaN(parsed)) {
return props.amount;
} else {
return satsToUsd(props.price, parsed, true);
}
});
const prettyPrint = createMemo(() => {
const parsed = Number(props.amount);
if (isNaN(parsed)) {
return props.amount;
} else {
return parsed.toLocaleString();
}
})
const prettyPrint = createMemo(() => {
const parsed = Number(props.amount);
if (isNaN(parsed)) {
return props.amount;
} else {
return parsed.toLocaleString();
}
});
return (
<div class="flex flex-col"
classList={{ "items-end": !props.center, "items-center": props.center }}>
<div class="text-base"
classList={{ "text-m-green": props.positive }}
>{props.positive && "+ "}{prettyPrint()}&nbsp;<span class="text-sm">SATS</span>
</div>
<div class="text-sm text-neutral-500">&#8776;&nbsp;{amountInUsd()}&nbsp;<span class="text-sm">USD</span></div>
return (
<div
class="flex flex-col"
classList={{ "items-end": !props.center, "items-center": props.center }}
>
<div class="text-base" classList={{ "text-m-green": props.positive }}>
{props.positive && "+ "}
{prettyPrint()}&nbsp;<span class="text-sm">SATS</span>
</div>
<div class="text-sm text-neutral-500">
&#8776;&nbsp;{amountInUsd()}&nbsp;<span class="text-sm">USD</span>
</div>
</div>
);
};
function LabelCircle(props: { name?: string; contact: boolean; label: boolean }) {
// TODO: don't need to run this if it's not a contact
const [gradient] = createResource(async () => {
return generateGradient(props.name || "?");
});
const text = () =>
props.contact && props.name && props.name.length ? props.name[0] : props.label ? "≡" : "?";
const bg = () => (props.name && props.contact ? gradient() : "gray");
return (
<div
class="flex-none h-[3rem] w-[3rem] rounded-full flex items-center justify-center text-3xl uppercase border-t border-b border-t-white/50 border-b-white/10"
style={{ background: bg() }}
>
{text()}
</div>
);
}
export type HackActivityType = "Lightning" | "OnChain" | "ChannelOpen";
export function ActivityItem(props: {
// This is actually the ActivityType enum but wasm is hard
kind: HackActivityType;
contacts: Contact[];
labels: string[];
amount: number | bigint;
date?: number | bigint;
positive?: boolean;
onClick?: () => void;
}) {
const [state, _actions] = useMegaStore();
const firstContact = () => (props.contacts?.length ? props.contacts[0] : null);
return (
<div
onClick={() => props.onClick && props.onClick()}
class="grid grid-cols-[auto_minmax(0,_1fr)_minmax(0,_max-content)] pb-4 gap-4 border-b border-neutral-800 last:border-b-0"
classList={{ "cursor-pointer": !!props.onClick }}
>
<div class="flex gap-2 md:gap-4 items-center">
<div class="">
<Switch>
<Match when={props.kind === "Lightning"}>
<img class="w-[1rem]" src={bolt} alt="lightning" />
</Match>
<Match when={props.kind === "OnChain"}>
<img class="w-[1rem]" src={chain} alt="onchain" />
</Match>
<Match when={props.kind === "ChannelOpen"}>
<img class="w-[1rem]" src={shuffle} alt="swap" />
</Match>
</Switch>
</div>
)
}
function LabelCircle(props: { name?: string, contact: boolean }) {
// TODO: don't need to run this if it's not a contact
const [gradient] = createResource(async () => {
return generateGradient(props.name || "?")
})
const text = () => (props.contact && props.name && props.name.length) ? props.name[0] : (props.name && props.name.length) ? "≡" : "?"
const bg = () => (props.name && props.contact) ? gradient() : "gray"
return (
<div class="flex-none h-[3rem] w-[3rem] rounded-full flex items-center justify-center text-3xl uppercase border-t border-b border-t-white/50 border-b-white/10"
style={{ background: bg() }}
>
{text()}
<div class="">
<LabelCircle
name={firstContact()?.name}
contact={props.contacts?.length > 0}
label={props.labels?.length > 0}
/>
</div>
)
</div>
<div class="flex flex-col">
<Switch>
<Match when={firstContact()?.name}>
<span class="text-base font-semibold truncate">{firstContact()?.name}</span>
</Match>
<Match when={props.labels.length > 0}>
<span class="text-base font-semibold truncate">{props.labels[0]}</span>
</Match>
<Match when={true}>
<span class="text-base font-semibold text-neutral-500">Unknown</span>
</Match>
</Switch>
<time class="text-sm text-neutral-500">{timeAgo(props.date)}</time>
</div>
<div class="">
<ActivityAmount
amount={props.amount.toString()}
price={state.price}
positive={props.positive}
/>
</div>
</div>
);
}
// function that takes a list of MutinyTagItems and returns bool if one of those items is of kind Contact
function includesContact(labels: MutinyTagItem[]) {
return labels.some((label) => label.kind === "Contact")
}
// sort the labels so that the contact is always first
function sortLabels(labels: MutinyTagItem[]) {
const contact = labels.find(label => label.kind === "Contact");
return contact ? [contact, ...labels.filter(label => label !== contact)] : labels;
}
// return a string of each label name separated by a comma and a space. if the array is empty return "Unknown"
function labelString(labels: MutinyTagItem[]) {
return labels.length ? labels.map(label => label.name).join(", ") : "Unknown"
}
export function ActivityItem(props: { kind: "lightning" | "onchain", labels: MutinyTagItem[], amount: number | bigint, date?: number | bigint, positive?: boolean, onClick?: () => void }) {
const labels = () => sortLabels(props.labels)
const [state, _actions] = useMegaStore();
return (
<div
onClick={() => props.onClick && props.onClick()}
class="grid grid-cols-[auto_minmax(0,_1fr)_minmax(0,_max-content)] pb-4 gap-4 border-b border-neutral-800 last:border-b-0"
classList={{ "cursor-pointer": !!props.onClick }}
>
<div class="flex gap-2 md:gap-4 items-center">
<div class="">
{props.kind === "lightning" ? <img class="w-[1rem]" src={bolt} alt="lightning" /> : <img class="w-[1rem]" src={chain} alt="onchain" />}
</div>
<div class="">
<LabelCircle name={labels().length ? labels()[0].name : ""} contact={includesContact(labels())} />
</div>
</div>
<div class="flex flex-col">
<span class="text-base font-semibold truncate" classList={{ "text-neutral-500": labels().length === 0 }}>{labelString(labels())}</span>
<time class="text-sm text-neutral-500">{timeAgo(props.date)}</time>
</div>
<div class="">
<ActivityAmount amount={props.amount.toString()} price={state.price} positive={props.positive} />
</div>
</div>
)
}

View File

@@ -1,51 +1,53 @@
import { Dialog } from "@kobalte/core"
import {
For,
JSX,
Match,
ParentComponent,
Show,
Suspense,
Switch,
createEffect,
createMemo,
} from "solid-js"
import { Hr, ModalCloseButton, TinyButton, VStack } from "~/components/layout"
import { MutinyInvoice } from "@mutinywallet/mutiny-wasm"
import { OnChainTx } from "./Activity"
createResource
} from "solid-js";
import { Hr, ModalCloseButton, TinyButton, VStack } from "~/components/layout";
import { MutinyInvoice } from "@mutinywallet/mutiny-wasm";
import { OnChainTx } from "./Activity";
import bolt from "~/assets/icons/bolt-black.svg"
import chain from "~/assets/icons/chain-black.svg"
import copyIcon from "~/assets/icons/copy.svg"
import bolt from "~/assets/icons/bolt-black.svg";
import chain from "~/assets/icons/chain-black.svg";
import copyIcon from "~/assets/icons/copy.svg";
import { ActivityAmount } from "./ActivityItem"
import { CopyButton } from "./ShareCard"
import { prettyPrintTime } from "~/utils/prettyPrintTime"
import { useMegaStore } from "~/state/megaStore"
import { tagToMutinyTag } from "~/utils/tags"
import { useCopy } from "~/utils/useCopy"
import mempoolTxUrl from "~/utils/mempoolTxUrl"
import { Network } from "~/logic/mutinyWalletSetup"
import { AmountSmall } from "./Amount"
import { ActivityAmount, HackActivityType } from "./ActivityItem";
import { CopyButton } from "./ShareCard";
import { prettyPrintTime } from "~/utils/prettyPrintTime";
import { useMegaStore } from "~/state/megaStore";
import { tagToMutinyTag } from "~/utils/tags";
import { useCopy } from "~/utils/useCopy";
import mempoolTxUrl from "~/utils/mempoolTxUrl";
import { Network } from "~/logic/mutinyWalletSetup";
import { AmountSmall } from "./Amount";
export const OVERLAY = "fixed inset-0 z-50 bg-black/50 backdrop-blur-sm"
export const DIALOG_POSITIONER = "fixed inset-0 z-50 flex items-center justify-center"
export const OVERLAY = "fixed inset-0 z-50 bg-black/50 backdrop-blur-sm";
export const DIALOG_POSITIONER = "fixed inset-0 z-50 flex items-center justify-center";
export const DIALOG_CONTENT =
"max-w-[500px] w-[90vw] max-h-[100dvh] overflow-y-scroll disable-scrollbars mx-4 p-4 bg-neutral-800/80 backdrop-blur-md shadow-xl rounded-xl border border-white/10"
"max-w-[500px] w-[90vw] max-h-[100dvh] overflow-y-scroll disable-scrollbars mx-4 p-4 bg-neutral-800/80 backdrop-blur-md shadow-xl rounded-xl border border-white/10";
function LightningHeader(props: { info: MutinyInvoice }) {
const [state, _actions] = useMegaStore()
const [state, _actions] = useMegaStore();
const tags = createMemo(() => {
if (props.info.labels.length) {
const contact = state.mutiny_wallet?.get_contact(props.info.labels[0])
const contact = state.mutiny_wallet?.get_contact(props.info.labels[0]);
if (contact) {
return [tagToMutinyTag(contact)]
return [tagToMutinyTag(contact)];
} else {
return []
return [];
}
} else {
return []
return [];
}
})
});
return (
<div class="flex flex-col items-center gap-4">
@@ -78,58 +80,54 @@ function LightningHeader(props: { info: MutinyInvoice }) {
}
function OnchainHeader(props: { info: OnChainTx }) {
const [state, _actions] = useMegaStore()
const [state, _actions] = useMegaStore();
const tags = createMemo(() => {
if (props.info.labels.length) {
const contact = state.mutiny_wallet?.get_contact(props.info.labels[0])
const contact = state.mutiny_wallet?.get_contact(props.info.labels[0]);
if (contact) {
return [tagToMutinyTag(contact)]
return [tagToMutinyTag(contact)];
} else {
return []
return [];
}
} else {
return []
return [];
}
})
});
const isSend = () => {
return props.info.sent > props.info.received
}
return props.info.sent > props.info.received;
};
const amount = () => {
if (isSend()) {
return (props.info.sent - props.info.received).toString()
return (props.info.sent - props.info.received).toString();
} else {
return (props.info.received - props.info.sent).toString()
return (props.info.received - props.info.sent).toString();
}
}
};
return (
<div class="flex flex-col items-center gap-4">
<div class="p-4 bg-neutral-100 rounded-full">
<img src={chain} alt="blockchain" class="w-8 h-8" />
</div>
<h1 class="uppercase font-semibold">
{isSend() ? "On-chain send" : "On-chain receive"}
</h1>
<ActivityAmount
center
amount={amount() ?? "0"}
price={state.price}
positive={!isSend()}
/>
<h1 class="uppercase font-semibold">{isSend() ? "On-chain send" : "On-chain receive"}</h1>
<ActivityAmount center amount={amount() ?? "0"} price={state.price} positive={!isSend()} />
<For each={tags()}>
{(tag) => (
<TinyButton tag={tag} onClick={() => {
// noop
}}>
<TinyButton
tag={tag}
onClick={() => {
// noop
}}
>
{tag.name}
</TinyButton>
)}
</For>
</div>
)
);
}
const KeyValue: ParentComponent<{ key: string }> = (props) => {
@@ -138,11 +136,11 @@ const KeyValue: ParentComponent<{ key: string }> = (props) => {
<span class="uppercase font-semibold whitespace-nowrap">{props.key}</span>
<span class="font-light">{props.children}</span>
</li>
)
}
);
};
function MiniStringShower(props: { text: string }) {
const [copy, _copied] = useCopy({ copiedTimeout: 1000 })
const [copy, _copied] = useCopy({ copiedTimeout: 1000 });
return (
<div class="w-full grid gap-1 grid-cols-[minmax(0,_1fr)_auto]">
@@ -151,7 +149,7 @@ function MiniStringShower(props: { text: string }) {
<img src={copyIcon} alt="copy" class="w-4 h-4" />
</button>
</div>
)
);
}
function LightningDetails(props: { info: MutinyInvoice }) {
@@ -159,20 +157,14 @@ function LightningDetails(props: { info: MutinyInvoice }) {
<VStack>
<ul class="flex flex-col gap-4">
<KeyValue key="Status">
<span class="text-neutral-300">
{props.info.paid ? "Paid" : "Unpaid"}
</span>
<span class="text-neutral-300">{props.info.paid ? "Paid" : "Unpaid"}</span>
</KeyValue>
<KeyValue key="When">
<span class="text-neutral-300">
{prettyPrintTime(Number(props.info.last_updated))}
</span>
<span class="text-neutral-300">{prettyPrintTime(Number(props.info.last_updated))}</span>
</KeyValue>
<Show when={props.info.description}>
<KeyValue key="Description">
<span class="text-neutral-300 truncate">
{props.info.description}
</span>
<span class="text-neutral-300 truncate">{props.info.description}</span>
</KeyValue>
</Show>
<KeyValue key="Fees">
@@ -191,32 +183,28 @@ function LightningDetails(props: { info: MutinyInvoice }) {
</KeyValue>
</ul>
</VStack>
)
);
}
function OnchainDetails(props: { info: OnChainTx }) {
const [state, _actions] = useMegaStore()
const [state, _actions] = useMegaStore();
const confirmationTime = () => {
return props.info.confirmation_time?.Confirmed?.time
}
return props.info.confirmation_time?.Confirmed?.time;
};
const network = state.mutiny_wallet?.get_network() as Network
const network = state.mutiny_wallet?.get_network() as Network;
return (
<VStack>
<ul class="flex flex-col gap-4">
<KeyValue key="Status">
<span class="text-neutral-300">
{confirmationTime() ? "Confirmed" : "Unconfirmed"}
</span>
<span class="text-neutral-300">{confirmationTime() ? "Confirmed" : "Unconfirmed"}</span>
</KeyValue>
<Show when={confirmationTime()}>
<KeyValue key="When">
<span class="text-neutral-300">
{confirmationTime()
? prettyPrintTime(Number(confirmationTime()))
: "Pending"}
{confirmationTime() ? prettyPrintTime(Number(confirmationTime())) : "Pending"}
</span>
</KeyValue>
</Show>
@@ -238,63 +226,86 @@ function OnchainDetails(props: { info: OnChainTx }) {
Mempool.space
</a>
</VStack>
)
);
}
export function DetailsModal(props: {
open: boolean
data: MutinyInvoice | OnChainTx
setOpen: (open: boolean) => void
children?: JSX.Element
export function DetailsIdModal(props: {
open: boolean;
kind?: HackActivityType;
id: string;
setOpen: (open: boolean) => void;
}) {
const json = createMemo(() => JSON.stringify(props.data, null, 2))
const [state, _actions] = useMegaStore();
const id = () => props.id;
const kind = () => props.kind;
// TODO: is there a cleaner way to do refetch when id changes?
const [data, { refetch }] = createResource(async () => {
if (kind() === "Lightning") {
console.log("reading invoice: ", id());
const invoice = await state.mutiny_wallet?.get_invoice_by_hash(id());
return invoice;
} else {
console.log("reading tx: ", id());
const tx = await state.mutiny_wallet?.get_transaction(id());
return tx;
}
});
createEffect(() => {
if (props.id && props.kind && props.open) {
refetch();
}
});
const json = createMemo(() => JSON.stringify(data() || "", null, 2));
const isInvoice = () => {
return ("bolt11" in props.data) as boolean
}
return props.kind === "Lightning";
};
return (
<Dialog.Root
open={props.open}
onOpenChange={props.setOpen}
>
<Dialog.Root open={props.open} onOpenChange={props.setOpen}>
<Dialog.Portal>
<Dialog.Overlay class={OVERLAY} />
<div class={DIALOG_POSITIONER}>
<Dialog.Content class={DIALOG_CONTENT}>
<div class="flex justify-between mb-2">
<div />
<Dialog.CloseButton>
<ModalCloseButton />
</Dialog.CloseButton>
</div>
<Dialog.Title>
<Switch>
<Match when={isInvoice()}>
<LightningHeader info={props.data as MutinyInvoice} />
</Match>
<Match when={true}>
<OnchainHeader info={props.data as OnChainTx} />
</Match>
</Switch>
</Dialog.Title>
<Hr />
<Dialog.Description class="flex flex-col gap-4">
<Switch>
<Match when={isInvoice()}>
<LightningDetails info={props.data as MutinyInvoice} />
</Match>
<Match when={true}>
<OnchainDetails info={props.data as OnChainTx} />
</Match>
</Switch>
<div class="flex justify-center">
<CopyButton title="Copy" text={json()} />
<Suspense>
<div class="flex justify-between mb-2">
<div />
<Dialog.CloseButton>
<ModalCloseButton />
</Dialog.CloseButton>
</div>
</Dialog.Description>
<Dialog.Title>
<Switch>
<Match when={isInvoice()}>
<LightningHeader info={data() as MutinyInvoice} />
</Match>
<Match when={true}>
<OnchainHeader info={data() as OnChainTx} />
</Match>
</Switch>
</Dialog.Title>
<Hr />
<Dialog.Description class="flex flex-col gap-4">
<Switch>
<Match when={isInvoice()}>
<LightningDetails info={data() as MutinyInvoice} />
</Match>
<Match when={true}>
<OnchainDetails info={data() as OnChainTx} />
</Match>
</Switch>
<div class="flex justify-center">
<CopyButton title="Copy" text={json()} />
</div>
</Dialog.Description>
</Suspense>
</Dialog.Content>
</div>
</Dialog.Portal>
</Dialog.Root>
)
);
}

View File

@@ -1,66 +1,86 @@
import { Show, createSignal, onMount } from "solid-js";
import { Show, createSignal } from "solid-js";
import { Button, ButtonLink, SmallHeader } from "./layout";
import { useMegaStore } from "~/state/megaStore";
import { showToast } from "./Toaster";
import save from "~/assets/icons/save.svg"
import save from "~/assets/icons/save.svg";
import close from "~/assets/icons/close.svg";
import restore from "~/assets/icons/upload.svg";
export function OnboardWarning() {
const [state, actions] = useMegaStore();
const [dismissedBackup, setDismissedBackup] = createSignal(false);
const [state, actions] = useMegaStore();
const [dismissedBackup, setDismissedBackup] = createSignal(false);
onMount(() => {
actions.sync()
})
function hasMoney() {
return state.balance?.confirmed || state.balance?.lightning || state.balance?.unconfirmed;
}
function hasMoney() {
return state.balance?.confirmed || state.balance?.lightning || state.balance?.unconfirmed
}
return (
<>
{/* TODO: show this once we have a restore flow */}
<Show when={false}>
<div class="grid grid-cols-[auto_minmax(0,_1fr)_auto] rounded-xl p-4 gap-4 bg-neutral-950/50">
<div class="self-center">
<img src={restore} alt="backup" class="w-8 h-8" />
</div>
<div class='flex md:flex-row flex-col items-center gap-4'>
<div class="flex flex-col">
<SmallHeader>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!
</p>
</div>
<Button intent="green" layout="xs" class="self-start md:self-auto" onClick={() => { showToast({ title: "Unimplemented", description: "We don't do that yet" }) }}>Restore</Button>
</div>
<button tabindex="-1" onClick={() => { actions.dismissRestorePrompt() }} class="self-center hover:bg-white/10 rounded-lg active:bg-m-blue w-8">
<img src={close} alt="Close" />
</button>
</div>
</Show>
<Show when={!state.has_backed_up && hasMoney() && !dismissedBackup()}>
<div class="grid grid-cols-[auto_minmax(0,_1fr)_auto] rounded-xl p-4 gap-4 bg-neutral-950/50">
<div class="self-center">
<img src={save} alt="backup" class="w-8 h-8" />
</div>
<div class='flex flex-row max-md:items-center justify-between gap-4'>
<div class="flex flex-col">
<SmallHeader>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.
</p>
</div>
<div class="flex items-center">
<ButtonLink intent="blue" layout="xs" class="self-auto" href="/backup">Backup</ButtonLink>
</div>
</div>
<button tabindex="-1" onClick={() => { setDismissedBackup(true) }} class="self-center hover:bg-white/10 rounded-lg active:bg-m-blue w-8">
<img src={close} alt="Close" />
</button>
</div>
</Show>
</>
)
}
return (
<>
{/* TODO: show this once we have a restore flow */}
<Show when={false}>
<div class="grid grid-cols-[auto_minmax(0,_1fr)_auto] rounded-xl p-4 gap-4 bg-neutral-950/50">
<div class="self-center">
<img src={restore} alt="backup" class="w-8 h-8" />
</div>
<div class="flex md:flex-row flex-col items-center gap-4">
<div class="flex flex-col">
<SmallHeader>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!
</p>
</div>
<Button
intent="green"
layout="xs"
class="self-start md:self-auto"
onClick={() => {
showToast({ title: "Unimplemented", description: "We don't do that yet" });
}}
>
Restore
</Button>
</div>
<button
tabindex="-1"
onClick={() => {
actions.dismissRestorePrompt();
}}
class="self-center hover:bg-white/10 rounded-lg active:bg-m-blue w-8"
>
<img src={close} alt="Close" />
</button>
</div>
</Show>
<Show when={!state.has_backed_up && hasMoney() && !dismissedBackup()}>
<div class="grid grid-cols-[auto_minmax(0,_1fr)_auto] rounded-xl p-4 gap-4 bg-neutral-950/50">
<div class="self-center">
<img src={save} alt="backup" class="w-8 h-8" />
</div>
<div class="flex flex-row max-md:items-center justify-between gap-4">
<div class="flex flex-col">
<SmallHeader>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.
</p>
</div>
<div class="flex items-center">
<ButtonLink intent="blue" layout="xs" class="self-auto" href="/backup">
Backup
</ButtonLink>
</div>
</div>
<button
tabindex="-1"
onClick={() => {
setDismissedBackup(true);
}}
class="self-center hover:bg-white/10 rounded-lg active:bg-m-blue w-8"
>
<img src={close} alt="Close" />
</button>
</div>
</Show>
</>
);
}

View File

@@ -389,8 +389,6 @@ export default function Send() {
const txid = await state.mutiny_wallet?.send_to_address(address()!, amountSats(), tags);
sentDetails.amount = amountSats();
sentDetails.destination = address();
// TODO: figure out if this is necessary, it takes forever
await actions.sync();
sentDetails.txid = txid;
}
setSentDetails(sentDetails as SentDetails);