diff --git a/app/src/lib/release-notes.ts b/app/src/lib/release-notes.ts index c9590800fee..6043c3a1250 100644 --- a/app/src/lib/release-notes.ts +++ b/app/src/lib/release-notes.ts @@ -10,6 +10,7 @@ import { getVersion } from '../ui/lib/app-proxy' import { formatDate } from './format-date' import { offsetFromNow } from './offset-from' import { encodePathAsUrl } from './path' +import { getUserAgent } from './http' // expects a release note entry to contain a header and then some text // example: @@ -102,7 +103,9 @@ export async function getChangeLog( changelogURL.searchParams.set('limit', limit.toString()) } - const response = await fetch(changelogURL.toString()) + const response = await fetch(changelogURL.toString(), { + headers: { 'user-agent': getUserAgent() }, + }) if (response.ok) { const releases: ReadonlyArray = await response.json() return releases diff --git a/app/src/lib/stats/stats-store.ts b/app/src/lib/stats/stats-store.ts index 27c99f8bb43..6bb2cb08189 100644 --- a/app/src/lib/stats/stats-store.ts +++ b/app/src/lib/stats/stats-store.ts @@ -36,6 +36,7 @@ import { isInApplicationFolder } from '../../ui/main-process-proxy' import { getRendererGUID } from '../get-renderer-guid' import { ValidNotificationPullRequestReviewState } from '../valid-notification-pull-request-review' import { useExternalCredentialHelperKey } from '../trampoline/use-external-credential-helper' +import { getUserAgent } from '../http' type PullRequestReviewStatFieldInfix = | 'Approved' @@ -424,7 +425,10 @@ export interface IStatsStore { const defaultPostImplementation = (body: Record) => fetch(StatsEndpoint, { method: 'POST', - headers: new Headers({ 'Content-Type': 'application/json' }), + headers: { + 'Content-Type': 'application/json', + 'user-agent': getUserAgent(), + }, body: JSON.stringify(body), }) diff --git a/app/src/main-process/menu/build-test-menu.ts b/app/src/main-process/menu/build-test-menu.ts index bad757febf4..0026f449e83 100644 --- a/app/src/main-process/menu/build-test-menu.ts +++ b/app/src/main-process/menu/build-test-menu.ts @@ -146,6 +146,10 @@ export function buildTestMenu() { label: 'Update banner', click: emit('test-update-banner'), }, + { + label: 'Update banner (priority)', + click: emit('test-prioritized-update-banner'), + }, { label: `Showcase Update banner`, click: emit('test-showcase-update-banner'), diff --git a/app/src/main-process/menu/menu-event.ts b/app/src/main-process/menu/menu-event.ts index 78dc361f0a2..93ca7880b07 100644 --- a/app/src/main-process/menu/menu-event.ts +++ b/app/src/main-process/menu/menu-event.ts @@ -82,6 +82,7 @@ const TestMenuEvents = [ 'test-undone-banner', 'test-untrusted-server', 'test-update-banner', + 'test-prioritized-update-banner', 'test-update-existing-git-lfs-filters', 'test-upstream-already-exists', ] as const diff --git a/app/src/ui/app.tsx b/app/src/ui/app.tsx index a636924ed98..db2cc8fe977 100644 --- a/app/src/ui/app.tsx +++ b/app/src/ui/app.tsx @@ -3130,6 +3130,8 @@ export class App extends React.Component { isX64ToARM64ImmediateAutoUpdate={ updateStore.state.isX64ToARM64ImmediateAutoUpdate } + prioritizeUpdate={updateStore.state.prioritizeUpdate} + prioritizeUpdateInfoUrl={updateStore.state.prioritizeUpdateInfoUrl} onDismissed={this.onUpdateAvailableDismissed} isUpdateShowcaseVisible={this.state.isUpdateShowcaseVisible} emoji={this.state.emoji} diff --git a/app/src/ui/banners/banner.tsx b/app/src/ui/banners/banner.tsx index 8b768b0a552..9c3762ae986 100644 --- a/app/src/ui/banners/banner.tsx +++ b/app/src/ui/banners/banner.tsx @@ -1,11 +1,13 @@ import * as React from 'react' import { Octicon } from '../octicons' import * as octicons from '../octicons/octicons.generated' +import classNames from 'classnames' interface IBannerProps { readonly id?: string readonly timeout?: number readonly dismissable?: boolean + readonly className?: string readonly onDismissed: () => void } @@ -19,8 +21,9 @@ export class Banner extends React.Component { private dismissalTimeoutId: number | null = null public render() { + const cn = classNames('banner', this.props.className) return ( -
+
{this.props.children}
{this.renderCloseButton()}
diff --git a/app/src/ui/banners/update-available.tsx b/app/src/ui/banners/update-available.tsx index fc1bbea495f..ea0bd6c14dd 100644 --- a/app/src/ui/banners/update-available.tsx +++ b/app/src/ui/banners/update-available.tsx @@ -24,31 +24,43 @@ interface IUpdateAvailableProps { readonly isUpdateShowcaseVisible: boolean readonly emoji: Map readonly onDismissed: () => void + readonly prioritizeUpdate: boolean + readonly prioritizeUpdateInfoUrl: string | undefined } /** * A component which tells the user an update is available and gives them the * option of moving into the future or being a luddite. */ -export class UpdateAvailable extends React.Component< - IUpdateAvailableProps, - {} -> { +export class UpdateAvailable extends React.Component { public render() { return ( - - {!this.props.isUpdateShowcaseVisible && ( - - )} - + + {this.renderIcon()} {this.renderMessage()} ) } + private renderIcon() { + if (this.props.isUpdateShowcaseVisible) { + return null + } + + if (this.props.prioritizeUpdate) { + return + } + + return ( + + ) + } + private renderMessage = () => { if (this.props.isX64ToARM64ImmediateAutoUpdate) { return ( @@ -87,6 +99,26 @@ export class UpdateAvailable extends React.Component< ) } + if (this.props.prioritizeUpdate) { + return ( + + This version of GitHub Desktop is missing{' '} + {this.props.prioritizeUpdateInfoUrl ? ( + + important updates + + ) : ( + 'important updates' + )} + . Please{' '} + + restart GitHub Desktop + {' '} + now to install pending updates. + + ) + } + return ( An updated version of GitHub Desktop is available and will be installed diff --git a/app/src/ui/lib/test-ui-components/test-ui-components.ts b/app/src/ui/lib/test-ui-components/test-ui-components.ts index 0db4c6dab15..8bba75729dc 100644 --- a/app/src/ui/lib/test-ui-components/test-ui-components.ts +++ b/app/src/ui/lib/test-ui-components/test-ui-components.ts @@ -156,6 +156,11 @@ export function showTestUI( }) case 'test-update-banner': return showFakeUpdateBanner({}) + case 'test-prioritized-update-banner': + return showFakeUpdateBanner({ + isPriority: true, + priorityInfoUrl: 'https://desktop.github.com', + }) case 'test-update-existing-git-lfs-filters': return dispatcher.showPopup({ type: PopupType.LFSAttributeMismatch }) case 'test-upstream-already-exists': @@ -179,6 +184,8 @@ export function showTestUI( function showFakeUpdateBanner(options: { isArm64?: boolean isShowcase?: boolean + isPriority?: boolean + priorityInfoUrl?: string }) { updateStore.setIsx64ToARM64ImmediateAutoUpdate(options.isArm64 === true) @@ -187,6 +194,12 @@ export function showTestUI( return } + if (options.isPriority !== undefined) { + updateStore.setPrioritizeUpdate(options.isPriority) + } + + updateStore.setPrioritizeUpdateInfoUrl(options.priorityInfoUrl) + dispatcher.setUpdateBannerVisibility(true) } diff --git a/app/src/ui/lib/update-store.ts b/app/src/ui/lib/update-store.ts index 00b2b9728d6..ce26a5ab4fd 100644 --- a/app/src/ui/lib/update-store.ts +++ b/app/src/ui/lib/update-store.ts @@ -23,6 +23,7 @@ import { enableUpdateFromEmulatedX64ToARM64 } from '../../lib/feature-flag' import { offsetFromNow } from '../../lib/offset-from' import { gte, SemVer } from 'semver' import { getVersion } from './app-proxy' +import { getUserAgent } from '../../lib/http' /** The last version a showcase was seen. */ export const lastShowCaseVersionSeen = 'version-of-last-showcase' @@ -50,6 +51,8 @@ export interface IUpdateState { lastSuccessfulCheck: Date | null isX64ToARM64ImmediateAutoUpdate: boolean newReleases: ReadonlyArray | null + prioritizeUpdate: boolean + prioritizeUpdateInfoUrl: string | undefined } /** A store which contains the current state of the auto updater. */ @@ -62,6 +65,16 @@ class UpdateStore { /** Is the most recent update check user initiated? */ private userInitiatedUpdate = true + private _prioritizeUpdate = false + private _prioritizeUpdateInfoUrl: string | undefined = undefined + + public get prioritizeUpdate() { + return this._prioritizeUpdate + } + + public get prioritizeUpdateInfoUrl() { + return this._prioritizeUpdateInfoUrl + } public constructor() { const lastSuccessfulCheckTime = getNumber(lastSuccessfulCheckKey, 0) @@ -127,6 +140,8 @@ class UpdateStore { (await isRunningUnderARM64Translation()) this.status = UpdateStatus.UpdateReady this.emitDidChange() + + this.updatePriorityUpdateStatus() } /** @@ -168,6 +183,8 @@ class UpdateStore { lastSuccessfulCheck: this.lastSuccessfulCheck, newReleases: this.newReleases, isX64ToARM64ImmediateAutoUpdate: this.isX64ToARM64ImmediateAutoUpdate, + prioritizeUpdate: this.prioritizeUpdate, + prioritizeUpdateInfoUrl: this.prioritizeUpdateInfoUrl, } } @@ -187,6 +204,7 @@ class UpdateStore { // button to crash the app if in the subsequent check, there is no update // available anymore due to a disabled update. if (this.status === UpdateStatus.UpdateReady) { + this.updatePriorityUpdateStatus() return } @@ -252,6 +270,32 @@ class UpdateStore { quitAndInstallUpdate() } + private async updatePriorityUpdateStatus() { + try { + const response = await fetch(await this.getUpdatesUrl(false), { + method: 'HEAD', + headers: { 'user-agent': getUserAgent() }, + }) + + const prioritizeUpdate = + response.headers.get('x-prioritize-update') === 'true' + + const prioritizeUpdateInfoUrl = + response.headers.get('x-prioritize-update-info-url') ?? undefined + + if ( + this._prioritizeUpdate !== prioritizeUpdate || + this._prioritizeUpdateInfoUrl !== prioritizeUpdateInfoUrl + ) { + this._prioritizeUpdate = prioritizeUpdate + this._prioritizeUpdateInfoUrl = prioritizeUpdateInfoUrl + this.emitDidChange() + } + } catch (e) { + log.error('Error updating priority update status', e) + } + } + /** * Method to determine if we should show an update showcase call to action. * @@ -302,6 +346,32 @@ class UpdateStore { this.isX64ToARM64ImmediateAutoUpdate = value } + + /** This method has only been added for ease of testing the update banner in + * this state and as such is limite to dev and test environments */ + public setPrioritizeUpdate(value: boolean) { + if ( + __RELEASE_CHANNEL__ !== 'development' && + __RELEASE_CHANNEL__ !== 'test' + ) { + return + } + + this._prioritizeUpdate = value + } + + /** This method has only been added for ease of testing the update banner in + * this state and as such is limite to dev and test environments */ + public setPrioritizeUpdateInfoUrl(value: string | undefined) { + if ( + __RELEASE_CHANNEL__ !== 'development' && + __RELEASE_CHANNEL__ !== 'test' + ) { + return + } + + this._prioritizeUpdateInfoUrl = value + } } /** The store which contains the current state of the auto updater. */ diff --git a/app/styles/_variables.scss b/app/styles/_variables.scss index 495b88deb1e..98fe19c9a71 100644 --- a/app/styles/_variables.scss +++ b/app/styles/_variables.scss @@ -477,6 +477,12 @@ $overlay-background-color: rgba(0, 0, 0, 0.4); --dialog-information-color: #{$blue-400}; --dialog-error-color: #{$red}; + /** Banner */ + --banner-warning-background: #{$yellow-100}; + --banner-warning-text-color: var(--text-color); + --banner-warning-link-color: #046ee7; + --banner-warning-icon-color: #{darken($yellow-700, 10%)}; + /** File warning */ --file-warning-background-color: #{$yellow-100}; --file-warning-color: #{darken($yellow-700, 10%)}; diff --git a/app/styles/themes/_dark.scss b/app/styles/themes/_dark.scss index 55bedc8ac5b..290c97ad9ce 100644 --- a/app/styles/themes/_dark.scss +++ b/app/styles/themes/_dark.scss @@ -376,6 +376,12 @@ body.theme-dark { --dialog-information-color: #{$blue-400}; --dialog-error-color: #{$red-600}; + /** Banner */ + --banner-warning-background: #272216; + --banner-warning-text-color: var(--text-color); + --banner-warning-link-color: #6682ff; + --banner-warning-icon-color: #{$yellow-700}; + /** File warning */ --file-warning-background-color: #{rgba($yellow-900, 0.4)}; --file-warning-color: #{$yellow-700}; diff --git a/app/styles/ui/banners/_update-available.scss b/app/styles/ui/banners/_update-available.scss index 2e1abd9aedf..c0d1e36073a 100644 --- a/app/styles/ui/banners/_update-available.scss +++ b/app/styles/ui/banners/_update-available.scss @@ -1,10 +1,21 @@ #update-available { - .download-icon { + .download-icon, + .warning-icon { margin-right: var(--spacing); + color: var(--banner-warning-icon-color); } .banner-emoji { display: inline-block; margin-right: var(--spacing-half); } + + &.priority { + background-color: var(--banner-warning-background); + color: var(--banner-warning-text-color); + + a { + color: var(--banner-warning-link-color); + } + } }