mirror of
https://github.com/aljazceru/mutiny-web.git
synced 2026-01-16 12:44:26 +01:00
300 lines
8.7 KiB
TypeScript
300 lines
8.7 KiB
TypeScript
import { Dialog } from "@kobalte/core"
|
|
import {
|
|
For,
|
|
JSX,
|
|
Match,
|
|
ParentComponent,
|
|
Show,
|
|
Switch,
|
|
createMemo,
|
|
} from "solid-js"
|
|
import { Hr, TinyButton, VStack } from "~/components/layout"
|
|
import { MutinyInvoice } from "@mutinywallet/mutiny-wasm"
|
|
import { OnChainTx } from "./Activity"
|
|
|
|
import close from "~/assets/icons/close.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"
|
|
|
|
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"
|
|
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"
|
|
|
|
function LightningHeader(props: { info: MutinyInvoice }) {
|
|
const [state, _actions] = useMegaStore()
|
|
|
|
const tags = createMemo(() => {
|
|
if (props.info.labels.length) {
|
|
let contact = state.mutiny_wallet?.get_contact(props.info.labels[0])
|
|
if (contact) {
|
|
return [tagToMutinyTag(contact)]
|
|
} else {
|
|
return []
|
|
}
|
|
} else {
|
|
return []
|
|
}
|
|
})
|
|
|
|
return (
|
|
<div class="flex flex-col items-center gap-4">
|
|
<div class="p-4 bg-neutral-100 rounded-full">
|
|
<img src={bolt} alt="lightning bolt" class="w-8 h-8" />
|
|
</div>
|
|
<h1 class="uppercase font-semibold">
|
|
{props.info.is_send ? "Lightning send" : "Lightning receive"}
|
|
</h1>
|
|
<ActivityAmount
|
|
center
|
|
amount={props.info.amount_sats?.toString() ?? "0"}
|
|
price={state.price}
|
|
positive={!props.info.is_send}
|
|
/>
|
|
<For each={tags()}>
|
|
{(tag) => (
|
|
<TinyButton tag={tag} onClick={() => {}}>
|
|
{tag.name}
|
|
</TinyButton>
|
|
)}
|
|
</For>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function OnchainHeader(props: { info: OnChainTx }) {
|
|
const [state, _actions] = useMegaStore()
|
|
|
|
const tags = createMemo(() => {
|
|
if (props.info.labels.length) {
|
|
let contact = state.mutiny_wallet?.get_contact(props.info.labels[0])
|
|
if (contact) {
|
|
return [tagToMutinyTag(contact)]
|
|
} else {
|
|
return []
|
|
}
|
|
} else {
|
|
return []
|
|
}
|
|
})
|
|
|
|
const isSend = () => {
|
|
return props.info.sent > props.info.received
|
|
}
|
|
|
|
const amount = () => {
|
|
if (isSend()) {
|
|
return (props.info.sent - props.info.received).toString()
|
|
} else {
|
|
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()}
|
|
/>
|
|
<For each={tags()}>
|
|
{(tag) => (
|
|
<TinyButton tag={tag} onClick={() => {}}>
|
|
{tag.name}
|
|
</TinyButton>
|
|
)}
|
|
</For>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const KeyValue: ParentComponent<{ key: string }> = (props) => {
|
|
return (
|
|
<li class="flex justify-between items-center gap-4">
|
|
<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 })
|
|
|
|
return (
|
|
<div class="w-full grid gap-1 grid-cols-[minmax(0,_1fr)_auto]">
|
|
<pre class="truncate text-neutral-300 font-light">{props.text}</pre>
|
|
<button class="w-[1rem]" onClick={() => copy(props.text)}>
|
|
<img src={copyIcon} alt="copy" class="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function LightningDetails(props: { info: MutinyInvoice }) {
|
|
return (
|
|
<VStack>
|
|
<ul class="flex flex-col gap-4">
|
|
<KeyValue key="Status">
|
|
<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>
|
|
</KeyValue>
|
|
<Show when={props.info.description}>
|
|
<KeyValue key="Description">
|
|
<span class="text-neutral-300 truncate">
|
|
{props.info.description}
|
|
</span>
|
|
</KeyValue>
|
|
</Show>
|
|
<KeyValue key="Fees">
|
|
<span class="text-neutral-300">
|
|
<AmountSmall amountSats={props.info.fees_paid} />
|
|
</span>
|
|
</KeyValue>
|
|
<KeyValue key="Bolt11">
|
|
<MiniStringShower text={props.info.bolt11 ?? ""} />
|
|
</KeyValue>
|
|
<KeyValue key="Payment Hash">
|
|
<MiniStringShower text={props.info.payment_hash ?? ""} />
|
|
</KeyValue>
|
|
<KeyValue key="Preimage">
|
|
<MiniStringShower text={props.info.preimage ?? ""} />
|
|
</KeyValue>
|
|
</ul>
|
|
</VStack>
|
|
)
|
|
}
|
|
|
|
function OnchainDetails(props: { info: OnChainTx }) {
|
|
const [state, _actions] = useMegaStore()
|
|
|
|
const confirmationTime = () => {
|
|
return props.info.confirmation_time?.Confirmed?.time
|
|
}
|
|
|
|
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>
|
|
</KeyValue>
|
|
<Show when={confirmationTime()}>
|
|
<KeyValue key="When">
|
|
<span class="text-neutral-300">
|
|
{confirmationTime()
|
|
? prettyPrintTime(Number(confirmationTime()))
|
|
: "Pending"}
|
|
</span>
|
|
</KeyValue>
|
|
</Show>
|
|
<KeyValue key="Fee">
|
|
<span class="text-neutral-300">
|
|
<AmountSmall amountSats={props.info.fee} />
|
|
</span>
|
|
</KeyValue>
|
|
<KeyValue key="Txid">
|
|
<MiniStringShower text={props.info.txid ?? ""} />
|
|
</KeyValue>
|
|
</ul>
|
|
<a
|
|
class="uppercase font-light text-center"
|
|
href={mempoolTxUrl(props.info.txid, network)}
|
|
target="_blank"
|
|
rel="noreferrer"
|
|
>
|
|
Mempool.space
|
|
</a>
|
|
</VStack>
|
|
)
|
|
}
|
|
|
|
export function DetailsModal(props: {
|
|
open: boolean
|
|
data: MutinyInvoice | OnChainTx
|
|
setOpen: (open: boolean) => void
|
|
children?: JSX.Element
|
|
}) {
|
|
const json = createMemo(() => JSON.stringify(props.data, null, 2))
|
|
|
|
const isInvoice = () => {
|
|
return ("bolt11" in props.data) as boolean
|
|
}
|
|
|
|
return (
|
|
<Dialog.Root
|
|
open={props.open}
|
|
onOpenChange={(isOpen) => props.setOpen(isOpen)}
|
|
>
|
|
<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>
|
|
<button
|
|
tabindex="-1"
|
|
class="self-center hover:bg-white/10 rounded-lg active:bg-m-blue "
|
|
>
|
|
<img src={close} alt="Close" class="w-8 h-8" />
|
|
</button>
|
|
</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()} />
|
|
</div>
|
|
</Dialog.Description>
|
|
</Dialog.Content>
|
|
</div>
|
|
</Dialog.Portal>
|
|
</Dialog.Root>
|
|
)
|
|
}
|