feat: integrate notification functionality with Tauri plugin

This commit is contained in:
cloudwithax 2026-02-15 19:54:22 -05:00
parent 4bf42706c3
commit 1626a40f32
8 changed files with 536 additions and 7 deletions

View File

@ -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=="],

View File

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

387
src-tauri/Cargo.lock generated
View File

@ -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",
]

View File

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

View File

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

View File

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

View File

@ -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()) {

94
src/lib/notifications.ts Normal file
View File

@ -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<boolean> {
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<void> {
// 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<void> {
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<void> {
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();
}