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 16:32:05 +01:00 committed by GitHub
parent 7b89d84acb
commit 4b4289e211
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 263 additions and 308 deletions

View File

@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 957 KiB

View File

@ -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();
});

View File

@ -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();

View File

@ -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";

View File

@ -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;
}
}

View File

@ -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. */

View File

@ -1,191 +0,0 @@
<style type="text/css">
/* we deliberately inline style here to avoid flash-of-CSS problems, and to avoid
* voodoo where we have to set display: none by default
*/
.mx_Header_title::after {
content: "!";
}
.mx_Parent {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
-webkit-box-pack: center;
-webkit-justify-content: center;
-ms-flex-pack: center;
justify-content: center;
-webkit-box-align: center;
-webkit-align-items: center;
-ms-flex-align: center;
align-items: center;
text-align: center;
padding: 25px 35px;
}
.mx_Logo {
height: 54px;
margin-top: 2px;
}
.mx_ButtonGroup {
margin-top: 10px;
}
.mx_ButtonRow {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-justify-content: space-around;
-ms-flex-pack: distribute;
-webkit-box-align: center;
-webkit-align-items: center;
-ms-flex-align: center;
align-items: center;
justify-content: space-between;
box-sizing: border-box;
margin: 12px 0 0;
}
.mx_ButtonRow > * {
margin: 0 10px;
}
.mx_ButtonRow > *:first-child {
margin-left: 0;
}
.mx_ButtonRow > *:last-child {
margin-right: 0;
}
.mx_ButtonParent {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
padding: 10px 20px;
-webkit-box-orient: horizontal;
-webkit-box-direction: normal;
-webkit-flex-direction: row;
-ms-flex-direction: row;
flex-direction: row;
-webkit-box-pack: center;
-webkit-justify-content: center;
-ms-flex-pack: center;
justify-content: center;
-webkit-box-align: center;
-webkit-align-items: center;
-ms-flex-align: center;
align-items: center;
border-radius: 4px;
width: 150px;
background-repeat: no-repeat;
background-position: 10px center;
text-decoration: none;
color: #2e2f32 !important;
}
.mx_ButtonLabel {
margin-left: 20px;
}
.mx_Header_title {
font-size: 24px;
font-weight: 600;
margin: 20px 0 0;
}
.mx_Header_subtitle {
font-size: 12px;
font-weight: normal;
margin: 8px 0 0;
}
.mx_ButtonSignIn {
background-color: #368bd6;
color: white !important;
}
.mx_ButtonCreateAccount {
background-color: #0dbd8b;
color: white !important;
}
.mx_SecondaryButton {
background-color: #ffffff;
color: #2e2f32;
}
.mx_Button_iconSignIn {
background-image: url("welcome/images/icon-sign-in.svg");
}
.mx_Button_iconCreateAccount {
background-image: url("welcome/images/icon-create-account.svg");
}
.mx_Button_iconHelp {
background-image: url("welcome/images/icon-help.svg");
}
.mx_Button_iconRoomDirectory {
background-image: url("welcome/images/icon-room-directory.svg");
}
/*
.mx_WelcomePage_loggedIn is applied by EmbeddedPage from the Welcome component
If it is set on the page, we should show the buttons. Otherwise, we have to assume
we don't have an account and should hide them. No account == no guest account either.
*/
.mx_WelcomePage:not(.mx_WelcomePage_loggedIn) .mx_WelcomePage_guestFunctions {
display: none;
}
.mx_ButtonRow.mx_WelcomePage_guestFunctions {
margin-top: 20px;
}
.mx_ButtonRow.mx_WelcomePage_guestFunctions > div {
margin: 0 auto;
}
@media only screen and (max-width: 480px) {
.mx_ButtonRow {
flex-direction: column;
}
.mx_ButtonRow > * {
margin: 0 0 10px 0;
}
}
</style>
<div class="mx_Parent">
<a href="https://element.io" target="_blank" rel="noopener">
<img src="$logoUrl" alt="$brand" class="mx_Logo" />
</a>
<h1 class="mx_Header_title">_t("welcome_to_element")</h1>
<!-- XXX: Our translations system isn't smart enough to recognize variables in the HTML, so we manually do it -->
<h2 class="mx_Header_subtitle">_t("powered_by_matrix_with_logo")</h2>
<div class="mx_ButtonGroup">
<div class="mx_ButtonRow">
<a href="#/login" class="mx_ButtonParent mx_ButtonSignIn mx_Button_iconSignIn">
<div class="mx_ButtonLabel">_t("action|sign_in")</div>
</a>
<a href="#/register" class="mx_ButtonParent mx_ButtonCreateAccount mx_Button_iconCreateAccount">
<div class="mx_ButtonLabel">_t("action|create_account")</div>
</a>
</div>
<div class="mx_ButtonRow mx_WelcomePage_guestFunctions">
<div>
<a href="#/directory" class="mx_ButtonParent mx_SecondaryButton mx_Button_iconRoomDirectory">
<div class="mx_ButtonLabel">_t("action|explore_rooms")</div>
</a>
</div>
</div>
</div>
</div>

