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/context/App.tsx b/src/context/App.tsx
index 67857be5e..77f7167e9 100644
--- a/src/context/App.tsx
+++ b/src/context/App.tsx
@@ -46,7 +46,7 @@ import { clearState, loadState, saveState } from '../utils/storage';
import { setTheme } from '../utils/theme';
import { zoomPercentageToLevel } from '../utils/zoom';
-const defaultAuth: AuthState = {
+export const defaultAuth: AuthState = {
accounts: [],
token: null,
enterpriseAccounts: [],
@@ -164,8 +164,9 @@ export const AppProvider = ({ children }: { children: ReactNode }) => {
useEffect(() => {
ipcRenderer.on('gitify:reset-app', () => {
- setAuth(defaultAuth);
clearState();
+ setAuth(defaultAuth);
+ setSettings(defaultSettings);
});
}, []);
diff --git a/src/electron/main.js b/src/electron/main.js
index b8d6f853f..34d0b975e 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();
@@ -244,6 +268,7 @@ function takeScreenshot() {
function resetApp() {
const cancelButtonId = 0;
+ const resetButtonId = 1;
const response = dialog.showMessageBoxSync(mb.window, {
type: 'warning',
@@ -255,10 +280,8 @@ function resetApp() {
cancelId: cancelButtonId,
});
- if (response === cancelButtonId) {
- return;
+ if (response === resetButtonId) {
+ mb.window.webContents.send('gitify:reset-app');
+ mb.app.quit();
}
-
- mb.window.webContents.send('gitify:reset-app');
- mb.app.quit();
}
diff --git a/src/hooks/useNotifications.test.ts b/src/hooks/useNotifications.test.ts
index 730364ea7..30e6f327a 100644
--- a/src/hooks/useNotifications.test.ts
+++ b/src/hooks/useNotifications.test.ts
@@ -476,7 +476,7 @@ describe('hooks/useNotifications.ts', () => {
describe('markRepoNotificationsRead', () => {
const repoSlug = 'gitify-app/notifications-test';
- it("should mark a repository's notifications as read with success", async () => {
+ it('should mark repository notifications as read with success', async () => {
nock('https://api.github.com/')
.put(`/repos/${repoSlug}/notifications`)
.reply(200);
@@ -497,7 +497,7 @@ describe('hooks/useNotifications.ts', () => {
expect(result.current.notifications.length).toBe(0);
});
- it("should mark a repository's notifications as read with failure", async () => {
+ it('should mark repository notifications as read with failure', async () => {
nock('https://api.github.com/')
.put(`/repos/${repoSlug}/notifications`)
.reply(400);
@@ -520,7 +520,7 @@ describe('hooks/useNotifications.ts', () => {
});
describe('markRepoNotificationsDone', () => {
- it("should mark a repository's notifications as done with success", async () => {
+ it('should mark repository notifications as done with success', async () => {
nock('https://api.github.com/')
.delete(`/notifications/threads/${id}`)
.reply(200);
@@ -541,7 +541,7 @@ describe('hooks/useNotifications.ts', () => {
expect(result.current.notifications.length).toBe(0);
});
- it("should mark a repository's notifications as done with failure", async () => {
+ it('should mark repository notifications as done with failure', async () => {
nock('https://api.github.com/')
.delete(`/notifications/threads/${id}`)
.reply(400);
diff --git a/src/hooks/useNotifications.ts b/src/hooks/useNotifications.ts
index a5f385ab2..a6d392938 100644
--- a/src/hooks/useNotifications.ts
+++ b/src/hooks/useNotifications.ts
@@ -60,6 +60,7 @@ export const useNotifications = (): NotificationsState => {
const fetchNotifications = useCallback(
async (state: GitifyState) => {
setStatus('loading');
+ setGlobalError(null);
const fetchedNotifications = await getAllNotifications(state);
diff --git a/src/routes/Notifications.tsx b/src/routes/Notifications.tsx
index 17543e543..0e60f1a8f 100644
--- a/src/routes/Notifications.tsx
+++ b/src/routes/Notifications.tsx
@@ -4,10 +4,12 @@ import { AllRead } from '../components/AllRead';
import { Oops } from '../components/Oops';
import { AppContext } from '../context/App';
import { getAccountUUID } from '../utils/auth/utils';
+import { Errors } from '../utils/constants';
import { getNotificationCount } from '../utils/notifications';
export const NotificationsRoute: FC = () => {
- const { notifications, globalError, settings } = useContext(AppContext);
+ const { notifications, status, globalError, settings } =
+ useContext(AppContext);
const hasMultipleAccounts = useMemo(
() => notifications.length > 1,
@@ -24,8 +26,8 @@ export const NotificationsRoute: FC = () => {
[notifications],
);
- if (globalError) {
- return
;
+ if (status === 'error') {
+ return
;
}
if (!hasNotifications && hasNoAccountErrors) {
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__/Notifications.test.tsx.snap b/src/routes/__snapshots__/Notifications.test.tsx.snap
index 5dbd73147..8bb99d300 100644
--- a/src/routes/__snapshots__/Notifications.test.tsx.snap
+++ b/src/routes/__snapshots__/Notifications.test.tsx.snap
@@ -144,13 +144,13 @@ exports[`routes/Notifications.tsx should render itself & its children (error con
"baseElement":
diff --git a/src/utils/comms.test.ts b/src/utils/comms.test.ts
index 3c7402ba4..e78779d14 100644
--- a/src/utils/comms.test.ts
+++ b/src/utils/comms.test.ts
@@ -72,6 +72,16 @@ describe('utils/comms.ts', () => {
});
});
+ it('should use default open preference if user settings not found', () => {
+ jest.spyOn(storage, 'loadState').mockReturnValue({ settings: null });
+
+ openExternalLink('https://www.gitify.io/' as Link);
+ expect(shell.openExternal).toHaveBeenCalledTimes(1);
+ expect(shell.openExternal).toHaveBeenCalledWith('https://www.gitify.io/', {
+ activate: true,
+ });
+ });
+
it('should ignore opening external local links file:///', () => {
openExternalLink('file:///Applications/SomeApp.app' as Link);
expect(shell.openExternal).toHaveBeenCalledTimes(0);
diff --git a/src/utils/comms.ts b/src/utils/comms.ts
index 13cef346e..0a11fa0c3 100644
--- a/src/utils/comms.ts
+++ b/src/utils/comms.ts
@@ -1,4 +1,5 @@
import { ipcRenderer, shell } from 'electron';
+import { defaultSettings } from '../context/App';
import { type Link, OpenPreference } from '../types';
import Constants from './constants';
import { loadState } from './storage';
@@ -8,8 +9,12 @@ export function openExternalLink(url: Link): void {
// Load the state from local storage to avoid having to pass settings as a parameter
const { settings } = loadState();
+ const openPreference = settings
+ ? settings.openLinks
+ : defaultSettings.openLinks;
+
shell.openExternal(url, {
- activate: settings.openLinks === OpenPreference.FOREGROUND,
+ activate: openPreference === OpenPreference.FOREGROUND,
});
}
}