From b0c67790151e93af84f63bbf064447bd3d1c0e7f Mon Sep 17 00:00:00 2001 From: Faisal Amir Date: Mon, 17 Feb 2025 12:08:08 +0700 Subject: [PATCH] feat: app updater with changelog (#4631) * feat: ui modal app updater with changelog * chore: update action when click update now * chore: update handler actions * chore: fix linter --- core/src/types/api/index.ts | 4 + electron/handlers/native.ts | 5 + electron/handlers/update.ts | 18 ++-- extensions/yarn.lock | 24 ++--- web/containers/Layout/index.tsx | 6 ++ .../ModalAppUpdaterChangelog/index.tsx | 91 +++++++++++++++++++ .../ModalAppUpdaterNotAvailable/index.tsx | 57 ++++++++++++ .../Providers/AppUpdateListener.tsx | 15 ++- web/helpers/atoms/App.atom.ts | 2 + web/hooks/useGetLatestRelease.ts | 33 +++++++ web/styles/components/marked.scss | 2 +- 11 files changed, 237 insertions(+), 20 deletions(-) create mode 100644 web/containers/ModalAppUpdaterChangelog/index.tsx create mode 100644 web/containers/ModalAppUpdaterNotAvailable/index.tsx create mode 100644 web/hooks/useGetLatestRelease.ts diff --git a/core/src/types/api/index.ts b/core/src/types/api/index.ts index 63b0eb10e..84f91457a 100644 --- a/core/src/types/api/index.ts +++ b/core/src/types/api/index.ts @@ -31,6 +31,8 @@ export enum NativeRoute { startServer = 'startServer', stopServer = 'stopServer', + + appUpdateDownload = 'appUpdateDownload', } /** @@ -50,6 +52,8 @@ export enum AppRoute { } export enum AppEvent { + onAppUpdateNotAvailable = 'onAppUpdateNotAvailable', + onAppUpdateAvailable = 'onAppUpdateAvailable', onAppUpdateDownloadUpdate = 'onAppUpdateDownloadUpdate', onAppUpdateDownloadError = 'onAppUpdateDownloadError', onAppUpdateDownloadSuccess = 'onAppUpdateDownloadSuccess', diff --git a/electron/handlers/native.ts b/electron/handlers/native.ts index 81a2fc7f5..7afeed285 100644 --- a/electron/handlers/native.ts +++ b/electron/handlers/native.ts @@ -1,4 +1,5 @@ import { app, ipcMain, dialog, shell, nativeTheme } from 'electron' +import { autoUpdater } from 'electron-updater' import { join } from 'path' import { windowManager } from '../managers/window' import { @@ -28,6 +29,10 @@ export function handleAppIPCs() { shell.openPath(getJanDataFolderPath()) }) + ipcMain.handle(NativeRoute.appUpdateDownload, async (_event) => { + autoUpdater.downloadUpdate() + }) + /** * Handles the "setNativeThemeLight" IPC message by setting the native theme source to "light". * This will change the appearance of the app to the light theme. diff --git a/electron/handlers/update.ts b/electron/handlers/update.ts index 5e2200e51..5517bb50c 100644 --- a/electron/handlers/update.ts +++ b/electron/handlers/update.ts @@ -16,15 +16,21 @@ export function handleAppUpdates() { if (!app.isPackaged) { return } + /* New Update Available */ autoUpdater.on('update-available', async (_info: UpdateInfo) => { - const action = await dialog.showMessageBox({ - title: 'Update Available', - message: 'Would you like to download and install it now?', - buttons: ['Download', 'Later'], - }) + windowManager.mainWindow?.webContents.send( + AppEvent.onAppUpdateAvailable, + {} + ) + }) - if (action.response === 0) await autoUpdater.downloadUpdate() + /* New Update Not Available */ + autoUpdater.on('update-not-available', async (_info: UpdateInfo) => { + windowManager.mainWindow?.webContents.send( + AppEvent.onAppUpdateNotAvailable, + {} + ) }) /* App Update Completion Message */ diff --git a/extensions/yarn.lock b/extensions/yarn.lock index 1aaa51bb5..36580b64d 100644 --- a/extensions/yarn.lock +++ b/extensions/yarn.lock @@ -509,61 +509,61 @@ __metadata: "@janhq/core@file:../../core/package.tgz::locator=%40janhq%2Fassistant-extension%40workspace%3Aassistant-extension": version: 0.1.10 - resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=5eb526&locator=%40janhq%2Fassistant-extension%40workspace%3Aassistant-extension" + resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=720675&locator=%40janhq%2Fassistant-extension%40workspace%3Aassistant-extension" dependencies: rxjs: "npm:^7.8.1" ulidx: "npm:^2.3.0" - checksum: 10c0/e53df943c345a1496d45d86e65bf40cf0fe0dd716ac1c1753453bad6877f36035a4fb305cb5e1690c18d426609ba125d1370304c7399fd4abac760e09fef2c52 + checksum: 10c0/6d870700c86244fafdb7b799232655fa2708b84103441e994a31ca3a0892866193e90771f09b41436400e251eca5891fd3278b13543fa0b90f3f1480199e0931 languageName: node linkType: hard "@janhq/core@file:../../core/package.tgz::locator=%40janhq%2Fconversational-extension%40workspace%3Aconversational-extension": version: 0.1.10 - resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=5eb526&locator=%40janhq%2Fconversational-extension%40workspace%3Aconversational-extension" + resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=720675&locator=%40janhq%2Fconversational-extension%40workspace%3Aconversational-extension" dependencies: rxjs: "npm:^7.8.1" ulidx: "npm:^2.3.0" - checksum: 10c0/e53df943c345a1496d45d86e65bf40cf0fe0dd716ac1c1753453bad6877f36035a4fb305cb5e1690c18d426609ba125d1370304c7399fd4abac760e09fef2c52 + checksum: 10c0/6d870700c86244fafdb7b799232655fa2708b84103441e994a31ca3a0892866193e90771f09b41436400e251eca5891fd3278b13543fa0b90f3f1480199e0931 languageName: node linkType: hard "@janhq/core@file:../../core/package.tgz::locator=%40janhq%2Fengine-management-extension%40workspace%3Aengine-management-extension": version: 0.1.10 - resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=5eb526&locator=%40janhq%2Fengine-management-extension%40workspace%3Aengine-management-extension" + resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=720675&locator=%40janhq%2Fengine-management-extension%40workspace%3Aengine-management-extension" dependencies: rxjs: "npm:^7.8.1" ulidx: "npm:^2.3.0" - checksum: 10c0/e53df943c345a1496d45d86e65bf40cf0fe0dd716ac1c1753453bad6877f36035a4fb305cb5e1690c18d426609ba125d1370304c7399fd4abac760e09fef2c52 + checksum: 10c0/6d870700c86244fafdb7b799232655fa2708b84103441e994a31ca3a0892866193e90771f09b41436400e251eca5891fd3278b13543fa0b90f3f1480199e0931 languageName: node linkType: hard "@janhq/core@file:../../core/package.tgz::locator=%40janhq%2Finference-cortex-extension%40workspace%3Ainference-cortex-extension": version: 0.1.10 - resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=5eb526&locator=%40janhq%2Finference-cortex-extension%40workspace%3Ainference-cortex-extension" + resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=720675&locator=%40janhq%2Finference-cortex-extension%40workspace%3Ainference-cortex-extension" dependencies: rxjs: "npm:^7.8.1" ulidx: "npm:^2.3.0" - checksum: 10c0/e53df943c345a1496d45d86e65bf40cf0fe0dd716ac1c1753453bad6877f36035a4fb305cb5e1690c18d426609ba125d1370304c7399fd4abac760e09fef2c52 + checksum: 10c0/6d870700c86244fafdb7b799232655fa2708b84103441e994a31ca3a0892866193e90771f09b41436400e251eca5891fd3278b13543fa0b90f3f1480199e0931 languageName: node linkType: hard "@janhq/core@file:../../core/package.tgz::locator=%40janhq%2Fmodel-extension%40workspace%3Amodel-extension": version: 0.1.10 - resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=5eb526&locator=%40janhq%2Fmodel-extension%40workspace%3Amodel-extension" + resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=720675&locator=%40janhq%2Fmodel-extension%40workspace%3Amodel-extension" dependencies: rxjs: "npm:^7.8.1" ulidx: "npm:^2.3.0" - checksum: 10c0/e53df943c345a1496d45d86e65bf40cf0fe0dd716ac1c1753453bad6877f36035a4fb305cb5e1690c18d426609ba125d1370304c7399fd4abac760e09fef2c52 + checksum: 10c0/6d870700c86244fafdb7b799232655fa2708b84103441e994a31ca3a0892866193e90771f09b41436400e251eca5891fd3278b13543fa0b90f3f1480199e0931 languageName: node linkType: hard "@janhq/core@file:../../core/package.tgz::locator=%40janhq%2Fmonitoring-extension%40workspace%3Amonitoring-extension": version: 0.1.10 - resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=5eb526&locator=%40janhq%2Fmonitoring-extension%40workspace%3Amonitoring-extension" + resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=720675&locator=%40janhq%2Fmonitoring-extension%40workspace%3Amonitoring-extension" dependencies: rxjs: "npm:^7.8.1" ulidx: "npm:^2.3.0" - checksum: 10c0/e53df943c345a1496d45d86e65bf40cf0fe0dd716ac1c1753453bad6877f36035a4fb305cb5e1690c18d426609ba125d1370304c7399fd4abac760e09fef2c52 + checksum: 10c0/6d870700c86244fafdb7b799232655fa2708b84103441e994a31ca3a0892866193e90771f09b41436400e251eca5891fd3278b13543fa0b90f3f1480199e0931 languageName: node linkType: hard diff --git a/web/containers/Layout/index.tsx b/web/containers/Layout/index.tsx index 5b17eb4fc..18c0edcab 100644 --- a/web/containers/Layout/index.tsx +++ b/web/containers/Layout/index.tsx @@ -29,6 +29,10 @@ import LoadingModal from '../LoadingModal' import MainViewContainer from '../MainViewContainer' +import ModalAppUpdaterChangelog from '../ModalAppUpdaterChangelog' + +import ModalAppUpdaterNotAvailable from '../ModalAppUpdaterNotAvailable' + import InstallingExtensionModal from './BottomPanel/InstallingExtension/InstallingExtensionModal' import { mainViewStateAtom } from '@/helpers/atoms/App.atom' @@ -222,6 +226,8 @@ const BaseLayout = () => { )} + + ) } diff --git a/web/containers/ModalAppUpdaterChangelog/index.tsx b/web/containers/ModalAppUpdaterChangelog/index.tsx new file mode 100644 index 000000000..705623a90 --- /dev/null +++ b/web/containers/ModalAppUpdaterChangelog/index.tsx @@ -0,0 +1,91 @@ +import React, { useEffect, useState } from 'react' + +import { Button, Modal } from '@janhq/joi' + +import { useAtom } from 'jotai' + +import { useGetLatestRelease } from '@/hooks/useGetLatestRelease' + +import { MarkdownTextMessage } from '@/screens/Thread/ThreadCenterPanel/TextMessage/MarkdownTextMessage' + +import LogoMark from '../Brand/Logo/Mark' + +import { appUpdateAvailableAtom } from '@/helpers/atoms/App.atom' + +const ModalAppUpdaterChangelog = () => { + const [appUpdateAvailable, setAppUpdateAvailable] = useAtom( + appUpdateAvailableAtom + ) + + const [open, setOpen] = useState(appUpdateAvailable) + + useEffect(() => { + setOpen(appUpdateAvailable) + }, [appUpdateAvailable]) + + const beta = VERSION.includes('beta') + const nightly = VERSION.includes('-') + + const { release } = useGetLatestRelease(beta ? true : false) + + return ( + +
+ +
App Update
+
+ {!nightly && ( +

+ Version {release?.name} is available and ready to install. +

+ )} + + } + open={open} + onOpenChange={() => setOpen(!open)} + content={ +
+ {nightly ? ( +

+ You are using a nightly build. This version is built from the + latest development branch and may not have release notes. +

+ ) : ( + <> +
+ +
+ + )} +
+ + +
+
+ } + /> + ) +} + +export default ModalAppUpdaterChangelog diff --git a/web/containers/ModalAppUpdaterNotAvailable/index.tsx b/web/containers/ModalAppUpdaterNotAvailable/index.tsx new file mode 100644 index 000000000..5f2b25fda --- /dev/null +++ b/web/containers/ModalAppUpdaterNotAvailable/index.tsx @@ -0,0 +1,57 @@ +import React, { useEffect, useState } from 'react' + +import { Button, Modal } from '@janhq/joi' + +import { useAtom } from 'jotai' + +import LogoMark from '../Brand/Logo/Mark' + +import { appUpdateNotAvailableAtom } from '@/helpers/atoms/App.atom' + +const ModalAppUpdaterNotAvailable = () => { + const [appUpdateNotAvailable, setAppUpdateNotAvailable] = useAtom( + appUpdateNotAvailableAtom + ) + + const [open, setOpen] = useState(appUpdateNotAvailable) + + useEffect(() => { + setOpen(appUpdateNotAvailable) + }, [appUpdateNotAvailable]) + + return ( + +
+ +
App Update
+
+ + } + open={open} + onOpenChange={() => setOpen(!open)} + content={ +
+

+ You’re up to date! No new updates available +

+
+ +
+
+ } + /> + ) +} + +export default ModalAppUpdaterNotAvailable diff --git a/web/containers/Providers/AppUpdateListener.tsx b/web/containers/Providers/AppUpdateListener.tsx index 4d05f6010..39b78aac7 100644 --- a/web/containers/Providers/AppUpdateListener.tsx +++ b/web/containers/Providers/AppUpdateListener.tsx @@ -5,12 +5,16 @@ import { useSetAtom } from 'jotai' import { appDownloadProgressAtom, + appUpdateAvailableAtom, updateVersionErrorAtom, + appUpdateNotAvailableAtom, } from '@/helpers/atoms/App.atom' const AppUpdateListener = () => { const setProgress = useSetAtom(appDownloadProgressAtom) const setUpdateVersionError = useSetAtom(updateVersionErrorAtom) + const setAppUpdateAvailable = useSetAtom(appUpdateAvailableAtom) + const setAppUpdateNotvailable = useSetAtom(appUpdateNotAvailableAtom) useEffect(() => { if (window && window.electronAPI) { @@ -36,8 +40,17 @@ const AppUpdateListener = () => { window.electronAPI.onAppUpdateDownloadSuccess(() => { setProgress(-1) }) + + window.electronAPI.onAppUpdateAvailable(() => { + setAppUpdateAvailable(true) + }) + + window.electronAPI.onAppUpdateNotAvailable(() => { + setAppUpdateAvailable(false) + setAppUpdateNotvailable(true) + }) } - }, [setProgress, setUpdateVersionError]) + }, [setProgress, setUpdateVersionError, setAppUpdateAvailable]) return } diff --git a/web/helpers/atoms/App.atom.ts b/web/helpers/atoms/App.atom.ts index 3d4d37534..fed0573c3 100644 --- a/web/helpers/atoms/App.atom.ts +++ b/web/helpers/atoms/App.atom.ts @@ -23,6 +23,8 @@ export const showRightPanelAtom = atomWithStorage( export const showSystemMonitorPanelAtom = atom(false) export const appDownloadProgressAtom = atom(-1) export const updateVersionErrorAtom = atom(undefined) +export const appUpdateAvailableAtom = atom(false) +export const appUpdateNotAvailableAtom = atom(false) const COPY_OVER_INSTRUCTION_ENABLED = 'copy_over_instruction_enabled' diff --git a/web/hooks/useGetLatestRelease.ts b/web/hooks/useGetLatestRelease.ts new file mode 100644 index 000000000..3b76c2127 --- /dev/null +++ b/web/hooks/useGetLatestRelease.ts @@ -0,0 +1,33 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import useSWR from 'swr' + +const fetchLatestRelease = async (includeBeta: boolean) => { + const res = await fetch('https://api.github.com/repos/janhq/jan/releases') + if (!res.ok) throw new Error('Failed to fetch releases') + + const releases = await res.json() + + // Filter stable and beta releases + const stableRelease = releases.find( + (release: { prerelease: any; draft: any }) => + !release.prerelease && !release.draft + ) + const betaRelease = releases.find( + (release: { prerelease: any }) => release.prerelease + ) + + return includeBeta ? (betaRelease ?? stableRelease) : stableRelease +} + +export function useGetLatestRelease(includeBeta = false) { + const { data, error, mutate } = useSWR( + ['latestRelease', includeBeta], + () => fetchLatestRelease(includeBeta), + { + revalidateOnFocus: false, + revalidateOnReconnect: true, + } + ) + + return { release: data, error, mutate } +} diff --git a/web/styles/components/marked.scss b/web/styles/components/marked.scss index 38369ff58..753f95df6 100644 --- a/web/styles/components/marked.scss +++ b/web/styles/components/marked.scss @@ -7,7 +7,7 @@ .markdown-content h6 { margin-top: 1rem; margin-bottom: 0.5rem; - font-weight: medium; + font-weight: 500; line-height: 1.2; color: hsla(var(--text-primary)); }