From abca4130bce1c8116816cdb6d5db7fe88f9fb838 Mon Sep 17 00:00:00 2001 From: Giacomo Cusinato <7659518+giacomocusinato@users.noreply.github.com> Date: Tue, 26 Nov 2024 19:01:07 +0100 Subject: [PATCH 1/3] feat: add donate footer to updater dialog --- .../ide-updater/ide-updater-dialog.tsx | 45 +++++++++++++++++++ .../src/browser/icons/link-open-icon.svg | 3 ++ .../src/browser/style/ide-updater-dialog.css | 32 +++++++++++++ i18n/en.json | 3 ++ 4 files changed, 83 insertions(+) create mode 100644 arduino-ide-extension/src/browser/icons/link-open-icon.svg diff --git a/arduino-ide-extension/src/browser/dialogs/ide-updater/ide-updater-dialog.tsx b/arduino-ide-extension/src/browser/dialogs/ide-updater/ide-updater-dialog.tsx index 0c6abbfaa..e73de643f 100644 --- a/arduino-ide-extension/src/browser/dialogs/ide-updater/ide-updater-dialog.tsx +++ b/arduino-ide-extension/src/browser/dialogs/ide-updater/ide-updater-dialog.tsx @@ -165,6 +165,50 @@ export class IDEUpdaterDialog extends ReactDialog { goToDownloadPageButton.focus(); } + private appendDonateFooter() { + const footer = document.createElement('div'); + footer.classList.add('ide-updater-dialog--footer'); + const footerContent = document.createElement('div'); + footerContent.classList.add('ide-updater-dialog--footer-content'); + footer.appendChild(footerContent); + + const footerLink = document.createElement('a'); + footerLink.innerText = nls.localize( + 'arduino/ide-updater/donateLinkText', + 'donate to support us' + ); + footerLink.classList.add('ide-updater-dialog--footer-link'); + footerLink.onclick = () => + this.openExternal('https://www.arduino.cc/en/donate'); + + const footerLinkIcon = document.createElement('span'); + footerLinkIcon.title = nls.localize( + 'arduino/ide-updater/donateLinkIconTitle', + 'open donation page' + ); + footerLinkIcon.classList.add('ide-updater-dialog--footer-link-icon'); + footerLink.appendChild(footerLinkIcon); + + const placeholderKey = '%%link%%'; + const footerText = nls.localize( + 'arduino/ide-updater/donateText', + 'Open source is love, {0}', + placeholderKey + ); + const placeholder = footerText.indexOf(placeholderKey); + if (placeholder !== -1) { + const parts = footerText.split(placeholderKey); + footerContent.appendChild(document.createTextNode(parts[0])); + footerContent.appendChild(footerLink); + footerContent.appendChild(document.createTextNode(parts[1])); + } else { + footerContent.appendChild(document.createTextNode(footerText)); + footerContent.appendChild(footerLink); + } + + this.controlPanel.insertAdjacentElement('afterend', footer); + } + private openDownloadPage(): void { this.openExternal('https://www.arduino.cc/en/software'); this.close(); @@ -187,6 +231,7 @@ export class IDEUpdaterDialog extends ReactDialog { downloadStarted: true, }); this.clearButtons(); + this.appendDonateFooter(); this.updater.downloadUpdate(); } diff --git a/arduino-ide-extension/src/browser/icons/link-open-icon.svg b/arduino-ide-extension/src/browser/icons/link-open-icon.svg new file mode 100644 index 000000000..21136c806 --- /dev/null +++ b/arduino-ide-extension/src/browser/icons/link-open-icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/arduino-ide-extension/src/browser/style/ide-updater-dialog.css b/arduino-ide-extension/src/browser/style/ide-updater-dialog.css index 5d5592baf..8d722c9d1 100644 --- a/arduino-ide-extension/src/browser/style/ide-updater-dialog.css +++ b/arduino-ide-extension/src/browser/style/ide-updater-dialog.css @@ -34,6 +34,37 @@ min-width: 0; } +.ide-updater-dialog--footer { + display: inline-block; + margin-top: -16px; + padding: 12px 0 24px 0; + border-top: 1px solid var(--theia-editorWidget-border); +} +.ide-updater-dialog--footer-content { + float: right; +} + +.ide-updater-dialog--footer-link { + display: inline-block; + color: var(--theia-textLink-foreground); + font-weight: 500; + line-height: 13px; +} +.ide-updater-dialog--footer-link:hover { + color: var(--theia-textLink-foreground); + cursor: pointer; +} +.ide-updater-dialog--footer-link-icon { + display: inline-block; + -webkit-mask: url(../icons/link-open-icon.svg) center no-repeat; + background-color: var(--theia-textLink-foreground); + height: 12px; + width: 12px; + cursor: pointer; + transform: translateY(2px); + margin-left: 4px; +} + .ide-updater-dialog .changelog { color: var(--theia-editor-foreground); background-color: var(--theia-editor-background); @@ -109,6 +140,7 @@ max-height: 100%; overflow: hidden; display: flex; + padding-bottom: 20px !important; } #ide-updater-dialog-container .skip-version-button { diff --git a/i18n/en.json b/i18n/en.json index 17cd55547..d7bd68b4f 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -275,6 +275,9 @@ "checkForUpdates": "Check for Arduino IDE Updates", "closeAndInstallButton": "Close and Install", "closeToInstallNotice": "Close the software and install the update on your machine.", + "donateLinkIconTitle": "open donation page", + "donateLinkText": "donate to support us", + "donateText": "Open source is love, {0}", "downloadButton": "Download", "downloadingNotice": "Downloading the latest version of the Arduino IDE.", "errorCheckingForUpdates": "Error while checking for Arduino IDE updates.\n{0}", From 86f41242866629c9974d790de7bb0da7817ec013 Mon Sep 17 00:00:00 2001 From: Giacomo Cusinato <7659518+giacomocusinato@users.noreply.github.com> Date: Tue, 26 Nov 2024 21:59:42 +0100 Subject: [PATCH 2/3] feat: introduce `VersionWelcomeDialog` Show donate dialog after the first time a first IDE version is loaded --- .../browser/arduino-ide-frontend-module.ts | 9 ++ .../contributions/check-for-ide-updates.ts | 58 +++++++++- .../dialogs/version-welcome-dialog.tsx | 104 ++++++++++++++++++ .../src/browser/style/index.css | 1 + .../browser/style/version-welcome-dialog.css | 7 ++ .../src/common/protocol/ide-updater.ts | 1 + i18n/en.json | 8 ++ 7 files changed, 186 insertions(+), 2 deletions(-) create mode 100644 arduino-ide-extension/src/browser/dialogs/version-welcome-dialog.tsx create mode 100644 arduino-ide-extension/src/browser/style/version-welcome-dialog.css diff --git a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts index 2b170dd7b..d6779c302 100644 --- a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts +++ b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts @@ -387,6 +387,10 @@ import { import { TreeViewDecoratorService } from '@theia/plugin-ext/lib/main/browser/view/tree-view-decorator-service'; import { PLUGIN_VIEW_DATA_FACTORY_ID } from '@theia/plugin-ext/lib/main/browser/view/plugin-view-registry'; import { TreeViewWidget } from './theia/plugin-ext/tree-view-widget'; +import { + VersionWelcomeDialog, + VersionWelcomeDialogProps, +} from './dialogs/version-welcome-dialog'; // Hack to fix copy/cut/paste issue after electron version update in Theia. // https://github.com/eclipse-theia/theia/issues/12487 @@ -1014,6 +1018,11 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { title: 'IDEUpdater', }); + bind(VersionWelcomeDialog).toSelf().inSingletonScope(); + bind(VersionWelcomeDialogProps).toConstantValue({ + title: 'VersionWelcomeDialog', + }); + bind(UserFieldsDialog).toSelf().inSingletonScope(); bind(UserFieldsDialogProps).toConstantValue({ title: 'UserFields', diff --git a/arduino-ide-extension/src/browser/contributions/check-for-ide-updates.ts b/arduino-ide-extension/src/browser/contributions/check-for-ide-updates.ts index ab5f62ac2..7ac57ac01 100644 --- a/arduino-ide-extension/src/browser/contributions/check-for-ide-updates.ts +++ b/arduino-ide-extension/src/browser/contributions/check-for-ide-updates.ts @@ -3,10 +3,14 @@ import { LocalStorageService } from '@theia/core/lib/browser/storage-service'; import { inject, injectable } from '@theia/core/shared/inversify'; import { IDEUpdater, + LAST_USED_IDE_VERSION, SKIP_IDE_VERSION, } from '../../common/protocol/ide-updater'; import { IDEUpdaterDialog } from '../dialogs/ide-updater/ide-updater-dialog'; import { Contribution } from './contribution'; +import { VersionWelcomeDialog } from '../dialogs/version-welcome-dialog'; +import { AppService } from '../app-service'; +import { SemVer } from 'semver'; @injectable() export class CheckForIDEUpdates extends Contribution { @@ -16,9 +20,15 @@ export class CheckForIDEUpdates extends Contribution { @inject(IDEUpdaterDialog) private readonly updaterDialog: IDEUpdaterDialog; + @inject(VersionWelcomeDialog) + private readonly versionWelcomeDialog: VersionWelcomeDialog; + @inject(LocalStorageService) private readonly localStorage: LocalStorageService; + @inject(AppService) + private readonly appService: AppService; + override onStart(): void { this.preferences.onPreferenceChanged( ({ preferenceName, newValue, oldValue }) => { @@ -36,7 +46,7 @@ export class CheckForIDEUpdates extends Contribution { ); } - override onReady(): void { + override async onReady(): Promise { this.updater .init( this.preferences.get('arduino.ide.updateChannel'), @@ -49,7 +59,13 @@ export class CheckForIDEUpdates extends Contribution { return this.updater.checkForUpdates(true); }) .then(async (updateInfo) => { - if (!updateInfo) return; + if (!updateInfo) { + const isNewVersion = await this.isNewStableVersion(); + if (isNewVersion) { + this.versionWelcomeDialog.open(); + } + return; + } const versionToSkip = await this.localStorage.getData( SKIP_IDE_VERSION ); @@ -64,6 +80,44 @@ export class CheckForIDEUpdates extends Contribution { e.message ) ); + }) + .finally(() => { + this.setCurrentIDEVersion(); }); } + + private async setCurrentIDEVersion(): Promise { + try { + const { appVersion } = await this.appService.info(); + const currSemVer = new SemVer(appVersion ?? ''); + this.localStorage.setData(LAST_USED_IDE_VERSION, currSemVer.format()); + } catch { + // ignore invalid versions + } + } + + /** + * Check if user is running a new IDE version for the first time. + * @returns true if the current IDE version is greater than the last used version + * and both are non-prerelease versions. + */ + private async isNewStableVersion(): Promise { + try { + const { appVersion } = await this.appService.info(); + const prevVersion = await this.localStorage.getData( + LAST_USED_IDE_VERSION + ); + + const prevSemVer = new SemVer(prevVersion ?? ''); + const currSemVer = new SemVer(appVersion ?? ''); + + if (prevSemVer.prerelease.length || currSemVer.prerelease.length) { + return false; + } + + return currSemVer.compare(prevSemVer) === 1; + } catch (e) { + return false; + } + } } diff --git a/arduino-ide-extension/src/browser/dialogs/version-welcome-dialog.tsx b/arduino-ide-extension/src/browser/dialogs/version-welcome-dialog.tsx new file mode 100644 index 000000000..6d5a71db0 --- /dev/null +++ b/arduino-ide-extension/src/browser/dialogs/version-welcome-dialog.tsx @@ -0,0 +1,104 @@ +import React from '@theia/core/shared/react'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { Message } from '@theia/core/shared/@phosphor/messaging'; +import { ReactDialog } from '../theia/dialogs/dialogs'; +import { nls } from '@theia/core'; +import { DialogProps } from '@theia/core/lib/browser'; +import { WindowService } from '@theia/core/lib/browser/window/window-service'; +import { AppService } from '../app-service'; + +@injectable() +export class VersionWelcomeDialogProps extends DialogProps {} + +@injectable() +export class VersionWelcomeDialog extends ReactDialog { + @inject(AppService) + private readonly appService: AppService; + + @inject(WindowService) + private readonly windowService: WindowService; + + constructor( + @inject(VersionWelcomeDialogProps) + protected override readonly props: VersionWelcomeDialogProps + ) { + super({ + title: nls.localize( + 'arduino/versionWelcome/title', + 'Welcome to a new version of the Arduino IDE!' + ), + }); + this.node.id = 'version-welcome-dialog-container'; + this.contentNode.classList.add('version-welcome-dialog'); + } + + protected render(): React.ReactNode { + return ( +
+

