From 1626a40f32328b37c8f837c6f56bd4ae56da479e Mon Sep 17 00:00:00 2001 From: cloudwithax Date: Sun, 15 Feb 2026 19:54:22 -0500 Subject: [PATCH] feat: integrate notification functionality with Tauri plugin --- bun.lock | 3 + package.json | 1 + src-tauri/Cargo.lock | 387 +++++++++++++++++++++++++++- src-tauri/Cargo.toml | 1 + src-tauri/capabilities/default.json | 5 +- src-tauri/src/lib.rs | 1 + src/App.tsx | 51 +++- src/lib/notifications.ts | 94 +++++++ 8 files changed, 536 insertions(+), 7 deletions(-) create mode 100644 src/lib/notifications.ts diff --git a/bun.lock b/bun.lock index 4d0753c..caa38b1 100644 --- a/bun.lock +++ b/bun.lock @@ -8,6 +8,7 @@ "@fontsource-variable/jetbrains-mono": "^5.0.0", "@fontsource/space-grotesk": "^5.2.0", "@tauri-apps/api": "^2", + "@tauri-apps/plugin-notification": "^2.3.3", "@tauri-apps/plugin-shell": "^2", "@thisbeyond/solid-dnd": "^0.7.5", "@tiptap/core": "^2.12.0", @@ -267,6 +268,8 @@ "@tauri-apps/cli-win32-x64-msvc": ["@tauri-apps/cli-win32-x64-msvc@2.10.0", "", { "os": "win32", "cpu": "x64" }, "sha512-NTpyQxkpzGmU6ceWBTY2xRIEaS0ZLbVx1HE1zTA3TY/pV3+cPoPPOs+7YScr4IMzXMtOw7tLw5LEXo5oIG3qaQ=="], + "@tauri-apps/plugin-notification": ["@tauri-apps/plugin-notification@2.3.3", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-Zw+ZH18RJb41G4NrfHgIuofJiymusqN+q8fGUIIV7vyCH+5sSn5coqRv/MWB9qETsUs97vmU045q7OyseCV3Qg=="], + "@tauri-apps/plugin-shell": ["@tauri-apps/plugin-shell@2.3.5", "", { "dependencies": { "@tauri-apps/api": "^2.10.1" } }, "sha512-jewtULhiQ7lI7+owCKAjc8tYLJr92U16bPOeAa472LHJdgaibLP83NcfAF2e+wkEcA53FxKQAZ7byDzs2eeizg=="], "@thisbeyond/solid-dnd": ["@thisbeyond/solid-dnd@0.7.5", "", { "peerDependencies": { "solid-js": "^1.5" } }, "sha512-DfI5ff+yYGpK9M21LhYwIPlbP2msKxN2ARwuu6GF8tT1GgNVDTI8VCQvH4TJFoVApP9d44izmAcTh/iTCH2UUw=="], diff --git a/package.json b/package.json index 605be72..6a18582 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "@fontsource-variable/jetbrains-mono": "^5.0.0", "@fontsource/space-grotesk": "^5.2.0", "@tauri-apps/api": "^2", + "@tauri-apps/plugin-notification": "^2.3.3", "@tauri-apps/plugin-shell": "^2", "@thisbeyond/solid-dnd": "^0.7.5", "@tiptap/core": "^2.12.0", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index f25e163..4fb9b00 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -189,6 +189,44 @@ dependencies = [ "syn 2.0.115", ] +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + [[package]] name = "async-io" version = "2.6.0" @@ -207,6 +245,70 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.115", +] + +[[package]] +name = "async-signal" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + [[package]] name = "async-trait" version = "0.1.89" @@ -446,6 +548,19 @@ dependencies = [ "objc2", ] +[[package]] +name = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + [[package]] name = "brotli" version = "8.0.2" @@ -1177,6 +1292,7 @@ dependencies = [ "sha2", "tauri", "tauri-build", + "tauri-plugin-notification", "tauri-plugin-shell", "tokio", "webkit2gtk", @@ -1247,6 +1363,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "endi" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" + [[package]] name = "enum-as-inner" version = "0.6.1" @@ -1259,6 +1381,27 @@ dependencies = [ "syn 2.0.115", ] +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.115", +] + [[package]] name = "env_filter" version = "1.0.0" @@ -1309,6 +1452,33 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + [[package]] name = "fdeflate" version = "0.3.7" @@ -1473,7 +1643,10 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" dependencies = [ + "fastrand", "futures-core", + "futures-io", + "parking", "pin-project-lite", ] @@ -3208,6 +3381,18 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" +[[package]] +name = "mac-notification-sys" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65fd3f75411f4725061682ed91f131946e912859d0044d39c4ec0aac818d7621" +dependencies = [ + "cc", + "objc2", + "objc2-foundation", + "time", +] + [[package]] name = "markup5ever" version = "0.14.1" @@ -3513,6 +3698,20 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "notify-rust" +version = "4.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21af20a1b50be5ac5861f74af1a863da53a11c38684d9818d82f1c42f7fdc6c2" +dependencies = [ + "futures-lite", + "log", + "mac-notification-sys", + "serde", + "tauri-winrt-notification", + "zbus", +] + [[package]] name = "num-bigint" version = "0.4.6" @@ -3836,6 +4035,16 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + [[package]] name = "os_pipe" version = "1.2.3" @@ -4094,6 +4303,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "piper" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + [[package]] name = "pkcs8" version = "0.10.2" @@ -4118,7 +4338,7 @@ checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" dependencies = [ "base64 0.22.1", "indexmap 2.13.0", - "quick-xml", + "quick-xml 0.38.4", "serde", "time", ] @@ -4331,6 +4551,15 @@ dependencies = [ "unsigned-varint 0.8.0", ] +[[package]] +name = "quick-xml" +version = "0.37.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" +dependencies = [ + "memchr", +] + [[package]] name = "quick-xml" version = "0.38.4" @@ -5637,6 +5866,25 @@ dependencies = [ "walkdir", ] +[[package]] +name = "tauri-plugin-notification" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01fc2c5ff41105bd1f7242d8201fdf3efd70749b82fa013a17f2126357d194cc" +dependencies = [ + "log", + "notify-rust", + "rand 0.9.2", + "serde", + "serde_json", + "serde_repr", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", + "time", + "url", +] + [[package]] name = "tauri-plugin-shell" version = "2.3.5" @@ -5759,6 +6007,31 @@ dependencies = [ "toml 0.9.12+spec-1.1.0", ] +[[package]] +name = "tauri-winrt-notification" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b1e66e07de489fe43a46678dd0b8df65e0c973909df1b60ba33874e297ba9b9" +dependencies = [ + "quick-xml 0.37.5", + "thiserror 2.0.18", + "windows 0.61.3", + "windows-version", +] + +[[package]] +name = "tempfile" +version = "3.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1" +dependencies = [ + "fastrand", + "getrandom 0.3.4", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + [[package]] name = "tendril" version = "0.4.3" @@ -6121,6 +6394,17 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +[[package]] +name = "uds_windows" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" +dependencies = [ + "memoffset", + "tempfile", + "winapi", +] + [[package]] name = "uint" version = "0.9.5" @@ -7283,6 +7567,67 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zbus" +version = "5.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfeff997a0aaa3eb20c4652baf788d2dfa6d2839a0ead0b3ff69ce2f9c4bdd1" +dependencies = [ + "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "libc", + "ordered-stream", + "rustix", + "serde", + "serde_repr", + "tracing", + "uds_windows", + "uuid", + "windows-sys 0.61.2", + "winnow 0.7.14", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bbd5a90dbe8feee5b13def448427ae314ccd26a49cac47905cafefb9ff846f1" +dependencies = [ + "proc-macro-crate 3.4.0", + "proc-macro2", + "quote", + "syn 2.0.115", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffd8af6d5b78619bab301ff3c560a5bd22426150253db278f164d6cf3b72c50f" +dependencies = [ + "serde", + "winnow 0.7.14", + "zvariant", +] + [[package]] name = "zerocopy" version = "0.8.39" @@ -7382,3 +7727,43 @@ name = "zmij" version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zvariant" +version = "5.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68b64ef4f40c7951337ddc7023dd03528a57a3ce3408ee9da5e948bd29b232c4" +dependencies = [ + "endi", + "enumflags2", + "serde", + "winnow 0.7.14", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "484d5d975eb7afb52cc6b929c13d3719a20ad650fea4120e6310de3fc55e415c" +dependencies = [ + "proc-macro-crate 3.4.0", + "proc-macro2", + "quote", + "syn 2.0.115", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75c23a64ef8f40f13a6989991e643554d9bef1d682a281160cf0c1bc389c5e9" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn 2.0.115", + "winnow 0.7.14", +] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index add6863..290c477 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -15,6 +15,7 @@ tauri-build = { version = "2", features = [] } [dependencies] tauri = { version = "2", features = [] } tauri-plugin-shell = "2" +tauri-plugin-notification = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" tokio = { version = "1", features = ["full"] } diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 239cfb9..dd1490c 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -10,6 +10,9 @@ "core:event:allow-emit", "core:event:allow-listen", "core:event:allow-unlisten", - "shell:allow-open" + "shell:allow-open", + "notification:default", + "notification:allow-is-permission-granted", + "notification:allow-request-permission" ] } \ No newline at end of file diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index aced06d..d8266cf 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -55,6 +55,7 @@ pub fn run() { tauri::Builder::default() .plugin(tauri_plugin_shell::init()) + .plugin(tauri_plugin_notification::init()) .manage(AppState::new()) .setup(|app| { // grant microphone/camera permissions on linux webkitgtk diff --git a/src/App.tsx b/src/App.tsx index 32ac792..634bbe3 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -48,6 +48,7 @@ import { setCategories, addCategory, categories, + channels, } from "./stores/channels"; import { addMessage, @@ -107,6 +108,12 @@ import type { DirectMessage, } from "./lib/types"; import { resetSettings } from "./stores/settings"; +import { + initNotifications, + notifyChannelMessage, + notifyDirectMessage, + isWindowFocused, +} from "./lib/notifications"; const App: Component = () => { let cleanupResize: (() => void) | undefined; @@ -276,6 +283,9 @@ const App: Component = () => { // settings not found, use defaults } + // initialize notification permission + await initNotifications(); + // load the peer directory and friends list try { const peers = await tauri.getKnownPeers(); @@ -322,11 +332,26 @@ const App: Component = () => { function handleDuskEvent(event: DuskEvent) { switch (event.kind) { - case "message_received": - if (event.payload.channel_id === activeChannelId()) { - addMessage(event.payload); + case "message_received": { + const msg = event.payload; + const currentChannelId = activeChannelId(); + const currentCommunity = activeCommunity(); + + // add to store if this is the active channel + if (msg.channel_id === currentChannelId) { + addMessage(msg); + } + + // send notification if window is not focused or this is not the active channel + if (!isWindowFocused() || msg.channel_id !== currentChannelId) { + const channelList = channels(); + const channel = channelList.find((c) => c.id === msg.channel_id); + const channelName = channel?.name ?? "unknown channel"; + const communityName = currentCommunity?.name ?? "unknown community"; + notifyChannelMessage(msg, channelName, communityName); } break; + } case "message_deleted": removeMessage(event.payload.message_id); break; @@ -385,9 +410,25 @@ const App: Component = () => { case "relay_status": setRelayConnected(event.payload.connected); break; - case "dm_received": - handleIncomingDM(event.payload); + case "dm_received": { + const dm = event.payload; + handleIncomingDM(dm); + + // send notification if window is not focused or this is not the active dm + const currentDMPeer = activeDMPeerId(); + if (!isWindowFocused() || dm.from_peer !== currentDMPeer) { + notifyDirectMessage({ + id: dm.id, + channel_id: "", + author_id: dm.from_peer, + author_name: dm.from_display_name, + content: dm.content, + timestamp: dm.timestamp, + edited: false, + }); + } break; + } case "dm_typing": // only show typing if the sender is the active dm peer if (event.payload.peer_id === activeDMPeerId()) { diff --git a/src/lib/notifications.ts b/src/lib/notifications.ts new file mode 100644 index 0000000..ed72018 --- /dev/null +++ b/src/lib/notifications.ts @@ -0,0 +1,94 @@ +import { + isPermissionGranted, + requestPermission, + sendNotification as tauriSendNotification, +} from "@tauri-apps/plugin-notification"; +import { settings } from "../stores/settings"; +import type { ChatMessage } from "./types"; + +// track if we have notification permission +let permissionGranted = false; + +// check and request notification permission on module load +export async function initNotifications(): Promise { + try { + // check if already granted + const granted = await isPermissionGranted(); + if (granted) { + permissionGranted = true; + return true; + } + + // request permission + const permission = await requestPermission(); + permissionGranted = permission === "granted"; + return permissionGranted; + } catch (error) { + console.error("failed to initialize notifications:", error); + return false; + } +} + +// send a desktop notification if settings allow +export async function sendNotification(title: string, body: string): Promise { + // check if notifications are enabled in settings + const currentSettings = settings(); + if (!currentSettings.enable_desktop_notifications) { + return; + } + + // check permission + if (!permissionGranted) { + const granted = await initNotifications(); + if (!granted) { + return; + } + } + + try { + tauriSendNotification({ title, body }); + } catch (error) { + console.error("failed to send notification:", error); + } +} + +// send notification for a channel message +export async function notifyChannelMessage( + message: ChatMessage, + channelName: string, + communityName: string, +): Promise { + const currentSettings = settings(); + + // dont notify if previews are disabled + if (!currentSettings.enable_message_preview) { + // send notification without message content + await sendNotification( + `${message.author_name} in ${communityName} > ${channelName}`, + "New message", + ); + return; + } + + await sendNotification( + `${message.author_name} in ${communityName} > ${channelName}`, + message.content, + ); +} + +// send notification for a direct message +export async function notifyDirectMessage(message: ChatMessage): Promise { + const currentSettings = settings(); + + if (!currentSettings.enable_message_preview) { + await sendNotification(`${message.author_name}`, "Sent you a message"); + return; + } + + await sendNotification(`${message.author_name}`, message.content); +} + +// check if the window is focused +export function isWindowFocused(): boolean { + return document.hasFocus(); +}