diff --git a/docs/config.md b/docs/config.md index 4745ddd07f..fa6ee5b07c 100644 --- a/docs/config.md +++ b/docs/config.md @@ -167,6 +167,10 @@ Starting with `desktop_builds`, the following subproperties are available: 1. `available`: Required. When `true`, the desktop app can be downloaded from somewhere. 2. `logo`: Required. A URL to a logo (SVG), intended to be shown at 24x24 pixels. 3. `url`: Required. The download URL for the app. This is used as a hyperlink. +4. `url_macos`: Optional. Direct link to download macOS desktop app. +5. `url_win32`: Optional. Direct link to download Windows 32-bit desktop app. +6. `url_win64`: Optional. Direct link to download Windows 64-bit desktop app. +7. `url_linux`: Optional. Direct link to download Linux desktop app. When `desktop_builds` is not specified at all, the app will assume desktop downloads are available from https://element.io diff --git a/package.json b/package.json index 6dfb7e5475..cf8908e623 100644 --- a/package.json +++ b/package.json @@ -68,11 +68,17 @@ }, "resolutions": { "@types/react-dom": "17.0.25", - "@types/react": "17.0.80" + "@types/react": "17.0.80", + "@vector-im/compound-design-tokens": "1.7.0", + "@vector-im/compound-web": "5.5.0", + "@floating-ui/react": "0.26.11", + "@radix-ui/react-id": "1.1.0" }, "dependencies": { "@formatjs/intl-segmenter": "^11.5.7", "@matrix-org/react-sdk-module-api": "^2.3.0", + "@vector-im/compound-design-tokens": "^1.6.1", + "@vector-im/compound-web": "^5.5.0", "jsrsasign": "^11.0.0", "katex": "^0.16.0", "lodash": "^4.17.21", diff --git a/res/css/structures/ErrorView.pcss b/res/css/structures/ErrorView.pcss index 704c68c1e9..e76932356a 100644 --- a/res/css/structures/ErrorView.pcss +++ b/res/css/structures/ErrorView.pcss @@ -14,94 +14,61 @@ See the License for the specific language governing permissions and limitations under the License. */ -/* import font-size variables manually, - ideally this file would get loaded by the theme which has all variables in context */ -@import "../../../node_modules/matrix-react-sdk/res/css/_font-sizes.pcss"; - .mx_ErrorView { - background: #c5e0f7; - background: -moz-linear-gradient(top, #c5e0f7 0%, #ffffff 100%); - background: -webkit-linear-gradient(top, #c5e0f7 0%, #ffffff 100%); - background: linear-gradient(to bottom, #c5e0f7 0%, #ffffff 100%); - /* stylelint-disable-next-line function-no-unknown */ - filter: progid:dximagetransform.microsoft.gradient(startColorstr='#c5e0f7', endColorstr='#ffffff', GradientType=0); + --width: 520px; + --cpd-separator-inset: calc(50% - (var(--width) / 2)); + --cpd-separator-spacing: var(--cpd-space-8x); + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; - color: #000; + text-align: center; + color: var(--cpd-color-text-primary); width: 100%; height: 100%; - overflow: auto; - padding: 0 20px; box-sizing: border-box; + overflow: auto; + padding: var(--cpd-space-20x); + + background-color: var(--cpd-color-theme-bg); + background-image: url("../../themes/element/img/compound/fade-arc-light.png"); + background-repeat: no-repeat; + background-size: 100% 100%; + + .mx_ErrorView_logo { + display: block; + margin: 0 auto; + } .mx_ErrorView_container { - max-width: 680px; - margin: auto; - } - - .mx_Button { - border: 0; - border-radius: 4px; - font-size: $font-18px; - margin-left: 4px; - margin-right: 4px; - min-width: 80px; - background-color: #03b381; - color: #fff; - cursor: pointer; - padding: 12px 22px; - word-break: break-word; - text-decoration: none; - } - - .mx_Center { - justify-content: center; - } - - .mx_HomePage_header { - color: #2e2f32; - display: flex; - align-items: center; - justify-content: center; - } - - font-size: $font-16px; - h1 { - font-size: $font-32px; - } - h2 { - font-size: $font-24px; - color: #000; - } - - .mx_HomePage_col { - display: flex; - flex-direction: row; - } - - .mx_HomePage_row { - flex: 1 1 0; - display: flex; - flex-direction: row; - flex-wrap: wrap; - } - - .mx_HomePage_logo { - margin: auto 20px auto 0; + max-width: var(--width); + margin: 0 auto var(--cpd-space-8x); } h1, h2 { - font-weight: 600; - margin-bottom: 32px; + color: var(--cpd-color-text-primary); } - .mx_Spacer { - margin-top: 24px; + h2 { + margin: var(--cpd-space-8x) 0; + /* Workaround for heading sm being broken in Compound design tokens */ + font-size: var(--cpd-font-size-heading-sm); + line-height: var(--cpd-font-line-height-tight); + font-weight: var(--cpd-font-weight-semibold); } - .mx_FooterLink { - color: #368bd6; - text-decoration: none; + p { + color: var(--cpd-color-text-secondary); + } + + .mx_Flex { + margin: 0 auto; + max-width: max-content; + flex-wrap: wrap; + justify-content: space-evenly; + } + + .mx_ErrorView_buttons { + margin-top: var(--cpd-space-6x); } } diff --git a/res/themes/element/img/compound/apple.svg b/res/themes/element/img/compound/apple.svg new file mode 100644 index 0000000000..5e9c996e1c --- /dev/null +++ b/res/themes/element/img/compound/apple.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/themes/element/img/compound/fade-arc-light.png b/res/themes/element/img/compound/fade-arc-light.png new file mode 100644 index 0000000000..901ba8ae37 Binary files /dev/null and b/res/themes/element/img/compound/fade-arc-light.png differ diff --git a/res/themes/element/img/compound/linux.svg b/res/themes/element/img/compound/linux.svg new file mode 100644 index 0000000000..6df5e6f959 --- /dev/null +++ b/res/themes/element/img/compound/linux.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/res/themes/element/img/compound/microsoft.svg b/res/themes/element/img/compound/microsoft.svg new file mode 100644 index 0000000000..221dcba716 --- /dev/null +++ b/res/themes/element/img/compound/microsoft.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/res/themes/element/img/logos/element-app-logo.png b/res/themes/element/img/logos/element-app-logo.png new file mode 100644 index 0000000000..54205a09fa Binary files /dev/null and b/res/themes/element/img/logos/element-app-logo.png differ diff --git a/src/async-components/structures/CompatibilityView.tsx b/src/async-components/structures/CompatibilityView.tsx deleted file mode 100644 index a9f1cce51a..0000000000 --- a/src/async-components/structures/CompatibilityView.tsx +++ /dev/null @@ -1,149 +0,0 @@ -/* -Copyright 2020 New Vector Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import React, { ReactNode } from "react"; -import SdkConfig from "matrix-react-sdk/src/SdkConfig"; - -import { _t } from "../../languageHandler"; - -// directly import the style here as this layer does not support rethemedex at this time so no matrix-react-sdk -// PostCSS variables will be accessible. -import "../../../res/css/structures/ErrorView.pcss"; - -interface IProps { - onAccept(): void; -} - -const CompatibilityView: React.FC = ({ onAccept }) => { - const brand = SdkConfig.get("brand"); - const mobileBuilds = SdkConfig.get("mobile_builds"); - - let ios: JSX.Element | undefined; - const iosCustomUrl = mobileBuilds?.ios; - if (iosCustomUrl !== null) { - // could be undefined or a string - ios = ( - <> -

