Absorb remainder of element-modules into monorepo

This commit is contained in:
Michael Telatynski
2026-06-08 10:32:12 +01:00
112 changed files with 4569 additions and 8 deletions
+22
View File
@@ -0,0 +1,22 @@
# @element-hq/element-web-module-restricted-guests
Restricted Guests module for Element Web.
Supports the following configuration options under the configuration key `io.element.element-web-modules.restricted-guests`:
| Key | Type | Description |
| ------------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| guest_user_homeserver_url | string | URL of the homeserver on which to register the guest, must be running the synapse module. |
| guest_user_prefix | string | Prefix to apply to all guests registered via the module, defaults to `@guest-`. |
| skip_single_sign_on | boolean | If true, the user will be forwarded to the login page instead of to the SSO login. This is only required if the home server has no SSO support. |
## Copyright & License
Copyright (c) 2025 New Vector Ltd
This software is multi licensed by New Vector Ltd (Element). It can be used either:
(1) for free under the terms of the GNU Affero General Public License (as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version); OR
(2) under the terms of a paid-for Element Commercial License agreement between you and Element (the terms of which may vary depending on what you and Element have agreed to).
Unless required by applicable law or agreed to in writing, software distributed under the Licenses is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the Licenses for the specific language governing permissions and limitations under the Licenses.
@@ -0,0 +1,304 @@
/*
Copyright 2025 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import {
type MasConfig,
type StartedMatrixAuthenticationServiceContainer,
type StartedSynapseContainer,
type SynapseContainer,
} from "@element-hq/element-web-playwright-common/lib/testcontainers/index.js";
import { type Credentials } from "@element-hq/element-web-playwright-common/lib/utils/api";
import { makePostgres, makeMas } from "@element-hq/element-web-playwright-common/lib/testcontainers/index.js";
import { RestrictedGuestsSynapseContainer, RestrictedGuestsSynapseWithMasContainer } from "./services";
import { test as subBase, expect } from "../../playwright/element-web-test";
const MAS_CLIENT_ID = "01ARZ3NDEKTSV4RRFFQ69G5FAV";
const MAS_CLIENT_SECRET = "restricted-guests-secret";
const MAS_SHARED_SECRET = "restricted-guests-shared-secret";
const MAS_INTERNAL_URL = "http://guest-mas:8080";
const GUEST_HOMESERVER_NAME = "guest-homeserver";
const MAS_HTTP_LISTENERS: NonNullable<MasConfig["http"]>["listeners"] = [
{
name: "web",
resources: [
{ name: "discovery" },
{ name: "human" },
{ name: "oauth" },
{ name: "compat" },
{ name: "graphql" },
{ name: "assets" },
{ name: "adminapi" },
],
binds: [
{
address: "[::]:8080",
},
],
proxy_protocol: false,
},
{
name: "internal",
resources: [
{
name: "health",
},
],
binds: [
{
address: "[::]:8081",
},
],
proxy_protocol: false,
},
];
const BASE_MAS_CONFIG: Partial<MasConfig> = {
http: {
listeners: MAS_HTTP_LISTENERS,
public_base: "",
},
policy: {
data: {
admin_clients: [MAS_CLIENT_ID],
client_registration: {
allow_insecure_uris: true,
},
},
},
clients: [
{
client_id: MAS_CLIENT_ID,
client_auth_method: "client_secret_basic",
client_secret: MAS_CLIENT_SECRET,
},
],
};
declare module "@element-hq/element-web-module-api" {
export interface Config {
embedded_pages?: {
login_for_welcome?: boolean;
};
}
}
// We do some wacky things here in order to run the test suite against multiple homeserver configurations
const base = subBase.extend<
{
testRoomId: string;
},
{
auth: "mas" | "legacy";
bot: Credentials;
guestMas?: StartedMatrixAuthenticationServiceContainer;
guestHomeserver: StartedSynapseContainer;
}
>({
testRoomId: [
async ({ homeserver, bot }, use) => {
const { room_id: roomId } = (await homeserver.csApi.request("POST", "/v3/createRoom", bot.accessToken, {
name: "Test room",
preset: "public_chat",
topic: "All about happy hour",
initial_state: [
{
// This is required to allow guests to join the room with this Synapse module
type: "m.room.join_rule",
state_key: "",
content: { join_rule: "knock" },
},
],
})) as { room_id: string };
await use(roomId);
},
{ scope: "test" },
],
bot: [
async ({ homeserver }, use) => {
const bot = await homeserver.registerUser("bot", "pAs5w0rD!", "Bot");
await use(bot);
},
{ scope: "worker" },
],
auth: ["mas", { scope: "worker" }],
// Optional MAS on the default homeserver, enabled only when we are testing the non-guest login UX
mas: [
async ({ logger, network, postgres, auth, synapseConfig }, use) => {
if (auth !== "mas" || synapseConfig.allow_guest_access !== false) {
return use(undefined);
}
const container = await makeMas(
postgres,
network,
logger,
{
...BASE_MAS_CONFIG,
matrix: {
kind: "synapse",
homeserver: "homeserver",
endpoint: "http://homeserver:8008",
secret: MAS_SHARED_SECRET,
},
},
"mas",
);
await use(container);
await container.stop();
},
{ scope: "worker" },
],
// Optional MAS on the module homeserver
guestMas: [
async ({ logger, network, auth }, use) => {
if (auth !== "mas") {
return use(undefined);
}
// We need a separate postgres so it doesn't fight with the default MAS
const postgres = await makePostgres(network, logger, "guest-mas-postgres");
const container = await makeMas(
postgres,
network,
logger,
{
...BASE_MAS_CONFIG,
matrix: {
kind: "synapse",
homeserver: GUEST_HOMESERVER_NAME,
endpoint: "http://guest-homeserver:8008",
secret: MAS_SHARED_SECRET,
},
},
"guest-mas",
);
await use(container);
await container.stop();
await postgres.stop();
},
{ scope: "worker" },
],
// Module homeserver
guestHomeserver: [
async ({ logger, synapseConfig, network, guestMas }, use) => {
let container: SynapseContainer;
if (guestMas) {
container = new RestrictedGuestsSynapseWithMasContainer({
adminApiBaseUrl: MAS_INTERNAL_URL,
oauthBaseUrl: MAS_INTERNAL_URL,
clientId: MAS_CLIENT_ID,
clientSecret: MAS_CLIENT_SECRET,
}).withMatrixAuthenticationService(guestMas);
} else {
container = new RestrictedGuestsSynapseContainer();
}
const startedContainer = await container
.withConfig(synapseConfig)
.withConfig({
server_name: GUEST_HOMESERVER_NAME,
})
.withNetwork(network)
.withNetworkAliases(GUEST_HOMESERVER_NAME)
.withLogConsumer(logger.getConsumer("guest_homeserver"))
.start();
await use(startedContainer);
await startedContainer.stop();
},
{ scope: "worker" },
],
displayName: "Tommy",
labsFlags: ["feature_ask_to_join"],
config: {
embedded_pages: {
login_for_welcome: true,
},
},
});
base.slow();
for (const auth of ["mas", "legacy"] as const) {
for (const guestsEnabled of [true, false]) {
const test = base.extend({
auth,
synapseConfig: {
allow_guest_access: guestsEnabled,
},
});
test.describe(`Restricted guests auth=${auth} guests=${guestsEnabled}`, () => {
test("should error if config is missing", async ({ page }) => {
await page.goto("/");
await expect(page.getByText("Your Element is misconfigured")).toBeVisible();
await expect(page.getByText("Errors in module configuration")).toBeVisible();
});
test.describe("with config", () => {
test.beforeEach(async ({ config, guestHomeserver, page, testRoomId }) => {
config["io.element.element-web-modules.restricted-guests"] = {
guest_user_homeserver_url: guestHomeserver.baseUrl,
};
// Go to a room we are not a member of
await page.goto(`/#/room/${testRoomId}`);
});
if (guestsEnabled) {
// The screenshots between the two auth type tests for guests should be identical.
test(
"should show the default room preview bar for logged in users",
{ tag: ["@screenshot"] },
async ({ page, user, testRoomId }) => {
// Go to a room we are not a member of
await page.goto(`/#/room/${testRoomId}`);
const button = page.getByRole("button", { name: "Join the discussion" });
await expect(button).toBeVisible();
},
);
test(
"should show the module's room preview bar for guests",
{ tag: ["@screenshot"] },
async ({ page }) => {
const button = page.getByRole("button", { name: "Join as guest", exact: true });
await expect(button).toBeVisible();
await expect(page.locator(".mx_RoomPreviewBar")).toMatchScreenshot(`preview-bar.png`);
await button.click();
const dialog = page.getByRole("dialog");
await expect(dialog).toMatchScreenshot(`dialog.png`);
await dialog.getByPlaceholder("Name").fill("Jim");
await dialog.getByRole("button", { name: "Continue as guest" }).click();
await expect(page.getByText("Ask to join?")).toBeVisible();
},
);
} else {
test("should show the module login ux", { tag: ["@screenshot"] }, async ({ page }) => {
const button = page.getByRole("button", { name: "Join as guest", exact: true });
await expect(button).toBeVisible();
await expect(page.getByRole("main")).toMatchScreenshot(`login-${auth}.png`);
await button.click();
const dialog = page.getByRole("dialog");
await expect(dialog).toMatchScreenshot(`dialog-login.png`);
await dialog.getByPlaceholder("Name").fill("Jim");
await dialog.getByRole("button", { name: "Continue as guest" }).click();
await expect(page.getByText("Join the discussion")).toBeVisible();
});
}
});
});
}
}
+63
View File
@@ -0,0 +1,63 @@
/*
Copyright 2025 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import {
StartedSynapseContainer,
SynapseContainer,
} from "@element-hq/element-web-playwright-common/lib/testcontainers/index.js";
import path, { dirname } from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = dirname(fileURLToPath(import.meta.url));
// We use the SynapseContainer as a base to have all of its utilities for config setting
export class RestrictedGuestsSynapseContainer extends SynapseContainer {
protected getModuleConfig(): Record<string, unknown> {
return {};
}
public override async start(): Promise<StartedSynapseContainer> {
this.withCopyDirectoriesToContainer([
{
source: path.resolve(__dirname, "..", "..", "synapse", "synapse_guest_module"),
target: "/modules/synapse_guest_module/",
},
]).withEnvironment({
PYTHONPATH: "/modules",
});
this.config.modules.push({
module: "synapse_guest_module.GuestModule",
config: this.getModuleConfig(),
});
return super.start();
}
}
interface RestrictedGuestsMasModuleConfig {
adminApiBaseUrl: string;
oauthBaseUrl?: string;
clientId: string;
clientSecret: string;
}
export class RestrictedGuestsSynapseWithMasContainer extends RestrictedGuestsSynapseContainer {
public constructor(private readonly masConfig: RestrictedGuestsMasModuleConfig) {
super();
}
protected override getModuleConfig(): Record<string, unknown> {
return {
mas: {
admin_api_base_url: this.masConfig.adminApiBaseUrl,
oauth_base_url: this.masConfig.oauthBaseUrl ?? this.masConfig.adminApiBaseUrl,
client_id: this.masConfig.clientId,
client_secret: this.masConfig.clientSecret,
},
};
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

+35
View File
@@ -0,0 +1,35 @@
{
"name": "@element-hq/element-web-module-restricted-guests",
"private": true,
"version": "0.0.0",
"type": "module",
"main": "lib/index.js",
"license": "SEE LICENSE IN README.md",
"scripts": {
"build": "vite build",
"lint": "pnpm lint:types && pnpm lint:js",
"lint:types": "tsc --noEmit",
"lint:js": "eslint --max-warnings 0 src -c ../.eslintrc.cjs",
"test:playwright": "playwright test -c ../playwright.config.ts",
"test:playwright:open": "yarn test:playwright -c ../playwright.config.ts --ui",
"test:playwright:screenshots": "playwright-screenshots yarn test:playwright --update-snapshots --grep @screenshot"
},
"devDependencies": {
"@arcmantle/vite-plugin-import-css-sheet": "^1.0.12",
"@element-hq/element-web-module-api": "workspace:*",
"@types/node": "^22.10.7",
"@types/react": "catalog:",
"@vitejs/plugin-react": "catalog:",
"react": "catalog:",
"rollup-plugin-external-globals": "^0.13.0",
"typescript": "catalog:",
"vite": "catalog:",
"vite-plugin-node-polyfills": "catalog:"
},
"dependencies": {
"@vector-im/compound-design-tokens": "^10.0.0",
"@vector-im/compound-web": "^9.0.0",
"styled-components": "^6.1.18",
"zod": "^4.0.0"
}
}
@@ -0,0 +1,59 @@
/*
Copyright 2026 Element Creations Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { type FC } from "react";
import { Button } from "@vector-im/compound-web";
import { type AccountAuthInfo, type Api } from "@element-hq/element-web-module-api";
import styled from "styled-components";
import { type ModuleConfig } from "./config.ts";
import RegisterDialog from "./RegisterDialog.tsx";
interface Props {
api: Api;
config: ModuleConfig;
onLoggedIn(data: AccountAuthInfo): void;
}
const Container = styled.aside`
margin: var(--cpd-space-3x) 0;
button {
width: 100%;
}
`;
const AuthFooter: FC<Props> = ({ api, config, onLoggedIn }) => {
const onTryJoin = async (): Promise<void> => {
const { finished } = api.openDialog(
{
title: api.i18n.translate("register_dialog_title"),
},
RegisterDialog,
{
api,
config,
},
);
const { model: accountAuthInfo, ok } = await finished;
if (ok && accountAuthInfo) {
onLoggedIn(accountAuthInfo);
}
};
return (
<Container>
<Button onClick={onTryJoin} size="md" kind="secondary">
{api.i18n.translate("join_cta")}
</Button>
</Container>
);
};
export default AuthFooter;
@@ -0,0 +1,99 @@
/*
Copyright 2025 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { type FC, useState, type JSX, type FormEvent } from "react";
import { type Api, type AccountAuthInfo, type DialogProps } from "@element-hq/element-web-module-api";
import { Form } from "@vector-im/compound-web";
import styled from "styled-components";
import { type ModuleConfig } from "./config.ts";
interface RegisterDialogProps extends DialogProps<AccountAuthInfo> {
api: Api;
config: ModuleConfig;
showLoginLink?: boolean;
}
const enum State {
Idle,
Busy,
Error,
}
const StyledFormRoot = styled(Form.Root)`
font: var(--cpd-font-body-md-regular);
letter-spacing: var(--cpd-font-letter-spacing-body-md);
font-feature-settings: normal;
`;
const RegisterDialog: FC<RegisterDialogProps> = ({ api, config, onCancel, onSubmit, showLoginLink }) => {
const [username, setUsername] = useState("");
const [state, setState] = useState<State>(State.Idle);
async function trySubmit(ev: FormEvent): Promise<void> {
ev.preventDefault();
setState(State.Busy);
try {
const homeserverUrl = config.guest_user_homeserver_url;
const url = new URL("/_synapse/client/register_guest", homeserverUrl);
const response = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ displayname: username }),
});
if (response.ok) {
const accountAuthInfo = await response.json();
onSubmit(accountAuthInfo);
}
} catch (e) {
console.error("Failed to create guest account", e);
setState(State.Error);
}
}
let message: JSX.Element | undefined;
if (state === State.Error) {
message = <Form.ErrorMessage>{api.i18n.translate("register_dialog_error")}</Form.ErrorMessage>;
} else if (state === State.Busy) {
message = <Form.LoadingMessage>{api.i18n.translate("register_dialog_busy")}</Form.LoadingMessage>;
}
const disabled = state !== State.Idle;
return (
<StyledFormRoot onSubmit={trySubmit}>
<Form.Field name="name">
<Form.Label>{api.i18n.translate("register_dialog_register_username_label")}</Form.Label>
<Form.TextControl
disabled={disabled}
value={username}
onChange={(event) => {
setUsername(event.currentTarget.value);
}}
placeholder={api.i18n.translate("register_dialog_field_label")}
/>
{message}
</Form.Field>
{showLoginLink && (
<a href={config.skip_single_sign_on ? "/#/login" : "/#/start_sso"} onClick={onCancel}>
{api.i18n.translate("register_dialog_existing_account")}
</a>
)}
<Form.Submit disabled={disabled || !username}>
{api.i18n.translate("register_dialog_continue_label")}
</Form.Submit>
</StyledFormRoot>
);
};
export default RegisterDialog;
@@ -0,0 +1,70 @@
/*
Copyright 2025 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { type FC, type JSX } from "react";
import { Button } from "@vector-im/compound-web";
import { type Api } from "@element-hq/element-web-module-api";
import styled from "styled-components";
import { useWatchable } from "@element-hq/element-web-module-api";
import { type ModuleConfig } from "./config.ts";
import RegisterDialog from "./RegisterDialog.tsx";
interface RoomPreviewBarProps {
api: Api;
config: ModuleConfig;
children: JSX.Element;
roomId?: string;
roomAlias?: string;
promptAskToJoin?: boolean;
}
const Container = styled.aside`
margin: auto;
font: var(--cpd-font-body-md-regular);
letter-spacing: var(--cpd-font-letter-spacing-body-md);
font-feature-settings: normal;
`;
const RoomPreviewBar: FC<RoomPreviewBarProps> = ({ api, config, roomId, roomAlias, promptAskToJoin, children }) => {
const profile = useWatchable(api.profile);
const isGuest = profile.isGuest;
if (promptAskToJoin || !isGuest || !(roomId || roomAlias)) return children;
const onTryJoin = async (): Promise<void> => {
const { finished } = api.openDialog(
{
title: api.i18n.translate("register_dialog_title"),
},
RegisterDialog,
{
api,
config,
showLoginLink: true,
},
);
const { model: accountAuthInfo, ok } = await finished;
if (ok && accountAuthInfo) {
await api.overwriteAccountAuth(accountAuthInfo);
await api.navigation.toMatrixToLink(`https://matrix.to/#/${roomId ?? roomAlias}`, true);
}
};
return (
<Container className="mx_RoomPreviewBar">
<div className="mx_RoomPreviewBar_message">{api.i18n.translate("join_message")}</div>
<div className="mx_RoomPreviewBar_actions">
<Button onClick={onTryJoin}>{api.i18n.translate("join_cta")}</Button>
</div>
</Container>
);
};
export default RoomPreviewBar;
+47
View File
@@ -0,0 +1,47 @@
/*
Copyright 2025 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { z, type ZodMiniType, type input } from "zod/mini";
z.config(z.locales.en());
export const ModuleConfig = z.object({
/**
* The URL of the homeserver where the guest users should be registered. This
* must have the `synapse-restricted-guests-module` installed.
* @example `https://synapse.local`
*/
guest_user_homeserver_url: z.url(),
/**
* The username prefix that identifies guest users.
* @defaultValue `@guest-`
*/
guest_user_prefix: z._default(z.string().check(z.regex(/@[a-zA-Z-_1-9]+/)), "@guest-"),
/**
* If true, the user will be forwarded to the login page instead of to the SSO
* login. This is only required if the home server has no SSO support.
* @defaultValue `false`
*/
skip_single_sign_on: z._default(z.boolean(), false),
});
export type ModuleConfig = z.infer<typeof ModuleConfig>;
type ConfigSchema = ZodMiniType<z.output<typeof ModuleConfig>, z.input<typeof ModuleConfig>>;
export const CONFIG_KEY = "io.element.element-web-modules.restricted-guests";
declare module "@element-hq/element-web-module-api" {
export interface Config {
[CONFIG_KEY]: input<ConfigSchema>;
sso_redirect_options?: {
immediate?: boolean; // incompatible option
};
}
}
+83
View File
@@ -0,0 +1,83 @@
/*
Copyright 2025 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import compound from "@vector-im/compound-web/dist/style.css" with { type: "css" };
import type { Module, Api, ModuleFactory } from "@element-hq/element-web-module-api";
import Translations from "./translations.json";
import { ModuleConfig, CONFIG_KEY } from "./config";
import { name as ModuleName } from "../package.json";
import RoomPreviewBar from "./RoomPreviewBar.tsx";
import AuthFooter from "./AuthFooter.tsx";
const GUEST_INVISIBLE_COMPONENTS = [
"UIComponent.sendInvites",
"UIComponent.roomCreation",
"UIComponent.spaceCreation",
"UIComponent.exploreRooms",
"UIComponent.roomOptionsMenu",
"UIComponent.addIntegrations",
];
class RestrictedGuestsModule implements Module {
public static readonly moduleApiVersion = "^1.0.0";
private config?: ModuleConfig;
public constructor(private api: Api) {}
public async load(): Promise<void> {
document.adoptedStyleSheets.push(compound);
this.api.i18n.register(Translations);
try {
this.config = ModuleConfig.parse(this.api.config.get(CONFIG_KEY));
} catch (e) {
console.error("Failed to init module", e);
throw new Error(`Errors in module configuration for "${ModuleName}"`);
}
const appConfig = this.api.config.get();
if (appConfig.sso_redirect_options?.immediate) {
console.warn(`${ModuleName} found incompatible option 'sso_redirect_options.immediate', turning it off.`);
appConfig.sso_redirect_options.immediate = false;
}
// Room preview bar customisations (for Matrix guest support)
this.api.customComponents.registerRoomPreviewBar((props, OriginalComponent) => (
<RoomPreviewBar {...props} api={this.api} config={this.config!}>
<OriginalComponent {...props} />
</RoomPreviewBar>
));
this.api.customisations.registerShouldShowComponent(this.shouldShowComponent);
// Login component customisations (for no guest support)
this.api.customComponents.registerLoginComponent((props, OriginalComponent) => (
<OriginalComponent {...props}>
<AuthFooter onLoggedIn={props.onLoggedIn} api={this.api} config={this.config!} />
</OriginalComponent>
));
}
/**
* Returns true, if the `userId` should see the `component`.
*
* @param component - the name of the component that is checked
* @returns true, if the user should see the component
*/
public readonly shouldShowComponent = (component: string): boolean => {
const profile = this.api.profile.value;
if (this.config && (profile.isGuest || profile.userId?.startsWith(this.config.guest_user_prefix))) {
return GUEST_INVISIBLE_COMPONENTS.includes(component);
}
return true;
};
}
export default RestrictedGuestsModule satisfies ModuleFactory;
@@ -0,0 +1,38 @@
{
"register_dialog_register_username_label": {
"en": "Username",
"de": "Benutzername"
},
"register_dialog_title": {
"en": "Request access",
"de": "Zugriff anfordern"
},
"register_dialog_busy": {
"en": "Creating your account...",
"de": "Erstelle dein Konto..."
},
"register_dialog_continue_label": {
"en": "Continue as guest",
"de": "Als Gast fortfahren"
},
"register_dialog_field_label": {
"en": "Name",
"de": "Name"
},
"register_dialog_existing_account": {
"en": "I already have an account.",
"de": "Ich habe bereits einen Account."
},
"register_dialog_error": {
"en": "The account creation failed.",
"de": "Die Anmeldung als Gastnutzer ist fehlgeschlagen."
},
"join_message": {
"en": "Join the room to participate",
"de": "Treten Sie dem Raum bei, um teilzunehmen"
},
"join_cta": {
"en": "Join as guest",
"de": "Als Gast beitreten"
}
}
+8
View File
@@ -0,0 +1,8 @@
/*
Copyright 2025 Element Creations Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
/// <reference types="@arcmantle/vite-plugin-import-css-sheet/client" />
+8
View File
@@ -0,0 +1,8 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "lib",
"jsx": "react-jsx"
},
"include": ["src"]
}
+60
View File
@@ -0,0 +1,60 @@
/*
Copyright 2025 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { defineConfig, esmExternalRequirePlugin } from "vite";
import react from "@vitejs/plugin-react";
import { nodePolyfills } from "vite-plugin-node-polyfills";
import externalGlobals from "rollup-plugin-external-globals";
import { importCSSSheet } from "@arcmantle/vite-plugin-import-css-sheet";
const __dirname = dirname(fileURLToPath(import.meta.url));
export default defineConfig({
build: {
lib: {
entry: resolve(__dirname, "src/index.tsx"),
name: "element-web-module-restricted-guests",
fileName: "index",
formats: ["es"],
},
outDir: "lib",
target: "esnext",
sourcemap: true,
rolldownOptions: {
plugins: [
esmExternalRequirePlugin({
external: ["react"],
}),
],
output: {
globals: {
// Reuse React from the host app
react: "window.React",
},
},
},
},
plugins: [
importCSSSheet(),
react(),
nodePolyfills({
include: ["events"],
}),
externalGlobals({
// Reuse React from the host app
react: "window.React",
}),
],
define: {
// Use production mode for the build as it is tested against production builds of Element Web,
// this is required for React JSX versions to be compatible.
"process.env.NODE_ENV": "'production'",
"process": { env: { NODE_ENV: "production" } },
},
});