View File

@ -1,3 +0,0 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M17 9C17 13.4183 13.4183 17 9 17C4.58172 17 1 13.4183 1 9C1 4.58172 4.58172 1 9 1C13.4183 1 17 4.58172 17 9ZM5.25 9C5.25 8.58579 5.58579 8.25 6 8.25H8.25V6C8.25 5.58579 8.58579 5.25 9 5.25C9.41421 5.25 9.75 5.58579 9.75 6V8.25H12C12.4142 8.25 12.75 8.58579 12.75 9C12.75 9.41421 12.4142 9.75 12 9.75H9.75V12C9.75 12.4142 9.41421 12.75 9 12.75C8.58579 12.75 8.25 12.4142 8.25 12V9.75H6C5.58579 9.75 5.25 9.41421 5.25 9Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 587 B

View File

@ -1,16 +0,0 @@
<svg width="22px" height="22px" viewBox="0 0 22 22" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="Experiments" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
<g id="Home" transform="translate(-672.000000, -577.000000)" stroke="#000000" stroke-width="1.6">
<g id="Group-11" transform="translate(621.000000, 176.000000)">
<g id="Group-10" transform="translate(39.000000, 391.000000)">
<g id="help-circle" transform="translate(13.000000, 11.000000)">
<circle id="Oval" cx="10" cy="10" r="10"></circle>
<path d="M7.09,7 C7.57543688,5.62004444 8.98538362,4.79140632 10.4271763,5.0387121 C11.868969,5.28601788 12.9221794,6.53715293 12.92,8 C12.92,10 9.92,11 9.92,11" id="Path"></path>
<path d="M10,15 L10.0050017,15.0050017" id="Path"></path>
</g>
</g>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -1,4 +0,0 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M17 9C17 13.4183 13.4183 17 9 17C4.58172 17 1 13.4183 1 9C1 4.58172 4.58172 1 9 1C13.4183 1 17 4.58172 17 9ZM13.375 5.3266C13.5583 4.92826 13.0716 4.44152 12.6733 4.62491L7.66968 6.9285C7.33893 7.08077 7.08014 7.33956 6.92787 7.67031L4.62428 12.6739C4.44089 13.0722 4.92763 13.559 5.32597 13.3756L10.3295 11.072C10.6603 10.9197 10.9191 10.6609 11.0714 10.3302L13.375 5.3266Z" fill="black"/>
<path d="M9.8835 9.88413C9.39534 10.3723 8.60389 10.3723 8.11573 9.88413C7.62757 9.39597 7.62757 8.60452 8.11573 8.11636C8.60389 7.62821 9.39534 7.62821 9.8835 8.11636C10.3717 8.60452 10.3717 9.39597 9.8835 9.88413Z" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 775 B

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 53 KiB

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;

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 }[];
};

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
apps/web/src/branding.ts Normal file
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 + " ");
};

View File

@ -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;

View File

@ -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;

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>
);
}

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": {

View File