- iOS (iPhone or iPad) -

- - Apple App Store - - - ); - } - - let android = [ -

- Android -

, - ]; - const andCustomUrl = mobileBuilds?.android; - const fdroidCustomUrl = mobileBuilds?.fdroid; - if (andCustomUrl !== null) { - // undefined or string - android.push( - - Google Play Store - , - ); - } - if (fdroidCustomUrl !== null) { - // undefined or string - android.push( - - F-Droid - , - ); - } - if (android.length === 1) { - // just a header, meaning no links - android = []; - } - - let mobileHeader: ReactNode =

{_t("use_brand_on_mobile", { brand })}

; - if (!android.length && !ios) { - mobileHeader = null; - } - - return ( -
-
-
- - Element - -

{_t("incompatible_browser|title")}

-
- -
-
-
-

{_t("incompatible_browser|summary", { brand })}

-

{_t("incompatible_browser|features", { brand })}

-

- {_t( - "incompatible_browser|browser_links", - {}, - { - chromeLink: (sub) => {sub}, - firefoxLink: (sub) => {sub}, - safariLink: (sub) => {sub}, - }, - )} -

-

{_t("incompatible_browser|feature_warning")}

- -
-
-
- -
-
-
- {mobileHeader} - {ios} - {android} -
-
-
- -
-

- - {_t("go_to_element_io")} - -

-
-
-
- ); -}; - -export default CompatibilityView; diff --git a/src/async-components/structures/ErrorView.tsx b/src/async-components/structures/ErrorView.tsx index 25ce1465c0..775e123b3f 100644 --- a/src/async-components/structures/ErrorView.tsx +++ b/src/async-components/structures/ErrorView.tsx @@ -14,9 +14,16 @@ See the License for the specific language governing permissions and limitations under the License. */ -import * as React from "react"; +import React, { ReactNode } from "react"; +import { Text, Heading, Button, Separator } from "@vector-im/compound-web"; +import SdkConfig from "matrix-react-sdk/src/SdkConfig"; +import { Flex } from "matrix-react-sdk/src/components/utils/Flex"; +import PopOutIcon from "@vector-im/compound-design-tokens/assets/web/icons/pop-out"; import { _t } from "../../languageHandler"; +import { Icon as AppleIcon } from "../../../res/themes/element/img/compound/apple.svg"; +import { Icon as MicrosoftIcon } from "../../../res/themes/element/img/compound/microsoft.svg"; +import { Icon as LinuxIcon } from "../../../res/themes/element/img/compound/linux.svg"; // directly import the style here as this layer does not support rethemedex at this time so no matrix-react-sdk // PostCSS variables will be accessible. @@ -26,36 +33,184 @@ interface IProps { // both of these should already be internationalised title: string; messages?: string[]; + footer?: ReactNode; } -const ErrorView: React.FC = ({ title, messages }) => { +export const ErrorView: React.FC = ({ title, messages, footer, children }) => { return ( -
+
+ Element
-
- - Element - -

