diff --git a/.eslintignore b/.eslintignore index 40ef626e4..72daf6812 100644 --- a/.eslintignore +++ b/.eslintignore @@ -200,6 +200,9 @@ packages/app-desktop/app.js.map packages/app-desktop/bridge.d.ts packages/app-desktop/bridge.js packages/app-desktop/bridge.js.map +packages/app-desktop/checkForUpdates.d.ts +packages/app-desktop/checkForUpdates.js +packages/app-desktop/checkForUpdates.js.map packages/app-desktop/commands/copyDevCommand.d.ts packages/app-desktop/commands/copyDevCommand.js packages/app-desktop/commands/copyDevCommand.js.map diff --git a/.gitignore b/.gitignore index 2030c6022..320dd47c8 100644 --- a/.gitignore +++ b/.gitignore @@ -186,6 +186,9 @@ packages/app-desktop/app.js.map packages/app-desktop/bridge.d.ts packages/app-desktop/bridge.js packages/app-desktop/bridge.js.map +packages/app-desktop/checkForUpdates.d.ts +packages/app-desktop/checkForUpdates.js +packages/app-desktop/checkForUpdates.js.map packages/app-desktop/commands/copyDevCommand.d.ts packages/app-desktop/commands/copyDevCommand.js packages/app-desktop/commands/copyDevCommand.js.map diff --git a/packages/app-desktop/app.ts b/packages/app-desktop/app.ts index 2a99aff98..5c8753649 100644 --- a/packages/app-desktop/app.ts +++ b/packages/app-desktop/app.ts @@ -103,6 +103,7 @@ const globalCommands = [ import editorCommandDeclarations from './gui/NoteEditor/commands/editorCommandDeclarations'; import ShareService from '@joplin/lib/services/share/ShareService'; +import checkForUpdates from './checkForUpdates'; const pluginClasses = [ require('./plugins/GotoAnything').default, @@ -167,10 +168,6 @@ class Application extends BaseApplication { return true; } - checkForUpdateLoggerPath() { - return `${Setting.value('profileDir')}/log-autoupdater.txt`; - } - reducer(state: AppState = appDefaultState, action: any) { let newState = state; @@ -711,7 +708,7 @@ class Application extends BaseApplication { if (shim.isWindows() || shim.isMac()) { const runAutoUpdateCheck = () => { if (Setting.value('autoUpdateEnabled')) { - bridge().checkForUpdates(true, bridge().window(), this.checkForUpdateLoggerPath(), { includePreReleases: Setting.value('autoUpdate.includePreReleases') }); + void checkForUpdates(true, bridge().window(), { includePreReleases: Setting.value('autoUpdate.includePreReleases') }); } }; diff --git a/packages/app-desktop/bridge.ts b/packages/app-desktop/bridge.ts index e2580d5aa..43178e53e 100644 --- a/packages/app-desktop/bridge.ts +++ b/packages/app-desktop/bridge.ts @@ -1,6 +1,5 @@ import ElectronAppWrapper from './ElectronAppWrapper'; import shim from '@joplin/lib/shim'; - import { _, setLocale } from '@joplin/lib/locale'; const { dirname, toSystemSlashes } = require('@joplin/lib/path-utils'); const { BrowserWindow, nativeTheme } = require('electron'); @@ -174,11 +173,6 @@ export class Bridge { return require('electron').shell.openPath(fullPath); } - checkForUpdates(inBackground: boolean, window: any, logFilePath: string, options: any) { - const { checkForUpdates } = require('./checkForUpdates.js'); - checkForUpdates(inBackground, window, logFilePath, options); - } - buildDir() { return this.electronApp().buildDir(); } diff --git a/packages/app-desktop/checkForUpdates.js b/packages/app-desktop/checkForUpdates.ts similarity index 51% rename from packages/app-desktop/checkForUpdates.js rename to packages/app-desktop/checkForUpdates.ts index 4625abce7..ffdf3c553 100644 --- a/packages/app-desktop/checkForUpdates.js +++ b/packages/app-desktop/checkForUpdates.ts @@ -1,44 +1,42 @@ -const { dialog } = require('electron'); -const shim = require('@joplin/lib/shim').default; -const Logger = require('@joplin/lib/Logger').default; -const { _ } = require('@joplin/lib/locale'); -const fetch = require('node-fetch'); +import shim from '@joplin/lib/shim'; +import Logger from '@joplin/lib/Logger'; +import { _ } from '@joplin/lib/locale'; +import bridge from './services/bridge'; +import KvStore from '@joplin/lib/services/KvStore'; const { fileExtension } = require('@joplin/lib/path-utils'); +const ArrayUtils = require('@joplin/lib/ArrayUtils'); const packageInfo = require('./packageInfo.js'); const compareVersions = require('compare-versions'); -let autoUpdateLogger_ = new Logger(); +const logger = Logger.create('checkForUpdates'); + let checkInBackground_ = false; let isCheckingForUpdate_ = false; -let parentWindow_ = null; -function showErrorMessageBox(message) { - return dialog.showMessageBox(parentWindow_, { - type: 'error', - message: message, - }); +interface CheckForUpdateOptions { + includePreReleases?: boolean; } function onCheckStarted() { - autoUpdateLogger_.info('checkForUpdates: Starting...'); + logger.info('Starting...'); isCheckingForUpdate_ = true; } function onCheckEnded() { - autoUpdateLogger_.info('checkForUpdates: Done.'); + logger.info('Done.'); isCheckingForUpdate_ = false; } -function getMajorMinorTagName(tagName) { +function getMajorMinorTagName(tagName: string) { const s = tagName.split('.'); s.pop(); return s.join('.'); } -async function fetchLatestRelease(options) { +async function fetchLatestRelease(options: CheckForUpdateOptions) { options = Object.assign({}, { includePreReleases: false }, options); - const response = await fetch('https://api.github.com/repos/laurent22/joplin/releases'); + const response = await shim.fetch('https://api.github.com/repos/laurent22/joplin/releases'); if (!response.ok) { const responseText = await response.text(); @@ -104,7 +102,7 @@ async function fetchLatestRelease(options) { } } - function cleanUpReleaseNotes(releaseNotes) { + function cleanUpReleaseNotes(releaseNotes: string[]) { const lines = releaseNotes.join('\n\n* * *\n\n').split('\n'); const output = []; for (const line of lines) { @@ -129,7 +127,7 @@ async function fetchLatestRelease(options) { }; } -function truncateText(text, length) { +function truncateText(text: string, length: number) { let truncated = text.substring(0, length); const lastNewLine = truncated.lastIndexOf('\n'); // Cut off at a line break unless we'd be cutting off half the text @@ -141,66 +139,80 @@ function truncateText(text, length) { return truncated; } -function checkForUpdates(inBackground, window, logFilePath, options) { +async function getSkippedVersions(): Promise { + const r = await KvStore.instance().value('updateCheck::skippedVersions'); + return r ? JSON.parse(r) : []; +} + +async function isSkippedVersion(v: string): Promise { + const versions = await getSkippedVersions(); + return versions.includes(v); +} + +async function addSkippedVersion(s: string) { + let versions = await getSkippedVersions(); + versions.push(s); + versions = ArrayUtils.unique(versions); + await KvStore.instance().setValue('updateCheck::skippedVersions', JSON.stringify(versions)); +} + +export default async function checkForUpdates(inBackground: boolean, parentWindow: any, options: CheckForUpdateOptions) { if (isCheckingForUpdate_) { - autoUpdateLogger_.info('checkForUpdates: Skipping check because it is already running'); + logger.info('Skipping check because it is already running'); return; } - parentWindow_ = window; - onCheckStarted(); - if (logFilePath && !autoUpdateLogger_.targets().length) { - autoUpdateLogger_ = new Logger(); - autoUpdateLogger_.addTarget('file', { path: logFilePath }); - autoUpdateLogger_.setLevel(Logger.LEVEL_DEBUG); - autoUpdateLogger_.info('checkForUpdates: Initializing...'); - } - checkInBackground_ = inBackground; - autoUpdateLogger_.info(`checkForUpdates: Checking with options ${JSON.stringify(options)}`); + logger.info(`Checking with options ${JSON.stringify(options)}`); - fetchLatestRelease(options).then(async (release) => { - autoUpdateLogger_.info(`Current version: ${packageInfo.version}`); - autoUpdateLogger_.info(`Latest version: ${release.version}`); - autoUpdateLogger_.info('Is Pre-release:', release.prerelease); + try { + const release = await fetchLatestRelease(options); + + logger.info(`Current version: ${packageInfo.version}`); + logger.info(`Latest version: ${release.version}`); + logger.info('Is Pre-release:', release.prerelease); if (compareVersions(release.version, packageInfo.version) <= 0) { if (!checkInBackground_) { - await dialog.showMessageBox({ - type: 'info', - message: _('Current version is up-to-date.'), - buttons: [_('OK')], - }); + await bridge().showMessageBox(_('Current version is up-to-date.')); } } else { - const fullReleaseNotes = release.notes.trim() ? `\n\n${release.notes.trim()}` : ''; - const MAX_RELEASE_NOTES_LENGTH = 1000; - const truncateReleaseNotes = fullReleaseNotes.length > MAX_RELEASE_NOTES_LENGTH; - const releaseNotes = truncateReleaseNotes ? truncateText(fullReleaseNotes, MAX_RELEASE_NOTES_LENGTH) : fullReleaseNotes; + const shouldSkip = checkInBackground_ && await isSkippedVersion(release.version); - const newVersionString = release.prerelease ? _('%s (pre-release)', release.version) : release.version; + if (shouldSkip) { + logger.info('Not displaying notification because version has been skipped'); + } else { + const fullReleaseNotes = release.notes.trim() ? `\n\n${release.notes.trim()}` : ''; + const MAX_RELEASE_NOTES_LENGTH = 1000; + const truncateReleaseNotes = fullReleaseNotes.length > MAX_RELEASE_NOTES_LENGTH; + const releaseNotes = truncateReleaseNotes ? truncateText(fullReleaseNotes, MAX_RELEASE_NOTES_LENGTH) : fullReleaseNotes; - const result = await dialog.showMessageBox(parentWindow_, { - type: 'info', - message: `${_('An update is available, do you want to download it now?')}`, - detail: `${_('Your version: %s', packageInfo.version)}\n${_('New version: %s', newVersionString)}${releaseNotes}`, - buttons: [_('Download'), _('Cancel'), _('Full changelog')], - cancelId: 1, - }); + const newVersionString = release.prerelease ? _('%s (pre-release)', release.version) : release.version; - const buttonIndex = result.response; - if (buttonIndex === 0) require('electron').shell.openExternal(release.downloadUrl ? release.downloadUrl : release.pageUrl); - if (buttonIndex === 2) require('electron').shell.openExternal('https://joplinapp.org/changelog/'); + const buttonIndex = await bridge().showMessageBox(parentWindow, { + type: 'info', + message: `${_('An update is available, do you want to download it now?')}`, + detail: `${_('Your version: %s', packageInfo.version)}\n${_('New version: %s', newVersionString)}${releaseNotes}`, + buttons: [_('Download'), _('Skip this version'), _('Full changelog'), _('Cancel')], + cancelId: 3, + }); + + if (buttonIndex === 0) { + bridge().openExternal(release.downloadUrl ? release.downloadUrl : release.pageUrl); + } else if (buttonIndex === 1) { + await addSkippedVersion(release.version); + } else if (buttonIndex === 2) { + bridge().openExternal('https://joplinapp.org/changelog/'); + } + } } - }).catch(error => { - autoUpdateLogger_.error(error); - if (!checkInBackground_) showErrorMessageBox(error.message); - }).then(() => { + } catch (error) { + logger.error(error); + if (!checkInBackground_) await bridge().showErrorMessageBox(error.message); + } finally { onCheckEnded(); - }); + } } - -module.exports.checkForUpdates = checkForUpdates; diff --git a/packages/app-desktop/gui/MenuBar.tsx b/packages/app-desktop/gui/MenuBar.tsx index 7d7b3b74e..5bbdbce16 100644 --- a/packages/app-desktop/gui/MenuBar.tsx +++ b/packages/app-desktop/gui/MenuBar.tsx @@ -17,6 +17,7 @@ import SpellCheckerService from '@joplin/lib/services/spellChecker/SpellCheckerS import menuCommandNames from './menuCommandNames'; import stateToWhenClauseContext from '../services/commands/stateToWhenClauseContext'; import bridge from '../services/bridge'; +import checkForUpdates from '../checkForUpdates'; const { connect } = require('react-redux'); import { reg } from '@joplin/lib/registry'; @@ -430,7 +431,7 @@ function useMenu(props: Props) { toolsItems.push(SpellCheckerService.instance().spellCheckerConfigMenuItem(props['spellChecker.language'], props['spellChecker.enabled'])); function _checkForUpdates() { - bridge().checkForUpdates(false, bridge().window(), `${Setting.value('profileDir')}/log-autoupdater.txt`, { includePreReleases: Setting.value('autoUpdate.includePreReleases') }); + void checkForUpdates(false, bridge().window(), { includePreReleases: Setting.value('autoUpdate.includePreReleases') }); } function _showAbout() { diff --git a/packages/lib/models/Setting.ts b/packages/lib/models/Setting.ts index 0b2e28c7c..5bd1bc8e7 100644 --- a/packages/lib/models/Setting.ts +++ b/packages/lib/models/Setting.ts @@ -992,7 +992,7 @@ class Setting extends BaseModel { }, - autoUpdateEnabled: { value: false, type: SettingItemType.Bool, storage: SettingStorage.File, section: 'application', public: platform !== 'linux', appTypes: ['desktop'], label: () => _('Automatically update the application') }, + autoUpdateEnabled: { value: true, type: SettingItemType.Bool, storage: SettingStorage.File, section: 'application', public: platform !== 'linux', appTypes: ['desktop'], label: () => _('Automatically update the application') }, 'autoUpdate.includePreReleases': { value: false, type: SettingItemType.Bool, section: 'application', storage: SettingStorage.File, public: true, appTypes: ['desktop'], label: () => _('Get pre-releases when checking for updates'), description: () => _('See the pre-release page for more details: %s', 'https://joplinapp.org/prereleases') }, 'clipperServer.autoStart': { value: false, type: SettingItemType.Bool, storage: SettingStorage.File, public: false }, 'sync.interval': { diff --git a/packages/lib/shim.ts b/packages/lib/shim.ts index 80301ee4d..dc0b1b9d5 100644 --- a/packages/lib/shim.ts +++ b/packages/lib/shim.ts @@ -169,7 +169,7 @@ const shim = { } }, - fetch: (_url: string, _options: any): any => { + fetch: (_url: string, _options: any = null): any => { throw new Error('Not implemented'); },