diff --git a/assets/images/tray-active-update.png b/assets/images/tray-active-update.png new file mode 100644 index 000000000..bfe8cf141 Binary files /dev/null and b/assets/images/tray-active-update.png differ diff --git a/assets/images/tray-active-update@2x.png b/assets/images/tray-active-update@2x.png new file mode 100644 index 000000000..97329b1c7 Binary files /dev/null and b/assets/images/tray-active-update@2x.png differ diff --git a/assets/images/tray-idle-update.png b/assets/images/tray-idle-update.png new file mode 100644 index 000000000..cc7a382f8 Binary files /dev/null and b/assets/images/tray-idle-update.png differ diff --git a/assets/images/tray-idle-update@2x.png b/assets/images/tray-idle-update@2x.png new file mode 100644 index 000000000..92d2bb29f Binary files /dev/null and b/assets/images/tray-idle-update@2x.png differ diff --git a/package.json b/package.json index 646134de3..612d23a9c 100644 --- a/package.json +++ b/package.json @@ -117,7 +117,8 @@ "react-router-dom": "6.26.0", "tailwind-merge": "2.4.0", "ts-loader": "9.5.1", - "typescript": "5.5.4" + "typescript": "5.5.4", + "update-electron-app": "3.0.0" }, "devDependencies": { "@biomejs/biome": "1.8.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c49c8e16b..41a7ae218 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -62,6 +62,9 @@ importers: typescript: specifier: 5.5.4 version: 5.5.4 + update-electron-app: + specifier: 3.0.0 + version: 3.0.0 devDependencies: '@biomejs/biome': specifier: 1.8.3 @@ -1649,6 +1652,9 @@ packages: resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} engines: {node: '>=10'} + github-url-to-object@4.0.6: + resolution: {integrity: sha512-NaqbYHMUAlPcmWFdrAB7bcxrNIiiJWJe8s/2+iOc9vlcHlwHqSGrPk+Yi3nu6ebTwgsZEa7igz+NH2vEq3gYwQ==} + glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -1851,6 +1857,9 @@ packages: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} + is-url@1.2.4: + resolution: {integrity: sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==} + isarray@1.0.0: resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} @@ -3067,6 +3076,9 @@ packages: peerDependencies: browserslist: '>= 4.21.0' + update-electron-app@3.0.0: + resolution: {integrity: sha512-Ccs46fgUEcMpSRPMNw82DFMux2MGi5tkKkEpV723JmtPNI3qAtxvTeiYkKczN2/LehA3U7JGrGr4MhraxGdRTw==} + uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} @@ -5039,6 +5051,10 @@ snapshots: get-stream@6.0.1: {} + github-url-to-object@4.0.6: + dependencies: + is-url: 1.2.4 + glob-parent@5.1.2: dependencies: is-glob: 4.0.3 @@ -5246,6 +5262,8 @@ snapshots: is-stream@2.0.1: {} + is-url@1.2.4: {} + isarray@1.0.0: {} isbinaryfile@4.0.10: {} @@ -6606,6 +6624,12 @@ snapshots: escalade: 3.1.2 picocolors: 1.0.1 + update-electron-app@3.0.0: + dependencies: + github-url-to-object: 4.0.6 + is-url: 1.2.4 + ms: 2.1.2 + uri-js@4.4.1: dependencies: punycode: 2.3.1 diff --git a/src/components/settings/SettingsFooter.test.tsx b/src/components/settings/SettingsFooter.test.tsx index 5e0b23c0b..365593422 100644 --- a/src/components/settings/SettingsFooter.test.tsx +++ b/src/components/settings/SettingsFooter.test.tsx @@ -76,48 +76,6 @@ describe('routes/components/settings/SettingsFooter.tsx', () => { }); }); - describe('update available visual indicator', () => { - it('using latest version', async () => { - await act(async () => { - render( - - - - - , - ); - }); - - expect( - screen.getByTitle('You are using the latest version'), - ).toMatchSnapshot(); - }); - - it('new version available', async () => { - await act(async () => { - render( - - - - - , - ); - }); - - expect(screen.getByTitle('New version available')).toMatchSnapshot(); - }); - }); - it('should open release notes', async () => { process.env = { ...originalEnv, diff --git a/src/components/settings/SettingsFooter.tsx b/src/components/settings/SettingsFooter.tsx index e53ca02ec..a5fd609a4 100644 --- a/src/components/settings/SettingsFooter.tsx +++ b/src/components/settings/SettingsFooter.tsx @@ -1,23 +1,12 @@ -import { - AlertFillIcon, - CheckCircleFillIcon, - PersonIcon, - XCircleIcon, -} from '@primer/octicons-react'; +import { PersonIcon, XCircleIcon } from '@primer/octicons-react'; import { type FC, useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { BUTTON_CLASS_NAME } from '../../styles/gitify'; -import { IconColor, Size } from '../../types'; +import { Size } from '../../types'; import { getAppVersion, quitApp } from '../../utils/comms'; import { openGitifyReleaseNotes } from '../../utils/links'; -interface ISettingsFooter { - isUpdateAvailable?: boolean; -} - -export const SettingsFooter: FC = ({ - isUpdateAvailable = false, -}: ISettingsFooter) => { +export const SettingsFooter: FC = () => { const [appVersion, setAppVersion] = useState(null); const navigate = useNavigate(); @@ -42,23 +31,6 @@ export const SettingsFooter: FC = ({ >
Gitify {appVersion} - - {isUpdateAvailable ? ( - - - - ) : ( - - - - )} -
diff --git a/src/components/settings/__snapshots__/SettingsFooter.test.tsx.snap b/src/components/settings/__snapshots__/SettingsFooter.test.tsx.snap index c06b29078..eaef3dec9 100644 --- a/src/components/settings/__snapshots__/SettingsFooter.test.tsx.snap +++ b/src/components/settings/__snapshots__/SettingsFooter.test.tsx.snap @@ -17,45 +17,3 @@ exports[`routes/components/settings/SettingsFooter.tsx app version should show p v0.0.1 `; - -exports[`routes/components/settings/SettingsFooter.tsx update available visual indicator new version available 1`] = ` - - - -`; - -exports[`routes/components/settings/SettingsFooter.tsx update available visual indicator using latest version 1`] = ` - - - -`; diff --git a/src/electron/main.js b/src/electron/main.js index b8d6f853f..dd36b70c9 100644 --- a/src/electron/main.js +++ b/src/electron/main.js @@ -5,27 +5,35 @@ const { globalShortcut, Menu, dialog, + MenuItem, } = require('electron/main'); const { menubar } = require('menubar'); -const { autoUpdater } = require('electron-updater'); const { onFirstRunMaybe } = require('./first-run'); const path = require('node:path'); const log = require('electron-log'); const fs = require('node:fs'); const os = require('node:os'); +const { autoUpdater } = require('electron-updater'); +const { updateElectronApp } = require('update-electron-app'); log.initialize(); -autoUpdater.logger = log; // TODO: Remove @electron/remote use - see #650 require('@electron/remote/main').initialize(); +// Tray Icons const idleIcon = path.resolve( `${__dirname}/../../assets/images/tray-idleTemplate.png`, ); +const idleUpdateAvailableIcon = path.resolve( + `${__dirname}/../../assets/images/tray-idle-update.png`, +); const activeIcon = path.resolve( `${__dirname}/../../assets/images/tray-active.png`, ); +const activeUpdateAvailableIcon = path.resolve( + `${__dirname}/../../assets/images/tray-active-update.png`, +); const browserWindowOpts = { width: 500, @@ -40,29 +48,32 @@ const browserWindowOpts = { }, }; -let isUpdateAvailable = false; -let isUpdateDownloaded = false; - -const contextMenu = Menu.buildFromTemplate([ - { - label: 'Check for updates', - visible: !isUpdateAvailable, - click: () => { - checkForUpdates(); - }, - }, - { - label: 'An update is available', - enabled: false, - visible: isUpdateAvailable, +const checkForUpdatesMenuItem = new MenuItem({ + label: 'Check for updates', + enabled: true, + click: () => { + autoUpdater.checkForUpdatesAndNotify(); }, - { - label: 'Restart to update', - visible: isUpdateDownloaded, - click: () => { - autoUpdater.quitAndInstall(); - }, +}); + +const updateAvailableMenuItem = new MenuItem({ + label: 'An update is available', + enabled: false, + visible: false, +}); + +const updateReadyForInstallMenuItem = new MenuItem({ + label: 'Restart to update', + visible: false, + click: () => { + autoUpdater.quitAndInstall(); }, +}); + +const contextMenu = Menu.buildFromTemplate([ + checkForUpdatesMenuItem, + updateAvailableMenuItem, + updateReadyForInstallMenuItem, { type: 'separator' }, { label: 'Developer', @@ -142,27 +153,6 @@ app.whenReady().then(async () => { mb.positioner.move('trayCenter', trayBounds); mb.window.resizable = false; }); - - // Auto Updater - checkForUpdates(); - setInterval(checkForUpdates, 24 * 60 * 60 * 1000); // 24 hours - - autoUpdater.on('update-available', () => { - log.info('Auto Updater: New update available'); - isUpdateAvailable = true; - mb.window.webContents.send('gitify:auto-updater', isUpdateAvailable); - }); - - autoUpdater.on('update-not-available', () => { - log.info('Auto Updater: Already on the latest version'); - isUpdateAvailable = false; - mb.window.webContents.send('gitify:auto-updater', isUpdateAvailable); - }); - - autoUpdater.on('update-downloaded', () => { - log.info('Auto Updater: Update downloaded'); - isUpdateDownloaded = true; - }); }); nativeTheme.on('updated', () => { @@ -186,19 +176,25 @@ app.whenReady().then(async () => { ipc.on('gitify:icon-active', () => { if (!mb.tray.isDestroyed()) { - mb.tray.setImage(activeIcon); + mb.tray.setImage( + updateAvailableMenuItem.visible + ? activeUpdateAvailableIcon + : activeIcon, + ); } }); ipc.on('gitify:icon-idle', () => { if (!mb.tray.isDestroyed()) { - mb.tray.setImage(idleIcon); + mb.tray.setImage( + updateAvailableMenuItem.visible ? idleUpdateAvailableIcon : idleIcon, + ); } }); ipc.on('gitify:update-title', (_, title) => { if (!mb.tray.isDestroyed()) { - mb.tray.setTitle(`${isUpdateAvailable ? '⤓' : ''}${title}`); + mb.tray.setTitle(title); } }); @@ -223,12 +219,40 @@ app.whenReady().then(async () => { ipc.on('gitify:update-auto-launch', (_, settings) => { app.setLoginItemSettings(settings); }); -}); -function checkForUpdates() { - log.info('Auto Updater: Checking for updates...'); - autoUpdater.checkForUpdatesAndNotify(); -} + // Auto Updater + updateElectronApp({ + updateInterval: '24 hours', + logger: log, + }); + + autoUpdater.on('checking-for-update', () => { + log.info('Auto Updater: Checking for update'); + checkForUpdatesMenuItem.enabled = false; + }); + + autoUpdater.on('error', (error) => { + log.error('Auto Updater: error checking for update', error); + checkForUpdatesMenuItem.enabled = true; + }); + + autoUpdater.on('update-available', () => { + log.info('Auto Updater: New update available'); + updateAvailableMenuItem.visible = true; + mb.tray.setToolTip('Gitify\nA new update is available'); + }); + + autoUpdater.on('update-downloaded', () => { + log.info('Auto Updater: Update downloaded'); + updateReadyForInstallMenuItem.visible = true; + mb.tray.setToolTip('Gitify\nA new update is ready to install'); + }); + + autoUpdater.on('update-not-available', () => { + log.info('Auto Updater: update not available'); + checkForUpdatesMenuItem.enabled = true; + }); +}); function takeScreenshot() { const date = new Date(); diff --git a/src/routes/Settings.tsx b/src/routes/Settings.tsx index 3d07f80ef..2ee168061 100644 --- a/src/routes/Settings.tsx +++ b/src/routes/Settings.tsx @@ -1,6 +1,5 @@ import { GearIcon } from '@primer/octicons-react'; -import { ipcRenderer } from 'electron'; -import { type FC, useContext, useEffect, useState } from 'react'; +import { type FC, useContext } from 'react'; import { Header } from '../components/Header'; import { AppearanceSettings } from '../components/settings/AppearanceSettings'; import { NotificationSettings } from '../components/settings/NotificationSettings'; @@ -10,13 +9,6 @@ import { AppContext } from '../context/App'; export const SettingsRoute: FC = () => { const { resetSettings } = useContext(AppContext); - const [isUpdateAvailable, setIsUpdateAvailable] = useState(false); - - useEffect(() => { - ipcRenderer.on('gitify:auto-updater', (_, isUpdateAvailable: boolean) => { - setIsUpdateAvailable(isUpdateAvailable); - }); - }, []); return (
@@ -40,7 +32,7 @@ export const SettingsRoute: FC = () => {
- +
); }; diff --git a/src/routes/__snapshots__/Settings.test.tsx.snap b/src/routes/__snapshots__/Settings.test.tsx.snap index 54e98764e..0f0b2a506 100644 --- a/src/routes/__snapshots__/Settings.test.tsx.snap +++ b/src/routes/__snapshots__/Settings.test.tsx.snap @@ -797,28 +797,6 @@ exports[`routes/Settings.tsx should render itself & its children 1`] = ` Gitify v0.0.1 - - - - -