{_t("failed_to_start")}

-
-
-
-
-

{title}

- {messages && messages.map((msg) =>

{msg}

)} -
-
-
- + + {title} + + {messages?.map((message) => ( + + {message} + + ))} + {children}
+ {footer}
); }; -export default ErrorView; +const MobileAppLinks: React.FC<{ + appleAppStoreUrl?: string; + googlePlayUrl?: string; + fdroidUrl?: string; +}> = ({ appleAppStoreUrl, googlePlayUrl, fdroidUrl }) => ( + + {appleAppStoreUrl && ( + + Apple App Store + + )} + {googlePlayUrl && ( + + Google Play Store + + )} + {fdroidUrl && ( + + F-Droid + + )} + +); + +const DesktopAppLinks: React.FC<{ + macOsUrl?: string; + win64Url?: string; + win32Url?: string; + linuxUrl?: string; +}> = ({ macOsUrl, win64Url, win32Url, linuxUrl }) => { + return ( + + {macOsUrl && ( + + )} + {win64Url && ( + + )} + {win32Url && ( + + )} + {linuxUrl && ( + + )} + + ); +}; + +const linkFactory = + (link: string) => + (text: string): JSX.Element => ( + + {text} + + ); + +export const UnsupportedBrowserView: React.FC<{ + onAccept?(): void; +}> = ({ onAccept }) => { + const config = SdkConfig.get(); + const brand = config.brand ?? "Element"; + + const hasDesktopBuilds = + config.desktop_builds?.available && + (config.desktop_builds?.url_macos || + config.desktop_builds?.url_win64 || + config.desktop_builds?.url_win32 || + config.desktop_builds?.url_linux); + const hasMobileBuilds = Boolean( + config.mobile_builds?.ios || config.mobile_builds?.android || config.mobile_builds?.fdroid, + ); + + return ( + + {/* We render the apps in the footer as they are wider than the 520px container */} + {(hasDesktopBuilds || hasMobileBuilds) && } + + {hasDesktopBuilds && ( + <> + + {_t("incompatible_browser|use_desktop_heading", { brand })} + + + + )} + + {hasMobileBuilds && ( + <> + + {hasDesktopBuilds + ? _t("incompatible_browser|use_mobile_heading_after_desktop") + : _t("incompatible_browser|use_mobile_heading", { brand })} + + + + )} + + } + > + + {_t( + "incompatible_browser|supported_browsers", + {}, + { + Chrome: linkFactory("https://google.com/chrome"), + Firefox: linkFactory("https://firefox.com"), + Edge: linkFactory("https://microsoft.com/edge"), + Safari: linkFactory("https://apple.com/safari"), + }, + )} + + + + + {onAccept && ( + + )} + + + ); +}; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 39725bfee7..03b38fe373 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -21,20 +21,24 @@ "invalid_json_generic": "Invalid JSON", "misconfigured": "Your Element is misconfigured" }, - "failed_to_start": "Failed to start", - "go_to_element_io": "Go to element.io", "incompatible_browser": { - "browser_links": "Please install Chrome, Firefox, or Safari for the best experience.", - "continue_warning": "I understand the risks and wish to continue", - "feature_warning": "You can continue using your current browser, but some or all features may not work and the look and feel of the application may be incorrect.", - "features": "%(brand)s uses advanced browser features which aren't supported by your current browser.", - "summary": "Your browser can't run %(brand)s", - "title": "Unsupported browser" + "continue": "Continue anyway", + "description": "%(brand)s uses some browser features which are not available in your current browser. %(detail)s", + "detail_can_continue": "If you continue, some features may stop working and there is a risk that you may lose data in the future.", + "detail_no_continue": "Try updating this browser if you're not using the latest version and try again.", + "learn_more": "Learn more", + "linux": "Linux", + "macos": "Mac", + "supported_browsers": "For the best experience, use Chrome, Firefox, Edge, or Safari.", + "title": "%(brand)s does not support this browser", + "use_desktop_heading": "Use %(brand)s Desktop instead", + "use_mobile_heading": "Use %(brand)s on mobile instead", + "use_mobile_heading_after_desktop": "Or use our mobile app", + "windows": "Windows (%(bits)s-bit)" }, "powered_by_matrix": "Powered by Matrix", "powered_by_matrix_with_logo": "Decentralised, encrypted chat & collaboration powered by $matrixLogo", "unknown_device": "Unknown device", - "use_brand_on_mobile": "Use %(brand)s on mobile", "web_default_device_name": "%(appName)s: %(browserName)s on %(osName)s", "welcome_to_element": "Welcome to Element" } diff --git a/src/vector/init.tsx b/src/vector/init.tsx index 76c238d0e4..277321eba4 100644 --- a/src/vector/init.tsx +++ b/src/vector/init.tsx @@ -109,12 +109,10 @@ export async function loadApp(fragParams: {}): Promise { } export async function showError(title: string, messages?: string[]): Promise { - const ErrorView = ( - await import( - /* webpackChunkName: "error-view" */ - "../async-components/structures/ErrorView" - ) - ).default; + const { ErrorView } = await import( + /* webpackChunkName: "error-view" */ + "../async-components/structures/ErrorView" + ); window.matrixChat = ReactDOM.render( , document.getElementById("matrixchat"), @@ -122,14 +120,12 @@ export async function showError(title: string, messages?: string[]): Promise void): Promise { - const CompatibilityView = ( - await import( - /* webpackChunkName: "compatibility-view" */ - "../async-components/structures/CompatibilityView" - ) - ).default; + const { UnsupportedBrowserView } = await import( + /* webpackChunkName: "error-view" */ + "../async-components/structures/ErrorView" + ); window.matrixChat = ReactDOM.render( - , + , document.getElementById("matrixchat"), ); } diff --git a/test/unit-tests/async-components/structures/ErrorView-test.tsx b/test/unit-tests/async-components/structures/ErrorView-test.tsx index 773c8106fd..266511dba7 100644 --- a/test/unit-tests/async-components/structures/ErrorView-test.tsx +++ b/test/unit-tests/async-components/structures/ErrorView-test.tsx @@ -16,8 +16,9 @@ limitations under the License. import * as React from "react"; import { render } from "@testing-library/react"; +import SdkConfig from "matrix-react-sdk/src/SdkConfig"; -import ErrorView from "../../../../src/async-components/structures/ErrorView"; +import { ErrorView, UnsupportedBrowserView } from "../../../../src/async-components/structures/ErrorView"; import { setupLanguageMock } from "../../../setup/setupLanguage"; describe("", () => { @@ -30,3 +31,15 @@ describe("", () => { expect(asFragment()).toMatchSnapshot(); }); }); + +describe("", () => { + beforeEach(() => { + setupLanguageMock(); + SdkConfig.put({}); + }); + + it("should match snapshot", () => { + const { asFragment } = render(); + expect(asFragment()).toMatchSnapshot(); + }); +}); diff --git a/test/unit-tests/async-components/structures/__snapshots__/ErrorView-test.tsx.snap b/test/unit-tests/async-components/structures/__snapshots__/ErrorView-test.tsx.snap index 9af471f05b..0bb96dc929 100644 --- a/test/unit-tests/async-components/structures/__snapshots__/ErrorView-test.tsx.snap +++ b/test/unit-tests/async-components/structures/__snapshots__/ErrorView-test.tsx.snap @@ -3,63 +3,248 @@ exports[` should match snapshot 1`] = `
+
-
- -

- Failed to start -

-
-
+

-

-
-

- TITLE -

-

- MSG1 -

-

- MSG2 -

-
-
-
- + MSG2 +

+
+
+
+`; + +exports[` should match snapshot 1`] = ` + +
+ +
+

+ Element does not support this browser +

+

+ Element uses some browser features which are not available in your current browser. Try updating this browser if you're not using the latest version and try again. +

+

+ + For the best experience, use + + Chrome + + , + + Firefox + + , + + Edge + + , or + + Safari + + . + +

+
+ +
+
+