Skip to content

Commit

Permalink
Set app badge to notification count periodically
Browse files Browse the repository at this point in the history
  • Loading branch information
rudolfs committed Jan 24, 2025
1 parent 2a7c705 commit 04362ed
Show file tree
Hide file tree
Showing 9 changed files with 160 additions and 9 deletions.
53 changes: 52 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions crates/radicle-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,16 @@ thiserror = { version = "1.0.64" }
ts-rs = { version = "10.0.0", features = ["serde-json-impl", "no-serde-warnings"] }
tokio = { version = "1.40.0", features = ["time"] }
tauri-plugin-dialog = { version = "2.2.0" }
tauri-plugin-os = "2"

[target.'cfg(target_os = "macos")'.dependencies]
objc2-foundation = { version = "0.2.2", features = [
"NSThread",
] }
objc2-app-kit = { version = "0.2.2", features = [
"NSApplication",
"NSDockTile",
] }

[features]
# by default Tauri runs in production mode
Expand Down
9 changes: 6 additions & 3 deletions crates/radicle-tauri/capabilities/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main"],
"windows": [
"main"
],
"permissions": [
"core:path:default",
"core:event:default",
Expand All @@ -17,6 +19,7 @@
"clipboard-manager:default",
"clipboard-manager:allow-write-text",
"log:default",
"dialog:default"
"dialog:default",
"os:default"
]
}
}
25 changes: 25 additions & 0 deletions crates/radicle-tauri/src/commands/inbox.rs
Original file line number Diff line number Diff line change
Expand Up @@ -179,3 +179,28 @@ pub fn clear_notifications(

Ok(())
}

#[tauri::command]
#[cfg(target_os = "macos")]
pub fn set_badge(count: i32) {
use objc2_app_kit::NSApp;
use objc2_foundation::{MainThreadMarker, NSString};

let label = if count > 0 {
Some(NSString::from_str(&format!("{}", count)))
} else {
None
};

if let Some(thread) = MainThreadMarker::new() {
unsafe {
let app = NSApp(thread);
let dock_tile = app.dockTile();

dock_tile.setBadgeLabel(label.as_deref());
dock_tile.display();
}
} else {
eprintln!("Failed to obtain MainThreadMarker.");
}
}
7 changes: 5 additions & 2 deletions crates/radicle-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ use commands::{auth, cob, diff, inbox, profile, repo, thread};
pub fn run() {
#[cfg(debug_assertions)]
let builder = tauri::Builder::default()
.plugin(tauri_plugin_os::init())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_log::Builder::new().build());
#[cfg(not(debug_assertions))]
Expand Down Expand Up @@ -84,9 +85,11 @@ pub fn run() {
repo::diff_stats,
repo::list_commits,
diff::get_diff,
inbox::list_notifications,
inbox::count_notifications_by_repo,
inbox::clear_notifications,
inbox::count_notifications_by_repo,
inbox::list_notifications,
#[cfg(target_os = "macos")]
inbox::set_badge,
cob::get_embed,
cob::save_embed_to_disk,
cob::save_embed_by_path,
Expand Down
10 changes: 10 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"@tauri-apps/plugin-clipboard-manager": "^2.2.0",
"@tauri-apps/plugin-dialog": "^2.2.0",
"@tauri-apps/plugin-log": "^2.2.0",
"@tauri-apps/plugin-os": "^2.2.0",
"@tauri-apps/plugin-shell": "^2.2.0",
"@tauri-apps/plugin-window-state": "^2.2.0"
},
Expand Down
44 changes: 43 additions & 1 deletion src/App.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
<script lang="ts">
import type { UnlistenFn } from "@tauri-apps/api/event";
import type { NotificationCount } from "@bindings/cob/inbox/NotificationCount";
import { onDestroy, onMount } from "svelte";
import { SvelteMap } from "svelte/reactivity";
import sum from "lodash/sum";
import { platform } from "@tauri-apps/plugin-os";
import { invoke } from "@app/lib/invoke";
import { listen } from "@tauri-apps/api/event";
Expand All @@ -19,12 +23,14 @@
import Patch from "@app/views/repo/Patch.svelte";
import Patches from "@app/views/repo/Patches.svelte";
import Repos from "./views/home/Repos.svelte";
import { dynamicInterval, checkAuth } from "./lib/auth";
import { dynamicInterval, checkAuth, authenticated } from "./lib/auth";
const activeRouteStore = router.activeRouteStore;
let unlistenEvents: UnlistenFn | undefined = undefined;
let unlistenNodeEvents: UnlistenFn | undefined = undefined;
let notificationPoll: ReturnType<typeof setInterval> | undefined = undefined;
let pollingNotificationsInProgress: boolean = false;
onMount(async () => {
if (window.__TAURI_INTERNALS__) {
Expand All @@ -39,13 +45,15 @@
try {
await invoke("authenticate");
authenticated.set(true);
void router.loadFromLocation();
void dynamicInterval(
checkAuth,
import.meta.env.VITE_AUTH_LONG_DELAY || 30_000,
);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (e: any) {
authenticated.set(false);
void router.push({
resource: "authenticationError",
params: {
Expand All @@ -55,15 +63,49 @@
});
void dynamicInterval(checkAuth, 1000);
}
if (window.__TAURI_OS_PLUGIN_INTERNALS__ && platform() === "macos") {
await setNotificationBadge();
notificationPoll = setInterval(async () => {
if (
pollingNotificationsInProgress ||
!$nodeRunning ||
!$authenticated
) {
return;
}
await setNotificationBadge();
}, 5_000);
}
});
async function setNotificationBadge() {
try {
pollingNotificationsInProgress = true;
let count = await invoke<Record<string, NotificationCount>>(

Check warning on line 87 in src/App.svelte

View workflow job for this annotation

GitHub Actions / lint typescript

'count' is never reassigned. Use 'const' instead
"count_notifications_by_repo",
);
const notificationCount = new SvelteMap(Object.entries(count));
invoke("set_badge", {

Check warning on line 91 in src/App.svelte

View workflow job for this annotation

GitHub Actions / lint typescript

Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator
count: sum(Array.from(notificationCount.values()).map(c => c.count)),
});
} finally {
pollingNotificationsInProgress = false;
}
}
onDestroy(() => {
if (unlistenEvents) {
unlistenEvents();
}
if (unlistenNodeEvents) {
unlistenNodeEvents();
}
if (notificationPoll) {
clearInterval(notificationPoll);
}
});
$effect(() => document.documentElement.setAttribute("data-theme", $theme));
Expand Down
10 changes: 8 additions & 2 deletions src/lib/auth.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { invoke } from "@app/lib/invoke";
import { activeRouteStore } from "@app/lib/router";
import { get } from "svelte/store";
import { writable } from "svelte/store";

import * as router from "@app/lib/router";
import { activeRouteStore } from "@app/lib/router";
import { invoke } from "@app/lib/invoke";

let intervalId: ReturnType<typeof setTimeout>;

export const authenticated = writable<boolean>(false);

export function dynamicInterval(callback: () => void, period: number) {
clearTimeout(intervalId);

Expand All @@ -23,12 +27,14 @@ export async function checkAuth() {
}
lock = true;
await invoke("authenticate");
authenticated.set(true);
if (get(activeRouteStore).resource === "authenticationError") {
window.history.back();
}
dynamicInterval(checkAuth, import.meta.env.VITE_AUTH_LONG_DELAY || 30_000);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (e: any) {
authenticated.set(false);
if (get(activeRouteStore).resource !== "authenticationError") {
await router.push({
resource: "authenticationError",
Expand Down

0 comments on commit 04362ed

Please sign in to comment.