+ {nls.localize( + 'arduino/versionWelcome/donateMessage', + 'Arduino is committed to keeping software free and open-source for everyone. Your donation helps us develop new features, improve libraries, and support millions of users worldwide.' + )} +

+

+ {nls.localize( + 'arduino/versionWelcome/donateMessage2', + 'Please consider supporting our work on the free open source Arduino IDE.' + )} +

+
+ ); + } + + override get value(): void { + return; + } + + private appendButtons(): void { + const cancelButton = this.createButton( + nls.localize('arduino/versionWelcome/cancelButton', 'Maybe later') + ); + cancelButton.classList.add('secondary'); + cancelButton.classList.add('cancel-button'); + this.addAction(cancelButton, this.close.bind(this), 'click'); + this.controlPanel.appendChild(cancelButton); + + const donateButton = this.createButton( + nls.localize('arduino/versionWelcome/donateButton', 'Donate now') + ); + this.addAction(donateButton, this.onDonateButtonClick.bind(this), 'click'); + this.controlPanel.appendChild(donateButton); + donateButton.focus(); + } + + private onDonateButtonClick(): void { + this.openDonationPage(); + this.close(); + } + + private readonly openDonationPage = () => { + const url = 'https://www.arduino.cc/en/donate'; + this.windowService.openNewWindow(url, { external: true }); + }; + + private async updateTitleVersion(): Promise { + const appInfo = await this.appService.info(); + const { appVersion } = appInfo; + + if (appVersion) { + this.titleNode.innerHTML = nls.localize( + 'arduino/versionWelcome/titleWithVersion', + 'Welcome to the new Arduino IDE {0}!', + appVersion + ); + } + } + + protected override onAfterAttach(msg: Message): void { + this.update(); + this.appendButtons(); + this.updateTitleVersion(); + super.onAfterAttach(msg); + } +} diff --git a/arduino-ide-extension/src/browser/style/index.css b/arduino-ide-extension/src/browser/style/index.css index fd7887ae1..593cf1eaf 100644 --- a/arduino-ide-extension/src/browser/style/index.css +++ b/arduino-ide-extension/src/browser/style/index.css @@ -10,6 +10,7 @@ @import "./settings-dialog.css"; @import "./firmware-uploader-dialog.css"; @import "./ide-updater-dialog.css"; +@import "./version-welcome-dialog.css"; @import "./certificate-uploader-dialog.css"; @import "./user-fields-dialog.css"; @import "./debug.css"; diff --git a/arduino-ide-extension/src/browser/style/version-welcome-dialog.css b/arduino-ide-extension/src/browser/style/version-welcome-dialog.css new file mode 100644 index 000000000..80d6f71e4 --- /dev/null +++ b/arduino-ide-extension/src/browser/style/version-welcome-dialog.css @@ -0,0 +1,7 @@ +#version-welcome-dialog-container > .dialogBlock { + width: 546px; + + .bold { + font-weight: bold; + } +} diff --git a/arduino-ide-extension/src/common/protocol/ide-updater.ts b/arduino-ide-extension/src/common/protocol/ide-updater.ts index 5608e63be..ebc8bfef2 100644 --- a/arduino-ide-extension/src/common/protocol/ide-updater.ts +++ b/arduino-ide-extension/src/common/protocol/ide-updater.ts @@ -71,3 +71,4 @@ export interface IDEUpdaterClient { } export const SKIP_IDE_VERSION = 'skipIDEVersion'; +export const LAST_USED_IDE_VERSION = 'lastUsedIDEVersion'; diff --git a/i18n/en.json b/i18n/en.json index d7bd68b4f..b769cd868 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -528,6 +528,14 @@ "renameSketchFolderMessage": "The sketch '{0}' cannot be used. {1} To get rid of this message, rename the sketch. Do you want to rename the sketch now?", "renameSketchFolderTitle": "Invalid sketch name" }, + "versionWelcome": { + "cancelButton": "Maybe later", + "donateButton": "Donate now", + "donateMessage": "Arduino is committed to keeping software free and open-source for everyone. Your donation helps us develop new features, improve libraries, and support millions of users worldwide.", + "donateMessage2": "Please consider supporting our work on the free open source Arduino IDE.", + "title": "Welcome to a new version of the Arduino IDE!", + "titleWithVersion": "Welcome to the new Arduino IDE {0}!" + }, "workspace": { "alreadyExists": "'{0}' already exists." } From 08d83dc112c18ea16ff1424774dc050a8f8bb403 Mon Sep 17 00:00:00 2001 From: Giacomo Cusinato <7659518+giacomocusinato@users.noreply.github.com> Date: Mon, 2 Dec 2024 10:01:18 +0100 Subject: [PATCH 3/3] feat: use `dompurify` to sanitize translations Pin same version of `dompurify` used in Theia --- arduino-ide-extension/package.json | 1 + .../dialogs/ide-updater/ide-updater-dialog.tsx | 16 +++++++++------- .../browser/dialogs/version-welcome-dialog.tsx | 11 +++++++---- yarn.lock | 5 +++++ 4 files changed, 22 insertions(+), 11 deletions(-) diff --git a/arduino-ide-extension/package.json b/arduino-ide-extension/package.json index 6dfb8e35a..79d087742 100644 --- a/arduino-ide-extension/package.json +++ b/arduino-ide-extension/package.json @@ -67,6 +67,7 @@ "cross-fetch": "^3.1.5", "dateformat": "^3.0.3", "deepmerge": "^4.2.2", + "dompurify": "^2.4.7", "drivelist": "^9.2.4", "electron-updater": "^4.6.5", "fast-deep-equal": "^3.1.3", diff --git a/arduino-ide-extension/src/browser/dialogs/ide-updater/ide-updater-dialog.tsx b/arduino-ide-extension/src/browser/dialogs/ide-updater/ide-updater-dialog.tsx index e73de643f..0598b2853 100644 --- a/arduino-ide-extension/src/browser/dialogs/ide-updater/ide-updater-dialog.tsx +++ b/arduino-ide-extension/src/browser/dialogs/ide-updater/ide-updater-dialog.tsx @@ -17,6 +17,7 @@ import { } from '../../../common/protocol/ide-updater'; import { LocalStorageService } from '@theia/core/lib/browser'; import { WindowService } from '@theia/core/lib/browser/window/window-service'; +import { sanitize } from 'dompurify'; @injectable() export class IDEUpdaterDialogProps extends DialogProps {} @@ -173,9 +174,8 @@ export class IDEUpdaterDialog extends ReactDialog { footer.appendChild(footerContent); const footerLink = document.createElement('a'); - footerLink.innerText = nls.localize( - 'arduino/ide-updater/donateLinkText', - 'donate to support us' + footerLink.innerText = sanitize( + nls.localize('arduino/ide-updater/donateLinkText', 'donate to support us') ); footerLink.classList.add('ide-updater-dialog--footer-link'); footerLink.onclick = () => @@ -190,10 +190,12 @@ export class IDEUpdaterDialog extends ReactDialog { footerLink.appendChild(footerLinkIcon); const placeholderKey = '%%link%%'; - const footerText = nls.localize( - 'arduino/ide-updater/donateText', - 'Open source is love, {0}', - placeholderKey + const footerText = sanitize( + nls.localize( + 'arduino/ide-updater/donateText', + 'Open source is love, {0}', + placeholderKey + ) ); const placeholder = footerText.indexOf(placeholderKey); if (placeholder !== -1) { diff --git a/arduino-ide-extension/src/browser/dialogs/version-welcome-dialog.tsx b/arduino-ide-extension/src/browser/dialogs/version-welcome-dialog.tsx index 6d5a71db0..380d980df 100644 --- a/arduino-ide-extension/src/browser/dialogs/version-welcome-dialog.tsx +++ b/arduino-ide-extension/src/browser/dialogs/version-welcome-dialog.tsx @@ -6,6 +6,7 @@ import { nls } from '@theia/core'; import { DialogProps } from '@theia/core/lib/browser'; import { WindowService } from '@theia/core/lib/browser/window/window-service'; import { AppService } from '../app-service'; +import { sanitize } from 'dompurify'; @injectable() export class VersionWelcomeDialogProps extends DialogProps {} @@ -87,10 +88,12 @@ export class VersionWelcomeDialog extends ReactDialog { const { appVersion } = appInfo; if (appVersion) { - this.titleNode.innerHTML = nls.localize( - 'arduino/versionWelcome/titleWithVersion', - 'Welcome to the new Arduino IDE {0}!', - appVersion + this.titleNode.innerText = sanitize( + nls.localize( + 'arduino/versionWelcome/titleWithVersion', + 'Welcome to the new Arduino IDE {0}!', + appVersion + ) ); } } diff --git a/yarn.lock b/yarn.lock index f70c3a914..0c3367a1b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5937,6 +5937,11 @@ dompurify@^2.2.9: resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.4.7.tgz#277adeb40a2c84be2d42a8bcd45f582bfa4d0cfc" integrity sha512-kxxKlPEDa6Nc5WJi+qRgPbOAbgTpSULL+vI3NUXsZMlkJxTqYI9wg5ZTay2sFrdZRWHPWNi+EdAhcJf81WtoMQ== +dompurify@^2.4.7: + version "2.5.7" + resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.5.7.tgz#6e0d36b9177db5a99f18ade1f28579db5ab839d7" + integrity sha512-2q4bEI+coQM8f5ez7kt2xclg1XsecaV9ASJk/54vwlfRRNQfDqJz2pzQ8t0Ix/ToBpXlVjrRIx7pFC/o8itG2Q== + dot-case@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/dot-case/-/dot-case-3.0.4.tgz#9b2b670d00a431667a8a75ba29cd1b98809ce751"