diff --git a/.github/workflows/static_analysis.yaml b/.github/workflows/static_analysis.yaml index 44e37f6ac64..895ea83dc2f 100644 --- a/.github/workflows/static_analysis.yaml +++ b/.github/workflows/static_analysis.yaml @@ -103,7 +103,7 @@ jobs: voip|element_call error|invalid_json error|misconfigured - welcome_to_element + welcome|title_element devtools|settings|elementCallUrl labs|sliding_sync_description settings|voip|noise_suppression_description diff --git a/apps/desktop/playwright/snapshots/launch/launch.spec.ts/App-launch-should-launch-and-render-the-welcome-view-successfully-1-linux.png b/apps/desktop/playwright/snapshots/launch/launch.spec.ts/App-launch-should-launch-and-render-the-welcome-view-successfully-1-linux.png index 7bcf260d521..baec548402d 100644 Binary files a/apps/desktop/playwright/snapshots/launch/launch.spec.ts/App-launch-should-launch-and-render-the-welcome-view-successfully-1-linux.png and b/apps/desktop/playwright/snapshots/launch/launch.spec.ts/App-launch-should-launch-and-render-the-welcome-view-successfully-1-linux.png differ diff --git a/apps/web/playwright/e2e/app-loading/guest-registration.spec.ts b/apps/web/playwright/e2e/app-loading/guest-registration.spec.ts index 960b6a66927..d5679bc017d 100644 --- a/apps/web/playwright/e2e/app-loading/guest-registration.spec.ts +++ b/apps/web/playwright/e2e/app-loading/guest-registration.spec.ts @@ -20,7 +20,7 @@ test.use({ test("Shows the welcome page by default", async ({ page }) => { await page.goto("/"); - await expect(page.getByRole("heading", { name: "Welcome to Element!" })).toBeVisible(); + await expect(page.getByRole("heading", { name: "Be in your element" })).toBeVisible(); await expect(page.getByRole("link", { name: "Sign in" })).toBeVisible(); }); diff --git a/apps/web/playwright/e2e/login/login-consent.spec.ts b/apps/web/playwright/e2e/login/login-consent.spec.ts index 19e095f50ab..be8b9669a22 100644 --- a/apps/web/playwright/e2e/login/login-consent.spec.ts +++ b/apps/web/playwright/e2e/login/login-consent.spec.ts @@ -126,7 +126,7 @@ test.describe("Login", () => { await page.goto("/"); // Should give us the welcome page initially - await expect(page.getByRole("heading", { name: "Welcome to Element!" })).toBeVisible(); + await expect(page.getByRole("heading", { name: "Be in your element" })).toBeVisible(); // Start the login process await expect(axe).toHaveNoViolations(); diff --git a/apps/web/res/css/_components.pcss b/apps/web/res/css/_components.pcss index c208a71ef7f..044a06bcb03 100644 --- a/apps/web/res/css/_components.pcss +++ b/apps/web/res/css/_components.pcss @@ -105,6 +105,7 @@ @import "./views/auth/_AuthPage.pcss"; @import "./views/auth/_CompleteSecurityBody.pcss"; @import "./views/auth/_CountryDropdown.pcss"; +@import "./views/auth/_DefaultWelcome.pcss"; @import "./views/auth/_InteractiveAuthEntryComponents.pcss"; @import "./views/auth/_LanguageSelector.pcss"; @import "./views/auth/_LoginWithQR.pcss"; diff --git a/apps/web/res/css/views/auth/_DefaultWelcome.pcss b/apps/web/res/css/views/auth/_DefaultWelcome.pcss new file mode 100644 index 00000000000..08183e77b10 --- /dev/null +++ b/apps/web/res/css/views/auth/_DefaultWelcome.pcss @@ -0,0 +1,43 @@ +/* +Copyright 2026 Element Creations Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +.mx_DefaultWelcome { + text-align: center; + + .mx_DefaultWelcome_logo img { + height: 48px; + aspect-ratio: auto; + display: block; + margin: 0 auto; + } + + h1 { + margin: var(--cpd-space-4x) 0 var(--cpd-space-2x); + } + + p { + color: var(--cpd-color-text-secondary); + margin-top: var(--cpd-space-2x); + } + + .mx_DefaultWelcome_buttons { + margin: var(--cpd-space-6x) 0 var(--cpd-space-1x); + padding-bottom: var(--cpd-space-4x); + border-bottom: 1px solid var(--cpd-color-separator-primary); + + a { + width: 380px; + margin-bottom: var(--cpd-space-4x); + } + } +} + +.mx_WelcomePage_registrationDisabled { + .mx_DefaultWelcome_buttons_register { + display: none; + } +} diff --git a/apps/web/res/css/views/auth/_Welcome.pcss b/apps/web/res/css/views/auth/_Welcome.pcss index 50a91aa7671..12598f32931 100644 --- a/apps/web/res/css/views/auth/_Welcome.pcss +++ b/apps/web/res/css/views/auth/_Welcome.pcss @@ -9,6 +9,10 @@ Please see LICENSE files in the repository root for full details. display: flex; flex-direction: column; align-items: center; + background-color: var(--cpd-color-bg-canvas-default); + box-sizing: border-box; + padding: var(--cpd-space-11x) var(--cpd-space-12x) var(--cpd-space-4x); + &.mx_WelcomePage_registrationDisabled { .mx_ButtonCreateAccount { display: none; @@ -18,7 +22,7 @@ Please see LICENSE files in the repository root for full details. .mx_Welcome .mx_AuthBody_language { width: 160px; - margin-bottom: 10px; + margin: var(--cpd-space-1x) 0; } /* Invert image colours in dark mode. */ diff --git a/apps/web/res/welcome.html b/apps/web/res/welcome.html deleted file mode 100644 index f1cf3911f40..00000000000 --- a/apps/web/res/welcome.html +++ /dev/null @@ -1,191 +0,0 @@ - - -
- - - -

_t("welcome_to_element")

- -

_t("powered_by_matrix_with_logo")

-
- - -
-
diff --git a/apps/web/res/welcome/images/icon-create-account.svg b/apps/web/res/welcome/images/icon-create-account.svg deleted file mode 100644 index 7bbef7f632c..00000000000 --- a/apps/web/res/welcome/images/icon-create-account.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/apps/web/res/welcome/images/icon-help.svg b/apps/web/res/welcome/images/icon-help.svg deleted file mode 100644 index dc96f8e0cf0..00000000000 --- a/apps/web/res/welcome/images/icon-help.svg +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/apps/web/res/welcome/images/icon-room-directory.svg b/apps/web/res/welcome/images/icon-room-directory.svg deleted file mode 100644 index 3786ce11535..00000000000 --- a/apps/web/res/welcome/images/icon-room-directory.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/apps/web/res/welcome/images/icon-sign-in.svg b/apps/web/res/welcome/images/icon-sign-in.svg deleted file mode 100644 index 9bc2fefa3f6..00000000000 --- a/apps/web/res/welcome/images/icon-sign-in.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/apps/web/src/@types/common.ts b/apps/web/src/@types/common.ts index 4c8c707c4af..bfb4f64c16b 100644 --- a/apps/web/src/@types/common.ts +++ b/apps/web/src/@types/common.ts @@ -52,3 +52,13 @@ export type AtLeastOne }> = Partial & U[k export type Assignable = { [Key in keyof Object]: Object[Key] extends Item ? Key : never; }[keyof Object]; + +/** + * Like `Partial` but for applied to all nested objects. + * Based on https://dev.to/perennialautodidact/adventures-in-typescript-deeppartial-2f2a + */ +export type DeepPartial = T extends object + ? { + [P in keyof T]?: DeepPartial; + } + : T; diff --git a/apps/web/src/IConfigOptions.ts b/apps/web/src/IConfigOptions.ts index fd39e2f8df8..a3a6a32c91d 100644 --- a/apps/web/src/IConfigOptions.ts +++ b/apps/web/src/IConfigOptions.ts @@ -52,8 +52,9 @@ export interface IConfigOptions { disable_3pid_login?: boolean; brand: string; - branding?: { - welcome_background_url?: string | string[]; // chosen at random if array + branding: { + welcome_background_url: string | string[]; // chosen at random if array + logo_link_url: string; auth_header_logo_url?: string; auth_footer_links?: { text: string; url: string }[]; }; diff --git a/apps/web/src/SdkConfig.ts b/apps/web/src/SdkConfig.ts index 1a26e4f815e..4be64648d8e 100644 --- a/apps/web/src/SdkConfig.ts +++ b/apps/web/src/SdkConfig.ts @@ -12,11 +12,16 @@ import { mergeWith } from "lodash"; import { SnakedObject } from "./utils/SnakedObject"; import { type IConfigOptions } from "./IConfigOptions"; import { isObject, objectClone } from "./utils/objects"; -import { type DeepReadonly, type Defaultize } from "./@types/common"; +import { type DeepPartial, type DeepReadonly, type Defaultize } from "./@types/common"; // see element-web config.md for docs, or the IConfigOptions interface for dev docs export const DEFAULTS: DeepReadonly = { brand: "Element", + branding: { + logo_link_url: "https://element.io", + auth_header_logo_url: "themes/element/img/logos/element-logo.svg", + welcome_background_url: "themes/element/img/backgrounds/lake.jpg", + }, help_url: "https://element.io/help", help_encryption_url: "https://element.io/help#encryption", help_key_storage_url: "https://element.io/help#encryption5", @@ -70,7 +75,7 @@ export type ConfigOptions = Defaultize; function mergeConfig( config: DeepReadonly, - changes: DeepReadonly>, + changes: DeepReadonly>, ): DeepReadonly { // return { ...config, ...changes }; return mergeWith(objectClone(config), changes, (objValue, srcValue) => { @@ -136,7 +141,7 @@ export default class SdkConfig { SdkConfig.setInstance(mergeConfig(DEFAULTS, {})); // safe to cast - defaults will be applied } - public static add(cfg: Partial): void { + public static add(cfg: DeepPartial): void { SdkConfig.put(mergeConfig(SdkConfig.get(), cfg)); } } diff --git a/apps/web/src/branding.ts b/apps/web/src/branding.ts new file mode 100644 index 00000000000..bfdc3f4d7f8 --- /dev/null +++ b/apps/web/src/branding.ts @@ -0,0 +1,18 @@ +/* +Copyright 2026 Element Creations Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +import SdkConfig from "./SdkConfig.ts"; + +/** + * Returns whether the app is currently branded. + * This is currently a naive check of whether the `brand` config starts with the substring `Element`, + * which correctly covers `Element` (release), `Element Nightly` & `Element Pro`. + */ +export const isElementBranded = (): boolean => { + const brand = SdkConfig.get("brand"); + return brand.startsWith("Element"); +}; diff --git a/apps/web/src/components/views/auth/AuthPage.tsx b/apps/web/src/components/views/auth/AuthPage.tsx index adc901f6c96..8d1c56ea7cc 100644 --- a/apps/web/src/components/views/auth/AuthPage.tsx +++ b/apps/web/src/components/views/auth/AuthPage.tsx @@ -31,16 +31,13 @@ export default class AuthPage extends React.PureComponent { + const brand = SdkConfig.get("brand"); + const branding = SdkConfig.getObject("branding"); + const logoUrl = branding.get("auth_header_logo_url"); + + const showGuestFunctions = !!MatrixClientPeg.get(); + const isElement = isElementBranded(); + + return ( +
+ + {brand} + + + {isElement ? _t("welcome|title_element") : _t("welcome|title_generic", { brand })} + + {isElement && {_t("welcome|tagline_element")}} + +
+ + + {showGuestFunctions && ( + + )} +
+
+ ); +}; + +export default DefaultWelcome; diff --git a/apps/web/src/components/views/auth/Welcome.tsx b/apps/web/src/components/views/auth/Welcome.tsx index e2b64028e41..f8c00b2127d 100644 --- a/apps/web/src/components/views/auth/Welcome.tsx +++ b/apps/web/src/components/views/auth/Welcome.tsx @@ -5,9 +5,10 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React from "react"; +import React, { type ReactNode } from "react"; import classNames from "classnames"; import { type EmptyObject } from "matrix-js-sdk/src/matrix"; +import { Glass } from "@vector-im/compound-web"; import SdkConfig from "../../../SdkConfig"; import AuthPage from "./AuthPage"; @@ -16,14 +17,12 @@ import { UIFeature } from "../../../settings/UIFeature"; import LanguageSelector from "./LanguageSelector"; import EmbeddedPage from "../../structures/EmbeddedPage"; import { MATRIX_LOGO_HTML } from "../../structures/static-page-vars"; +import DefaultWelcome from "./DefaultWelcome.tsx"; export default class Welcome extends React.PureComponent { public render(): React.ReactNode { const pagesConfig = SdkConfig.getObject("embedded_pages"); - let pageUrl: string | undefined; - if (pagesConfig) { - pageUrl = pagesConfig.get("welcome_url"); - } + const pageUrl = pagesConfig?.get("welcome_url"); const replaceMap: Record = { "$brand": SdkConfig.get("brand"), @@ -33,25 +32,25 @@ export default class Welcome extends React.PureComponent { "[matrix]": MATRIX_LOGO_HTML, }; - if (!pageUrl) { - // Fall back to default and replace $logoUrl in welcome.html - const brandingConfig = SdkConfig.getObject("branding"); - const logoUrl = brandingConfig?.get("auth_header_logo_url") ?? "themes/element/img/logos/element-logo.svg"; - replaceMap["$logoUrl"] = logoUrl; - pageUrl = "welcome.html"; + let body: ReactNode; + if (pageUrl) { + body = ; + } else { + body = ; } return ( - -
- - -
+ + +
+ {body} + +
+
); } diff --git a/apps/web/src/i18n/strings/en_EN.json b/apps/web/src/i18n/strings/en_EN.json index ec66fdade77..b216dc45bdf 100644 --- a/apps/web/src/i18n/strings/en_EN.json +++ b/apps/web/src/i18n/strings/en_EN.json @@ -42,7 +42,7 @@ "copy_link": "Copy link", "create": "Create", "create_a_room": "Create a room", - "create_account": "Create Account", + "create_account": "Create account", "decline": "Decline", "decline_and_block": "Decline and block", "decline_invite": "Decline invite", @@ -1816,7 +1816,6 @@ "restricted": "Restricted" }, "powered_by_matrix": "Powered by Matrix", - "powered_by_matrix_with_logo": "Decentralised, encrypted chat & collaboration powered by $matrixLogo", "presence": { "away": "Away", "busy": "Busy", @@ -3981,7 +3980,11 @@ "you_are_presenting": "You are presenting" }, "web_default_device_name": "%(appName)s: %(browserName)s on %(osName)s", - "welcome_to_element": "Welcome to Element", + "welcome": { + "tagline_element": "Supercharged for speed and simplicity.", + "title_element": "Be in your element", + "title_generic": "Welcome to %(brand)s" + }, "widget": { "added_by": "Widget added by", "capabilities_dialog": { diff --git a/apps/web/test/unit-tests/components/structures/MatrixChat-test.tsx b/apps/web/test/unit-tests/components/structures/MatrixChat-test.tsx index 8287cc03c8e..226decf6e2b 100644 --- a/apps/web/test/unit-tests/components/structures/MatrixChat-test.tsx +++ b/apps/web/test/unit-tests/components/structures/MatrixChat-test.tsx @@ -9,7 +9,6 @@ Please see LICENSE files in the repository root for full details. import "fake-indexeddb/auto"; import React, { type ComponentProps } from "react"; import { fireEvent, render, type RenderResult, screen, waitFor, within, act } from "jest-matrix-react"; -import fetchMock from "@fetch-mock/jest"; import { type Mocked, mocked } from "jest-mock"; import { ClientEvent, type MatrixClient, MatrixEvent, Room, SyncState } from "matrix-js-sdk/src/matrix"; import { type MediaHandler } from "matrix-js-sdk/src/webrtc/mediaHandler"; @@ -1637,7 +1636,6 @@ describe("", () => { // Flaky test, see https://github.com/element-hq/element-web/issues/30337 it("waits for other tab to stop during startup", async () => { - fetchMock.get("end:/welcome.html", { body: "

Hello

" }); jest.spyOn(Lifecycle, "attemptDelegatedAuthLogin"); // simulate an active window @@ -1668,7 +1666,7 @@ describe("", () => { expect(Lifecycle.attemptDelegatedAuthLogin).toHaveBeenCalled(); // should just show the welcome screen - await rendered.findByText("Hello"); + await rendered.findByText("Welcome to Test"); expect(rendered.container).toMatchSnapshot(); }); diff --git a/apps/web/test/unit-tests/components/structures/__snapshots__/MatrixChat-test.tsx.snap b/apps/web/test/unit-tests/components/structures/__snapshots__/MatrixChat-test.tsx.snap index 1607005b1de..03220e62db8 100644 --- a/apps/web/test/unit-tests/components/structures/__snapshots__/MatrixChat-test.tsx.snap +++ b/apps/web/test/unit-tests/components/structures/__snapshots__/MatrixChat-test.tsx.snap @@ -124,13 +124,9 @@ exports[` Multi-tab lockout waits for other tab to stop during sta class="mx_AuthPage" >
-
Multi-tab lockout waits for other tab to stop during sta tabindex="-1" >
-

- Hello + +

+ Welcome to Test

+
-
-
diff --git a/apps/web/webpack.config.ts b/apps/web/webpack.config.ts index f6b0c0c6d97..28b71436ad7 100644 --- a/apps/web/webpack.config.ts +++ b/apps/web/webpack.config.ts @@ -711,8 +711,6 @@ export default (env: string, argv: Record): webpack.Configuration = "res/jitsi_external_api.min.js", "res/jitsi_external_api.min.js.LICENSE.txt", "res/manifest.json", - "res/welcome.html", - { from: "welcome/**", context: path.resolve(__dirname, "res") }, { from: "themes/**", context: path.resolve(__dirname, "res") }, { from: "vector-icons/**", context: path.resolve(__dirname, "res") }, { from: "decoder-ring/**", context: path.resolve(__dirname, "res") }, diff --git a/docs/config.md b/docs/config.md index 113255674af..046a737447e 100644 --- a/docs/config.md +++ b/docs/config.md @@ -214,14 +214,15 @@ Starting with `branding`, the following subproperties are available: 1. `welcome_background_url`: When a string, the URL for the full-page image background of the login, registration, and welcome pages. This property can additionally be an array to have the app choose an image at random from the selections. -2. `auth_header_logo_url`: A URL to the logo used on the login, registration, etc pages. -3. `auth_footer_links`: A list of links to add to the footer during login, registration, etc. Each entry must have a `text` and +2. `logo_link_url`: When rendering the a brand Logo, if it is linkified, this is the link it should direct to. Defaults to `https://element.io`. +3. `auth_header_logo_url`: A URL to the logo used on the login, registration, etc pages. +4. `auth_footer_links`: A list of links to add to the footer during login, registration, etc. Each entry must have a `text` and `url` property. `embedded_pages` can be configured as such: -1. `welcome_url`: A URL to an HTML page to show as a welcome page (landing on `#/welcome`). When not specified, the default - `welcome.html` that ships with Element will be used instead. +1. `welcome_url`: A URL to an HTML page to show as a welcome page (landing on `#/welcome`). + When not specified, a default internal component will be used instead. 2. `home_url`: A URL to an HTML page to show within the app as the "home" page. When the app doesn't have a room/screen to show the user, it will use the home page instead. The home page is additionally accessible from the user menu. By default, no home page is set and therefore a hardcoded landing screen is used. More documentation and examples are [here](./custom-home.md).