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
This commit is contained in:
Faisal Amir 2025-02-17 12:08:08 +07:00 committed by GitHub
parent 476c7f723f
commit b0c6779015
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 237 additions and 20 deletions

View File

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

View File

@ -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.

View File

@ -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 */

View File

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

View File

@ -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 = () => {
)}
</div>
<BottomPanel />
<ModalAppUpdaterChangelog />
<ModalAppUpdaterNotAvailable />
</div>
)
}

View File

@ -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 (
<Modal
hideClose={true}
title={
<>
<div className="flex items-center gap-x-2">
<LogoMark width={40} height={40} />
<h6>App Update</h6>
</div>
{!nightly && (
<p className="mt-2 text-sm font-normal">
Version <b>{release?.name}</b> is available and ready to install.
</p>
)}
</>
}
open={open}
onOpenChange={() => setOpen(!open)}
content={
<div className="mt-3">
{nightly ? (
<p className="mt-2 text-sm font-normal">
You are using a nightly build. This version is built from the
latest development branch and may not have release notes.
</p>
) : (
<>
<div className="markdown-content max-h-[400px] overflow-y-auto rounded-lg border border-[hsla(var(--app-border))] px-2 pb-4 pt-0">
<MarkdownTextMessage text={release?.body} />
</div>
</>
)}
<div className="mt-4 flex items-center justify-end gap-x-2">
<Button
theme="ghost"
variant="outline"
onClick={() => {
setOpen(false)
setAppUpdateAvailable(false)
}}
>
Later
</Button>
<Button
autoFocus
onClick={() => {
window.core?.api?.appUpdateDownload()
setOpen(false)
setAppUpdateAvailable(false)
}}
>
Update Now
</Button>
</div>
</div>
}
/>
)
}
export default ModalAppUpdaterChangelog

View File

@ -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 (
<Modal
hideClose={true}
title={
<>
<div className="flex items-center gap-x-2">
<LogoMark width={40} height={40} />
<h6>App Update</h6>
</div>
</>
}
open={open}
onOpenChange={() => setOpen(!open)}
content={
<div className="mt-3">
<p className="mt-2 text-sm font-normal">
Youre up to date! No new updates available
</p>
<div className="mt-4 flex items-center justify-end gap-x-2">
<Button
autoFocus
onClick={() => {
setOpen(false)
setAppUpdateNotAvailable(false)
}}
>
Check back later
</Button>
</div>
</div>
}
/>
)
}
export default ModalAppUpdaterNotAvailable

View File

@ -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 <Fragment></Fragment>
}

View File

@ -23,6 +23,8 @@ export const showRightPanelAtom = atomWithStorage<boolean>(
export const showSystemMonitorPanelAtom = atom<boolean>(false)
export const appDownloadProgressAtom = atom<number>(-1)
export const updateVersionErrorAtom = atom<string | undefined>(undefined)
export const appUpdateAvailableAtom = atom<boolean>(false)
export const appUpdateNotAvailableAtom = atom<boolean>(false)
const COPY_OVER_INSTRUCTION_ENABLED = 'copy_over_instruction_enabled'

View File

@ -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 }
}

View File

@ -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));
}