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 afa539e
Show file tree
Hide file tree
Showing 6 changed files with 84 additions and 5 deletions.
2 changes: 2 additions & 0 deletions Cargo.lock

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

9 changes: 9 additions & 0 deletions crates/radicle-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,15 @@ 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" }

[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
# when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL
Expand Down
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.");
}
}
5 changes: 3 additions & 2 deletions crates/radicle-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,10 @@ 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,
inbox::set_badge,
cob::get_embed,
cob::save_embed_to_disk,
cob::save_embed_by_path,
Expand Down
38 changes: 37 additions & 1 deletion src/App.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
<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 { invoke } from "@app/lib/invoke";
import { listen } from "@tauri-apps/api/event";
Expand All @@ -19,12 +22,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 +44,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 +62,44 @@
});
void dynamicInterval(checkAuth, 1000);
}
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 80 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 84 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)),
});
console.log("YAY");
} 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 afa539e

Please sign in to comment.