[MM-18552] Autoupdater (#1714)

* wip

* background download

* various fixes

* wip

* wokring autoupgrade

* fix menu

* fix windows

* cleanup

* add publishername

* fix messages and titles

* Test updates

* Moved module and added functionality to click icon to install (instead of just download)

* Add auto update setting, update on close app if downloaded

* Tests, changes for security fixes, update version number

* Update E2E tests

* Lint fix

* Update to latest electron-updater

* Revert to stable electron-builder (only needed to update electron-updater)

* Fix package-lock

* skip flaky test

* Update package

* Fix E2E test

* Fixes for enabling/disabled autoupdater

* Fixed GPO definitions

* [MM-38300] Set localhost as the test server

* blank

* Switch to s3 bucket for testing

* Update icons to match spec

* Add menu items for download/update actions

* Type and test fixes

* Fix notification circle

* Fix macOS app not restarting on Restart/Update

* Update dialog box titles

* Turn off file system check for Linux

* Changes to support deployments

* Testing autoupdater deployments to s3

* disable tests for now

* asfrehwf

* fine no windows WHATEVER

* remove windows again

* Try universal all in one

* pffftttngggguhhhh

* make sure it's working

* Missed artifacts script

* Modify destination as well

* one more time!

* Update yml files

* Oops

* add yq manually

* oof

* Fix the script to work properly

* Fix release script

* Fix script again so it runs in time

* Build version 2

* Revert build specific changes

* Lint override

* Fix build apps for PR builds

* One more change

* Add file generation for .deb repo

* Deb repo test

* skip tests for now

* Fix artifact push

* Persist after repo creation

* Put tests back

* Fix unit tests

* Enable mac generated builds temp

* Temporarily disable tests

* Fix issue where notification doesn't pop dialog box

* Try version 2 again

* Put the version back

* Attempting to debug mac app path issue

* Fix issue where Mac app will quarantine itself after first update

* Lock versions of yq

* Fix yq for mac

* As usual, Mac is difficult :P

* Add quotes to anti-quarantine command

* Change to spawn to avoid command injection

* Oops

* Nightly deployment changes (#2005)

* Test nightly deploy

* I fixed a some things

* aaaaaaaaa

* Restore old bucket

* Added progress indicator via tooltip

* Ship nightly builds to main S3 bucket

* PR feedback

* Fix a couple security exploits

* Fix opacity on light mode button

* Use large app icon

* Resize icon for Windows

* Resize icon for Mac

* Update to electron-updater final

* Remove Mac support and deb repo

* Typo

* Remove deb script

* Remove checksum function

* Removed autoUpdateSettingsPath

* Update URL

Co-authored-by: = <=>
Co-authored-by: Devin Binnie <devin.binnie@mattermost.com>
Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
Co-authored-by: Devin Binnie <52460000+devinbinnie@users.noreply.github.com>
This commit is contained in:
Guillermo Vayá
2022-03-08 17:38:38 +01:00
committed by GitHub
parent 0ab6a1f80f
commit d2435a561c
56 changed files with 2323 additions and 1502 deletions

View File

@@ -2,11 +2,13 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import classNames from 'classnames';
import React, {Fragment} from 'react';
import {Container, Row} from 'react-bootstrap';
import {DropResult} from 'react-beautiful-dnd';
import DotsVerticalIcon from 'mdi-react/DotsVerticalIcon';
import {IpcRendererEvent} from 'electron/renderer';
import prettyBytes from 'pretty-bytes';
import {TeamWithTabs} from 'types/config';
@@ -36,6 +38,11 @@ import {
CLOSE_TEAMS_DROPDOWN,
OPEN_TEAMS_DROPDOWN,
SWITCH_TAB,
UPDATE_AVAILABLE,
UPDATE_DOWNLOADED,
UPDATE_PROGRESS,
START_UPGRADE,
START_DOWNLOAD,
CLOSE_TAB,
} from 'common/communication';
@@ -50,6 +57,7 @@ import TabBar from './TabBar';
import ExtraBar from './ExtraBar';
import ErrorView from './ErrorView';
import TeamDropdownButton from './TeamDropdownButton';
import '../css/components/UpgradeButton.scss';
enum Status {
LOADING = 1,
@@ -59,6 +67,13 @@ enum Status {
NOSERVERS = -2,
}
enum UpgradeStatus {
NONE = 0,
AVAILABLE = 1,
DOWNLOADING = 2,
DOWNLOADED = 3,
}
type Props = {
teams: TeamWithTabs[];
lastActiveTeam?: number;
@@ -82,6 +97,14 @@ type State = {
fullScreen?: boolean;
showExtraBar?: boolean;
isMenuOpen: boolean;
upgradeStatus: UpgradeStatus;
upgradeProgress?: {
total: number;
delta: number;
transferred: number;
percent: number;
bytesPerSecond: number;
};
};
type TabViewStatus = {
@@ -119,6 +142,7 @@ export default class MainPage extends React.PureComponent<Props, State> {
tabViewStatus: new Map(this.props.teams.map((team) => team.tabs.map((tab) => getTabViewName(team.name, tab.name))).flat().map((tabViewName) => [tabViewName, {status: Status.LOADING}])),
darkMode: this.props.darkMode,
isMenuOpen: false,
upgradeStatus: UpgradeStatus.NONE,
};
}
@@ -221,6 +245,27 @@ export default class MainPage extends React.PureComponent<Props, State> {
this.setState({isMenuOpen: true});
});
window.ipcRenderer.on(UPDATE_AVAILABLE, () => {
this.setState({upgradeStatus: UpgradeStatus.AVAILABLE});
});
window.ipcRenderer.on(UPDATE_DOWNLOADED, () => {
this.setState({upgradeStatus: UpgradeStatus.DOWNLOADED});
});
window.ipcRenderer.on(UPDATE_PROGRESS, (event, total, delta, transferred, percent, bytesPerSecond) => {
this.setState({
upgradeStatus: UpgradeStatus.DOWNLOADING,
upgradeProgress: {
total,
delta,
transferred,
percent,
bytesPerSecond,
},
});
});
if (window.process.platform !== 'darwin') {
window.ipcRenderer.on(FOCUS_THREE_DOT_MENU, () => {
if (this.threeDotMenu.current) {
@@ -334,16 +379,11 @@ export default class MainPage extends React.PureComponent<Props, State> {
/>
);
let topBarClassName = 'topBar';
if (window.process.platform === 'darwin') {
topBarClassName += ' macOS';
}
if (this.state.darkMode) {
topBarClassName += ' darkMode';
}
if (this.state.fullScreen) {
topBarClassName += ' fullScreen';
}
const topBarClassName = classNames('topBar', {
macOS: window.process.platform === 'darwin',
darkMode: this.state.darkMode,
fullScreen: this.state.fullScreen,
});
let maxButton;
if (this.state.maximized || this.state.fullScreen) {
@@ -366,11 +406,46 @@ export default class MainPage extends React.PureComponent<Props, State> {
);
}
let overlayGradient;
if (window.process.platform !== 'darwin') {
overlayGradient = (
<span className='overlay-gradient'/>
);
let upgradeTooltip;
switch (this.state.upgradeStatus) {
case UpgradeStatus.AVAILABLE:
upgradeTooltip = 'Update available';
break;
case UpgradeStatus.DOWNLOADED:
upgradeTooltip = 'Update ready to install';
break;
case UpgradeStatus.DOWNLOADING:
upgradeTooltip = `Downloading update. ${String(this.state.upgradeProgress?.percent).split('.')[0]}% of ${prettyBytes(this.state.upgradeProgress?.total || 0)} @ ${prettyBytes(this.state.upgradeProgress?.bytesPerSecond || 0)}/s`;
break;
}
let upgradeIcon;
if (this.state.upgradeStatus !== UpgradeStatus.NONE) {
upgradeIcon = (
<span className={classNames('upgrade-btns', {darkMode: this.state.darkMode})}>
<div
className={classNames('button upgrade-button', {
rotate: this.state.upgradeStatus === UpgradeStatus.DOWNLOADING,
})}
title={upgradeTooltip}
onClick={() => {
if (this.state.upgradeStatus === UpgradeStatus.DOWNLOADING) {
return;
}
window.ipcRenderer.send(this.state.upgradeStatus === UpgradeStatus.DOWNLOADED ? START_UPGRADE : START_DOWNLOAD);
}}
>
<i
className={classNames({
'icon-arrow-down-bold-circle-outline': this.state.upgradeStatus === UpgradeStatus.AVAILABLE,
'icon-sync': this.state.upgradeStatus === UpgradeStatus.DOWNLOADING,
'icon-arrow-up-bold-circle-outline': this.state.upgradeStatus === UpgradeStatus.DOWNLOADED,
})}
/>
{(this.state.upgradeStatus !== UpgradeStatus.DOWNLOADING) && <div className={'circle'}/>}
</div>
</span>);
}
let titleBarButtons;
@@ -435,7 +510,7 @@ export default class MainPage extends React.PureComponent<Props, State> {
darkMode={this.state.darkMode}
/>
{tabsRow}
{overlayGradient}
{upgradeIcon}
{titleBarButtons}
</div>
</Row>

View File

@@ -24,14 +24,15 @@ import {
GET_DOWNLOAD_LOCATION,
RELOAD_CONFIGURATION,
GET_AVAILABLE_SPELL_CHECKER_LANGUAGES,
CHECK_FOR_UPDATES,
} from 'common/communication';
import AutoSaveIndicator, {SavingState} from './AutoSaveIndicator';
const CONFIG_TYPE_SERVERS = 'servers';
const CONFIG_TYPE_UPDATES = 'updates';
const CONFIG_TYPE_APP_OPTIONS = 'appOptions';
type ConfigType = typeof CONFIG_TYPE_SERVERS | typeof CONFIG_TYPE_APP_OPTIONS;
type ConfigType = typeof CONFIG_TYPE_UPDATES | typeof CONFIG_TYPE_APP_OPTIONS;
type State = DeepPartial<CombinedConfig> & {
ready: boolean;
@@ -40,11 +41,12 @@ type State = DeepPartial<CombinedConfig> & {
userOpenedDownloadDialog: boolean;
allowSaveSpellCheckerURL: boolean;
availableLanguages: Array<{label: string; value: string}>;
canUpgrade?: boolean;
}
type SavingStateItems = {
appOptions: SavingState;
servers: SavingState;
updates: SavingState;
};
type SaveQueueItem = {
@@ -66,6 +68,7 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
useSpellCheckerRef: React.RefObject<HTMLInputElement>;
spellCheckerURLRef: React.RefObject<HTMLInputElement>;
enableHardwareAccelerationRef: React.RefObject<HTMLInputElement>;
autoCheckForUpdatesRef: React.RefObject<HTMLInputElement>;
saveQueue: SaveQueueItem[];
@@ -77,7 +80,7 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
ready: false,
savingState: {
appOptions: SavingState.SAVING_STATE_DONE,
servers: SavingState.SAVING_STATE_DONE,
updates: SavingState.SAVING_STATE_DONE,
},
userOpenedDownloadDialog: false,
allowSaveSpellCheckerURL: false,
@@ -97,6 +100,7 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
this.useSpellCheckerRef = React.createRef();
this.enableHardwareAccelerationRef = React.createRef();
this.spellCheckerURLRef = React.createRef();
this.autoCheckForUpdatesRef = React.createRef();
this.saveQueue = [];
this.selectedSpellCheckerLocales = [];
@@ -125,7 +129,7 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
const newState = Object.assign({} as State, configData);
newState.savingState = currentState.savingState || {
appOptions: SavingState.SAVING_STATE_DONE,
servers: SavingState.SAVING_STATE_DONE,
updates: SavingState.SAVING_STATE_DONE,
};
this.selectedSpellCheckerLocales = configData.spellCheckerLocales?.map((language: string) => ({label: localeTranslations[language] || language, value: language})) || [];
return newState;
@@ -147,7 +151,7 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
updateSaveState = () => {
let queuedUpdateCounts = {
[CONFIG_TYPE_SERVERS]: 0,
[CONFIG_TYPE_UPDATES]: 0,
[CONFIG_TYPE_APP_OPTIONS]: 0,
};
@@ -284,6 +288,21 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
});
}
handleChangeAutoCheckForUpdates = () => {
window.timers.setImmediate(this.saveSetting, CONFIG_TYPE_UPDATES, {key: 'autoCheckForUpdates', data: this.autoCheckForUpdatesRef.current?.checked});
this.setState({
autoCheckForUpdates: this.autoCheckForUpdatesRef.current?.checked,
}, () => {
if (this.state.autoCheckForUpdates) {
this.checkForUpdates();
}
});
}
checkForUpdates = () => {
window.ipcRenderer.send(CHECK_FOR_UPDATES);
}
handleChangeSpellCheckerLocales = (value: OptionsType<{label: string; value: string}>, actionMeta: ActionMeta<{label: string; value: string}>) => {
switch (actionMeta.action) {
case 'select-option':
@@ -407,6 +426,12 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
container: {
paddingBottom: '40px',
},
checkForUpdatesButton: {
marginBottom: '4px',
marginLeft: '16px',
marginTop: '8px',
},
};
const options = [];
@@ -757,7 +782,7 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
<Row>
<Col md={12}>
<h2 style={settingsPage.sectionHeading}>{'App Options'}</h2>
<div className='IndicatorContainer'>
<div className='IndicatorContainer appOptionsSaveIndicator'>
<AutoSaveIndicator
id='appOptionsSaveIndicator'
savingState={this.state.savingState.appOptions}
@@ -774,9 +799,60 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
);
}
let updateRow = null;
if (this.state.canUpgrade) {
updateRow = (
<>
<Row>
<Col md={12}>
<h2 style={settingsPage.sectionHeading}>{'Updates'}</h2>
<div className='IndicatorContainer updatesSaveIndicator'>
<AutoSaveIndicator
id='updatesSaveIndicator'
savingState={this.state.savingState.updates}
errorMessage={'Can\'t save your changes. Please try again.'}
/>
</div>
<FormGroup
key='inputAutoCheckForUpdates'
>
<FormCheck>
<FormCheck.Input
type='checkbox'
key='inputAutoCheckForUpdates'
id='inputAutoCheckForUpdates'
ref={this.autoCheckForUpdatesRef}
checked={this.state.autoCheckForUpdates}
onChange={this.handleChangeAutoCheckForUpdates}
/>
{'Automatically check for updates'}
<FormText>
{'If enabled, updates to the Desktop App will download automatically and you will be notified when ready to install.'}
</FormText>
</FormCheck>
<Button
style={settingsPage.checkForUpdatesButton}
id='checkForUpdatesNow'
onClick={this.checkForUpdates}
>
<span>{'Check for Updates Now'}</span>
</Button>
</FormGroup>
</Col>
</Row>
<hr/>
</>
);
}
let waitForIpc;
if (this.state.ready) {
waitForIpc = optionsRow;
waitForIpc = (
<>
{updateRow}
{optionsRow}
</>
);
} else {
waitForIpc = (<p>{'Loading configuration...'}</p>);
}

View File

@@ -7,7 +7,6 @@ import React, {useEffect} from 'react';
import {CLOSE_TEAMS_DROPDOWN, OPEN_TEAMS_DROPDOWN} from 'common/communication';
import '../css/components/TeamDropdownButton.scss';
import '../css/compass-icons.css';
type Props = {
isDisabled?: boolean;