Radish alpha
r
rad:z4D5UCArafTzTQpDZNQRuqswh3ury
Radicle desktop app
Radicle
Git
Add Check for Update into Settings popover
Sebastian Martinez committed 10 months ago
commit 641b809d562725b0236de7ec4084901092316cb3
parent 1758695
7 files changed +227 -24
modified package-lock.json
@@ -18,6 +18,7 @@
        "@tauri-apps/plugin-window-state": "^2.2.2",
        "overlayscrollbars": "^2.11.4",
        "overlayscrollbars-svelte": "^0.5.5",
+
        "semver": "^7.7.2",
        "zod": "^3.24.4"
      },
      "devDependencies": {
@@ -31,6 +32,7 @@
        "@types/lodash": "^4.17.16",
        "@types/md5": "^2.3.5",
        "@types/node": "^22.15.17",
+
        "@types/semver": "^7.7.0",
        "@types/wait-on": "^5.3.4",
        "@wooorm/starry-night": "^3.7.0",
        "baconjs": "^3.0.23",
@@ -1487,6 +1489,13 @@
        "undici-types": "~6.21.0"
      }
    },
+
    "node_modules/@types/semver": {
+
      "version": "7.7.0",
+
      "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.0.tgz",
+
      "integrity": "sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==",
+
      "dev": true,
+
      "license": "MIT"
+
    },
    "node_modules/@types/trusted-types": {
      "version": "2.0.7",
      "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
@@ -4714,10 +4723,10 @@
      }
    },
    "node_modules/semver": {
-
      "version": "7.7.1",
-
      "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
-
      "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
-
      "dev": true,
+
      "version": "7.7.2",
+
      "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
+
      "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
+
      "license": "ISC",
      "bin": {
        "semver": "bin/semver.js"
      },
modified package.json
@@ -33,6 +33,7 @@
    "@tauri-apps/plugin-window-state": "^2.2.2",
    "overlayscrollbars": "^2.11.4",
    "overlayscrollbars-svelte": "^0.5.5",
+
    "semver": "^7.7.2",
    "zod": "^3.24.4"
  },
  "devDependencies": {
@@ -46,6 +47,7 @@
    "@types/lodash": "^4.17.16",
    "@types/md5": "^2.3.5",
    "@types/node": "^22.15.17",
+
    "@types/semver": "^7.7.0",
    "@types/wait-on": "^5.3.4",
    "@wooorm/starry-night": "^3.7.0",
    "baconjs": "^3.0.23",
modified src/components/GuideButton.svelte
@@ -6,6 +6,7 @@
  import type { Config } from "@bindings/config/Config";

  import { activeRouteStore, push } from "@app/lib/router";
+
  import { settingsPopoverToggleId } from "./Settings.svelte";
  import { addRepoPopoverToggleId } from "./AddRepoButton.svelte";
  import { didFromPublicKey, truncateDid } from "@app/lib/utils";
  import { nodeRunning } from "@app/lib/events";
@@ -97,6 +98,22 @@
          </CopyableId>
          you can share this with anyone to find you on the network.
        </div>
+
        <div class="txt-small" style:margin-top="1rem">
+
          We release a new version of the app every two weeks. To stay up to
+
          date, go to
+
          <button
+
            class="txt-small"
+
            onclick={async () => {
+
              const settingsButton = document.getElementById(
+
                settingsPopoverToggleId,
+
              );
+
              await sleep(1);
+
              settingsButton?.click();
+
            }}>
+
            Settings
+
          </button>
+
          and enable 'Check for updates' to receive notifications about new releases.
+
        </div>
        <div class="spacer"></div>
        {#if radicleInstalled() || $nodeRunning}
          <div class="global-flex txt-small">
modified src/components/OutlineButton.svelte
@@ -2,6 +2,7 @@
  import type { Snippet } from "svelte";

  interface Props {
+
    id?: string;
    popoverToggle?: string;
    active?: boolean;
    children: Snippet;
@@ -13,6 +14,7 @@
  }

  const {
+
    id,
    popoverToggle,
    active = false,
    children,
@@ -283,6 +285,7 @@

<!-- svelte-ignore a11y_click_events_have_key_events -->
<div
+
  {id}
  data-popover-toggle={popoverToggle}
  class:active
  class:disabled
modified src/components/Settings.svelte
@@ -1,17 +1,23 @@
+
<script lang="ts" module>
+
  export const settingsPopoverToggleId = "settings-popover-toggle";
+
</script>
+

<script lang="ts">
  import type { ComponentProps } from "svelte";

-
  import { onMount } from "svelte";
-
  import { invoke } from "@app/lib/invoke";
+
  import { updateChecker } from "@app/lib/updateChecker.svelte";

  import AnnounceSwitch from "./AnnounceSwitch.svelte";
  import Border from "./Border.svelte";
  import CopyableId from "./CopyableId.svelte";
+
  import ExternalLink from "./ExternalLink.svelte";
  import FontSizeSwitch from "./FontSizeSwitch.svelte";
  import Icon from "./Icon.svelte";
  import NakedButton from "./NakedButton.svelte";
+
  import OutlineButton from "./OutlineButton.svelte";
  import Popover from "./Popover.svelte";
  import ThemeSwitch from "./ThemeSwitch.svelte";
+
  import UpdateSwitch from "./UpdateSwitch.svelte";

  interface Props {
    compact?: boolean;
@@ -19,12 +25,6 @@
    popoverProps: Partial<ComponentProps<typeof Popover>>;
  }

-
  let version = $state("");
-

-
  onMount(async () => {
-
    version = await invoke<string>("version");
-
  });
-

  const {
    compact = true,
    styleHeight = "2.5rem",
@@ -36,17 +36,33 @@

<Popover {...popoverProps} bind:expanded={popoverExpanded}>
  {#snippet toggle(onclick)}
-
    <NakedButton
-
      title="Settings"
-
      variant="ghost"
-
      {onclick}
-
      {styleHeight}
-
      active={popoverExpanded}>
-
      <Icon name="settings" />
-
      {#if !compact}
-
        Settings
-
      {/if}
-
    </NakedButton>
+
    {#if updateChecker.newVersion}
+
      <OutlineButton
+
        {styleHeight}
+
        id={settingsPopoverToggleId}
+
        title="Settings"
+
        {onclick}
+
        variant="secondary"
+
        active={popoverExpanded}>
+
        <Icon name="settings" />
+
        {#if !compact}
+
          Settings
+
        {/if}
+
      </OutlineButton>
+
    {:else}
+
      <NakedButton
+
        id={settingsPopoverToggleId}
+
        title="Settings"
+
        variant="ghost"
+
        {onclick}
+
        {styleHeight}
+
        active={popoverExpanded}>
+
        <Icon name="settings" />
+
        {#if !compact}
+
          Settings
+
        {/if}
+
      </NakedButton>
+
    {/if}
  {/snippet}
  {#snippet popover()}
    <Border variant="ghost" stylePadding="0.5rem 1rem" styleWidth="27rem">
@@ -61,13 +77,33 @@
          style:justify-content="space-between"
          style:width="100%"
          style:min-height="2rem">
-
          Version <CopyableId id={version} />
+
          Version
+

+
          <div class="global-flex">
+
            {#if updateChecker.currentVersion}
+
              <CopyableId id={updateChecker.currentVersion} />
+
            {/if}
+
            {#if updateChecker.newVersion}
+
              -> <ExternalLink href="https://radicle.xyz/desktop">
+
                Update to {updateChecker.newVersion}
+
              </ExternalLink>
+
            {/if}
+
          </div>
        </div>

        <div
          class="global-flex"
          style:justify-content="space-between"
          style:width="100%">
+
          Check for updates <UpdateSwitch
+
            active={updateChecker.isEnabled}
+
            disable={updateChecker.disable}
+
            enable={updateChecker.enable} />
+
        </div>
+
        <div
+
          class="global-flex"
+
          style:justify-content="space-between"
+
          style:width="100%">
          Theme <ThemeSwitch />
        </div>
        <div
added src/components/UpdateSwitch.svelte
@@ -0,0 +1,28 @@
+
<script lang="ts">
+
  import Button from "./Button.svelte";
+

+
  interface Props {
+
    active: boolean;
+
    enable: () => void;
+
    disable: () => void;
+
  }
+

+
  const { active, enable, disable }: Props = $props();
+
</script>
+

+
<style>
+
  .container {
+
    display: flex;
+
    align-items: center;
+
  }
+
</style>
+

+
<div class="container">
+
  <Button flatRight active={!active} variant="ghost" onclick={disable}>
+
    Disable
+
  </Button>
+

+
  <Button flatLeft variant="ghost" active={Boolean(active)} onclick={enable}>
+
    Enable
+
  </Button>
+
</div>
added src/lib/updateChecker.svelte.ts
@@ -0,0 +1,108 @@
+
import * as z from "zod";
+
import * as semver from "semver";
+

+
import useLocalStorage from "./useLocalStorage.svelte";
+
import { invoke } from "./invoke";
+

+
interface LatestVersionInfo {
+
  version: string;
+
}
+

+
const fetchLatestVersion = async (): Promise<LatestVersionInfo> => {
+
  const response = await fetch(
+
    "https://minio-api.radworks.garden/radworks-releases/radicle-desktop/latest/latest.json",
+
  );
+
  const body: LatestVersionInfo = await response.json();
+
  return body;
+
};
+

+
// Check for new version every hour.
+
const VERSION_CHECK_INTERVAL = 3600 * 1000;
+

+
const isEnabledStore = useLocalStorage(
+
  "updateChecker.isEnabled",
+
  z.boolean().nullable(),
+
  null,
+
  !window.localStorage,
+
);
+

+
class UpdateChecker {
+
  private checkInterval: number | undefined = $state();
+
  private latestVersionInfo: LatestVersionInfo | undefined = $state();
+
  public currentVersion: string | undefined = $state();
+

+
  public isEnabled = $derived.by(() => {
+
    if (isEnabledStore.value === null) {
+
      return false;
+
    } else {
+
      return isEnabledStore.value;
+
    }
+
  });
+

+
  // A state that holds the `LatestVersionInfo` if this feature has
+
  // been enabled and if there is a newer version available.
+
  public newVersion = $derived.by(() => {
+
    if (this.latestVersionInfo && this.currentVersion) {
+
      if (semver.gt(this.latestVersionInfo.version, this.currentVersion)) {
+
        return this.latestVersionInfo.version;
+
      } else {
+
        return undefined;
+
      }
+
    } else {
+
      return undefined;
+
    }
+
  });
+

+
  public static init(): UpdateChecker {
+
    const updateChecker = new UpdateChecker();
+
    if (isEnabledStore.value) {
+
      updateChecker.enable();
+
    }
+

+
    void invoke<string>("version").then(currentVersion => {
+
      const version = semver.coerce(currentVersion);
+
      if (version) {
+
        updateChecker.currentVersion = version.toString();
+
      }
+
    });
+

+
    return updateChecker;
+
  }
+

+
  // Disable background update checking.
+
  public disable = (): void => {
+
    isEnabledStore.value = false;
+

+
    if (this.checkInterval) {
+
      clearInterval(this.checkInterval);
+
      this.checkInterval = undefined;
+
    }
+
  };
+

+
  private async checkNewVersion(): Promise<boolean> {
+
    try {
+
      this.latestVersionInfo = await fetchLatestVersion();
+
    } catch {
+
      return false;
+
    }
+

+
    return (
+
      this.currentVersion !== undefined &&
+
      semver.gt(this.latestVersionInfo.version, this.currentVersion)
+
    );
+
  }
+

+
  // Enable background udpate checking.
+
  public enable = (): void => {
+
    isEnabledStore.value = true;
+

+
    void this.checkNewVersion();
+
    if (!this.checkInterval) {
+
      this.checkInterval = window.setInterval(() => {
+
        void this.checkNewVersion();
+
      }, VERSION_CHECK_INTERVAL);
+
    }
+
  };
+
}
+

+
export const updateChecker = UpdateChecker.init();