Implement new design for Welcome page (#33211)

* Convert welcome.html to React component

In advance of changes to use Compound

* Fix types

* Fix tests

* Update styling to match Figma

* Fix random capitalisation

* Tweak styling

* Regenerate i18n

* Update tests

* Make linter happy

* Iterate
This commit is contained in:
Michael Telatynski
2026-04-22 15:32:05 +00:00
committed by GitHub
parent 7b89d84acb
commit 4b4289e211
24 changed files with 263 additions and 308 deletions
+10
View File
@@ -52,3 +52,13 @@ export type AtLeastOne<T, U = { [K in keyof T]: Pick<T, K> }> = Partial<T> & U[k
export type Assignable<Object, Item> = {
[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> = T extends object
? {
[P in keyof T]?: DeepPartial<T[P]>;
}
: T;
+3 -2
View File
@@ -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 }[];
};
+8 -3
View File
@@ -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<IConfigOptions> = {
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<IConfigOptions, typeof DEFAULTS>;
function mergeConfig(
config: DeepReadonly<IConfigOptions>,
changes: DeepReadonly<Partial<IConfigOptions>>,
changes: DeepReadonly<DeepPartial<IConfigOptions>>,
): DeepReadonly<IConfigOptions> {
// 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<ConfigOptions>): void {
public static add(cfg: DeepPartial<ConfigOptions>): void {
SdkConfig.put(mergeConfig(SdkConfig.get(), cfg));
}
}
+20
View File
@@ -0,0 +1,20 @@
/*
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";
const ELEMENT_BRAND = "Element";
/**
* Returns whether the app is currently branded.
* This is currently a naive check of whether the `brand` config starts with the substring `Element ` or is the literal `Element`,
* which correctly covers `Element` (release), `Element Nightly` & `Element Pro`.
*/
export const isElementBranded = (): boolean => {
const brand = SdkConfig.get("brand");
return brand === ELEMENT_BRAND || brand.startsWith(ELEMENT_BRAND + " ");
};
@@ -31,16 +31,13 @@ export default class AuthPage extends React.PureComponent<React.PropsWithChildre
if (AuthPage.welcomeBackgroundUrl) return AuthPage.welcomeBackgroundUrl;
const brandingConfig = SdkConfig.getObject("branding");
AuthPage.welcomeBackgroundUrl = "themes/element/img/backgrounds/lake.jpg";
const configuredUrl = brandingConfig?.get("welcome_background_url");
if (configuredUrl) {
if (Array.isArray(configuredUrl)) {
const index = Math.floor(Math.random() * configuredUrl.length);
AuthPage.welcomeBackgroundUrl = configuredUrl[index];
} else {
AuthPage.welcomeBackgroundUrl = configuredUrl;
}
const urls = brandingConfig.get("welcome_background_url");
if (Array.isArray(urls)) {
const index = Math.floor(Math.random() * urls.length);
AuthPage.welcomeBackgroundUrl = urls[index];
} else {
AuthPage.welcomeBackgroundUrl = urls;
}
return AuthPage.welcomeBackgroundUrl;
@@ -0,0 +1,51 @@
/*
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 React from "react";
import { Button, Heading, Text } from "@vector-im/compound-web";
import { _t } from "../../../languageHandler";
import SdkConfig from "../../../SdkConfig.ts";
import { MatrixClientPeg } from "../../../MatrixClientPeg.ts";
import { isElementBranded } from "../../../branding.ts";
const DefaultWelcome: React.FC = () => {
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 (
<div className="mx_DefaultWelcome">
<a href={branding.get("logo_link_url")} target="_blank" rel="noopener" className="mx_DefaultWelcome_logo">
<img src={logoUrl} alt={brand} />
</a>
<Heading as="h1" weight="semibold">
{isElement ? _t("welcome|title_element") : _t("welcome|title_generic", { brand })}
</Heading>
{isElement && <Text size="md">{_t("welcome|tagline_element")}</Text>}
<div className="mx_DefaultWelcome_buttons">
<Button as="a" href="#/login" kind="primary" size="sm">
{_t("action|sign_in")}
</Button>
<Button as="a" href="#/register" kind="secondary" size="sm">
{_t("action|create_account")}
</Button>
{showGuestFunctions && (
<Button as="a" href="#/directory" kind="tertiary" size="sm">
{_t("action|explore_rooms")}
</Button>
)}
</div>
</div>
);
};
export default DefaultWelcome;
+20 -21
View File
@@ -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<EmptyObject> {
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<string, string> = {
"$brand": SdkConfig.get("brand"),
@@ -33,25 +32,25 @@ export default class Welcome extends React.PureComponent<EmptyObject> {
"[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 = <EmbeddedPage className="mx_WelcomePage" url={pageUrl} replaceMap={replaceMap} />;
} else {
body = <DefaultWelcome />;
}
return (
<AuthPage>
<div
className={classNames("mx_Welcome", {
mx_WelcomePage_registrationDisabled: !SettingsStore.getValue(UIFeature.Registration),
})}
data-testid="mx_welcome_screen"
>
<EmbeddedPage className="mx_WelcomePage" url={pageUrl} replaceMap={replaceMap} />
<LanguageSelector />
</div>
<AuthPage addBlur={false}>
<Glass>
<div
className={classNames("mx_Welcome", {
mx_WelcomePage_registrationDisabled: !SettingsStore.getValue(UIFeature.Registration),
})}
>
{body}
<LanguageSelector />
</div>
</Glass>
</AuthPage>
);
}
+6 -3
View File
@@ -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 &amp; 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": {