Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1be84f3139 | ||
|
|
04bde7dd43 | ||
|
|
f1504d99ac | ||
|
|
e928f2ee37 | ||
|
|
ccab78ca11 | ||
|
|
5b13c3e45c | ||
|
|
e6b97ab9ae | ||
|
|
bb6badfed6 | ||
|
|
0c4b309a11 | ||
|
|
f4400d711f | ||
|
|
a3e46aa96b | ||
|
|
a4fdcfdf0b | ||
|
|
25d07303a3 |
@@ -26,6 +26,7 @@
|
|||||||
"i18next-resources-to-backend": "^1.2.0",
|
"i18next-resources-to-backend": "^1.2.0",
|
||||||
"nostr-tools": "^2.3.1",
|
"nostr-tools": "^2.3.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
|
"react-currency-input-field": "^3.8.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-i18next": "^14.0.5",
|
"react-i18next": "^14.0.5",
|
||||||
"slate": "^0.101.5",
|
"slate": "^0.101.5",
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ input::-ms-clear {
|
|||||||
}
|
}
|
||||||
|
|
||||||
media-controller {
|
media-controller {
|
||||||
@apply w-full;
|
@apply w-full overflow-hidden rounded-xl;
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer utilities {
|
@layer utilities {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { locale, platform } from "@tauri-apps/plugin-os";
|
|||||||
import { PersistQueryClientProvider } from "@tanstack/react-query-persist-client";
|
import { PersistQueryClientProvider } from "@tanstack/react-query-persist-client";
|
||||||
import { createSyncStoragePersister } from "@tanstack/query-sync-storage-persister";
|
import { createSyncStoragePersister } from "@tanstack/query-sync-storage-persister";
|
||||||
import { routeTree } from "./router.gen"; // auto generated file
|
import { routeTree } from "./router.gen"; // auto generated file
|
||||||
|
import { CancelCircleIcon, CheckCircleIcon, InfoCircleIcon } from "@lume/icons";
|
||||||
|
|
||||||
const queryClient = new QueryClient({
|
const queryClient = new QueryClient({
|
||||||
defaultOptions: {
|
defaultOptions: {
|
||||||
@@ -72,7 +73,16 @@ if (!rootElement.innerHTML) {
|
|||||||
persistOptions={{ persister }}
|
persistOptions={{ persister }}
|
||||||
>
|
>
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<Toaster position="top-center" richColors />
|
<Toaster
|
||||||
|
position="bottom-right"
|
||||||
|
icons={{
|
||||||
|
success: <CheckCircleIcon className="size-5" />,
|
||||||
|
info: <InfoCircleIcon className="size-5" />,
|
||||||
|
error: <CancelCircleIcon className="size-5" />,
|
||||||
|
}}
|
||||||
|
closeButton
|
||||||
|
theme="system"
|
||||||
|
/>
|
||||||
<App />
|
<App />
|
||||||
</StrictMode>
|
</StrictMode>
|
||||||
</PersistQueryClientProvider>
|
</PersistQueryClientProvider>
|
||||||
|
|||||||
@@ -45,7 +45,8 @@ function Inactive({ pubkey }: { pubkey: string }) {
|
|||||||
|
|
||||||
const changeAccount = async (npub: string) => {
|
const changeAccount = async (npub: string) => {
|
||||||
const select = await ark.load_selected_account(npub);
|
const select = await ark.load_selected_account(npub);
|
||||||
if (select) navigate({ to: "/$account/home", params: { account: npub } });
|
if (select)
|
||||||
|
navigate({ to: "/$account/home/local", params: { account: npub } });
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -60,13 +61,17 @@ function Inactive({ pubkey }: { pubkey: string }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function Active({ pubkey }: { pubkey: string }) {
|
function Active({ pubkey }: { pubkey: string }) {
|
||||||
const [open, setOpen] = useState(true);
|
const ark = useArk();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
// @ts-ignore, magic !!!
|
// @ts-ignore, magic !!!
|
||||||
const { guest } = useSearch({ strict: false });
|
const { guest } = useSearch({ strict: false });
|
||||||
|
// @ts-ignore, magic !!!
|
||||||
|
const { account } = useParams({ strict: false });
|
||||||
|
|
||||||
if (guest) {
|
if (guest) {
|
||||||
return (
|
return (
|
||||||
<Popover.Root open={open} onOpenChange={setOpen}>
|
<Popover.Root open={true}>
|
||||||
<Popover.Trigger asChild>
|
<Popover.Trigger asChild>
|
||||||
<button type="button">
|
<button type="button">
|
||||||
<User.Provider pubkey={pubkey}>
|
<User.Provider pubkey={pubkey}>
|
||||||
@@ -119,23 +124,20 @@ function Active({ pubkey }: { pubkey: string }) {
|
|||||||
>
|
>
|
||||||
<DropdownMenu.Item className="group relative flex h-9 select-none items-center rounded-md px-3 text-sm font-medium leading-none outline-none hover:bg-neutral-900 dark:hover:bg-neutral-100">
|
<DropdownMenu.Item className="group relative flex h-9 select-none items-center rounded-md px-3 text-sm font-medium leading-none outline-none hover:bg-neutral-900 dark:hover:bg-neutral-100">
|
||||||
Add account
|
Add account
|
||||||
<div className="ml-auto pl-5 text-xs text-neutral-800 dark:text-neutral-200">
|
|
||||||
⌘+Shift+N
|
|
||||||
</div>
|
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
<DropdownMenu.Item className="group relative flex h-9 select-none items-center rounded-md px-3 text-sm font-medium leading-none outline-none hover:bg-neutral-900 dark:hover:bg-neutral-100">
|
<DropdownMenu.Item
|
||||||
|
onClick={() => ark.open_profile(account)}
|
||||||
|
className="group relative flex h-9 select-none items-center rounded-md px-3 text-sm font-medium leading-none outline-none hover:bg-neutral-900 dark:hover:bg-neutral-100"
|
||||||
|
>
|
||||||
Profile
|
Profile
|
||||||
<div className="ml-auto pl-5 text-xs text-neutral-800 dark:text-neutral-200">
|
<div className="ml-auto pl-5 text-xs text-neutral-800 dark:text-neutral-200">
|
||||||
⌘+Shift+P
|
⌘+Shift+P
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenu.Item>
|
</DropdownMenu.Item>
|
||||||
<DropdownMenu.Item className="group relative flex h-9 select-none items-center rounded-md px-3 text-sm font-medium leading-none outline-none hover:bg-neutral-900 dark:hover:bg-neutral-100">
|
<DropdownMenu.Item
|
||||||
Settings
|
onClick={() => navigate({ to: "/", search: { manually: true } })}
|
||||||
<div className="ml-auto pl-5 text-xs text-neutral-800 dark:text-neutral-200">
|
className="group relative flex h-9 select-none items-center rounded-md px-3 text-sm font-medium leading-none outline-none hover:bg-neutral-900 dark:hover:bg-neutral-100"
|
||||||
⌘+Shift+S
|
>
|
||||||
</div>
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
<DropdownMenu.Item className="group relative flex h-9 select-none items-center rounded-md px-3 text-sm font-medium leading-none outline-none hover:bg-neutral-900 dark:hover:bg-neutral-100">
|
|
||||||
Logout
|
Logout
|
||||||
<div className="ml-auto pl-5 text-xs text-neutral-800 dark:text-neutral-200">
|
<div className="ml-auto pl-5 text-xs text-neutral-800 dark:text-neutral-200">
|
||||||
⌘+Shift+L
|
⌘+Shift+L
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ export function BackupDialog() {
|
|||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<Link
|
<Link
|
||||||
to="/$account/home"
|
to="/$account/home/local"
|
||||||
params={{ account }}
|
params={{ account }}
|
||||||
search={{ guest: false }}
|
search={{ guest: false }}
|
||||||
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"
|
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"
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { useArk } from "@lume/ark";
|
||||||
|
import { User } from "@lume/ui";
|
||||||
|
import { getBitcoinDisplayValues } from "@lume/utils";
|
||||||
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
export function Balance({
|
||||||
|
recipient,
|
||||||
|
account,
|
||||||
|
}: {
|
||||||
|
recipient: string;
|
||||||
|
account: string;
|
||||||
|
}) {
|
||||||
|
const [t] = useTranslation();
|
||||||
|
const [balance, setBalance] = useState(0);
|
||||||
|
|
||||||
|
const ark = useArk();
|
||||||
|
const value = useMemo(() => getBitcoinDisplayValues(balance), [balance]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function getBalance() {
|
||||||
|
const val = await ark.get_balance();
|
||||||
|
setBalance(val);
|
||||||
|
}
|
||||||
|
|
||||||
|
getBalance();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-tauri-drag-region
|
||||||
|
className="flex h-16 items-center justify-end px-3"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="text-end">
|
||||||
|
<div className="text-sm leading-tight text-neutral-700 dark:text-neutral-300">
|
||||||
|
Your balance
|
||||||
|
</div>
|
||||||
|
<div className="font-medium leading-tight">
|
||||||
|
₿ {value.bitcoinFormatted}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<User.Provider pubkey={account}>
|
||||||
|
<User.Root>
|
||||||
|
<User.Avatar className="size-9 rounded-full" />
|
||||||
|
</User.Root>
|
||||||
|
</User.Provider>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -11,11 +11,16 @@ export function LoginDialog() {
|
|||||||
|
|
||||||
const [nsec, setNsec] = useState("");
|
const [nsec, setNsec] = useState("");
|
||||||
const [passphase, setPassphase] = useState("");
|
const [passphase, setPassphase] = useState("");
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
|
|
||||||
const login = async () => {
|
const login = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
if (!nsec.length) {
|
||||||
|
return toast.info("You must enter a valid nsec or ncrypto");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nsec.startsWith("ncrypto") && !passphase.length) {
|
||||||
|
return toast.warning("You must provide a passphase for ncrypto key");
|
||||||
|
}
|
||||||
|
|
||||||
const save = await ark.save_account(nsec, passphase);
|
const save = await ark.save_account(nsec, passphase);
|
||||||
|
|
||||||
@@ -23,7 +28,6 @@ export function LoginDialog() {
|
|||||||
navigate({ to: "/", search: { guest: false } });
|
navigate({ to: "/", search: { guest: false } });
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setLoading(false);
|
|
||||||
toast.error(String(e));
|
toast.error(String(e));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -85,6 +89,7 @@ export function LoginDialog() {
|
|||||||
<input
|
<input
|
||||||
name="nsec"
|
name="nsec"
|
||||||
type="text"
|
type="text"
|
||||||
|
placeholder="nsec or ncrypto..."
|
||||||
value={nsec}
|
value={nsec}
|
||||||
onChange={(e) => setNsec(e.target.value)}
|
onChange={(e) => setNsec(e.target.value)}
|
||||||
className="h-11 w-full resize-none rounded-lg border-transparent bg-neutral-100 placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-100 dark:bg-neutral-900 dark:focus:ring-blue-900"
|
className="h-11 w-full resize-none rounded-lg border-transparent bg-neutral-100 placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-100 dark:bg-neutral-900 dark:focus:ring-blue-900"
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ function App() {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
onClick={() => ark.open_settings()}
|
||||||
className="inline-flex size-8 items-center justify-center rounded-full bg-neutral-200 text-neutral-800 hover:bg-neutral-400 dark:bg-neutral-800 dark:text-neutral-200 dark:hover:bg-neutral-600"
|
className="inline-flex size-8 items-center justify-center rounded-full bg-neutral-200 text-neutral-800 hover:bg-neutral-400 dark:bg-neutral-800 dark:text-neutral-200 dark:hover:bg-neutral-600"
|
||||||
>
|
>
|
||||||
<HorizontalDotsIcon className="size-5" />
|
<HorizontalDotsIcon className="size-5" />
|
||||||
@@ -68,7 +69,7 @@ function Navigation() {
|
|||||||
data-tauri-drag-region
|
data-tauri-drag-region
|
||||||
className="flex h-full flex-1 items-center gap-2"
|
className="flex h-full flex-1 items-center gap-2"
|
||||||
>
|
>
|
||||||
<Link to="/$account/home" params={{ account }}>
|
<Link to="/$account/home/local" params={{ account }}>
|
||||||
{({ isActive }) => (
|
{({ isActive }) => (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|||||||
@@ -1,133 +0,0 @@
|
|||||||
import { RepostNote } from "@/components/repost";
|
|
||||||
import { Suggest } from "@/components/suggest";
|
|
||||||
import { TextNote } from "@/components/text";
|
|
||||||
import { useArk } from "@lume/ark";
|
|
||||||
import {
|
|
||||||
LoaderIcon,
|
|
||||||
ArrowRightCircleIcon,
|
|
||||||
RefreshIcon,
|
|
||||||
InfoIcon,
|
|
||||||
} from "@lume/icons";
|
|
||||||
import { Event, Kind } from "@lume/types";
|
|
||||||
import { EmptyFeed } from "@lume/ui";
|
|
||||||
import { FETCH_LIMIT } from "@lume/utils";
|
|
||||||
import { useInfiniteQuery } from "@tanstack/react-query";
|
|
||||||
import { createLazyFileRoute } from "@tanstack/react-router";
|
|
||||||
import { Virtualizer } from "virtua";
|
|
||||||
|
|
||||||
export const Route = createLazyFileRoute("/$account/home")({
|
|
||||||
component: Home,
|
|
||||||
});
|
|
||||||
|
|
||||||
function Home() {
|
|
||||||
const ark = useArk();
|
|
||||||
const currentDate = new Date().toLocaleString("default", {
|
|
||||||
weekday: "long",
|
|
||||||
month: "long",
|
|
||||||
day: "numeric",
|
|
||||||
});
|
|
||||||
|
|
||||||
const { account } = Route.useParams();
|
|
||||||
const {
|
|
||||||
data,
|
|
||||||
hasNextPage,
|
|
||||||
isLoading,
|
|
||||||
isRefetching,
|
|
||||||
isFetchingNextPage,
|
|
||||||
fetchNextPage,
|
|
||||||
refetch,
|
|
||||||
} = useInfiniteQuery({
|
|
||||||
queryKey: ["local_newsfeed", account],
|
|
||||||
initialPageParam: 0,
|
|
||||||
queryFn: async ({ pageParam }: { pageParam: number }) => {
|
|
||||||
const events = await ark.get_events(
|
|
||||||
"local",
|
|
||||||
FETCH_LIMIT,
|
|
||||||
pageParam,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
return events;
|
|
||||||
},
|
|
||||||
getNextPageParam: (lastPage) => {
|
|
||||||
const lastEvent = lastPage?.at(-1);
|
|
||||||
if (!lastEvent) return;
|
|
||||||
return lastEvent.created_at - 1;
|
|
||||||
},
|
|
||||||
select: (data) => data?.pages.flatMap((page) => page),
|
|
||||||
refetchOnWindowFocus: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const renderItem = (event: Event) => {
|
|
||||||
if (!event) return;
|
|
||||||
switch (event.kind) {
|
|
||||||
case Kind.Repost:
|
|
||||||
return <RepostNote key={event.id} event={event} />;
|
|
||||||
default:
|
|
||||||
return <TextNote key={event.id} event={event} />;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col gap-3">
|
|
||||||
<div className="mx-auto flex h-12 w-full max-w-xl shrink-0 items-center justify-between border-b border-neutral-100 dark:border-neutral-900">
|
|
||||||
<h3 className="text-sm font-medium uppercase leading-tight text-neutral-600 dark:text-neutral-400">
|
|
||||||
{currentDate}
|
|
||||||
</h3>
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => refetch()}
|
|
||||||
className="text-neutral-700 hover:text-blue-500 dark:text-neutral-300"
|
|
||||||
>
|
|
||||||
<RefreshIcon className="size-4" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="mx-auto flex w-full max-w-xl flex-1 flex-col">
|
|
||||||
<div className="flex-1">
|
|
||||||
{isLoading || isRefetching ? (
|
|
||||||
<div className="flex h-20 w-full flex-col items-center justify-center gap-1">
|
|
||||||
<LoaderIcon className="size-5 animate-spin" />
|
|
||||||
</div>
|
|
||||||
) : !data.length ? (
|
|
||||||
<div className="flex flex-col gap-3">
|
|
||||||
<div className="flex items-center gap-2 rounded-xl bg-neutral-100 p-3 dark:bg-neutral-900">
|
|
||||||
<InfoIcon className="size-5" />
|
|
||||||
<p>
|
|
||||||
Empty newsfeed. Or you can go to{" "}
|
|
||||||
<a href="" className="text-blue-500 hover:text-blue-600">
|
|
||||||
Discover
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<Suggest />
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<Virtualizer overscan={3}>
|
|
||||||
{data.map((item) => renderItem(item))}
|
|
||||||
</Virtualizer>
|
|
||||||
)}
|
|
||||||
<div className="flex h-20 items-center justify-center">
|
|
||||||
{hasNextPage ? (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => fetchNextPage()}
|
|
||||||
disabled={!hasNextPage || isFetchingNextPage}
|
|
||||||
className="inline-flex h-12 w-36 items-center justify-center gap-2 rounded-full bg-neutral-100 px-3 font-medium hover:bg-neutral-200 focus:outline-none dark:bg-neutral-900 dark:hover:bg-neutral-800"
|
|
||||||
>
|
|
||||||
{isFetchingNextPage ? (
|
|
||||||
<LoaderIcon className="size-5 animate-spin" />
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<ArrowRightCircleIcon className="size-5" />
|
|
||||||
Load more
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import { GlobalIcon, LoaderIcon, LocalIcon, RefreshIcon } from "@lume/icons";
|
||||||
|
import { cn } from "@lume/utils";
|
||||||
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { Link } from "@tanstack/react-router";
|
||||||
|
import { Outlet, createFileRoute } from "@tanstack/react-router";
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/$account/home")({
|
||||||
|
component: Screen,
|
||||||
|
});
|
||||||
|
|
||||||
|
function Screen() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const { account } = Route.useParams();
|
||||||
|
|
||||||
|
const refresh = async () => {
|
||||||
|
const queryKey = `${window.location.pathname.split("/").at(-1)}_newsfeed`;
|
||||||
|
await queryClient.refetchQueries({ queryKey: [queryKey, account] });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="mx-auto mb-4 flex h-16 w-full max-w-xl shrink-0 items-center justify-between border-b border-neutral-100 dark:border-neutral-900">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Link to="/$account/home/local">
|
||||||
|
{({ isActive }) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"inline-flex items-center justify-center gap-2 rounded-full px-4 py-2 text-sm leading-tight hover:bg-neutral-100 dark:hover:bg-neutral-900",
|
||||||
|
isActive
|
||||||
|
? "bg-neutral-100 font-semibold text-neutral-900 hover:bg-neutral-200 dark:bg-neutral-900 dark:text-neutral-100 dark:hover:bg-neutral-800"
|
||||||
|
: "text-neutral-600 dark:text-neutral-400",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<LocalIcon className="size-4" />
|
||||||
|
Local
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
<Link to="/$account/home/global">
|
||||||
|
{({ isActive }) => (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"inline-flex items-center justify-center gap-2 rounded-full px-4 py-2 text-sm leading-tight hover:bg-neutral-100 dark:hover:bg-neutral-900",
|
||||||
|
isActive
|
||||||
|
? "bg-neutral-100 font-semibold text-neutral-900 hover:bg-neutral-200 dark:bg-neutral-900 dark:text-neutral-100 dark:hover:bg-neutral-800"
|
||||||
|
: "text-neutral-600 dark:text-neutral-400",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<GlobalIcon className="size-4" />
|
||||||
|
Global
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={refresh}
|
||||||
|
className="text-neutral-700 hover:text-blue-500 dark:text-neutral-300"
|
||||||
|
>
|
||||||
|
<RefreshIcon className="size-4" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import { RepostNote } from "@/components/repost";
|
||||||
|
import { Suggest } from "@/components/suggest";
|
||||||
|
import { TextNote } from "@/components/text";
|
||||||
|
import { useArk } from "@lume/ark";
|
||||||
|
import { LoaderIcon, ArrowRightCircleIcon, InfoIcon } from "@lume/icons";
|
||||||
|
import { Event, Kind } from "@lume/types";
|
||||||
|
import { FETCH_LIMIT } from "@lume/utils";
|
||||||
|
import { useInfiniteQuery } from "@tanstack/react-query";
|
||||||
|
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||||
|
import { Virtualizer } from "virtua";
|
||||||
|
|
||||||
|
export const Route = createLazyFileRoute("/$account/home/global")({
|
||||||
|
component: Screen,
|
||||||
|
});
|
||||||
|
|
||||||
|
function Screen() {
|
||||||
|
const ark = useArk();
|
||||||
|
const { account } = Route.useParams();
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
hasNextPage,
|
||||||
|
isLoading,
|
||||||
|
isRefetching,
|
||||||
|
isFetchingNextPage,
|
||||||
|
fetchNextPage,
|
||||||
|
} = useInfiniteQuery({
|
||||||
|
queryKey: ["global_newsfeed", account],
|
||||||
|
initialPageParam: 0,
|
||||||
|
queryFn: async ({ pageParam }: { pageParam: number }) => {
|
||||||
|
const events = await ark.get_events(
|
||||||
|
"global",
|
||||||
|
FETCH_LIMIT,
|
||||||
|
pageParam,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
return events;
|
||||||
|
},
|
||||||
|
getNextPageParam: (lastPage) => {
|
||||||
|
const lastEvent = lastPage?.at(-1);
|
||||||
|
if (!lastEvent) return;
|
||||||
|
return lastEvent.created_at - 1;
|
||||||
|
},
|
||||||
|
select: (data) => data?.pages.flatMap((page) => page),
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const renderItem = (event: Event) => {
|
||||||
|
if (!event) return;
|
||||||
|
switch (event.kind) {
|
||||||
|
case Kind.Repost:
|
||||||
|
return <RepostNote key={event.id} event={event} />;
|
||||||
|
default:
|
||||||
|
return <TextNote key={event.id} event={event} />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx-auto flex w-full max-w-xl flex-1 flex-col">
|
||||||
|
<div className="flex-1">
|
||||||
|
{isLoading || isRefetching ? (
|
||||||
|
<div className="flex h-20 w-full flex-col items-center justify-center gap-1">
|
||||||
|
<LoaderIcon className="size-5 animate-spin" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Virtualizer overscan={3}>
|
||||||
|
{data.map((item) => renderItem(item))}
|
||||||
|
</Virtualizer>
|
||||||
|
)}
|
||||||
|
<div className="flex h-20 items-center justify-center">
|
||||||
|
{hasNextPage ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => fetchNextPage()}
|
||||||
|
disabled={!hasNextPage || isFetchingNextPage}
|
||||||
|
className="inline-flex h-12 w-36 items-center justify-center gap-2 rounded-full bg-neutral-100 px-3 font-medium hover:bg-neutral-200 focus:outline-none dark:bg-neutral-900 dark:hover:bg-neutral-800"
|
||||||
|
>
|
||||||
|
{isFetchingNextPage ? (
|
||||||
|
<LoaderIcon className="size-5 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<ArrowRightCircleIcon className="size-5" />
|
||||||
|
Load more
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
import { RepostNote } from "@/components/repost";
|
||||||
|
import { Suggest } from "@/components/suggest";
|
||||||
|
import { TextNote } from "@/components/text";
|
||||||
|
import { useArk } from "@lume/ark";
|
||||||
|
import { LoaderIcon, ArrowRightCircleIcon, InfoIcon } from "@lume/icons";
|
||||||
|
import { Event, Kind } from "@lume/types";
|
||||||
|
import { FETCH_LIMIT } from "@lume/utils";
|
||||||
|
import { useInfiniteQuery } from "@tanstack/react-query";
|
||||||
|
import { Link } from "@tanstack/react-router";
|
||||||
|
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||||
|
import { Virtualizer } from "virtua";
|
||||||
|
|
||||||
|
export const Route = createLazyFileRoute("/$account/home/local")({
|
||||||
|
component: Screen,
|
||||||
|
});
|
||||||
|
|
||||||
|
function Screen() {
|
||||||
|
const ark = useArk();
|
||||||
|
const { account } = Route.useParams();
|
||||||
|
const {
|
||||||
|
data,
|
||||||
|
hasNextPage,
|
||||||
|
isLoading,
|
||||||
|
isRefetching,
|
||||||
|
isFetchingNextPage,
|
||||||
|
fetchNextPage,
|
||||||
|
} = useInfiniteQuery({
|
||||||
|
queryKey: ["local_newsfeed", account],
|
||||||
|
initialPageParam: 0,
|
||||||
|
queryFn: async ({ pageParam }: { pageParam: number }) => {
|
||||||
|
const events = await ark.get_events(
|
||||||
|
"local",
|
||||||
|
FETCH_LIMIT,
|
||||||
|
pageParam,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
return events;
|
||||||
|
},
|
||||||
|
getNextPageParam: (lastPage) => {
|
||||||
|
const lastEvent = lastPage?.at(-1);
|
||||||
|
if (!lastEvent) return;
|
||||||
|
return lastEvent.created_at - 1;
|
||||||
|
},
|
||||||
|
select: (data) => data?.pages.flatMap((page) => page),
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const renderItem = (event: Event) => {
|
||||||
|
if (!event) return;
|
||||||
|
switch (event.kind) {
|
||||||
|
case Kind.Repost:
|
||||||
|
return <RepostNote key={event.id} event={event} />;
|
||||||
|
default:
|
||||||
|
return <TextNote key={event.id} event={event} />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx-auto flex w-full max-w-xl flex-1 flex-col">
|
||||||
|
<div className="flex-1">
|
||||||
|
{isLoading || isRefetching ? (
|
||||||
|
<div className="flex h-20 w-full flex-col items-center justify-center gap-1">
|
||||||
|
<LoaderIcon className="size-5 animate-spin" />
|
||||||
|
</div>
|
||||||
|
) : !data.length ? (
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<div className="flex items-center gap-2 rounded-xl bg-neutral-50 p-5 dark:bg-neutral-950">
|
||||||
|
<InfoIcon className="size-6" />
|
||||||
|
<p>
|
||||||
|
Empty newsfeed. Or you view the{" "}
|
||||||
|
<Link
|
||||||
|
to="/$account/home/global"
|
||||||
|
className="text-blue-500 hover:text-blue-600"
|
||||||
|
>
|
||||||
|
Global Newsfeed
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Suggest />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Virtualizer overscan={3}>
|
||||||
|
{data.map((item) => renderItem(item))}
|
||||||
|
</Virtualizer>
|
||||||
|
)}
|
||||||
|
<div className="flex h-20 items-center justify-center">
|
||||||
|
{hasNextPage ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => fetchNextPage()}
|
||||||
|
disabled={!hasNextPage || isFetchingNextPage}
|
||||||
|
className="inline-flex h-12 w-36 items-center justify-center gap-2 rounded-full bg-neutral-100 px-3 font-medium hover:bg-neutral-200 focus:outline-none dark:bg-neutral-900 dark:hover:bg-neutral-800"
|
||||||
|
>
|
||||||
|
{isFetchingNextPage ? (
|
||||||
|
<LoaderIcon className="size-5 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<ArrowRightCircleIcon className="size-5" />
|
||||||
|
Load more
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -26,7 +26,7 @@ function Create() {
|
|||||||
try {
|
try {
|
||||||
await ark.save_account(keys);
|
await ark.save_account(keys);
|
||||||
navigate({
|
navigate({
|
||||||
to: "/$account/home",
|
to: "/$account/home/local",
|
||||||
params: { account: keys.npub },
|
params: { account: keys.npub },
|
||||||
search: { onboarding: true },
|
search: { onboarding: true },
|
||||||
replace: true,
|
replace: true,
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ function Import() {
|
|||||||
nsec: key,
|
nsec: key,
|
||||||
});
|
});
|
||||||
navigate({
|
navigate({
|
||||||
to: "/$account/home",
|
to: "/$account/home/local",
|
||||||
params: { account: npub },
|
params: { account: npub },
|
||||||
search: { onboarding: true },
|
search: { onboarding: true },
|
||||||
replace: true,
|
replace: true,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { createFileRoute, redirect, useNavigate } from "@tanstack/react-router";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
export const Route = createFileRoute("/")({
|
export const Route = createFileRoute("/")({
|
||||||
beforeLoad: async ({ context }) => {
|
beforeLoad: async ({ search, context }) => {
|
||||||
const ark = context.ark;
|
const ark = context.ark;
|
||||||
const accounts = await ark.get_all_accounts();
|
const accounts = await ark.get_all_accounts();
|
||||||
|
|
||||||
@@ -15,18 +15,22 @@ export const Route = createFileRoute("/")({
|
|||||||
case 0:
|
case 0:
|
||||||
const guest = await ark.create_guest_account();
|
const guest = await ark.create_guest_account();
|
||||||
throw redirect({
|
throw redirect({
|
||||||
to: "/$account/home",
|
to: "/$account/home/local",
|
||||||
params: { account: guest },
|
params: { account: guest },
|
||||||
search: { guest: true },
|
search: { guest: true },
|
||||||
replace: true,
|
replace: true,
|
||||||
});
|
});
|
||||||
// Only 1 account, skip account selection screen
|
// Only 1 account, skip account selection screen
|
||||||
case 1:
|
case 1:
|
||||||
|
// @ts-ignore, totally fine !!!
|
||||||
|
if (search.manually) return;
|
||||||
|
|
||||||
const account = accounts[0].npub;
|
const account = accounts[0].npub;
|
||||||
const loadAccount = await ark.load_selected_account(account);
|
const loadedAccount = await ark.load_selected_account(account);
|
||||||
if (loadAccount) {
|
|
||||||
|
if (loadedAccount) {
|
||||||
throw redirect({
|
throw redirect({
|
||||||
to: "/$account/home",
|
to: "/$account/home/local",
|
||||||
params: { account },
|
params: { account },
|
||||||
replace: true,
|
replace: true,
|
||||||
});
|
});
|
||||||
@@ -50,7 +54,7 @@ function Screen() {
|
|||||||
const loadAccount = await ark.load_selected_account(npub);
|
const loadAccount = await ark.load_selected_account(npub);
|
||||||
if (loadAccount) {
|
if (loadAccount) {
|
||||||
navigate({
|
navigate({
|
||||||
to: "/$account/home",
|
to: "/$account/home/local",
|
||||||
params: { account: npub },
|
params: { account: npub },
|
||||||
replace: true,
|
replace: true,
|
||||||
});
|
});
|
||||||
@@ -72,7 +76,9 @@ function Screen() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-center gap-6">
|
<div className="flex items-center justify-center gap-6">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<LoaderIcon className="size-6 animate-spin text-white" />
|
<div className="inline-flex size-6 items-center justify-center">
|
||||||
|
<LoaderIcon className="size-6 animate-spin text-white" />
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{ark.accounts.map((account) => (
|
{ark.accounts.map((account) => (
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import { useArk } from "@lume/ark";
|
||||||
|
import { ArrowRightIcon, ZapIcon } from "@lume/icons";
|
||||||
|
import { Container } from "@lume/ui";
|
||||||
|
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
export const Route = createLazyFileRoute("/nwc")({
|
||||||
|
component: Screen,
|
||||||
|
});
|
||||||
|
|
||||||
|
function Screen() {
|
||||||
|
const ark = useArk();
|
||||||
|
|
||||||
|
const [uri, setUri] = useState("");
|
||||||
|
const [isDone, setIsDone] = useState(false);
|
||||||
|
|
||||||
|
const save = async () => {
|
||||||
|
const nwc = await ark.set_nwc(uri);
|
||||||
|
|
||||||
|
if (nwc) {
|
||||||
|
setIsDone(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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 resize-none rounded-lg border-transparent bg-white placeholder:text-neutral-600 focus:border-blue-500 focus:ring focus:ring-blue-100 dark:bg-black dark:focus:ring-blue-900"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={save}
|
||||||
|
className="inline-flex h-11 w-full items-center justify-between gap-1.5 rounded-lg bg-blue-500 px-5 font-medium text-white hover:bg-blue-600"
|
||||||
|
>
|
||||||
|
<div className="size-5" />
|
||||||
|
<div>Save & Connect</div>
|
||||||
|
<ArrowRightIcon className="size-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div>Done</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
import { TextNote } from "@/components/text";
|
import { TextNote } from "@/components/text";
|
||||||
import { RepostNote } from "@/components/repost";
|
import { RepostNote } from "@/components/repost";
|
||||||
import { useArk } from "@lume/ark";
|
import { useArk } from "@lume/ark";
|
||||||
import { ArrowRightCircleIcon, LoaderIcon } from "@lume/icons";
|
import { ArrowRightCircleIcon, InfoIcon, LoaderIcon } from "@lume/icons";
|
||||||
import { Event, Kind } from "@lume/types";
|
import { Event, Kind } from "@lume/types";
|
||||||
import { EmptyFeed } from "@lume/ui";
|
|
||||||
import { FETCH_LIMIT } from "@lume/utils";
|
import { FETCH_LIMIT } from "@lume/utils";
|
||||||
import { useInfiniteQuery } from "@tanstack/react-query";
|
import { useInfiniteQuery } from "@tanstack/react-query";
|
||||||
|
|
||||||
@@ -43,7 +42,10 @@ export function EventList({ id }: { id: string }) {
|
|||||||
<LoaderIcon className="size-5 animate-spin" />
|
<LoaderIcon className="size-5 animate-spin" />
|
||||||
</div>
|
</div>
|
||||||
) : !data.length ? (
|
) : !data.length ? (
|
||||||
<EmptyFeed />
|
<div className="flex items-center gap-2 rounded-xl bg-neutral-50 p-5 dark:bg-neutral-950">
|
||||||
|
<InfoIcon className="size-6" />
|
||||||
|
<p>Empty newsfeed.</p>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
data.map((item) => renderItem(item))
|
data.map((item) => renderItem(item))
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
import { Balance } from "@/components/balance";
|
||||||
|
import { useArk } from "@lume/ark";
|
||||||
|
import { Box, Container, User } from "@lume/ui";
|
||||||
|
import { createLazyFileRoute } from "@tanstack/react-router";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { getCurrent } from "@tauri-apps/api/webviewWindow";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import CurrencyInput from "react-currency-input-field";
|
||||||
|
|
||||||
|
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 [message, setMessage] = useState("");
|
||||||
|
const [isCompleted, setIsCompleted] = useState(false);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const ark = useArk();
|
||||||
|
|
||||||
|
const submit = async () => {
|
||||||
|
try {
|
||||||
|
// start loading
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
const val = await ark.zap_event(id, amount, message);
|
||||||
|
|
||||||
|
if (val) {
|
||||||
|
setIsCompleted(true);
|
||||||
|
const window = getCurrent();
|
||||||
|
// close current window
|
||||||
|
window.close();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
setIsLoading(false);
|
||||||
|
toast.error(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<Balance recipient={pubkey} account={account} />
|
||||||
|
<Box className="flex flex-col gap-3">
|
||||||
|
<div className="flex h-full flex-col justify-between py-5">
|
||||||
|
<div className="flex h-11 shrink-0 items-center justify-center gap-2">
|
||||||
|
{t("note.zap.modalTitle")}{" "}
|
||||||
|
<User.Provider pubkey={pubkey}>
|
||||||
|
<User.Root className="inline-flex items-center gap-2 rounded-full bg-neutral-100 p-1 dark:bg-neutral-900">
|
||||||
|
<User.Avatar className="size-6 rounded-full" />
|
||||||
|
<User.Name className="pr-2 text-sm font-medium" />
|
||||||
|
</User.Root>
|
||||||
|
</User.Provider>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-1 flex-col justify-between px-5">
|
||||||
|
<div className="relative flex flex-1 flex-col pb-8">
|
||||||
|
<div className="inline-flex h-full flex-1 items-center justify-center 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="w-full flex-1 border-none bg-transparent text-right text-4xl font-semibold placeholder:text-neutral-600 focus:outline-none focus:ring-0 dark:text-neutral-400"
|
||||||
|
/>
|
||||||
|
<span className="w-full flex-1 text-left text-4xl font-semibold 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
|
||||||
|
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 w-full flex-col gap-2">
|
||||||
|
<input
|
||||||
|
name="message"
|
||||||
|
value={message}
|
||||||
|
onChange={(e) => setMessage(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>
|
||||||
|
);
|
||||||
|
}
|
||||||
+64
-8
@@ -404,8 +404,26 @@ export class Ark {
|
|||||||
try {
|
try {
|
||||||
const cmd: boolean = await invoke("set_nwc", { uri });
|
const cmd: boolean = await invoke("set_nwc", { uri });
|
||||||
return cmd;
|
return cmd;
|
||||||
} catch {
|
} catch (e) {
|
||||||
return false;
|
throw new Error(String(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async load_nwc() {
|
||||||
|
try {
|
||||||
|
const cmd: boolean = await invoke("load_nwc");
|
||||||
|
return cmd;
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(String(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async get_balance() {
|
||||||
|
try {
|
||||||
|
const cmd: number = await invoke("get_balance");
|
||||||
|
return cmd;
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(String(e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -413,8 +431,8 @@ export class Ark {
|
|||||||
try {
|
try {
|
||||||
const cmd: boolean = await invoke("zap_profile", { id, amount, message });
|
const cmd: boolean = await invoke("zap_profile", { id, amount, message });
|
||||||
return cmd;
|
return cmd;
|
||||||
} catch {
|
} catch (e) {
|
||||||
return false;
|
throw new Error(String(e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -422,8 +440,8 @@ export class Ark {
|
|||||||
try {
|
try {
|
||||||
const cmd: boolean = await invoke("zap_event", { id, amount, message });
|
const cmd: boolean = await invoke("zap_event", { id, amount, message });
|
||||||
return cmd;
|
return cmd;
|
||||||
} catch {
|
} catch (e) {
|
||||||
return false;
|
throw new Error(String(e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -477,8 +495,7 @@ export class Ark {
|
|||||||
|
|
||||||
return content.url as string;
|
return content.url as string;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(String(e));
|
throw new Error(String(e));
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -526,4 +543,43 @@ export class Ark {
|
|||||||
fileDropEnabled: true,
|
fileDropEnabled: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public open_nwc() {
|
||||||
|
return new WebviewWindow("nwc", {
|
||||||
|
title: "Nostr Wallet Connect",
|
||||||
|
url: "/nwc",
|
||||||
|
minWidth: 400,
|
||||||
|
width: 400,
|
||||||
|
height: 600,
|
||||||
|
hiddenTitle: true,
|
||||||
|
titleBarStyle: "overlay",
|
||||||
|
fileDropEnabled: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public open_zap(id: string, pubkey: string, account: string) {
|
||||||
|
return new WebviewWindow(`zap-${id}`, {
|
||||||
|
title: "Nostr Wallet Connect",
|
||||||
|
url: `/zap/${id}?pubkey=${pubkey}&account=${account}`,
|
||||||
|
minWidth: 400,
|
||||||
|
width: 400,
|
||||||
|
height: 500,
|
||||||
|
hiddenTitle: true,
|
||||||
|
titleBarStyle: "overlay",
|
||||||
|
fileDropEnabled: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public open_settings() {
|
||||||
|
return new WebviewWindow("settings", {
|
||||||
|
title: "Settings",
|
||||||
|
url: "/settings",
|
||||||
|
minWidth: 600,
|
||||||
|
width: 800,
|
||||||
|
height: 500,
|
||||||
|
hiddenTitle: true,
|
||||||
|
titleBarStyle: "overlay",
|
||||||
|
fileDropEnabled: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -116,3 +116,7 @@ export * from "./src/arrowUp";
|
|||||||
export * from "./src/arrowUpSquare";
|
export * from "./src/arrowUpSquare";
|
||||||
export * from "./src/arrowDown";
|
export * from "./src/arrowDown";
|
||||||
export * from "./src/link";
|
export * from "./src/link";
|
||||||
|
export * from "./src/local";
|
||||||
|
export * from "./src/global";
|
||||||
|
export * from "./src/infoCircle";
|
||||||
|
export * from "./src/cancelCircle";
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
export function CancelCircleIcon(props: JSX.IntrinsicElements["svg"]) {
|
||||||
|
return (
|
||||||
|
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" {...props}>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12Zm7.707-3.707a1 1 0 0 0-1.414 1.414L10.586 12l-2.293 2.293a1 1 0 1 0 1.414 1.414L12 13.414l2.293 2.293a1 1 0 0 0 1.414-1.414L13.414 12l2.293-2.293a1 1 0 0 0-1.414-1.414L12 10.586 9.707 8.293Z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,22 +1,15 @@
|
|||||||
import { SVGProps } from 'react';
|
import { SVGProps } from "react";
|
||||||
|
|
||||||
export function CheckCircleIcon(
|
export function CheckCircleIcon(
|
||||||
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>
|
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" {...props}>
|
||||||
width={24}
|
|
||||||
height={24}
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<path
|
<path
|
||||||
fillRule="evenodd"
|
|
||||||
clipRule="evenodd"
|
|
||||||
d="M12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2ZM15.5805 9.97493C15.8428 9.65434 15.7955 9.18183 15.4749 8.91953C15.1543 8.65724 14.6818 8.70449 14.4195 9.02507L10.4443 13.8837L9.03033 12.4697C8.73744 12.1768 8.26256 12.1768 7.96967 12.4697C7.67678 12.7626 7.67678 13.2374 7.96967 13.5303L9.96967 15.5303C10.1195 15.6802 10.3257 15.7596 10.5374 15.7491C10.749 15.7385 10.9463 15.6389 11.0805 15.4749L15.5805 9.97493Z"
|
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M12 2C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2Zm3.774 8.133a1 1 0 0 0-1.548-1.266l-3.8 4.645-1.219-1.22a1 1 0 0 0-1.414 1.415l2 2a1 1 0 0 0 1.481-.074l4.5-5.5Z"
|
||||||
|
clipRule="evenodd"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
export function GlobalIcon(props: JSX.IntrinsicElements["svg"]) {
|
||||||
|
return (
|
||||||
|
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" {...props}>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M12 2a1 1 0 0 0 0 2 8 8 0 0 1 8 8 1 1 0 0 0 2 0c0-5.523-4.477-10-10-10Z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M12 5a1 1 0 0 0 0 2 5 5 0 0 1 5 5 1 1 0 0 0 2 0 7 7 0 0 0-7-7ZM7.39 6.977a2.9 2.9 0 0 0-2.258-.857c-.83.064-1.632.52-2.065 1.38-1.89 3.749-1.27 8.44 1.862 11.571 3.132 3.132 7.822 3.751 11.57 1.862a2.494 2.494 0 0 0 1.38-2.066 2.9 2.9 0 0 0-.856-2.258L12.914 12.5l.793-.793a1 1 0 0 0-1.414-1.414l-.793.793-4.11-4.11Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { SVGProps } from "react";
|
||||||
|
|
||||||
|
export function InfoCircleIcon(
|
||||||
|
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" {...props}>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M12 2C6.477 2 2 6.477 2 12s4.477 10 10 10 10-4.477 10-10S17.523 2 12 2Zm-2 9a1 1 0 0 1 1-1h1a1 1 0 0 1 1 1v5a1 1 0 1 1-2 0v-4a1 1 0 0 1-1-1Zm2-4a1 1 0 1 0 0 2 1 1 0 0 0 0-2Z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,9 +1,15 @@
|
|||||||
import { SVGProps } from 'react';
|
import { SVGProps } from "react";
|
||||||
|
|
||||||
export function LoaderIcon(props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) {
|
export function LoaderIcon(
|
||||||
|
props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>,
|
||||||
|
) {
|
||||||
return (
|
return (
|
||||||
<svg {...props} xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
<svg
|
||||||
<title id="loading">Loading</title>
|
{...props}
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
<circle
|
<circle
|
||||||
className="opacity-25"
|
className="opacity-25"
|
||||||
cx="12"
|
cx="12"
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
export function LocalIcon(props: JSX.IntrinsicElements["svg"]) {
|
||||||
|
return (
|
||||||
|
<svg width="24" height="24" fill="none" viewBox="0 0 24 24" {...props}>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M11.19 2.413a.996.996 0 0 0-.19.603V17a1 1 0 1 0 2 0v-6.426l5.504-3.21a1 1 0 0 0 0-1.728l-5.987-3.492a.995.995 0 0 0-1.007-.016.993.993 0 0 0-.32.285Z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M8.19 15.346a1 1 0 1 0-.38-1.964c-1.552.3-2.928.773-3.945 1.398C2.89 15.38 2 16.282 2 17.5c0 .858.45 1.566 1.03 2.099.58.532 1.361.965 2.244 1.308C7.044 21.596 9.423 22 12 22s4.956-.404 6.726-1.093c.883-.343 1.665-.776 2.244-1.308.58-.533 1.03-1.241 1.03-2.099 0-1.218-.89-2.12-1.865-2.72-1.017-.625-2.393-1.098-3.945-1.398a1 1 0 1 0-.38 1.964c1.412.273 2.535.681 3.278 1.138.784.482.912.86.912 1.016 0 .11-.053.322-.384.627-.332.305-.868.626-1.614.916-1.487.578-3.608.957-6.002.957-2.394 0-4.515-.379-6.002-.957-.746-.29-1.282-.611-1.614-.916C4.053 17.822 4 17.609 4 17.5c0-.155.128-.534.912-1.016.743-.457 1.866-.865 3.278-1.138Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -33,7 +33,6 @@
|
|||||||
"react-hook-form": "^7.51.0",
|
"react-hook-form": "^7.51.0",
|
||||||
"react-hotkeys-hook": "^4.5.0",
|
"react-hotkeys-hook": "^4.5.0",
|
||||||
"react-i18next": "^14.0.5",
|
"react-i18next": "^14.0.5",
|
||||||
"react-router-dom": "^6.22.2",
|
|
||||||
"react-string-replace": "^1.1.1",
|
"react-string-replace": "^1.1.1",
|
||||||
"slate": "^0.101.5",
|
"slate": "^0.101.5",
|
||||||
"slate-react": "^0.101.6",
|
"slate-react": "^0.101.6",
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export function Box({
|
|||||||
<div className="h-full w-full flex-1 px-2 pb-2">
|
<div className="h-full w-full flex-1 px-2 pb-2">
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"h-full w-full overflow-hidden overflow-y-auto rounded-xl bg-white shadow-[rgba(50,_50,_105,_0.15)_0px_2px_5px_0px,_rgba(0,_0,_0,_0.05)_0px_1px_1px_0px] dark:bg-black dark:shadow-none dark:ring-1 dark:ring-white/5",
|
"h-full w-full overflow-y-auto rounded-xl bg-white shadow-[rgba(50,_50,_105,_0.15)_0px_2px_5px_0px,_rgba(0,_0,_0,_0.05)_0px_1px_1px_0px] dark:bg-black dark:shadow-none dark:ring-1 dark:ring-white/5",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
import { ReactNode } from "react";
|
|
||||||
import { Routes } from "react-router-dom";
|
|
||||||
|
|
||||||
export function ColumnContent({ children }: { children: ReactNode }) {
|
|
||||||
return <Routes>{children}</Routes>;
|
|
||||||
}
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
import {
|
|
||||||
ChevronDownIcon,
|
|
||||||
MoveLeftIcon,
|
|
||||||
MoveRightIcon,
|
|
||||||
RefreshIcon,
|
|
||||||
TrashIcon,
|
|
||||||
} from "@lume/icons";
|
|
||||||
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
|
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
|
|
||||||
export function ColumnHeader({ queryKey }: { queryKey?: string[] }) {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
|
|
||||||
const refresh = async () => {
|
|
||||||
if (queryKey) await queryClient.refetchQueries({ queryKey });
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DropdownMenu.Root>
|
|
||||||
<div className="flex h-11 w-full shrink-0 items-center justify-center gap-2 border-b border-neutral-100 px-3 dark:border-neutral-900">
|
|
||||||
<DropdownMenu.Trigger asChild>
|
|
||||||
<div className="inline-flex items-center gap-1.5">
|
|
||||||
<div className="text-[13px] font-medium">{column.title}</div>
|
|
||||||
<ChevronDownIcon className="size-5" />
|
|
||||||
</div>
|
|
||||||
</DropdownMenu.Trigger>
|
|
||||||
<DropdownMenu.Portal>
|
|
||||||
<DropdownMenu.Content
|
|
||||||
sideOffset={5}
|
|
||||||
className="flex w-[200px] flex-col overflow-hidden rounded-2xl bg-white/50 p-2 ring-1 ring-black/10 backdrop-blur-2xl focus:outline-none dark:bg-black/50 dark:ring-white/10"
|
|
||||||
>
|
|
||||||
<DropdownMenu.Item asChild>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={refresh}
|
|
||||||
className="inline-flex h-9 items-center gap-3 rounded-lg px-3 text-sm font-medium text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
|
|
||||||
>
|
|
||||||
<RefreshIcon className="size-4" />
|
|
||||||
{t("global.refresh")}
|
|
||||||
</button>
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
<DropdownMenu.Item asChild>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => move(column.id, "left")}
|
|
||||||
className="inline-flex h-9 items-center gap-3 rounded-lg px-3 text-sm font-medium text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
|
|
||||||
>
|
|
||||||
<MoveLeftIcon className="size-4" />
|
|
||||||
{t("global.moveLeft")}
|
|
||||||
</button>
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
<DropdownMenu.Item asChild>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => move(column.id, "right")}
|
|
||||||
className="inline-flex h-9 items-center gap-3 rounded-lg px-3 text-sm font-medium text-black/70 hover:bg-black/10 hover:text-black focus:outline-none dark:text-white/70 dark:hover:bg-white/10 dark:hover:text-white"
|
|
||||||
>
|
|
||||||
<MoveRightIcon className="size-4" />
|
|
||||||
{t("global.moveRight")}
|
|
||||||
</button>
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
<DropdownMenu.Separator className="my-1 h-px bg-black/10 dark:bg-white/10" />
|
|
||||||
<DropdownMenu.Item asChild>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => remove(column.id)}
|
|
||||||
className="inline-flex h-9 items-center gap-3 rounded-lg px-3 text-sm font-medium text-red-500 hover:bg-red-500 hover:text-red-50 focus:outline-none"
|
|
||||||
>
|
|
||||||
<TrashIcon className="size-4" />
|
|
||||||
{t("global.delete")}
|
|
||||||
</button>
|
|
||||||
</DropdownMenu.Item>
|
|
||||||
</DropdownMenu.Content>
|
|
||||||
</DropdownMenu.Portal>
|
|
||||||
</div>
|
|
||||||
</DropdownMenu.Root>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import { Route } from "react-router-dom";
|
|
||||||
import { ColumnContent } from "./content";
|
|
||||||
import { ColumnHeader } from "./header";
|
|
||||||
import { ColumnLiveWidget } from "./live";
|
|
||||||
import { ColumnProvider } from "./provider";
|
|
||||||
import { ColumnRoot } from "./root";
|
|
||||||
|
|
||||||
export const Column = {
|
|
||||||
Provider: ColumnProvider,
|
|
||||||
Root: ColumnRoot,
|
|
||||||
Live: ColumnLiveWidget,
|
|
||||||
Header: ColumnHeader,
|
|
||||||
Content: ColumnContent,
|
|
||||||
Route: Route,
|
|
||||||
};
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import { ArrowUpIcon } from "@lume/icons";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
|
|
||||||
export function ColumnLiveWidget({
|
|
||||||
filter,
|
|
||||||
onClick,
|
|
||||||
}: {
|
|
||||||
filter: NDKFilter;
|
|
||||||
onClick: (event: NDKEvent[]) => void;
|
|
||||||
}) {
|
|
||||||
const ark = useArk();
|
|
||||||
const [events, setEvents] = useState<NDKEvent[]>([]);
|
|
||||||
|
|
||||||
const update = async () => {
|
|
||||||
onClick(events);
|
|
||||||
// reset
|
|
||||||
setEvents([]);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const sub = ark.subscribe({
|
|
||||||
filter,
|
|
||||||
closeOnEose: false,
|
|
||||||
cb: (event: NDKEvent) => setEvents((prev) => [...prev, event]),
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (sub) sub.stop();
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (!events.length) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="absolute left-0 z-40 flex items-center justify-center w-full top-12 h-11">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={update}
|
|
||||||
className="inline-flex items-center justify-center h-9 gap-1 pl-4 pr-3 text-sm font-semibold rounded-full w-max bg-neutral-950 dark:bg-neutral-50 hover:bg-neutral-900 dark:hover:bg-neutral-100 text-neutral-50 dark:text-neutral-950"
|
|
||||||
>
|
|
||||||
{events.length} {events.length === 1 ? "new note" : "new notes"}
|
|
||||||
<ArrowUpIcon className="size-4" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import { LumeColumn } from "@lume/types";
|
|
||||||
import { ReactNode, createContext, useContext } from "react";
|
|
||||||
|
|
||||||
const ColumnContext = createContext<LumeColumn>(null);
|
|
||||||
|
|
||||||
export function ColumnProvider({
|
|
||||||
column,
|
|
||||||
children,
|
|
||||||
}: { column: LumeColumn; children: ReactNode }) {
|
|
||||||
return (
|
|
||||||
<ColumnContext.Provider value={column}>{children}</ColumnContext.Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useColumnContext() {
|
|
||||||
const context = useContext(ColumnContext);
|
|
||||||
if (!context) {
|
|
||||||
throw new Error(
|
|
||||||
"Please import Column Provider to use useColumnContext() hook",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return context;
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
import { cn } from "@lume/utils";
|
|
||||||
import { Resizable } from "re-resizable";
|
|
||||||
import { ReactNode, useState } from "react";
|
|
||||||
import { MemoryRouter, UNSAFE_LocationContext } from "react-router-dom";
|
|
||||||
|
|
||||||
export function ColumnRoot({
|
|
||||||
children,
|
|
||||||
className,
|
|
||||||
}: {
|
|
||||||
children: ReactNode;
|
|
||||||
className?: string;
|
|
||||||
}) {
|
|
||||||
const [width, setWidth] = useState(420);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<UNSAFE_LocationContext.Provider value={null}>
|
|
||||||
<Resizable
|
|
||||||
size={{ width, height: "100%" }}
|
|
||||||
onResizeStart={(e) => e.preventDefault()}
|
|
||||||
onResizeStop={(_e, _direction, _ref, d) => {
|
|
||||||
setWidth((prevWidth) => prevWidth + d.width);
|
|
||||||
}}
|
|
||||||
minWidth={420}
|
|
||||||
maxWidth={600}
|
|
||||||
className={cn(
|
|
||||||
"relative flex flex-col border-r-2 border-neutral-50 hover:border-neutral-100 dark:border-neutral-950 dark:hover:border-neutral-900",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
enable={{ right: true }}
|
|
||||||
>
|
|
||||||
<MemoryRouter future={{ v7_startTransition: true }}>
|
|
||||||
{children}
|
|
||||||
</MemoryRouter>
|
|
||||||
</Resizable>
|
|
||||||
</UNSAFE_LocationContext.Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import { InfoIcon } from "@lume/icons";
|
|
||||||
import { cn } from "@lume/utils";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
|
|
||||||
export function EmptyFeed({
|
|
||||||
text,
|
|
||||||
subtext,
|
|
||||||
className,
|
|
||||||
}: {
|
|
||||||
text?: string;
|
|
||||||
subtext?: string;
|
|
||||||
className?: string;
|
|
||||||
}) {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"flex w-full flex-col items-center justify-center gap-2 rounded-xl bg-neutral-50 py-3 dark:bg-neutral-950",
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<InfoIcon className="size-8 text-blue-500" />
|
|
||||||
<div className="text-center">
|
|
||||||
<p className="text-lg font-semibold">
|
|
||||||
{text ? text : t("global.emptyFeedTitle")}
|
|
||||||
</p>
|
|
||||||
<p className="text-sm leading-tight">
|
|
||||||
{subtext ? subtext : t("global.emptyFeedSubtitle")}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
export * from "./user";
|
export * from "./user";
|
||||||
export * from "./note";
|
export * from "./note";
|
||||||
export * from "./column";
|
|
||||||
export * from "./emptyFeed";
|
|
||||||
|
|
||||||
// UI
|
// UI
|
||||||
export * from "./container";
|
export * from "./container";
|
||||||
|
|||||||
@@ -1,9 +1,29 @@
|
|||||||
|
import { useArk } from "@lume/ark";
|
||||||
import { ZapIcon } from "@lume/icons";
|
import { ZapIcon } from "@lume/icons";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { useNoteContext } from "../provider";
|
||||||
|
|
||||||
export function NoteZap() {
|
export function NoteZap() {
|
||||||
|
const ark = useArk();
|
||||||
|
const event = useNoteContext();
|
||||||
|
|
||||||
|
const zap = async () => {
|
||||||
|
try {
|
||||||
|
const nwc = await ark.load_nwc();
|
||||||
|
if (!nwc) {
|
||||||
|
ark.open_nwc();
|
||||||
|
} else {
|
||||||
|
ark.open_zap(event.id, event.pubkey);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
toast.error(String(e));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
onClick={zap}
|
||||||
className="group inline-flex size-7 items-center justify-center text-neutral-800 dark:text-neutral-200"
|
className="group inline-flex size-7 items-center justify-center text-neutral-800 dark:text-neutral-200"
|
||||||
>
|
>
|
||||||
<ZapIcon className="size-5 group-hover:text-blue-500" />
|
<ZapIcon className="size-5 group-hover:text-blue-500" />
|
||||||
|
|||||||
@@ -1,11 +1,5 @@
|
|||||||
import { NOSTR_MENTIONS } from "@lume/utils";
|
|
||||||
import { nanoid } from "nanoid";
|
|
||||||
import { ReactNode, useMemo } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import reactStringReplace from "react-string-replace";
|
|
||||||
import { User } from "../user";
|
import { User } from "../user";
|
||||||
import { Hashtag } from "./mentions/hashtag";
|
|
||||||
import { MentionUser } from "./mentions/user";
|
|
||||||
import { useEvent } from "@lume/ark";
|
import { useEvent } from "@lume/ark";
|
||||||
|
|
||||||
export function NoteChild({
|
export function NoteChild({
|
||||||
@@ -18,66 +12,6 @@ export function NoteChild({
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { isLoading, isError, data } = useEvent(eventId);
|
const { isLoading, isError, data } = useEvent(eventId);
|
||||||
|
|
||||||
const richContent = useMemo(() => {
|
|
||||||
if (!data) return "";
|
|
||||||
|
|
||||||
let parsedContent: string | ReactNode[] =
|
|
||||||
data.content.substring(0, 160) + "...";
|
|
||||||
|
|
||||||
const text = data.content;
|
|
||||||
const words = text.split(/( |\n)/);
|
|
||||||
|
|
||||||
const hashtags = words.filter((word) => word.startsWith("#"));
|
|
||||||
const mentions = words.filter((word) =>
|
|
||||||
NOSTR_MENTIONS.some((el) => word.startsWith(el)),
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (hashtags.length) {
|
|
||||||
for (const hashtag of hashtags) {
|
|
||||||
const regex = new RegExp(`(|^)${hashtag}\\b`, "g");
|
|
||||||
parsedContent = reactStringReplace(parsedContent, regex, () => {
|
|
||||||
return <Hashtag key={nanoid()} tag={hashtag} />;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mentions.length) {
|
|
||||||
for (const mention of mentions) {
|
|
||||||
parsedContent = reactStringReplace(
|
|
||||||
parsedContent,
|
|
||||||
mention,
|
|
||||||
(match, i) => <MentionUser key={match + i} pubkey={mention} />,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
parsedContent = reactStringReplace(
|
|
||||||
parsedContent,
|
|
||||||
/(https?:\/\/\S+)/g,
|
|
||||||
(match, i) => {
|
|
||||||
const url = new URL(match);
|
|
||||||
return (
|
|
||||||
<a
|
|
||||||
key={match + i}
|
|
||||||
href={url.toString()}
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
className="content-break font-normal text-blue-500 hover:text-blue-600"
|
|
||||||
>
|
|
||||||
{url.toString()}
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return parsedContent;
|
|
||||||
} catch (e) {
|
|
||||||
console.log(e);
|
|
||||||
return parsedContent;
|
|
||||||
}
|
|
||||||
}, [data]);
|
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="relative flex gap-3">
|
<div className="relative flex gap-3">
|
||||||
@@ -102,8 +36,8 @@ export function NoteChild({
|
|||||||
<div className="relative flex gap-3">
|
<div className="relative flex gap-3">
|
||||||
<div className="relative flex-1 rounded-xl bg-neutral-100 p-3 dark:bg-neutral-900">
|
<div className="relative flex-1 rounded-xl bg-neutral-100 p-3 dark:bg-neutral-900">
|
||||||
<div className="absolute right-0 top-[18px] h-3 w-3 -translate-y-1/2 translate-x-1/2 rotate-45 transform bg-neutral-100 dark:bg-neutral-900" />
|
<div className="absolute right-0 top-[18px] h-3 w-3 -translate-y-1/2 translate-x-1/2 rotate-45 transform bg-neutral-100 dark:bg-neutral-900" />
|
||||||
<div className="content-break mt-6 select-text leading-normal text-neutral-900 dark:text-neutral-100">
|
<div className="content-break mt-6 line-clamp-3 select-text leading-normal text-neutral-900 dark:text-neutral-100">
|
||||||
{richContent}
|
{data.content}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<User.Provider pubkey={data.pubkey}>
|
<User.Provider pubkey={data.pubkey}>
|
||||||
|
|||||||
@@ -5,28 +5,24 @@ import {
|
|||||||
NOSTR_EVENTS,
|
NOSTR_EVENTS,
|
||||||
NOSTR_MENTIONS,
|
NOSTR_MENTIONS,
|
||||||
VIDEOS,
|
VIDEOS,
|
||||||
canPreview,
|
|
||||||
cn,
|
cn,
|
||||||
} from "@lume/utils";
|
} from "@lume/utils";
|
||||||
import { NIP89 } from "./nip89";
|
import { NIP89 } from "./nip89";
|
||||||
import { useNoteContext } from "./provider";
|
import { useNoteContext } from "./provider";
|
||||||
import { ReactNode, useMemo } from "react";
|
import { ReactNode, useMemo } from "react";
|
||||||
import reactStringReplace from "react-string-replace";
|
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import { MentionUser } from "./mentions/user";
|
import { MentionUser } from "./mentions/user";
|
||||||
import { MentionNote } from "./mentions/note";
|
import { MentionNote } from "./mentions/note";
|
||||||
import { Hashtag } from "./mentions/hashtag";
|
import { Hashtag } from "./mentions/hashtag";
|
||||||
import { VideoPreview } from "./preview/video";
|
import { VideoPreview } from "./preview/video";
|
||||||
import { stripHtml } from "string-strip-html";
|
|
||||||
import getUrl from "get-urls";
|
|
||||||
import { ImagePreview } from "./preview/image";
|
import { ImagePreview } from "./preview/image";
|
||||||
|
import reactStringReplace from "react-string-replace";
|
||||||
|
|
||||||
export function NoteContent({ className }: { className?: string }) {
|
export function NoteContent({ className }: { className?: string }) {
|
||||||
const event = useNoteContext();
|
const event = useNoteContext();
|
||||||
const content = useMemo(() => {
|
const content = useMemo(() => {
|
||||||
const text = stripHtml(event.content.trim()).result;
|
const text = event.content.trim();
|
||||||
const words = text.split(/( |\n)/);
|
const words = text.split(/( |\n)/);
|
||||||
const urls = [...getUrl(text)];
|
|
||||||
|
|
||||||
// @ts-ignore, kaboom !!!
|
// @ts-ignore, kaboom !!!
|
||||||
let parsedContent: ReactNode[] = text;
|
let parsedContent: ReactNode[] = text;
|
||||||
|
|||||||
@@ -1,13 +1,7 @@
|
|||||||
import { NOSTR_MENTIONS } from "@lume/utils";
|
|
||||||
import { ReactNode, useMemo } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import reactStringReplace from "react-string-replace";
|
|
||||||
import { User } from "../../user";
|
import { User } from "../../user";
|
||||||
import { Hashtag } from "./hashtag";
|
|
||||||
import { MentionUser } from "./user";
|
|
||||||
import { useArk, useEvent } from "@lume/ark";
|
import { useArk, useEvent } from "@lume/ark";
|
||||||
import { LinkIcon } from "@lume/icons";
|
import { LinkIcon } from "@lume/icons";
|
||||||
import { stripHtml } from "string-strip-html";
|
|
||||||
|
|
||||||
export function MentionNote({
|
export function MentionNote({
|
||||||
eventId,
|
eventId,
|
||||||
@@ -16,72 +10,11 @@ export function MentionNote({
|
|||||||
eventId: string;
|
eventId: string;
|
||||||
openable?: boolean;
|
openable?: boolean;
|
||||||
}) {
|
}) {
|
||||||
|
const ark = useArk();
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { isLoading, isError, data } = useEvent(eventId);
|
const { isLoading, isError, data } = useEvent(eventId);
|
||||||
|
|
||||||
const ark = useArk();
|
|
||||||
const content = useMemo(() => {
|
|
||||||
if (!data) return "";
|
|
||||||
|
|
||||||
const text = stripHtml(data.content.trim()).result;
|
|
||||||
const words = text.split(/( |\n)/);
|
|
||||||
|
|
||||||
// @ts-ignore, kaboom !!!
|
|
||||||
let parsedContent: ReactNode[] = text;
|
|
||||||
|
|
||||||
const hashtags = words.filter((word) => word.startsWith("#"));
|
|
||||||
const mentions = words.filter((word) =>
|
|
||||||
NOSTR_MENTIONS.some((el) => word.startsWith(el)),
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (hashtags.length) {
|
|
||||||
for (const hashtag of hashtags) {
|
|
||||||
parsedContent = reactStringReplace(
|
|
||||||
parsedContent,
|
|
||||||
hashtag,
|
|
||||||
(match, i) => {
|
|
||||||
return <Hashtag key={match + i} tag={hashtag} />;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mentions.length) {
|
|
||||||
for (const mention of mentions) {
|
|
||||||
parsedContent = reactStringReplace(
|
|
||||||
parsedContent,
|
|
||||||
mention,
|
|
||||||
(match, i) => <MentionUser key={match + i} pubkey={mention} />,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
parsedContent = reactStringReplace(
|
|
||||||
parsedContent,
|
|
||||||
/(https?:\/\/\S+)/g,
|
|
||||||
(match, i) => {
|
|
||||||
const url = new URL(match);
|
|
||||||
return (
|
|
||||||
<a
|
|
||||||
key={match + i}
|
|
||||||
href={url.toString()}
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
className="content-break inline-block w-full truncate font-normal text-blue-500 hover:text-blue-600"
|
|
||||||
>
|
|
||||||
{url.toString()}
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return parsedContent;
|
|
||||||
} catch (e) {
|
|
||||||
return text;
|
|
||||||
}
|
|
||||||
}, [data]);
|
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -120,7 +53,7 @@ export function MentionNote({
|
|||||||
</User.Root>
|
</User.Root>
|
||||||
</User.Provider>
|
</User.Provider>
|
||||||
<div className="line-clamp-4 select-text whitespace-normal text-balance leading-normal">
|
<div className="line-clamp-4 select-text whitespace-normal text-balance leading-normal">
|
||||||
{content}
|
{data.content}
|
||||||
</div>
|
</div>
|
||||||
{openable ? (
|
{openable ? (
|
||||||
<div className="flex h-10 items-center justify-between">
|
<div className="flex h-10 items-center justify-between">
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { writeText } from "@tauri-apps/plugin-clipboard-manager";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useNoteContext } from "./provider";
|
import { useNoteContext } from "./provider";
|
||||||
import { useArk } from "@lume/ark";
|
import { useArk } from "@lume/ark";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
export function NoteMenu() {
|
export function NoteMenu() {
|
||||||
const ark = useArk();
|
const ark = useArk();
|
||||||
@@ -13,20 +14,24 @@ export function NoteMenu() {
|
|||||||
|
|
||||||
const copyID = async () => {
|
const copyID = async () => {
|
||||||
await writeText(await ark.event_to_bech32(event.id, [""]));
|
await writeText(await ark.event_to_bech32(event.id, [""]));
|
||||||
|
toast.success("Copied");
|
||||||
};
|
};
|
||||||
|
|
||||||
const copyRaw = async () => {
|
const copyRaw = async () => {
|
||||||
await writeText(JSON.stringify(event));
|
await writeText(JSON.stringify(event));
|
||||||
|
toast.success("Copied");
|
||||||
};
|
};
|
||||||
|
|
||||||
const copyNpub = async () => {
|
const copyNpub = async () => {
|
||||||
await writeText(await ark.user_to_bech32(event.pubkey, [""]));
|
await writeText(await ark.user_to_bech32(event.pubkey, [""]));
|
||||||
|
toast.success("Copied");
|
||||||
};
|
};
|
||||||
|
|
||||||
const copyLink = async () => {
|
const copyLink = async () => {
|
||||||
await writeText(
|
await writeText(
|
||||||
`https://njump.me/${await ark.event_to_bech32(event.id, [""])}`,
|
`https://njump.me/${await ark.event_to_bech32(event.id, [""])}`,
|
||||||
);
|
);
|
||||||
|
toast.success("Copied");
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export function ImagePreview({ url }: { url: string }) {
|
|||||||
// biome-ignore lint/a11y/useKeyWithClickEvents: <explanation>
|
// biome-ignore lint/a11y/useKeyWithClickEvents: <explanation>
|
||||||
<div
|
<div
|
||||||
onClick={open}
|
onClick={open}
|
||||||
className="group relative my-1 overflow-hidden rounded-xl"
|
className="group relative my-1 overflow-hidden rounded-xl ring-1 ring-neutral-100 dark:ring-neutral-900"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src={url}
|
src={url}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export function VideoPreview({ url }: { url: string }) {
|
|||||||
<MediaControlBar>
|
<MediaControlBar>
|
||||||
<MediaPlayButton />
|
<MediaPlayButton />
|
||||||
<MediaTimeRange />
|
<MediaTimeRange />
|
||||||
<MediaTimeDisplay showDuration />
|
<MediaTimeDisplay />
|
||||||
<MediaMuteButton />
|
<MediaMuteButton />
|
||||||
</MediaControlBar>
|
</MediaControlBar>
|
||||||
</MediaController>
|
</MediaController>
|
||||||
|
|||||||
@@ -2,18 +2,15 @@ import { cn } from "@lume/utils";
|
|||||||
import { ReactNode } from "react";
|
import { ReactNode } from "react";
|
||||||
|
|
||||||
export function NoteRoot({
|
export function NoteRoot({
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
}: {
|
}: {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
className?: string;
|
className?: string;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={cn("h-min w-full", className)} contentEditable={false}>
|
||||||
className={cn("h-min w-full overflow-hidden", className)}
|
{children}
|
||||||
contentEditable={false}
|
</div>
|
||||||
>
|
);
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tanstack/react-query": "^5.24.1",
|
"@tanstack/react-query": "^5.24.1",
|
||||||
|
"bitcoin-units": "^1.0.0",
|
||||||
"clsx": "^2.1.0",
|
"clsx": "^2.1.0",
|
||||||
"dayjs": "^1.11.10",
|
"dayjs": "^1.11.10",
|
||||||
"nostr-tools": "^2.3.1",
|
"nostr-tools": "^2.3.1",
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import relativeTime from "dayjs/plugin/relativeTime";
|
|||||||
import updateLocale from "dayjs/plugin/updateLocale";
|
import updateLocale from "dayjs/plugin/updateLocale";
|
||||||
import { nip19 } from "nostr-tools";
|
import { nip19 } from "nostr-tools";
|
||||||
import { AUDIOS, IMAGES, VIDEOS } from "./constants";
|
import { AUDIOS, IMAGES, VIDEOS } from "./constants";
|
||||||
|
import { BitcoinUnit } from "bitcoin-units";
|
||||||
|
|
||||||
dayjs.extend(relativeTime);
|
dayjs.extend(relativeTime);
|
||||||
dayjs.extend(updateLocale);
|
dayjs.extend(updateLocale);
|
||||||
@@ -89,3 +90,34 @@ export function canPreview(text: string) {
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// source: https://github.com/synonymdev/bitkit/blob/master/src/utils/displayValues/index.ts
|
||||||
|
export function getBitcoinDisplayValues(satoshis: number) {
|
||||||
|
let bitcoinFormatted = new BitcoinUnit(satoshis, "satoshis")
|
||||||
|
.getValue()
|
||||||
|
.toFixed(10)
|
||||||
|
.replace(/\.?0+$/, "");
|
||||||
|
|
||||||
|
const [bitcoinWhole, bitcoinDecimal] = bitcoinFormatted.split(".");
|
||||||
|
|
||||||
|
// format sats to group thousands
|
||||||
|
// 4000000 -> 4 000 000
|
||||||
|
let res = "";
|
||||||
|
bitcoinFormatted
|
||||||
|
.split("")
|
||||||
|
.reverse()
|
||||||
|
.forEach((c, index) => {
|
||||||
|
if (index > 0 && index % 3 === 0) {
|
||||||
|
res = " " + res;
|
||||||
|
}
|
||||||
|
res = c + res;
|
||||||
|
});
|
||||||
|
|
||||||
|
bitcoinFormatted = res;
|
||||||
|
|
||||||
|
return {
|
||||||
|
bitcoinFormatted,
|
||||||
|
bitcoinWhole,
|
||||||
|
bitcoinDecimal,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
Generated
+16
-31
@@ -111,6 +111,9 @@ importers:
|
|||||||
react:
|
react:
|
||||||
specifier: ^18.2.0
|
specifier: ^18.2.0
|
||||||
version: 18.2.0
|
version: 18.2.0
|
||||||
|
react-currency-input-field:
|
||||||
|
specifier: ^3.8.0
|
||||||
|
version: 3.8.0(react@18.2.0)
|
||||||
react-dom:
|
react-dom:
|
||||||
specifier: ^18.2.0
|
specifier: ^18.2.0
|
||||||
version: 18.2.0(react@18.2.0)
|
version: 18.2.0(react@18.2.0)
|
||||||
@@ -935,9 +938,6 @@ importers:
|
|||||||
react-i18next:
|
react-i18next:
|
||||||
specifier: ^14.0.5
|
specifier: ^14.0.5
|
||||||
version: 14.0.5(i18next@23.10.0)(react-dom@18.2.0)(react@18.2.0)
|
version: 14.0.5(i18next@23.10.0)(react-dom@18.2.0)(react@18.2.0)
|
||||||
react-router-dom:
|
|
||||||
specifier: ^6.22.2
|
|
||||||
version: 6.22.2(react-dom@18.2.0)(react@18.2.0)
|
|
||||||
react-string-replace:
|
react-string-replace:
|
||||||
specifier: ^1.1.1
|
specifier: ^1.1.1
|
||||||
version: 1.1.1
|
version: 1.1.1
|
||||||
@@ -987,6 +987,9 @@ importers:
|
|||||||
'@tanstack/react-query':
|
'@tanstack/react-query':
|
||||||
specifier: ^5.24.1
|
specifier: ^5.24.1
|
||||||
version: 5.24.1(react@18.2.0)
|
version: 5.24.1(react@18.2.0)
|
||||||
|
bitcoin-units:
|
||||||
|
specifier: ^1.0.0
|
||||||
|
version: 1.0.0
|
||||||
clsx:
|
clsx:
|
||||||
specifier: ^2.1.0
|
specifier: ^2.1.0
|
||||||
version: 2.1.0
|
version: 2.1.0
|
||||||
@@ -2625,11 +2628,6 @@ packages:
|
|||||||
'@babel/runtime': 7.24.0
|
'@babel/runtime': 7.24.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/@remix-run/router@1.15.2:
|
|
||||||
resolution: {integrity: sha512-+Rnav+CaoTE5QJc4Jcwh5toUpnVLKYbpU6Ys0zqbakqbaLQHeglLVHPfxOiQqdNmUy5C2lXz5dwC6tQNX2JW2Q==}
|
|
||||||
engines: {node: '>=14.0.0'}
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/@rollup/plugin-virtual@3.0.2:
|
/@rollup/plugin-virtual@3.0.2:
|
||||||
resolution: {integrity: sha512-10monEYsBp3scM4/ND4LNH5Rxvh3e/cVeL3jWTgZ2SrQ+BmUoQcopVQvnaMcOnykb1VkxUFuDAN+0FnpTFRy2A==}
|
resolution: {integrity: sha512-10monEYsBp3scM4/ND4LNH5Rxvh3e/cVeL3jWTgZ2SrQ+BmUoQcopVQvnaMcOnykb1VkxUFuDAN+0FnpTFRy2A==}
|
||||||
engines: {node: '>=14.0.0'}
|
engines: {node: '>=14.0.0'}
|
||||||
@@ -3636,10 +3634,20 @@ packages:
|
|||||||
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
|
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/big.js@6.2.1:
|
||||||
|
resolution: {integrity: sha512-bCtHMwL9LeDIozFn+oNhhFoq+yQ3BNdnsLSASUxLciOb1vgvpHsIO1dsENiGMgbb4SkP5TrzWzRiLddn8ahVOQ==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/binary-extensions@2.2.0:
|
/binary-extensions@2.2.0:
|
||||||
resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==}
|
resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
/bitcoin-units@1.0.0:
|
||||||
|
resolution: {integrity: sha512-brac+Ttz7ovf/8D0jQHSWHnN2hmdjxDRBStxhjO752URLJlQIFpfZxzUteSZ81UYnRNiMkvsW9WsYPDuxHfnYA==}
|
||||||
|
dependencies:
|
||||||
|
big.js: 6.2.1
|
||||||
|
dev: false
|
||||||
|
|
||||||
/bl@4.1.0:
|
/bl@4.1.0:
|
||||||
resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
|
resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
|
||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
@@ -5973,29 +5981,6 @@ packages:
|
|||||||
use-sidecar: 1.1.2(@types/react@18.2.61)(react@18.2.0)
|
use-sidecar: 1.1.2(@types/react@18.2.61)(react@18.2.0)
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/react-router-dom@6.22.2(react-dom@18.2.0)(react@18.2.0):
|
|
||||||
resolution: {integrity: sha512-WgqxD2qySEIBPZ3w0sHH+PUAiamDeszls9tzqMPBDA1YYVucTBXLU7+gtRfcSnhe92A3glPnvSxK2dhNoAVOIQ==}
|
|
||||||
engines: {node: '>=14.0.0'}
|
|
||||||
peerDependencies:
|
|
||||||
react: '>=16.8'
|
|
||||||
react-dom: '>=16.8'
|
|
||||||
dependencies:
|
|
||||||
'@remix-run/router': 1.15.2
|
|
||||||
react: 18.2.0
|
|
||||||
react-dom: 18.2.0(react@18.2.0)
|
|
||||||
react-router: 6.22.2(react@18.2.0)
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/react-router@6.22.2(react@18.2.0):
|
|
||||||
resolution: {integrity: sha512-YD3Dzprzpcq+tBMHBS822tCjnWD3iIZbTeSXMY9LPSG541EfoBGyZ3bS25KEnaZjLcmQpw2AVLkFyfgXY8uvcw==}
|
|
||||||
engines: {node: '>=14.0.0'}
|
|
||||||
peerDependencies:
|
|
||||||
react: '>=16.8'
|
|
||||||
dependencies:
|
|
||||||
'@remix-run/router': 1.15.2
|
|
||||||
react: 18.2.0
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/react-string-replace@1.1.1:
|
/react-string-replace@1.1.1:
|
||||||
resolution: {integrity: sha512-26TUbLzLfHQ5jO5N7y3Mx88eeKo0Ml0UjCQuX4BMfOd/JX+enQqlKpL1CZnmjeBRvQE8TR+ds9j1rqx9CxhKHQ==}
|
resolution: {integrity: sha512-26TUbLzLfHQ5jO5N7y3Mx88eeKo0Ml0UjCQuX4BMfOd/JX+enQqlKpL1CZnmjeBRvQE8TR+ds9j1rqx9CxhKHQ==}
|
||||||
engines: {node: '>=0.12.0'}
|
engines: {node: '>=0.12.0'}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
"splash",
|
"splash",
|
||||||
"editor",
|
"editor",
|
||||||
"settings",
|
"settings",
|
||||||
|
"nwc",
|
||||||
|
"zap-*",
|
||||||
"event-*",
|
"event-*",
|
||||||
"user-*",
|
"user-*",
|
||||||
"column-*"
|
"column-*"
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
{"desktop-capability":{"identifier":"desktop-capability","description":"Capability for the desktop","local":true,"windows":["main","splash","editor","settings","event-*","user-*","column-*"],"permissions":["path:default","event:default","window:default","app:default","resources:default","menu:default","tray:default","notification:allow-is-permission-granted","notification:allow-request-permission","notification:default","os:allow-locale","os:allow-platform","updater:allow-check","updater:default","window:allow-start-dragging","store:allow-get","clipboard-manager:allow-write","clipboard-manager:allow-read","webview:allow-create-webview-window","webview:allow-create-webview","dialog:allow-open","fs:allow-read-file","shell:allow-open",{"identifier":"http:default","allow":[{"url":"http://**/"},{"url":"https://**/"}]},{"identifier":"fs:allow-read-text-file","allow":[{"path":"$RESOURCE/locales/*"}]}],"platforms":["linux","macOS","windows"]}}
|
{"desktop-capability":{"identifier":"desktop-capability","description":"Capability for the desktop","local":true,"windows":["main","splash","editor","settings","nwc","zap-*","event-*","user-*","column-*"],"permissions":["path:default","event:default","window:default","app:default","resources:default","menu:default","tray:default","notification:allow-is-permission-granted","notification:allow-request-permission","notification:default","os:allow-locale","os:allow-platform","updater:allow-check","updater:default","window:allow-start-dragging","store:allow-get","clipboard-manager:allow-write","clipboard-manager:allow-read","webview:allow-create-webview-window","webview:allow-create-webview","dialog:allow-open","fs:allow-read-file","shell:allow-open",{"identifier":"http:default","allow":[{"url":"http://**/"},{"url":"https://**/"}]},{"identifier":"fs:allow-read-text-file","allow":[{"path":"$RESOURCE/locales/*"}]}],"platforms":["linux","macOS","windows"]}}
|
||||||
+22
-17
@@ -22,7 +22,6 @@ fn main() {
|
|||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
let _tray = tray::create_tray(app.handle()).unwrap();
|
let _tray = tray::create_tray(app.handle()).unwrap();
|
||||||
let handle = app.handle().clone();
|
let handle = app.handle().clone();
|
||||||
let resource_dir = handle.path().resource_dir().unwrap();
|
|
||||||
let home_dir = handle.path().home_dir().unwrap();
|
let home_dir = handle.path().home_dir().unwrap();
|
||||||
|
|
||||||
// create data folder if not exist
|
// create data folder if not exist
|
||||||
@@ -30,12 +29,13 @@ fn main() {
|
|||||||
|
|
||||||
tauri::async_runtime::spawn(async move {
|
tauri::async_runtime::spawn(async move {
|
||||||
// Create nostr database connection
|
// Create nostr database connection
|
||||||
let nostr_db = SQLiteDatabase::open(resource_dir.join("lume.db"))
|
let sqlite = SQLiteDatabase::open(home_dir.join("Lume/lume.db")).await;
|
||||||
.await
|
|
||||||
.expect("Open database failed.");
|
|
||||||
|
|
||||||
// Create nostr connection
|
// Create nostr connection
|
||||||
let client = ClientBuilder::default().database(nostr_db).build();
|
let client = match sqlite {
|
||||||
|
Ok(db) => ClientBuilder::default().database(db).build(),
|
||||||
|
Err(_) => ClientBuilder::default().build(),
|
||||||
|
};
|
||||||
|
|
||||||
// Add some bootstrap relays
|
// Add some bootstrap relays
|
||||||
// #TODO: Pull bootstrap relays from user's settings
|
// #TODO: Pull bootstrap relays from user's settings
|
||||||
@@ -101,8 +101,9 @@ fn main() {
|
|||||||
nostr::metadata::get_interest,
|
nostr::metadata::get_interest,
|
||||||
nostr::metadata::set_settings,
|
nostr::metadata::set_settings,
|
||||||
nostr::metadata::get_settings,
|
nostr::metadata::get_settings,
|
||||||
nostr::metadata::get_nwc_status,
|
|
||||||
nostr::metadata::set_nwc,
|
nostr::metadata::set_nwc,
|
||||||
|
nostr::metadata::load_nwc,
|
||||||
|
nostr::metadata::get_balance,
|
||||||
nostr::metadata::zap_profile,
|
nostr::metadata::zap_profile,
|
||||||
nostr::metadata::zap_event,
|
nostr::metadata::zap_event,
|
||||||
nostr::event::get_event,
|
nostr::event::get_event,
|
||||||
@@ -120,16 +121,20 @@ fn main() {
|
|||||||
])
|
])
|
||||||
.build(tauri::generate_context!())
|
.build(tauri::generate_context!())
|
||||||
.expect("error while running tauri application")
|
.expect("error while running tauri application")
|
||||||
.run(|app, event| {
|
.run(
|
||||||
if let tauri::RunEvent::Opened { urls } = event {
|
#[allow(unused_variables)]
|
||||||
if let Some(w) = app.get_webview_window("main") {
|
|app, event| {
|
||||||
let urls = urls
|
#[cfg(any(target_os = "macos"))]
|
||||||
.iter()
|
if let tauri::RunEvent::Opened { urls } = event {
|
||||||
.map(|u| u.as_str())
|
if let Some(w) = app.get_webview_window("main") {
|
||||||
.collect::<Vec<_>>()
|
let urls = urls
|
||||||
.join(",");
|
.iter()
|
||||||
let _ = w.eval(&format!("window.onFileOpen(`{urls}`)"));
|
.map(|u| u.as_str())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(",");
|
||||||
|
let _ = w.eval(&format!("window.onFileOpen(`{urls}`)"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -132,6 +132,7 @@ pub async fn load_selected_account(npub: &str, state: State<'_, Nostr>) -> Resul
|
|||||||
if let Some(event) = events.first() {
|
if let Some(event) = events.first() {
|
||||||
let list = nip65::extract_relay_list(&event);
|
let list = nip65::extract_relay_list(&event);
|
||||||
for item in list.into_iter() {
|
for item in list.into_iter() {
|
||||||
|
println!("connecting to relay: {}", item.0.to_string());
|
||||||
client
|
client
|
||||||
.connect_relay(item.0.to_string())
|
.connect_relay(item.0.to_string())
|
||||||
.await
|
.await
|
||||||
@@ -140,6 +141,9 @@ pub async fn load_selected_account(npub: &str, state: State<'_, Nostr>) -> Resul
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// #TODO
|
||||||
|
// Subscribe new event for activity and local newsfeed
|
||||||
|
|
||||||
Ok(true)
|
Ok(true)
|
||||||
} else {
|
} else {
|
||||||
Err("nsec not found".into())
|
Err("nsec not found".into())
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
use crate::Nostr;
|
use crate::Nostr;
|
||||||
|
use keyring::Entry;
|
||||||
use nostr_sdk::prelude::*;
|
use nostr_sdk::prelude::*;
|
||||||
use std::{str::FromStr, time::Duration};
|
use std::{str::FromStr, time::Duration};
|
||||||
use tauri::State;
|
use tauri::State;
|
||||||
@@ -273,30 +274,65 @@ pub async fn get_settings(id: &str, state: State<'_, Nostr>) -> Result<String, S
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
|
||||||
pub async fn get_nwc_status(state: State<'_, Nostr>) -> Result<bool, ()> {
|
|
||||||
let client = &state.client;
|
|
||||||
let zapper = client.zapper().await.is_ok();
|
|
||||||
|
|
||||||
Ok(zapper)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn set_nwc(uri: &str, state: State<'_, Nostr>) -> Result<bool, String> {
|
pub async fn set_nwc(uri: &str, state: State<'_, Nostr>) -> Result<bool, String> {
|
||||||
let client = &state.client;
|
let client = &state.client;
|
||||||
|
|
||||||
if let Ok(uri) = NostrWalletConnectURI::from_str(&uri) {
|
if let Ok(nwc_uri) = NostrWalletConnectURI::from_str(&uri) {
|
||||||
if let Ok(nwc) = NWC::new(uri).await {
|
if let Ok(nwc) = NWC::new(nwc_uri).await {
|
||||||
let _ = client.set_zapper(nwc);
|
let keyring = Entry::new("Lume Secret Storage", "NWC").unwrap();
|
||||||
|
let _ = keyring.set_password(uri);
|
||||||
|
let _ = client.set_zapper(nwc).await;
|
||||||
|
|
||||||
Ok(true)
|
Ok(true)
|
||||||
} else {
|
} else {
|
||||||
Ok(false)
|
Err("URI is not valid".into())
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Err("Set NWC failed".into())
|
Err("Set NWC failed".into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn load_nwc(state: State<'_, Nostr>) -> Result<bool, bool> {
|
||||||
|
let client = &state.client;
|
||||||
|
let keyring = Entry::new("Lume Secret Storage", "NWC").unwrap();
|
||||||
|
|
||||||
|
match keyring.get_password() {
|
||||||
|
Ok(val) => {
|
||||||
|
let uri = NostrWalletConnectURI::from_str(&val).unwrap();
|
||||||
|
if let Ok(nwc) = NWC::new(uri).await {
|
||||||
|
client.set_zapper(nwc).await;
|
||||||
|
Ok(true)
|
||||||
|
} else {
|
||||||
|
Err(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => Err(false),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn get_balance() -> Result<u64, String> {
|
||||||
|
let keyring = Entry::new("Lume Secret Storage", "NWC").unwrap();
|
||||||
|
|
||||||
|
match keyring.get_password() {
|
||||||
|
Ok(val) => {
|
||||||
|
let uri = NostrWalletConnectURI::from_str(&val).unwrap();
|
||||||
|
if let Ok(nwc) = NWC::new(uri).await {
|
||||||
|
if let Ok(balance) = nwc.get_balance().await {
|
||||||
|
Ok(balance)
|
||||||
|
} else {
|
||||||
|
Err("Get balance failed".into())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Err("Cannot connect to NWC".into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => Err("Something wrong".into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn zap_profile(
|
pub async fn zap_profile(
|
||||||
id: &str,
|
id: &str,
|
||||||
|
|||||||
+25
-11
@@ -1,8 +1,32 @@
|
|||||||
use tauri::{tray::ClickType, Manager, Runtime};
|
use tauri::{Manager, Runtime};
|
||||||
|
|
||||||
pub fn create_tray<R: Runtime>(app: &tauri::AppHandle<R>) -> tauri::Result<()> {
|
pub fn create_tray<R: Runtime>(app: &tauri::AppHandle<R>) -> tauri::Result<()> {
|
||||||
|
let version = app.package_info().version.to_string();
|
||||||
let tray = app.tray().unwrap();
|
let tray = app.tray().unwrap();
|
||||||
let menu = tauri::menu::MenuBuilder::new(app)
|
let menu = tauri::menu::MenuBuilder::new(app)
|
||||||
|
.item(
|
||||||
|
&tauri::menu::MenuItem::with_id(app, "open_lume", "Open Lume", true, None::<&str>).unwrap(),
|
||||||
|
)
|
||||||
|
.item(&tauri::menu::MenuItem::with_id(app, "editor", "New Post", true, None::<&str>).unwrap())
|
||||||
|
.separator()
|
||||||
|
.item(
|
||||||
|
&tauri::menu::MenuItem::with_id(
|
||||||
|
app,
|
||||||
|
"version",
|
||||||
|
format!("Version {}", version),
|
||||||
|
false,
|
||||||
|
None::<&str>,
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.item(&tauri::menu::MenuItem::with_id(app, "about", "About Lume", true, None::<&str>).unwrap())
|
||||||
|
.item(
|
||||||
|
&tauri::menu::MenuItem::with_id(app, "update", "Check for Updates", true, None::<&str>)
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.item(
|
||||||
|
&tauri::menu::MenuItem::with_id(app, "settings", "Settings...", true, None::<&str>).unwrap(),
|
||||||
|
)
|
||||||
.item(&tauri::menu::MenuItem::with_id(app, "quit", "Quit", true, None::<&str>).unwrap())
|
.item(&tauri::menu::MenuItem::with_id(app, "quit", "Quit", true, None::<&str>).unwrap())
|
||||||
.build()
|
.build()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -16,15 +40,5 @@ pub fn create_tray<R: Runtime>(app: &tauri::AppHandle<R>) -> tauri::Result<()> {
|
|||||||
_ => {}
|
_ => {}
|
||||||
});
|
});
|
||||||
|
|
||||||
tray.on_tray_icon_event(|tray, event| {
|
|
||||||
if event.click_type == ClickType::Left {
|
|
||||||
let app = tray.app_handle();
|
|
||||||
if let Some(window) = app.get_webview_window("main") {
|
|
||||||
let _ = window.show();
|
|
||||||
let _ = window.set_focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
|
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
|
||||||
"productName": "Lume",
|
"productName": "Lume",
|
||||||
"version": "4.0.0-alpha+1",
|
"version": "4.0.0-alpha+2",
|
||||||
"identifier": "nu.lume.Lume",
|
"identifier": "nu.lume.Lume",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeBuildCommand": "pnpm desktop:build",
|
"beforeBuildCommand": "pnpm desktop:build",
|
||||||
|
|||||||
Reference in New Issue
Block a user