From 799835a6292b37fa7765d92e3de3a72cbac77473 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=A8=E5=AE=AE=E8=93=AE?= <123083837+reyamir@users.noreply.github.com> Date: Thu, 6 Jun 2024 14:32:30 +0700 Subject: [PATCH] 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 --- apps/desktop2/package.json | 1 + .../src/components/note/mentions/hashtag.tsx | 2 +- apps/desktop2/src/components/user/avatar.tsx | 2 +- apps/desktop2/src/routes/$account.tsx | 74 +-- .../src/routes/activity.$account.texts.tsx | 61 --- .../desktop2/src/routes/activity.$account.tsx | 50 -- .../src/routes/activity.$account.zaps.tsx | 67 --- apps/desktop2/src/routes/panel.tsx | 308 ++++++++++++ packages/system/src/account.ts | 4 + packages/system/src/commands.ts | 22 +- packages/system/src/query.ts | 26 +- packages/system/src/window.ts | 2 +- pnpm-lock.yaml | 31 ++ src-tauri/Cargo.lock | 449 +++++------------- src-tauri/Cargo.toml | 17 +- src-tauri/capabilities/main.json | 167 +++---- src-tauri/gen/schemas/capabilities.json | 2 +- src-tauri/src/fns.rs | 180 +++++++ src-tauri/src/main.rs | 61 ++- src-tauri/src/nostr/event.rs | 6 +- src-tauri/src/nostr/keys.rs | 115 ++++- src-tauri/src/nostr/metadata.rs | 136 +++--- src-tauri/src/tray.rs | 180 ------- src-tauri/tauri.conf.json | 5 +- src-tauri/tauri.macos.conf.json | 15 + 25 files changed, 1006 insertions(+), 977 deletions(-) delete mode 100644 apps/desktop2/src/routes/activity.$account.texts.tsx delete mode 100644 apps/desktop2/src/routes/activity.$account.tsx delete mode 100644 apps/desktop2/src/routes/activity.$account.zaps.tsx create mode 100644 apps/desktop2/src/routes/panel.tsx create mode 100644 src-tauri/src/fns.rs delete mode 100644 src-tauri/src/tray.rs diff --git a/apps/desktop2/package.json b/apps/desktop2/package.json index 653a4ccc..c2b351c7 100644 --- a/apps/desktop2/package.json +++ b/apps/desktop2/package.json @@ -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", diff --git a/apps/desktop2/src/components/note/mentions/hashtag.tsx b/apps/desktop2/src/components/note/mentions/hashtag.tsx index e1754678..b7def94f 100644 --- a/apps/desktop2/src/components/note/mentions/hashtag.tsx +++ b/apps/desktop2/src/components/note/mentions/hashtag.tsx @@ -2,7 +2,7 @@ export function Hashtag({ tag }: { tag: string }) { return ( - - ); -} diff --git a/apps/desktop2/src/routes/activity.$account.texts.tsx b/apps/desktop2/src/routes/activity.$account.texts.tsx deleted file mode 100644 index 629b99c6..00000000 --- a/apps/desktop2/src/routes/activity.$account.texts.tsx +++ /dev/null @@ -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 ( - - - - - } - > - - {(events) => - events.map((event) => ( -
- - -
- - -
- - -
- -
-
-
-
- )) - } -
-
-
- ); -} diff --git a/apps/desktop2/src/routes/activity.$account.tsx b/apps/desktop2/src/routes/activity.$account.tsx deleted file mode 100644 index 99d22c0a..00000000 --- a/apps/desktop2/src/routes/activity.$account.tsx +++ /dev/null @@ -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 ( - - -
-
- - {({ isActive }) => ( -
- Notes -
- )} - - - {({ isActive }) => ( -
- Zaps -
- )} - -
-
-
- -
-
-
- ); -} diff --git a/apps/desktop2/src/routes/activity.$account.zaps.tsx b/apps/desktop2/src/routes/activity.$account.zaps.tsx deleted file mode 100644 index a0eb15bc..00000000 --- a/apps/desktop2/src/routes/activity.$account.zaps.tsx +++ /dev/null @@ -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 ( - - - - - } - > - - {(events) => - events.map((event) => ( -
- - -
- ₿ {decodeZapInvoice(event.tags).bitcoinFormatted} -
-
-
- - -
-
- zapped you -
-
-
-
-
- )) - } -
-
-
- ); -} diff --git a/apps/desktop2/src/routes/panel.tsx b/apps/desktop2/src/routes/panel.tsx new file mode 100644 index 00000000..a77ad7cd --- /dev/null +++ b/apps/desktop2/src/routes/panel.tsx @@ -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(null); + const [events, setEvents] = useState([]); + + const texts = useMemo( + () => events.filter((ev) => ev.kind === Kind.Text), + [events], + ); + + const zaps = useMemo(() => { + const groups = new Map(); + 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(); + 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( + "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 ( +
+ Please log in. +
+ ); + } + + return ( +
+
+
+

Notifications

+
+
+ + + + + + +
+
+ + + + Replies + + + Reactions + + + Zaps + + +
+ + {texts.map((event) => ( + + ))} + + + {[...reactions.entries()].map(([root, events]) => ( +
+
+
+ +
+
+ {events.map((event) => ( + + + +
+ {event.kind === Kind.Reaction ? ( + event.content === "+" ? ( + "👍" + ) : ( + event.content + ) + ) : ( + + )} +
+
+
+ ))} +
+
+
+ ))} +
+ + {[...zaps.entries()].map(([root, events]) => ( +
+
+
+ +
+
+ {events.map((event) => ( + tag[0] == "P")[1]} + > + + +
+ ₿ {decodeZapInvoice(event.tags).bitcoinFormatted} +
+
+
+ ))} +
+
+
+ ))} +
+
+
+
+ ); +} + +function RootNote({ id }: { id: string }) { + const { isLoading, isError, data } = useEvent(id); + + if (isLoading) { + return ( +
+
+
+
+ ); + } + + if (isError || !data) { + return ( +
+
+ +
+

+ Event not found with your current relay set +

+
+ ); + } + + return ( + + + + + + + +
{data.content}
+
+
+ ); +} + +function TextNote({ event }: { event: NostrEvent }) { + const pTags = event.tags + .filter((tag) => tag[0] === "p") + .map((tag) => tag[1]) + .slice(0, 3); + + return ( + + + + + +
+
+ + + {formatCreatedAt(event.created_at)} + +
+
+ + Reply to: + +
+ {pTags.map((replyTo) => ( + + + + + + ))} +
+
+
+
+
+
+
+
{event.content}
+
+ + + ); +} diff --git a/packages/system/src/account.ts b/packages/system/src/account.ts index 537639c5..5feb44d9 100644 --- a/packages/system/src/account.ts +++ b/packages/system/src/account.ts @@ -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); diff --git a/packages/system/src/commands.ts b/packages/system/src/commands.ts index 244cacf6..3bb09a6c 100644 --- a/packages/system/src/commands.ts +++ b/packages/system/src/commands.ts @@ -144,20 +144,6 @@ export const commands = { else return { status: "error", error: e as any }; } }, - async getActivities( - account: string, - kind: string, - ): Promise> { - 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> { try { return { @@ -334,6 +320,14 @@ export const commands = { else return { status: "error", error: e as any }; } }, + async getNotifications(): Promise> { + 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> { try { return { status: "ok", data: await TAURI_INVOKE("get_event", { id }) }; diff --git a/packages/system/src/query.ts b/packages/system/src/query.ts index c37b5014..afa83444 100644 --- a/packages/system/src/query.ts +++ b/packages/system/src/query.ts @@ -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); diff --git a/packages/system/src/window.ts b/packages/system/src/window.ts index e7de3957..691acbfe 100644 --- a/packages/system/src/window.ts +++ b/packages/system/src/window.ts @@ -92,7 +92,7 @@ export class LumeWindow { const query = await commands.openWindow( label, "Settings", - "/settings", + "/settings/general", 800, 500, ); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4bb3dc15..b76c3072 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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: diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index b71b62cb..c7cc123c 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -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" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index fd5f2604..a64839f1 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -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" diff --git a/src-tauri/capabilities/main.json b/src-tauri/capabilities/main.json index 35f76a2a..69e83613 100644 --- a/src-tauri/capabilities/main.json +++ b/src-tauri/capabilities/main.json @@ -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/*" + } + ] + } + ] } diff --git a/src-tauri/gen/schemas/capabilities.json b/src-tauri/gen/schemas/capabilities.json index e6d74327..4875d4ba 100644 --- a/src-tauri/gen/schemas/capabilities.json +++ b/src-tauri/gen/schemas/capabilities.json @@ -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"]}} \ No newline at end of file +{"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"]}} \ No newline at end of file diff --git a/src-tauri/src/fns.rs b/src-tauri/src/fns.rs new file mode 100644 index 00000000..ca48b009 --- /dev/null +++ b/src-tauri/src/fns.rs @@ -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::(scale_factor); + + let monitor_size = visible_area.size().to_logical::(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) { + 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() +} diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 4818b7e8..805006ea 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -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()) diff --git a/src-tauri/src/nostr/event.rs b/src-tauri/src/nostr/event.rs index 9da97073..a0a56575 100644 --- a/src-tauri/src/nostr/event.rs +++ b/src-tauri/src/nostr/event.rs @@ -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()), diff --git a/src-tauri/src/nostr/keys.rs b/src-tauri/src/nostr/keys.rs index bee6e163..74f53eb2 100644 --- a/src-tauri/src/nostr/keys.rs +++ b/src-tauri/src/nostr/keys.rs @@ -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 { + 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::(); + 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::(); 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) diff --git a/src-tauri/src/nostr/metadata.rs b/src-tauri/src/nostr/metadata.rs index dc8891b4..8450dcea 100644 --- a/src-tauri/src/nostr/metadata.rs +++ b/src-tauri/src/nostr/metadata.rs @@ -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, 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 { - let client = &state.client; - - match PublicKey::from_bech32(npub) { - Ok(author) => { - let mut contact_list: Vec = 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 { @@ -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 { + let client = &state.client; + + match PublicKey::from_bech32(npub) { + Ok(author) => { + let mut contact_list: Vec = 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, 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()), + } +} diff --git a/src-tauri/src/tray.rs b/src-tauri/src/tray.rs deleted file mode 100644 index 30db2ae9..00000000 --- a/src-tauri/src/tray.rs +++ /dev/null @@ -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(app: &tauri::AppHandle) -> 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(()) -} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 911ed311..a71a8418 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -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": { diff --git a/src-tauri/tauri.macos.conf.json b/src-tauri/tauri.macos.conf.json index aef0f280..5509fbf5 100644 --- a/src-tauri/tauri.macos.conf.json +++ b/src-tauri/tauri.macos.conf.json @@ -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"] + } } ] }