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>
This commit is contained in:
雨宮蓮 2024-06-06 14:32:30 +07:00 committed by GitHub
parent 4e7da4108b
commit 799835a629
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 1006 additions and 977 deletions

View File

@ -21,6 +21,7 @@
"@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",

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

@ -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

@ -1,25 +1,17 @@
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";
export const Route = createFileRoute("/$account")({
@ -60,7 +52,6 @@ function Screen() {
<ComposeFilledIcon className="size-4" />
New Post
</button>
<Bell />
<button
type="button"
onClick={() => LumeWindow.openSearch()}
@ -195,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

@ -0,0 +1,308 @@
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 (
<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>
);
}

View File

@ -1,5 +1,6 @@
import { Metadata } from "@lume/types";
import { Result, commands } from "./commands";
import { Window } from "@tauri-apps/api/window";
export class NostrAccount {
static async getAccounts() {
@ -23,6 +24,9 @@ export class NostrAccount {
}
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);

View File

@ -144,20 +144,6 @@ export const commands = {
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 {
@ -334,6 +320,14 @@ export const commands = {
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 }) };

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);

View File

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

View File

@ -90,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)
@ -2250,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:

449
src-tauri/Cargo.lock generated
View File

@ -499,29 +499,6 @@ version = "0.10.0-beta"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98f7eed2b2781a6f0b5c903471d48e15f56fb4e1165df8a9a2337fd1a59d45ea"
[[package]]
name = "bindgen"
version = "0.69.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a00dc851838a2120612785d195287475a3ac45514741da670b735818822129a0"
dependencies = [
"bitflags 2.5.0",
"cexpr",
"clang-sys",
"itertools",
"lazy_static",
"lazycell",
"log",
"prettyplease",
"proc-macro2",
"quote",
"regex",
"rustc-hash",
"shlex",
"syn 2.0.66",
"which",
]
[[package]]
name = "bip39"
version = "2.0.0"
@ -705,17 +682,6 @@ dependencies = [
"serde",
]
[[package]]
name = "bzip2-sys"
version = "0.1.11+1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc"
dependencies = [
"cc",
"libc",
"pkg-config",
]
[[package]]
name = "cairo-rs"
version = "0.18.5"
@ -797,10 +763,6 @@ name = "cc"
version = "1.0.94"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17f6e324229dc011159fcc089755d1e2e216a90d43a7dea6853ca740b84f35e7"
dependencies = [
"jobserver",
"libc",
]
[[package]]
name = "cesu8"
@ -808,15 +770,6 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
[[package]]
name = "cexpr"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
dependencies = [
"nom",
]
[[package]]
name = "cfb"
version = "0.7.3"
@ -913,17 +866,6 @@ dependencies = [
"zeroize",
]
[[package]]
name = "clang-sys"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
dependencies = [
"glob",
"libc",
"libloading 0.8.3",
]
[[package]]
name = "clipboard-win"
version = "5.3.1"
@ -1168,36 +1110,6 @@ dependencies = [
"syn 2.0.66",
]
[[package]]
name = "curl"
version = "0.4.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e2161dd6eba090ff1594084e95fd67aeccf04382ffea77999ea94ed42ec67b6"
dependencies = [
"curl-sys",
"libc",
"openssl-probe",
"openssl-sys",
"schannel",
"socket2 0.5.7",
"windows-sys 0.52.0",
]
[[package]]
name = "curl-sys"
version = "0.4.72+curl-8.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29cbdc8314c447d11e8fd156dcdd031d9e02a7a976163e396b548c03153bc9ea"
dependencies = [
"cc",
"libc",
"libz-sys",
"openssl-sys",
"pkg-config",
"vcpkg",
"windows-sys 0.52.0",
]
[[package]]
name = "darling"
version = "0.20.9"
@ -1245,6 +1157,47 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c297a1c74b71ae29df00c3e22dd9534821d60eb9af5a0192823fa2acea70c2a"
[[package]]
name = "deadpool"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb84100978c1c7b37f09ed3ce3e5f843af02c2a2c431bae5b19230dad2c1b490"
dependencies = [
"async-trait",
"deadpool-runtime",
"num_cpus",
"tokio",
]
[[package]]
name = "deadpool-runtime"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b"
dependencies = [
"tokio",
]
[[package]]
name = "deadpool-sqlite"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8010e36e12f3be22543a5e478b4af20aeead9a700dd69581a5e050a070fc22c"
dependencies = [
"deadpool",
"deadpool-sync",
"rusqlite",
]
[[package]]
name = "deadpool-sync"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2cc213ac28dbe3eda41e1c1e40fad9a04b3ee3bb12f18dee5930a36aca7fa0e8"
dependencies = [
"deadpool-runtime",
]
[[package]]
name = "deranged"
version = "0.3.11"
@ -1587,6 +1540,18 @@ dependencies = [
"zune-inflate",
]
[[package]]
name = "fallible-iterator"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649"
[[package]]
name = "fallible-streaming-iterator"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
[[package]]
name = "fastrand"
version = "1.9.0"
@ -2232,6 +2197,15 @@ dependencies = [
"allocator-api2",
]
[[package]]
name = "hashlink"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7"
dependencies = [
"hashbrown 0.14.5",
]
[[package]]
name = "heck"
version = "0.4.1"
@ -2286,15 +2260,6 @@ dependencies = [
"digest",
]
[[package]]
name = "home"
version = "0.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5"
dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "html5ever"
version = "0.26.0"
@ -2303,26 +2268,12 @@ checksum = "bea68cab48b8459f17cf1c944c67ddc572d272d9f2b274140f223ecb1da4a3b7"
dependencies = [
"log",
"mac",
"markup5ever 0.11.0",
"markup5ever",
"proc-macro2",
"quote",
"syn 1.0.109",
]
[[package]]
name = "html5ever"
version = "0.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c13771afe0e6e846f1e67d038d4cb29998a6779f93c809212e4e9c32efd244d4"
dependencies = [
"log",
"mac",
"markup5ever 0.12.1",
"proc-macro2",
"quote",
"syn 2.0.66",
]
[[package]]
name = "http"
version = "1.1.0"
@ -2627,15 +2578,6 @@ dependencies = [
"once_cell",
]
[[package]]
name = "itertools"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "0.4.8"
@ -2693,15 +2635,6 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
[[package]]
name = "jobserver"
version = "0.1.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab46a6e9526ddef3ae7f787c06f0f2600639ba80ea3eade3d8e670a2230f51d6"
dependencies = [
"libc",
]
[[package]]
name = "jpeg-decoder"
version = "0.3.1"
@ -2778,7 +2711,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f29e4755b7b995046f510a7520c42b2fed58b77bd94d5a87a8eb43d2fd126da8"
dependencies = [
"cssparser",
"html5ever 0.26.0",
"html5ever",
"indexmap 1.9.3",
"matches",
"selectors",
@ -2790,12 +2723,6 @@ version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "lazycell"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
[[package]]
name = "lebe"
version = "0.5.2"
@ -2863,27 +2790,12 @@ dependencies = [
]
[[package]]
name = "librocksdb-sys"
version = "0.16.0+8.10.0"
name = "libsqlite3-sys"
version = "0.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce3d60bc059831dc1c83903fb45c103f75db65c5a7bf22272764d9cc683e348c"
dependencies = [
"bindgen",
"bzip2-sys",
"cc",
"glob",
"libc",
"libz-sys",
]
[[package]]
name = "libz-sys"
version = "1.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e143b5e666b2695d28f6bca6497720813f699c9602dd7f5cac91008b8ada7f9"
checksum = "cf4e226dcd58b4be396f7bd3c20da8fdee2911400705297ba7d2d7cc2c30f716"
dependencies = [
"cc",
"libc",
"pkg-config",
"vcpkg",
]
@ -2975,8 +2887,7 @@ dependencies = [
"cocoa",
"keyring",
"keyring-search",
"nostr-ndb",
"nostr-rocksdb",
"monitor",
"nostr-sdk",
"objc",
"rand 0.8.5",
@ -2985,6 +2896,7 @@ dependencies = [
"specta",
"tauri",
"tauri-build",
"tauri-nspanel",
"tauri-plugin-clipboard-manager",
"tauri-plugin-decorum",
"tauri-plugin-dialog",
@ -2999,7 +2911,6 @@ dependencies = [
"tauri-plugin-upload",
"tauri-specta",
"tokio",
"webpage",
]
[[package]]
@ -3044,32 +2955,6 @@ dependencies = [
"tendril",
]
[[package]]
name = "markup5ever"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16ce3abbeba692c8b8441d036ef91aea6df8da2c6b6e21c7e14d3c18e526be45"
dependencies = [
"log",
"phf 0.11.2",
"phf_codegen 0.11.2",
"string_cache",
"string_cache_codegen",
"tendril",
]
[[package]]
name = "markup5ever_rcdom"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edaa21ab3701bfee5099ade5f7e1f84553fd19228cf332f13cd6e964bf59be18"
dependencies = [
"html5ever 0.27.0",
"markup5ever 0.12.1",
"tendril",
"xml5ever",
]
[[package]]
name = "matchers"
version = "0.1.0"
@ -3115,12 +3000,6 @@ version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "minimal-lexical"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "minisign-verify"
version = "0.2.1"
@ -3148,6 +3027,20 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "monitor"
version = "0.1.0"
source = "git+https://github.com/ahkohd/tauri-toolkit?branch=v2#d43d8b8789752f15d9bfd227cbb36d8f47ac5c73"
dependencies = [
"cocoa",
"core-foundation",
"core-graphics",
"objc",
"serde",
"tauri",
"thiserror",
]
[[package]]
name = "muda"
version = "0.13.5"
@ -3256,16 +3149,6 @@ version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb"
[[package]]
name = "nom"
version = "7.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
dependencies = [
"memchr",
"minimal-lexical",
]
[[package]]
name = "nostr"
version = "0.31.2"
@ -3311,19 +3194,6 @@ dependencies = [
"tracing",
]
[[package]]
name = "nostr-ndb"
version = "0.31.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0b1ef671e7ae44165900656ebef42e01ba2aa29440bf1ca533e9db11584a1ca"
dependencies = [
"async-trait",
"nostr",
"nostr-database",
"nostrdb",
"tracing",
]
[[package]]
name = "nostr-relay-pool"
version = "0.31.0"
@ -3340,22 +3210,6 @@ dependencies = [
"tracing",
]
[[package]]
name = "nostr-rocksdb"
version = "0.31.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10441955e4c65b8246310448f8d014f7e3f563edb33f12b63603bc1b10554d5c"
dependencies = [
"async-trait",
"jobserver",
"nostr",
"nostr-database",
"num_cpus",
"rocksdb",
"tokio",
"tracing",
]
[[package]]
name = "nostr-sdk"
version = "0.31.0"
@ -3366,10 +3220,9 @@ dependencies = [
"lnurl-pay",
"nostr",
"nostr-database",
"nostr-ndb",
"nostr-relay-pool",
"nostr-rocksdb",
"nostr-signer",
"nostr-sqlite",
"nostr-zapper",
"nwc",
"thiserror",
@ -3391,6 +3244,22 @@ dependencies = [
"tracing",
]
[[package]]
name = "nostr-sqlite"
version = "0.31.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1dca940d759c07d3928008842ad0ac63fa693efd83f4e8821c9a1badb0be226e"
dependencies = [
"async-trait",
"deadpool-sqlite",
"nostr",
"nostr-database",
"rusqlite",
"thiserror",
"tokio",
"tracing",
]
[[package]]
name = "nostr-zapper"
version = "0.31.0"
@ -3402,21 +3271,6 @@ dependencies = [
"thiserror",
]
[[package]]
name = "nostrdb"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c9f900e272f974ff492e8414c7e581f6e18771cf62ddc6ec200cc49774d4ea0"
dependencies = [
"bindgen",
"cc",
"flatbuffers",
"libc",
"tokio",
"tracing",
"tracing-subscriber",
]
[[package]]
name = "nu-ansi-term"
version = "0.46.0"
@ -3992,16 +3846,6 @@ dependencies = [
"phf_shared 0.10.0",
]
[[package]]
name = "phf_codegen"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a"
dependencies = [
"phf_generator 0.11.2",
"phf_shared 0.11.2",
]
[[package]]
name = "phf_generator"
version = "0.8.0"
@ -4222,16 +4066,6 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
[[package]]
name = "prettyplease"
version = "0.2.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f12335488a2f3b0a83b14edad48dca9879ce89b2edd10e80237e4e852dd645e"
dependencies = [
"proc-macro2",
"syn 2.0.66",
]
[[package]]
name = "proc-macro-crate"
version = "1.3.1"
@ -4631,13 +4465,17 @@ dependencies = [
]
[[package]]
name = "rocksdb"
version = "0.22.0"
name = "rusqlite"
version = "0.30.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6bd13e55d6d7b8cd0ea569161127567cd587676c99f4472f779a0279aa60a7a7"
checksum = "a78046161564f5e7cd9008aff3b2990b3850dc8e0349119b98e8f251e099f24d"
dependencies = [
"libc",
"librocksdb-sys",
"bitflags 2.5.0",
"fallible-iterator",
"fallible-streaming-iterator",
"hashlink",
"libsqlite3-sys",
"smallvec",
]
[[package]]
@ -4646,12 +4484,6 @@ version = "0.1.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
[[package]]
name = "rustc-hash"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "rustc_version"
version = "0.4.0"
@ -5094,12 +4926,6 @@ dependencies = [
"winapi",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "signal-hook-registry"
version = "1.4.2"
@ -5562,6 +5388,22 @@ dependencies = [
"tauri-utils",
]
[[package]]
name = "tauri-nspanel"
version = "2.0.0-beta"
source = "git+https://github.com/ahkohd/tauri-nspanel?branch=v2#326463faaaa21fd41369207d50cfe5e3c4399997"
dependencies = [
"bitflags 2.5.0",
"block",
"cocoa",
"core-foundation",
"core-graphics",
"objc",
"objc-foundation",
"objc_id",
"tauri",
]
[[package]]
name = "tauri-plugin"
version = "2.0.0-beta.17"
@ -5878,7 +5720,7 @@ dependencies = [
"ctor",
"dunce",
"glob",
"html5ever 0.26.0",
"html5ever",
"infer",
"json-patch",
"kuchikiki",
@ -6740,20 +6582,6 @@ dependencies = [
"system-deps",
]
[[package]]
name = "webpage"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70862efc041d46e6bbaa82bb9c34ae0596d090e86cbd14bd9e93b36ee6802eac"
dependencies = [
"curl",
"html5ever 0.27.0",
"markup5ever_rcdom",
"serde",
"serde_json",
"url",
]
[[package]]
name = "webpki-roots"
version = "0.26.1"
@ -6805,18 +6633,6 @@ version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082"
[[package]]
name = "which"
version = "4.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7"
dependencies = [
"either",
"home",
"once_cell",
"rustix 0.38.34",
]
[[package]]
name = "winapi"
version = "0.3.9"
@ -7189,7 +7005,7 @@ dependencies = [
"dunce",
"gdkx11",
"gtk",
"html5ever 0.26.0",
"html5ever",
"http",
"javascriptcore-rs",
"jni",
@ -7275,17 +7091,6 @@ dependencies = [
"winapi",
]
[[package]]
name = "xml5ever"
version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c376f76ed09df711203e20c3ef5ce556f0166fa03d39590016c0fd625437fad"
dependencies = [
"log",
"mac",
"markup5ever 0.12.1",
]
[[package]]
name = "zbus"
version = "3.15.2"

View File

@ -11,11 +11,11 @@ rust-version = "1.70"
tauri-build = { version = "2.0.0-beta", features = [] }
[dependencies]
nostr-rocksdb = "^0.31"
nostr-sdk = { version = "0.31", features = ["ndb", "rocksdb"] }
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",
@ -33,16 +33,13 @@ tauri-plugin-process = { git = "https://github.com/tauri-apps/plugins-workspace"
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-plugin-decorum = "0.1.0"
webpage = { version = "2.0", features = ["serde"] }
keyring = "2"
keyring-search = "0.2.0"
specta = "^2.0.0-rc.12"
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"
[target.'cfg(target_family = "unix")'.dependencies]
nostr-ndb = "^0.31"
tauri-plugin-decorum = "0.1.0"
specta = "^2.0.0-rc.12"
keyring = "2"
keyring-search = "0.2.0"
[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/*"
}
]
}
]
}

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"]}}

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,
}
@ -39,7 +49,6 @@ fn main() {
nostr::keys::event_to_bech32,
nostr::keys::user_to_bech32,
nostr::keys::verify_nip05,
nostr::metadata::get_activities,
nostr::metadata::get_current_user_profile,
nostr::metadata::get_profile,
nostr::metadata::get_contact_list,
@ -55,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,
@ -90,25 +100,45 @@ 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();
let _ = fs::create_dir_all(home_dir.join("Lume/"));
tauri::async_runtime::block_on(async move {
// Create nostr database connection
let db_path = home_dir.join(&"Lume/database");
#[cfg(target_family = "unix")]
let database = NdbDatabase::open(db_path.to_str().unwrap());
#[cfg(target_os = "windows")]
let database = RocksDatabase::open(db_path.to_str().unwrap()).await;
// Create nostr connection
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(),
@ -135,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

@ -122,11 +122,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 {
@ -106,6 +107,7 @@ pub async fn load_account(
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();
@ -167,7 +169,7 @@ pub async fn load_account(
// Add relay to relay pool
let _ = client
.add_relay_with_opts(relay_url.clone(), opts)
.add_relay_with_opts(&relay_url, opts)
.await
.unwrap_or_default();
@ -177,20 +179,51 @@ pub async fn load_account(
}
};
// 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;
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
// 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(activity_id.clone(), vec![subscription], None)
.subscribe_with_id(notification_id.clone(), vec![filter], None)
.await;
// Handle notifications
@ -202,8 +235,68 @@ pub async fn load_account(
..
} = notification
{
if subscription_id == activity_id {
let _ = app.emit("activity", event.as_json());
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)

View File

@ -6,73 +6,6 @@ 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]
pub async fn get_current_user_profile(state: State<'_, Nostr>) -> Result<String, String> {
@ -468,6 +401,42 @@ pub async fn zap_event(
#[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,
@ -500,8 +469,6 @@ pub async fn get_following(
Ok(ret)
}
#[tauri::command]
#[specta::specta]
pub async fn get_followers(
state: State<'_, Nostr>,
public_key: &str,
@ -529,3 +496,34 @@ pub async fn get_followers(
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

@ -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

@ -13,9 +13,10 @@
"macOSPrivateApi": true,
"withGlobalTauri": true,
"trayIcon": {
"id": "main_tray",
"id": "tray_panel",
"iconPath": "./icons/tray.png",
"iconAsTemplate": true
"iconAsTemplate": true,
"menuOnLeftClick": false
},
"security": {
"assetProtocol": {

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"]
}
}
]
}