mirror of
https://github.com/lumehq/lume.git
synced 2025-03-17 21:32:32 +01:00
Add bitcoin connect (#215)
* feat: add bitcoin connect * feat: improve zap screen
This commit is contained in:
parent
1283432632
commit
3fbd66dece
@ -9,6 +9,7 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@getalby/bitcoin-connect-react": "^3.5.3",
|
||||
"@lume/icons": "workspace:^",
|
||||
"@lume/system": "workspace:^",
|
||||
"@lume/ui": "workspace:^",
|
||||
|
@ -19,42 +19,33 @@ function Screen() {
|
||||
|
||||
return (
|
||||
<Container withDrag>
|
||||
<div className="h-full w-full flex-1 px-5">
|
||||
{!isDone ? (
|
||||
<>
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="inline-flex size-14 items-center justify-center rounded-xl bg-black text-white shadow-md">
|
||||
<ZapIcon className="size-5" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-2xl font-light">
|
||||
Connect <span className="font-semibold">bitcoin wallet</span>{" "}
|
||||
to start zapping to your favorite content and creator.
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-10 flex flex-col gap-2">
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label>Paste a Nostr Wallet Connect connection string</label>
|
||||
<textarea
|
||||
value={uri}
|
||||
onChange={(e) => setUri(e.target.value)}
|
||||
placeholder="nostrconnect://"
|
||||
className="h-24 w-full rounded-lg border-neutral-300 bg-transparent px-3 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:border-neutral-700 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={save}
|
||||
className="inline-flex h-11 w-full items-center justify-center gap-1.5 rounded-lg bg-blue-500 px-5 font-medium text-white hover:bg-blue-600"
|
||||
>
|
||||
Save & Connect
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div>Done</div>
|
||||
)}
|
||||
<div className="flex-1 w-full h-full px-5">
|
||||
<div className="flex flex-col gap-2">
|
||||
<div>
|
||||
<h3 className="text-2xl font-light">
|
||||
Connect <span className="font-semibold">bitcoin wallet</span> to
|
||||
start zapping to your favorite content and creator.
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-2 mt-10">
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label>Paste a Nostr Wallet Connect connection string</label>
|
||||
<textarea
|
||||
value={uri}
|
||||
onChange={(e) => setUri(e.target.value)}
|
||||
placeholder="nostrconnect://"
|
||||
className="w-full h-24 px-3 bg-transparent rounded-lg border-neutral-300 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:border-neutral-700 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={save}
|
||||
className="inline-flex h-11 w-full items-center justify-center gap-1.5 rounded-lg bg-blue-500 px-5 font-medium text-white hover:bg-blue-600"
|
||||
>
|
||||
Save & Connect
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Container>
|
||||
);
|
||||
|
@ -79,7 +79,7 @@ function Screen() {
|
||||
);
|
||||
}}
|
||||
</Link>
|
||||
<Link to="/settings/zap">
|
||||
<Link to="/settings/wallet">
|
||||
{({ isActive }) => {
|
||||
return (
|
||||
<div
|
||||
@ -91,9 +91,7 @@ function Screen() {
|
||||
)}
|
||||
>
|
||||
<ZapIcon className="size-5 shrink-0" />
|
||||
<p className="text-sm font-medium">
|
||||
{t("settings.zap.title")}
|
||||
</p>
|
||||
<p className="text-sm font-medium">Wallet</p>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
|
39
apps/desktop2/src/routes/settings/bitcoin-connect.tsx
Normal file
39
apps/desktop2/src/routes/settings/bitcoin-connect.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
import { Button, init } from "@getalby/bitcoin-connect-react";
|
||||
import { NostrAccount } from "@lume/system";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { getCurrent } from "@tauri-apps/api/webviewWindow";
|
||||
|
||||
export const Route = createFileRoute("/settings/bitcoin-connect")({
|
||||
beforeLoad: () => {
|
||||
init({
|
||||
appName: "Lume",
|
||||
filters: ["nwc"],
|
||||
showBalance: true,
|
||||
});
|
||||
},
|
||||
component: Screen,
|
||||
});
|
||||
|
||||
function Screen() {
|
||||
const setNwcUri = async (uri: string) => {
|
||||
const cmd = await NostrAccount.setWallet(uri);
|
||||
if (cmd) getCurrent().close();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center size-full">
|
||||
<div className="flex flex-col items-center justify-center gap-3 text-center">
|
||||
<div>
|
||||
<p className="text-sm text-black/70 dark:text-white/70">
|
||||
Click to the button below to connect with your Bitcoin wallet.
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
onConnected={(provider) =>
|
||||
setNwcUri(provider.client.nostrWalletConnectUrl)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -51,7 +51,7 @@ function Screen() {
|
||||
return (
|
||||
<div className="w-full max-w-xl mx-auto">
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex items-center w-full h-12 px-3 text-sm rounded-xl bg-black/5 dark:bg-white/5">
|
||||
<div className="flex items-center w-full px-3 text-sm rounded-lg h-11 bg-black/5 dark:bg-white/5">
|
||||
* Setting changes require restarting the app to take effect.
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
|
59
apps/desktop2/src/routes/settings/wallet.tsx
Normal file
59
apps/desktop2/src/routes/settings/wallet.tsx
Normal file
@ -0,0 +1,59 @@
|
||||
import { NostrAccount } from "@lume/system";
|
||||
import { getBitcoinDisplayValues } from "@lume/utils";
|
||||
import { createFileRoute, redirect } from "@tanstack/react-router";
|
||||
|
||||
export const Route = createFileRoute("/settings/wallet")({
|
||||
beforeLoad: async () => {
|
||||
const wallet = await NostrAccount.loadWallet();
|
||||
if (!wallet) {
|
||||
throw redirect({ to: "/settings/bitcoin-connect" });
|
||||
}
|
||||
const balance = getBitcoinDisplayValues(wallet);
|
||||
return { balance };
|
||||
},
|
||||
component: Screen,
|
||||
});
|
||||
|
||||
function Screen() {
|
||||
const { balance } = Route.useRouteContext();
|
||||
|
||||
const disconnect = async () => {
|
||||
window.localStorage.removeItem("bc:config");
|
||||
await NostrAccount.removeWallet();
|
||||
|
||||
return redirect({ to: "/settings/bitcoin-connect" });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-xl mx-auto">
|
||||
<div className="flex flex-col w-full gap-3">
|
||||
<div className="flex flex-col w-full px-3 bg-black/5 dark:bg-white/5 rounded-xl">
|
||||
<div className="flex items-center justify-between w-full gap-4 py-3">
|
||||
<div className="flex-1">
|
||||
<h3 className="font-medium">Connection</h3>
|
||||
</div>
|
||||
<div className="flex justify-end w-36 shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => disconnect()}
|
||||
className="h-8 w-max px-2.5 text-sm rounded-lg inline-flex items-center justify-center bg-black/10 dark:bg-white/10 hover:bg-black/20 dark:hover:bg-white/20"
|
||||
>
|
||||
Disconnect
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col w-full px-3 bg-black/5 dark:bg-white/5 rounded-xl">
|
||||
<div className="flex items-center justify-between w-full gap-4 py-3">
|
||||
<div className="flex-1">
|
||||
<h3 className="font-medium">Current Balance</h3>
|
||||
</div>
|
||||
<div className="flex justify-end w-36 shrink-0">
|
||||
₿ {balance.bitcoinFormatted}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,102 +0,0 @@
|
||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { message } from "@tauri-apps/plugin-dialog";
|
||||
import { useState } from "react";
|
||||
|
||||
export const Route = createLazyFileRoute("/settings/zap")({
|
||||
component: Screen,
|
||||
});
|
||||
|
||||
function Screen() {
|
||||
return (
|
||||
<div className="w-full max-w-xl mx-auto">
|
||||
<div className="flex flex-col gap-3 divide-y divide-neutral-300 dark:divide-neutral-700">
|
||||
<div className="flex flex-col gap-6 py-3">
|
||||
<Connection />
|
||||
<DefaultAmount />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Connection() {
|
||||
const [uri, setUri] = useState("");
|
||||
|
||||
const connect = async () => {
|
||||
try {
|
||||
await invoke("set_nwc", { uri });
|
||||
} catch (e) {
|
||||
await message(String(e), { title: "Zap", kind: "info" });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-start gap-6">
|
||||
<div className="text-sm font-medium w-36 shrink-0 text-end">
|
||||
Connection
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="flex flex-col w-full gap-1">
|
||||
<label
|
||||
htmlFor="nwc"
|
||||
className="text-sm font-medium text-neutral-700 dark:text-neutral-300"
|
||||
>
|
||||
Nostr Wallet Connect
|
||||
</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
name="nwc"
|
||||
type="text"
|
||||
value={uri}
|
||||
onChange={(e) => setUri(e.target.value)}
|
||||
placeholder="nostrconnect://"
|
||||
className="w-full px-3 bg-transparent rounded-lg h-9 border-neutral-300 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:border-neutral-700 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => connect()}
|
||||
className="inline-flex items-center justify-center w-24 text-sm font-medium rounded-lg h-9 bg-neutral-200 hover:bg-neutral-300 dark:bg-neutral-900 dark:hover:bg-neutral-700"
|
||||
>
|
||||
Connect
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DefaultAmount() {
|
||||
return (
|
||||
<div className="flex items-start gap-6">
|
||||
<div className="text-sm font-medium w-36 shrink-0 text-end">
|
||||
Default amount
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="flex flex-col w-full gap-1">
|
||||
<label
|
||||
htmlFor="amount"
|
||||
className="text-sm font-medium text-neutral-700 dark:text-neutral-300"
|
||||
>
|
||||
Set default amount for quick zapping
|
||||
</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
name="amount"
|
||||
type="number"
|
||||
value={21}
|
||||
className="w-full px-3 bg-transparent rounded-lg h-9 border-neutral-300 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:border-neutral-700 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center justify-center w-24 text-sm font-medium rounded-lg h-9 bg-neutral-200 hover:bg-neutral-300 dark:bg-neutral-900 dark:hover:bg-neutral-700"
|
||||
>
|
||||
Update
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,127 +0,0 @@
|
||||
import { Balance } from "@/components/balance";
|
||||
import { User } from "@/components/user";
|
||||
import { LumeEvent } from "@lume/system";
|
||||
import { Box, Container } from "@lume/ui";
|
||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||
import { getCurrent } from "@tauri-apps/api/webviewWindow";
|
||||
import { message } from "@tauri-apps/plugin-dialog";
|
||||
import { useState } from "react";
|
||||
import CurrencyInput from "react-currency-input-field";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const DEFAULT_VALUES = [69, 100, 200, 500];
|
||||
|
||||
export const Route = createLazyFileRoute("/zap/$id")({
|
||||
component: Screen,
|
||||
});
|
||||
|
||||
function Screen() {
|
||||
const { t } = useTranslation();
|
||||
const { id } = Route.useParams();
|
||||
// @ts-ignore, magic !!!
|
||||
const { pubkey, account } = Route.useSearch();
|
||||
|
||||
const [amount, setAmount] = useState(21);
|
||||
const [content, setContent] = useState("");
|
||||
const [isCompleted, setIsCompleted] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const submit = async () => {
|
||||
try {
|
||||
// start loading
|
||||
setIsLoading(true);
|
||||
|
||||
const val = await LumeEvent.zap(id, amount, content);
|
||||
|
||||
if (val) {
|
||||
setIsCompleted(true);
|
||||
const window = getCurrent();
|
||||
// close current window
|
||||
window.close();
|
||||
}
|
||||
} catch (e) {
|
||||
setIsLoading(false);
|
||||
await message(String(e), {
|
||||
title: "Zap",
|
||||
kind: "error",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Balance account={account} />
|
||||
<Box className="flex flex-col gap-3">
|
||||
<div className="flex flex-col justify-between h-full py-5">
|
||||
<div className="flex items-center justify-center gap-2 h-11 shrink-0">
|
||||
{t("note.zap.modalTitle")}{" "}
|
||||
<User.Provider pubkey={pubkey}>
|
||||
<User.Root className="inline-flex items-center gap-2 p-1 rounded-full bg-neutral-100 dark:bg-neutral-900">
|
||||
<User.Avatar className="rounded-full size-6" />
|
||||
<User.Name className="pr-2 text-sm font-medium" />
|
||||
</User.Root>
|
||||
</User.Provider>
|
||||
</div>
|
||||
<div className="flex flex-col justify-between flex-1 px-5">
|
||||
<div className="relative flex flex-col flex-1 pb-8">
|
||||
<div className="inline-flex items-center justify-center flex-1 h-full gap-1">
|
||||
<CurrencyInput
|
||||
placeholder="0"
|
||||
defaultValue={21}
|
||||
value={amount}
|
||||
decimalsLimit={2}
|
||||
min={0} // 0 sats
|
||||
max={10000} // 1M sats
|
||||
maxLength={10000} // 1M sats
|
||||
onValueChange={(value) => setAmount(Number(value))}
|
||||
className="flex-1 w-full text-4xl font-semibold text-right bg-transparent border-none placeholder:text-neutral-600 focus:outline-none focus:ring-0 dark:text-neutral-400"
|
||||
/>
|
||||
<span className="flex-1 w-full text-4xl font-semibold text-left text-neutral-500 dark:text-neutral-400">
|
||||
sats
|
||||
</span>
|
||||
</div>
|
||||
<div className="inline-flex items-center justify-center gap-2">
|
||||
{DEFAULT_VALUES.map((value) => (
|
||||
<button
|
||||
key={value}
|
||||
type="button"
|
||||
onClick={() => setAmount(value)}
|
||||
className="w-max rounded-full bg-neutral-100 px-2.5 py-1 text-sm font-medium hover:bg-neutral-200 dark:bg-neutral-900 dark:hover:bg-neutral-800"
|
||||
>
|
||||
{value} sats
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col w-full gap-2">
|
||||
<input
|
||||
name="message"
|
||||
value={content}
|
||||
onChange={(e) => setContent(e.target.value)}
|
||||
spellCheck={false}
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
placeholder={t("note.zap.messagePlaceholder")}
|
||||
className="h-11 w-full resize-none rounded-lg border-transparent bg-neutral-100 px-3 !outline-none placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:bg-neutral-950 dark:text-neutral-400"
|
||||
/>
|
||||
<div className="flex flex-col gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => submit()}
|
||||
className="inline-flex h-9 w-full items-center justify-center rounded-lg border-t border-neutral-900 bg-neutral-950 pb-[2px] font-semibold text-neutral-50 hover:bg-neutral-900 dark:border-neutral-800 dark:bg-neutral-900 dark:hover:bg-neutral-800"
|
||||
>
|
||||
{isCompleted
|
||||
? t("note.zap.buttonFinish")
|
||||
: isLoading
|
||||
? t("note.zap.buttonLoading")
|
||||
: t("note.zap.zap")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Box>
|
||||
</Container>
|
||||
);
|
||||
}
|
119
apps/desktop2/src/routes/zap.$id.tsx
Normal file
119
apps/desktop2/src/routes/zap.$id.tsx
Normal file
@ -0,0 +1,119 @@
|
||||
import { User } from "@/components/user";
|
||||
import { NostrQuery } from "@lume/system";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { getCurrent } from "@tauri-apps/api/webviewWindow";
|
||||
import { message } from "@tauri-apps/plugin-dialog";
|
||||
import { useState } from "react";
|
||||
import CurrencyInput from "react-currency-input-field";
|
||||
|
||||
const DEFAULT_VALUES = [21, 50, 100, 200];
|
||||
|
||||
export const Route = createFileRoute("/zap/$id")({
|
||||
beforeLoad: async ({ params }) => {
|
||||
const event = await NostrQuery.getEvent(params.id);
|
||||
return { event };
|
||||
},
|
||||
component: Screen,
|
||||
});
|
||||
|
||||
function Screen() {
|
||||
const { event } = Route.useRouteContext();
|
||||
|
||||
const [amount, setAmount] = useState(21);
|
||||
const [content, setContent] = useState("");
|
||||
const [isCompleted, setIsCompleted] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const submit = async () => {
|
||||
try {
|
||||
// start loading
|
||||
setIsLoading(true);
|
||||
|
||||
// Zap
|
||||
const val = await event.zap(amount, content);
|
||||
|
||||
if (val) {
|
||||
setIsCompleted(true);
|
||||
// close current window
|
||||
await getCurrent().close();
|
||||
}
|
||||
} catch (e) {
|
||||
setIsLoading(false);
|
||||
await message(String(e), {
|
||||
title: "Zap",
|
||||
kind: "error",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div data-tauri-drag-region className="flex flex-col pb-5 size-full">
|
||||
<div
|
||||
data-tauri-drag-region
|
||||
className="flex items-center justify-center h-24 gap-2 shrink-0"
|
||||
>
|
||||
<p className="text-sm">Send zap to </p>
|
||||
<User.Provider pubkey={event.pubkey}>
|
||||
<User.Root className="inline-flex items-center gap-2 p-1 rounded-full bg-black/5 dark:bg-white/5">
|
||||
<User.Avatar className="rounded-full size-6" />
|
||||
<User.Name className="pr-2 text-sm font-medium" />
|
||||
</User.Root>
|
||||
</User.Provider>
|
||||
</div>
|
||||
<div className="flex flex-col justify-between h-full">
|
||||
<div className="flex flex-col justify-between flex-1 px-5">
|
||||
<div className="relative flex flex-col flex-1 pb-8">
|
||||
<div className="inline-flex items-center justify-center flex-1 h-full gap-1">
|
||||
<CurrencyInput
|
||||
placeholder="0"
|
||||
defaultValue={21}
|
||||
value={amount}
|
||||
decimalsLimit={2}
|
||||
min={0} // 0 sats
|
||||
max={10000} // 1M sats
|
||||
maxLength={10000} // 1M sats
|
||||
onValueChange={(value) => setAmount(Number(value))}
|
||||
className="flex-1 w-full text-4xl font-semibold text-right bg-transparent border-none placeholder:text-neutral-600 focus:outline-none focus:ring-0 dark:text-neutral-400"
|
||||
/>
|
||||
<span className="flex-1 w-full text-4xl font-semibold text-left text-neutral-500 dark:text-neutral-400">
|
||||
sats
|
||||
</span>
|
||||
</div>
|
||||
<div className="inline-flex items-center justify-center gap-2">
|
||||
{DEFAULT_VALUES.map((value) => (
|
||||
<button
|
||||
key={value}
|
||||
type="button"
|
||||
onClick={() => setAmount(value)}
|
||||
className="w-max rounded-full bg-black/10 px-2.5 py-1 text-xs font-medium hover:bg-black/20 dark:bg-white/10 dark:hover:bg-white/20"
|
||||
>
|
||||
{value} sats
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col w-full gap-2">
|
||||
<input
|
||||
name="message"
|
||||
value={content}
|
||||
onChange={(e) => setContent(e.target.value)}
|
||||
spellCheck={false}
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
autoCapitalize="off"
|
||||
placeholder="Enter message (optional)"
|
||||
className="h-11 w-full resize-none rounded-xl border-transparent bg-black/5 px-3 !outline-none placeholder:text-neutral-600 focus:border-blue-500 focus:ring-0 dark:bg-white/5"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => submit()}
|
||||
className="inline-flex items-center justify-center w-full h-10 font-medium rounded-xl bg-neutral-950 text-neutral-50 hover:bg-neutral-900 dark:bg-white/20 dark:hover:bg-white/30"
|
||||
>
|
||||
{isCompleted ? "Zapped" : isLoading ? "Processing..." : "Zap"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import type { Metadata } from "@lume/types";
|
||||
import { type Result, commands } from "./commands";
|
||||
import { Window } from "@tauri-apps/api/window";
|
||||
import { type Result, commands } from "./commands";
|
||||
|
||||
export class NostrAccount {
|
||||
static async getAccounts() {
|
||||
@ -99,8 +99,28 @@ export class NostrAccount {
|
||||
}
|
||||
}
|
||||
|
||||
static async loadWallet() {
|
||||
const query = await commands.loadWallet();
|
||||
|
||||
if (query.status === "ok") {
|
||||
return Number.parseInt(query.data);
|
||||
} else {
|
||||
throw new Error(query.error);
|
||||
}
|
||||
}
|
||||
|
||||
static async setWallet(uri: string) {
|
||||
const query = await commands.setNwc(uri);
|
||||
const query = await commands.setWallet(uri);
|
||||
|
||||
if (query.status === "ok") {
|
||||
return query.data;
|
||||
} else {
|
||||
throw new Error(query.error);
|
||||
}
|
||||
}
|
||||
|
||||
static async removeWallet() {
|
||||
const query = await commands.removeWallet();
|
||||
|
||||
if (query.status === "ok") {
|
||||
return query.data;
|
||||
@ -110,7 +130,7 @@ export class NostrAccount {
|
||||
}
|
||||
|
||||
static async getProfile() {
|
||||
const query = await commands.getCurrentUserProfile();
|
||||
const query = await commands.getCurrentProfile();
|
||||
|
||||
if (query.status === "ok") {
|
||||
return JSON.parse(query.data) as Metadata;
|
||||
@ -119,16 +139,6 @@ export class NostrAccount {
|
||||
}
|
||||
}
|
||||
|
||||
static async getBalance() {
|
||||
const query = await commands.getBalance();
|
||||
|
||||
if (query.status === "ok") {
|
||||
return Number.parseInt(query.data);
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
static async getContactList() {
|
||||
const query = await commands.getContactList();
|
||||
|
||||
|
@ -180,25 +180,25 @@ try {
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async setNwc(uri: string) : Promise<Result<boolean, string>> {
|
||||
async setWallet(uri: string) : Promise<Result<boolean, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("set_nwc", { uri }) };
|
||||
return { status: "ok", data: await TAURI_INVOKE("set_wallet", { uri }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async loadNwc() : Promise<Result<boolean, string>> {
|
||||
async loadWallet() : Promise<Result<string, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("load_nwc") };
|
||||
return { status: "ok", data: await TAURI_INVOKE("load_wallet") };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async getBalance() : Promise<Result<string, string>> {
|
||||
async removeWallet() : Promise<Result<null, null>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("get_balance") };
|
||||
return { status: "ok", data: await TAURI_INVOKE("remove_wallet") };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
@ -407,9 +407,9 @@ try {
|
||||
else return { status: "error", error: e as any };
|
||||
}
|
||||
},
|
||||
async openWindow(label: string, title: string, url: string, width: number, height: number) : Promise<Result<null, string>> {
|
||||
async openWindow(window: Window) : Promise<Result<null, string>> {
|
||||
try {
|
||||
return { status: "ok", data: await TAURI_INVOKE("open_window", { label, title, url, width, height }) };
|
||||
return { status: "ok", data: await TAURI_INVOKE("open_window", { window }) };
|
||||
} catch (e) {
|
||||
if(e instanceof Error) throw e;
|
||||
else return { status: "error", error: e as any };
|
||||
@ -438,6 +438,7 @@ export type Meta = { content: string; images: string[]; videos: string[]; events
|
||||
export type Relays = { connected: string[]; read: string[] | null; write: string[] | null; both: string[] | null }
|
||||
export type RichEvent = { raw: string; parsed: Meta | null }
|
||||
export type Settings = { proxy: string | null; image_resize_service: string | null; use_relay_hint: boolean; content_warning: boolean; display_avatar: boolean; display_zap_button: boolean; display_repost_button: boolean; display_media: boolean }
|
||||
export type Window = { label: string; title: string; url: string; width: number; height: number; maximizable: boolean; minimizable: boolean }
|
||||
|
||||
/** tauri-specta globals **/
|
||||
|
||||
|
@ -18,7 +18,15 @@ export class LumeWindow {
|
||||
const label = `event-${event.id}`;
|
||||
const url = `/events/${root ?? reply ?? event.id}`;
|
||||
|
||||
const query = await commands.openWindow(label, "Thread", url, 500, 800);
|
||||
const query = await commands.openWindow({
|
||||
label,
|
||||
url,
|
||||
title: "Thread",
|
||||
width: 500,
|
||||
height: 800,
|
||||
maximizable: true,
|
||||
minimizable: true,
|
||||
});
|
||||
|
||||
if (query.status === "ok") {
|
||||
return query.data;
|
||||
@ -29,13 +37,15 @@ export class LumeWindow {
|
||||
|
||||
static async openProfile(pubkey: string) {
|
||||
const label = `user-${pubkey}`;
|
||||
const query = await commands.openWindow(
|
||||
const query = await commands.openWindow({
|
||||
label,
|
||||
"Profile",
|
||||
`/users/${pubkey}`,
|
||||
500,
|
||||
800,
|
||||
);
|
||||
url: `/users/${pubkey}`,
|
||||
title: "Profile",
|
||||
width: 500,
|
||||
height: 800,
|
||||
maximizable: true,
|
||||
minimizable: true,
|
||||
});
|
||||
|
||||
if (query.status === "ok") {
|
||||
return query.data;
|
||||
@ -60,7 +70,15 @@ export class LumeWindow {
|
||||
}
|
||||
|
||||
const label = `editor-${reply_to ? reply_to : 0}`;
|
||||
const query = await commands.openWindow(label, "Editor", url, 560, 340);
|
||||
const query = await commands.openWindow({
|
||||
label,
|
||||
url,
|
||||
title: "Editor",
|
||||
width: 560,
|
||||
height: 340,
|
||||
maximizable: true,
|
||||
minimizable: false,
|
||||
});
|
||||
|
||||
if (query.status === "ok") {
|
||||
return query.data;
|
||||
@ -69,45 +87,35 @@ export class LumeWindow {
|
||||
}
|
||||
}
|
||||
|
||||
static async openZap(id: string, pubkey: string) {
|
||||
const nwc = await commands.loadNwc();
|
||||
static async openZap(id: string) {
|
||||
const wallet = await commands.loadWallet();
|
||||
|
||||
if (nwc.status === "ok") {
|
||||
const status = nwc.data;
|
||||
|
||||
if (!status) {
|
||||
const label = "nwc";
|
||||
await commands.openWindow(
|
||||
label,
|
||||
"Nostr Wallet Connect",
|
||||
"/nwc",
|
||||
400,
|
||||
600,
|
||||
);
|
||||
} else {
|
||||
const label = `zap-${id}`;
|
||||
await commands.openWindow(
|
||||
label,
|
||||
"Zap",
|
||||
`/zap/${id}?pubkey=${pubkey}`,
|
||||
400,
|
||||
500,
|
||||
);
|
||||
}
|
||||
if (wallet.status === "ok") {
|
||||
await commands.openWindow({
|
||||
label: `zap-${id}`,
|
||||
url: `/zap/${id}`,
|
||||
title: "Zap",
|
||||
width: 360,
|
||||
height: 460,
|
||||
maximizable: false,
|
||||
minimizable: false,
|
||||
});
|
||||
} else {
|
||||
throw new Error(nwc.error);
|
||||
await LumeWindow.openSettings("bitcoin-connect");
|
||||
}
|
||||
}
|
||||
|
||||
static async openSettings() {
|
||||
static async openSettings(path?: string) {
|
||||
const label = "settings";
|
||||
const query = await commands.openWindow(
|
||||
const query = await commands.openWindow({
|
||||
label,
|
||||
"Settings",
|
||||
"/settings/general",
|
||||
800,
|
||||
500,
|
||||
);
|
||||
url: path ? `/settings/${path}` : "/settings/general",
|
||||
title: "Settings",
|
||||
width: 800,
|
||||
height: 500,
|
||||
maximizable: false,
|
||||
minimizable: false,
|
||||
});
|
||||
|
||||
if (query.status === "ok") {
|
||||
return query.data;
|
||||
@ -118,30 +126,15 @@ export class LumeWindow {
|
||||
|
||||
static async openSearch() {
|
||||
const label = "search";
|
||||
const query = await commands.openWindow(
|
||||
const query = await commands.openWindow({
|
||||
label,
|
||||
"Search",
|
||||
"/search",
|
||||
400,
|
||||
600,
|
||||
);
|
||||
|
||||
if (query.status === "ok") {
|
||||
return query.data;
|
||||
} else {
|
||||
throw new Error(query.error);
|
||||
}
|
||||
}
|
||||
|
||||
static async openActivity(account: string) {
|
||||
const label = "activity";
|
||||
const query = await commands.openWindow(
|
||||
label,
|
||||
"Activity",
|
||||
`/activity/${account}/texts`,
|
||||
400,
|
||||
600,
|
||||
);
|
||||
url: "/search",
|
||||
title: "Search",
|
||||
width: 400,
|
||||
height: 600,
|
||||
maximizable: false,
|
||||
minimizable: false,
|
||||
});
|
||||
|
||||
if (query.status === "ok") {
|
||||
return query.data;
|
||||
|
@ -81,7 +81,7 @@ export function displayLongHandle(str: string) {
|
||||
const handle = split[0];
|
||||
const service = split[1];
|
||||
|
||||
return handle.substring(0, 16) + "..." + "@" + service;
|
||||
return `${handle.substring(0, 16)}...@${service}`;
|
||||
}
|
||||
|
||||
// convert number to K, M, B, T, etc.
|
||||
@ -127,7 +127,7 @@ export function getBitcoinDisplayValues(satoshis: number) {
|
||||
.reverse()
|
||||
.forEach((c, index) => {
|
||||
if (index > 0 && index % 3 === 0) {
|
||||
res = " " + res;
|
||||
res = ` ${res}`;
|
||||
}
|
||||
res = c + res;
|
||||
});
|
||||
|
114
pnpm-lock.yaml
generated
114
pnpm-lock.yaml
generated
@ -54,6 +54,9 @@ importers:
|
||||
|
||||
apps/desktop2:
|
||||
dependencies:
|
||||
'@getalby/bitcoin-connect-react':
|
||||
specifier: ^3.5.3
|
||||
version: 3.5.3(@types/react@18.3.3)(react@18.3.1)(typescript@5.4.5)
|
||||
'@lume/icons':
|
||||
specifier: workspace:^
|
||||
version: link:../../packages/icons
|
||||
@ -1196,6 +1199,49 @@ packages:
|
||||
resolution: {integrity: sha512-7ncjjSpRSRKvjJEoru092iFiEoC89lz4oG4+SGg9hh7DI/5SXf+kE+dg+6Fv/bwiK/WJCo4Q2gvPZGRlCE5mcA==}
|
||||
dev: false
|
||||
|
||||
/@getalby/bitcoin-connect-react@3.5.3(@types/react@18.3.3)(react@18.3.1)(typescript@5.4.5):
|
||||
resolution: {integrity: sha512-/oAPFFva/T946JzuNv6X/AuCGb46co2rLxfiINy4am/jFN+mAZ1HNGjOycTodpsTXnNpr3Ih37wV+YuI/03ueQ==}
|
||||
peerDependencies:
|
||||
react: ^18.2.0
|
||||
dependencies:
|
||||
'@getalby/bitcoin-connect': 3.5.3(@types/react@18.3.3)(react@18.3.1)(typescript@5.4.5)
|
||||
react: 18.3.1
|
||||
transitivePeerDependencies:
|
||||
- '@types/react'
|
||||
- immer
|
||||
- typescript
|
||||
dev: false
|
||||
|
||||
/@getalby/bitcoin-connect@3.5.3(@types/react@18.3.3)(react@18.3.1)(typescript@5.4.5):
|
||||
resolution: {integrity: sha512-csVNT4gXzuJtXP3ZyXnNnSGpt4JEQJvynLhc/aG3VNZVqPhNgot5Npj1J4XIewrsn4sWVIr9WkAtCxGfQ6XmSQ==}
|
||||
dependencies:
|
||||
'@getalby/lightning-tools': 5.0.3
|
||||
'@getalby/sdk': 3.5.1(typescript@5.4.5)
|
||||
'@lightninglabs/lnc-web': 0.3.1-alpha
|
||||
qrcode-generator: 1.4.4
|
||||
zustand: 4.5.2(@types/react@18.3.3)(react@18.3.1)
|
||||
transitivePeerDependencies:
|
||||
- '@types/react'
|
||||
- immer
|
||||
- react
|
||||
- typescript
|
||||
dev: false
|
||||
|
||||
/@getalby/lightning-tools@5.0.3:
|
||||
resolution: {integrity: sha512-QG3/SBI5n2py5IgsjP3K+c8eq55eiI3PQB12yo9Pot0b5hcN7TNNoTKn0fgLJjO1iEVCUkF513kDOpjjXwK0hQ==}
|
||||
engines: {node: '>=14'}
|
||||
dev: false
|
||||
|
||||
/@getalby/sdk@3.5.1(typescript@5.4.5):
|
||||
resolution: {integrity: sha512-Qz9GgXMoVpupDLqbzA2CHpru+9yqijQrxeRN7CDfV6l39js/BGwin93MFTh7eFj2TsMo+i8JeM3BVn+SJn/iRg==}
|
||||
engines: {node: '>=14'}
|
||||
dependencies:
|
||||
eventemitter3: 5.0.1
|
||||
nostr-tools: 1.17.0(typescript@5.4.5)
|
||||
transitivePeerDependencies:
|
||||
- typescript
|
||||
dev: false
|
||||
|
||||
/@img/sharp-darwin-arm64@0.33.4:
|
||||
resolution: {integrity: sha512-p0suNqXufJs9t3RqLBO6vvrgr5OhgbWp76s5gTRvdmxmuv9E1rcaqGUsl3l4mKVmXPkTkTErXediAui4x+8PSA==}
|
||||
engines: {glibc: '>=2.26', node: ^18.17.0 || ^20.3.0 || >=21.0.0, npm: '>=9.6.5', pnpm: '>=7.1.0', yarn: '>=3.2.0'}
|
||||
@ -1433,6 +1479,21 @@ packages:
|
||||
resolution: {integrity: sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==}
|
||||
dev: false
|
||||
|
||||
/@lightninglabs/lnc-core@0.3.1-alpha:
|
||||
resolution: {integrity: sha512-I/hThdItLWJ6RU8Z27ZIXhpBS2JJuD3+TjtaQXX2CabaUYXlcN4sk+Kx8N/zG/fk8qZvjlRWum4vHu4ZX554Fg==}
|
||||
dev: false
|
||||
|
||||
/@lightninglabs/lnc-web@0.3.1-alpha:
|
||||
resolution: {integrity: sha512-yL5SgBkl6kd6ISzJHGlSN7TXbiDoo1pfGvTOIdVWYVyXtEeW8PT+x6YGOmyQXGFT2OOf7fC7PfP9VnskDPuFaA==}
|
||||
dependencies:
|
||||
'@lightninglabs/lnc-core': 0.3.1-alpha
|
||||
crypto-js: 4.2.0
|
||||
dev: false
|
||||
|
||||
/@noble/ciphers@0.2.0:
|
||||
resolution: {integrity: sha512-6YBxJDAapHSdd3bLDv6x2wRPwq4QFMUaB3HvljNBUTThDd12eSm7/3F+2lnfzx2jvM+S6Nsy0jEt9QbPqSwqRw==}
|
||||
dev: false
|
||||
|
||||
/@noble/ciphers@0.5.3:
|
||||
resolution: {integrity: sha512-B0+6IIHiqEs3BPMT0hcRmHvEj2QHOLu+uwt+tqDDeVd0oyVzh7BPrDcPjRnV1PV/5LaknXJJQvOuRGR0zQJz+w==}
|
||||
dev: false
|
||||
@ -3434,6 +3495,10 @@ packages:
|
||||
shebang-command: 2.0.0
|
||||
which: 2.0.2
|
||||
|
||||
/crypto-js@4.2.0:
|
||||
resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==}
|
||||
dev: false
|
||||
|
||||
/cssesc@3.0.0:
|
||||
resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
|
||||
engines: {node: '>=4'}
|
||||
@ -4720,6 +4785,23 @@ packages:
|
||||
resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
/nostr-tools@1.17.0(typescript@5.4.5):
|
||||
resolution: {integrity: sha512-LZmR8GEWKZeElbFV5Xte75dOeE9EFUW/QLI1Ncn3JKn0kFddDKEfBbFN8Mu4TMs+L4HR/WTPha2l+PPuRnJcMw==}
|
||||
peerDependencies:
|
||||
typescript: '>=5.0.0'
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@noble/ciphers': 0.2.0
|
||||
'@noble/curves': 1.1.0
|
||||
'@noble/hashes': 1.3.1
|
||||
'@scure/base': 1.1.1
|
||||
'@scure/bip32': 1.3.1
|
||||
'@scure/bip39': 1.2.1
|
||||
typescript: 5.4.5
|
||||
dev: false
|
||||
|
||||
/nostr-tools@2.7.0(typescript@5.4.5):
|
||||
resolution: {integrity: sha512-jJoL2J1CBiKDxaXZww27nY/Wsuxzx7AULxmGKFce4sskDu1tohNyfnzYQ8BvDyvkstU8kNZUAXPL32tre33uig==}
|
||||
peerDependencies:
|
||||
@ -5019,6 +5101,10 @@ packages:
|
||||
resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==}
|
||||
dev: false
|
||||
|
||||
/qrcode-generator@1.4.4:
|
||||
resolution: {integrity: sha512-HM7yY8O2ilqhmULxGMpcHSF1EhJJ9yBj8gvDEuZ6M+KGJ0YY2hKpnXvRD+hZPLrDVck3ExIGhmPtSdcjC+guuw==}
|
||||
dev: false
|
||||
|
||||
/queue-microtask@1.2.3:
|
||||
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
||||
|
||||
@ -5921,6 +6007,14 @@ packages:
|
||||
tslib: 2.6.3
|
||||
dev: false
|
||||
|
||||
/use-sync-external-store@1.2.0(react@18.3.1):
|
||||
resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==}
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||
dependencies:
|
||||
react: 18.3.1
|
||||
dev: false
|
||||
|
||||
/use-sync-external-store@1.2.2(react@18.3.1):
|
||||
resolution: {integrity: sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==}
|
||||
peerDependencies:
|
||||
@ -6327,6 +6421,26 @@ packages:
|
||||
/zod@3.23.8:
|
||||
resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==}
|
||||
|
||||
/zustand@4.5.2(@types/react@18.3.3)(react@18.3.1):
|
||||
resolution: {integrity: sha512-2cN1tPkDVkwCy5ickKrI7vijSjPksFRfqS6237NzT0vqSsztTNnQdHw9mmN7uBdk3gceVXU0a+21jFzFzAc9+g==}
|
||||
engines: {node: '>=12.7.0'}
|
||||
peerDependencies:
|
||||
'@types/react': '>=16.8'
|
||||
immer: '>=9.0.6'
|
||||
react: '>=16.8'
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
immer:
|
||||
optional: true
|
||||
react:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@types/react': 18.3.3
|
||||
react: 18.3.1
|
||||
use-sync-external-store: 1.2.0(react@18.3.1)
|
||||
dev: false
|
||||
|
||||
/zwitch@2.0.4:
|
||||
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
|
||||
dev: false
|
||||
|
@ -3,6 +3,8 @@ use std::str::FromStr;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
use cocoa::{appkit::NSApp, base::nil, foundation::NSString};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
use tauri::{LogicalPosition, LogicalSize, Manager, State, WebviewUrl};
|
||||
#[cfg(target_os = "macos")]
|
||||
use tauri::TitleBarStyle;
|
||||
@ -14,6 +16,17 @@ use url::Url;
|
||||
|
||||
use crate::Nostr;
|
||||
|
||||
#[derive(Serialize, Deserialize, Type)]
|
||||
pub struct Window {
|
||||
label: String,
|
||||
title: String,
|
||||
url: String,
|
||||
width: f64,
|
||||
height: f64,
|
||||
maximizable: bool,
|
||||
minimizable: bool,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn create_column(
|
||||
@ -121,15 +134,8 @@ pub fn resize_column(
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub fn open_window(
|
||||
label: &str,
|
||||
title: &str,
|
||||
url: &str,
|
||||
width: f64,
|
||||
height: f64,
|
||||
app_handle: tauri::AppHandle,
|
||||
) -> Result<(), String> {
|
||||
if let Some(window) = app_handle.get_window(label) {
|
||||
pub fn open_window(window: Window, app_handle: tauri::AppHandle) -> Result<(), String> {
|
||||
if let Some(window) = app_handle.get_window(&window.label) {
|
||||
if window.is_visible().unwrap_or_default() {
|
||||
let _ = window.set_focus();
|
||||
} else {
|
||||
@ -138,21 +144,27 @@ pub fn open_window(
|
||||
};
|
||||
} else {
|
||||
#[cfg(target_os = "macos")]
|
||||
let window = WebviewWindowBuilder::new(&app_handle, label, WebviewUrl::App(PathBuf::from(url)))
|
||||
.title(title)
|
||||
.min_inner_size(width, height)
|
||||
.inner_size(width, height)
|
||||
.hidden_title(true)
|
||||
.title_bar_style(TitleBarStyle::Overlay)
|
||||
.transparent(true)
|
||||
.effects(WindowEffectsConfig {
|
||||
state: None,
|
||||
effects: vec![Effect::WindowBackground],
|
||||
radius: None,
|
||||
color: None,
|
||||
})
|
||||
.build()
|
||||
.unwrap();
|
||||
let window = WebviewWindowBuilder::new(
|
||||
&app_handle,
|
||||
&window.label,
|
||||
WebviewUrl::App(PathBuf::from(window.url)),
|
||||
)
|
||||
.title(&window.title)
|
||||
.min_inner_size(window.width, window.height)
|
||||
.inner_size(window.width, window.height)
|
||||
.hidden_title(true)
|
||||
.title_bar_style(TitleBarStyle::Overlay)
|
||||
.transparent(true)
|
||||
.minimizable(window.minimizable)
|
||||
.maximizable(window.maximizable)
|
||||
.effects(WindowEffectsConfig {
|
||||
state: None,
|
||||
effects: vec![Effect::WindowBackground],
|
||||
radius: None,
|
||||
color: None,
|
||||
})
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
let window = WebviewWindowBuilder::new(&app_handle, label, WebviewUrl::App(PathBuf::from(url)))
|
||||
@ -180,10 +192,6 @@ pub fn open_window(
|
||||
#[cfg(target_os = "windows")]
|
||||
// Create a custom titlebar for Windows
|
||||
window.create_overlay_titlebar().unwrap();
|
||||
|
||||
// Set a custom inset to the traffic lights
|
||||
#[cfg(target_os = "macos")]
|
||||
window.set_traffic_lights_inset(8.0, 16.0).unwrap();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
@ -9,20 +9,20 @@ extern crate cocoa;
|
||||
#[macro_use]
|
||||
extern crate objc;
|
||||
|
||||
use std::sync::Mutex;
|
||||
use std::time::Duration;
|
||||
use std::{
|
||||
fs,
|
||||
io::{self, BufRead},
|
||||
str::FromStr,
|
||||
};
|
||||
use std::sync::Mutex;
|
||||
use std::time::Duration;
|
||||
|
||||
use nostr_sdk::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use specta::Type;
|
||||
use tauri::{Manager, path::BaseDirectory};
|
||||
#[cfg(target_os = "macos")]
|
||||
use tauri::tray::{MouseButtonState, TrayIconEvent};
|
||||
use tauri::{path::BaseDirectory, Manager};
|
||||
use tauri_nspanel::ManagerExt;
|
||||
use tauri_plugin_decorum::WebviewWindowExt;
|
||||
|
||||
@ -98,9 +98,9 @@ fn main() {
|
||||
nostr::metadata::toggle_contact,
|
||||
nostr::metadata::get_nstore,
|
||||
nostr::metadata::set_nstore,
|
||||
nostr::metadata::set_nwc,
|
||||
nostr::metadata::load_nwc,
|
||||
nostr::metadata::get_balance,
|
||||
nostr::metadata::set_wallet,
|
||||
nostr::metadata::load_wallet,
|
||||
nostr::metadata::remove_wallet,
|
||||
nostr::metadata::zap_profile,
|
||||
nostr::metadata::zap_event,
|
||||
nostr::metadata::friend_to_friend,
|
||||
|
@ -310,12 +310,12 @@ pub async fn get_nstore(key: &str, state: State<'_, Nostr>) -> Result<String, St
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn set_nwc(uri: &str, state: State<'_, Nostr>) -> Result<bool, String> {
|
||||
pub async fn set_wallet(uri: &str, state: State<'_, Nostr>) -> Result<bool, String> {
|
||||
let client = &state.client;
|
||||
|
||||
if let Ok(nwc_uri) = NostrWalletConnectURI::from_str(uri) {
|
||||
let nwc = NWC::new(nwc_uri);
|
||||
let keyring = Entry::new("Lume Secret Storage", "NWC").map_err(|e| e.to_string())?;
|
||||
let keyring = Entry::new("Lume Secret", "Bitcoin Connect").map_err(|e| e.to_string())?;
|
||||
keyring.set_password(uri).map_err(|e| e.to_string())?;
|
||||
client.set_zapper(nwc).await;
|
||||
|
||||
@ -327,38 +327,42 @@ pub async fn set_nwc(uri: &str, state: State<'_, Nostr>) -> Result<bool, String>
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn load_nwc(state: State<'_, Nostr>) -> Result<bool, String> {
|
||||
pub async fn load_wallet(state: State<'_, Nostr>) -> Result<String, String> {
|
||||
let client = &state.client;
|
||||
let keyring = Entry::new("Lume Secret Storage", "NWC").map_err(|e| e.to_string())?;
|
||||
let keyring = Entry::new("Lume Secret", "Bitcoin Connect").unwrap();
|
||||
|
||||
match keyring.get_password() {
|
||||
Ok(val) => {
|
||||
let uri = NostrWalletConnectURI::from_str(&val).map_err(|e| e.to_string())?;
|
||||
let uri = NostrWalletConnectURI::from_str(&val).unwrap();
|
||||
let nwc = NWC::new(uri);
|
||||
|
||||
// Get current balance
|
||||
let balance = nwc.get_balance().await;
|
||||
|
||||
// Update zapper
|
||||
client.set_zapper(nwc).await;
|
||||
|
||||
Ok(true)
|
||||
match balance {
|
||||
Ok(val) => Ok(val.to_string()),
|
||||
Err(_) => Err("Get balance failed.".into()),
|
||||
}
|
||||
}
|
||||
Err(_) => Ok(false),
|
||||
Err(_) => Err("NWC not found.".into()),
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
#[specta::specta]
|
||||
pub async fn get_balance() -> Result<String, String> {
|
||||
let keyring = Entry::new("Lume Secret Storage", "NWC").map_err(|e| e.to_string())?;
|
||||
pub async fn remove_wallet(state: State<'_, Nostr>) -> Result<(), ()> {
|
||||
let client = &state.client;
|
||||
let keyring = Entry::new("Lume Secret", "Bitcoin Connect").unwrap();
|
||||
|
||||
match keyring.get_password() {
|
||||
Ok(val) => {
|
||||
let uri = NostrWalletConnectURI::from_str(&val).map_err(|e| e.to_string())?;
|
||||
let nwc = NWC::new(uri);
|
||||
nwc
|
||||
.get_balance()
|
||||
.await
|
||||
.map(|balance| balance.to_string())
|
||||
.map_err(|_| "Get balance failed".into())
|
||||
match keyring.delete_password() {
|
||||
Ok(_) => {
|
||||
client.unset_zapper().await;
|
||||
Ok(())
|
||||
}
|
||||
Err(_) => Err("Something wrong".into()),
|
||||
Err(_) => Err(()),
|
||||
}
|
||||
}
|
||||
|
||||
@ -371,32 +375,28 @@ pub async fn zap_profile(
|
||||
state: State<'_, Nostr>,
|
||||
) -> Result<bool, String> {
|
||||
let client = &state.client;
|
||||
let public_key: Option<PublicKey> = match Nip19::from_bech32(id) {
|
||||
let public_key = match Nip19::from_bech32(id) {
|
||||
Ok(val) => match val {
|
||||
Nip19::Pubkey(key) => Some(key),
|
||||
Nip19::Profile(profile) => Some(profile.public_key),
|
||||
_ => None,
|
||||
Nip19::Pubkey(key) => key,
|
||||
Nip19::Profile(profile) => profile.public_key,
|
||||
_ => return Err("Public Key is not valid.".into()),
|
||||
},
|
||||
Err(_) => match PublicKey::from_str(id) {
|
||||
Ok(val) => Some(val),
|
||||
Err(_) => None,
|
||||
Ok(val) => val,
|
||||
Err(_) => return Err("Public Key is not valid.".into()),
|
||||
},
|
||||
};
|
||||
|
||||
if let Some(recipient) = public_key {
|
||||
let details = ZapDetails::new(ZapType::Public).message(message);
|
||||
let num = match amount.parse::<u64>() {
|
||||
Ok(val) => val,
|
||||
Err(_) => return Err("Invalid amount.".into()),
|
||||
};
|
||||
let details = ZapDetails::new(ZapType::Private).message(message);
|
||||
let num = match amount.parse::<u64>() {
|
||||
Ok(val) => val,
|
||||
Err(_) => return Err("Invalid amount.".into()),
|
||||
};
|
||||
|
||||
if client.zap(recipient, num, Some(details)).await.is_ok() {
|
||||
Ok(true)
|
||||
} else {
|
||||
Err("Zap profile failed".into())
|
||||
}
|
||||
if client.zap(public_key, num, Some(details)).await.is_ok() {
|
||||
Ok(true)
|
||||
} else {
|
||||
Err("Parse public key failed".into())
|
||||
Err("Zap profile failed".into())
|
||||
}
|
||||
}
|
||||
|
||||
@ -409,32 +409,28 @@ pub async fn zap_event(
|
||||
state: State<'_, Nostr>,
|
||||
) -> Result<bool, String> {
|
||||
let client = &state.client;
|
||||
let event_id: Option<EventId> = match Nip19::from_bech32(id) {
|
||||
let event_id = match Nip19::from_bech32(id) {
|
||||
Ok(val) => match val {
|
||||
Nip19::EventId(id) => Some(id),
|
||||
Nip19::Event(event) => Some(event.event_id),
|
||||
_ => None,
|
||||
Nip19::EventId(id) => id,
|
||||
Nip19::Event(event) => event.event_id,
|
||||
_ => return Err("Event ID is invalid.".into()),
|
||||
},
|
||||
Err(_) => match EventId::from_hex(id) {
|
||||
Ok(val) => Some(val),
|
||||
Err(_) => None,
|
||||
Ok(val) => val,
|
||||
Err(_) => return Err("Event ID is invalid.".into()),
|
||||
},
|
||||
};
|
||||
|
||||
if let Some(recipient) = event_id {
|
||||
let details = ZapDetails::new(ZapType::Public).message(message);
|
||||
let num = match amount.parse::<u64>() {
|
||||
Ok(val) => val,
|
||||
Err(_) => return Err("Invalid amount.".into()),
|
||||
};
|
||||
let details = ZapDetails::new(ZapType::Private).message(message);
|
||||
let num = match amount.parse::<u64>() {
|
||||
Ok(val) => val,
|
||||
Err(_) => return Err("Invalid amount.".into()),
|
||||
};
|
||||
|
||||
if client.zap(recipient, num, Some(details)).await.is_ok() {
|
||||
Ok(true)
|
||||
} else {
|
||||
Err("Zap event failed".into())
|
||||
}
|
||||
if client.zap(event_id, num, Some(details)).await.is_ok() {
|
||||
Ok(true)
|
||||
} else {
|
||||
Err("Parse event ID failed".into())
|
||||
Err("Zap event failed".into())
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user