add channelopen and channelclose modals

This commit is contained in:
Paul Miller
2023-06-08 17:04:03 -05:00
parent 10e569654d
commit 6ad4184566
6 changed files with 271 additions and 80 deletions

View File

@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 20C5.45 20 4.979 19.804 4.587 19.412C4.195 19.02 3.99934 18.5493 4 18V15H6V18H18V15H20V18C20 18.55 19.804 19.021 19.412 19.413C19.02 19.805 18.5493 20.0007 18 20H6ZM12 16L7 11L8.4 9.55L11 12.15V4H13V12.15L15.6 9.55L17 11L12 16Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 359 B

View File

@@ -0,0 +1,3 @@
<svg width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4 8.99985h3.5c.736 0 1.393.391 1.851 1.00095.32529-.60198.7255-1.16042 1.191-1.66195-.803-.823-1.866-1.339-3.042-1.339H4c-.26522 0-.51957.10536-.70711.29289C3.10536 7.48028 3 7.73463 3 7.99985s.10536.51957.29289.70711c.18754.18753.44189.29289.70711.29289Zm7.685 3.11095c.551-1.657 2.256-3.11095 3.649-3.11095h1.838l-1.293 1.29295c-.0928.0929-.1665.2031-.2167.3244-.0503.1213-.0761.2513-.0761.3826 0 .1314.0258.2614.0761.3827.0502.1213.1239.2315.2167.3243.0928.0929.2031.1665.3244.2168.1213.0502.2513.0761.3826.0761.1313 0 .2613-.0259.3826-.0761.1213-.0503.2316-.1239.3244-.2168L21 7.99985l-3.707-3.707c-.0928-.09285-.2031-.16649-.3244-.21674C16.8473 4.02586 16.7173 4 16.586 4c-.1313 0-.2613.02586-.3826.07611-.1213.05025-.2316.12389-.3244.21674-.0928.09284-.1665.20307-.2167.32437-.0503.12131-.0761.25133-.0761.38263 0 .1313.0258.26132.0761.38262.0502.12131.1239.23153.2167.32438l1.293 1.293h-1.838c-2.274 0-4.711 1.967-5.547 4.47895l-.472 1.411c-.641 1.926-2.072 3.11-2.815 3.11H4c-.26522 0-.51957.1054-.70711.2929-.18753.1876-.29289.4419-.29289.7071 0 .2653.10536.5196.29289.7072.18754.1875.44189.2928.70711.2928h2.5c1.837 0 3.863-1.925 4.713-4.479l.472-1.41Zm4.194 1.182c-.0929.0928-.1667.203-.217.3244-.0503.1213-.0762.2513-.0762.3826 0 .1314.0259.2614.0762.3827.0503.1214.1241.2316.217.3243l1.293 1.293h-2.338c-1.268 0-2.33-.891-2.691-2.108-.2661.773-.6326 1.5076-1.09 2.185.886 1.162 2.243 1.923 3.781 1.923h2.338l-1.293 1.293c-.0928.0929-.1665.2031-.2167.3244-.0503.1213-.0761.2513-.0761.3826 0 .1314.0258.2614.0761.3827.0502.1213.1239.2315.2167.3244.0928.0928.2031.1664.3244.2167.1213.0502.2513.0761.3826.0761.1313 0 .2613-.0259.3826-.0761.1213-.0503.2316-.1239.3244-.2167L21 16.9998l-3.707-3.707c-.0928-.0929-.203-.1666-.3243-.2169-.1213-.0504-.2514-.0763-.3827-.0763-.1313 0-.2614.0259-.3827.0763-.1213.0503-.2315.124-.3243.2169Z" fill="#000"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 20C5.45 20 4.979 19.804 4.587 19.412C4.195 19.02 3.99934 18.5493 4 18V15H6V18H18V15H20V18C20 18.55 19.804 19.021 19.412 19.413C19.02 19.805 18.5493 20.0007 18 20H6ZM11 16V7.85L8.4 10.45L7 9L12 4L17 9L15.6 10.45L13 7.85V16H11Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 357 B

View File

