import { TagItem } from "@mutinywallet/mutiny-wasm"; import { cache, createAsync, useNavigate } from "@solidjs/router"; import { Plus, Save, Search, Shuffle, Users } from "lucide-solid"; import { createEffect, createMemo, createResource, createSignal, For, Match, Show, Switch } from "solid-js"; import { ActivityDetailsModal, Button, ButtonCard, ContactButton, NiceP, SimpleDialog } from "~/components"; import { useI18n } from "~/i18n/context"; import { PrivacyLevel } from "~/routes"; import { useMegaStore } from "~/state/megaStore"; import { actuallyFetchNostrProfile, createDeepSignal, hexpubFromNpub, profileToPseudoContact, PseudoContact, timeAgo } from "~/utils"; import { GenericItem } from "./GenericItem"; export type HackActivityType = | "Lightning" | "OnChain" | "ChannelOpen" | "ChannelClose"; export interface IActivityItem { kind: HackActivityType; id: string; amount_sats: number; inbound: boolean; labels: string[]; contacts: TagItem[]; last_updated: number; privacy_level: PrivacyLevel; } async function fetchContactForNpub( npub: string ): Promise { const hexpub = await hexpubFromNpub(npub); if (!hexpub) { return undefined; } const profile = await actuallyFetchNostrProfile(hexpub); if (!profile) { return undefined; } const pseudoContact = profileToPseudoContact(profile); return pseudoContact; } export function UnifiedActivityItem(props: { item: IActivityItem; onClick: (id: string, kind: HackActivityType) => void; onNewContactClick: (profile: PseudoContact) => void; }) { const navigate = useNavigate(); const click = () => { props.onClick( props.item.id, props.item.kind as unknown as HackActivityType ); }; const primaryContact = createMemo(() => { if (props.item.contacts.length === 0) { return undefined; } return props.item.contacts[0]; }); const getContact = cache(async (npub) => { return await fetchContactForNpub(npub); }, "profile"); const profileFromNostr = createAsync(async () => { if (props.item.contacts.length === 0) { if (props.item.labels) { const npub = props.item.labels.find((l) => l.startsWith("npub") ); if (npub) { try { const newContact = await getContact(npub); return newContact; } catch (e) { console.error(e); } } } } return undefined; }); // TODO: figure out what other shit we should filter out const message = () => { const filtered = props.item.labels.filter( (l) => l !== "SWAP" && !l.startsWith("LN Channel:") && !l.startsWith("npub") ); if (filtered.length === 0) { return undefined; } return filtered[0]; }; const shouldShowShuffle = () => { return ( props.item.kind === "ChannelOpen" || props.item.kind === "ChannelClose" || (props.item.labels.length > 0 && props.item.labels[0] === "SWAP") ); }; const verb = () => { if (props.item.kind === "ChannelOpen") { return "opened a"; } if (props.item.kind === "ChannelClose") { return "closed a"; } if (props.item.labels.length > 0 && props.item.labels[0] === "SWAP") { return "swapped to"; } if ( props.item.labels.length > 0 && props.item.labels[0] === "Swept Force Close" ) { return undefined; } return "sent"; }; const primaryName = () => { return props.item.inbound ? primaryContact()?.name || "Unknown" : "You"; }; const secondaryName = () => { if (props.item.labels.length > 0 && props.item.labels[0] === "SWAP") { return "Lightning"; } if ( props.item.kind === "ChannelOpen" || props.item.kind === "ChannelClose" ) { return "Lightning channel"; } if (!props.item.inbound) { return primaryContact()?.name || "Unknown"; } return "you"; }; const shouldShowGeneric = () => { if (props.item.inbound && primaryName() === "Unknown") { return true; } if (!props.item.inbound && secondaryName() === "Unknown") { return true; } }; return (
: undefined} primaryOnClick={() => primaryContact()?.id ? navigate(`/chat/${primaryContact()?.id}`) : profileFromNostr() ? props.onNewContactClick(profileFromNostr()!) : undefined } amountOnClick={click} primaryName={ props.item.inbound ? primaryContact()?.name ? primaryContact()!.name : profileFromNostr()?.name || "Unknown" : "You" } genericAvatar={shouldShowGeneric()} verb={verb()} message={message()} secondaryName={secondaryName()} amount={ props.item.amount_sats ? BigInt(props.item.amount_sats || 0) : undefined } date={timeAgo(props.item.last_updated)} accent={props.item.inbound ? "green" : undefined} visibility={ props.item.privacy_level === "Public" ? "public" : "private" } />
); } function NewContactModal(props: { profile: PseudoContact; close: () => void }) { const i18n = useI18n(); const navigate = useNavigate(); const [state, _actions] = useMegaStore(); async function createContact() { try { const existingContact = await state.mutiny_wallet?.get_contact_for_npub( props.profile.hexpub ); if (existingContact) { navigate(`/chat/${existingContact.id}`); return; } const contactId = await state.mutiny_wallet?.create_new_contact( props.profile.name, props.profile.hexpub, props.profile.ln_address, props.profile.lnurl, props.profile.image_url ); if (!contactId) { throw new Error("no contact id returned"); } const tagItem = await state.mutiny_wallet?.get_tag_item(contactId); if (!tagItem) { throw new Error("no contact returned"); } navigate(`/chat/${contactId}`); } catch (e) { console.error(e); } } return ( { props.close(); }} > {i18n.t("activity.start_a_chat_are_you_sure")} {}} />
); } export function CombinedActivity() { const [state, _actions] = useMegaStore(); const i18n = useI18n(); const [detailsOpen, setDetailsOpen] = createSignal(false); const [detailsKind, setDetailsKind] = createSignal(); const [detailsId, setDetailsId] = createSignal(""); const navigate = useNavigate(); function openDetailsModal(id: string, kind: HackActivityType) { console.log("Opening details modal: ", id, kind); if (!id) { console.warn("No id provided to openDetailsModal"); return; } setDetailsId(id); setDetailsKind(kind); setDetailsOpen(true); } async function getActivity() { try { console.log("refetching activity"); const activity = await state.mutiny_wallet?.get_activity( 50, undefined ); if (!activity) return []; return activity as IActivityItem[]; } catch (e) { console.error(e); return [] as IActivityItem[]; } } const [activity, { refetch }] = createResource(getActivity, { initialValue: [], storage: createDeepSignal }); createEffect(() => { // Should re-run after every sync if (!state.is_syncing) { refetch(); } }); const [newContact, setNewContact] = createSignal(); return ( <> setNewContact(undefined)} /> navigate("/settings/federations")} >
{i18n.t("home.federation")}
navigate("/receive")}>
{i18n.t("home.receive")}
navigate("/search")}>
{i18n.t("home.find")}
navigate("/settings/backup")} >
{i18n.t("home.backup")}
= 0}> navigate("/settings/backup")} >
{i18n.t("home.backup")}
{(activityItem) => ( )}
); }