From 3bb1ca7825a0381ddd8dbe7f44f7dcf4a788b165 Mon Sep 17 00:00:00 2001 From: André Oliveira <37463445+SpecialAro@users.noreply.github.com> Date: Tue, 19 Jul 2022 12:52:31 +0100 Subject: Feature: Add Release Notes (#491) Co-authored-by: Vijay A Co-authored-by: Ricardo Cino --- package-lock.json | 252 +++++++++++++++++++++ package.json | 2 + src/components/AppUpdateInfoBar.tsx | 23 +- src/components/auth/AuthLayout.jsx | 5 +- src/components/layout/AppLayout.js | 216 ------------------ src/components/layout/AppLayout.jsx | 226 ++++++++++++++++++ src/components/settings/SettingsLayout.jsx | 4 +- .../settings/navigation/SettingsNavigation.jsx | 24 +- .../releaseNotes/ReleaseNotesDashboard.tsx | 99 ++++++++ .../settings/releaseNotes/ReleaseNotesLayout.tsx | 79 +++++++ .../settings/settings/EditSettingsForm.jsx | 31 ++- src/components/ui/effects/Appear.tsx | 10 +- src/containers/auth/AuthLayoutContainer.tsx | 1 + src/containers/auth/AuthReleaseNotesScreen.tsx | 107 +++++++++ src/containers/layout/AppLayoutContainer.tsx | 1 + src/containers/settings/EditSettingsScreen.tsx | 3 +- src/containers/settings/ReleaseNotesScreen.tsx | 16 ++ src/containers/settings/ReleaseNotesWindow.tsx | 42 ++++ .../workspaces/components/WorkspaceItem.tsx | 10 +- src/helpers/update-helpers.ts | 65 ++++++ src/i18n/locales/en-US.json | 5 + src/jsUtils.ts | 2 + src/lib/Menu.js | 12 +- src/routes.tsx | 22 ++ src/stores/AppStore.ts | 6 +- src/styles/auth.scss | 59 +++++ src/styles/settings.scss | 95 ++++++++ test/helpers/update-helpers.test.ts | 89 ++++++++ test/jsUtils.test.ts | 12 + tsconfig.json | 3 +- 30 files changed, 1262 insertions(+), 259 deletions(-) delete mode 100644 src/components/layout/AppLayout.js create mode 100644 src/components/layout/AppLayout.jsx create mode 100644 src/components/settings/releaseNotes/ReleaseNotesDashboard.tsx create mode 100644 src/components/settings/releaseNotes/ReleaseNotesLayout.tsx create mode 100644 src/containers/auth/AuthReleaseNotesScreen.tsx create mode 100644 src/containers/settings/ReleaseNotesScreen.tsx create mode 100644 src/containers/settings/ReleaseNotesWindow.tsx create mode 100644 src/helpers/update-helpers.ts create mode 100644 test/helpers/update-helpers.test.ts diff --git a/package-lock.json b/package-lock.json index ae32a4ec9..e191b216b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "@krisdages/electron-process-manager": "3.0.0", "@mdi/js": "6.9.96", "@mdi/react": "1.6.0", + "@octokit/core": "4.0.4", "@superwf/mobx-react-router": "7.4.0", "auto-launch": "5.0.5", "btoa": "1.2.1", @@ -47,6 +48,7 @@ "languagedetect": "2.0.0", "lodash": "4.17.21", "macos-version": "5.2.1", + "markdown-to-jsx": "7.1.7", "mime-types": "2.1.35", "minimist": "1.2.6", "mobx": "6.6.1", @@ -4354,6 +4356,118 @@ "node": ">=10" } }, + "node_modules/@octokit/auth-token": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-3.0.0.tgz", + "integrity": "sha512-MDNFUBcJIptB9At7HiV7VCvU3NcL4GnfCQaP8C5lrxWrRPMJBnemYtehaKSOlaM7AYxeRyj9etenu8LVpSpVaQ==", + "dependencies": { + "@octokit/types": "^6.0.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@octokit/core": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-4.0.4.tgz", + "integrity": "sha512-sUpR/hc4Gc7K34o60bWC7WUH6Q7T6ftZ2dUmepSyJr9PRF76/qqkWjE2SOEzCqLA5W83SaISymwKtxks+96hPQ==", + "dependencies": { + "@octokit/auth-token": "^3.0.0", + "@octokit/graphql": "^5.0.0", + "@octokit/request": "^6.0.0", + "@octokit/request-error": "^3.0.0", + "@octokit/types": "^6.0.3", + "before-after-hook": "^2.2.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@octokit/endpoint": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-7.0.0.tgz", + "integrity": "sha512-Kz/mIkOTjs9rV50hf/JK9pIDl4aGwAtT8pry6Rpy+hVXkAPhXanNQRxMoq6AeRgDCZR6t/A1zKniY2V1YhrzlQ==", + "dependencies": { + "@octokit/types": "^6.0.3", + "is-plain-object": "^5.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@octokit/endpoint/node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@octokit/graphql": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-5.0.0.tgz", + "integrity": "sha512-1ZZ8tX4lUEcLPvHagfIVu5S2xpHYXAmgN0+95eAOPoaVPzCfUXJtA5vASafcpWcO86ze0Pzn30TAx72aB2aguQ==", + "dependencies": { + "@octokit/request": "^6.0.0", + "@octokit/types": "^6.0.3", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@octokit/openapi-types": { + "version": "12.9.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-12.9.0.tgz", + "integrity": "sha512-x0wjPEnD487oMjODOSIDdVNBebyrAPE4edY0bsxp/ZX1XPPnWQWXseixbhMa5KcwpbHVdk4qbC3zzedoMdP/YQ==" + }, + "node_modules/@octokit/request": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-6.2.0.tgz", + "integrity": "sha512-7IAmHnaezZrgUqtRShMlByJK33MT9ZDnMRgZjnRrRV9a/jzzFwKGz0vxhFU6i7VMLraYcQ1qmcAOin37Kryq+Q==", + "dependencies": { + "@octokit/endpoint": "^7.0.0", + "@octokit/request-error": "^3.0.0", + "@octokit/types": "^6.16.1", + "is-plain-object": "^5.0.0", + "node-fetch": "^2.6.7", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@octokit/request-error": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-3.0.0.tgz", + "integrity": "sha512-WBtpzm9lR8z4IHIMtOqr6XwfkGvMOOILNLxsWvDwtzm/n7f5AWuqJTXQXdDtOvPfTDrH4TPhEvW2qMlR4JFA2w==", + "dependencies": { + "@octokit/types": "^6.0.3", + "deprecation": "^2.0.0", + "once": "^1.4.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@octokit/request/node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@octokit/types": { + "version": "6.39.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.39.0.tgz", + "integrity": "sha512-Mq4N9sOAYCitTsBtDdRVrBE80lIrMBhL9Jbrw0d+j96BAzlq4V+GLHFJbHokEsVvO/9tQupQdoFdgVYhD2C8UQ==", + "dependencies": { + "@octokit/openapi-types": "^12.7.0" + } + }, "node_modules/@sideway/address": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.4.tgz", @@ -7150,6 +7264,11 @@ "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==" }, + "node_modules/before-after-hook": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.2.tgz", + "integrity": "sha512-3pZEU3NT5BFUo/AD5ERPWOgQOCZITni6iavr5AUw5AUwQjMlI0kzu5btnyD39AF0gUEsDPwJT+oY1ORBJijPjQ==" + }, "node_modules/big-integer": { "version": "1.6.51", "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", @@ -9599,6 +9718,11 @@ "node": ">= 0.6" } }, + "node_modules/deprecation": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", + "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==" + }, "node_modules/destroy": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", @@ -18159,6 +18283,17 @@ "node": ">=0.10.0" } }, + "node_modules/markdown-to-jsx": { + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/markdown-to-jsx/-/markdown-to-jsx-7.1.7.tgz", + "integrity": "sha512-VI3TyyHlGkO8uFle0IOibzpO1c1iJDcXcS/zBrQrXQQvJ2tpdwVzVZ7XdKsyRz1NdRmre4dqQkMZzUHaKIG/1w==", + "engines": { + "node": ">= 10" + }, + "peerDependencies": { + "react": ">= 0.14.0" + } + }, "node_modules/matchdep": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/matchdep/-/matchdep-2.0.0.tgz", @@ -25483,6 +25618,11 @@ "node": ">=8" } }, + "node_modules/universal-user-agent": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz", + "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==" + }, "node_modules/universalify": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", @@ -30464,6 +30604,98 @@ } } }, + "@octokit/auth-token": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-3.0.0.tgz", + "integrity": "sha512-MDNFUBcJIptB9At7HiV7VCvU3NcL4GnfCQaP8C5lrxWrRPMJBnemYtehaKSOlaM7AYxeRyj9etenu8LVpSpVaQ==", + "requires": { + "@octokit/types": "^6.0.3" + } + }, + "@octokit/core": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-4.0.4.tgz", + "integrity": "sha512-sUpR/hc4Gc7K34o60bWC7WUH6Q7T6ftZ2dUmepSyJr9PRF76/qqkWjE2SOEzCqLA5W83SaISymwKtxks+96hPQ==", + "requires": { + "@octokit/auth-token": "^3.0.0", + "@octokit/graphql": "^5.0.0", + "@octokit/request": "^6.0.0", + "@octokit/request-error": "^3.0.0", + "@octokit/types": "^6.0.3", + "before-after-hook": "^2.2.0", + "universal-user-agent": "^6.0.0" + } + }, + "@octokit/endpoint": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-7.0.0.tgz", + "integrity": "sha512-Kz/mIkOTjs9rV50hf/JK9pIDl4aGwAtT8pry6Rpy+hVXkAPhXanNQRxMoq6AeRgDCZR6t/A1zKniY2V1YhrzlQ==", + "requires": { + "@octokit/types": "^6.0.3", + "is-plain-object": "^5.0.0", + "universal-user-agent": "^6.0.0" + }, + "dependencies": { + "is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==" + } + } + }, + "@octokit/graphql": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-5.0.0.tgz", + "integrity": "sha512-1ZZ8tX4lUEcLPvHagfIVu5S2xpHYXAmgN0+95eAOPoaVPzCfUXJtA5vASafcpWcO86ze0Pzn30TAx72aB2aguQ==", + "requires": { + "@octokit/request": "^6.0.0", + "@octokit/types": "^6.0.3", + "universal-user-agent": "^6.0.0" + } + }, + "@octokit/openapi-types": { + "version": "12.9.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-12.9.0.tgz", + "integrity": "sha512-x0wjPEnD487oMjODOSIDdVNBebyrAPE4edY0bsxp/ZX1XPPnWQWXseixbhMa5KcwpbHVdk4qbC3zzedoMdP/YQ==" + }, + "@octokit/request": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-6.2.0.tgz", + "integrity": "sha512-7IAmHnaezZrgUqtRShMlByJK33MT9ZDnMRgZjnRrRV9a/jzzFwKGz0vxhFU6i7VMLraYcQ1qmcAOin37Kryq+Q==", + "requires": { + "@octokit/endpoint": "^7.0.0", + "@octokit/request-error": "^3.0.0", + "@octokit/types": "^6.16.1", + "is-plain-object": "^5.0.0", + "node-fetch": "^2.6.7", + "universal-user-agent": "^6.0.0" + }, + "dependencies": { + "is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==" + } + } + }, + "@octokit/request-error": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-3.0.0.tgz", + "integrity": "sha512-WBtpzm9lR8z4IHIMtOqr6XwfkGvMOOILNLxsWvDwtzm/n7f5AWuqJTXQXdDtOvPfTDrH4TPhEvW2qMlR4JFA2w==", + "requires": { + "@octokit/types": "^6.0.3", + "deprecation": "^2.0.0", + "once": "^1.4.0" + } + }, + "@octokit/types": { + "version": "6.39.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.39.0.tgz", + "integrity": "sha512-Mq4N9sOAYCitTsBtDdRVrBE80lIrMBhL9Jbrw0d+j96BAzlq4V+GLHFJbHokEsVvO/9tQupQdoFdgVYhD2C8UQ==", + "requires": { + "@octokit/openapi-types": "^12.7.0" + } + }, "@sideway/address": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.4.tgz", @@ -32721,6 +32953,11 @@ "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==" }, + "before-after-hook": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.2.tgz", + "integrity": "sha512-3pZEU3NT5BFUo/AD5ERPWOgQOCZITni6iavr5AUw5AUwQjMlI0kzu5btnyD39AF0gUEsDPwJT+oY1ORBJijPjQ==" + }, "big-integer": { "version": "1.6.51", "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", @@ -34642,6 +34879,11 @@ "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==" }, + "deprecation": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", + "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==" + }, "destroy": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", @@ -41317,6 +41559,11 @@ "object-visit": "^1.0.0" } }, + "markdown-to-jsx": { + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/markdown-to-jsx/-/markdown-to-jsx-7.1.7.tgz", + "integrity": "sha512-VI3TyyHlGkO8uFle0IOibzpO1c1iJDcXcS/zBrQrXQQvJ2tpdwVzVZ7XdKsyRz1NdRmre4dqQkMZzUHaKIG/1w==" + }, "matchdep": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/matchdep/-/matchdep-2.0.0.tgz", @@ -47045,6 +47292,11 @@ "crypto-random-string": "^2.0.0" } }, + "universal-user-agent": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz", + "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==" + }, "universalify": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", diff --git a/package.json b/package.json index 2d2c5c6da..2ff08af3e 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ "@krisdages/electron-process-manager": "3.0.0", "@mdi/js": "6.9.96", "@mdi/react": "1.6.0", + "@octokit/core": "4.0.4", "@superwf/mobx-react-router": "7.4.0", "auto-launch": "5.0.5", "btoa": "1.2.1", @@ -82,6 +83,7 @@ "languagedetect": "2.0.0", "lodash": "4.17.21", "macos-version": "5.2.1", + "markdown-to-jsx": "7.1.7", "mime-types": "2.1.35", "minimist": "1.2.6", "mobx": "6.6.1", diff --git a/src/components/AppUpdateInfoBar.tsx b/src/components/AppUpdateInfoBar.tsx index 48efbfb7f..3ff488b74 100644 --- a/src/components/AppUpdateInfoBar.tsx +++ b/src/components/AppUpdateInfoBar.tsx @@ -2,10 +2,10 @@ import { defineMessages, useIntl } from 'react-intl'; import { mdiInformation } from '@mdi/js'; import InfoBar from './ui/InfoBar'; -import { GITHUB_FERDIUM_URL } from '../config'; -import { openExternalUrl } from '../helpers/url-helpers'; import Icon from './ui/icon'; +import { onAuthGoToReleaseNotes } from '../helpers/update-helpers'; + const messages = defineMessages({ updateAvailable: { id: 'infobar.updateAvailable', @@ -24,9 +24,14 @@ const messages = defineMessages({ type Props = { onInstallUpdate: () => void; onHide: () => void; + updateVersionParsed: string; }; -const AppUpdateInfoBar = ({ onInstallUpdate, onHide }: Props) => { +const AppUpdateInfoBar = ({ + onInstallUpdate, + updateVersionParsed, + onHide, +}: Props) => { const intl = useIntl(); return ( @@ -41,12 +46,12 @@ const AppUpdateInfoBar = ({ onInstallUpdate, onHide }: Props) => { diff --git a/src/components/auth/AuthLayout.jsx b/src/components/auth/AuthLayout.jsx index 8a88cedb1..5c87c3080 100644 --- a/src/components/auth/AuthLayout.jsx +++ b/src/components/auth/AuthLayout.jsx @@ -14,6 +14,7 @@ import { oneOrManyChildElements, globalError as globalErrorPropType, } from '../../prop-types'; +import { updateVersionParse } from '../../helpers/update-helpers'; import globalMessages from '../../i18n/globalMessages'; import { isWindows } from '../../environment'; @@ -34,6 +35,7 @@ class AuthLayout extends Component { isFullScreen: PropTypes.bool.isRequired, installAppUpdate: PropTypes.func.isRequired, appUpdateIsDownloaded: PropTypes.bool.isRequired, + updateVersion: PropTypes.string.isRequired, }; constructor() { @@ -55,10 +57,10 @@ class AuthLayout extends Component { isFullScreen, installAppUpdate, appUpdateIsDownloaded, + updateVersion, } = this.props; const { intl } = this.props; - let serverNameParse = serverName(); serverNameParse = serverNameParse === 'Custom' ? 'your Custom Server' : serverNameParse; @@ -81,6 +83,7 @@ class AuthLayout extends Component { {appUpdateIsDownloaded && this.state.shouldShowAppUpdateInfoBar && ( { this.setState({ shouldShowAppUpdateInfoBar: false }); }} diff --git a/src/components/layout/AppLayout.js b/src/components/layout/AppLayout.js deleted file mode 100644 index f7860afc6..000000000 --- a/src/components/layout/AppLayout.js +++ /dev/null @@ -1,216 +0,0 @@ -import { Component } from 'react'; -import PropTypes from 'prop-types'; -import { observer } from 'mobx-react'; -import { defineMessages, injectIntl } from 'react-intl'; -import { TitleBar } from 'electron-react-titlebar/renderer'; -import injectSheet from 'react-jss'; -import { ipcRenderer } from 'electron'; - -import { mdiFlash, mdiPowerPlug } from '@mdi/js'; -import { Outlet } from 'react-router-dom'; -import InfoBar from '../ui/InfoBar'; -import { Component as BasicAuth } from '../../features/basicAuth'; -import { Component as QuickSwitch } from '../../features/quickSwitch'; -import { Component as PublishDebugInfo } from '../../features/publishDebugInfo'; -import ErrorBoundary from '../util/ErrorBoundary'; - -// import globalMessages from '../../i18n/globalMessages'; - -import { isWindows, isMac } from '../../environment'; -import WorkspaceSwitchingIndicator from '../../features/workspaces/components/WorkspaceSwitchingIndicator'; -import { workspaceStore } from '../../features/workspaces'; -import AppUpdateInfoBar from '../AppUpdateInfoBar'; -import Todos from '../../features/todos/containers/TodosScreen'; -import Icon from '../ui/icon'; - -import LockedScreen from '../../containers/auth/LockedScreen'; - -const messages = defineMessages({ - servicesUpdated: { - id: 'infobar.servicesUpdated', - defaultMessage: 'Your services have been updated.', - }, - buttonReloadServices: { - id: 'infobar.buttonReloadServices', - defaultMessage: 'Reload services', - }, - requiredRequestsFailed: { - id: 'infobar.requiredRequestsFailed', - defaultMessage: 'Could not load services and user information', - }, - authRequestFailed: { - id: 'infobar.authRequestFailed', - defaultMessage: - 'There were errors while trying to perform an authenticated request. Please try logging out and back in if this error persists.', - }, -}); - -let transition = 'none'; - -if (window && window.matchMedia('(prefers-reduced-motion: no-preference)')) { - transition = 'transform 0.5s ease'; -} - -const styles = theme => ({ - appContent: { - // width: `calc(100% + ${theme.workspaces.drawer.width}px)`, - width: '100%', - transition, - transform() { - return workspaceStore.isWorkspaceDrawerOpen - ? 'translateX(0)' - : `translateX(-${theme.workspaces.drawer.width}px)`; - }, - }, - titleBar: { - display: 'block', - zIndex: 1, - width: '100%', - height: '10px', - position: 'absolute', - top: 0, - }, -}); - -const toggleFullScreen = () => { - ipcRenderer.send('window.toolbar-double-clicked'); -}; - -class AppLayout extends Component { - static propTypes = { - classes: PropTypes.object.isRequired, - settings: PropTypes.object.isRequired, - isFullScreen: PropTypes.bool.isRequired, - sidebar: PropTypes.element.isRequired, - workspacesDrawer: PropTypes.element.isRequired, - services: PropTypes.element.isRequired, - showServicesUpdatedInfoBar: PropTypes.bool.isRequired, - appUpdateIsDownloaded: PropTypes.bool.isRequired, - authRequestFailed: PropTypes.bool.isRequired, - installAppUpdate: PropTypes.func.isRequired, - showRequiredRequestsError: PropTypes.bool.isRequired, - areRequiredRequestsSuccessful: PropTypes.bool.isRequired, - retryRequiredRequests: PropTypes.func.isRequired, - areRequiredRequestsLoading: PropTypes.bool.isRequired, - }; - - state = { - shouldShowAppUpdateInfoBar: true, - shouldShowServicesUpdatedInfoBar: true, - }; - - render() { - const { - classes, - isFullScreen, - workspacesDrawer, - sidebar, - services, - showServicesUpdatedInfoBar, - appUpdateIsDownloaded, - authRequestFailed, - installAppUpdate, - settings, - showRequiredRequestsError, - areRequiredRequestsSuccessful, - retryRequiredRequests, - areRequiredRequestsLoading, - } = this.props; - - const { intl } = this.props; - - const { locked, automaticUpdates } = settings.app; - if (locked) { - return ; - } - - return ( - <> - {isMac && !isFullScreen && ( -
- )} - -
- {isWindows && !isFullScreen && ( - - )} - {isMac && !isFullScreen && ( - - )} -
- {workspacesDrawer} - {sidebar} -
- - {!areRequiredRequestsSuccessful && showRequiredRequestsError && ( - - - {intl.formatMessage(messages.requiredRequestsFailed)} - - )} - {authRequestFailed && ( - - - {intl.formatMessage(messages.authRequestFailed)} - - )} - {automaticUpdates && showServicesUpdatedInfoBar && - this.state.shouldShowServicesUpdatedInfoBar && ( - window.location.reload()} - onHide={() => { - this.setState({ - shouldShowServicesUpdatedInfoBar: false, - }); - }} - > - - {intl.formatMessage(messages.servicesUpdated)} - - )} - {automaticUpdates && appUpdateIsDownloaded && this.state.shouldShowAppUpdateInfoBar && ( - { - this.setState({ shouldShowAppUpdateInfoBar: false }); - }} - /> - )} - - - - {services} - -
- -
-
-
- - ); - } -} - -export default injectIntl( - injectSheet(styles, { injectTheme: true })(observer(AppLayout)), -); diff --git a/src/components/layout/AppLayout.jsx b/src/components/layout/AppLayout.jsx new file mode 100644 index 000000000..685839c0a --- /dev/null +++ b/src/components/layout/AppLayout.jsx @@ -0,0 +1,226 @@ +import { Component } from 'react'; +import PropTypes from 'prop-types'; +import { observer } from 'mobx-react'; +import { defineMessages, injectIntl } from 'react-intl'; +import { TitleBar } from 'electron-react-titlebar/renderer'; +import injectSheet from 'react-jss'; +import { ipcRenderer } from 'electron'; + +import { mdiFlash, mdiPowerPlug } from '@mdi/js'; +import { Outlet } from 'react-router-dom'; +import InfoBar from '../ui/InfoBar'; +import { Component as BasicAuth } from '../../features/basicAuth'; +import { Component as QuickSwitch } from '../../features/quickSwitch'; +import { Component as PublishDebugInfo } from '../../features/publishDebugInfo'; +import ErrorBoundary from '../util/ErrorBoundary'; +import { updateVersionParse } from '../../helpers/update-helpers'; + +// import globalMessages from '../../i18n/globalMessages'; + +import { isWindows, isMac } from '../../environment'; +import WorkspaceSwitchingIndicator from '../../features/workspaces/components/WorkspaceSwitchingIndicator'; +import { workspaceStore } from '../../features/workspaces'; +import AppUpdateInfoBar from '../AppUpdateInfoBar'; +import Todos from '../../features/todos/containers/TodosScreen'; +import Icon from '../ui/icon'; + +import LockedScreen from '../../containers/auth/LockedScreen'; + +const messages = defineMessages({ + servicesUpdated: { + id: 'infobar.servicesUpdated', + defaultMessage: 'Your services have been updated.', + }, + buttonReloadServices: { + id: 'infobar.buttonReloadServices', + defaultMessage: 'Reload services', + }, + requiredRequestsFailed: { + id: 'infobar.requiredRequestsFailed', + defaultMessage: 'Could not load services and user information', + }, + authRequestFailed: { + id: 'infobar.authRequestFailed', + defaultMessage: + 'There were errors while trying to perform an authenticated request. Please try logging out and back in if this error persists.', + }, +}); + +let transition = 'none'; + +if (window && window.matchMedia('(prefers-reduced-motion: no-preference)')) { + transition = 'transform 0.5s ease'; +} + +const styles = theme => ({ + appContent: { + // width: `calc(100% + ${theme.workspaces.drawer.width}px)`, + width: '100%', + transition, + transform() { + return workspaceStore.isWorkspaceDrawerOpen + ? 'translateX(0)' + : `translateX(-${theme.workspaces.drawer.width}px)`; + }, + }, + titleBar: { + display: 'block', + zIndex: 1, + width: '100%', + height: '10px', + position: 'absolute', + top: 0, + }, +}); + +const toggleFullScreen = () => { + ipcRenderer.send('window.toolbar-double-clicked'); +}; + +class AppLayout extends Component { + static propTypes = { + classes: PropTypes.object.isRequired, + settings: PropTypes.object.isRequired, + isFullScreen: PropTypes.bool.isRequired, + sidebar: PropTypes.element.isRequired, + workspacesDrawer: PropTypes.element.isRequired, + services: PropTypes.element.isRequired, + showServicesUpdatedInfoBar: PropTypes.bool.isRequired, + appUpdateIsDownloaded: PropTypes.bool.isRequired, + authRequestFailed: PropTypes.bool.isRequired, + installAppUpdate: PropTypes.func.isRequired, + showRequiredRequestsError: PropTypes.bool.isRequired, + areRequiredRequestsSuccessful: PropTypes.bool.isRequired, + retryRequiredRequests: PropTypes.func.isRequired, + areRequiredRequestsLoading: PropTypes.bool.isRequired, + }; + + constructor(props) { + super(props); + + this.state = { + shouldShowAppUpdateInfoBar: true, + shouldShowServicesUpdatedInfoBar: true, + }; + } + + render() { + const { + classes, + isFullScreen, + workspacesDrawer, + sidebar, + services, + showServicesUpdatedInfoBar, + appUpdateIsDownloaded, + authRequestFailed, + installAppUpdate, + settings, + showRequiredRequestsError, + areRequiredRequestsSuccessful, + retryRequiredRequests, + areRequiredRequestsLoading, + updateVersion, + } = this.props; + + const { intl } = this.props; + + const { locked, automaticUpdates } = settings.app; + if (locked) { + return ; + } + + return ( + <> + {isMac && !isFullScreen &&
} + +
+ {isWindows && !isFullScreen && ( + + )} + {isMac && !isFullScreen && ( + + )} +
+ {workspacesDrawer} + {sidebar} +
+ + {!areRequiredRequestsSuccessful && showRequiredRequestsError && ( + + + {intl.formatMessage(messages.requiredRequestsFailed)} + + )} + {authRequestFailed && ( + + + {intl.formatMessage(messages.authRequestFailed)} + + )} + {automaticUpdates && + showServicesUpdatedInfoBar && + this.state.shouldShowServicesUpdatedInfoBar && ( + window.location.reload()} + onHide={() => { + this.setState({ + shouldShowServicesUpdatedInfoBar: false, + }); + }} + > + + {intl.formatMessage(messages.servicesUpdated)} + + )} + {automaticUpdates && + appUpdateIsDownloaded && + this.state.shouldShowAppUpdateInfoBar && ( + { + this.setState({ shouldShowAppUpdateInfoBar: false }); + }} + /> + )} + + + + {services} + +
+ +
+
+
+ + ); + } +} + +export default injectIntl( + injectSheet(styles, { injectTheme: true })(observer(AppLayout)), +); diff --git a/src/components/settings/SettingsLayout.jsx b/src/components/settings/SettingsLayout.jsx index dea4bb387..989c428f2 100644 --- a/src/components/settings/SettingsLayout.jsx +++ b/src/components/settings/SettingsLayout.jsx @@ -8,6 +8,7 @@ import { Outlet } from 'react-router-dom'; import ErrorBoundary from '../util/ErrorBoundary'; import Appear from '../ui/effects/Appear'; import Icon from '../ui/icon'; +import { isEscKeyPress } from '../../jsUtils'; const messages = defineMessages({ closeSettings: { @@ -36,8 +37,7 @@ class SettingsLayout extends Component { } handleKeyDown(e) { - if (e.keyCode === 27) { - // escape key + if (isEscKeyPress(e.keyCode)) { this.props.closeSettings(); } } diff --git a/src/components/settings/navigation/SettingsNavigation.jsx b/src/components/settings/navigation/SettingsNavigation.jsx index bbbe8d888..e1242a7fe 100644 --- a/src/components/settings/navigation/SettingsNavigation.jsx +++ b/src/components/settings/navigation/SettingsNavigation.jsx @@ -5,7 +5,11 @@ import { inject, observer } from 'mobx-react'; import { RouterStore } from '@superwf/mobx-react-router'; import { NavLink } from 'react-router-dom'; -import { LOCAL_SERVER, LIVE_FERDIUM_API, LIVE_FRANZ_API } from '../../../config'; +import { + LOCAL_SERVER, + LIVE_FERDIUM_API, + LIVE_FRANZ_API, +} from '../../../config'; import UIStore from '../../../stores/UIStore'; import SettingsStore from '../../../stores/SettingsStore'; import UserStore from '../../../stores/UserStore'; @@ -32,6 +36,10 @@ const messages = defineMessages({ id: 'settings.navigation.team', defaultMessage: 'Manage Team', }, + releaseNotes: { + id: 'settings.navigation.releaseNotes', + defaultMessage: 'Release Notes', + }, supportFerdium: { id: 'settings.navigation.supportFerdium', defaultMessage: 'About Ferdium', @@ -164,6 +172,16 @@ class SettingsNavigation extends Component { )} + + isActive + ? 'settings-navigation__link is-active' + : 'settings-navigation__link' + } + > + {intl.formatMessage(messages.releaseNotes)} + @@ -190,4 +208,6 @@ class SettingsNavigation extends Component { } } -export default injectIntl(inject('stores', 'actions')(observer(SettingsNavigation))); +export default injectIntl( + inject('stores', 'actions')(observer(SettingsNavigation)), +); diff --git a/src/components/settings/releaseNotes/ReleaseNotesDashboard.tsx b/src/components/settings/releaseNotes/ReleaseNotesDashboard.tsx new file mode 100644 index 000000000..d0be82312 --- /dev/null +++ b/src/components/settings/releaseNotes/ReleaseNotesDashboard.tsx @@ -0,0 +1,99 @@ +import { Component } from 'react'; +import { observer } from 'mobx-react'; +import { defineMessages, injectIntl } from 'react-intl'; +import Markdown from 'markdown-to-jsx'; +import { openExternalUrl } from '../../../helpers/url-helpers'; +import { ferdiumVersion } from '../../../environment-remote'; +import { + getFerdiumVersion, + getUpdateInfoFromGH, +} from '../../../helpers/update-helpers'; + +const messages = defineMessages({ + headline: { + id: 'settings.releasenotes.headline', + defaultMessage: 'Release Notes', + }, + connectionError: { + id: 'settings.releasenotes.connectionError', + defaultMessage: + 'An error occured when connecting to Github, please try again later.', + }, + connectionErrorPageMissing: { + id: 'settings.releasenotes.connectionErrorPageMissing', + defaultMessage: + 'An error occured when connecting to Github, the page you are looking for is missing.', + }, +}); + +interface IProps { + intl: any; +} + +class ReleaseNotesDashboard extends Component { + state = { + data: '', + }; + + constructor(props) { + super(props); + + this.state = { data: '' }; + } + + async componentDidMount() { + const { intl } = this.props; + + const data = await getUpdateInfoFromGH( + window.location.href, + ferdiumVersion, + intl, + ); + + this.setState({ + data, + }); + + for (const link of document.querySelectorAll('.releasenotes__body a')) { + link.addEventListener('click', this.handleClick.bind(this), false); + } + } + + handleClick(e) { + e.preventDefault(); + openExternalUrl(e.target.href); + } + + componentWillUnmount() { + document.removeEventListener( + 'click', + // eslint-disable-next-line unicorn/no-invalid-remove-event-listener + this.handleClick.bind(this), + false, + ); + } + + render() { + const { intl } = this.props; + + const { data } = this.state; + return ( +
+
+ + Ferdium {getFerdiumVersion(window.location.href, ferdiumVersion)}{' '} + {' | '} + + + {intl.formatMessage(messages.headline)} + +
+
+ {data} +
+
+ ); + } +} + +export default injectIntl(observer(ReleaseNotesDashboard)); diff --git a/src/components/settings/releaseNotes/ReleaseNotesLayout.tsx b/src/components/settings/releaseNotes/ReleaseNotesLayout.tsx new file mode 100644 index 000000000..ee0ba75a8 --- /dev/null +++ b/src/components/settings/releaseNotes/ReleaseNotesLayout.tsx @@ -0,0 +1,79 @@ +import { Component } from 'react'; +import { inject, observer } from 'mobx-react'; +import { defineMessages, injectIntl } from 'react-intl'; + +import { mdiClose } from '@mdi/js'; +import { Outlet } from 'react-router-dom'; +import { StoresProps } from '../../../@types/ferdium-components.types'; +import ErrorBoundary from '../../util/ErrorBoundary'; +import Appear from '../../ui/effects/Appear'; +import Icon from '../../ui/icon'; +import { isEscKeyPress } from '../../../jsUtils'; + +const messages = defineMessages({ + closeSettings: { + id: 'settings.app.closeSettings', + defaultMessage: 'Close settings', + }, +}); + +interface IProps extends StoresProps { + intl: any; +} + +class ReleaseNotesLayout extends Component { + componentDidMount() { + document.addEventListener('keydown', this.handleKeyDown.bind(this), false); + } + + componentWillUnmount() { + document.removeEventListener( + 'keydown', + // eslint-disable-next-line unicorn/no-invalid-remove-event-listener + this.handleKeyDown.bind(this), + false, + ); + } + + handleKeyDown(e) { + if (isEscKeyPress(e.keyCode)) { + this.props.actions.ui.closeSettings(); + } + } + + render() { + const { closeSettings } = this.props.actions.ui; + + const { intl } = this.props; + + return ( + +
+ + +
+ +
+ + ); + } +} + +export default injectIntl<'intl', IProps>( + inject('stores', 'actions')(observer(ReleaseNotesLayout)), +); diff --git a/src/components/settings/settings/EditSettingsForm.jsx b/src/components/settings/settings/EditSettingsForm.jsx index cb1d261f8..a10d89570 100644 --- a/src/components/settings/settings/EditSettingsForm.jsx +++ b/src/components/settings/settings/EditSettingsForm.jsx @@ -15,6 +15,13 @@ import Input from '../../ui/Input'; import ColorPickerInput from '../../ui/ColorPickerInput'; import Infobox from '../../ui/Infobox'; import { H1, H2, H3, H5 } from '../../ui/headline'; +import { + ferdiumVersion, + userDataPath, + userDataRecipesPath, +} from '../../../environment-remote'; + +import { updateVersionParse } from '../../../helpers/update-helpers'; import { DEFAULT_ACCENT_COLOR, @@ -25,11 +32,6 @@ import { SPLIT_COLUMNS_MIN, } from '../../../config'; import { isMac, isWindows, lockFerdiumShortcutKey } from '../../../environment'; -import { - ferdiumVersion, - userDataPath, - userDataRecipesPath, -} from '../../../environment-remote'; import { openExternalUrl, openPath } from '../../../helpers/url-helpers'; import globalMessages from '../../../i18n/globalMessages'; import Icon from '../../ui/icon'; @@ -213,6 +215,10 @@ const messages = defineMessages({ id: 'settings.app.buttonInstallUpdate', defaultMessage: 'Restart & install update', }, + buttonShowChangelog: { + id: 'settings.app.buttonShowChangelog', + defaultMessage: 'Show changelog', + }, updateStatusSearching: { id: 'settings.app.updateStatusSearching', defaultMessage: 'Searching for updates...', @@ -328,6 +334,7 @@ class EditSettingsForm extends Component { checkForUpdates, installUpdate, form, + updateVersion, isCheckingForUpdates, isAdaptableDarkModeEnabled, isUseGrayscaleServicesEnabled, @@ -1002,6 +1009,20 @@ class EditSettingsForm extends Component { loaded={!isCheckingForUpdates || !isUpdateAvailable} /> )} + {(isUpdateAvailable || updateIsReadyToInstall) && ( +

diff --git a/src/components/ui/effects/Appear.tsx b/src/components/ui/effects/Appear.tsx index 117c02f97..228c7888a 100644 --- a/src/components/ui/effects/Appear.tsx +++ b/src/components/ui/effects/Appear.tsx @@ -1,10 +1,10 @@ -import { ReactChildren, useEffect, useState } from 'react'; +import { ReactNode, useEffect, useState } from 'react'; import ReactCSSTransitionGroup from 'react-addons-css-transition-group'; type Props = { - children: ReactChildren; + children: ReactNode; transitionName: string; - className: string; + className?: string; }; const Appear = ({ children, @@ -36,4 +36,8 @@ const Appear = ({ ); }; +Appear.defaultProps = { + className: '', +}; + export default Appear; diff --git a/src/containers/auth/AuthLayoutContainer.tsx b/src/containers/auth/AuthLayoutContainer.tsx index 8d31cfb48..6fc6713f1 100644 --- a/src/containers/auth/AuthLayoutContainer.tsx +++ b/src/containers/auth/AuthLayoutContainer.tsx @@ -49,6 +49,7 @@ class AuthLayoutContainer extends Component { appUpdateIsDownloaded={ app.updateStatus === app.updateStatusTypes.DOWNLOADED } + updateVersion={app.updateVersion} > diff --git a/src/containers/auth/AuthReleaseNotesScreen.tsx b/src/containers/auth/AuthReleaseNotesScreen.tsx new file mode 100644 index 000000000..c717529fa --- /dev/null +++ b/src/containers/auth/AuthReleaseNotesScreen.tsx @@ -0,0 +1,107 @@ +import { Component } from 'react'; +import { inject, observer } from 'mobx-react'; + +import { defineMessages, injectIntl } from 'react-intl'; +import Markdown from 'markdown-to-jsx'; +import { mdiArrowLeftCircle } from '@mdi/js'; +import { openExternalUrl } from '../../helpers/url-helpers'; +import Icon from '../../components/ui/icon'; +import { ferdiumVersion } from '../../environment-remote'; +import { + getFerdiumVersion, + getUpdateInfoFromGH, +} from '../../helpers/update-helpers'; + +const messages = defineMessages({ + headline: { + id: 'settings.releasenotes.headline', + defaultMessage: 'Release Notes', + }, +}); + +interface IProps { + intl: any; +} + +class AuthReleaseNotesScreen extends Component { + state = { + data: '', + }; + + constructor(props) { + super(props); + + this.state = { data: '' }; + } + + async componentDidMount() { + const { intl } = this.props; + + const data = await getUpdateInfoFromGH( + window.location.href, + ferdiumVersion, + intl, + ); + + this.setState({ + data, + }); + + for (const link of document.querySelectorAll('.releasenotes__body a')) { + link.addEventListener('click', this.handleClick.bind(this), false); + } + } + + handleClick(e) { + e.preventDefault(); + openExternalUrl(e.target.href); + } + + componentWillUnmount() { + document.removeEventListener( + 'click', + // eslint-disable-next-line unicorn/no-invalid-remove-event-listener + this.handleClick.bind(this), + false, + ); + } + + render() { + const { intl } = this.props; + + const { data } = this.state; + return ( +

+
+
+ + Ferdium {getFerdiumVersion(window.location.href, ferdiumVersion)}{' '} + {' | '} + + + {intl.formatMessage(messages.headline)} + +
+
+ {data} +
+
+ +
+
+
+ ); + } +} + +export default injectIntl<'intl', IProps>( + inject('stores', 'actions')(observer(AuthReleaseNotesScreen)), +); diff --git a/src/containers/layout/AppLayoutContainer.tsx b/src/containers/layout/AppLayoutContainer.tsx index a4857a426..9c03b0ba4 100644 --- a/src/containers/layout/AppLayoutContainer.tsx +++ b/src/containers/layout/AppLayoutContainer.tsx @@ -157,6 +157,7 @@ class AppLayoutContainer extends Component { areRequiredRequestsSuccessful={requests.areRequiredRequestsSuccessful} retryRequiredRequests={retryRequiredRequests} areRequiredRequestsLoading={requests.areRequiredRequestsLoading} + updateVersion={app.updateVersion} > diff --git a/src/containers/settings/EditSettingsScreen.tsx b/src/containers/settings/EditSettingsScreen.tsx index a6c4561dd..fbbed629a 100644 --- a/src/containers/settings/EditSettingsScreen.tsx +++ b/src/containers/settings/EditSettingsScreen.tsx @@ -853,6 +853,7 @@ class EditSettingsScreen extends Component { const { app } = this.props.stores; const { updateStatus, + updateVersion, updateStatusTypes, isClearingAllCache, lockingFeatureEnabled, @@ -860,13 +861,13 @@ class EditSettingsScreen extends Component { const { checkForUpdates, installUpdate, clearAllCache } = this.props.actions.app; const form = this.prepareForm(); - return ( + + + ); + } +} + +export default ReleaseNotesScreen; diff --git a/src/containers/settings/ReleaseNotesWindow.tsx b/src/containers/settings/ReleaseNotesWindow.tsx new file mode 100644 index 000000000..3e43727d0 --- /dev/null +++ b/src/containers/settings/ReleaseNotesWindow.tsx @@ -0,0 +1,42 @@ +import { inject, observer } from 'mobx-react'; +import { Component, ReactPortal } from 'react'; +import ReactDOM from 'react-dom'; +import { Outlet } from 'react-router-dom'; + +import { StoresProps } from '../../@types/ferdium-components.types'; +import Layout from '../../components/settings/releaseNotes/ReleaseNotesLayout'; +import ErrorBoundary from '../../components/util/ErrorBoundary'; + +class SettingsContainer extends Component { + portalRoot: any; + + el: HTMLDivElement; + + constructor(props: StoresProps) { + super(props); + + this.portalRoot = document.querySelector('#portalContainer'); + this.el = document.createElement('div'); + } + + componentDidMount(): void { + this.portalRoot.append(this.el); + } + + componentWillUnmount(): void { + this.el.remove(); + } + + render(): ReactPortal { + return ReactDOM.createPortal( + + + + + , + this.el, + ); + } +} + +export default inject('stores', 'actions')(observer(SettingsContainer)); diff --git a/src/features/workspaces/components/WorkspaceItem.tsx b/src/features/workspaces/components/WorkspaceItem.tsx index f46375c7a..eb33a0376 100644 --- a/src/features/workspaces/components/WorkspaceItem.tsx +++ b/src/features/workspaces/components/WorkspaceItem.tsx @@ -1,8 +1,6 @@ import { Component } from 'react'; -import PropTypes from 'prop-types'; import { observer } from 'mobx-react'; import injectSheet from 'react-jss'; - import Workspace from '../models/Workspace'; const styles = theme => ({ @@ -18,17 +16,11 @@ const styles = theme => ({ type Props = { classes: any; - workspace: any; + workspace: typeof Workspace; onItemClick: (workspace) => void; }; class WorkspaceItem extends Component { - static propTypes = { - classes: PropTypes.object.isRequired, - workspace: PropTypes.instanceOf(Workspace).isRequired, - onItemClick: PropTypes.func.isRequired, - }; - render() { const { classes, workspace, onItemClick } = this.props; diff --git a/src/helpers/update-helpers.ts b/src/helpers/update-helpers.ts new file mode 100644 index 000000000..daeef5413 --- /dev/null +++ b/src/helpers/update-helpers.ts @@ -0,0 +1,65 @@ +import { Octokit } from '@octokit/core'; +import { defineMessages, IntlShape } from 'react-intl'; + +export function getFerdiumVersion( + currentLocation: string, + ferdiumVersion: string, +): string { + const matches = currentLocation?.match(/version=([^&]*)/); + if (matches !== null) { + return `v${matches[1]}`; + } + return `v${ferdiumVersion}`; +} + +export function updateVersionParse(updateVersion: string): string { + return updateVersion !== '' ? `?version=${updateVersion}` : ''; +} + +export function onAuthGoToReleaseNotes( + currentLocation: string, + updateVersionParsed: string = '', +): string { + return currentLocation.includes('#/auth') + ? `#/auth/releasenotes${updateVersionParsed}` + : `#/releasenotes${updateVersionParsed}`; +} + +const messages = defineMessages({ + connectionError: { + id: 'settings.releasenotes.connectionError', + defaultMessage: + 'An error occured when connecting to Github, please try again later.', + }, + connectionErrorPageMissing: { + id: 'settings.releasenotes.connectionErrorPageMissing', + defaultMessage: + 'An error occured when connecting to Github, the page you are looking for is missing.', + }, +}); + +export async function getUpdateInfoFromGH( + currentLocation: string, + ferdiumVersion: string, + intl: IntlShape, +): Promise { + const octokit = new Octokit(); + try { + const response = await octokit.request( + 'GET /repos/{owner}/{repo}/releases/tags/{tag}', + { + owner: 'ferdium', + repo: 'ferdium-app', + tag: getFerdiumVersion(currentLocation, ferdiumVersion), + }, + ); + + if (response.status === 200) { + const json = response.data.body; + return json || `### ${intl.formatMessage(messages.connectionError)}`; + } + return `### ${intl.formatMessage(messages.connectionError)}`; + } catch { + return `### ${intl.formatMessage(messages.connectionErrorPageMissing)}`; + } +} diff --git a/src/i18n/locales/en-US.json b/src/i18n/locales/en-US.json index 3c3beea6f..d45ab5c3c 100644 --- a/src/i18n/locales/en-US.json +++ b/src/i18n/locales/en-US.json @@ -194,6 +194,7 @@ "settings.app.buttonOpenFerdiumServiceRecipesFolder": "Open Service Recipes folder", "settings.app.buttonOpenImportExport": "Import / Export", "settings.app.buttonSearchForUpdate": "Check for updates", + "settings.app.buttonShowChangelog": "Show changelog", "settings.app.cacheInfo": "Ferdium cache is currently using {size} of disk space.", "settings.app.cacheNotCleared": "Couldn't clear all cache", "settings.app.closeSettings": "Close settings", @@ -302,6 +303,7 @@ "settings.navigation.account": "Account", "settings.navigation.availableServices": "Available services", "settings.navigation.logout": "Logout", + "settings.navigation.releaseNotes": "Release Notes", "settings.navigation.supportFerdium": "About Ferdium", "settings.navigation.team": "Manage Team", "settings.navigation.yourServices": "Your services", @@ -319,6 +321,9 @@ "settings.recipes.missingService": "Missing a service?", "settings.recipes.nothingFound": "Sorry, but no service matched your search term - but you can still probably add it using the \"Custom Website\" option. Please note that the website might show more services that have been added to Ferdium since the version that you are currently on. To get those new services, please consider upgrading to a newer version of Ferdium.", "settings.recipes.servicesSuccessfulAddedInfo": "Service successfully added", + "settings.releasenotes.connectionError": "An error occured when connecting to Github, please try again later.", + "settings.releasenotes.connectionErrorPageMissing": "An error occured when connecting to Github, the page you are looking for is missing.", + "settings.releasenotes.headline": "Release Notes", "settings.searchService": "Search service", "settings.service.error.goBack": "Back to services", "settings.service.error.headline": "Error", diff --git a/src/jsUtils.ts b/src/jsUtils.ts index f5b39a000..36d70da25 100644 --- a/src/jsUtils.ts +++ b/src/jsUtils.ts @@ -20,3 +20,5 @@ export const convertToJSON = (data: string | any | undefined | null) => export const cleanseJSObject = (data: any | undefined | null) => JSON.parse(JSON.stringify(data)); + +export const isEscKeyPress = (keyCode : Number) => keyCode === 27; diff --git a/src/lib/Menu.js b/src/lib/Menu.js index 8fd3e9e01..41b4aa9f7 100644 --- a/src/lib/Menu.js +++ b/src/lib/Menu.js @@ -32,11 +32,7 @@ import { nodeVersion, osArch, } from '../environment'; -import { - CUSTOM_WEBSITE_RECIPE_ID, - GITHUB_FERDIUM_URL, - LIVE_API_FERDIUM_WEBSITE, -} from '../config'; +import { CUSTOM_WEBSITE_RECIPE_ID, LIVE_API_FERDIUM_WEBSITE } from '../config'; import { ferdiumVersion } from '../environment-remote'; import { todoActions } from '../features/todos/actions'; import workspaceActions from '../features/workspaces/actions'; @@ -44,6 +40,7 @@ import { workspaceStore } from '../features/workspaces/index'; import { importExportURL, serverBase, serverName } from '../api/apiBase'; import { openExternalUrl } from '../helpers/url-helpers'; import globalMessages from '../i18n/globalMessages'; +import { onAuthGoToReleaseNotes } from '../helpers/update-helpers'; // @ts-expect-error Cannot find module '../buildInfo.json' or its corresponding type declarations. import * as buildInfo from '../buildInfo.json'; @@ -586,10 +583,7 @@ const _titleBarTemplateFactory = (intl, locked) => [ { label: intl.formatMessage(menuItems.changelog), click() { - openExternalUrl( - `${GITHUB_FERDIUM_URL}/ferdium-app/releases/tag/v${ferdiumVersion}`, - true, - ); + window.location.href = onAuthGoToReleaseNotes(window.location.href); }, }, { diff --git a/src/routes.tsx b/src/routes.tsx index 1f929531e..478d3dfe8 100644 --- a/src/routes.tsx +++ b/src/routes.tsx @@ -9,6 +9,7 @@ import { import AppLayoutContainer from './containers/layout/AppLayoutContainer'; import SettingsWindow from './containers/settings/SettingsWindow'; +import ReleaseNotesWindow from './containers/settings/ReleaseNotesWindow'; import RecipesScreen from './containers/settings/RecipesScreen'; import ServicesScreen from './containers/settings/ServicesScreen'; import EditServiceScreen from './containers/settings/EditServiceScreen'; @@ -18,8 +19,10 @@ import EditUserScreen from './containers/settings/EditUserScreen'; import EditSettingsScreen from './containers/settings/EditSettingsScreen'; import InviteSettingsScreen from './containers/settings/InviteScreen'; import SupportFerdiumScreen from './containers/settings/SupportScreen'; +import ReleaseNotesScreen from './containers/settings/ReleaseNotesScreen'; import WelcomeScreen from './containers/auth/WelcomeScreen'; import LoginScreen from './containers/auth/LoginScreen'; +import AuthReleaseNotesScreen from './containers/auth/AuthReleaseNotesScreen'; import PasswordScreen from './containers/auth/PasswordScreen'; import ChangeServerScreen from './containers/auth/ChangeServerScreen'; import SignupScreen from './containers/auth/SignupScreen'; @@ -100,9 +103,24 @@ class FerdiumRoutes extends Component { path="/auth/logout" element={} /> + + } + /> }> + } + > + } + /> + } @@ -155,6 +173,10 @@ class FerdiumRoutes extends Component { path="/settings/support" element={} /> + } + /> diff --git a/src/stores/AppStore.ts b/src/stores/AppStore.ts index 116e64756..af1a0daae 100644 --- a/src/stores/AppStore.ts +++ b/src/stores/AppStore.ts @@ -82,6 +82,8 @@ export default class AppStore extends TypedStore { @observable updateStatus = ''; + @observable updateVersion = ''; + @observable locale = ferdiumLocale; @observable isSystemMuteOverridden = false; @@ -94,7 +96,8 @@ export default class AppStore extends TypedStore { @observable isFocused = true; - @observable lockingFeatureEnabled = DEFAULT_APP_SETTINGS.lockingFeatureEnabled; + @observable lockingFeatureEnabled = + DEFAULT_APP_SETTINGS.lockingFeatureEnabled; @observable launchInBackground = DEFAULT_APP_SETTINGS.autoLaunchInBackground; @@ -181,6 +184,7 @@ export default class AppStore extends TypedStore { ipcRenderer.on('autoUpdate', (_, data) => { if (this.updateStatus !== this.updateStatusTypes.FAILED) { if (data.available) { + this.updateVersion = data.version; this.updateStatus = this.updateStatusTypes.AVAILABLE; if (isMac && this.stores.settings.app.automaticUpdates) { app.dock.bounce(); diff --git a/src/styles/auth.scss b/src/styles/auth.scss index e60c4971c..53c03a40a 100644 --- a/src/styles/auth.scss +++ b/src/styles/auth.scss @@ -61,6 +61,12 @@ width: 350px; &.auth__container--signup { width: 450px; } + + &.auth__container--releasenotes { + width: 90%; + height: -webkit-fill-available; + max-height: 80%; + } } .auth__logo { @@ -92,6 +98,32 @@ } } + .releasenotes__body { + overflow-y: scroll; + + &::-webkit-scrollbar { + width: 8px; + } + + /* Track */ + &::-webkit-scrollbar-track { + background: none; + border-radius: 10px; + -webkit-border-radius: 10px; + } + + /* Handle */ + &::-webkit-scrollbar-thumb { + background: $theme-gray-lighter; + border-radius: 10px; + -webkit-border-radius: 10px; + } + + &::-webkit-scrollbar-thumb:window-inactive { + background: none; + } + } + .touchid__button { margin-bottom: 25px; } @@ -135,6 +167,7 @@ padding-top: 2%; padding-bottom: 2%; justify-content: center; + height: fit-content; } .auth__adlk { @@ -168,3 +201,29 @@ text-align: center; } } + +.auth__main--releasenotes { + margin: 4% 2% 0% 4%; + display: flex; + flex-direction: column; + justify-content: center; + height: -webkit-fill-available; +} + +.auth__header { + font-size: x-large; + display: inline-flex; + flex-direction: row; + align-content: center; + justify-content: center; + flex-wrap: wrap; + height: fit-content; + + &-item__secondary { + padding-left: 5px + } +} + +.auth__body { + margin-top: 2%; +} diff --git a/src/styles/settings.scss b/src/styles/settings.scss index 76d167fd4..f16fc91fd 100644 --- a/src/styles/settings.scss +++ b/src/styles/settings.scss @@ -234,6 +234,10 @@ .settings__header-item { @extend %headline; + + &__secondary { + padding-left: 5px + } } .separator { @@ -630,3 +634,94 @@ display: inline-block; } } + +.settings__updates__changelog-button { + margin-left: 2% !important; +} + +.releasenotes__body { + line-height: 150%; + + h1 { + margin-top: 0; + font-weight: 400; + font-size: xx-large; + text-align: center; + } + + h2 { + margin-top: 10%; + font-weight: 400; + font-size: x-large; + text-align: center; + margin-bottom: 2%; + } + + h3 { + margin-top: 5%; + font-weight: 400; + font-size: large; + text-align: center; + margin-bottom: 2%; + } + + h4 { + margin-top: 5%; + font-weight: 400; + font-size: medium; + text-align: center; + margin-bottom: 2%; + } + + h5 { + margin-top: 5%; + font-weight: 400; + text-align: center; + margin-bottom: 2%; + } + + p { + margin-top: 4%; + margin-bottom: 4%; + user-select: text; + } + + a { + color: rgb(0, 102, 255) !important; + user-select: text; + } + + code { + background: #f4f4f4; + border: 1px solid #ddd; + border-radius: 5px; + padding: 0.2em 0.4em; + font-size: 85%; + color: #666; + font-family: monospace; + margin-bottom: 1.6em; + max-width: 100%; + overflow: auto; + user-select: text; + } + + ul { + padding-left: 2rem; + } + + li { + margin-top: 2%; + list-style-type: disc; + user-select: text; + } + + img { + max-width: 100%; + margin-top: 2%; + margin-bottom: 2%; + align-items: center; + display: block; + margin-left: auto; + margin-right: auto; + } +} diff --git a/test/helpers/update-helpers.test.ts b/test/helpers/update-helpers.test.ts new file mode 100644 index 000000000..0494c6855 --- /dev/null +++ b/test/helpers/update-helpers.test.ts @@ -0,0 +1,89 @@ +import * as update_helpers from '../../src/helpers/update-helpers'; + +describe('getFerdiumVersion', () => { + const baseVersion = '6.0.0-nightly.3'; + it(`returns ${baseVersion} for empty string`, () => { + const result = update_helpers.getFerdiumVersion('', baseVersion); + expect(result).toEqual(`v${baseVersion}`); + }); + + it(`returns ${baseVersion} for ${baseVersion}`, () => { + const result = update_helpers.getFerdiumVersion('', baseVersion); + expect(result).toEqual(`v${baseVersion}`); + }); + + it(`returns v6.0.0-beta.3`, () => { + const result = update_helpers.getFerdiumVersion( + '?version=6.0.0-beta.3', + baseVersion, + ); + expect(result).toEqual(`v6.0.0-beta.3`); + }); + + it(`returns v6.0.0`, () => { + const result = update_helpers.getFerdiumVersion( + '?version=6.0.0', + baseVersion, + ); + expect(result).toEqual(`v6.0.0`); + }); + + it(`returns ${baseVersion}`, () => { + const result = update_helpers.getFerdiumVersion( + 'http://test/=6.0.0', + baseVersion, + ); + expect(result).toEqual(`v${baseVersion}`); + }); + + it(`returns ${baseVersion} for missing 'version='`, () => { + const result = update_helpers.getFerdiumVersion( + 'http://test/', + baseVersion, + ); + expect(result).toEqual(`v${baseVersion}`); + }); +}); + +describe('updateVersionParse', () => { + it(`returns empty string for empty string`, () => { + const result = update_helpers.updateVersionParse(''); + expect(result).toEqual(''); + }); + it(`returns '?version=x.x for x.x`, () => { + const result = update_helpers.updateVersionParse('6.0.0'); + expect(result).toEqual('?version=6.0.0'); + }); +}); + +describe('onAuthGoToReleaseNotes', () => { + it(`returns '#/releasenotes' string for empty string`, () => { + const result = update_helpers.onAuthGoToReleaseNotes('', ''); + expect(result).toEqual('#/releasenotes'); + }); + + it(`returns '#/releasenotes' string for empty string`, () => { + const result = update_helpers.onAuthGoToReleaseNotes('', '?version=6.0.0'); + expect(result).toEqual('#/releasenotes?version=6.0.0'); + }); + + it(`returns '#/releasenotes' string for empty string`, () => { + const result = update_helpers.onAuthGoToReleaseNotes(''); + expect(result).toEqual('#/releasenotes'); + }); + + it(`returns '#/releasenotes' string for empty string`, () => { + const result = update_helpers.onAuthGoToReleaseNotes('#/auth', ''); + expect(result).toEqual('#/auth/releasenotes'); + }); + + it(`returns '#/releasenotes' string for empty string`, () => { + const result = update_helpers.onAuthGoToReleaseNotes('#/auth', '?version=6.0.0'); + expect(result).toEqual('#/auth/releasenotes?version=6.0.0'); + }); + + it(`returns '#/releasenotes' string for empty string`, () => { + const result = update_helpers.onAuthGoToReleaseNotes('#/auth'); + expect(result).toEqual('#/auth/releasenotes'); + }); +}); diff --git a/test/jsUtils.test.ts b/test/jsUtils.test.ts index 8ef69b46f..406326d4b 100644 --- a/test/jsUtils.test.ts +++ b/test/jsUtils.test.ts @@ -110,4 +110,16 @@ describe('jsUtils', () => { expect(result).toEqual([{ a: 'b' }, { c: 'd' }]); }); }); + + describe('isEscKeyPress', () => { + it('returns true if the key number is 27', () => { + const result = jsUtils.isEscKeyPress(27); + expect(result).toEqual(true); + }); + + it('returns false if the key number is 27', () => { + const result = jsUtils.isEscKeyPress(28); + expect(result).toEqual(false); + }); + }); }); diff --git a/tsconfig.json b/tsconfig.json index 7a50a4a2d..d1465de86 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,7 +7,8 @@ "target": "esnext", "lib": [ "esnext", - "dom" + "dom", + "dom.iterable" ], "module": "CommonJS", "jsx": "react-jsx", -- cgit v1.2.3-54-g00ecf