@@ -1,5 +1,13 @@
import { NiceP } from "./layout";
import { For, Match, Show, Switch, createEffect, createSignal } from "solid-js";
import {
For,
Match,
Show,
Switch,
createEffect,
createSignal,
onMount
} from "solid-js";
import { useMegaStore } from "~/state/megaStore";
import { ActivityItem as MutinyActivity } from "@mutinywallet/mutiny-wasm";
import { ActivityItem, HackActivityType } from "./ActivityItem";
@@ -43,6 +51,10 @@ function UnifiedActivityItem(props: {
item: MutinyActivity;
onClick: (id: string, kind: HackActivityType) => void;
}) {
onMount(() => {
console.log(props.item);
});
const click = () => {
props.onClick(
props.item.id,

View File

@@ -9,7 +9,10 @@ import { satsToUsd } from "~/utils/conversions";
import bolt from "~/assets/icons/bolt.svg";
import chain from "~/assets/icons/chain.svg";
import shuffle from "~/assets/icons/shuffle.svg";
import on from "~/assets/icons/upload-channel.svg";
import off from "~/assets/icons/download-channel.svg";
import { timeAgo } from "~/utils/prettyPrintTime";
import { generateGradient } from "~/utils/gradientHash";
import { useMegaStore } from "~/state/megaStore";
import { Contact } from "@mutinywallet/mutiny-wasm";
@@ -65,10 +68,14 @@ function LabelCircle(props: {
name?: string;
contact: boolean;
label: boolean;
channel?: HackActivityType;
}) {
// TODO: don't need to run this if it's not a contact
const [gradient] = createResource(async () => {
return generateGradient(props.name || "?");
if (props.name && props.contact) {
return generateGradient(props.name || "?");
} else {
return undefined;
}
});
const text = () =>
@@ -77,19 +84,31 @@ function LabelCircle(props: {
: props.label
? "≡"
: "?";
const bg = () => (props.name && props.contact ? gradient() : "gray");
const bg = () => (props.name && props.contact ? gradient() : "");
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"
class="flex-none h-[3rem] w-[3rem] rounded-full bg-neutral-700 flex items-center justify-center text-3xl uppercase border-t border-b border-t-white/50 border-b-white/10"
style={{ background: bg() }}
>
{text()}
<Switch>
<Match when={props.channel === "ChannelOpen"}>
<img src={on} alt="channel open" />
</Match>
<Match when={props.channel === "ChannelClose"}>
<img src={off} alt="channel close" />
</Match>
<Match when={true}>{text()}</Match>
</Switch>
</div>
);
}
export type HackActivityType = "Lightning" | "OnChain" | "ChannelOpen";
export type HackActivityType =
| "Lightning"
| "OnChain"
| "ChannelOpen"
| "ChannelClose";
export function ActivityItem(props: {
// This is actually the ActivityType enum but wasm is hard
@@ -121,7 +140,12 @@ export function ActivityItem(props: {
<Match when={props.kind === "OnChain"}>
<img class="w-[1rem]" src={chain} alt="onchain" />
</Match>
<Match when={props.kind === "ChannelOpen"}>
<Match
when={
props.kind === "ChannelOpen" ||
props.kind === "ChannelClose"
}
>
<img class="w-[1rem]" src={shuffle} alt="swap" />
</Match>
</Switch>
@@ -131,11 +155,22 @@ export function ActivityItem(props: {
name={firstContact()?.name}
contact={props.contacts?.length > 0}
label={props.labels?.length > 0}
channel={props.kind}
/>
</div>
</div>
<div class="flex flex-col">
<Switch>
<Match when={props.kind === "ChannelClose"}>
<span class="text-base font-semibold text-neutral-500">
Channel Close
</span>
</Match>
<Match when={props.kind === "ChannelOpen"}>
<span class="text-base font-semibold text-neutral-500">
Channel Open
</span>{" "}
</Match>
<Match when={firstContact()?.name}>
<span class="text-base font-semibold truncate">
{firstContact()?.name}
@@ -146,9 +181,15 @@ export function ActivityItem(props: {
{props.labels[0]}
</span>
</Match>
<Match when={true}>
<Match when={props.positive}>
<span class="text-base font-semibold text-neutral-500">
Unknown
Unknown sender
</span>
</Match>
<Match when={!props.positive}>
<span class="text-base font-semibold text-neutral-500">
Unknown receiver
</span>
</Match>
</Switch>
@@ -164,11 +205,18 @@ export function ActivityItem(props: {
</Switch>
</div>
<div class="">
<ActivityAmount
amount={props.amount.toString()}
price={state.price}
positive={props.positive}
/>
<Switch>
<Match when={props.kind === "ChannelClose"}>
<div />
</Match>
<Match when={true}>
<ActivityAmount
amount={props.amount.toString()}
price={state.price}
positive={props.positive}
/>
</Match>{" "}
</Switch>
</div>
</div>
);

View File

@@ -11,22 +11,29 @@ import {
createResource
} from "solid-js";
import { Hr, ModalCloseButton, TinyButton, VStack } from "~/components/layout";
import { MutinyInvoice } from "@mutinywallet/mutiny-wasm";
import {
ChannelClosure,
MutinyChannel,
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 shuffle from "~/assets/icons/shuffle-black.svg";
import { ActivityAmount, HackActivityType } from "./ActivityItem";
import { CopyButton, TruncateMiddle } from "./ShareCard";
import { prettyPrintTime } from "~/utils/prettyPrintTime";
import { useMegaStore } from "~/state/megaStore";
import { tagToMutinyTag } from "~/utils/tags";
import { MutinyTagItem, tagToMutinyTag } from "~/utils/tags";
import { useCopy } from "~/utils/useCopy";
import mempoolTxUrl from "~/utils/mempoolTxUrl";
import { Network } from "~/logic/mutinyWalletSetup";
import { AmountSmall } from "./Amount";
import { ExternalLink } from "./layout/ExternalLink";
import { InfoBox } from "./InfoBox";
export const OVERLAY = "fixed inset-0 z-50 bg-black/50 backdrop-blur-sm";
export const DIALOG_POSITIONER =
@@ -34,24 +41,12 @@ export const DIALOG_POSITIONER =
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";
function LightningHeader(props: { info: MutinyInvoice }) {
function LightningHeader(props: {
info: MutinyInvoice;
tags: MutinyTagItem[];
}) {
const [state, _actions] = useMegaStore();
const tags = createMemo(() => {
if (props.info.labels.length) {
const 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">
@@ -66,7 +61,7 @@ function LightningHeader(props: { info: MutinyInvoice }) {
price={state.price}
positive={props.info.inbound}
/>
<For each={tags()}>
<For each={props.tags}>
{(tag) => (
<TinyButton
tag={tag}
@@ -82,24 +77,13 @@ function LightningHeader(props: { info: MutinyInvoice }) {
);
}
function OnchainHeader(props: { info: OnChainTx }) {
function OnchainHeader(props: {
info: OnChainTx;
tags: MutinyTagItem[];
kind?: HackActivityType;
}) {
const [state, _actions] = useMegaStore();
const tags = createMemo(() => {
if (props.info.labels.length) {
const 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;
};
@@ -115,18 +99,38 @@ function OnchainHeader(props: { info: OnChainTx }) {
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" />
<Switch>
<Match
when={
props.kind === "ChannelOpen" ||
props.kind === "ChannelClose"
}
>
<img src={shuffle} alt="swap" class="w-8 h-8" />
</Match>
<Match when={true}>
<img src={chain} alt="blockchain" class="w-8 h-8" />
</Match>
</Switch>
</div>
<h1 class="uppercase font-semibold">
{isSend() ? "On-chain send" : "On-chain receive"}
{props.kind === "ChannelOpen"
? "Channel Open"
: props.kind === "ChannelClose"
? "Channel Close"
: isSend()
? "On-chain send"
: "On-chain receive"}
</h1>
<ActivityAmount
center
amount={amount() ?? "0"}
price={state.price}
positive={!isSend()}
/>
<For each={tags()}>
<Show when={props.kind !== "ChannelClose"}>
<ActivityAmount
center
amount={amount() ?? "0"}
price={state.price}
positive={!isSend()}
/>
</Show>
<For each={props.tags}>
{(tag) => (
<TinyButton
tag={tag}
@@ -211,7 +215,7 @@ function LightningDetails(props: { info: MutinyInvoice }) {
);
}
function OnchainDetails(props: { info: OnChainTx }) {
function OnchainDetails(props: { info: OnChainTx; kind?: HackActivityType }) {
const [state, _actions] = useMegaStore();
const confirmationTime = () => {
@@ -220,9 +224,30 @@ function OnchainDetails(props: { info: OnChainTx }) {
const network = state.mutiny_wallet?.get_network() as Network;
// Can return nothing if the channel is already closed
const [channelInfo] = createResource(async () => {
if (props.kind === "ChannelOpen") {
try {
const channels =
await (state.mutiny_wallet?.list_channels() as Promise<
MutinyChannel[]
>);
const channel = channels.find((channel) =>
channel.outpoint?.startsWith(props.info.txid)
);
console.log(channel);
return channel;
} catch (e) {
console.error(e);
}
} else {
return undefined;
}
});
return (
<VStack>
{/* <pre>{JSON.stringify(props.info, null, 2)}</pre> */}
{/* <pre>{JSON.stringify(channelInfo() || "", null, 2)}</pre> */}
<ul class="flex flex-col gap-4">
<KeyValue key="Status">
<span class="text-neutral-300">
@@ -248,15 +273,70 @@ function OnchainDetails(props: { info: OnChainTx }) {
<KeyValue key="Txid">
<MiniStringShower text={props.info.txid ?? ""} />
</KeyValue>
<Switch>
<Match when={props.kind === "ChannelOpen" && channelInfo()}>
<KeyValue key="Balance">
<span class="text-neutral-300">
<AmountSmall
amountSats={channelInfo()?.balance}
/>
</span>
</KeyValue>
<KeyValue key="Reserve">
<span class="text-neutral-300">
<AmountSmall
amountSats={channelInfo()?.reserve}
/>
</span>
</KeyValue>
<KeyValue key="Peer">
<span class="text-neutral-300">
<MiniStringShower
text={channelInfo()?.peer ?? ""}
/>
</span>
</KeyValue>
</Match>
<Match when={props.kind === "ChannelOpen"}>
<InfoBox accent="blue">
No channel details found, which means this channel
has likely been closed.
</InfoBox>
</Match>
</Switch>
</ul>
<div class="text-center">
<ExternalLink href={mempoolTxUrl(props.info.txid, network)}>
View Transaction
</ExternalLink>
</div>
</VStack>
);
}
function ChannelCloseDetails(props: { info: ChannelClosure }) {
return (
<VStack>
{/* <pre>{JSON.stringify(props.info.value, null, 2)}</pre> */}
<ul class="flex flex-col gap-4">
<KeyValue key="Channel ID">
<MiniStringShower text={props.info.channel_id ?? ""} />
</KeyValue>
<Show when={props.info.timestamp}>
<KeyValue key="When">
<span class="text-neutral-300">
{props.info.timestamp
? prettyPrintTime(Number(props.info.timestamp))
: "Pending"}
</span>
</KeyValue>
</Show>
<KeyValue key="Reason">
<p class="text-neutral-300 text-right">
{props.info.reason ?? ""}
</p>
</KeyValue>
</ul>
<a
class="uppercase font-light text-center"
href={mempoolTxUrl(props.info.txid, network)}
target="_blank"
rel="noreferrer"
>
Mempool.space
</a>
</VStack>
);
}
@@ -280,13 +360,34 @@ export function DetailsIdModal(props: {
id()
);
return invoice;
} else if (kind() === "ChannelClose") {
console.log("reading channel close: ", id());
const closeItem = await state.mutiny_wallet?.get_channel_closure(
id()
);
return closeItem;
} else {
console.log("reading tx: ", id());
const tx = await state.mutiny_wallet?.get_transaction(id());
return tx;
}
});
const tags = createMemo(() => {
if (data() && data().labels && data().labels.length > 0) {
const contact = state.mutiny_wallet?.get_contact(data().labels[0]);
if (contact) {
return [tagToMutinyTag(contact)];
} else {
return [];
}
} else {
return [];
}
});
createEffect(() => {
if (props.id && props.kind && props.open) {
refetch();
@@ -295,10 +396,6 @@ export function DetailsIdModal(props: {
const json = createMemo(() => JSON.stringify(data() || "", null, 2));
const isInvoice = () => {
return props.kind === "Lightning";
};
return (
<Dialog.Root open={props.open} onOpenChange={props.setOpen}>
<Dialog.Portal>
@@ -314,14 +411,23 @@ export function DetailsIdModal(props: {
</div>
<Dialog.Title>
<Switch>
<Match when={isInvoice()}>
<Match when={props.kind === "Lightning"}>
<LightningHeader
info={data() as MutinyInvoice}
tags={tags()}
/>
</Match>
<Match when={true}>
<Match
when={
props.kind === "OnChain" ||
props.kind === "ChannelOpen" ||
props.kind === "ChannelClose"
}
>
<OnchainHeader
info={data() as OnChainTx}
tags={tags()}
kind={props.kind}
/>
</Match>
</Switch>
@@ -329,20 +435,36 @@ export function DetailsIdModal(props: {
<Hr />
<Dialog.Description class="flex flex-col gap-4">
<Switch>
<Match when={isInvoice()}>
<Match when={props.kind === "Lightning"}>
<LightningDetails
info={data() as MutinyInvoice}
/>
</Match>
<Match when={true}>
<Match
when={
props.kind === "OnChain" ||
props.kind === "ChannelOpen"
}
>
<OnchainDetails
info={data() as OnChainTx}
kind={props.kind}
/>
</Match>
<Match when={props.kind === "ChannelClose"}>
<ChannelCloseDetails
info={data() as ChannelClosure}
/>
</Match>
</Switch>
<div class="flex justify-center">
<CopyButton title="Copy" text={json()} />
</div>
<Show when={props.kind !== "ChannelClose"}>
<div class="flex justify-center">
<CopyButton
title="Copy"
text={json()}
/>
</div>
</Show>
</Dialog.Description>
</Suspense>
</Dialog.Content>