mirror of
https://github.com/aljazceru/mutiny-web.git
synced 2026-02-23 15:14:19 +01:00
lots of tagging works but not sends rn
This commit is contained in:
@@ -1,16 +1,13 @@
|
||||
import send from '~/assets/icons/send.svg';
|
||||
import receive from '~/assets/icons/receive.svg';
|
||||
import { ButtonLink, Card, LoadingSpinner, NiceP, SmallAmount, SmallHeader, VStack } from './layout';
|
||||
import { For, Match, ParentComponent, Show, Suspense, Switch, createMemo, createResource, createSignal } from 'solid-js';
|
||||
import { LoadingSpinner, NiceP, SmallAmount, SmallHeader } from './layout';
|
||||
import { For, Match, ParentComponent, Show, Switch, createMemo, createResource, createSignal } from 'solid-js';
|
||||
import { useMegaStore } from '~/state/megaStore';
|
||||
import { MutinyInvoice } from '@mutinywallet/mutiny-wasm';
|
||||
import { prettyPrintTime } from '~/utils/prettyPrintTime';
|
||||
import { JsonModal } from '~/components/JsonModal';
|
||||
import mempoolTxUrl from '~/utils/mempoolTxUrl';
|
||||
import wave from "~/assets/wave.gif"
|
||||
import utxoIcon from '~/assets/icons/coin.svg';
|
||||
import { getRedshifted } from '~/utils/fakeLabels';
|
||||
import { ActivityItem } from './ActivityItem';
|
||||
import { MutinyTagItem } from '~/utils/tags';
|
||||
|
||||
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'
|
||||
@@ -40,14 +37,14 @@ export type UtxoItem = {
|
||||
}
|
||||
keychain: string
|
||||
is_spent: boolean,
|
||||
redshifted?: boolean
|
||||
redshifted?: boolean,
|
||||
}
|
||||
|
||||
const SubtleText: ParentComponent = (props) => {
|
||||
return <h3 class='text-xs text-gray-500 uppercase'>{props.children}</h3>
|
||||
}
|
||||
|
||||
function OnChainItem(props: { item: OnChainTx, labels: string[] }) {
|
||||
function OnChainItem(props: { item: OnChainTx, labels: MutinyTagItem[] }) {
|
||||
const [store, actions] = useMegaStore();
|
||||
const isReceive = createMemo(() => props.item.received > 0);
|
||||
|
||||
@@ -69,26 +66,11 @@ function OnChainItem(props: { item: OnChainTx, labels: string[] }) {
|
||||
positive={isReceive()}
|
||||
onClick={() => setOpen(!open())}
|
||||
/>
|
||||
{/* <div class={THREE_COLUMNS} onClick={() => setOpen(!open())}>
|
||||
<div class="flex items-center">
|
||||
{isReceive() ? <img src={receive} alt="receive arrow" /> : <img src={send} alt="send arrow" />}
|
||||
</div>
|
||||
<div class={CENTER_COLUMN}>
|
||||
<h2 class={MISSING_LABEL}>Unknown</h2>
|
||||
{isReceive() ? <SmallAmount amount={props.item.received} /> : <SmallAmount amount={props.item.sent} />}
|
||||
</div>
|
||||
<div class={RIGHT_COLUMN}>
|
||||
<SmallHeader>
|
||||
<span class="text-neutral-500">On-chain</span> {isReceive() ? <span class="text-m-green">Receive</span> : <span class="text-m-red">Send</span>}
|
||||
</SmallHeader>
|
||||
<SubtleText>{props.item.confirmation_time?.Confirmed ? prettyPrintTime(props.item.confirmation_time?.Confirmed?.time) : "Unconfirmed"}</SubtleText>
|
||||
</div>
|
||||
</div> */}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function InvoiceItem(props: { item: MutinyInvoice, labels: string[] }) {
|
||||
function InvoiceItem(props: { item: MutinyInvoice, labels: MutinyTagItem[] }) {
|
||||
const [store, actions] = useMegaStore();
|
||||
const isSend = createMemo(() => props.item.is_send);
|
||||
|
||||
@@ -98,21 +80,6 @@ function InvoiceItem(props: { item: MutinyInvoice, labels: string[] }) {
|
||||
<>
|
||||
<JsonModal open={open()} data={props.item} title="Lightning Transaction" setOpen={setOpen} />
|
||||
<ActivityItem kind={"lightning"} labels={props.labels} amount={props.item.amount_sats || 0n} date={props.item.last_updated} positive={!isSend()} onClick={() => setOpen(!open())} />
|
||||
{/* <div class={THREE_COLUMNS} onClick={() => setOpen(!open())}>
|
||||
<div class="flex items-center">
|
||||
{isSend() ? <img src={send} alt="send arrow" /> : <img src={receive} alt="receive arrow" />}
|
||||
</div>
|
||||
<div class={CENTER_COLUMN}>
|
||||
<h2 class={MISSING_LABEL}>Unknown</h2>
|
||||
<SmallAmount amount={props.item.amount_sats || 0} />
|
||||
</div>
|
||||
<div class={RIGHT_COLUMN}>
|
||||
<SmallHeader>
|
||||
<span class="text-neutral-500">Lightning</span> {!isSend() ? <span class="text-m-green">Receive</span> : <span class="text-m-red">Send</span>}
|
||||
</SmallHeader>
|
||||
<SubtleText>{prettyPrintTime(Number(props.item.expire))}</SubtleText>
|
||||
</div>
|
||||
</div > */}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -149,57 +116,49 @@ function Utxo(props: { item: UtxoItem }) {
|
||||
)
|
||||
}
|
||||
|
||||
type ActivityItem = { type: "onchain" | "lightning", item: OnChainTx | MutinyInvoice, time: number }
|
||||
type ActivityItem = { type: "onchain" | "lightning", item: OnChainTx | MutinyInvoice, time: number, labels: MutinyTagItem[] }
|
||||
|
||||
function sortByTime(a: ActivityItem, b: ActivityItem) {
|
||||
return b.time - a.time;
|
||||
}
|
||||
|
||||
export function CombinedActivity(props: { limit?: number }) {
|
||||
|
||||
const [state, _] = 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();
|
||||
|
||||
const activity: ActivityItem[] = [];
|
||||
let activity: ActivityItem[] = [];
|
||||
|
||||
txs.forEach((tx) => {
|
||||
activity.push({ type: "onchain", item: tx, time: tx.confirmation_time?.Confirmed?.time || Date.now() })
|
||||
})
|
||||
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: [] })
|
||||
}
|
||||
|
||||
invoices.forEach((invoice) => {
|
||||
if (invoice.paid) {
|
||||
activity.push({ type: "lightning", item: invoice, time: Number(invoice.expire) })
|
||||
for (let i = 0; i < invoices.length; i++) {
|
||||
if (invoices[i].paid) {
|
||||
activity.push({ type: "lightning", item: invoices[i], time: Number(invoices[i].expire), labels: [] })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (props.limit) {
|
||||
return activity.sort(sortByTime).slice(0, props.limit);
|
||||
activity = activity.sort(sortByTime).slice(0, props.limit);
|
||||
} else {
|
||||
return activity.sort(sortByTime);
|
||||
activity.sort(sortByTime);
|
||||
}
|
||||
|
||||
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] = createResource(getAllActivity);
|
||||
|
||||
// const addressLabels = createMemo(() => {
|
||||
// const labels = state.mutiny_wallet?.get_address_labels();
|
||||
// console.log(labels);
|
||||
// return labels || [];
|
||||
// // return labels.filter((label) => label.address === props.item.txid)
|
||||
// })
|
||||
|
||||
// const invoiceLabels = createMemo(() => {
|
||||
// const labels = state.mutiny_wallet?.get_address_labels();
|
||||
// console.log(labels);
|
||||
// if (!labels) return ["abcdefg"];
|
||||
// return labels;
|
||||
// // return labels.filter((label) => label.address === props.item.txid)
|
||||
// })
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
<Match when={activity.loading}>
|
||||
@@ -214,11 +173,11 @@ export function CombinedActivity(props: { limit?: number }) {
|
||||
<Switch>
|
||||
<Match when={activityItem.type === "onchain"}>
|
||||
{/* FIXME */}
|
||||
<OnChainItem item={activityItem.item as OnChainTx} labels={activityItem.item.labels} />
|
||||
<OnChainItem item={activityItem.item as OnChainTx} labels={activityItem.labels} />
|
||||
</Match>
|
||||
<Match when={activityItem.type === "lightning"}>
|
||||
{/* FIXME */}
|
||||
<InvoiceItem item={activityItem.item as MutinyInvoice} labels={activityItem.item.labels} />
|
||||
<InvoiceItem item={activityItem.item as MutinyInvoice} labels={activityItem.labels} />
|
||||
</Match>
|
||||
</Switch>
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { ParentComponent, createMemo } from "solid-js";
|
||||
import { ParentComponent, createMemo, createResource } from "solid-js";
|
||||
import { InlineAmount } from "./AmountCard";
|
||||
import { satsToUsd } from "~/utils/conversions";
|
||||
import bolt from "~/assets/icons/bolt.svg"
|
||||
import chain from "~/assets/icons/chain.svg"
|
||||
import { timeAgo } from "~/utils/prettyPrintTime";
|
||||
import { MutinyTagItem } from "~/utils/tags";
|
||||
import { generateGradient } from "~/utils/gradientHash";
|
||||
|
||||
export const ActivityAmount: ParentComponent<{ amount: string, price: number, positive?: boolean }> = (props) => {
|
||||
const amountInUsd = createMemo(() => {
|
||||
@@ -35,17 +37,43 @@ export const ActivityAmount: ParentComponent<{ amount: string, price: number, po
|
||||
)
|
||||
}
|
||||
|
||||
function LabelCircle(props: { name: string }) {
|
||||
function LabelCircle(props: { name?: string, contact: boolean }) {
|
||||
|
||||
// TODO: don't need to run this if it's not a contact
|
||||
const [gradient] = createResource(props.name, async (name: string) => {
|
||||
return generateGradient(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: "gray" }}
|
||||
style={{ background: bg() }}
|
||||
>
|
||||
{props.name[0] || "?"}
|
||||
{text()}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function ActivityItem(props: { kind: "lightning" | "onchain", labels: string[], amount: number | bigint, date?: number | bigint, positive?: boolean, onClick?: () => void }) {
|
||||
// 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)
|
||||
return (
|
||||
<div
|
||||
onClick={() => props.onClick && props.onClick()}
|
||||
@@ -54,14 +82,14 @@ export function ActivityItem(props: { kind: "lightning" | "onchain", labels: str
|
||||
>
|
||||
<div class="flex gap-2 md:gap-4 items-center">
|
||||
<div class="">
|
||||
{props.kind === "lightning" ? <img src={bolt} alt="lightning" /> : <img src={chain} alt="onchain" />}
|
||||
{props.kind === "lightning" ? <img class="w-[1rem]" src={bolt} alt="lightning" /> : <img class="w-[1rem]" src={chain} alt="onchain" />}
|
||||
</div>
|
||||
<div class="">
|
||||
<LabelCircle name={props.labels.length ? props.labels[0] : "?"} />
|
||||
<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": props.labels.length === 0 }}>{props.labels.length ? props.labels[0] : "Unknown"}</span>
|
||||
<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="">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import logo from '~/assets/icons/mutiny-logo.svg';
|
||||
import { DefaultMain, SafeArea, VStack, Card, FullscreenLoader } from "~/components/layout";
|
||||
import BalanceBox from "~/components/BalanceBox";
|
||||
import { DefaultMain, SafeArea, VStack, Card, LoadingSpinner } from "~/components/layout";
|
||||
import BalanceBox, { LoadingShimmer } from "~/components/BalanceBox";
|
||||
import NavBar from "~/components/NavBar";
|
||||
import ReloadPrompt from "~/components/Reload";
|
||||
import { A } from 'solid-start';
|
||||
@@ -28,7 +28,7 @@ export default function App() {
|
||||
<Card title="Activity">
|
||||
<div class="p-1" />
|
||||
<VStack>
|
||||
<Show when={!state.wallet_loading} fallback={<FullscreenLoader />}>
|
||||
<Show when={!state.wallet_loading} fallback={<LoadingShimmer />}>
|
||||
<CombinedActivity limit={3} />
|
||||
</Show>
|
||||
{/* <ButtonLink href="/activity">View All</ButtonLink> */}
|
||||
|
||||
@@ -11,7 +11,7 @@ function prettyPrintAmount(n?: number | bigint): string {
|
||||
return n.toLocaleString()
|
||||
}
|
||||
|
||||
function LoadingShimmer() {
|
||||
export function LoadingShimmer() {
|
||||
return (<div class="flex flex-col gap-2 animate-pulse">
|
||||
<h1 class="text-4xl font-light">
|
||||
<div class="w-[12rem] rounded bg-neutral-700 h-[2.5rem]"></div>
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
import { RadioGroup as Kobalte } from '@kobalte/core';
|
||||
import { type JSX, Show, splitProps, For } from 'solid-js';
|
||||
|
||||
type RadioGroupProps = {
|
||||
name: string;
|
||||
label?: string | undefined;
|
||||
options: { label: string; value: string }[];
|
||||
value: string | undefined;
|
||||
error: string;
|
||||
required?: boolean | undefined;
|
||||
disabled?: boolean | undefined;
|
||||
ref: (element: HTMLInputElement | HTMLTextAreaElement) => void;
|
||||
onInput: JSX.EventHandler<HTMLInputElement | HTMLTextAreaElement, InputEvent>;
|
||||
onChange: JSX.EventHandler<HTMLInputElement | HTMLTextAreaElement, Event>;
|
||||
onBlur: JSX.EventHandler<HTMLInputElement | HTMLTextAreaElement, FocusEvent>;
|
||||
};
|
||||
type Color = "blue" | "green" | "red" | "gray"
|
||||
|
||||
export const colorVariants = {
|
||||
blue: "bg-m-blue",
|
||||
green: "bg-m-green",
|
||||
red: "bg-m-red",
|
||||
gray: "bg-[#898989]",
|
||||
}
|
||||
|
||||
export function ColorRadioGroup(props: RadioGroupProps) {
|
||||
const [rootProps, inputProps] = splitProps(
|
||||
props,
|
||||
['name', 'value', 'required', 'disabled'],
|
||||
['ref', 'onInput', 'onChange', 'onBlur']
|
||||
);
|
||||
return (
|
||||
<Kobalte.Root
|
||||
{...rootProps}
|
||||
validationState={props.error ? 'invalid' : 'valid'}
|
||||
class="flex flex-col gap-2"
|
||||
>
|
||||
<Show when={props.label}>
|
||||
<Kobalte.Label class="text-sm uppercase font-semibold">
|
||||
{props.label}
|
||||
</Kobalte.Label>
|
||||
</Show>
|
||||
<div class="flex gap-2">
|
||||
<For each={props.options}>
|
||||
{(option) => (
|
||||
<Kobalte.Item value={option.value} class="ui-checked:bg-neutral-950 rounded outline outline-black/50 ui-checked:outline-white ui-checked:outline-2">
|
||||
<Kobalte.ItemInput {...inputProps} />
|
||||
<Kobalte.ItemControl class={`${colorVariants[option.value as Color]} w-8 h-8 rounded`}>
|
||||
<Kobalte.ItemIndicator />
|
||||
</Kobalte.ItemControl>
|
||||
{/* <Kobalte.ItemLabel>{option.label}</Kobalte.ItemLabel> */}
|
||||
</Kobalte.Item>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
<Kobalte.ErrorMessage>{props.error}</Kobalte.ErrorMessage>
|
||||
</Kobalte.Root>
|
||||
);
|
||||
}
|
||||
@@ -1,24 +1,17 @@
|
||||
import { Match, Switch, createSignal, createUniqueId } from 'solid-js';
|
||||
import { Match, Switch, createSignal } from 'solid-js';
|
||||
import { SmallHeader, TinyButton } from '~/components/layout';
|
||||
import { Dialog } from '@kobalte/core';
|
||||
import close from "~/assets/icons/close.svg";
|
||||
import { SubmitHandler } from '@modular-forms/solid';
|
||||
import { ContactItem } from '~/state/contacts';
|
||||
import { ContactForm } from './ContactForm';
|
||||
import { ContactFormValues } from './ContactViewer';
|
||||
|
||||
const INITIAL: ContactItem = { id: createUniqueId(), kind: "contact", name: "", color: "gray" }
|
||||
|
||||
export function ContactEditor(props: { createContact: (contact: ContactItem) => void, list?: boolean }) {
|
||||
export function ContactEditor(props: { createContact: (contact: ContactFormValues) => void, list?: boolean }) {
|
||||
const [isOpen, setIsOpen] = createSignal(false);
|
||||
|
||||
// What we're all here for in the first place: returning a value
|
||||
const handleSubmit: SubmitHandler<ContactItem> = (c: ContactItem) => {
|
||||
// TODO: why do the id and color disappear?
|
||||
|
||||
const odd = { id: createUniqueId(), kind: "contact" }
|
||||
|
||||
props.createContact({ ...odd, ...c })
|
||||
|
||||
const handleSubmit: SubmitHandler<ContactFormValues> = (c: ContactFormValues) => {
|
||||
props.createContact(c)
|
||||
setIsOpen(false);
|
||||
}
|
||||
|
||||
@@ -50,7 +43,7 @@ export function ContactEditor(props: { createContact: (contact: ContactItem) =>
|
||||
<img src={close} alt="Close" />
|
||||
</button>
|
||||
</div>
|
||||
<ContactForm title="New contact" cta="Create contact" handleSubmit={handleSubmit} initialValues={INITIAL} />
|
||||
<ContactForm title="New contact" cta="Create contact" handleSubmit={handleSubmit} />
|
||||
</Dialog.Content>
|
||||
</div>
|
||||
</Dialog.Portal>
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import { SubmitHandler, createForm, required } from "@modular-forms/solid";
|
||||
import { ContactItem } from "~/state/contacts";
|
||||
import { Button, LargeHeader, VStack } from "~/components/layout";
|
||||
import { TextField } from "~/components/layout/TextField";
|
||||
import { ColorRadioGroup } from "~/components/ColorRadioGroup";
|
||||
import { ContactFormValues } from "./ContactViewer";
|
||||
|
||||
const colorOptions = [{ label: "blue", value: "blue" }, { label: "green", value: "green" }, { label: "red", value: "red" }, { label: "gray", value: "gray" }]
|
||||
|
||||
export function ContactForm(props: { handleSubmit: SubmitHandler<ContactItem>, initialValues?: ContactItem, title: string, cta: string }) {
|
||||
const [_contactForm, { Form, Field }] = createForm<ContactItem>({ initialValues: props.initialValues });
|
||||
export function ContactForm(props: { handleSubmit: SubmitHandler<ContactFormValues>, initialValues?: ContactFormValues, title: string, cta: string }) {
|
||||
const [_contactForm, { Form, Field }] = createForm<ContactFormValues>({ initialValues: props.initialValues });
|
||||
|
||||
return (
|
||||
<Form onSubmit={props.handleSubmit} class="flex flex-col flex-1 justify-around gap-4 max-w-[400px] mx-auto w-full">
|
||||
@@ -24,11 +21,6 @@ export function ContactForm(props: { handleSubmit: SubmitHandler<ContactItem>, i
|
||||
<TextField {...props} placeholder='npub...' value={field.value} error={field.error} label="Nostr npub or NIP-05 (optional)" />
|
||||
)}
|
||||
</Field>
|
||||
<Field name="color">
|
||||
{(field, props) => (
|
||||
<ColorRadioGroup options={colorOptions} {...props} value={field.value} error={field.error} label="Color" />
|
||||
)}
|
||||
</Field>
|
||||
</VStack>
|
||||
</div>
|
||||
<Button type="submit" intent="blue" class="w-full flex-none">
|
||||
|
||||
@@ -3,16 +3,23 @@ import { Button, Card, NiceP, SmallHeader } from '~/components/layout';
|
||||
import { Dialog } from '@kobalte/core';
|
||||
import close from "~/assets/icons/close.svg";
|
||||
import { SubmitHandler } from '@modular-forms/solid';
|
||||
import { ContactItem } from '~/state/contacts';
|
||||
import { ContactForm } from './ContactForm';
|
||||
import { showToast } from './Toaster';
|
||||
import { Contact } from '@mutinywallet/mutiny-wasm';
|
||||
|
||||
export function ContactViewer(props: { contact: ContactItem, gradient: string, saveContact: (contact: ContactItem) => void }) {
|
||||
export type ContactFormValues = {
|
||||
name: string,
|
||||
npub?: string,
|
||||
}
|
||||
|
||||
export function ContactViewer(props: { contact: Contact, gradient: string, saveContact: (contact: Contact) => void }) {
|
||||
const [isOpen, setIsOpen] = createSignal(false);
|
||||
const [isEditing, setIsEditing] = createSignal(false);
|
||||
|
||||
const handleSubmit: SubmitHandler<ContactItem> = (c: ContactItem) => {
|
||||
props.saveContact({ ...props.contact, ...c })
|
||||
const handleSubmit: SubmitHandler<ContactFormValues> = (c: ContactFormValues) => {
|
||||
// FIXME: merge with existing contact if saving (need edit contact method)
|
||||
const contact = new Contact(c.name, c.npub ?? undefined, undefined, undefined)
|
||||
props.saveContact(contact)
|
||||
setIsEditing(false)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { Select, createOptions } from "@thisbeyond/solid-select";
|
||||
import "~/styles/solid-select.css"
|
||||
import { For, createUniqueId } from "solid-js";
|
||||
import { For } from "solid-js";
|
||||
import { ContactEditor } from "./ContactEditor";
|
||||
import { ContactItem, TagItem, TextItem, addContact } from "~/state/contacts";
|
||||
import { TinyButton } from "./layout";
|
||||
import { ContactFormValues } from "./ContactViewer";
|
||||
import { MutinyTagItem } from "~/utils/tags";
|
||||
import { Contact } from "@mutinywallet/mutiny-wasm";
|
||||
import { useMegaStore } from "~/state/megaStore";
|
||||
|
||||
// take two arrays, subtract the second from the first, then return the first
|
||||
function subtract<T>(a: T[], b: T[]) {
|
||||
@@ -11,12 +14,20 @@ function subtract<T>(a: T[], b: T[]) {
|
||||
return a.filter(x => !set.has(x));
|
||||
}
|
||||
|
||||
const createValue = (name: string): TextItem => {
|
||||
return { id: createUniqueId(), name, kind: "text" };
|
||||
const createLabelValue = (label: string): Partial<MutinyTagItem> => {
|
||||
return { id: label, name: label, kind: "Label" };
|
||||
};
|
||||
|
||||
export function TagEditor(props: { values: TagItem[], setValues: (values: TagItem[]) => void, selectedValues: TagItem[], setSelectedValues: (values: TagItem[]) => void, placeholder: string }) {
|
||||
const onChange = (selected: TagItem[]) => {
|
||||
export function TagEditor(props: {
|
||||
values: MutinyTagItem[],
|
||||
setValues: (values: MutinyTagItem[]) => void,
|
||||
selectedValues: MutinyTagItem[],
|
||||
setSelectedValues: (values: MutinyTagItem[]) => void,
|
||||
placeholder: string
|
||||
}) {
|
||||
const [state, actions] = useMegaStore();
|
||||
|
||||
const onChange = (selected: MutinyTagItem[]) => {
|
||||
props.setSelectedValues(selected);
|
||||
|
||||
console.log(selected)
|
||||
@@ -31,12 +42,22 @@ export function TagEditor(props: { values: TagItem[], setValues: (values: TagIte
|
||||
key: "name",
|
||||
disable: (value) => props.selectedValues.includes(value),
|
||||
filterable: true, // Default
|
||||
createable: createValue,
|
||||
createable: createLabelValue,
|
||||
});
|
||||
|
||||
const newContact = async (contact: ContactItem) => {
|
||||
await addContact(contact)
|
||||
onChange([...props.selectedValues, contact])
|
||||
async function createContact(contact: ContactFormValues) {
|
||||
// FIXME: undefineds
|
||||
const c = new Contact(contact.name, undefined, undefined, undefined);
|
||||
const newContactId = await state.mutiny_wallet?.create_new_contact(c);
|
||||
const contactItem = await state.mutiny_wallet?.get_contact(newContactId ?? "");
|
||||
const mutinyContactItem: MutinyTagItem = { id: contactItem?.id || "", name: contactItem?.name || "", kind: "Contact", last_used_time: 0n };
|
||||
if (contactItem) {
|
||||
// @ts-ignore
|
||||
// FIXME: make typescript less mad about this
|
||||
onChange([...props.selectedValues, mutinyContactItem])
|
||||
} else {
|
||||
console.error("Failed to create contact")
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -52,13 +73,13 @@ export function TagEditor(props: { values: TagItem[], setValues: (values: TagIte
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
<For each={subtract(props.values, props.selectedValues).slice(0, 3)}>
|
||||
{(tag) => (
|
||||
<TinyButton onClick={() => onChange([...props.selectedValues, tag])}
|
||||
<TinyButton tag={tag} onClick={() => onChange([...props.selectedValues, tag])}
|
||||
>
|
||||
{tag.name}
|
||||
</TinyButton>
|
||||
)}
|
||||
</For>
|
||||
<ContactEditor createContact={newContact} />
|
||||
<ContactEditor createContact={createContact} />
|
||||
</div>
|
||||
</div >
|
||||
)
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { JSX, ParentComponent, Show, Suspense, createSignal } from "solid-js"
|
||||
import { JSX, ParentComponent, Show, Suspense, createResource, createSignal } from "solid-js"
|
||||
import Linkify from "./Linkify"
|
||||
import { Button, ButtonLink } from "./Button"
|
||||
import { Checkbox as KCheckbox, Separator } from "@kobalte/core"
|
||||
import { useMegaStore } from "~/state/megaStore"
|
||||
import check from "~/assets/icons/check.svg"
|
||||
import { MutinyTagItem } from "~/utils/tags"
|
||||
import { generateGradient } from "~/utils/gradientHash"
|
||||
|
||||
export {
|
||||
Button,
|
||||
@@ -122,9 +124,20 @@ export const NiceP: ParentComponent = (props) => {
|
||||
return (<p class="text-xl font-light">{props.children}</p>)
|
||||
}
|
||||
|
||||
export const TinyButton: ParentComponent<{ onClick: () => void }> = (props) => {
|
||||
export const TinyButton: ParentComponent<{ onClick: () => void, tag?: MutinyTagItem }> = (props) => {
|
||||
// TODO: don't need to run this if it's not a contact
|
||||
const [gradient] = createResource(props.tag?.name, async (name: string) => {
|
||||
return generateGradient(name || "?")
|
||||
})
|
||||
|
||||
const bg = () => (props.tag?.name && props.tag?.kind === "Contact") ? gradient() : "rgb(255 255 255 / 0.1)"
|
||||
|
||||
console.log("tiny tag", props.tag?.name, gradient())
|
||||
|
||||
return (
|
||||
<button class="py-1 px-2 rounded-lg bg-white/10" onClick={() => props.onClick()}>
|
||||
<button class="py-1 px-2 rounded-lg bg-white/10" onClick={() => props.onClick()}
|
||||
style={{ background: bg() }}
|
||||
>
|
||||
{props.children}
|
||||
</button>
|
||||
)
|
||||
|
||||
@@ -5,23 +5,40 @@ import { BackLink } from "~/components/layout/BackLink";
|
||||
import { CombinedActivity } from "~/components/Activity";
|
||||
import { A } from "solid-start";
|
||||
import settings from '~/assets/icons/settings.svg';
|
||||
import { ContactItem, addContact, editContact, listContacts } from "~/state/contacts";
|
||||
import { Tabs } from "@kobalte/core";
|
||||
import { gradientsPerContact } from "~/utils/gradientHash";
|
||||
import { ContactEditor } from "~/components/ContactEditor";
|
||||
import { ContactViewer } from "~/components/ContactViewer";
|
||||
import { ContactFormValues, ContactViewer } from "~/components/ContactViewer";
|
||||
import { useMegaStore } from "~/state/megaStore";
|
||||
import { Contact } from "@mutinywallet/mutiny-wasm";
|
||||
import { showToast } from "~/components/Toaster";
|
||||
|
||||
function ContactRow() {
|
||||
const [contacts, { refetch }] = createResource(listContacts)
|
||||
const [state, actions] = useMegaStore();
|
||||
const [contacts, { refetch }] = createResource(async () => {
|
||||
const contacts = state.mutiny_wallet?.get_contacts();
|
||||
console.log(contacts)
|
||||
|
||||
let c: Contact[] = []
|
||||
if (contacts) {
|
||||
for (let contact in contacts) {
|
||||
c.push(contacts[contact])
|
||||
}
|
||||
}
|
||||
return c || []
|
||||
})
|
||||
const [gradients] = createResource(contacts, gradientsPerContact);
|
||||
|
||||
async function createContact(contact: ContactItem) {
|
||||
await addContact(contact)
|
||||
async function createContact(contact: ContactFormValues) {
|
||||
const c = new Contact(contact.name, contact.npub ?? undefined, undefined, undefined);
|
||||
await state.mutiny_wallet?.create_new_contact(c)
|
||||
refetch();
|
||||
}
|
||||
|
||||
async function saveContact(contact: ContactItem) {
|
||||
await editContact(contact)
|
||||
//
|
||||
async function saveContact(contact: ContactFormValues) {
|
||||
showToast(new Error("Unimplemented"))
|
||||
// await editContact(contact)
|
||||
refetch();
|
||||
}
|
||||
|
||||
@@ -31,7 +48,7 @@ function ContactRow() {
|
||||
<Show when={contacts() && gradients()}>
|
||||
<For each={contacts()}>
|
||||
{(contact) => (
|
||||
<ContactViewer contact={contact} gradient={gradients()?.get(contact.id)} saveContact={saveContact} />
|
||||
<ContactViewer contact={contact} gradient={gradients()?.get(contact.name)} saveContact={saveContact} />
|
||||
)}
|
||||
</For>
|
||||
</Show>
|
||||
@@ -49,6 +66,7 @@ export default function Activity() {
|
||||
<BackLink />
|
||||
<LargeHeader action={<A class="md:hidden p-2 hover:bg-white/5 rounded-lg active:bg-m-blue" href="/settings"><img src={settings} alt="Settings" /></A>}>Activity</LargeHeader>
|
||||
<ContactRow />
|
||||
|
||||
<Tabs.Root defaultValue="mutiny">
|
||||
<Tabs.List class="relative flex justify-around mt-4 mb-8 gap-1 bg-neutral-950 p-1 rounded-xl">
|
||||
<Tabs.Trigger value="mutiny" class={TAB}>Mutiny</Tabs.Trigger>
|
||||
|
||||
@@ -14,10 +14,10 @@ import { StyledRadioGroup } from "~/components/layout/Radio";
|
||||
import { showToast } from "~/components/Toaster";
|
||||
import { useNavigate } from "solid-start";
|
||||
import megacheck from "~/assets/icons/megacheck.png";
|
||||
import { TagItem, listTags } from "~/state/contacts";
|
||||
import { AmountCard } from "~/components/AmountCard";
|
||||
import { ShareCard } from "~/components/ShareCard";
|
||||
import { BackButton } from "~/components/layout/BackButton";
|
||||
import { MutinyTagItem, UNKNOWN_TAG, sortByLastUsed, tagsToIds } from "~/utils/tags";
|
||||
|
||||
type OnChainTx = {
|
||||
transaction: {
|
||||
@@ -43,22 +43,14 @@ type OnChainTx = {
|
||||
}
|
||||
}
|
||||
|
||||
const createUniqueId = () => Math.random().toString(36).substr(2, 9);
|
||||
|
||||
const RECEIVE_FLAVORS = [{ value: "unified", label: "Unified", caption: "Sender decides" }, { value: "lightning", label: "Lightning", caption: "Fast and cool" }, { value: "onchain", label: "On-chain", caption: "Just like Satoshi did it" }]
|
||||
|
||||
type ReceiveFlavor = "unified" | "lightning" | "onchain"
|
||||
type ReceiveState = "edit" | "show" | "paid"
|
||||
type PaidState = "lightning_paid" | "onchain_paid";
|
||||
|
||||
function tagItemsToLabels(items: TagItem[]) {
|
||||
const labels = items.map(item => item.kind === "contact" ? item.id : item.name)
|
||||
console.log("Labels", labels)
|
||||
return labels;
|
||||
}
|
||||
|
||||
export default function Receive() {
|
||||
const [state, _] = useMegaStore()
|
||||
const [state, actions] = useMegaStore()
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [amount, setAmount] = createSignal("")
|
||||
@@ -68,8 +60,8 @@ export default function Receive() {
|
||||
const [shouldShowAmountEditor, setShouldShowAmountEditor] = createSignal(true)
|
||||
|
||||
// Tagging stuff
|
||||
const [selectedValues, setSelectedValues] = createSignal<TagItem[]>([]);
|
||||
const [values, setValues] = createSignal<TagItem[]>([{ id: createUniqueId(), name: "Unknown", kind: "text" }]);
|
||||
const [selectedValues, setSelectedValues] = createSignal<MutinyTagItem[]>([]);
|
||||
const [values, setValues] = createSignal<MutinyTagItem[]>([UNKNOWN_TAG]);
|
||||
|
||||
// The data we get after a payment
|
||||
const [paymentTx, setPaymentTx] = createSignal<OnChainTx>();
|
||||
@@ -92,8 +84,8 @@ export default function Receive() {
|
||||
})
|
||||
|
||||
onMount(() => {
|
||||
listTags().then((tags) => {
|
||||
setValues(prev => [...prev, ...tags || []])
|
||||
actions.listTags().then((tags) => {
|
||||
setValues(prev => [...prev, ...tags.sort(sortByLastUsed) || []])
|
||||
});
|
||||
})
|
||||
|
||||
@@ -109,11 +101,8 @@ export default function Receive() {
|
||||
|
||||
async function getUnifiedQr(amount: string) {
|
||||
const bigAmount = BigInt(amount);
|
||||
console.log(selectedValues());
|
||||
console.log(tagItemsToLabels(selectedValues()))
|
||||
try {
|
||||
// FIXME: actual labels
|
||||
const raw = await state.mutiny_wallet?.create_bip21(bigAmount, tagItemsToLabels(selectedValues()));
|
||||
const raw = await state.mutiny_wallet?.create_bip21(bigAmount, tagsToIds(selectedValues()));
|
||||
// Save the raw info so we can watch the address and invoice
|
||||
setBip21Raw(raw);
|
||||
|
||||
|
||||
@@ -17,9 +17,9 @@ import mempoolTxUrl from "~/utils/mempoolTxUrl";
|
||||
import { BackLink } from "~/components/layout/BackLink";
|
||||
import { useNavigate } from "solid-start";
|
||||
import { TagEditor } from "~/components/TagEditor";
|
||||
import { TagItem, createUniqueId, listTags } from "~/state/contacts";
|
||||
import { StringShower } from "~/components/ShareCard";
|
||||
import { AmountCard } from "~/components/AmountCard";
|
||||
import { MutinyTagItem, UNKNOWN_TAG, sortByLastUsed, tagsToIds } from "~/utils/tags";
|
||||
|
||||
type SendSource = "lightning" | "onchain";
|
||||
|
||||
@@ -97,23 +97,6 @@ function DestinationShower(props: {
|
||||
)
|
||||
}
|
||||
|
||||
function SendTags() {
|
||||
// Tagging stuff
|
||||
const [selectedValues, setSelectedValues] = createSignal<TagItem[]>([]);
|
||||
const [values, setValues] = createSignal<TagItem[]>([{ id: createUniqueId(), name: "Unknown", kind: "text" }]);
|
||||
|
||||
onMount(() => {
|
||||
listTags().then((tags) => {
|
||||
setValues(prev => [...prev, ...tags || []])
|
||||
});
|
||||
})
|
||||
|
||||
return (
|
||||
<TagEditor values={values()} setValues={setValues} selectedValues={selectedValues()} setSelectedValues={setSelectedValues} placeholder="Where's it going to?" />
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export default function Send() {
|
||||
const [state, actions] = useMegaStore();
|
||||
const navigate = useNavigate()
|
||||
@@ -136,6 +119,10 @@ export default function Send() {
|
||||
const [sending, setSending] = createSignal(false);
|
||||
const [sentDetails, setSentDetails] = createSignal<SentDetails>();
|
||||
|
||||
// Tagging stuff
|
||||
const [selectedValues, setSelectedValues] = createSignal<MutinyTagItem[]>([]);
|
||||
const [values, setValues] = createSignal<MutinyTagItem[]>([UNKNOWN_TAG]);
|
||||
|
||||
function clearAll() {
|
||||
setDestination(undefined);
|
||||
setAmountSats(0n);
|
||||
@@ -158,6 +145,10 @@ export default function Send() {
|
||||
setDestination(state.scan_result);
|
||||
actions.setScanResult(undefined);
|
||||
}
|
||||
|
||||
actions.listTags().then((tags) => {
|
||||
setValues(prev => [...prev, ...tags.sort(sortByLastUsed) || []])
|
||||
});
|
||||
})
|
||||
|
||||
// Rerun every time the destination changes
|
||||
@@ -235,20 +226,16 @@ export default function Send() {
|
||||
sentDetails.destination = bolt11;
|
||||
// If the invoice has sats use that, otherwise we pass the user-defined amount
|
||||
if (invoice()?.amount_sats) {
|
||||
// FIXME: labels
|
||||
await state.mutiny_wallet?.pay_invoice(firstNode, bolt11, undefined, []);
|
||||
await state.mutiny_wallet?.pay_invoice(firstNode, bolt11, undefined, tagsToIds(selectedValues()));
|
||||
sentDetails.amount = invoice()?.amount_sats;
|
||||
} else {
|
||||
// FIXME: labels
|
||||
await state.mutiny_wallet?.pay_invoice(firstNode, bolt11, amountSats(), []);
|
||||
await state.mutiny_wallet?.pay_invoice(firstNode, bolt11, amountSats(), tagsToIds(selectedValues()));
|
||||
sentDetails.amount = amountSats();
|
||||
}
|
||||
} else if (source() === "lightning" && nodePubkey()) {
|
||||
const nodes = await state.mutiny_wallet?.list_nodes();
|
||||
const firstNode = nodes[0] as string || ""
|
||||
// FIXME: labels
|
||||
const payment = await state.mutiny_wallet?.keysend(firstNode, nodePubkey()!, amountSats(), []);
|
||||
console.log(payment?.value)
|
||||
const payment = await state.mutiny_wallet?.keysend(firstNode, nodePubkey()!, amountSats(), tagsToIds(selectedValues()));
|
||||
|
||||
// TODO: handle timeouts
|
||||
if (!payment?.paid) {
|
||||
@@ -257,9 +244,8 @@ export default function Send() {
|
||||
sentDetails.amount = amountSats();
|
||||
}
|
||||
} else if (source() === "onchain" && address()) {
|
||||
// FIXME: actual labels
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const txid = await state.mutiny_wallet?.send_to_address(address()!, amountSats(), []);
|
||||
const txid = await state.mutiny_wallet?.send_to_address(address()!, amountSats(), tagsToIds(selectedValues()));
|
||||
sentDetails.amount = amountSats();
|
||||
sentDetails.destination = address();
|
||||
// TODO: figure out if this is necessary, it takes forever
|
||||
@@ -323,7 +309,7 @@ export default function Send() {
|
||||
<Card>
|
||||
<VStack>
|
||||
<DestinationShower source={source()} description={description()} invoice={invoice()} address={address()} nodePubkey={nodePubkey()} clearAll={clearAll} />
|
||||
<SendTags />
|
||||
<TagEditor values={values()} setValues={setValues} selectedValues={selectedValues()} setSelectedValues={setSelectedValues} placeholder="Where's it going to?" />
|
||||
</VStack>
|
||||
</Card>
|
||||
<AmountCard amountSats={amountSats().toString()} setAmountSats={setAmountSats} fee={fakeFee().toString()} isAmountEditable={!(invoice()?.amount_sats)} />
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
export type TagItem = TextItem | ContactItem;
|
||||
|
||||
export type TextItem = {
|
||||
id: string;
|
||||
kind: "text";
|
||||
name: string;
|
||||
}
|
||||
|
||||
export type ContactItem = {
|
||||
id: string;
|
||||
kind: "contact";
|
||||
name: string;
|
||||
npub?: string;
|
||||
color: Color;
|
||||
}
|
||||
|
||||
export type Color = "blue" | "green" | "red" | "gray"
|
||||
|
||||
export const createUniqueId = () => Math.random().toString(36).substr(2, 9);
|
||||
|
||||
export async function listContacts(): Promise<ContactItem[]> {
|
||||
// get contacts from localstorage
|
||||
const contacts: ContactItem[] = JSON.parse(localStorage.getItem("contacts") || "[]");
|
||||
return contacts;
|
||||
}
|
||||
|
||||
export async function listTexts(): Promise<TextItem[]> {
|
||||
// get texts from localstorage
|
||||
const texts: TextItem[] = JSON.parse(localStorage.getItem("texts") || "[]");
|
||||
return texts;
|
||||
}
|
||||
|
||||
export async function listTags(): Promise<TagItem[]> {
|
||||
const contacts = await listContacts();
|
||||
const texts = await listTexts();
|
||||
return [...contacts, ...texts];
|
||||
}
|
||||
|
||||
export async function addContact(contact: ContactItem): Promise<void> {
|
||||
const contacts = await listContacts();
|
||||
contacts.push(contact);
|
||||
localStorage.setItem("contacts", JSON.stringify(contacts));
|
||||
}
|
||||
|
||||
export async function editContact(contact: ContactItem): Promise<void> {
|
||||
const contacts = await listContacts();
|
||||
const index = contacts.findIndex(c => c.id === contact.id);
|
||||
contacts[index] = contact;
|
||||
localStorage.setItem("contacts", JSON.stringify(contacts));
|
||||
}
|
||||
|
||||
export async function addTextTag(text: TextItem): Promise<void> {
|
||||
const texts = await listTexts();
|
||||
texts.push(text);
|
||||
localStorage.setItem("texts", JSON.stringify(texts));
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { createStore } from "solid-js/store";
|
||||
import { MutinyWalletSettingStrings, setupMutinyWallet } from "~/logic/mutinyWalletSetup";
|
||||
import { MutinyBalance, MutinyWallet } from "@mutinywallet/mutiny-wasm";
|
||||
import { ParsedParams } from "~/routes/Scanner";
|
||||
import { MutinyTagItem } from "~/utils/tags";
|
||||
|
||||
const MegaStoreContext = createContext<MegaStore>();
|
||||
|
||||
@@ -33,6 +34,7 @@ export type MegaStore = [{
|
||||
sync(): Promise<void>;
|
||||
dismissRestorePrompt(): void;
|
||||
setHasBackedUp(): void;
|
||||
listTags(): Promise<MutinyTagItem[]>;
|
||||
}];
|
||||
|
||||
export const Provider: ParentComponent = (props) => {
|
||||
@@ -119,6 +121,9 @@ export const Provider: ParentComponent = (props) => {
|
||||
dismissRestorePrompt() {
|
||||
localStorage.setItem("dismissed_restore_prompt", "true")
|
||||
setState({ dismissed_restore_prompt: true })
|
||||
},
|
||||
async listTags(): Promise<MutinyTagItem[]> {
|
||||
return state.mutiny_wallet?.get_tag_items() as MutinyTagItem[]
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ContactItem } from "~/state/contacts";
|
||||
import { Contact } from "@mutinywallet/mutiny-wasm";
|
||||
|
||||
async function generateGradientFromHashedString(str: string) {
|
||||
export async function generateGradient(str: string) {
|
||||
const encoder = new TextEncoder();
|
||||
const data = encoder.encode(str);
|
||||
const digestBuffer = await crypto.subtle.digest('SHA-256', data);
|
||||
@@ -13,11 +13,12 @@ async function generateGradientFromHashedString(str: string) {
|
||||
return gradient;
|
||||
}
|
||||
|
||||
export async function gradientsPerContact(contacts: ContactItem[]) {
|
||||
export async function gradientsPerContact(contacts: Contact[]) {
|
||||
console.log(contacts);
|
||||
const gradients = new Map();
|
||||
for (const contact of contacts) {
|
||||
const gradient = await generateGradientFromHashedString(contact.name);
|
||||
gradients.set(contact.id, gradient);
|
||||
const gradient = await generateGradient(contact.name);
|
||||
gradients.set(contact.name, gradient);
|
||||
}
|
||||
|
||||
return gradients;
|
||||
|
||||
27
src/utils/tags.ts
Normal file
27
src/utils/tags.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { TagItem } from "@mutinywallet/mutiny-wasm"
|
||||
|
||||
export type MutinyTagItem = {
|
||||
id: string,
|
||||
kind: "Label" | "Contact"
|
||||
name: string,
|
||||
last_used_time: bigint,
|
||||
npub?: string,
|
||||
ln_address?: string,
|
||||
lnurl?: string,
|
||||
}
|
||||
|
||||
export const UNKNOWN_TAG: MutinyTagItem = { id: "Unknown", kind: "Label", name: "Unknown", last_used_time: 0n }
|
||||
|
||||
export function tagsToIds(tags: MutinyTagItem[]): string[] {
|
||||
return tags.filter((tag) => tag.id !== "Unknown").map((tag) => tag.id)
|
||||
}
|
||||
|
||||
export function tagToMutinyTag(tag: TagItem): MutinyTagItem {
|
||||
// @ts-ignore
|
||||
// FIXME: make typescript less mad about this
|
||||
return tag as MutinyTagItem
|
||||
}
|
||||
|
||||
export function sortByLastUsed(a: MutinyTagItem, b: MutinyTagItem) {
|
||||
return Number(b.last_used_time - a.last_used_time);
|
||||
}
|
||||
Reference in New Issue
Block a user