Skip to content

Commit

Permalink
feat: add better notifications of available updates (#110)
Browse files Browse the repository at this point in the history
* feat: add better notifications of available updates
* feat: allow for manual update checks
  • Loading branch information
mscharley authored Mar 15, 2022
1 parent b7f1297 commit f522554
Show file tree
Hide file tree
Showing 24 changed files with 233 additions and 116 deletions.
3 changes: 3 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,6 @@

# If this is set to any value then it will force update checks in local development.
# FORCE_CHECK_UPDATES=true

# Uncomment this if you want to use a local wine prefix for building Windows packages.
# WINEPREFIX=./.wine
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ coverage/
dev-app-update.yml
.env
*.tsbuildinfo
.wine/
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
"test:build": "electron-builder --linux --publish never && ./dist/Notes-${npm_package_version}.AppImage --self-test",
"viteshot": "viteshot",
"viteshot:ci": "node ./bin/viteshot-ci.mjs",
"pack": "npm run release -- --publish never",
"pack": "set -a; [ -f ./.env ] && source ./.env; npm run release -- --publish never",
"release": "electron-builder --win --linux"
},
"watch": {
Expand Down
3 changes: 1 addition & 2 deletions src/main/Entrypoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,7 @@ export class Entrypoint {
this.application.on('second-instance', this.onSecondInstance);
this.application.on('window-all-closed', this.onWindowAllClosed);

// The ready handler should be the last registered as it may be fired immediately.
this.application.on('ready', this.onReady);
this.application.whenReady().then(this.onReady).catch(this.onFatalError);
};

private readonly onSecondInstance = (
Expand Down
4 changes: 4 additions & 0 deletions src/main/MainWindow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ export class MainWindow implements OnReadyHandler {
}
};

public readonly send = (channel: string, ...args: unknown[]): void => {
this.window?.webContents.send(channel, ...args);
};

private readonly isAllowedInternalNavigationUrl = (url: string): boolean => {
const parsed = new URL(url);

Expand Down
1 change: 1 addition & 0 deletions src/main/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ class EditorApiImpl implements EditorApi {
public readonly aboutDetails: EditorApi['aboutDetails'] = ipcRenderer.invoke('about-details');
public readonly addFolder: EditorApi['addFolder'] = async (name: string, localPath: string) =>
ipcRenderer.invoke('add-folder', name, localPath);
public readonly checkForUpdates: EditorApi['checkForUpdates'] = () => ipcRenderer.send('check-updates');
public readonly deleteFolder: EditorApi['deleteFolder'] = async (uuid: string) =>
ipcRenderer.invoke('delete-folder', uuid);
public readonly doLinuxInstallation: EditorApi['doLinuxInstallation'] = async (options) =>
Expand Down
8 changes: 1 addition & 7 deletions src/main/services/AboutElectron.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,29 @@
import { ElectronApp, ElectronIpcMain } from '~main/inversify/tokens';
import { inject, injectable, unmanaged } from 'inversify';
import type { AboutDetails } from '~shared/model/AboutDetails';
import { compare } from 'compare-versions';
import { DevTools } from '~main/services/DevTools';
import { injectToken } from 'inversify-token';
import type { OnReadyHandler } from '~main/interfaces/OnReadyHandler';
import { UpdatesProvider } from '~main/services/UpdatesProvider';

@injectable()
export class AboutElectron implements OnReadyHandler {
public constructor(
@injectToken(ElectronApp) private readonly app: ElectronApp,
@injectToken(ElectronIpcMain) private readonly ipcMain: ElectronIpcMain,
@inject(UpdatesProvider) private readonly updates: UpdatesProvider,
@inject(DevTools) private readonly devtools: DevTools,
@unmanaged() private readonly process: NodeJS.Process = globalThis.process,
) {}

public readonly onAppReady = (): void => {
// eslint-disable-next-line @typescript-eslint/require-await
this.ipcMain.handle('about-details', async (): Promise<AboutDetails> => {
const updateCheckResults = await this.updates.checkResults;
const version = this.app.getVersion();

return {
electronVersion: this.process.versions.electron,
version,
isDevBuild: this.devtools.isDev,

updateExists: compare(updateCheckResults?.updateInfo.version ?? '0.0.0', version, '>'),
updateVersion: updateCheckResults?.updateInfo.version,

osName: this.process.platform,
osVersion: this.process.getSystemVersion(),
};
Expand Down
2 changes: 1 addition & 1 deletion src/main/services/Configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export class Configuration implements ReadyHandler {
this.ipcMain.handle('set-last-folder', (_ev, uuid: string) => this.store.set('lastFolder', uuid));
this.ipcMain.handle('set-last-file', (_ev, url: string) => this.store.set('lastFile', url));
this.onChange(() => {
this.window.window?.webContents.send('configuration', this.asAppConfiguration());
this.window.send('configuration', this.asAppConfiguration());
});
};

Expand Down
2 changes: 1 addition & 1 deletion src/main/services/FileSystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ export class FileSystem implements CustomProtocolProvider, OnReadyHandler {

private readonly republishFileList = async (): Promise<void> => {
const files = await this.listFiles();
this.mainWindow.window?.webContents.send('files-updated', files);
this.mainWindow.send('files-updated', files);
};

private readonly serveLocalFile = (basepath: string, filepath: string, url: string): string | ProtocolResponse => {
Expand Down
71 changes: 68 additions & 3 deletions src/main/services/UpdatesProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,22 @@ import type { AppUpdater, UpdateCheckResult } from 'electron-updater';
import { inject, injectable, unmanaged } from 'inversify';
import { autoUpdater } from 'electron-updater';
import { DevTools } from '~main/services/DevTools';
import { ElectronIpcMain } from '~main/inversify/tokens';
import { injectToken } from 'inversify-token';
import log from 'electron-log';
import { MainWindow } from '~main/MainWindow';
import type { OnReadyHandler } from '~main/interfaces/OnReadyHandler';
import type { UpdateStatus } from '~shared/model';

// eslint-disable-next-line @typescript-eslint/no-magic-numbers
export const ONE_WEEK_IN_MILLISECONDS = 7 * 24 * 60 * 60 * 1000;

@injectable()
export class UpdatesProvider implements OnReadyHandler {
public constructor(
@inject(DevTools) private readonly devtools: DevTools,
@inject(MainWindow) private readonly mainWindow: MainWindow,
@injectToken(ElectronIpcMain) private readonly ipcMain: ElectronIpcMain,
@unmanaged() private readonly updater: AppUpdater = autoUpdater,
) {
this.updater.logger = log;
Expand All @@ -20,13 +29,69 @@ export class UpdatesProvider implements OnReadyHandler {
}

public readonly onAppReady = (): void => {
this.updateStatus({
canCheckForUpdates: false,
checkingForUpdate: false,
updateDownloaded: false,
updateExists: false,
});
// TODO: make this configurable.
this.updater.autoDownload = true;
if (!this.devtools.isDev || 'FORCE_CHECK_UPDATES' in process.env) {
this._checkResults = this.updater.checkForUpdates().catch((_e) => {
// Don't both logging here, electron-updater already logs the error.
this.ipcMain.on('check-updates', this.doSingleUpdateRun);
setInterval(this.doSingleUpdateRun, ONE_WEEK_IN_MILLISECONDS);
}
};

private readonly doSingleUpdateRun = (): void => {
this.updateStatus({
canCheckForUpdates: true,
checkingForUpdate: true,
updateDownloaded: false,
updateExists: false,
});
this._checkResults = this.updater
.checkForUpdates()
.then((update) => {
if (update.downloadPromise != null) {
this.updateStatus({
canCheckForUpdates: true,
checkingForUpdate: false,
updateDownloaded: false,
updateExists: true,
updateVersion: update.updateInfo.version,
});
update.downloadPromise
.then(() => {
this.updateStatus({
canCheckForUpdates: true,
checkingForUpdate: false,
updateDownloaded: true,
updateExists: true,
updateVersion: update.updateInfo.version,
});
})
.catch((e) => {
log.error(e);
});
} else {
this.updateStatus({
canCheckForUpdates: true,
checkingForUpdate: false,
updateDownloaded: false,
updateExists: false,
});
}

return update;
})
.catch((_e) => {
// Don't bother logging here, electron-updater already logs the error.
return null;
});
}
};

private readonly updateStatus = (status: UpdateStatus): void => {
this.mainWindow.send('update-status', status);
};
}
58 changes: 0 additions & 58 deletions src/main/services/__tests__/AboutElectron.ts

This file was deleted.

36 changes: 28 additions & 8 deletions src/renderer/DataProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
setCurrentFolder,
setFatalError,
setFileListing,
setUpdateStatus,
updateAppConfiguration,
} from '~renderer/redux';
import { useAppDispatch, useAppSelector } from '~renderer/hooks';
Expand Down Expand Up @@ -50,22 +51,41 @@ export const DataProvider: React.FC = ({ children }) => {
}, [dispatch, handleFileUpdates]);

useEffect(() => {
editorApi.aboutDetails
.then((details) => dispatch(setAboutDetails(details)))
.catch((e) => dispatch(setFatalError(e)));
}, [dispatch]);

useEffect(() => {
editorApi
const result = editorApi
.getAppConfiguration()
.then((configuration) => {
setLoadedConfiguration(true);
editorApi.on('configuration', (newConfiguration) => {
const handler = editorApi.on('configuration', (newConfiguration) => {
dispatch(updateAppConfiguration(newConfiguration));
});
dispatch(updateAppConfiguration(configuration));

return handler;
})
.catch((e) => dispatch(setFatalError(e)));

return (): void => {
result
.then((v) => (typeof v === 'number' ? editorApi.off('configuration', v) : undefined))
.catch((e) => dispatch(setFatalError(e)));
};
}, [dispatch]);

useEffect(() => {
const handler = editorApi.on('update-status', (status) => {
dispatch(setUpdateStatus(status));
});
editorApi.checkForUpdates();

return (): void => {
editorApi.off('update-status', handler);
};
}, [dispatch]);

useEffect(() => {
editorApi.aboutDetails
.then((details) => dispatch(setAboutDetails(details)))
.catch((e) => dispatch(setFatalError(e)));
}, [dispatch]);

useEffect(() => {
Expand Down
23 changes: 22 additions & 1 deletion src/renderer/components/AboutDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import Dialog from '@mui/material/Dialog';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
import IconButton from '@mui/material/IconButton';
import { noop } from '~shared/util';
import { SpinningRefreshIcon } from './SpinningRefreshIcon';
import Typography from '@mui/material/Typography';
import { useAppSelector } from '~renderer/hooks';

Expand All @@ -15,6 +17,12 @@ export interface AboutDialogProps {

export const AboutDialog: React.FC<AboutDialogProps> = ({ open, onClose = noop }) => {
const details = useAppSelector((s) => s.about.details);
const updates = useAppSelector((s) => s.updates.status) ?? {
canCheckForUpdates: false,
checkingForUpdate: false,
updateDownloaded: false,
updateExists: false,
};

return (
<Dialog open={open} onClose={onClose} fullWidth={true} maxWidth={'sm'}>
Expand All @@ -27,7 +35,20 @@ export const AboutDialog: React.FC<AboutDialogProps> = ({ open, onClose = noop }
<Typography paragraph>
<strong>Version:</strong>
{` ${details.version}${details.isDevBuild ? ' (dev)' : ''}`}
{`${details.updateExists ? ` - update to ${details.updateVersion} by restarting the application.` : ''}`}
{updates.updateDownloaded ? (
<> - update to {updates.updateVersion} by restarting the application.</>
) : updates.updateExists ? (
<> - update to {updates.updateVersion} downloading...</>
) : updates.canCheckForUpdates ? (
<>
<IconButton onClick={(): void => editorApi.checkForUpdates()}>
<SpinningRefreshIcon spinning={updates.checkingForUpdate} />
</IconButton>
Check for updates
</>
) : (
''
)}
<br />
<strong>Electron Version:</strong>
{` ${details.electronVersion}`}
Expand Down
Loading

0 comments on commit f522554

Please sign in to comment.