Compare commits

...

18 Commits

Author SHA1 Message Date
reya
6996e30889 chore: update github ci 2024-06-07 11:26:20 +07:00
reya
7ba793fad8 chore: bump version 2024-06-07 10:03:04 +07:00
reya
f11f836518 chore: update tray icon 2024-06-07 09:56:57 +07:00
reya
04fe0fcec8 feat: respect the relay hint 2024-06-07 09:07:33 +07:00
雨宮蓮
799835a629 Notification Panel (#200)
* feat: add tauri nspanel

* feat: add notification panel

* feat: move notification service to backend

* feat: add sync notification job

* feat: enable panel to join all spaces including fullscreen (#203)

* feat: fetch notification

* feat: listen for new notification

* feat: finish panel

---------

Co-authored-by: Victor Aremu <me@victorare.mu>
2024-06-06 14:32:30 +07:00
XIAO YU
4e7da4108b feat: Add get user following function (#202)
* feat: Add get user following function

* refactor: Refactor get_following function to use state and string public key

* feat: Update get_following function to use timeout duration

* feat: Fix connect_remote_account function to return remote_npub without conversion

* feat: Refactor get_following function to handle public key parsing errors

* Refactor get_followers function to handle public key parsing errors and use timeout duration
2024-06-05 13:24:32 +07:00
reya
7c7b082b3a fix: memory leak in image component 2024-06-03 07:32:34 +07:00
reya
38d6c51921 feat: use nostrdb for unix and rocksdb for windows 2024-06-02 08:16:59 +07:00
reya
1738cbdd97 chore: upgrade tauri 2024-06-01 14:54:35 +07:00
reya
2e885b76a1 feat: improve text wrap 2024-06-01 08:27:22 +07:00
reya
f94680e487 fix: column overlapped after change account 2024-05-31 15:25:47 +07:00
XIAO YU
c682a58842 chore: Remove unused modules and update metadata.rs (#199) 2024-05-31 12:53:35 +07:00
reya
921cf871ee chore: bump version 2024-05-31 08:54:53 +07:00
reya
d5b1593aca feat: improve nostr connect flow 2024-05-31 08:54:17 +07:00
reya
6676b4e2a4 Revert "chore: Update dependencies and add thiserror crate (#196)"
This reverts commit e254ee3203.
2024-05-30 15:40:44 +07:00
reya
5f30ddcfca Merge branch 'main' of github.com:lumehq/lume 2024-05-30 15:23:11 +07:00
reya
41d0de539d feat: revamp nostr connect flow 2024-05-30 15:21:33 +07:00
XIAO YU
e254ee3203 chore: Update dependencies and add thiserror crate (#196) 2024-05-30 07:12:46 +07:00
50 changed files with 2191 additions and 1901 deletions

View File

@@ -16,6 +16,8 @@ jobs:
args: "--target aarch64-apple-darwin"
- platform: "macos-latest" # for Intel based macs.
args: "--target x86_64-apple-darwin"
- platform: "macos-latest" # for Intel based macs.
args: "--target universal-apple-darwin"
#- platform: 'ubuntu-22.04'
# args: ''
#- platform: 'windows-latest'

View File

@@ -21,16 +21,16 @@
"@radix-ui/react-hover-card": "^1.0.7",
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-tooltip": "^1.0.7",
"@tanstack/query-sync-storage-persister": "^5.40.0",
"@tanstack/react-query": "^5.40.0",
"@tanstack/react-query-persist-client": "^5.40.0",
"@tanstack/react-router": "^1.34.5",
"@tanstack/react-router": "^1.34.9",
"i18next": "^23.11.5",
"i18next-resources-to-backend": "^1.2.1",
"minidenticons": "^4.2.1",
"nanoid": "^5.0.7",
"nostr-tools": "^2.6.0",
"react": "^18.3.1",
"react-currency-input-field": "^3.8.0",
"react-dom": "^18.3.1",
@@ -48,8 +48,8 @@
"@lume/tailwindcss": "workspace:^",
"@lume/tsconfig": "workspace:^",
"@lume/types": "workspace:^",
"@tanstack/router-devtools": "^1.34.5",
"@tanstack/router-vite-plugin": "^1.34.1",
"@tanstack/router-devtools": "^1.34.9",
"@tanstack/router-vite-plugin": "^1.34.8",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react-swc": "^3.7.0",

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

@@ -3,7 +3,7 @@ import type { LumeColumn } from "@lume/types";
import { cn } from "@lume/utils";
import { invoke } from "@tauri-apps/api/core";
import { getCurrent } from "@tauri-apps/api/webviewWindow";
import { useEffect, useRef, useState } from "react";
import { useEffect, useMemo, useRef, useState } from "react";
export function Column({
column,
@@ -17,7 +17,10 @@ export function Column({
isResize: boolean;
}) {
const container = useRef<HTMLDivElement>(null);
const webviewLabel = `column-${account}_${column.label}`;
const webviewLabel = useMemo(
() => `column-${account}_${column.label}`,
[account],
);
const [isCreated, setIsCreated] = useState(false);
@@ -48,6 +51,8 @@ export function Column({
}, [isScroll]);
useEffect(() => {
if (!container?.current) return;
const rect = container.current.getBoundingClientRect();
const url = `${column.content}?account=${account}&label=${column.label}&name=${column.name}`;
@@ -59,11 +64,16 @@ export function Column({
width: rect.width,
height: rect.height,
url,
}).then(() => setIsCreated(true));
}).then(() => {
console.log("created: ", webviewLabel);
setIsCreated(true);
});
// close webview when unmounted
return () => {
invoke("close_column", { label: webviewLabel });
invoke("close_column", { label: webviewLabel }).then(() => {
console.log("closed: ", webviewLabel);
});
};
}, [account]);

View File

@@ -102,7 +102,7 @@ export function NoteContent({
<div className="flex flex-col gap-2">
<div
className={cn(
"select-text text-[15px] text-balance break-words overflow-hidden",
"select-text text-[15px] text-pretty content-break overflow-hidden",
event.content.length > 500 ? "max-h-[300px] gradient-mask-b-0" : "",
className,
)}

View File

@@ -145,7 +145,7 @@ export function NoteContentLarge({
return (
<div className={cn("select-text", className)}>
<div className="text-[15px] text-balance content-break leading-normal">
<div className="text-[15px] text-pretty content-break leading-normal">
{content}
</div>
</div>

View File

@@ -2,7 +2,7 @@ export function Hashtag({ tag }: { tag: string }) {
return (
<button
type="button"
className="break-all cursor-default leading-normal group"
className="break-all cursor-default leading-normal group text-start"
>
<span className="text-blue-500">#</span>
<span className="underline-offset-1 underline decoration-2 decoration-blue-200 dark:decoration-blue-800 group-hover:decoration-blue-500">

View File

@@ -48,7 +48,7 @@ export function MentionNote({
</User.Provider>
<div
className={cn(
"px-3 select-text content-break whitespace-normal text-balance leading-normal",
"px-3 select-text whitespace-normal text-pretty content-break leading-normal",
data.content.length > 100 ? "max-h-[150px] gradient-mask-b-0" : "",
)}
>

View File

@@ -1,61 +1,44 @@
import { CheckCircleIcon, DownloadIcon } from "@lume/icons";
import { downloadDir } from "@tauri-apps/api/path";
import { WebviewWindow } from "@tauri-apps/api/webviewWindow";
import { download } from "@tauri-apps/plugin-upload";
import { type SyntheticEvent, useState } from "react";
export function ImagePreview({ url }: { url: string }) {
const [downloaded, setDownloaded] = useState(false);
const open = async (url: string) => {
const name = new URL(url).pathname
.split("/")
.pop()
.replace(/[^a-zA-Z ]/g, "");
const label = `viewer-${name}`;
const window = WebviewWindow.getByLabel(label);
const downloadImage = async (e: { stopPropagation: () => void }) => {
try {
e.stopPropagation();
if (!window) {
const newWindow = new WebviewWindow(label, {
url,
title: "Image Viewer",
width: 800,
height: 800,
titleBarStyle: "overlay",
});
const downloadDirPath = await downloadDir();
const filename = url.substring(url.lastIndexOf("/") + 1);
await download(url, `${downloadDirPath}/${filename}`);
setDownloaded(true);
} catch (e) {
console.error(e);
return newWindow;
}
};
const open = async () => {
const name = new URL(url).pathname.split("/").pop();
return new WebviewWindow("image-viewer", {
url,
title: name,
});
};
const fallback = (event: SyntheticEvent<HTMLImageElement, Event>) => {
event.currentTarget.src = "/fallback-image.jpg";
return await window.setFocus();
};
return (
// biome-ignore lint/a11y/useKeyWithClickEvents: <explanation>
<div onClick={() => open()} className="group relative my-1">
<div className="group relative my-1">
<img
src={url}
alt={url}
loading="lazy"
decoding="async"
style={{ contentVisibility: "auto" }}
onError={fallback}
className="max-h-[600px] w-auto object-cover rounded-lg outline outline-1 -outline-offset-1 outline-black/15"
onClick={() => open(url)}
onError={({ currentTarget }) => {
currentTarget.onerror = null;
currentTarget.src = "/404.jpg";
}}
/>
<button
type="button"
onClick={(e) => downloadImage(e)}
className="absolute right-2 top-2 z-20 hidden size-8 items-center justify-center rounded-md bg-white/10 text-white/70 backdrop-blur-2xl hover:bg-blue-500 hover:text-white group-hover:inline-flex"
>
{downloaded ? (
<CheckCircleIcon className="size-4" />
) : (
<DownloadIcon className="size-4" />
)}
</button>
</div>
);
}

View File

@@ -36,6 +36,10 @@ export function Images({ urls }: { urls: string[] }) {
style={{ contentVisibility: "auto" }}
className="max-h-[400px] w-auto object-cover rounded-lg outline outline-1 -outline-offset-1 outline-black/15"
onClick={() => open(urls[0])}
onError={({ currentTarget }) => {
currentTarget.onerror = null;
currentTarget.src = "/404.jpg";
}}
/>
</div>
);
@@ -54,6 +58,10 @@ export function Images({ urls }: { urls: string[] }) {
style={{ contentVisibility: "auto" }}
className="w-full h-full object-cover rounded-lg outline outline-1 -outline-offset-1 outline-black/15"
onClick={() => open(item)}
onError={({ currentTarget }) => {
currentTarget.onerror = null;
currentTarget.src = "/404.jpg";
}}
/>
</CarouselItem>
)}

View File

@@ -23,7 +23,7 @@ export function UserAvatar({ className }: { className?: string }) {
alt={user.pubkey}
loading="eager"
decoding="async"
className={cn("outline-[.5px] outline-black/5", className)}
className={cn("outline-[.5px] outline-black/5 object-cover", className)}
/>
<Avatar.Fallback delayMs={120}>
<img

View File

@@ -13,9 +13,11 @@ import { VList, type VListHandle } from "virtua";
export const Route = createFileRoute("/$account/home")({
loader: async () => {
const columns = NostrQuery.getColumns();
const columns = await NostrQuery.getColumns();
return columns;
},
gcTime: 0,
shouldReload: false,
component: Screen,
});
@@ -24,11 +26,16 @@ function Screen() {
const initialColumnList = Route.useLoaderData();
const vlistRef = useRef<VListHandle>(null);
const [columns, setColumns] = useState<LumeColumn[]>([]);
const [selectedIndex, setSelectedIndex] = useState(-1);
const [columns, setColumns] = useState([]);
const [isScroll, setIsScroll] = useState(false);
const [isResize, setIsResize] = useState(false);
const reset = () => {
setColumns(null);
setSelectedIndex(-1);
};
const goLeft = () => {
const prevIndex = Math.max(selectedIndex - 1, 0);
setSelectedIndex(prevIndex);
@@ -64,7 +71,7 @@ function Screen() {
// scroll to the newest column
vlistRef.current.scrollToIndex(newCols.length - 1, {
align: "end",
align: "center",
});
}, 150);
@@ -103,12 +110,15 @@ function Screen() {
}, [initialColumnList]);
useEffect(() => {
// save state
NostrQuery.setColumns(columns);
// save current state
if (columns?.length) {
NostrQuery.setColumns(columns).then(() => console.log("saved"));
}
}, [columns]);
useEffect(() => {
const unlistenColEvent = listen<EventColumns>("columns", (data) => {
if (data.payload.type === "reset") reset();
if (data.payload.type === "add") add(data.payload.column);
if (data.payload.type === "remove") remove(data.payload.label);
if (data.payload.type === "set_title")
@@ -137,9 +147,9 @@ function Screen() {
onScrollEnd={() => setIsScroll(false)}
className="scrollbar-none h-full w-full overflow-x-auto focus:outline-none"
>
{columns.map((column) => (
{columns?.map((column) => (
<Column
key={column.label}
key={account + column.label}
column={column}
account={account}
isScroll={isScroll}

View File

@@ -1,36 +1,23 @@
import {
BellIcon,
ComposeFilledIcon,
HorizontalDotsIcon,
PlusIcon,
SearchIcon,
} from "@lume/icons";
import { type NostrEvent, Kind } from "@lume/types";
import { User } from "@/components/user";
import {
cn,
decodeZapInvoice,
displayNpub,
sendNativeNotification,
} from "@lume/utils";
import { cn } from "@lume/utils";
import { Outlet, createFileRoute } from "@tanstack/react-router";
import { invoke } from "@tauri-apps/api/core";
import { getCurrent } from "@tauri-apps/api/window";
import { useEffect, useMemo, useState } from "react";
import { toast } from "sonner";
import * as Popover from "@radix-ui/react-popover";
import { LumeWindow, NostrAccount, NostrQuery } from "@lume/system";
import { LumeWindow, NostrAccount } from "@lume/system";
import { Link } from "@tanstack/react-router";
type AccountSearch = {
accounts?: string[];
};
export const Route = createFileRoute("/$account")({
validateSearch: (search: Record<string, unknown>): AccountSearch => {
return {
accounts: (search?.accounts as string[]) || [],
};
beforeLoad: async () => {
const accounts = await NostrAccount.getAccounts();
return { accounts };
},
component: Screen,
});
@@ -50,7 +37,7 @@ function Screen() {
<div className="flex items-center gap-3">
<Accounts />
<Link
to="/landing/"
to="/landing"
className="inline-flex size-8 shrink-0 items-center justify-center rounded-full bg-black/10 text-neutral-800 hover:bg-black/20 dark:bg-white/10 dark:text-neutral-200 dark:hover:bg-white/20"
>
<PlusIcon className="size-5" />
@@ -65,7 +52,6 @@ function Screen() {
<ComposeFilledIcon className="size-4" />
New Post
</button>
<Bell />
<button
type="button"
onClick={() => LumeWindow.openSearch()}
@@ -84,7 +70,7 @@ function Screen() {
}
function Accounts() {
const { accounts } = Route.useSearch();
const { accounts } = Route.useRouteContext();
const { account } = Route.useParams();
const [windowWidth, setWindowWidth] = useState<number>(null);
@@ -108,11 +94,20 @@ function Accounts() {
return await LumeWindow.openProfile(account);
}
// change current account and update signer
// Change current account and update signer
const select = await NostrAccount.loadAccount(npub);
if (select) {
return navigate({ to: "/$account/home", params: { account: npub } });
// Reset current columns
await getCurrent().emit("columns", { type: "reset" });
// Redirect to new account
return navigate({
to: "/$account/home",
params: { account: npub },
resetScroll: true,
replace: true,
});
} else {
toast.warning("Something wrong.");
}
@@ -191,64 +186,3 @@ function Accounts() {
</div>
);
}
function Bell() {
const { account } = Route.useParams();
const [count, setCount] = useState(0);
useEffect(() => {
const unlisten = getCurrent().listen<string>(
"activity",
async (payload) => {
setCount((prevCount) => prevCount + 1);
await invoke("set_badge", { count });
const event: NostrEvent = JSON.parse(payload.payload);
const user = await NostrQuery.getProfile(event.pubkey);
const userName =
user.display_name || user.name || displayNpub(event.pubkey, 16);
switch (event.kind) {
case Kind.Text: {
sendNativeNotification("Mentioned you in a note", userName);
break;
}
case Kind.Repost: {
sendNativeNotification("Reposted your note", userName);
break;
}
case Kind.ZapReceipt: {
const amount = decodeZapInvoice(event.tags);
sendNativeNotification(
`Zapped ₿ ${amount.bitcoinFormatted}`,
userName,
);
break;
}
default:
break;
}
},
);
return () => {
unlisten.then((f) => f());
};
}, []);
return (
<button
type="button"
onClick={() => {
setCount(0);
LumeWindow.openActivity(account);
}}
className="relative inline-flex size-8 items-center justify-center rounded-full text-neutral-800 hover:bg-black/10 dark:text-neutral-200 dark:hover:bg-white/10"
>
<BellIcon className="size-5" />
{count > 0 ? (
<span className="absolute right-0 top-0 block size-2 rounded-full bg-teal-500 ring-1 ring-black/5" />
) : null}
</button>
);
}

View File

@@ -1,61 +0,0 @@
import { Spinner } from "@lume/ui";
import { Note } from "@/components/note";
import { Await, createFileRoute, defer } from "@tanstack/react-router";
import { Suspense } from "react";
import { Virtualizer } from "virtua";
import { NostrQuery } from "@lume/system";
export const Route = createFileRoute("/activity/$account/texts")({
loader: async ({ params }) => {
return { data: defer(NostrQuery.getUserActivities(params.account, "1")) };
},
component: Screen,
});
function Screen() {
const { data } = Route.useLoaderData();
return (
<Virtualizer overscan={3}>
<Suspense
fallback={
<div className="flex h-20 w-full flex-col items-center justify-center gap-1">
<button
type="button"
className="inline-flex items-center gap-2 text-sm font-medium"
disabled
>
<Spinner className="size-5" />
Loading...
</button>
</div>
}
>
<Await promise={data}>
{(events) =>
events.map((event) => (
<div
key={event.id}
className="flex flex-col gap-2 mb-3 bg-white dark:bg-black/20 backdrop-blur-lg rounded-xl shadow-primary dark:ring-1 ring-neutral-800/50"
>
<Note.Provider event={event}>
<Note.Root>
<div className="px-3 h-14 flex items-center justify-between">
<Note.User />
<Note.Menu />
</div>
<Note.Activity className="px-3" />
<Note.Content className="px-3" quote={false} clean />
<div className="mt-3 flex items-center gap-4 h-14 px-3">
<Note.Open />
</div>
</Note.Root>
</Note.Provider>
</div>
))
}
</Await>
</Suspense>
</Virtualizer>
);
}

View File

@@ -1,50 +0,0 @@
import { Box, Container } from "@lume/ui";
import { cn } from "@lume/utils";
import { Link, Outlet } from "@tanstack/react-router";
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/activity/$account")({
component: Screen,
});
function Screen() {
const { account } = Route.useParams();
return (
<Container withDrag>
<Box className="scrollbar-none shadow-none bg-black/5 dark:bg-white/5 backdrop-blur-sm flex flex-col overflow-y-auto">
<div className="h-14 shrink-0 flex w-full items-center gap-1 px-3">
<div className="inline-flex h-full w-full items-center gap-1">
<Link to="/activity/$account/texts" params={{ account }}>
{({ isActive }) => (
<div
className={cn(
"inline-flex h-7 w-max items-center justify-center gap-2 rounded-full px-3 text-sm font-medium",
isActive ? "bg-neutral-50 dark:bg-white/10" : "opacity-50",
)}
>
Notes
</div>
)}
</Link>
<Link to="/activity/$account/zaps" params={{ account }}>
{({ isActive }) => (
<div
className={cn(
"inline-flex h-7 w-max items-center justify-center gap-2 rounded-full px-3 text-sm font-medium",
isActive ? "bg-neutral-50 dark:bg-white/10" : "opacity-50",
)}
>
Zaps
</div>
)}
</Link>
</div>
</div>
<div className="px-2 flex-1 overflow-y-auto w-full h-full scrollbar-none">
<Outlet />
</div>
</Box>
</Container>
);
}

View File

@@ -1,67 +0,0 @@
import { User } from "@/components/user";
import { NostrQuery } from "@lume/system";
import { Spinner } from "@lume/ui";
import { decodeZapInvoice } from "@lume/utils";
import { Await, createFileRoute, defer } from "@tanstack/react-router";
import { Suspense } from "react";
import { Virtualizer } from "virtua";
export const Route = createFileRoute("/activity/$account/zaps")({
loader: async ({ params }) => {
return {
data: defer(NostrQuery.getUserActivities(params.account, "9735")),
};
},
component: Screen,
});
function Screen() {
const { data } = Route.useLoaderData();
return (
<Virtualizer overscan={3}>
<Suspense
fallback={
<div className="flex h-20 w-full flex-col items-center justify-center gap-1">
<button
type="button"
className="inline-flex items-center gap-2 text-sm font-medium"
disabled
>
<Spinner className="size-5" />
Loading...
</button>
</div>
}
>
<Await promise={data}>
{(events) =>
events.map((event) => (
<div
key={event.id}
className="flex flex-col gap-2 mb-3 bg-white dark:bg-black/20 backdrop-blur-lg rounded-xl shadow-primary dark:ring-1 ring-neutral-800/50"
>
<User.Provider pubkey={event.pubkey}>
<User.Root className="flex flex-col">
<div className="text-lg h-20 font-medium leading-tight flex w-full items-center justify-center">
{decodeZapInvoice(event.tags).bitcoinFormatted}
</div>
<div className="h-11 border-t border-neutral-100 dark:border-neutral-900 flex items-center gap-1 px-2">
<div className="inline-flex items-center gap-2">
<User.Avatar className="size-7 rounded-full shrink-0" />
<User.Name className="text-sm font-medium" />
</div>
<div className="text-sm text-neutral-700 dark:text-neutral-300">
zapped you
</div>
</div>
</User.Root>
</User.Provider>
</div>
))
}
</Await>
</Suspense>
</Virtualizer>
);
}

View File

@@ -33,7 +33,8 @@ function Screen() {
}
return navigate({
to: "/auth/settings",
to: "/auth/$account/settings",
params: { account },
});
}

View File

@@ -3,12 +3,11 @@ import { NostrQuery } from "@lume/system";
import { Spinner } from "@lume/ui";
import * as Switch from "@radix-ui/react-switch";
import { createFileRoute } from "@tanstack/react-router";
import { requestPermission } from "@tauri-apps/plugin-notification";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
export const Route = createFileRoute("/auth/settings")({
export const Route = createFileRoute("/auth/$account/settings")({
beforeLoad: async () => {
const settings = await NostrQuery.getSettings();
return { settings };
@@ -19,6 +18,7 @@ export const Route = createFileRoute("/auth/settings")({
function Screen() {
const { settings } = Route.useRouteContext();
const { account } = Route.useParams();
const { t } = useTranslation();
const [newSettings, setNewSettings] = useState(settings);
@@ -26,21 +26,6 @@ function Screen() {
const navigate = Route.useNavigate();
const toggleNofitication = async () => {
await requestPermission();
setNewSettings((prev) => ({
...prev,
notification: !newSettings.notification,
}));
};
const toggleAutoUpdate = () => {
setNewSettings((prev) => ({
...prev,
autoUpdate: !newSettings.autoUpdate,
}));
};
const toggleEnhancedPrivacy = () => {
setNewSettings((prev) => ({
...prev,
@@ -72,7 +57,8 @@ function Screen() {
if (eventId) {
return navigate({
to: "/",
to: "/$account/home",
params: { account },
replace: true,
});
}
@@ -99,22 +85,6 @@ function Screen() {
</div>
<div className="flex flex-col gap-5">
<div className="flex flex-col gap-3">
<div className="flex w-full items-start justify-between gap-4 rounded-lg bg-neutral-100 px-5 py-4 dark:bg-white/10">
<div className="flex-1">
<h3 className="font-semibold">Push Notification</h3>
<p className="text-sm text-neutral-700 dark:text-neutral-300">
Enabling push notifications will allow you to receive
notifications from Lume.
</p>
</div>
<Switch.Root
checked={newSettings.notification}
onClick={() => toggleNofitication()}
className="relative mt-1 h-7 w-12 shrink-0 cursor-default rounded-full bg-neutral-200 outline-none data-[state=checked]:bg-blue-500 dark:bg-white/20"
>
<Switch.Thumb className="block size-6 translate-x-0.5 rounded-full bg-white transition-transform duration-100 will-change-transform data-[state=checked]:translate-x-[19px]" />
</Switch.Root>
</div>
<div className="flex w-full items-start justify-between gap-4 rounded-lg bg-neutral-100 px-5 py-4 dark:bg-white/10">
<div className="flex-1">
<h3 className="font-semibold">Enhanced Privacy</h3>
@@ -131,21 +101,6 @@ function Screen() {
<Switch.Thumb className="block size-6 translate-x-0.5 rounded-full bg-white transition-transform duration-100 will-change-transform data-[state=checked]:translate-x-[19px]" />
</Switch.Root>
</div>
<div className="flex w-full items-start justify-between gap-4 rounded-lg bg-neutral-100 px-5 py-4 dark:bg-white/10">
<div className="flex-1">
<h3 className="font-semibold">Auto Update</h3>
<p className="text-sm text-neutral-700 dark:text-neutral-300">
Automatically download and install new version.
</p>
</div>
<Switch.Root
checked={newSettings.autoUpdate}
onClick={() => toggleAutoUpdate()}
className="relative mt-1 h-7 w-12 shrink-0 cursor-default rounded-full bg-neutral-200 outline-none data-[state=checked]:bg-blue-500 dark:bg-white/20"
>
<Switch.Thumb className="block size-6 translate-x-0.5 rounded-full bg-white transition-transform duration-100 will-change-transform data-[state=checked]:translate-x-[19px]" />
</Switch.Root>
</div>
<div className="flex w-full items-start justify-between gap-4 rounded-lg bg-neutral-100 px-5 py-4 dark:bg-white/10">
<div className="flex-1">
<h3 className="font-semibold">Zap</h3>
@@ -185,7 +140,7 @@ function Screen() {
disabled={loading}
className="mb-1 inline-flex h-11 w-full shrink-0 items-center justify-center rounded-lg bg-blue-500 font-semibold text-white hover:bg-blue-600 disabled:opacity-50"
>
{t("global.continue")}
{loading ? <Spinner /> : t("global.continue")}
</button>
</div>
</div>

View File

@@ -28,7 +28,8 @@ function Screen() {
if (npub) {
navigate({
to: "/auth/settings",
to: "/auth/$account/settings",
params: { account: npub },
replace: true,
});
}

View File

@@ -23,11 +23,12 @@ function Screen() {
try {
setLoading(true);
const npub = await NostrAccount.connectRemoteAccount(uri);
const remoteAccount = await NostrAccount.connectRemoteAccount(uri);
if (npub) {
navigate({
to: "/auth/settings",
if (remoteAccount?.length) {
return navigate({
to: "/auth/$account/settings",
params: { account: remoteAccount },
replace: true,
});
}

View File

@@ -41,9 +41,6 @@ function Screen() {
return navigate({
to: "/$account/home",
params: { account: npub },
search: {
accounts: context.accounts,
},
replace: true,
});
}
@@ -60,7 +57,10 @@ function Screen() {
});
return (
<div className="relative flex h-full w-full items-center justify-center">
<div
data-tauri-drag-region
className="relative flex h-full w-full items-center justify-center"
>
<div className="relative z-20 flex flex-col items-center gap-16">
<div className="text-center">
<h2 className="text-xl text-neutral-700 dark:text-neutral-300">

View File

@@ -0,0 +1,314 @@
import { Note } from "@/components/note";
import { User } from "@/components/user";
import { LumeWindow, NostrQuery, useEvent } from "@lume/system";
import { Kind, NostrEvent } from "@lume/types";
import { createFileRoute } from "@tanstack/react-router";
import { getCurrent } from "@tauri-apps/api/window";
import { useEffect, useMemo, useState } from "react";
import * as Tabs from "@radix-ui/react-tabs";
import { InfoIcon, RepostIcon, SettingsIcon } from "@lume/icons";
import { decodeZapInvoice, formatCreatedAt } from "@lume/utils";
interface EmitAccount {
account: string;
}
export const Route = createFileRoute("/panel")({
component: Screen,
});
function Screen() {
const [account, setAccount] = useState<string>(null);
const [events, setEvents] = useState<NostrEvent[]>([]);
const texts = useMemo(
() => events.filter((ev) => ev.kind === Kind.Text),
[events],
);
const zaps = useMemo(() => {
const groups = new Map<string, NostrEvent[]>();
const list = events.filter((ev) => ev.kind === Kind.ZapReceipt);
for (const event of list) {
const rootId = event.tags.filter((tag) => tag[0] === "e")[0]?.[1];
if (rootId) {
if (groups.has(rootId)) {
groups.get(rootId).push(event);
} else {
groups.set(rootId, [event]);
}
}
}
return groups;
}, [events]);
const reactions = useMemo(() => {
const groups = new Map<string, NostrEvent[]>();
const list = events.filter(
(ev) => ev.kind === Kind.Repost || ev.kind === Kind.Reaction,
);
for (const event of list) {
const rootId = event.tags.filter((tag) => tag[0] === "e")[0]?.[1];
if (rootId) {
if (groups.has(rootId)) {
groups.get(rootId).push(event);
} else {
groups.set(rootId, [event]);
}
}
}
return groups;
}, [events]);
useEffect(() => {
if (account?.length && account?.startsWith("npub1")) {
NostrQuery.getNotifications()
.then((data) => {
const sorted = data.sort((a, b) => b.created_at - a.created_at);
setEvents(sorted);
})
.catch((e) => console.log(e));
}
}, [account]);
useEffect(() => {
const unlistenLoad = getCurrent().listen<EmitAccount>(
"load-notification",
(data) => {
setAccount(data.payload.account);
},
);
const unlistenNewEvent = getCurrent().listen("notification", (data) => {
const event: NostrEvent = JSON.parse(data.payload as string);
setEvents((prev) => [event, ...prev]);
});
return () => {
unlistenLoad.then((f) => f());
unlistenNewEvent.then((f) => f());
};
}, []);
if (!account) {
return (
<div className="w-full h-full flex items-center justify-center text-sm">
Please log in.
</div>
);
}
return (
<div className="w-full h-full flex flex-col">
<div className="h-11 shrink-0 flex items-center justify-between border-b border-black/5 px-4">
<div>
<h1 className="text-sm font-semibold">Notifications</h1>
</div>
<div className="inline-flex items-center gap-2">
<User.Provider pubkey={account}>
<User.Root>
<User.Avatar className="size-7 rounded-full" />
</User.Root>
</User.Provider>
<button
type="button"
onClick={() => LumeWindow.openSettings()}
className="size-7 inline-flex items-center justify-center bg-black/5 dark:bg-white/5 rounded-full"
>
<SettingsIcon className="size-4" />
</button>
</div>
</div>
<Tabs.Root
defaultValue="replies"
className="flex-1 overflow-y-auto overflow-x-hidden scrollbar-none"
>
<Tabs.List className="flex items-center">
<Tabs.Trigger
className="flex-1 inline-flex h-8 items-center justify-center gap-2 px-2 text-sm font-medium border-b border-black/10 data-[state=active]:border-black/30 dark:data-[state=active] data-[state=inactive]:opacity-50"
value="replies"
>
Replies
</Tabs.Trigger>
<Tabs.Trigger
className="flex-1 inline-flex h-8 items-center justify-center gap-2 px-2 text-sm font-medium border-b border-black/10 data-[state=active]:border-black/30 dark:data-[state=active] data-[state=inactive]:opacity-50"
value="reactions"
>
Reactions
</Tabs.Trigger>
<Tabs.Trigger
className="flex-1 inline-flex h-8 items-center justify-center gap-2 px-2 text-sm font-medium border-b border-black/10 data-[state=active]:border-black/30 dark:data-[state=active] data-[state=inactive]:opacity-50"
value="zaps"
>
Zaps
</Tabs.Trigger>
</Tabs.List>
<div className="p-2">
<Tabs.Content value="replies" className="flex flex-col gap-2">
{texts.map((event) => (
<TextNote event={event} />
))}
</Tabs.Content>
<Tabs.Content value="reactions" className="flex flex-col gap-2">
{[...reactions.entries()].map(([root, events]) => (
<div
key={root}
className="shrink-0 flex flex-col gap-1 rounded-lg p-2 backdrop-blur-md bg-black/10 dark:bg-white/10"
>
<div className="min-w-0 flex-1 flex flex-col gap-2">
<div className="pb-2 border-b border-black/5 dark:border-white/5 flex items-center gap-2">
<RootNote id={root} />
</div>
<div className="flex flex-wrap items-center gap-3">
{events.map((event) => (
<User.Provider pubkey={event.pubkey}>
<User.Root className="shrink-0 flex rounded-full h-8 bg-black/10 dark:bg-white/10 backdrop-blur-md p-[2px]">
<User.Avatar className="flex-1 size-7 rounded-full" />
<div className="flex-1 size-7 rounded-full inline-flex items-center justify-center text-xs truncate">
{event.kind === Kind.Reaction ? (
event.content === "+" ? (
"👍"
) : (
event.content
)
) : (
<RepostIcon className="size-4 dark:text-teal-600 text-teal-400" />
)}
</div>
</User.Root>
</User.Provider>
))}
</div>
</div>
</div>
))}
</Tabs.Content>
<Tabs.Content value="zaps" className="flex flex-col gap-2">
{[...zaps.entries()].map(([root, events]) => (
<div
key={root}
className="shrink-0 flex flex-col gap-1 rounded-lg p-2 backdrop-blur-md bg-black/10 dark:bg-white/10"
>
<div className="min-w-0 flex-1 flex flex-col gap-2">
<div className="pb-2 border-b border-black/5 dark:border-white/5 flex items-center gap-2">
<RootNote id={root} />
</div>
<div className="flex flex-wrap items-center gap-3">
{events.map((event) => (
<User.Provider
pubkey={event.tags.find((tag) => tag[0] == "P")[1]}
>
<User.Root className="shrink-0 flex gap-1.5 rounded-full h-8 bg-black/10 dark:bg-white/10 backdrop-blur-md p-[2px]">
<User.Avatar className="flex-1 size-7 rounded-full" />
<div className="flex-1 h-7 w-max pr-1.5 rounded-full inline-flex items-center justify-center text-sm truncate">
{decodeZapInvoice(event.tags).bitcoinFormatted}
</div>
</User.Root>
</User.Provider>
))}
</div>
</div>
</div>
))}
</Tabs.Content>
</div>
</Tabs.Root>
</div>
);
}
function RootNote({ id }: { id: string }) {
const { isLoading, isError, data } = useEvent(id);
if (isLoading) {
return (
<div className="pb-2 mb-2 flex items-center">
<div className="size-8 shrink-0 rounded-full bg-black/20 dark:bg-white/20 animate-pulse" />
<div className="animate-pulse rounded-md h-4 w-2/3 bg-black/20 dark:bg-white/20" />
</div>
);
}
if (isError || !data) {
return (
<div className="flex items-center gap-2">
<div className="size-8 shrink-0 rounded-full bg-red-500 text-white inline-flex items-center justify-center">
<InfoIcon className="size-5" />
</div>
<p className="text-red-500 text-sm">
Event not found with your current relay set
</p>
</div>
);
}
return (
<Note.Provider event={data}>
<Note.Root className="flex items-center gap-2">
<User.Provider pubkey={data.pubkey}>
<User.Root className="shrink-0">
<User.Avatar className="size-8 shrink-0 rounded-full" />
</User.Root>
</User.Provider>
<div className="line-clamp-1">{data.content}</div>
</Note.Root>
</Note.Provider>
);
}
function TextNote({ event }: { event: NostrEvent }) {
const pTags = event.tags
.filter((tag) => tag[0] === "p")
.map((tag) => tag[1])
.slice(0, 3);
return (
<button
type="button"
key={event.id}
onClick={() => LumeWindow.openEvent(event)}
>
<Note.Provider event={event}>
<Note.Root className="shrink-0 flex flex-col gap-1 rounded-lg p-2 backdrop-blur-md bg-black/10 dark:bg-white/10">
<User.Provider pubkey={event.pubkey}>
<User.Root className="inline-flex items-center gap-2">
<User.Avatar className="size-9 shrink-0 rounded-full" />
<div className="flex-1 flex flex-col">
<div className="w-full flex items-baseline justify-between">
<User.Name className="leading-tight text-sm font-semibold" />
<span className="leading-tight text-sm text-black/50 dark:text-white/50">
{formatCreatedAt(event.created_at)}
</span>
</div>
<div className="inline-flex items-baseline gap-1 text-xs">
<span className="leading-tight text-black/50 dark:text-white/50">
Reply to:
</span>
<div className="inline-flex items-baseline gap-1">
{pTags.map((replyTo) => (
<User.Provider pubkey={replyTo}>
<User.Root>
<User.Name className="leading-tight font-medium" />
</User.Root>
</User.Provider>
))}
</div>
</div>
</div>
</User.Root>
</User.Provider>
<div className="flex gap-2">
<div className="w-9 shrink-0" />
<div className="line-clamp-1">{event.content}</div>
</div>
</Note.Root>
</Note.Provider>
</button>
);
}

View File

@@ -1,36 +1,35 @@
{
"name": "lume",
"private": true,
"version": "4.0.0",
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev",
"web:dev": "turbo run dev --filter web",
"desktop:dev": "turbo run dev --filter desktop2",
"desktop:build": "turbo run build --filter desktop2",
"tauri": "tauri"
},
"devDependencies": {
"@biomejs/biome": "1.7.3",
"@tauri-apps/cli": "2.0.0-beta.12",
"turbo": "^1.13.3"
},
"packageManager": "pnpm@8.9.0",
"engines": {
"node": ">=18"
},
"dependencies": {
"@tauri-apps/api": "2.0.0-beta.13",
"@tauri-apps/plugin-autostart": "2.0.0-beta.3",
"@tauri-apps/plugin-clipboard-manager": "2.1.0-beta.1",
"@tauri-apps/plugin-dialog": "2.0.0-beta.3",
"@tauri-apps/plugin-fs": "2.0.0-beta.3",
"@tauri-apps/plugin-http": "2.0.0-beta.3",
"@tauri-apps/plugin-notification": "2.0.0-beta.3",
"@tauri-apps/plugin-os": "2.0.0-beta.3",
"@tauri-apps/plugin-process": "2.0.0-beta.3",
"@tauri-apps/plugin-shell": "2.0.0-beta.4",
"@tauri-apps/plugin-updater": "2.0.0-beta.3",
"@tauri-apps/plugin-upload": "2.0.0-beta.4"
}
"name": "lume",
"private": true,
"version": "4.0.0",
"scripts": {
"build": "turbo run build",
"dev": "turbo run dev",
"web:dev": "turbo run dev --filter web",
"desktop:dev": "turbo run dev --filter desktop2",
"desktop:build": "turbo run build --filter desktop2",
"tauri": "tauri"
},
"devDependencies": {
"@biomejs/biome": "1.7.3",
"@tauri-apps/cli": "2.0.0-beta.20",
"turbo": "^1.13.3"
},
"packageManager": "pnpm@8.9.0",
"engines": {
"node": ">=18"
},
"dependencies": {
"@tauri-apps/api": "2.0.0-beta.13",
"@tauri-apps/plugin-clipboard-manager": "2.1.0-beta.3",
"@tauri-apps/plugin-dialog": "2.0.0-beta.5",
"@tauri-apps/plugin-fs": "2.0.0-beta.5",
"@tauri-apps/plugin-http": "2.0.0-beta.5",
"@tauri-apps/plugin-notification": "2.0.0-beta.5",
"@tauri-apps/plugin-os": "2.0.0-beta.5",
"@tauri-apps/plugin-process": "2.0.0-beta.5",
"@tauri-apps/plugin-shell": "2.0.0-beta.6",
"@tauri-apps/plugin-updater": "2.0.0-beta.5",
"@tauri-apps/plugin-upload": "2.0.0-beta.6"
}
}

View File

@@ -1,5 +1,6 @@
import { Metadata } from "@lume/types";
import { commands } from "./commands";
import { Result, commands } from "./commands";
import { Window } from "@tauri-apps/api/window";
export class NostrAccount {
static async getAccounts() {
@@ -13,9 +14,19 @@ export class NostrAccount {
}
static async loadAccount(npub: string) {
const query = await commands.loadAccount(npub);
const bunker: string = localStorage.getItem(`${npub}_bunker`);
let query: Result<boolean, string>;
if (bunker?.length && bunker?.startsWith("bunker://")) {
query = await commands.loadAccount(npub, bunker);
} else {
query = await commands.loadAccount(npub, null);
}
if (query.status === "ok") {
const panel = Window.getByLabel("panel");
panel.emit("load-notification", { account: npub }); // trigger load notification
return query.data;
} else {
throw new Error(query.error);
@@ -62,19 +73,19 @@ export class NostrAccount {
}
static async connectRemoteAccount(uri: string) {
const remoteKey = uri.replace("bunker://", "").split("?")[0];
const npub = await commands.toNpub(remoteKey);
const connect = await commands.connectRemoteAccount(uri);
if (npub.status === "ok") {
const connect = await commands.nostrConnect(npub.data, uri);
if (connect.status === "ok") {
const npub = connect.data;
const parsed = new URL(uri);
parsed.searchParams.delete("secret");
if (connect.status === "ok") {
return connect.data;
} else {
throw new Error(connect.error);
}
// save connection string
localStorage.setItem(`${npub}_bunker`, parsed.toString());
return npub;
} else {
throw new Error(npub.error);
throw new Error(connect.error);
}
}

View File

@@ -1,5 +1,8 @@
// This file was generated by [tauri-specta](https://github.com/oscartbeaumont/tauri-specta). Do not edit this file manually.
/** user-defined commands **/
export const commands = {
async getRelays() : Promise<Result<Relays, null>> {
try {
@@ -57,17 +60,17 @@ try {
else return { status: "error", error: e as any };
}
},
async nostrConnect(npub: string, uri: string) : Promise<Result<string, string>> {
async connectRemoteAccount(uri: string) : Promise<Result<string, string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("nostr_connect", { npub, uri }) };
return { status: "ok", data: await TAURI_INVOKE("connect_remote_account", { uri }) };
} catch (e) {
if(e instanceof Error) throw e;
else return { status: "error", error: e as any };
}
},
async loadAccount(npub: string) : Promise<Result<boolean, string>> {
async loadAccount(npub: string, bunker: string | null) : Promise<Result<boolean, string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("load_account", { npub }) };
return { status: "ok", data: await TAURI_INVOKE("load_account", { npub, bunker }) };
} catch (e) {
if(e instanceof Error) throw e;
else return { status: "error", error: e as any };
@@ -89,14 +92,6 @@ try {
else return { status: "error", error: e as any };
}
},
async toNpub(hex: string) : Promise<Result<string, null>> {
try {
return { status: "ok", data: await TAURI_INVOKE("to_npub", { hex }) };
} catch (e) {
if(e instanceof Error) throw e;
else return { status: "error", error: e as any };
}
},
async verifyNip05(key: string, nip05: string) : Promise<Result<boolean, string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("verify_nip05", { key, nip05 }) };
@@ -105,14 +100,6 @@ try {
else return { status: "error", error: e as any };
}
},
async getActivities(account: string, kind: string) : Promise<Result<string[], string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("get_activities", { account, kind }) };
} catch (e) {
if(e instanceof Error) throw e;
else return { status: "error", error: e as any };
}
},
async getCurrentUserProfile() : Promise<Result<string, string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("get_current_user_profile") };
@@ -233,6 +220,14 @@ try {
else return { status: "error", error: e as any };
}
},
async getNotifications() : Promise<Result<string[], string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("get_notifications") };
} catch (e) {
if(e instanceof Error) throw e;
else return { status: "error", error: e as any };
}
},
async getEvent(id: string) : Promise<Result<string, string>> {
try {
return { status: "ok", data: await TAURI_INVOKE("get_event", { id }) };
@@ -345,8 +340,14 @@ await TAURI_INVOKE("set_badge", { count });
}
}
/** user-defined events **/
/** user-defined statics **/
/** user-defined types **/
export type Account = { npub: string; nsec: string }

View File

@@ -22,7 +22,7 @@ export class LumeEvent {
return this.tags.filter((tag) => tag[0] === "p").map((tag) => tag[1]);
}
static getEventThread(tags: string[][], gossip?: boolean) {
static getEventThread(tags: string[][], gossip = true) {
let root: string = null;
let reply: string = null;
@@ -30,14 +30,22 @@ export class LumeEvent {
const events = tags.filter((el) => el[0] === "e" && el[3] !== "mention");
if (gossip) {
const relays = tags.filter((el) => el[0] === "e" && el[2]?.length);
const relays = tags
.filter((el) => el[0] === "e" && el[2]?.length)
.map((tag) => tag[2]);
if (relays.length >= 1) {
for (const relay of relays) {
if (relay[2]?.length)
commands
.connectRelay(relay[2])
.then(() => console.log("[gossip]: ", relay[2]));
try {
if (relay.length) {
const url = new URL(relay);
commands
.connectRelay(url.toString())
.then(() => console.log("[relay hint]: ", url));
}
} catch (e) {
console.log("[relay hint] error: ", relay);
}
}
}
}
@@ -82,6 +90,14 @@ export class LumeEvent {
for (const tag of tags) {
const rootIndex = events.findIndex((el) => el.id === tag[1]);
// Relay Hint
if (tag[2]?.length) {
const url = new URL(tag[2]);
commands
.connectRelay(url.toString())
.then(() => console.log("[relay hint]: ", url));
}
if (rootIndex !== -1) {
const rootEvent = events[rootIndex];

View File

@@ -68,6 +68,18 @@ export class NostrQuery {
}
}
static async getNotifications() {
const query = await commands.getNotifications();
if (query.status === "ok") {
const events = query.data.map((item) => JSON.parse(item) as NostrEvent);
return events;
} else {
console.error(query.error);
return [];
}
}
static async getProfile(pubkey: string) {
const normalize = pubkey.replace("nostr:", "").replace(/[^\w\s]/gi, "");
const query = await commands.getProfile(normalize);
@@ -104,20 +116,6 @@ export class NostrQuery {
}
}
static async getUserActivities(
account: string,
kind: "1" | "6" | "9735" = "1",
) {
const query = await commands.getActivities(account, kind);
if (query.status === "ok") {
const events = query.data.map((item) => JSON.parse(item) as NostrEvent);
return events;
} else {
return [];
}
}
static async getLocalEvents(pubkeys: string[], asOf?: number) {
const until: string = asOf && asOf > 0 ? asOf.toString() : undefined;
const query = await commands.getLocalEvents(pubkeys, until);
@@ -242,15 +240,19 @@ export class NostrQuery {
const systemColumns: LumeColumn[] = JSON.parse(resourceFile);
const query = await commands.getNstore(NSTORE_KEYS.columns);
if (query.status === "ok") {
const columns: LumeColumn[] = query.data ? JSON.parse(query.data) : [];
try {
if (query.status === "ok") {
const columns: LumeColumn[] = JSON.parse(query.data);
if (columns.length < 1) {
if (!columns?.length) {
return systemColumns;
}
return columns;
} else {
return systemColumns;
}
return columns;
} else {
} catch {
return systemColumns;
}
}

View File

@@ -92,7 +92,7 @@ export class LumeWindow {
const query = await commands.openWindow(
label,
"Settings",
"/settings",
"/settings/general",
800,
500,
);

View File

@@ -110,7 +110,7 @@ export interface LumeColumn {
}
export interface EventColumns {
type: "add" | "remove" | "update" | "left" | "right" | "set_title";
type: "reset" | "add" | "remove" | "update" | "left" | "right" | "set_title";
label?: string;
title?: string;
column?: LumeColumn;

View File

@@ -13,7 +13,7 @@
"clsx": "^2.1.1",
"dayjs": "^1.11.11",
"light-bolt11-decoder": "^3.1.1",
"nostr-tools": "^2.6.0",
"nostr-tools": "^2.7.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"slate": "^0.103.0",

358
pnpm-lock.yaml generated
View File

@@ -11,46 +11,43 @@ importers:
'@tauri-apps/api':
specifier: 2.0.0-beta.13
version: 2.0.0-beta.13
'@tauri-apps/plugin-autostart':
specifier: 2.0.0-beta.3
version: 2.0.0-beta.3
'@tauri-apps/plugin-clipboard-manager':
specifier: 2.1.0-beta.1
version: 2.1.0-beta.1
specifier: 2.1.0-beta.3
version: 2.1.0-beta.3
'@tauri-apps/plugin-dialog':
specifier: 2.0.0-beta.3
version: 2.0.0-beta.3
specifier: 2.0.0-beta.5
version: 2.0.0-beta.5
'@tauri-apps/plugin-fs':
specifier: 2.0.0-beta.3
version: 2.0.0-beta.3
specifier: 2.0.0-beta.5
version: 2.0.0-beta.5
'@tauri-apps/plugin-http':
specifier: 2.0.0-beta.3
version: 2.0.0-beta.3
specifier: 2.0.0-beta.5
version: 2.0.0-beta.5
'@tauri-apps/plugin-notification':
specifier: 2.0.0-beta.3
version: 2.0.0-beta.3
specifier: 2.0.0-beta.5
version: 2.0.0-beta.5
'@tauri-apps/plugin-os':
specifier: 2.0.0-beta.3
version: 2.0.0-beta.3
specifier: 2.0.0-beta.5
version: 2.0.0-beta.5
'@tauri-apps/plugin-process':
specifier: 2.0.0-beta.3
version: 2.0.0-beta.3
specifier: 2.0.0-beta.5
version: 2.0.0-beta.5
'@tauri-apps/plugin-shell':
specifier: 2.0.0-beta.4
version: 2.0.0-beta.4
specifier: 2.0.0-beta.6
version: 2.0.0-beta.6
'@tauri-apps/plugin-updater':
specifier: 2.0.0-beta.3
version: 2.0.0-beta.3
specifier: 2.0.0-beta.5
version: 2.0.0-beta.5
'@tauri-apps/plugin-upload':
specifier: 2.0.0-beta.4
version: 2.0.0-beta.4
specifier: 2.0.0-beta.6
version: 2.0.0-beta.6
devDependencies:
'@biomejs/biome':
specifier: 1.7.3
version: 1.7.3
'@tauri-apps/cli':
specifier: 2.0.0-beta.12
version: 2.0.0-beta.12
specifier: 2.0.0-beta.20
version: 2.0.0-beta.20
turbo:
specifier: ^1.13.3
version: 1.13.3
@@ -93,6 +90,9 @@ importers:
'@radix-ui/react-switch':
specifier: ^1.0.3
version: 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-tabs':
specifier: ^1.0.4
version: 1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-tooltip':
specifier: ^1.0.7
version: 1.0.7(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
@@ -106,8 +106,8 @@ importers:
specifier: ^5.40.0
version: 5.40.0(@tanstack/react-query@5.40.0)(react@18.3.1)
'@tanstack/react-router':
specifier: ^1.34.5
version: 1.34.5(react-dom@18.3.1)(react@18.3.1)
specifier: ^1.34.9
version: 1.34.9(react-dom@18.3.1)(react@18.3.1)
i18next:
specifier: ^23.11.5
version: 23.11.5
@@ -120,9 +120,6 @@ importers:
nanoid:
specifier: ^5.0.7
version: 5.0.7
nostr-tools:
specifier: ^2.6.0
version: 2.6.0(typescript@5.4.5)
react:
specifier: ^18.3.1
version: 18.3.1
@@ -170,11 +167,11 @@ importers:
specifier: workspace:^
version: link:../../packages/types
'@tanstack/router-devtools':
specifier: ^1.34.5
version: 1.34.5(@tanstack/react-router@1.34.5)(csstype@3.1.3)(react-dom@18.3.1)(react@18.3.1)
specifier: ^1.34.9
version: 1.34.9(@tanstack/react-router@1.34.9)(csstype@3.1.3)(react-dom@18.3.1)(react@18.3.1)
'@tanstack/router-vite-plugin':
specifier: ^1.34.1
version: 1.34.1(vite@5.2.12)
specifier: ^1.34.8
version: 1.34.8(vite@5.2.12)
'@types/react':
specifier: ^18.3.3
version: 18.3.3
@@ -365,8 +362,8 @@ importers:
specifier: ^3.1.1
version: 3.1.1
nostr-tools:
specifier: ^2.6.0
version: 2.6.0(typescript@5.4.5)
specifier: ^2.7.0
version: 2.7.0(typescript@5.4.5)
react:
specifier: ^18.3.1
version: 18.3.1
@@ -521,7 +518,7 @@ packages:
engines: {node: ^18.17.1 || ^20.3.0 || >=21.0.0}
dependencies:
ci-info: 4.0.0
debug: 4.3.4
debug: 4.3.5
dlv: 1.1.3
dset: 3.1.3
is-docker: 3.0.0
@@ -557,7 +554,7 @@ packages:
'@babel/traverse': 7.24.6
'@babel/types': 7.24.6
convert-source-map: 2.0.0
debug: 4.3.4
debug: 4.3.5
gensync: 1.0.0-beta.2
json5: 2.2.3
semver: 6.3.1
@@ -818,7 +815,7 @@ packages:
'@babel/helper-split-export-declaration': 7.24.6
'@babel/parser': 7.24.6
'@babel/types': 7.24.6
debug: 4.3.4
debug: 4.3.5
globals: 11.12.0
transitivePeerDependencies:
- supports-color
@@ -2256,6 +2253,34 @@ packages:
react-dom: 18.3.1(react@18.3.1)
dev: false
/@radix-ui/react-tabs@1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-egZfYY/+wRNCflXNHx+dePvnz9FbmssDTJBtgRfDY7e8SE5oIo3Py2eCB1ckAbh1Q7cQ/6yJZThJ++sgbxibog==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
dependencies:
'@babel/runtime': 7.24.6
'@radix-ui/primitive': 1.0.1
'@radix-ui/react-context': 1.0.1(@types/react@18.3.3)(react@18.3.1)
'@radix-ui/react-direction': 1.0.1(@types/react@18.3.3)(react@18.3.1)
'@radix-ui/react-id': 1.0.1(@types/react@18.3.3)(react@18.3.1)
'@radix-ui/react-presence': 1.0.1(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-roving-focus': 1.0.4(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1)
'@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.3.3)(react@18.3.1)
'@types/react': 18.3.3
'@types/react-dom': 18.3.0
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
dev: false
/@radix-ui/react-tooltip@1.0.7(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-lPh5iKNFVQ/jav/j6ZrWq3blfDJ0OH9R6FlNUHPMqdLuQ9vwDgFsRxvl8b7Asuy5c8xmoojHUxKHQSOAvMHxyw==}
peerDependencies:
@@ -2562,8 +2587,8 @@ packages:
resolution: {integrity: sha512-CqYyepN4SnBopaoXYwng4NO8riB5ask/LTCkhOFq+GNGtr2X+aKeD767eYdqYukeixEUvv4bXdyTYVaogj7KBw==}
dev: false
/@swc/core-darwin-arm64@1.5.7:
resolution: {integrity: sha512-bZLVHPTpH3h6yhwVl395k0Mtx8v6CGhq5r4KQdAoPbADU974Mauz1b6ViHAJ74O0IVE5vyy7tD3OpkQxL/vMDQ==}
/@swc/core-darwin-arm64@1.5.24:
resolution: {integrity: sha512-M7oLOcC0sw+UTyAuL/9uyB9GeO4ZpaBbH76JSH6g1m0/yg7LYJZGRmplhDmwVSDAR5Fq4Sjoi1CksmmGkgihGA==}
engines: {node: '>=10'}
cpu: [arm64]
os: [darwin]
@@ -2571,8 +2596,8 @@ packages:
dev: true
optional: true
/@swc/core-darwin-x64@1.5.7:
resolution: {integrity: sha512-RpUyu2GsviwTc2qVajPL0l8nf2vKj5wzO3WkLSHAHEJbiUZk83NJrZd1RVbEknIMO7+Uyjh54hEh8R26jSByaw==}
/@swc/core-darwin-x64@1.5.24:
resolution: {integrity: sha512-MfcFjGGYognpSBSos2pYUNYJSmqEhuw5ceGr6qAdME7ddbjGXliza4W6FggsM+JnWwpqa31+e7/R+GetW4WkaQ==}
engines: {node: '>=10'}
cpu: [x64]
os: [darwin]
@@ -2580,8 +2605,8 @@ packages:
dev: true
optional: true
/@swc/core-linux-arm-gnueabihf@1.5.7:
resolution: {integrity: sha512-cTZWTnCXLABOuvWiv6nQQM0hP6ZWEkzdgDvztgHI/+u/MvtzJBN5lBQ2lue/9sSFYLMqzqff5EHKlFtrJCA9dQ==}
/@swc/core-linux-arm-gnueabihf@1.5.24:
resolution: {integrity: sha512-amI2pwtcWV3E/m/nf+AQtn1LWDzKLZyjCmWd3ms7QjEueWYrY8cU1Y4Wp7wNNsxIoPOi8zek1Uj2wwFD/pttNQ==}
engines: {node: '>=10'}
cpu: [arm]
os: [linux]
@@ -2589,8 +2614,8 @@ packages:
dev: true
optional: true
/@swc/core-linux-arm64-gnu@1.5.7:
resolution: {integrity: sha512-hoeTJFBiE/IJP30Be7djWF8Q5KVgkbDtjySmvYLg9P94bHg9TJPSQoC72tXx/oXOgXvElDe/GMybru0UxhKx4g==}
/@swc/core-linux-arm64-gnu@1.5.24:
resolution: {integrity: sha512-sTSvmqMmgT1ynH/nP75Pc51s+iT4crZagHBiDOf5cq+kudUYjda9lWMs7xkXB/TUKFHPCRK0HGunl8bkwiIbuw==}
engines: {node: '>=10'}
cpu: [arm64]
os: [linux]
@@ -2598,8 +2623,8 @@ packages:
dev: true
optional: true
/@swc/core-linux-arm64-musl@1.5.7:
resolution: {integrity: sha512-+NDhK+IFTiVK1/o7EXdCeF2hEzCiaRSrb9zD7X2Z7inwWlxAntcSuzZW7Y6BRqGQH89KA91qYgwbnjgTQ22PiQ==}
/@swc/core-linux-arm64-musl@1.5.24:
resolution: {integrity: sha512-vd2/hfOBGbrX21FxsFdXCUaffjkHvlZkeE2UMRajdXifwv79jqOHIJg3jXG1F3ZrhCghCzirFts4tAZgcG8XWg==}
engines: {node: '>=10'}
cpu: [arm64]
os: [linux]
@@ -2607,8 +2632,8 @@ packages:
dev: true
optional: true
/@swc/core-linux-x64-gnu@1.5.7:
resolution: {integrity: sha512-25GXpJmeFxKB+7pbY7YQLhWWjkYlR+kHz5I3j9WRl3Lp4v4UD67OGXwPe+DIcHqcouA1fhLhsgHJWtsaNOMBNg==}
/@swc/core-linux-x64-gnu@1.5.24:
resolution: {integrity: sha512-Zrdzi7NqzQxm2BvAG5KyOSBEggQ7ayrxh599AqqevJmsUXJ8o2nMiWQOBvgCGp7ye+Biz3pvZn1EnRzAp+TpUg==}
engines: {node: '>=10'}
cpu: [x64]
os: [linux]
@@ -2616,8 +2641,8 @@ packages:
dev: true
optional: true
/@swc/core-linux-x64-musl@1.5.7:
resolution: {integrity: sha512-0VN9Y5EAPBESmSPPsCJzplZHV26akC0sIgd3Hc/7S/1GkSMoeuVL+V9vt+F/cCuzr4VidzSkqftdP3qEIsXSpg==}
/@swc/core-linux-x64-musl@1.5.24:
resolution: {integrity: sha512-1F8z9NRi52jdZQCGc5sflwYSctL6omxiVmIFVp8TC9nngjQKc00TtX/JC2Eo2HwvgupkFVl5YQJidAck9YtmJw==}
engines: {node: '>=10'}
cpu: [x64]
os: [linux]
@@ -2625,8 +2650,8 @@ packages:
dev: true
optional: true
/@swc/core-win32-arm64-msvc@1.5.7:
resolution: {integrity: sha512-RtoNnstBwy5VloNCvmvYNApkTmuCe4sNcoYWpmY7C1+bPR+6SOo8im1G6/FpNem8AR5fcZCmXHWQ+EUmRWJyuA==}
/@swc/core-win32-arm64-msvc@1.5.24:
resolution: {integrity: sha512-cKpP7KvS6Xr0jFSTBXY53HZX/YfomK5EMQYpCVDOvfsZeYHN20sQSKXfpVLvA/q2igVt1zzy1XJcOhpJcgiKLg==}
engines: {node: '>=10'}
cpu: [arm64]
os: [win32]
@@ -2634,8 +2659,8 @@ packages:
dev: true
optional: true
/@swc/core-win32-ia32-msvc@1.5.7:
resolution: {integrity: sha512-Xm0TfvcmmspvQg1s4+USL3x8D+YPAfX2JHygvxAnCJ0EHun8cm2zvfNBcsTlnwYb0ybFWXXY129aq1wgFC9TpQ==}
/@swc/core-win32-ia32-msvc@1.5.24:
resolution: {integrity: sha512-IoPWfi0iwqjZuf7gE223+B97/ZwkKbu7qL5KzGP7g3hJrGSKAvv7eC5Y9r2iKKtLKyv5R/T6Ho0kFR/usi7rHw==}
engines: {node: '>=10'}
cpu: [ia32]
os: [win32]
@@ -2643,8 +2668,8 @@ packages:
dev: true
optional: true
/@swc/core-win32-x64-msvc@1.5.7:
resolution: {integrity: sha512-tp43WfJLCsKLQKBmjmY/0vv1slVywR5Q4qKjF5OIY8QijaEW7/8VwPyUyVoJZEnDgv9jKtUTG5PzqtIYPZGnyg==}
/@swc/core-win32-x64-msvc@1.5.24:
resolution: {integrity: sha512-zHgF2k1uVJL8KIW+PnVz1To4a3Cz9THbh2z2lbehaF/gKHugH4c3djBozU4das1v35KOqf5jWIEviBLql2wDLQ==}
engines: {node: '>=10'}
cpu: [x64]
os: [win32]
@@ -2652,12 +2677,12 @@ packages:
dev: true
optional: true
/@swc/core@1.5.7:
resolution: {integrity: sha512-U4qJRBefIJNJDRCCiVtkfa/hpiZ7w0R6kASea+/KLp+vkus3zcLSB8Ub8SvKgTIxjWpwsKcZlPf5nrv4ls46SQ==}
/@swc/core@1.5.24:
resolution: {integrity: sha512-Eph9zvO4xvqWZGVzTdtdEJ0Vqf0VIML/o/e4Qd2RLOqtfgnlRi7avmMu5C0oqciJ0tk+hqdUKVUZ4JPoPaiGvQ==}
engines: {node: '>=10'}
requiresBuild: true
peerDependencies:
'@swc/helpers': ^0.5.0
'@swc/helpers': '*'
peerDependenciesMeta:
'@swc/helpers':
optional: true
@@ -2665,16 +2690,16 @@ packages:
'@swc/counter': 0.1.3
'@swc/types': 0.1.7
optionalDependencies:
'@swc/core-darwin-arm64': 1.5.7
'@swc/core-darwin-x64': 1.5.7
'@swc/core-linux-arm-gnueabihf': 1.5.7
'@swc/core-linux-arm64-gnu': 1.5.7
'@swc/core-linux-arm64-musl': 1.5.7
'@swc/core-linux-x64-gnu': 1.5.7
'@swc/core-linux-x64-musl': 1.5.7
'@swc/core-win32-arm64-msvc': 1.5.7
'@swc/core-win32-ia32-msvc': 1.5.7
'@swc/core-win32-x64-msvc': 1.5.7
'@swc/core-darwin-arm64': 1.5.24
'@swc/core-darwin-x64': 1.5.24
'@swc/core-linux-arm-gnueabihf': 1.5.24
'@swc/core-linux-arm64-gnu': 1.5.24
'@swc/core-linux-arm64-musl': 1.5.24
'@swc/core-linux-x64-gnu': 1.5.24
'@swc/core-linux-x64-musl': 1.5.24
'@swc/core-win32-arm64-msvc': 1.5.24
'@swc/core-win32-ia32-msvc': 1.5.24
'@swc/core-win32-x64-msvc': 1.5.24
dev: true
/@swc/counter@0.1.3:
@@ -2749,8 +2774,8 @@ packages:
react: 18.3.1
dev: false
/@tanstack/react-router@1.34.5(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-mOMbNHSJ1cAgRuJj9W35wteQL7zFiCNJYgg3QHkxj+obO9zQWiAwycFs0hQTRxqzGfC+jhVLJe1+cW93BhqKyA==}
/@tanstack/react-router@1.34.9(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-dU7rkiWQTH7glJIcnUcFezkmANzqQwlgRiJFiHJOPjYSoKVGI9dWQMbNc8W5d4JjXB/Nhh0QOiI/+GoYZ3WrhA==}
engines: {node: '>=12'}
peerDependencies:
react: '>=16.8'
@@ -2774,15 +2799,15 @@ packages:
react-dom: 18.3.1(react@18.3.1)
use-sync-external-store: 1.2.2(react@18.3.1)
/@tanstack/router-devtools@1.34.5(@tanstack/react-router@1.34.5)(csstype@3.1.3)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-ps0SC14ijFkYkmvqRDlIo7M182Kinvs9syNbQJ5NZT3HjC4yNSZpKWIFkEEDL9IhQvIXQbk0FROz/c7Us7VpsA==}
/@tanstack/router-devtools@1.34.9(@tanstack/react-router@1.34.9)(csstype@3.1.3)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-8gc/ptUjiB/ry1jWCBcPNypfpfQ/eFtC4vqg8dfppvKUm2DwLp0gNM6YvGbcfySTLLei3RLPlmuob0/2vbeS7A==}
engines: {node: '>=12'}
peerDependencies:
'@tanstack/react-router': ^1.34.5
'@tanstack/react-router': ^1.34.9
react: '>=16.8'
react-dom: '>=16.8'
dependencies:
'@tanstack/react-router': 1.34.5(react-dom@18.3.1)(react@18.3.1)
'@tanstack/react-router': 1.34.9(react-dom@18.3.1)(react@18.3.1)
clsx: 2.1.1
date-fns: 2.30.0
goober: 2.1.14(csstype@3.1.3)
@@ -2792,16 +2817,16 @@ packages:
- csstype
dev: true
/@tanstack/router-generator@1.33.12:
resolution: {integrity: sha512-CTZ9yWJkmV4hmVqZnqlrTBfAnjqkWkqF0V5kRdNNK7Q8mfUhL5WPAdZfrCVhq7uBFDEJ1XyMVwn2lQytAMrHnA==}
/@tanstack/router-generator@1.34.8:
resolution: {integrity: sha512-xi0otLis4zQ8lYVgvNfUTDHKnboL2swXpHcLiG+bKYDihPnw4r3qRO0ULGsqV1n/w6yZAaQ2tZwvZ7Zf0G2pPg==}
engines: {node: '>=12'}
dependencies:
prettier: 3.2.5
zod: 3.23.8
dev: true
/@tanstack/router-vite-plugin@1.34.1(vite@5.2.12):
resolution: {integrity: sha512-UtSDFKdDzhiFuUUHQp+/bUYkTLnJeYnVXEdXo+D7rUH12i9WFpbZY5dcG3Nj/c8y4WTljwtE6e8N/+yAyXYKeg==}
/@tanstack/router-vite-plugin@1.34.8(vite@5.2.12):
resolution: {integrity: sha512-FZRl7GVda/1NMEbnalXhYI2e9Qe8MduxXRNZ2cJMYgPR/FxMiao+XQAN+mTD4OoMxJ8gKY+IshgRxCc3xwu6bA==}
engines: {node: '>=12'}
dependencies:
'@babel/core': 7.24.6
@@ -2813,7 +2838,7 @@ packages:
'@babel/template': 7.24.6
'@babel/traverse': 7.24.6
'@babel/types': 7.24.6
'@tanstack/router-generator': 1.33.12
'@tanstack/router-generator': 1.34.8
'@types/babel__core': 7.20.5
'@types/babel__generator': 7.6.8
'@types/babel__template': 7.4.4
@@ -2828,18 +2853,13 @@ packages:
/@tanstack/store@0.1.3:
resolution: {integrity: sha512-GnolmC8Fr4mvsHE1fGQmR3Nm0eBO3KnZjDU0a+P3TeQNM/dDscFGxtA7p31NplQNW3KwBw4t1RVFmz0VeKLxcw==}
/@tauri-apps/api@2.0.0-beta.11:
resolution: {integrity: sha512-wJRY+fBUm3KpqZDHMIz5HRv+1vlnvRJ/dFxiyY3NlINTx2qXqDou5qWYcP1CuZXsd39InWVPV3FAZvno/kGCkA==}
engines: {node: '>= 18', npm: '>= 6.6.0', yarn: '>= 1.19.1'}
dev: false
/@tauri-apps/api@2.0.0-beta.13:
resolution: {integrity: sha512-Np1opKANzRMF3lgJ9gDquBCB9SxlE2lRmNpVx1+L6RyzAmigkuh0ZulT5jMnDA3JLsuSDU135r/s4t/Pmx4atg==}
engines: {node: '>= 18', npm: '>= 6.6.0', yarn: '>= 1.19.1'}
dev: false
/@tauri-apps/cli-darwin-arm64@2.0.0-beta.12:
resolution: {integrity: sha512-+Ksrxc4QQE1R62OreCybM8PNnF090qhf+ZeRZTZ9JxOujz4Tq/RiE1vC5nRlDu+Cd3pL63fZ2TsANj2PnpN+qw==}
/@tauri-apps/cli-darwin-arm64@2.0.0-beta.20:
resolution: {integrity: sha512-oCJOCib7GuYkwkBXx+ekamR8NZZU+2i3MLP+DHpDxK5gS2uhCE+CBkamJkNt6y1x6xdVnwyqZOm5RvN4SRtyIA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
@@ -2847,8 +2867,8 @@ packages:
dev: true
optional: true
/@tauri-apps/cli-darwin-x64@2.0.0-beta.12:
resolution: {integrity: sha512-KEkcJp7jfWMEiRFTJ3/yJhy8EFwgJPNGnNYjKTD5CAba9wMexxs23U7YLAWdSNrbSgPAox0pBqXaSj6p2HH0Wg==}
/@tauri-apps/cli-darwin-x64@2.0.0-beta.20:
resolution: {integrity: sha512-lC5QSnRExedYN4Ds6ZlSvC2PxP8qfIYBJQ5ktf+PJI5gQALdNeVtd6YnTG1ODCEklfLq9WKkGwp7JdALTU5wDA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
@@ -2856,8 +2876,8 @@ packages:
dev: true
optional: true
/@tauri-apps/cli-linux-arm-gnueabihf@2.0.0-beta.12:
resolution: {integrity: sha512-EUDen1XNF/WfG5hjTZ6P/8IcpYPFm2Ox/FwlBPlePKZJrc+/3IveME/oyC3wrloscodV41yL2ise4SZuJtNAHA==}
/@tauri-apps/cli-linux-arm-gnueabihf@2.0.0-beta.20:
resolution: {integrity: sha512-nZCeBMHHye5DLOJV5k2w658hnCS+LYaOZ8y/G9l3ei+g0L/HBjlSy6r4simsAT5TG8+l3oCZzLBngfTMdDS/YA==}
engines: {node: '>= 10'}
cpu: [arm]
os: [linux]
@@ -2865,8 +2885,8 @@ packages:
dev: true
optional: true
/@tauri-apps/cli-linux-arm64-gnu@2.0.0-beta.12:
resolution: {integrity: sha512-VUhCBB6kaQmi7MyXTRccqemmz7s5n15Z718OIW1n6wI68oh0IsvC9KsrbDfgOLZVG6RXzmtzyXwnaIFQAgl+Cg==}
/@tauri-apps/cli-linux-arm64-gnu@2.0.0-beta.20:
resolution: {integrity: sha512-B79ISVLPVBgwnCchVqwTKU+vxnFYqxKomcR4rmsvxfs0NVtT5QuNzE1k4NUQnw3966yjwhYR3mnHsSJQSB4Eyw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
@@ -2874,8 +2894,8 @@ packages:
dev: true
optional: true
/@tauri-apps/cli-linux-arm64-musl@2.0.0-beta.12:
resolution: {integrity: sha512-eus/fGCQZPcvwEa2HH2TJh1Xb2FeeVrnxL6lQseLPkgD7Wcu5mWz07AlLCMDOreUobb5vyOmm0L7d1HMIPrvEQ==}
/@tauri-apps/cli-linux-arm64-musl@2.0.0-beta.20:
resolution: {integrity: sha512-ojIkv/1uZHhcrgfIN8xgn4BBeo/Xg+bnV0wer6lD78zyxkUMWeEZ+u3mae1ejCJNhhaZOxNaUQ67MvDOiGyr5Q==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
@@ -2883,8 +2903,8 @@ packages:
dev: true
optional: true
/@tauri-apps/cli-linux-x64-gnu@2.0.0-beta.12:
resolution: {integrity: sha512-C4SkDnjuhFSdSgsK8KNRj2b/oe/j1vrvI8+ZxUj+/o1jK71A2eiCN8pJlyMZYCN8wZUksGazxaFz62g0jceAEg==}
/@tauri-apps/cli-linux-x64-gnu@2.0.0-beta.20:
resolution: {integrity: sha512-xBy1FNbHKlc7T6pOmFQQPECxJaI5A9QWX7Kb9N64cNVusoOGlvc3xHYkXMS4PTr7xXOT0yiE1Ww2OwDRJ3lYsg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
@@ -2892,8 +2912,8 @@ packages:
dev: true
optional: true
/@tauri-apps/cli-linux-x64-musl@2.0.0-beta.12:
resolution: {integrity: sha512-V8NlJ8wgNcMfKTGOubgfjRyyJVQsZxpOZkGjdfn/YK0UNdOC6iLuqxIki47hbnoJMqvuxa37lr7Z1JVawOMUyw==}
/@tauri-apps/cli-linux-x64-musl@2.0.0-beta.20:
resolution: {integrity: sha512-+O6zq5jmtUxA1FUAAwF2ywPysy4NRo2Y6G+ESZDkY9XosRwdt5OUjqAsYktZA3AxDMZVei8r9buwTqUwi9ny/g==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
@@ -2901,8 +2921,8 @@ packages:
dev: true
optional: true
/@tauri-apps/cli-win32-arm64-msvc@2.0.0-beta.12:
resolution: {integrity: sha512-dIsE5U4JRoVImW0PX9K82/dMjwugqc3DmgKbFKLYXgTFmSNaTNdojI/5VOPmbOIMJ8BNGDF8sjS80I0PZuEqvw==}
/@tauri-apps/cli-win32-arm64-msvc@2.0.0-beta.20:
resolution: {integrity: sha512-RswgMbWyOQcv53CHvIuiuhAh4kKDqaGyZfWD4VlxqX/XhkoF5gsNgr0MxzrY7pmoL+89oVI+fiGVJz4nOQE5vA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
@@ -2910,8 +2930,8 @@ packages:
dev: true
optional: true
/@tauri-apps/cli-win32-ia32-msvc@2.0.0-beta.12:
resolution: {integrity: sha512-vTNrGzVV/LclD+4u+IOOwl1Ia2CqwZYK6AmMLp0ROLzbSn/9ROJJYe47V5VAZrnMjThEdb9fRL0FPYPk9yDaNA==}
/@tauri-apps/cli-win32-ia32-msvc@2.0.0-beta.20:
resolution: {integrity: sha512-5lgWmDVXhX3SBGbiv5SduM1yajiRnUEJClWhSdRrEEJeXdsxpCsBEhxYnUnDCEzPKxLLn5fdBv3VrVctJ03csQ==}
engines: {node: '>= 10'}
cpu: [ia32]
os: [win32]
@@ -2919,8 +2939,8 @@ packages:
dev: true
optional: true
/@tauri-apps/cli-win32-x64-msvc@2.0.0-beta.12:
resolution: {integrity: sha512-Z/++lJ1FraoN1ZaNxYuCynNm7SyEGC+yd/adYQvhaQyudZa5HW/8eFkLx8FRcIQkEkSqLBkHf9VpwjnTDBVlwQ==}
/@tauri-apps/cli-win32-x64-msvc@2.0.0-beta.20:
resolution: {integrity: sha512-SuSiiVQTQPSzWlsxQp/NMzWbzDS9TdVDOw7CCfgiG5wnT2GsxzrcIAVN6i7ILsVFLxrjr0bIgPldSJcdcH84Yw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
@@ -2928,87 +2948,81 @@ packages:
dev: true
optional: true
/@tauri-apps/cli@2.0.0-beta.12:
resolution: {integrity: sha512-MFh4Z093O+PXWI6KFR8E2zIOgpJ4zL8waDhFXVQgBpqiD4ieiqywjmbmNIWkVqYBGAia7ZI0juxpkZSyXT4f1A==}
/@tauri-apps/cli@2.0.0-beta.20:
resolution: {integrity: sha512-707q9uIc2oNrYHd2dtMvxTrpZXVpart5EIktnRymNOpphkLlB6WUBjHD+ga45WqTU6cNGKbYvkKqTNfshNul9Q==}
engines: {node: '>= 10'}
hasBin: true
optionalDependencies:
'@tauri-apps/cli-darwin-arm64': 2.0.0-beta.12
'@tauri-apps/cli-darwin-x64': 2.0.0-beta.12
'@tauri-apps/cli-linux-arm-gnueabihf': 2.0.0-beta.12
'@tauri-apps/cli-linux-arm64-gnu': 2.0.0-beta.12
'@tauri-apps/cli-linux-arm64-musl': 2.0.0-beta.12
'@tauri-apps/cli-linux-x64-gnu': 2.0.0-beta.12
'@tauri-apps/cli-linux-x64-musl': 2.0.0-beta.12
'@tauri-apps/cli-win32-arm64-msvc': 2.0.0-beta.12
'@tauri-apps/cli-win32-ia32-msvc': 2.0.0-beta.12
'@tauri-apps/cli-win32-x64-msvc': 2.0.0-beta.12
'@tauri-apps/cli-darwin-arm64': 2.0.0-beta.20
'@tauri-apps/cli-darwin-x64': 2.0.0-beta.20
'@tauri-apps/cli-linux-arm-gnueabihf': 2.0.0-beta.20
'@tauri-apps/cli-linux-arm64-gnu': 2.0.0-beta.20
'@tauri-apps/cli-linux-arm64-musl': 2.0.0-beta.20
'@tauri-apps/cli-linux-x64-gnu': 2.0.0-beta.20
'@tauri-apps/cli-linux-x64-musl': 2.0.0-beta.20
'@tauri-apps/cli-win32-arm64-msvc': 2.0.0-beta.20
'@tauri-apps/cli-win32-ia32-msvc': 2.0.0-beta.20
'@tauri-apps/cli-win32-x64-msvc': 2.0.0-beta.20
dev: true
/@tauri-apps/plugin-autostart@2.0.0-beta.3:
resolution: {integrity: sha512-+qIHvO+cfe8MnRxa23l5ebF5rZMt6Jd7FjCD9zoVm/KdYpb0VIhixsYvrLjty9eUWcp8k3XVknfttnvw/veHmg==}
/@tauri-apps/plugin-clipboard-manager@2.1.0-beta.3:
resolution: {integrity: sha512-j48jTdZWeX5BylIGLYbXtg6ynZAcFKDKK6dguHAbG3zviqNE/XMf2vHOPJeFDZsDE5kVOZZS7nKOj7T2WiCpvw==}
dependencies:
'@tauri-apps/api': 2.0.0-beta.11
'@tauri-apps/api': 2.0.0-beta.13
dev: false
/@tauri-apps/plugin-clipboard-manager@2.1.0-beta.1:
resolution: {integrity: sha512-nipr29/eQVlgNw6A1futwsFwWeeS0tBbq1Pww7yb+IZDKfhIFXYJ1NmrEcVGQ6vYz4KPIu+llR6D9pjKl/vSvA==}
/@tauri-apps/plugin-dialog@2.0.0-beta.5:
resolution: {integrity: sha512-jkaBCsx2v6WB6sB77fTMCeijuvT3FlzwschiHnPlD7aU6CHvQgRlpCv/FttPdTq4ih2t6MIlM4oX85hNYgfs6w==}
dependencies:
'@tauri-apps/api': 2.0.0-beta.11
'@tauri-apps/api': 2.0.0-beta.13
dev: false
/@tauri-apps/plugin-dialog@2.0.0-beta.3:
resolution: {integrity: sha512-B3KxLjFxCVB3AIYWN/xY9HM6tO7m4u6aQk1t0ZrBToXThowRU9f62527EO0oj0uNlyKlXx20up68NkpdlER0yg==}
/@tauri-apps/plugin-fs@2.0.0-beta.5:
resolution: {integrity: sha512-uTqCDFA1z8KDtTe5fKlbRrKV4zxh63UVUvD/PR8GnyNLV+qxj/fEuJuGvMdfMbVKrTljGqSpun5wnP5jqD5fMg==}
dependencies:
'@tauri-apps/api': 2.0.0-beta.11
'@tauri-apps/api': 2.0.0-beta.13
dev: false
/@tauri-apps/plugin-fs@2.0.0-beta.3:
resolution: {integrity: sha512-LBgA7S10NwcitHaugIfmCSkewz45vSz1VOpMHhzvE38i1r1KpuTSHlr3MZ0LLq93tH/lvhYZ+3LAml4Sriwthw==}
/@tauri-apps/plugin-http@2.0.0-beta.5:
resolution: {integrity: sha512-zRWWWCNw/TdewCSrBY8Racm2dwMpkzMOnFmHGExEIoklqUUIq3FU/hkqcX/67Nplf+t1y+WljOxKpCHbm1fOQw==}
dependencies:
'@tauri-apps/api': 2.0.0-beta.11
'@tauri-apps/api': 2.0.0-beta.13
dev: false
/@tauri-apps/plugin-http@2.0.0-beta.3:
resolution: {integrity: sha512-TsSFQje2F3Sy5FYuOV+TcgtlvpO9FVdODGT/dJQa+kq/a8Ocel6V57Emj2FiCIyhOJ84us+MiMbQWR1GTfa+mQ==}
/@tauri-apps/plugin-notification@2.0.0-beta.5:
resolution: {integrity: sha512-5zYK2aT1ZvR+LnuwsnTvg28iEhI7FiZSPvBqQ8y6fj28T4oIHVox19Pk4YRyTyfyJN1nZclXxVAqWLSXLI9SKQ==}
dependencies:
'@tauri-apps/api': 2.0.0-beta.11
'@tauri-apps/api': 2.0.0-beta.13
dev: false
/@tauri-apps/plugin-notification@2.0.0-beta.3:
resolution: {integrity: sha512-OCwvX10JrOgJh50HXIGD9EHgGUMtdYAZ+Q5peGCSS3/aA8XWL1GOENtC7CNb0P+W7HjRAj2N8jtEApgV0kD6Gg==}
/@tauri-apps/plugin-os@2.0.0-beta.5:
resolution: {integrity: sha512-Qfs/clZ9R05J+OVOGkko+9OaYaL+xJaGICeQ1G5CnLFpUdTfMV10D+1nBBauxDdiLU4ay5I0iprJ5aG5GJBunQ==}
dependencies:
'@tauri-apps/api': 2.0.0-beta.11
'@tauri-apps/api': 2.0.0-beta.13
dev: false
/@tauri-apps/plugin-os@2.0.0-beta.3:
resolution: {integrity: sha512-kuTfns6z7z/RKAqij3293fnSsgRHlogO/SzNWziFDGHpijGUm/peH70Cv45LKvHw+7kEMX+nfHFWOZm8UEVy/w==}
/@tauri-apps/plugin-process@2.0.0-beta.5:
resolution: {integrity: sha512-UMiBm6TtNYfxRb6GwzA4PalkZGwalHdclI/t0MVG33fNXgX1PaWONR/NZW/k7JE+WpvRAnN/Kf9ur8aEzjVVSQ==}
dependencies:
'@tauri-apps/api': 2.0.0-beta.11
'@tauri-apps/api': 2.0.0-beta.13
dev: false
/@tauri-apps/plugin-process@2.0.0-beta.3:
resolution: {integrity: sha512-N3TkHxoQQb45FP8mbKfcI+e6foR17QlLrrwvPK8SH+LhEP9uo1b5EUvpZM1TIegAmL/3LQIBtJw1loyHVn8Esg==}
/@tauri-apps/plugin-shell@2.0.0-beta.6:
resolution: {integrity: sha512-g3nM9cQQGl7Iv4MvyFuco/aPTiwOI/MixcoKso3VQIg5Aqd64NqR0r+GfsB0qx52txItqzSXwmeaj1eZjO9Q6Q==}
dependencies:
'@tauri-apps/api': 2.0.0-beta.11
'@tauri-apps/api': 2.0.0-beta.13
dev: false
/@tauri-apps/plugin-shell@2.0.0-beta.4:
resolution: {integrity: sha512-y9APB3ZLtkY4IM6syE+dn/R4XR2UTHBcAI/n9L5PgnvOjJAuuEzVYURn10G51nKnLw+sFbxO7LW4VW66Uoaa9g==}
/@tauri-apps/plugin-updater@2.0.0-beta.5:
resolution: {integrity: sha512-h8uNFQDtXaZPFyQcNAB5SxiSIvPbYRlM1fXf/lCcW8bAaMTanyszbHvR2vAYtVa/wIzKAOYriC/MpsJaKLjyhg==}
dependencies:
'@tauri-apps/api': 2.0.0-beta.11
'@tauri-apps/api': 2.0.0-beta.13
dev: false
/@tauri-apps/plugin-updater@2.0.0-beta.3:
resolution: {integrity: sha512-bD1ikPz80uK9YJKNYpYlA6StSp9lr0Ob1kGLG2XdmOgspv7SZLLNVzMORtKeqgepxwG99qdYGDDegT3Ll6+UlA==}
/@tauri-apps/plugin-upload@2.0.0-beta.6:
resolution: {integrity: sha512-CbIj5ew167/Xfi/tsomPlRhty8U52m+FXEIj2tVNu6F1Kc6NEfd5BQM6iLf6RfKQ4WsUo+zwUkftnlpgP4FCpA==}
dependencies:
'@tauri-apps/api': 2.0.0-beta.11
dev: false
/@tauri-apps/plugin-upload@2.0.0-beta.4:
resolution: {integrity: sha512-nToNaeA7NqDteIOBkjVeqqVDVniWjjGxpIIux3/PdFZwUivJGL50WXlu8n5ad/0B80t+dDjMdCYfF3tdb5j00g==}
dependencies:
'@tauri-apps/api': 2.0.0-beta.11
'@tauri-apps/api': 2.0.0-beta.13
dev: false
/@types/babel__core@7.20.5:
@@ -3110,7 +3124,7 @@ packages:
peerDependencies:
vite: ^4 || ^5
dependencies:
'@swc/core': 1.5.7
'@swc/core': 1.5.24
vite: 5.2.12
transitivePeerDependencies:
- '@swc/helpers'
@@ -3335,7 +3349,7 @@ packages:
common-ancestor-path: 1.0.1
cookie: 0.6.0
cssesc: 3.0.0
debug: 4.3.4
debug: 4.3.5
deterministic-object-hash: 2.0.2
devalue: 5.0.0
diff: 5.2.0
@@ -3367,7 +3381,7 @@ packages:
shiki: 1.6.1
string-width: 7.1.0
strip-ansi: 7.1.0
tsconfck: 3.0.3(typescript@5.4.5)
tsconfck: 3.1.0(typescript@5.4.5)
unist-util-visit: 5.0.0
vfile: 6.0.1
vite: 5.2.12
@@ -3467,7 +3481,7 @@ packages:
hasBin: true
dependencies:
caniuse-lite: 1.0.30001625
electron-to-chromium: 1.4.783
electron-to-chromium: 1.4.788
node-releases: 2.0.14
update-browserslist-db: 1.0.16(browserslist@4.23.0)
@@ -3649,8 +3663,8 @@ packages:
resolution: {integrity: sha512-okzr3f11N6WuqYtZSvm+F776mB41wRZMhKP+hc34YdW+KmtYYK9iqvHSwo2k9FEH3fhGXvOPV6yz2IcSrfRUDg==}
dev: false
/debug@4.3.4:
resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
/debug@4.3.5:
resolution: {integrity: sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==}
engines: {node: '>=6.0'}
peerDependencies:
supports-color: '*'
@@ -3723,8 +3737,8 @@ packages:
/eastasianwidth@0.2.0:
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
/electron-to-chromium@1.4.783:
resolution: {integrity: sha512-bT0jEz/Xz1fahQpbZ1D7LgmPYZ3iHVY39NcWWro1+hA2IvjiPeaXtfSqrQ+nXjApMvQRE2ASt1itSLRrebHMRQ==}
/electron-to-chromium@1.4.788:
resolution: {integrity: sha512-ubp5+Ev/VV8KuRoWnfP2QF2Bg+O2ZFdb49DiiNbz2VmgkIqrnyYaqIOqj8A6K/3p1xV0QcU5hBQ1+BmB6ot1OA==}
/emmet@2.4.7:
resolution: {integrity: sha512-O5O5QNqtdlnQM2bmKHtJgyChcrFMgQuulI+WdiOw2NArzprUqqxUW6bgYtKvzKgrsYpuLWalOkdhNP+1jluhCA==}
@@ -4821,7 +4835,7 @@ packages:
resolution: {integrity: sha512-o/sd0nMof8kYff+TqcDx3VSrgBTcZpSvYcAHIfHhv5VAuNmisCxjhx6YmxS8PFEpb9z5WKWKPdzf0jM23ro3RQ==}
dependencies:
'@types/debug': 4.1.12
debug: 4.3.4
debug: 4.3.5
decode-named-character-reference: 1.0.2
devlop: 1.1.0
micromark-core-commonmark: 2.0.1
@@ -4925,8 +4939,8 @@ packages:
resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==}
engines: {node: '>=0.10.0'}
/nostr-tools@2.6.0(typescript@5.4.5):
resolution: {integrity: sha512-7Jm+rZZPd8TjfHhkxuQHtUZV+3Dd1gl5YqFLMWhmtPBsPDyFJMBsLaPtrBmuWjYGxzoOYop98hw+chI2Xaq0UQ==}
/nostr-tools@2.7.0(typescript@5.4.5):
resolution: {integrity: sha512-jJoL2J1CBiKDxaXZww27nY/Wsuxzx7AULxmGKFce4sskDu1tohNyfnzYQ8BvDyvkstU8kNZUAXPL32tre33uig==}
peerDependencies:
typescript: '>=5.0.0'
peerDependenciesMeta:
@@ -5884,8 +5898,8 @@ packages:
/ts-interface-checker@0.1.13:
resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
/tsconfck@3.0.3(typescript@5.4.5):
resolution: {integrity: sha512-4t0noZX9t6GcPTfBAbIbbIU4pfpCwh0ueq3S4O/5qXI1VwK1outmxhe9dOiEWqMz3MW2LKgDTpqWV+37IWuVbA==}
/tsconfck@3.1.0(typescript@5.4.5):
resolution: {integrity: sha512-CMjc5zMnyAjcS9sPLytrbFmj89st2g+JYtY/c02ug4Q+CZaAtCgbyviI0n1YvjZE/pzoc6FbNsINS13DOL1B9w==}
engines: {node: ^18 || >=20}
hasBin: true
peerDependencies:
@@ -6229,7 +6243,7 @@ packages:
vite: '>=2.8'
dependencies:
'@rollup/plugin-virtual': 3.0.2
'@swc/core': 1.5.7
'@swc/core': 1.5.24
uuid: 9.0.1
vite: 5.2.12
transitivePeerDependencies:
@@ -6245,9 +6259,9 @@ packages:
vite:
optional: true
dependencies:
debug: 4.3.4
debug: 4.3.5
globrex: 0.1.2
tsconfck: 3.0.3(typescript@5.4.5)
tsconfck: 3.1.0(typescript@5.4.5)
vite: 5.2.12
transitivePeerDependencies:
- supports-color

1096
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,7 +5,7 @@ description = "nostr client"
authors = ["npub1zfss807aer0j26mwp2la0ume0jqde3823rmu97ra6sgyyg956e0s6xw445"]
repository = "https://github.com/lumehq/lume"
edition = "2021"
rust-version = "1.68"
rust-version = "1.70"
[build-dependencies]
tauri-build = { version = "2.0.0-beta", features = [] }
@@ -15,6 +15,7 @@ nostr-sdk = { version = "0.31", features = ["sqlite"] }
tokio = { version = "1", features = ["full"] }
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
monitor = { git = "https://github.com/ahkohd/tauri-toolkit", branch = "v2" }
tauri = { version = "2.0.0-beta", features = [
"unstable",
"tray-icon",
@@ -22,24 +23,23 @@ tauri = { version = "2.0.0-beta", features = [
"native-tls-vendored",
"protocol-asset",
] }
tauri-plugin-clipboard-manager = "2.1.0-beta"
tauri-plugin-dialog = "2.0.0-beta"
tauri-plugin-fs = "2.0.0-beta"
tauri-plugin-http = "2.0.0-beta"
tauri-plugin-notification = "2.0.0-beta"
tauri-plugin-os = "2.0.0-beta"
tauri-plugin-process = "2.0.0-beta"
tauri-plugin-shell = "2.0.0-beta"
tauri-plugin-updater = "2.0.0-beta"
tauri-plugin-upload = "2.0.0-beta"
tauri-plugin-window-state = "2.0.0-beta"
tauri-plugin-clipboard-manager = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
tauri-plugin-dialog = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
tauri-plugin-fs = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
tauri-plugin-http = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
tauri-plugin-notification = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
tauri-plugin-os = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
tauri-plugin-process = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
tauri-plugin-shell = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
tauri-plugin-updater = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
tauri-plugin-upload = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
tauri-nspanel = { git = "https://github.com/ahkohd/tauri-nspanel", branch = "v2" }
tauri-specta = { version = "^2.0.0-rc.11", features = ["typescript"] }
tauri-plugin-theme = "0.4.1"
tauri-plugin-decorum = "0.1.0"
webpage = { version = "2.0", features = ["serde"] }
specta = "^2.0.0-rc.12"
keyring = "2"
keyring-search = "0.2.0"
specta = "=2.0.0-rc.12"
tauri-specta = { version = "=2.0.0-rc.10", features = ["typescript"] }
tauri-plugin-theme = "0.4.1"
[target.'cfg(target_os = "macos")'.dependencies]
cocoa = "0.25.0"

View File

@@ -1,85 +1,86 @@
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "desktop-capability",
"description": "Capability for the desktop",
"platforms": ["linux", "macOS", "windows"],
"windows": [
"main",
"splash",
"settings",
"search",
"nwc",
"activity",
"zap-*",
"event-*",
"user-*",
"editor-*",
"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",
"os:allow-os-type",
"updater:default",
"updater:allow-check",
"updater:allow-download-and-install",
"window:allow-start-dragging",
"window:allow-create",
"window:allow-close",
"window:allow-set-focus",
"window:allow-center",
"window:allow-minimize",
"window:allow-maximize",
"window:allow-set-size",
"window:allow-set-focus",
"window:allow-start-dragging",
"decorum:allow-show-snap-overlay",
"clipboard-manager:allow-write-text",
"clipboard-manager:allow-read-text",
"webview:allow-create-webview-window",
"webview:allow-create-webview",
"webview:allow-set-webview-size",
"webview:allow-set-webview-position",
"webview:allow-webview-close",
"dialog:allow-open",
"dialog:allow-ask",
"dialog:allow-message",
"process:allow-restart",
"fs:allow-read-file",
"theme:allow-set-theme",
"theme:allow-get-theme",
"shell:allow-open",
{
"identifier": "http:default",
"allow": [
{
"url": "http://**/"
},
{
"url": "https://**/"
}
]
},
{
"identifier": "fs:allow-read-text-file",
"allow": [
{
"path": "$RESOURCE/locales/*"
},
{
"path": "$RESOURCE/resources/*"
}
]
}
]
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "desktop-capability",
"description": "Capability for the desktop",
"platforms": ["linux", "macOS", "windows"],
"windows": [
"main",
"panel",
"splash",
"settings",
"search",
"nwc",
"activity",
"zap-*",
"event-*",
"user-*",
"editor-*",
"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",
"os:allow-os-type",
"updater:default",
"updater:allow-check",
"updater:allow-download-and-install",
"window:allow-start-dragging",
"window:allow-create",
"window:allow-close",
"window:allow-set-focus",
"window:allow-center",
"window:allow-minimize",
"window:allow-maximize",
"window:allow-set-size",
"window:allow-set-focus",
"window:allow-start-dragging",
"decorum:allow-show-snap-overlay",
"clipboard-manager:allow-write-text",
"clipboard-manager:allow-read-text",
"webview:allow-create-webview-window",
"webview:allow-create-webview",
"webview:allow-set-webview-size",
"webview:allow-set-webview-position",
"webview:allow-webview-close",
"dialog:allow-open",
"dialog:allow-ask",
"dialog:allow-message",
"process:allow-restart",
"fs:allow-read-file",
"theme:allow-set-theme",
"theme:allow-get-theme",
"shell:allow-open",
{
"identifier": "http:default",
"allow": [
{
"url": "http://**/"
},
{
"url": "https://**/"
}
]
},
{
"identifier": "fs:allow-read-text-file",
"allow": [
{
"path": "$RESOURCE/locales/*"
},
{
"path": "$RESOURCE/resources/*"
}
]
}
]
}

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
{"desktop-capability":{"identifier":"desktop-capability","description":"Capability for the desktop","local":true,"windows":["main","splash","settings","search","nwc","activity","zap-*","event-*","user-*","editor-*","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","os:allow-os-type","updater:default","updater:allow-check","updater:allow-download-and-install","window:allow-start-dragging","window:allow-create","window:allow-close","window:allow-set-focus","window:allow-center","window:allow-minimize","window:allow-maximize","window:allow-set-size","window:allow-set-focus","window:allow-start-dragging","decorum:allow-show-snap-overlay","clipboard-manager:allow-write-text","clipboard-manager:allow-read-text","webview:allow-create-webview-window","webview:allow-create-webview","webview:allow-set-webview-size","webview:allow-set-webview-position","webview:allow-webview-close","dialog:allow-open","dialog:allow-ask","dialog:allow-message","process:allow-restart","fs:allow-read-file","theme:allow-set-theme","theme:allow-get-theme","shell:allow-open",{"identifier":"http:default","allow":[{"url":"http://**/"},{"url":"https://**/"}]},{"identifier":"fs:allow-read-text-file","allow":[{"path":"$RESOURCE/locales/*"},{"path":"$RESOURCE/resources/*"}]}],"platforms":["linux","macOS","windows"]}}
{"desktop-capability":{"identifier":"desktop-capability","description":"Capability for the desktop","local":true,"windows":["main","panel","splash","settings","search","nwc","activity","zap-*","event-*","user-*","editor-*","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","os:allow-os-type","updater:default","updater:allow-check","updater:allow-download-and-install","window:allow-start-dragging","window:allow-create","window:allow-close","window:allow-set-focus","window:allow-center","window:allow-minimize","window:allow-maximize","window:allow-set-size","window:allow-set-focus","window:allow-start-dragging","decorum:allow-show-snap-overlay","clipboard-manager:allow-write-text","clipboard-manager:allow-read-text","webview:allow-create-webview-window","webview:allow-create-webview","webview:allow-set-webview-size","webview:allow-set-webview-position","webview:allow-webview-close","dialog:allow-open","dialog:allow-ask","dialog:allow-message","process:allow-restart","fs:allow-read-file","theme:allow-set-theme","theme:allow-get-theme","shell:allow-open",{"identifier":"http:default","allow":[{"url":"http://**/"},{"url":"https://**/"}]},{"identifier":"fs:allow-read-text-file","allow":[{"path":"$RESOURCE/locales/*"},{"path":"$RESOURCE/resources/*"}]}],"platforms":["linux","macOS","windows"]}}

View File

@@ -5863,6 +5863,13 @@
"updater:allow-check"
]
},
{
"description": "updater:allow-download -> Enables the download command without any pre-configured scope.",
"type": "string",
"enum": [
"updater:allow-download"
]
},
{
"description": "updater:allow-download-and-install -> Enables the download_and_install command without any pre-configured scope.",
"type": "string",
@@ -5870,6 +5877,13 @@
"updater:allow-download-and-install"
]
},
{
"description": "updater:allow-install -> Enables the install command without any pre-configured scope.",
"type": "string",
"enum": [
"updater:allow-install"
]
},
{
"description": "updater:deny-check -> Denies the check command without any pre-configured scope.",
"type": "string",
@@ -5877,6 +5891,13 @@
"updater:deny-check"
]
},
{
"description": "updater:deny-download -> Denies the download command without any pre-configured scope.",
"type": "string",
"enum": [
"updater:deny-download"
]
},
{
"description": "updater:deny-download-and-install -> Denies the download_and_install command without any pre-configured scope.",
"type": "string",
@@ -5884,6 +5905,13 @@
"updater:deny-download-and-install"
]
},
{
"description": "updater:deny-install -> Denies the install command without any pre-configured scope.",
"type": "string",
"enum": [
"updater:deny-install"
]
},
{
"type": "string",
"enum": [
@@ -6261,6 +6289,13 @@
"window:allow-minimize"
]
},
{
"description": "window:allow-monitor-from-point -> Enables the monitor_from_point command without any pre-configured scope.",
"type": "string",
"enum": [
"window:allow-monitor-from-point"
]
},
{
"description": "window:allow-outer-position -> Enables the outer_position command without any pre-configured scope.",
"type": "string",
@@ -6695,6 +6730,13 @@
"window:deny-minimize"
]
},
{
"description": "window:deny-monitor-from-point -> Denies the monitor_from_point command without any pre-configured scope.",
"type": "string",
"enum": [
"window:deny-monitor-from-point"
]
},
{
"description": "window:deny-outer-position -> Denies the outer_position command without any pre-configured scope.",
"type": "string",
@@ -6967,54 +7009,6 @@
"enum": [
"window:deny-unminimize"
]
},
{
"type": "string",
"enum": [
"window-state:default"
]
},
{
"description": "window-state:allow-filename -> Enables the filename command without any pre-configured scope.",
"type": "string",
"enum": [
"window-state:allow-filename"
]
},
{
"description": "window-state:allow-restore-state -> Enables the restore_state command without any pre-configured scope.",
"type": "string",
"enum": [
"window-state:allow-restore-state"
]
},
{
"description": "window-state:allow-save-window-state -> Enables the save_window_state command without any pre-configured scope.",
"type": "string",
"enum": [
"window-state:allow-save-window-state"
]
},
{
"description": "window-state:deny-filename -> Denies the filename command without any pre-configured scope.",
"type": "string",
"enum": [
"window-state:deny-filename"
]
},
{
"description": "window-state:deny-restore-state -> Denies the restore_state command without any pre-configured scope.",
"type": "string",
"enum": [
"window-state:deny-restore-state"
]
},
{
"description": "window-state:deny-save-window-state -> Denies the save_window_state command without any pre-configured scope.",
"type": "string",
"enum": [
"window-state:deny-save-window-state"
]
}
]
},

View File

@@ -5863,6 +5863,13 @@
"updater:allow-check"
]
},
{
"description": "updater:allow-download -> Enables the download command without any pre-configured scope.",
"type": "string",
"enum": [
"updater:allow-download"
]
},
{
"description": "updater:allow-download-and-install -> Enables the download_and_install command without any pre-configured scope.",
"type": "string",
@@ -5870,6 +5877,13 @@
"updater:allow-download-and-install"
]
},
{
"description": "updater:allow-install -> Enables the install command without any pre-configured scope.",
"type": "string",
"enum": [
"updater:allow-install"
]
},
{
"description": "updater:deny-check -> Denies the check command without any pre-configured scope.",
"type": "string",
@@ -5877,6 +5891,13 @@
"updater:deny-check"
]
},
{
"description": "updater:deny-download -> Denies the download command without any pre-configured scope.",
"type": "string",
"enum": [
"updater:deny-download"
]
},
{
"description": "updater:deny-download-and-install -> Denies the download_and_install command without any pre-configured scope.",
"type": "string",
@@ -5884,6 +5905,13 @@
"updater:deny-download-and-install"
]
},
{
"description": "updater:deny-install -> Denies the install command without any pre-configured scope.",
"type": "string",
"enum": [
"updater:deny-install"
]
},
{
"type": "string",
"enum": [
@@ -6261,6 +6289,13 @@
"window:allow-minimize"
]
},
{
"description": "window:allow-monitor-from-point -> Enables the monitor_from_point command without any pre-configured scope.",
"type": "string",
"enum": [
"window:allow-monitor-from-point"
]
},
{
"description": "window:allow-outer-position -> Enables the outer_position command without any pre-configured scope.",
"type": "string",
@@ -6695,6 +6730,13 @@
"window:deny-minimize"
]
},
{
"description": "window:deny-monitor-from-point -> Denies the monitor_from_point command without any pre-configured scope.",
"type": "string",
"enum": [
"window:deny-monitor-from-point"
]
},
{
"description": "window:deny-outer-position -> Denies the outer_position command without any pre-configured scope.",
"type": "string",
@@ -6967,54 +7009,6 @@
"enum": [
"window:deny-unminimize"
]
},
{
"type": "string",
"enum": [
"window-state:default"
]
},
{
"description": "window-state:allow-filename -> Enables the filename command without any pre-configured scope.",
"type": "string",
"enum": [
"window-state:allow-filename"
]
},
{
"description": "window-state:allow-restore-state -> Enables the restore_state command without any pre-configured scope.",
"type": "string",
"enum": [
"window-state:allow-restore-state"
]
},
{
"description": "window-state:allow-save-window-state -> Enables the save_window_state command without any pre-configured scope.",
"type": "string",
"enum": [
"window-state:allow-save-window-state"
]
},
{
"description": "window-state:deny-filename -> Denies the filename command without any pre-configured scope.",
"type": "string",
"enum": [
"window-state:deny-filename"
]
},
{
"description": "window-state:deny-restore-state -> Denies the restore_state command without any pre-configured scope.",
"type": "string",
"enum": [
"window-state:deny-restore-state"
]
},
{
"description": "window-state:deny-save-window-state -> Denies the save_window_state command without any pre-configured scope.",
"type": "string",
"enum": [
"window-state:deny-save-window-state"
]
}
]
},

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB

180
src-tauri/src/fns.rs Normal file
View File

@@ -0,0 +1,180 @@
use cocoa::appkit::NSWindowCollectionBehavior;
use std::ffi::CString;
use tauri::Manager;
use tauri_nspanel::{
block::ConcreteBlock,
cocoa::{
appkit::{NSMainMenuWindowLevel, NSView, NSWindow},
base::{id, nil},
foundation::{NSPoint, NSRect},
},
objc::{class, msg_send, runtime::NO, sel, sel_impl},
panel_delegate, ManagerExt, WebviewWindowExt,
};
#[allow(non_upper_case_globals)]
const NSWindowStyleMaskNonActivatingPanel: i32 = 1 << 7;
pub fn swizzle_to_menubar_panel(app_handle: &tauri::AppHandle) {
let window = app_handle.get_webview_window("panel").unwrap();
let panel = window.to_panel().unwrap();
let handle = app_handle.to_owned();
let delegate = panel_delegate!(MyPanelDelegate {
window_did_become_key,
window_did_resign_key
});
delegate.set_listener(Box::new(move |delegate_name: String| {
match delegate_name.as_str() {
"window_did_become_key" => {
let app_name = handle.package_info().name.to_owned();
println!("[info]: {:?} panel becomes key window!", app_name);
}
"window_did_resign_key" => {
println!("[info]: panel resigned from key window!");
}
_ => (),
}
}));
panel.set_level(NSMainMenuWindowLevel + 1);
panel.set_style_mask(NSWindowStyleMaskNonActivatingPanel);
panel.set_collection_behaviour(
NSWindowCollectionBehavior::NSWindowCollectionBehaviorCanJoinAllSpaces
| NSWindowCollectionBehavior::NSWindowCollectionBehaviorStationary
| NSWindowCollectionBehavior::NSWindowCollectionBehaviorFullScreenAuxiliary,
);
panel.set_delegate(delegate);
}
pub fn setup_menubar_panel_listeners(app_handle: &tauri::AppHandle) {
fn hide_menubar_panel(app_handle: &tauri::AppHandle) {
if check_menubar_frontmost() {
return;
}
let panel = app_handle.get_webview_panel("panel").unwrap();
panel.order_out(None);
}
let handle = app_handle.clone();
app_handle.listen_any("menubar_panel_did_resign_key", move |_| {
hide_menubar_panel(&handle);
});
let handle = app_handle.clone();
let callback = Box::new(move || {
hide_menubar_panel(&handle);
});
register_workspace_listener(
"NSWorkspaceDidActivateApplicationNotification".into(),
callback.clone(),
);
register_workspace_listener(
"NSWorkspaceActiveSpaceDidChangeNotification".into(),
callback,
);
}
pub fn update_menubar_appearance(app_handle: &tauri::AppHandle) {
let window = app_handle.get_window("panel").unwrap();
set_corner_radius(&window, 13.0);
}
pub fn set_corner_radius(window: &tauri::Window, radius: f64) {
let win: id = window.ns_window().unwrap() as _;
unsafe {
let view: id = win.contentView();
view.wantsLayer();
let layer: id = view.layer();
let _: () = msg_send![layer, setCornerRadius: radius];
}
}
pub fn position_menubar_panel(app_handle: &tauri::AppHandle, padding_top: f64) {
let window = app_handle.get_webview_window("panel").unwrap();
let monitor = monitor::get_monitor_with_cursor().unwrap();
let scale_factor = monitor.scale_factor();
let visible_area = monitor.visible_area();
let monitor_pos = visible_area.position().to_logical::<f64>(scale_factor);
let monitor_size = visible_area.size().to_logical::<f64>(scale_factor);
let mouse_location: NSPoint = unsafe { msg_send![class!(NSEvent), mouseLocation] };
let handle: id = window.ns_window().unwrap() as _;
let mut win_frame: NSRect = unsafe { msg_send![handle, frame] };
win_frame.origin.y = (monitor_pos.y + monitor_size.height) - win_frame.size.height;
win_frame.origin.y -= padding_top;
win_frame.origin.x = {
let top_right = mouse_location.x + (win_frame.size.width / 2.0);
let is_offscreen = top_right > monitor_pos.x + monitor_size.width;
if !is_offscreen {
mouse_location.x - (win_frame.size.width / 2.0)
} else {
let diff = top_right - (monitor_pos.x + monitor_size.width);
mouse_location.x - (win_frame.size.width / 2.0) - diff
}
};
let _: () = unsafe { msg_send![handle, setFrame: win_frame display: NO] };
}
fn register_workspace_listener(name: String, callback: Box<dyn Fn()>) {
let workspace: id = unsafe { msg_send![class!(NSWorkspace), sharedWorkspace] };
let notification_center: id = unsafe { msg_send![workspace, notificationCenter] };
let block = ConcreteBlock::new(move |_notif: id| {
callback();
});
let block = block.copy();
let name: id =
unsafe { msg_send![class!(NSString), stringWithCString: CString::new(name).unwrap()] };
unsafe {
let _: () = msg_send![
notification_center,
addObserverForName: name object: nil queue: nil usingBlock: block
];
}
}
fn app_pid() -> i32 {
let process_info: id = unsafe { msg_send![class!(NSProcessInfo), processInfo] };
let pid: i32 = unsafe { msg_send![process_info, processIdentifier] };
pid
}
fn get_frontmost_app_pid() -> i32 {
let workspace: id = unsafe { msg_send![class!(NSWorkspace), sharedWorkspace] };
let frontmost_application: id = unsafe { msg_send![workspace, frontmostApplication] };
let pid: i32 = unsafe { msg_send![frontmost_application, processIdentifier] };
pid
}
pub fn check_menubar_frontmost() -> bool {
get_frontmost_app_pid() == app_pid()
}

View File

@@ -4,8 +4,8 @@
)]
pub mod commands;
pub mod fns;
pub mod nostr;
pub mod tray;
#[cfg(target_os = "macos")]
extern crate cocoa;
@@ -17,8 +17,18 @@ extern crate objc;
use nostr_sdk::prelude::*;
use std::fs;
use tauri::Manager;
use tauri_nspanel::ManagerExt;
use tauri_plugin_decorum::WebviewWindowExt;
#[cfg(target_os = "macos")]
use crate::fns::{
position_menubar_panel, setup_menubar_panel_listeners, swizzle_to_menubar_panel,
update_menubar_appearance,
};
#[cfg(target_os = "macos")]
use tauri::tray::{MouseButtonState, TrayIconEvent};
pub struct Nostr {
client: Client,
}
@@ -34,13 +44,11 @@ fn main() {
nostr::keys::create_account,
nostr::keys::save_account,
nostr::keys::get_encrypted_key,
nostr::keys::nostr_connect,
nostr::keys::connect_remote_account,
nostr::keys::load_account,
nostr::keys::event_to_bech32,
nostr::keys::user_to_bech32,
nostr::keys::to_npub,
nostr::keys::verify_nip05,
nostr::metadata::get_activities,
nostr::metadata::get_current_user_profile,
nostr::metadata::get_profile,
nostr::metadata::get_contact_list,
@@ -56,6 +64,7 @@ fn main() {
nostr::metadata::zap_profile,
nostr::metadata::zap_event,
nostr::metadata::friend_to_friend,
nostr::metadata::get_notifications,
nostr::event::get_event,
nostr::event::get_replies,
nostr::event::get_events_by,
@@ -91,20 +100,46 @@ fn main() {
#[cfg(target_os = "macos")]
main_window.set_traffic_lights_inset(8.0, 16.0).unwrap();
// Setup app tray
let handle = app.handle().clone();
tray::create_tray(app.handle()).unwrap();
// Create panel
#[cfg(target_os = "macos")]
swizzle_to_menubar_panel(&app.handle());
#[cfg(target_os = "macos")]
update_menubar_appearance(&app.handle());
#[cfg(target_os = "macos")]
setup_menubar_panel_listeners(&app.handle());
// Setup tray icon
#[cfg(target_os = "macos")]
let tray = app.tray_by_id("tray_panel").unwrap();
// Handle tray icon event
#[cfg(target_os = "macos")]
tray.on_tray_icon_event(|tray, event| match event {
TrayIconEvent::Click { button_state, .. } => {
if button_state == MouseButtonState::Up {
let app = tray.app_handle();
let panel = app.get_webview_panel("panel").unwrap();
match panel.is_visible() {
true => panel.order_out(None),
false => {
position_menubar_panel(&app, 0.0);
panel.show();
}
}
}
}
_ => {}
});
// Create data folder if not exist
let home_dir = app.path().home_dir().unwrap();
fs::create_dir_all(home_dir.join("Lume/")).unwrap();
let _ = fs::create_dir_all(home_dir.join("Lume/"));
tauri::async_runtime::block_on(async move {
// Create nostr database connection
let sqlite = SQLiteDatabase::open(home_dir.join("Lume/lume.db")).await;
// Create nostr connection
let client = match sqlite {
let database = SQLiteDatabase::open(home_dir.join("Lume/lume.db")).await;
let client = match database {
Ok(db) => ClientBuilder::default().database(db).build(),
Err(_) => ClientBuilder::default().build(),
};
@@ -130,11 +165,12 @@ fn main() {
client.connect().await;
// Update global state
handle.manage(Nostr { client })
app.handle().manage(Nostr { client })
});
Ok(())
})
.plugin(tauri_nspanel::init())
.plugin(tauri_plugin_theme::init(ctx.config_mut()))
.plugin(tauri_plugin_decorum::init())
.plugin(tauri_plugin_clipboard_manager::init())

View File

@@ -13,9 +13,8 @@ pub async fn get_event(id: &str, state: State<'_, Nostr>) -> Result<String, Stri
Nip19::Event(event) => {
let relays = event.relays;
for relay in relays.into_iter() {
let url = Url::from_str(&relay).unwrap();
let _ = client.add_relay(&url).await.unwrap_or_default();
client.connect_relay(&url).await.unwrap_or_default();
let _ = client.add_relay(&relay).await.unwrap_or_default();
client.connect_relay(&relay).await.unwrap_or_default();
}
Some(event.event_id)
}
@@ -122,11 +121,11 @@ pub async fn get_local_events(
let filter = Filter::new()
.kinds(vec![Kind::TextNote, Kind::Repost])
.limit(20)
.authors(authors)
.until(as_of);
.until(as_of)
.authors(authors);
match client
.get_events_of(vec![filter], Some(Duration::from_secs(8)))
.get_events_of(vec![filter], Some(Duration::from_secs(10)))
.await
{
Ok(events) => Ok(events.into_iter().map(|ev| ev.as_json()).collect()),

View File

@@ -6,7 +6,8 @@ use serde::Serialize;
use specta::Type;
use std::str::FromStr;
use std::time::Duration;
use tauri::{Manager, State};
use tauri::{EventTarget, Manager, State};
use tauri_plugin_notification::NotificationExt;
#[derive(Serialize, Type)]
pub struct Account {
@@ -102,158 +103,236 @@ pub async fn save_account(
#[specta::specta]
pub async fn load_account(
npub: &str,
bunker: Option<&str>,
state: State<'_, Nostr>,
app: tauri::AppHandle,
) -> Result<bool, String> {
let handle = app.clone();
let client = &state.client;
let keyring = Entry::new(npub, "nostr_secret").unwrap();
match keyring.get_password() {
Ok(password) => {
if password.starts_with("bunker://") {
let local_keyring = Entry::new(npub, "bunker_local_account").unwrap();
if let Ok(password) = keyring.get_password() {
match bunker {
Some(uri) => {
let app_keys = Keys::parse(password).expect("Secret Key is modified, please check again.");
match local_keyring.get_password() {
Ok(local_password) => {
let secret_key = SecretKey::from_bech32(local_password).unwrap();
let app_keys = Keys::new(secret_key);
let bunker_uri = NostrConnectURI::parse(password).unwrap();
let signer = Nip46Signer::new(bunker_uri, app_keys, Duration::from_secs(60), None)
.await
.unwrap();
// Update signer
client.set_signer(Some(signer.into())).await;
match NostrConnectURI::parse(uri) {
Ok(bunker_uri) => {
match Nip46Signer::new(bunker_uri, app_keys, Duration::from_secs(30), None).await {
Ok(signer) => client.set_signer(Some(signer.into())).await,
Err(err) => return Err(err.to_string()),
}
}
Err(_) => todo!(),
Err(err) => return Err(err.to_string()),
}
} else {
let secret_key = SecretKey::from_bech32(password).expect("Get secret key failed");
let keys = Keys::new(secret_key);
}
None => {
let keys = Keys::parse(password).expect("Secret Key is modified, please check again.");
let signer = NostrSigner::Keys(keys);
// Update signer
client.set_signer(Some(signer)).await;
}
}
// Verify signer
let signer = client.signer().await.unwrap();
let public_key = signer.public_key().await.unwrap();
// Verify signer
let signer = client.signer().await.unwrap();
let public_key = signer.public_key().await.unwrap();
let filter = Filter::new()
.author(public_key)
.kind(Kind::RelayList)
.limit(1);
let filter = Filter::new()
.author(public_key)
.kind(Kind::RelayList)
.limit(1);
// Connect to user's relay (NIP-65)
// #TODO: Let rust-nostr handle it
match client
.get_events_of(vec![filter], Some(Duration::from_secs(10)))
.await
{
Ok(events) => {
if let Some(event) = events.first() {
let relay_list = nip65::extract_relay_list(event);
for item in relay_list.into_iter() {
println!("connecting to relay: {} - {:?}", item.0, item.1);
// Connect to user's relay (NIP-65)
// #TODO: Let rust-nostr handle it
if let Ok(events) = client
.get_events_of(vec![filter], Some(Duration::from_secs(10)))
.await
{
if let Some(event) = events.first() {
let relay_list = nip65::extract_relay_list(event);
for item in relay_list.into_iter() {
println!("connecting to relay: {} - {:?}", item.0, item.1);
let relay_url = item.0.to_string();
let opts = match item.1 {
Some(val) => {
if val == &RelayMetadata::Read {
RelayOptions::new().read(true).write(false)
} else {
RelayOptions::new().write(true).read(false)
}
}
None => RelayOptions::new(),
};
// Add relay to relay pool
let _ = client
.add_relay_with_opts(relay_url.clone(), opts)
.await
.unwrap_or_default();
// Connect relay
client.connect_relay(relay_url).await.unwrap_or_default();
}
}
}
Err(_) => todo!(),
};
// Run notification service
tauri::async_runtime::spawn(async move {
let window = app.get_window("main").unwrap();
let state = window.state::<Nostr>();
let client = &state.client;
let subscription = Filter::new()
.pubkey(public_key)
.kinds(vec![Kind::TextNote, Kind::Repost, Kind::ZapReceipt])
.since(Timestamp::now());
let activity_id = SubscriptionId::new("activity");
// Create a subscription for activity
client
.subscribe_with_id(activity_id.clone(), vec![subscription], None)
.await;
// Handle notifications
let _ = client
.handle_notifications(|notification| async {
if let RelayPoolNotification::Event {
subscription_id,
event,
..
} = notification
{
if subscription_id == activity_id {
let _ = app.emit("activity", event.as_json());
let relay_url = item.0.to_string();
let opts = match item.1 {
Some(val) => {
if val == &RelayMetadata::Read {
RelayOptions::new().read(true).write(false)
} else {
RelayOptions::new().write(true).read(false)
}
}
Ok(false)
})
.await;
});
None => RelayOptions::new(),
};
Ok(true)
}
Err(err) => Err(err.to_string()),
// Add relay to relay pool
let _ = client
.add_relay_with_opts(&relay_url, opts)
.await
.unwrap_or_default();
// Connect relay
client.connect_relay(relay_url).await.unwrap_or_default();
}
}
};
// Run sync service
tauri::async_runtime::spawn(async move {
let window = handle.get_window("main").unwrap();
let state = window.state::<Nostr>();
let client = &state.client;
let filter = Filter::new()
.pubkey(public_key)
.kinds(vec![
Kind::TextNote,
Kind::Repost,
Kind::Reaction,
Kind::ZapReceipt,
])
.limit(500);
match client.reconcile(filter, NegentropyOptions::default()).await {
Ok(_) => println!("Sync notification done."),
Err(_) => println!("Sync notification failed."),
}
});
// Run notification service
tauri::async_runtime::spawn(async move {
println!("Starting notification service...");
let window = app.get_window("main").unwrap();
let state = window.state::<Nostr>();
let client = &state.client;
// Create a subscription for notification
let notification_id = SubscriptionId::new("notification");
let filter = Filter::new()
.pubkey(public_key)
.kinds(vec![
Kind::TextNote,
Kind::Repost,
Kind::Reaction,
Kind::ZapReceipt,
])
.since(Timestamp::now());
// Subscribe
client
.subscribe_with_id(notification_id.clone(), vec![filter], None)
.await;
// Handle notifications
let _ = client
.handle_notifications(|notification| async {
if let RelayPoolNotification::Event {
subscription_id,
event,
..
} = notification
{
if subscription_id == notification_id {
println!("new notification: {}", event.as_json());
if let Err(_) = app.emit_to(
EventTarget::window("panel"),
"notification",
event.as_json(),
) {
println!("Emit new notification failed.")
}
let handle = app.app_handle();
let author = client.metadata(event.pubkey).await.unwrap();
match event.kind() {
Kind::TextNote => {
if let Err(e) = handle
.notification()
.builder()
.body("Mentioned you in a thread.")
.title(author.display_name.unwrap_or_else(|| "Lume".to_string()))
.show()
{
println!("Failed to show notification: {:?}", e);
}
}
Kind::Repost => {
if let Err(e) = handle
.notification()
.builder()
.body("Reposted your note.")
.title(author.display_name.unwrap_or_else(|| "Lume".to_string()))
.show()
{
println!("Failed to show notification: {:?}", e);
}
}
Kind::Reaction => {
let content = event.content();
if let Err(e) = handle
.notification()
.builder()
.body(content)
.title(author.display_name.unwrap_or_else(|| "Lume".to_string()))
.show()
{
println!("Failed to show notification: {:?}", e);
}
}
Kind::ZapReceipt => {
if let Err(e) = handle
.notification()
.builder()
.body("Zapped you.")
.title(author.display_name.unwrap_or_else(|| "Lume".to_string()))
.show()
{
println!("Failed to show notification: {:?}", e);
}
}
_ => {}
}
}
}
Ok(false)
})
.await;
});
Ok(true)
} else {
Err("Key not found.".into())
}
}
#[tauri::command]
#[specta::specta]
pub async fn nostr_connect(
npub: &str,
uri: &str,
state: State<'_, Nostr>,
) -> Result<String, String> {
pub async fn connect_remote_account(uri: &str, state: State<'_, Nostr>) -> Result<String, String> {
let client = &state.client;
let local_key = Keys::generate();
match NostrConnectURI::parse(uri) {
Ok(bunker_uri) => {
match Nip46Signer::new(
bunker_uri,
local_key.clone(),
Duration::from_secs(120),
None,
)
.await
{
let app_keys = Keys::generate();
let app_secret = app_keys.secret_key().unwrap().to_string();
// Get remote user
let remote_user = bunker_uri.signer_public_key().unwrap();
let remote_npub = remote_user.to_bech32().unwrap();
match Nip46Signer::new(bunker_uri, app_keys, Duration::from_secs(120), None).await {
Ok(signer) => {
let local_secret = local_key.secret_key().unwrap().to_bech32().unwrap();
let secret_keyring = Entry::new(&npub, "nostr_secret").unwrap();
let account_keyring = Entry::new(&npub, "bunker_local_account").unwrap();
let _ = secret_keyring.set_password(uri);
let _ = account_keyring.set_password(&local_secret);
let keyring = Entry::new(&remote_npub, "nostr_secret").unwrap();
let _ = keyring.set_password(&app_secret);
// Update signer
let _ = client.set_signer(Some(signer.into())).await;
Ok(npub.into())
Ok(remote_npub)
}
Err(err) => Err(err.to_string()),
}
@@ -299,15 +378,6 @@ pub fn user_to_bech32(key: &str, relays: Vec<String>) -> Result<String, ()> {
Ok(profile.to_bech32().unwrap())
}
#[tauri::command]
#[specta::specta]
pub fn to_npub(hex: &str) -> Result<String, ()> {
let public_key = PublicKey::from_str(hex).unwrap();
let npub = Nip19::Pubkey(public_key);
Ok(npub.to_bech32().unwrap())
}
#[tauri::command]
#[specta::specta]
pub async fn verify_nip05(key: &str, nip05: &str) -> Result<bool, String> {

View File

@@ -1,76 +1,9 @@
use super::get_latest_event;
use crate::Nostr;
use keyring::Entry;
use nostr_sdk::prelude::*;
use std::{str::FromStr, time::Duration};
use tauri::State;
use url::Url;
#[tauri::command]
#[specta::specta]
pub async fn get_activities(
account: &str,
kind: &str,
state: State<'_, Nostr>,
) -> Result<Vec<String>, String> {
let client = &state.client;
if let Ok(pubkey) = PublicKey::from_str(account) {
if let Ok(kind) = Kind::from_str(kind) {
let filter = Filter::new()
.pubkey(pubkey)
.kind(kind)
.limit(100)
.until(Timestamp::now());
match client.get_events_of(vec![filter], None).await {
Ok(events) => Ok(events.into_iter().map(|ev| ev.as_json()).collect()),
Err(err) => Err(err.to_string()),
}
} else {
Err("Kind is not valid, please check again.".into())
}
} else {
Err("Public Key is not valid, please check again.".into())
}
}
#[tauri::command]
#[specta::specta]
pub async fn friend_to_friend(npub: &str, state: State<'_, Nostr>) -> Result<bool, String> {
let client = &state.client;
match PublicKey::from_bech32(npub) {
Ok(author) => {
let mut contact_list: Vec<Contact> = Vec::new();
let contact_list_filter = Filter::new()
.author(author)
.kind(Kind::ContactList)
.limit(1);
if let Ok(contact_list_events) = client.get_events_of(vec![contact_list_filter], None).await {
for event in contact_list_events.into_iter() {
for tag in event.into_iter_tags() {
if let Some(TagStandard::PublicKey {
public_key,
relay_url,
alias,
uppercase: false,
}) = tag.to_standardized()
{
contact_list.push(Contact::new(public_key, relay_url, alias))
}
}
}
}
match client.set_contact_list(contact_list).await {
Ok(_) => Ok(true),
Err(err) => Err(err.to_string()),
}
}
Err(err) => Err(err.to_string()),
}
}
#[tauri::command]
#[specta::specta]
@@ -88,7 +21,7 @@ pub async fn get_current_user_profile(state: State<'_, Nostr>) -> Result<String,
.await
{
Ok(events) => {
if let Some(event) = events.first() {
if let Some(event) = get_latest_event(&events) {
if let Ok(metadata) = Metadata::from_json(&event.content) {
Ok(metadata.as_json())
} else {
@@ -109,7 +42,14 @@ pub async fn get_profile(id: &str, state: State<'_, Nostr>) -> Result<String, St
let public_key: Option<PublicKey> = match Nip19::from_bech32(id) {
Ok(val) => match val {
Nip19::Pubkey(pubkey) => Some(pubkey),
Nip19::Profile(profile) => Some(profile.public_key),
Nip19::Profile(profile) => {
let relays = profile.relays;
for relay in relays.into_iter() {
let _ = client.add_relay(&relay).await.unwrap_or_default();
client.connect_relay(&relay).await.unwrap_or_default();
}
Some(profile.public_key)
}
_ => None,
},
Err(_) => match PublicKey::from_str(id) {
@@ -464,3 +404,132 @@ pub async fn zap_event(
Err("Parse public key failed".into())
}
}
#[tauri::command]
#[specta::specta]
pub async fn friend_to_friend(npub: &str, state: State<'_, Nostr>) -> Result<bool, String> {
let client = &state.client;
match PublicKey::from_bech32(npub) {
Ok(author) => {
let mut contact_list: Vec<Contact> = Vec::new();
let contact_list_filter = Filter::new()
.author(author)
.kind(Kind::ContactList)
.limit(1);
if let Ok(contact_list_events) = client.get_events_of(vec![contact_list_filter], None).await {
for event in contact_list_events.into_iter() {
for tag in event.into_iter_tags() {
if let Some(TagStandard::PublicKey {
public_key,
relay_url,
alias,
uppercase: false,
}) = tag.to_standardized()
{
contact_list.push(Contact::new(public_key, relay_url, alias))
}
}
}
}
match client.set_contact_list(contact_list).await {
Ok(_) => Ok(true),
Err(err) => Err(err.to_string()),
}
}
Err(err) => Err(err.to_string()),
}
}
pub async fn get_following(
state: State<'_, Nostr>,
public_key: &str,
timeout: Option<u64>,
) -> Result<Vec<String>, String> {
let client = &state.client;
let public_key = match PublicKey::from_str(public_key) {
Ok(val) => val,
Err(err) => return Err(err.to_string()),
};
let duration = timeout.map(Duration::from_secs);
let filter = Filter::new().kind(Kind::ContactList).author(public_key);
let events = match client.get_events_of(vec![filter], duration).await {
Ok(events) => events,
Err(err) => return Err(err.to_string()),
};
let mut ret: Vec<String> = vec![];
if let Some(latest_event) = events.iter().max_by_key(|event| event.created_at()) {
ret.extend(latest_event.tags().iter().filter_map(|tag| {
if let Some(TagStandard::PublicKey {
uppercase: false, ..
}) = <nostr_sdk::Tag as Clone>::clone(tag).to_standardized()
{
tag.content().map(String::from)
} else {
None
}
}));
}
Ok(ret)
}
pub async fn get_followers(
state: State<'_, Nostr>,
public_key: &str,
timeout: Option<u64>,
) -> Result<Vec<String>, String> {
let client = &state.client;
let public_key = match PublicKey::from_str(public_key) {
Ok(val) => val,
Err(err) => return Err(err.to_string()),
};
let duration = timeout.map(Duration::from_secs);
let filter = Filter::new().kind(Kind::ContactList).custom_tag(
SingleLetterTag::lowercase(Alphabet::P),
vec![public_key.to_hex()],
);
let events = match client.get_events_of(vec![filter], duration).await {
Ok(events) => events,
Err(err) => return Err(err.to_string()),
};
let ret: Vec<String> = events
.into_iter()
.map(|event| event.author().to_hex())
.collect();
Ok(ret)
//todo: get more than 500 events
}
#[tauri::command]
#[specta::specta]
pub async fn get_notifications(state: State<'_, Nostr>) -> Result<Vec<String>, String> {
let client = &state.client;
match client.signer().await {
Ok(signer) => {
let public_key = signer.public_key().await.unwrap();
let filter = Filter::new()
.pubkey(public_key)
.kinds(vec![
Kind::TextNote,
Kind::Repost,
Kind::Reaction,
Kind::ZapReceipt,
])
.limit(200);
match client
.database()
.query(vec![filter], Order::default())
.await
{
Ok(events) => Ok(events.into_iter().map(|ev| ev.as_json()).collect()),
Err(err) => Err(err.to_string()),
}
}
Err(err) => Err(err.to_string()),
}
}

View File

@@ -2,3 +2,5 @@ pub mod event;
pub mod keys;
pub mod metadata;
pub mod relay;
mod utils;
pub use utils::get_latest_event;

View File

@@ -0,0 +1,5 @@
use nostr_sdk::Event;
pub fn get_latest_event(events: &[Event]) -> Option<&Event> {
events.iter().max_by_key(|event| event.created_at())
}

View File

@@ -1,180 +0,0 @@
use std::path::PathBuf;
#[cfg(target_os = "macos")]
use tauri::TitleBarStyle;
use tauri::{
utils::config::WindowEffectsConfig, window::Effect, Manager, Runtime, WebviewUrl,
WebviewWindowBuilder,
};
use tauri_plugin_shell::ShellExt;
pub fn create_tray<R: Runtime>(app: &tauri::AppHandle<R>) -> tauri::Result<()> {
let version = app.package_info().version.to_string();
let tray = app.tray_by_id("main_tray").unwrap();
let menu = tauri::menu::MenuBuilder::new(app)
.item(&tauri::menu::MenuItem::with_id(app, "open", "Open Lume", true, None::<&str>).unwrap())
.item(&tauri::menu::MenuItem::with_id(app, "editor", "New Post", true, Some("cmd+n")).unwrap())
.item(&tauri::menu::MenuItem::with_id(app, "search", "Search", true, Some("cmd+k")).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, Some("cmd+,")).unwrap(),
)
.separator()
.item(&tauri::menu::MenuItem::with_id(app, "quit", "Quit", true, None::<&str>).unwrap())
.build()
.unwrap();
let _ = tray.set_menu(Some(menu));
tray.on_menu_event(move |app, event| match event.id.0.as_str() {
"open" => {
if let Some(window) = app.get_window("main") {
if window.is_visible().unwrap_or_default() {
let _ = window.set_focus();
} else {
let _ = window.show();
let _ = window.set_focus();
};
}
}
"editor" => {
if let Some(window) = app.get_window("editor-0") {
if window.is_visible().unwrap_or_default() {
let _ = window.set_focus();
} else {
let _ = window.show();
let _ = window.set_focus();
};
} else {
#[cfg(target_os = "macos")]
let _ =
WebviewWindowBuilder::new(app, "editor-0", WebviewUrl::App(PathBuf::from("editor")))
.title("Editor")
.min_inner_size(560., 340.)
.inner_size(560., 340.)
.hidden_title(true)
.title_bar_style(TitleBarStyle::Overlay)
.transparent(true)
.effects(WindowEffectsConfig {
state: None,
effects: vec![Effect::WindowBackground],
radius: None,
color: None,
})
.build()
.unwrap();
#[cfg(not(target_os = "macos"))]
let _ =
WebviewWindowBuilder::new(app, "editor-0", WebviewUrl::App(PathBuf::from("editor")))
.title("Editor")
.min_inner_size(560., 340.)
.inner_size(560., 340.)
.build()
.unwrap();
}
}
"search" => {
if let Some(window) = app.get_window("search") {
if window.is_visible().unwrap_or_default() {
let _ = window.set_focus();
} else {
let _ = window.show();
let _ = window.set_focus();
};
} else {
#[cfg(target_os = "macos")]
let _ = WebviewWindowBuilder::new(app, "search", WebviewUrl::App(PathBuf::from("search")))
.title("Search")
.inner_size(400., 600.)
.minimizable(false)
.title_bar_style(TitleBarStyle::Overlay)
.transparent(true)
.effects(WindowEffectsConfig {
state: None,
effects: vec![Effect::WindowBackground],
radius: None,
color: None,
})
.build()
.unwrap();
#[cfg(not(target_os = "macos"))]
let _ = WebviewWindowBuilder::new(app, "Search", WebviewUrl::App(PathBuf::from("search")))
.title("Search")
.inner_size(750., 470.)
.minimizable(false)
.resizable(false)
.build()
.unwrap();
}
}
"about" => {
app.shell().open("https://lume.nu", None).unwrap();
}
"update" => {
println!("todo!")
}
"settings" => {
if let Some(window) = app.get_window("settings") {
if window.is_visible().unwrap_or_default() {
let _ = window.set_focus();
} else {
let _ = window.show();
let _ = window.set_focus();
};
} else {
#[cfg(target_os = "macos")]
let _ = WebviewWindowBuilder::new(
app,
"settings",
WebviewUrl::App(PathBuf::from("settings/general")),
)
.title("Settings")
.inner_size(800., 500.)
.title_bar_style(TitleBarStyle::Overlay)
.hidden_title(true)
.resizable(false)
.minimizable(false)
.transparent(true)
.effects(WindowEffectsConfig {
state: None,
effects: vec![Effect::WindowBackground],
radius: None,
color: None,
})
.build()
.unwrap();
#[cfg(not(target_os = "macos"))]
let _ = WebviewWindowBuilder::new(
app,
"settings",
WebviewUrl::App(PathBuf::from("settings/general")),
)
.title("Settings")
.inner_size(800., 500.)
.resizable(false)
.minimizable(false)
.build()
.unwrap();
}
}
"quit" => {
app.exit(0);
}
_ => {}
});
Ok(())
}

View File

@@ -1,123 +1,124 @@
{
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
"productName": "Lume",
"version": "4.0.7",
"identifier": "nu.lume.Lume",
"build": {
"beforeBuildCommand": "pnpm desktop:build",
"beforeDevCommand": "pnpm desktop:dev",
"devUrl": "http://localhost:3000",
"frontendDist": "../dist"
},
"app": {
"macOSPrivateApi": true,
"withGlobalTauri": true,
"trayIcon": {
"id": "main_tray",
"iconPath": "./icons/tray.png",
"iconAsTemplate": true
},
"security": {
"assetProtocol": {
"enable": true,
"scope": [
"$APPDATA/*",
"$DATA/*",
"$LOCALDATA/*",
"$DESKTOP/*",
"$DOCUMENT/*",
"$DOWNLOAD/*",
"$HOME/*",
"$PICTURE/*",
"$PUBLIC/*",
"$VIDEO/*",
"$APPCONFIG/*",
"$RESOURCE/*"
]
}
}
},
"bundle": {
"licenseFile": "../LICENSE",
"longDescription": "nostr client for desktop",
"shortDescription": "nostr client",
"targets": "all",
"active": true,
"category": "SocialNetworking",
"resources": ["resources/*", "locales/*"],
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"linux": {
"appimage": {
"bundleMediaFramework": true,
"files": {}
},
"deb": {
"files": {}
},
"rpm": {
"epoch": 0,
"files": {},
"release": "1"
}
},
"macOS": {
"dmg": {
"appPosition": {
"x": 180,
"y": 170
},
"applicationFolderPosition": {
"x": 480,
"y": 170
},
"windowSize": {
"height": 400,
"width": 660
}
},
"files": {},
"minimumSystemVersion": "10.15"
},
"windows": {
"allowDowngrades": true,
"certificateThumbprint": null,
"digestAlgorithm": "sha256",
"nsis": null,
"timestampUrl": null,
"tsp": false,
"webviewFixedRuntimePath": null,
"webviewInstallMode": {
"silent": true,
"type": "downloadBootstrapper"
},
"wix": null
},
"fileAssociations": [
{
"name": "bech32",
"description": "Nostr Bech32",
"ext": ["npub", "nsec", "nprofile", "nevent", "naddr", "nrelay"],
"role": "Viewer"
}
]
},
"plugins": {
"updater": {
"active": true,
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEU3OTdCMkM3RjU5QzE2NzkKUldSNUZwejF4N0tYNTVHYjMrU0JkL090SlEyNUVLYU5TM2hTU3RXSWtEWngrZWJ4a0pydUhXZHEK",
"windows": {
"installMode": "quiet"
},
"endpoints": [
"https://lus.reya3772.workers.dev/v1/{{target}}/{{arch}}/{{current_version}}",
"https://lus.reya3772.workers.dev/{{target}}/{{current_version}}"
]
}
}
"$schema": "../node_modules/@tauri-apps/cli/schema.json",
"productName": "Lume",
"version": "4.0.9",
"identifier": "nu.lume.Lume",
"build": {
"beforeBuildCommand": "pnpm desktop:build",
"beforeDevCommand": "pnpm desktop:dev",
"devUrl": "http://localhost:3000",
"frontendDist": "../dist"
},
"app": {
"macOSPrivateApi": true,
"withGlobalTauri": true,
"trayIcon": {
"id": "tray_panel",
"iconPath": "./icons/tray.png",
"iconAsTemplate": true,
"menuOnLeftClick": false
},
"security": {
"assetProtocol": {
"enable": true,
"scope": [
"$APPDATA/*",
"$DATA/*",
"$LOCALDATA/*",
"$DESKTOP/*",
"$DOCUMENT/*",
"$DOWNLOAD/*",
"$HOME/*",
"$PICTURE/*",
"$PUBLIC/*",
"$VIDEO/*",
"$APPCONFIG/*",
"$RESOURCE/*"
]
}
}
},
"bundle": {
"licenseFile": "../LICENSE",
"longDescription": "nostr client for desktop",
"shortDescription": "nostr client",
"targets": "all",
"active": true,
"category": "SocialNetworking",
"resources": ["resources/*", "locales/*"],
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"linux": {
"appimage": {
"bundleMediaFramework": true,
"files": {}
},
"deb": {
"files": {}
},
"rpm": {
"epoch": 0,
"files": {},
"release": "1"
}
},
"macOS": {
"dmg": {
"appPosition": {
"x": 180,
"y": 170
},
"applicationFolderPosition": {
"x": 480,
"y": 170
},
"windowSize": {
"height": 400,
"width": 660
}
},
"files": {},
"minimumSystemVersion": "10.15"
},
"windows": {
"allowDowngrades": true,
"certificateThumbprint": null,
"digestAlgorithm": "sha256",
"nsis": null,
"timestampUrl": null,
"tsp": false,
"webviewFixedRuntimePath": null,
"webviewInstallMode": {
"silent": true,
"type": "downloadBootstrapper"
},
"wix": null
},
"fileAssociations": [
{
"name": "bech32",
"description": "Nostr Bech32",
"ext": ["npub", "nsec", "nprofile", "nevent", "naddr", "nrelay"],
"role": "Viewer"
}
]
},
"plugins": {
"updater": {
"active": true,
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEU3OTdCMkM3RjU5QzE2NzkKUldSNUZwejF4N0tYNTVHYjMrU0JkL090SlEyNUVLYU5TM2hTU3RXSWtEWngrZWJ4a0pydUhXZHEK",
"windows": {
"installMode": "quiet"
},
"endpoints": [
"https://lus.reya3772.workers.dev/v1/{{target}}/{{arch}}/{{current_version}}",
"https://lus.reya3772.workers.dev/{{target}}/{{current_version}}"
]
}
}
}

View File

@@ -16,6 +16,21 @@
"windowEffects": {
"effects": ["windowBackground"]
}
},
{
"title": "Lume Panel",
"label": "panel",
"url": "/panel",
"width": 350,
"height": 500,
"fullscreen": false,
"resizable": false,
"decorations": false,
"transparent": true,
"visible": false,
"windowEffects": {
"effects": ["popover"]
}
}
]
}