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:
@@ -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;
|
||||
|
||||
@@ -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 }[];
|
||||
};
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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": {
|
||||
|
||||
Reference in New Issue
Block a user