Radish alpha
r
Radicle desktop app
Radicle
Git (anonymous pull)
Log in to clone via SSH
Set app badge to notification count periodically
Rūdolfs Ošiņš committed 1 year ago
commit 18a3d6fec3568a35899b6f3bc9f39db53a464922
parent ea43ffd44e343a4887ae952ed5e5d0983265298c
9 files changed +160 -9
modified Cargo.lock
@@ -1925,6 +1925,16 @@ dependencies = [
]

[[package]]
+
name = "gethostname"
+
version = "0.5.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "dc3655aa6818d65bc620d6911f05aa7b6aeb596291e1e9f79e52df85583d1e30"
+
dependencies = [
+
 "rustix",
+
 "windows-targets 0.52.6",
+
]
+

+
[[package]]
name = "getrandom"
version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3457,6 +3467,17 @@ dependencies = [
]

[[package]]
+
name = "os_info"
+
version = "3.9.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "6e6520c8cc998c5741ee68ec1dc369fc47e5f0ea5320018ecf2a1ccd6328f48b"
+
dependencies = [
+
 "log",
+
 "serde",
+
 "windows-sys 0.52.0",
+
]
+

+
[[package]]
name = "os_pipe"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -4112,6 +4133,8 @@ dependencies = [
 "anyhow",
 "base64 0.22.1",
 "log",
+
 "objc2-app-kit",
+
 "objc2-foundation",
 "radicle",
 "radicle-surf",
 "radicle-types",
@@ -4122,6 +4145,7 @@ dependencies = [
 "tauri-plugin-clipboard-manager",
 "tauri-plugin-dialog",
 "tauri-plugin-log",
+
 "tauri-plugin-os",
 "tauri-plugin-shell",
 "tauri-plugin-window-state",
 "thiserror 1.0.69",
@@ -5288,6 +5312,15 @@ dependencies = [
]

[[package]]
+
name = "sys-locale"
+
version = "0.3.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4"
+
dependencies = [
+
 "libc",
+
]
+

+
[[package]]
name = "system-deps"
version = "6.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -5584,6 +5617,24 @@ dependencies = [
]

[[package]]
+
name = "tauri-plugin-os"
+
version = "2.2.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "dda2d571a9baf0664c1f2088db227e3072f9028602fafa885deade7547c3b738"
+
dependencies = [
+
 "gethostname 0.5.0",
+
 "log",
+
 "os_info",
+
 "serde",
+
 "serde_json",
+
 "serialize-to-javascript",
+
 "sys-locale",
+
 "tauri",
+
 "tauri-plugin",
+
 "thiserror 2.0.7",
+
]
+

+
[[package]]
name = "tauri-plugin-shell"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -7288,7 +7339,7 @@ version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d91ffca73ee7f68ce055750bf9f6eca0780b8c85eff9bc046a3b0da41755e12"
dependencies = [
-
 "gethostname",
+
 "gethostname 0.4.3",
 "rustix",
 "x11rb-protocol",
]
modified crates/radicle-tauri/Cargo.toml
@@ -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
modified crates/radicle-tauri/capabilities/default.json
@@ -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",
@@ -17,6 +19,7 @@
    "clipboard-manager:default",
    "clipboard-manager:allow-write-text",
    "log:default",
-
    "dialog:default"
+
    "dialog:default",
+
    "os:default"
  ]
-
}
+
}

\ No newline at end of file
modified crates/radicle-tauri/src/commands/inbox.rs
@@ -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.");
+
    }
+
}
modified crates/radicle-tauri/src/lib.rs
@@ -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))]
@@ -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,
modified package-lock.json
@@ -14,6 +14,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"
      },
@@ -1312,6 +1313,15 @@
        "@tauri-apps/api": "^2.0.0"
      }
    },
+
    "node_modules/@tauri-apps/plugin-os": {
+
      "version": "2.2.0",
+
      "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-os/-/plugin-os-2.2.0.tgz",
+
      "integrity": "sha512-HszbCdbisMlu5QhCNAN8YIWyz2v33abAWha6+uvV2CKX8P5VSct/y+kEe22JeyqrxCnWlQ3DRx7s49Byg7/0EA==",
+
      "license": "MIT OR Apache-2.0",
+
      "dependencies": {
+
        "@tauri-apps/api": "^2.0.0"
+
      }
+
    },
    "node_modules/@tauri-apps/plugin-shell": {
      "version": "2.2.0",
      "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-shell/-/plugin-shell-2.2.0.tgz",
modified package.json
@@ -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"
  },
modified src/App.svelte
@@ -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";
@@ -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__) {
@@ -39,6 +45,7 @@

    try {
      await invoke("authenticate");
+
      authenticated.set(true);
      void router.loadFromLocation();
      void dynamicInterval(
        checkAuth,
@@ -46,6 +53,7 @@
      );
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
    } catch (e: any) {
+
      authenticated.set(false);
      void router.push({
        resource: "authenticationError",
        params: {
@@ -55,8 +63,39 @@
      });
      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;
+

+
      const count = await invoke<Record<string, NotificationCount>>(
+
        "count_notifications_by_repo",
+
      );
+
      const notificationCount = new SvelteMap(Object.entries(count));
+
      await invoke("set_badge", {
+
        count: sum(Array.from(notificationCount.values()).map(c => c.count)),
+
      });
+
    } finally {
+
      pollingNotificationsInProgress = false;
+
    }
+
  }
+

  onDestroy(() => {
    if (unlistenEvents) {
      unlistenEvents();
@@ -64,6 +103,9 @@
    if (unlistenNodeEvents) {
      unlistenNodeEvents();
    }
+
    if (notificationPoll) {
+
      clearInterval(notificationPoll);
+
    }
  });

  $effect(() => document.documentElement.setAttribute("data-theme", $theme));
modified src/lib/auth.ts
@@ -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);

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