@ -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("<MatrixChat />", () => {
// 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: "<h1>Hello</h1>" });
jest.spyOn(Lifecycle, "attemptDelegatedAuthLogin");
// simulate an active window
@ -1668,7 +1666,7 @@ describe("<MatrixChat />", () => {
expect(Lifecycle.attemptDelegatedAuthLogin).toHaveBeenCalled();
// should just show the welcome screen
await rendered.findByText("Hello");
await rendered.findByText("Welcome to Test");
expect(rendered.container).toMatchSnapshot();
});

View File

@ -124,13 +124,9 @@ exports[`<MatrixChat /> Multi-tab lockout waits for other tab to stop during sta
class="mx_AuthPage"
>
<div
class="mx_AuthPage_modal mx_AuthPage_modal_withBlur"
class="mx_AuthPage_modal"
style="position: relative;"
>
<div
class="mx_AuthPage_modalBlur"
style="position: absolute; top: 0px; right: 0px; bottom: 0px; left: 0px; filter: blur(40px);"
/>
<main
aria-live="polite"
class="mx_AuthPage_modalContent"
@ -138,53 +134,99 @@ exports[`<MatrixChat /> Multi-tab lockout waits for other tab to stop during sta
tabindex="-1"
>
<div
class="mx_Welcome"
data-testid="mx_welcome_screen"
class="_glass_sepwu_8"
>
<div
class="mx_WelcomePage mx_WelcomePage_loggedIn"
class="mx_Welcome"
>
<div
class="mx_WelcomePage_body"
class="mx_DefaultWelcome"
>
<h1>
Hello
<a
class="mx_DefaultWelcome_logo"
href="https://element.io"
rel="noopener"
target="_blank"
>
<img
alt="Test"
src="themes/element/img/logos/element-logo.svg"
/>
</a>
<h1
class="_typography_6v6n8_153 _font-heading-md-semibold_6v6n8_112"
>
Welcome to Test
</h1>
<div
class="mx_DefaultWelcome_buttons"
>
<a
class="_button_13vu4_8"
data-kind="primary"
data-size="sm"
href="#/login"
role="link"
tabindex="0"
>
Sign in
</a>
<a
class="_button_13vu4_8"
data-kind="secondary"
data-size="sm"
href="#/register"
role="link"
tabindex="0"
>
Create account
</a>
<a
class="_button_13vu4_8"
data-kind="tertiary"
data-size="sm"
href="#/directory"
role="link"
tabindex="0"
>
Explore rooms
</a>
</div>
</div>
</div>
<div
class="mx_Dropdown mx_LanguageDropdown mx_AuthBody_language"
>
<div
aria-describedby="mx_LanguageDropdown_value"
aria-expanded="false"
aria-haspopup="listbox"
aria-label="Language Dropdown"
aria-owns="mx_LanguageDropdown_input"
class="mx_AccessibleButton mx_Dropdown_input mx_no_textinput"
role="button"
tabindex="0"
class="mx_Dropdown mx_LanguageDropdown mx_AuthBody_language"
>
<div
class="mx_Dropdown_option"
id="mx_LanguageDropdown_value"
aria-describedby="mx_LanguageDropdown_value"
aria-expanded="false"
aria-haspopup="listbox"
aria-label="Language Dropdown"
aria-owns="mx_LanguageDropdown_input"
class="mx_AccessibleButton mx_Dropdown_input mx_no_textinput"
role="button"
tabindex="0"
>
<div>
English
<div
class="mx_Dropdown_option"
id="mx_LanguageDropdown_value"
>
<div>
English
</div>
</div>
<svg
class="mx_Dropdown_arrow"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 14.95q-.2 0-.375-.062a.9.9 0 0 1-.325-.213l-4.6-4.6a.95.95 0 0 1-.275-.7q0-.425.275-.7a.95.95 0 0 1 .7-.275q.425 0 .7.275l3.9 3.9 3.9-3.9a.95.95 0 0 1 .7-.275q.425 0 .7.275a.95.95 0 0 1 .275.7.95.95 0 0 1-.275.7l-4.6 4.6q-.15.15-.325.212a1.1 1.1 0 0 1-.375.063"
/>
</svg>
</div>
<svg
class="mx_Dropdown_arrow"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 14.95q-.2 0-.375-.062a.9.9 0 0 1-.325-.213l-4.6-4.6a.95.95 0 0 1-.275-.7q0-.425.275-.7a.95.95 0 0 1 .7-.275q.425 0 .7.275l3.9 3.9 3.9-3.9a.95.95 0 0 1 .7-.275q.425 0 .7.275a.95.95 0 0 1 .275.7.95.95 0 0 1-.275.7l-4.6 4.6q-.15.15-.325.212a1.1 1.1 0 0 1-.375.063"
/>
</svg>
</div>
</div>
</div>

View File

@ -711,8 +711,6 @@ export default (env: string, argv: Record<string, any>): 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") },

View File

@ -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).