mv element.io @types __mocks__/ debian docker module_system/ playwright res src test webapp Dockerfile .dockerignore .eslintignore .stylelintrc.cjs babel.config.cjs recorder-worklet-loader.cjs .modernizr.json components.json config.json config.sample.json package.json project.json tsconfig.json tsconfig.module_system.json jest.config.ts playwright.config.ts webpack.config.ts build_config.sample.yaml apps/web/

mkdir apps/web/scripts
mv scripts/{cleanup.sh,ci_package.sh,copy-res.ts,deploy.py,package.sh} apps/web/scripts

And a couple of gitignore tweaks

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
Michael Telatynski
2026-02-24 15:43:58 +00:00
parent e7509c92a1
commit 91a3cb03c1
3408 changed files with 28 additions and 32 deletions
+263
View File
@@ -0,0 +1,263 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2020 The Matrix.org Foundation C.I.C.
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2018, 2019 New Vector Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2015, 2016 OpenMarket 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.
*/
// To ensure we load the browser-matrix version first
import "matrix-js-sdk/src/browser-index";
import React, { type ReactElement, StrictMode } from "react";
import { logger } from "matrix-js-sdk/src/logger";
import { AutoDiscovery, type ClientConfig } from "matrix-js-sdk/src/matrix";
import { WrapperLifecycle, type WrapperOpts } from "@matrix-org/react-sdk-module-api/lib/lifecycles/WrapperLifecycle";
import type { QueryDict } from "matrix-js-sdk/src/utils";
import PlatformPeg from "../PlatformPeg";
import AutoDiscoveryUtils from "../utils/AutoDiscoveryUtils";
import * as Lifecycle from "../Lifecycle";
import SdkConfig from "../SdkConfig";
import { type IConfigOptions } from "../IConfigOptions";
import { SnakedObject } from "../utils/SnakedObject";
import MatrixChat from "../components/structures/MatrixChat";
import { type ValidatedServerConfig } from "../utils/ValidatedServerConfig";
import { ModuleRunner } from "../modules/ModuleRunner";
import { parseQs } from "./url_utils";
import { getInitialScreenAfterLogin, getScreenFromLocation, init as initRouting, onNewScreen } from "./routing";
import { UserFriendlyError } from "../languageHandler";
import { ModuleApi } from "../modules/Api";
import { RoomView } from "../components/structures/RoomView";
import RoomAvatar from "../components/views/avatars/RoomAvatar";
import { ModuleNotificationDecoration } from "../modules/components/ModuleNotificationDecoration";
import Login from "../Login.ts";
import { startOidcLogin } from "../utils/oidc/authorize.ts";
logger.log(`Application is running in ${process.env.NODE_ENV} mode`);
window.matrixLogger = logger;
function onTokenLoginCompleted(): void {
// if we did a token login, we're now left with the token, hs and is
// url as query params in the url;
// if we did an oidc authorization code flow login, we're left with the auth code and state
// as query params in the url;
// a little nasty but let's redirect to clear them.
const url = new URL(window.location.href);
url.searchParams.delete("no_universal_links");
url.searchParams.delete("loginToken");
url.searchParams.delete("state");
url.searchParams.delete("code");
logger.log(`Redirecting to ${url.href} to drop delegated authentication params from queryparams`);
window.history.replaceState(null, "", url.href);
}
async function redirectToSso(config: ValidatedServerConfig): Promise<boolean> {
logger.log("Bypassing app load to redirect to SSO");
try {
const login = new Login(config.hsUrl, config.isUrl, null, {
delegatedAuthentication: config.delegatedAuthentication,
});
const flows = await login.getFlows();
const nativeOidcFlow = flows.find((flow) => "clientId" in flow);
if (nativeOidcFlow && config.delegatedAuthentication) {
await startOidcLogin(config.delegatedAuthentication, nativeOidcFlow.clientId, config.hsUrl, config.isUrl);
return true;
}
const flow = flows.find((flow) => flow.type === "m.login.sso" || flow.type === "m.login.cas");
PlatformPeg.get()!.startSingleSignOn(
login.createTemporaryClient(),
flow?.type === "m.login.cas" ? "cas" : "sso",
`/${getScreenFromLocation(window.location).screen}`,
);
return true;
} catch (e) {
console.error("Error encountered during sso redirect", e);
}
return false;
}
export async function loadApp(fragParams: QueryDict, matrixChatRef: React.Ref<MatrixChat>): Promise<ReactElement> {
// XXX: This lives here because certain components import so many things that importing it in a sensible place (eg.
// the builtins module or init.tsx) causes a circular dependency.
ModuleApi.instance.builtins.setComponents({
roomView: RoomView,
roomAvatar: RoomAvatar,
notificationDecoration: ModuleNotificationDecoration,
});
initRouting();
const platform = PlatformPeg.get();
const params = parseQs(window.location);
const urlWithoutQuery = window.location.protocol + "//" + window.location.host + window.location.pathname;
logger.log("Vector starting at " + urlWithoutQuery);
platform?.startUpdater();
// Don't bother loading the app until the config is verified
const config = await verifyServerConfig();
const snakedConfig = new SnakedObject<IConfigOptions>(config);
// Before we continue, let's see if we're supposed to do an SSO redirect
const [userId] = await Lifecycle.getStoredSessionOwner();
const hasPossibleToken = !!userId;
const isReturningFromSso = !!params.loginToken || (!!params.code && !!params.state);
const ssoRedirects = config.sso_redirect_options || {};
let autoRedirect = ssoRedirects.immediate === true;
// XXX: This path matching is a bit brittle, but better to do it early instead of in the app code.
const isWelcomeOrLanding =
window.location.hash === "#/welcome" || window.location.hash === "#" || window.location.hash === "";
const isLoginPage = window.location.hash === "#/login";
if (!autoRedirect && ssoRedirects.on_welcome_page && isWelcomeOrLanding) {
autoRedirect = true;
}
if (!autoRedirect && ssoRedirects.on_login_page && isLoginPage) {
autoRedirect = true;
}
// getInitialScreenAfterLogin has a side effect to write to sessionStorage, perform it before auto-redirect
const initialScreenAfterLogin = getInitialScreenAfterLogin(window.location);
if (!hasPossibleToken && !isReturningFromSso && autoRedirect && config.validated_server_config) {
const redirecting = await redirectToSso(config.validated_server_config);
// We return here because startSingleSignOn() will asynchronously redirect us. We don't
// care to wait for it, and don't want to show any UI while we wait (not even half a welcome
// page). As such, just don't even bother loading the MatrixChat component.
if (redirecting) {
return <React.Fragment />;
}
}
const defaultDeviceName =
snakedConfig.get("default_device_display_name") ?? platform?.getDefaultDeviceDisplayName();
const wrapperOpts: WrapperOpts = { Wrapper: React.Fragment };
ModuleRunner.instance.invoke(WrapperLifecycle.Wrapper, wrapperOpts);
return (
<wrapperOpts.Wrapper>
<StrictMode>
<MatrixChat
ref={matrixChatRef}
onNewScreen={onNewScreen}
config={config}
realQueryParams={params}
startingFragmentQueryParams={fragParams}
enableGuest={!config.disable_guests}
onTokenLoginCompleted={onTokenLoginCompleted}
initialScreenAfterLogin={initialScreenAfterLogin}
defaultDeviceDisplayName={defaultDeviceName}
/>
</StrictMode>
</wrapperOpts.Wrapper>
);
}
async function verifyServerConfig(): Promise<IConfigOptions> {
let validatedConfig: ValidatedServerConfig;
try {
logger.log("Verifying homeserver configuration");
// Note: the query string may include is_url and hs_url - we only respect these in the
// context of email validation. Because we don't respect them otherwise, we do not need
// to parse or consider them here.
// Note: Although we throw all 3 possible configuration options through a .well-known-style
// verification, we do not care if the servers are online at this point. We do moderately
// care if they are syntactically correct though, so we shove them through the .well-known
// validators for that purpose.
const config = SdkConfig.get();
let wkConfig = config["default_server_config"]; // overwritten later under some conditions
const serverName = config["default_server_name"];
const hsUrl = config["default_hs_url"];
const isUrl = config["default_is_url"];
const incompatibleOptions = [wkConfig, serverName, hsUrl].filter((i) => !!i);
if (hsUrl && (wkConfig || serverName)) {
// noinspection ExceptionCaughtLocallyJS
throw new UserFriendlyError("error|invalid_configuration_mixed_server");
}
if (incompatibleOptions.length < 1) {
// noinspection ExceptionCaughtLocallyJS
throw new UserFriendlyError("error|invalid_configuration_no_server");
}
if (hsUrl) {
logger.log("Config uses a default_hs_url - constructing a default_server_config using this information");
logger.warn(
"DEPRECATED CONFIG OPTION: In the future, default_hs_url will not be accepted. Please use " +
"default_server_config instead.",
);
wkConfig = {
"m.homeserver": {
base_url: hsUrl,
},
};
if (isUrl) {
wkConfig["m.identity_server"] = {
base_url: isUrl,
};
}
}
let discoveryResult: ClientConfig | undefined;
if (!serverName && wkConfig) {
logger.log("Config uses a default_server_config - validating object");
discoveryResult = await AutoDiscovery.fromDiscoveryConfig(wkConfig);
}
if (serverName) {
logger.log("Config uses a default_server_name - doing .well-known lookup");
logger.warn(
"DEPRECATED CONFIG OPTION: In the future, default_server_name will not be accepted. Please " +
"use default_server_config instead.",
);
discoveryResult = await AutoDiscovery.findClientConfig(serverName);
if (discoveryResult["m.homeserver"].base_url === null && wkConfig) {
logger.log("Finding base_url failed but a default_server_config was found - using it as a fallback");
discoveryResult = await AutoDiscovery.fromDiscoveryConfig(wkConfig);
}
}
validatedConfig = await AutoDiscoveryUtils.buildValidatedConfigFromDiscovery(serverName, discoveryResult, true);
} catch (e) {
const { hsUrl, isUrl, userId } = await Lifecycle.getStoredSessionVars();
if (hsUrl && userId) {
logger.error(e);
logger.warn("A session was found - suppressing config error and using the session's homeserver");
logger.log("Using pre-existing hsUrl and isUrl: ", { hsUrl, isUrl });
validatedConfig = await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(hsUrl, isUrl, true);
} else {
// the user is not logged in, so scream
throw e;
}
}
validatedConfig.isDefault = true;
// Just in case we ever have to debug this
logger.log("Using homeserver config:", validatedConfig);
// Add the newly built config to the actual config for use by the app
logger.log("Updating SdkConfig with validated discovery information");
SdkConfig.add({ validated_server_config: validatedConfig });
return SdkConfig.get();
}
+54
View File
@@ -0,0 +1,54 @@
/*
Copyright 2018-2024 New Vector 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 type { IConfigOptions } from "../IConfigOptions";
// Load the config file. First try to load up a domain-specific config of the
// form "config.$domain.json" and if that fails, fall back to config.json.
export async function getVectorConfig(relativeLocation = ""): Promise<IConfigOptions | undefined> {
if (relativeLocation !== "" && !relativeLocation.endsWith("/")) relativeLocation += "/";
// Handle trailing dot FQDNs
let domain = window.location.hostname.trimEnd();
if (domain.endsWith(".")) {
domain = domain.slice(0, -1);
}
const specificConfigPromise = getConfig(`${relativeLocation}config.${domain}.json`);
const generalConfigPromise = getConfig(relativeLocation + "config.json");
try {
const configJson = await specificConfigPromise;
// 404s succeed with an empty json config, so check that there are keys
if (!configJson || Object.keys(configJson).length === 0) {
throw new Error(); // throw to enter the catch
}
return configJson;
} catch {
return generalConfigPromise;
}
}
async function getConfig(configJsonFilename: string): Promise<IConfigOptions | undefined> {
const url = new URL(configJsonFilename, window.location.href);
url.searchParams.set("cachebuster", Date.now().toString());
const res = await fetch(url, {
cache: "no-cache",
method: "GET",
});
if (res.status === 404 || res.status === 0) {
// Lack of a config isn't an error, we should just use the defaults.
// Also treat a blank config as no config, assuming the status code is 0, because we don't get 404s from file:
// URIs so this is the only way we can not fail if the file doesn't exist when loading from a file:// URI.
return {} as IConfigOptions;
}
if (res.ok) {
return res.json();
}
}
+85
View File
@@ -0,0 +1,85 @@
<!doctype html>
<html lang="en" style="height: 100%;">
<head>
<meta charset="utf-8">
<title>Element</title>
<link rel="apple-touch-icon" sizes="120x120" href="<%= require('../../res/vector-icons/120.png') %>">
<link rel="apple-touch-icon" sizes="144x144" href="<%= require('../../res/vector-icons/144.png') %>">
<link rel="apple-touch-icon" sizes="152x152" href="<%= require('../../res/vector-icons/152.png') %>">
<link rel="apple-touch-icon" sizes="180x180" href="<%= require('../../res/vector-icons/180.png') %>">
<link rel="manifest" href="manifest.json">
<meta name="referrer" content="no-referrer">
<link rel="icon" type="image/png" sizes="24x24" href="<%= require('../../res/vector-icons/24.png') %>">
<link rel="icon" type="image/png" sizes="144x144" href="<%= require('../../res/vector-icons/144.png') %>">
<link rel="icon" type="image/png" sizes="512x512" href="<%= require('../../res/vector-icons/512.png') %>">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="apple-mobile-web-app-title" content="Element">
<meta name="application-name" content="Element">
<meta name="theme-color" content="#ffffff">
<meta property="og:image" content="<%= og_image_url %>" />
<meta http-equiv="Content-Security-Policy" content="
default-src 'none';
style-src 'self' 'unsafe-inline' <%= csp_extra_source %>;
script-src 'self' 'wasm-unsafe-eval' https://www.recaptcha.net/recaptcha/ https://www.gstatic.com/recaptcha/ <%= csp_extra_source %>;
img-src * blob: data:;
connect-src * blob:;
font-src 'self' data: <%= csp_extra_source %>;
media-src * blob: data:;
child-src * blob: data:;
worker-src 'self' blob: <%= csp_extra_source %>;
frame-src * blob: data:;
form-action 'self' <%= csp_extra_source %>;
manifest-src 'self' <%= csp_extra_source %>;
">
<% for (var i=0; i < htmlWebpackPlugin.files.css.length; i++) {
var file = htmlWebpackPlugin.files.css[i];
var match = file.match(/^bundles\/.*?\/theme-(.*)\.css$/);
if (match) {
var title = match[1].charAt(0).toUpperCase() + match[1].slice(1);
%>
<link rel="stylesheet" disabled="disabled" data-mx-theme="<%= title %>" title="<%= title %>" href="<%= file %>">
<% } else { %>
<link rel="stylesheet" href="<%= file %>">
<% }
} %>
<% for (const tag of htmlWebpackPlugin.tags.headTags) {
let path = tag.attributes && tag.attributes.href;
if (path && path.includes("/Inter/")) { %>
<link rel="preload" as="font" href="<%= path %>" crossorigin="anonymous"/>
<% }
} %>
</head>
<body style="height: 100%; margin: 0;">
<noscript>Sorry, Element requires JavaScript to be enabled.</noscript> <!-- TODO: Translate this? -->
<div id="matrixchat" style="height: 100%;" class="notranslate"></div>
<%
// insert <script> tags for the JS entry points
for (let file of htmlWebpackPlugin.files.js) {
if (file.includes("bundle.js") || file.includes("unhomoglyph_data")) {
%> <script src="<%= file %>"></script>
<%
}
}
%>
<!-- Legacy supporting Prefetch images -->
<img src="<%= require('@vector-im/compound-design-tokens/icons/warning.svg').default %>" aria-hidden alt="" width="24" height="23" style="visibility: hidden; position: absolute; top: 0px; left: 0px;"/>
<img src="<%= require('@vector-im/compound-design-tokens/icons/bold.svg').default %>" aria-hidden alt="" width="25" height="22" style="visibility: hidden; position: absolute; top: 0px; left: 0px;"/>
<img src="<%= require('@vector-im/compound-design-tokens/icons/inline-code.svg').default %>" aria-hidden alt="" width="25" height="22" style="visibility: hidden; position: absolute; top: 0px; left: 0px;"/>
<img src="<%= require('@vector-im/compound-design-tokens/icons/italic.svg').default %>" aria-hidden alt="" width="25" height="22" style="visibility: hidden; position: absolute; top: 0px; left: 0px;"/>
<img src="<%= require('@vector-im/compound-design-tokens/icons/quote.svg').default %>" aria-hidden alt="" width="25" height="22" style="visibility: hidden; position: absolute; top: 0px; left: 0px;"/>
<img src="<%= require('@vector-im/compound-design-tokens/icons/strikethrough.svg').default %>" aria-hidden alt="" width="25" height="22" style="visibility: hidden; position: absolute; top: 0px; left: 0px;"/>
<!-- let CSS themes pass constants to the app -->
<div id="mx_theme_accentColor"></div><div id="mx_theme_secondaryAccentColor"></div><div id="mx_theme_tertiaryAccentColor"></div>
<!-- We eagerly create these containers to ensure their CSS stacking context order is sensible -->
<div id="mx_PersistedElement_container"></div>
<div id="mx_Dialog_StaticContainer"></div>
<div id="mx_Dialog_Container"></div>
<div id="mx_ContextualMenu_Container"></div>
</body>
</html>
+269
View File
@@ -0,0 +1,269 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2020 The Matrix.org Foundation C.I.C.
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2018, 2019 New Vector Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2015, 2016 OpenMarket 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 { logger } from "matrix-js-sdk/src/logger";
import { shouldPolyfill as shouldPolyFillIntlSegmenter } from "@formatjs/intl-segmenter/should-polyfill.js";
// These are things that can run before the skin loads - be careful not to reference the react-sdk though.
import { parseQsFromFragment } from "./url_utils";
import "./modernizr.cjs";
// Import shared components CSS
import "@element-hq/web-shared-components/dist/element-web-shared-components.css";
// Require common CSS here; this will make webpack process it into bundle.css.
// Our own CSS (which is themed) is imported via separate webpack entry points
// in webpack.config.js
// eslint-disable-next-line @typescript-eslint/no-require-imports
require("katex/dist/katex.css");
// eslint-disable-next-line @typescript-eslint/no-require-imports
require("./localstorage-fix");
async function settled(...promises: Array<Promise<any>>): Promise<void> {
for (const prom of promises) {
try {
await prom;
} catch (e) {
logger.error(e);
}
}
}
function checkBrowserFeatures(): boolean {
if (!window.Modernizr) {
logger.error("Cannot check features - Modernizr global is missing.");
return false;
}
// Custom checks atop Modernizr because it doesn't have checks in it for
// some features we depend on.
// Modernizr requires rules to be lowercase with no punctuation.
// ES2018: http://262.ecma-international.org/9.0/#sec-promise.prototype.finally
window.Modernizr.addTest("promiseprototypefinally", () => typeof window.Promise?.prototype?.finally === "function");
// ES2020: http://262.ecma-international.org/#sec-promise.allsettled
window.Modernizr.addTest("promiseallsettled", () => typeof window.Promise?.allSettled === "function");
// ES2024: https://2ality.com/2024/05/proposal-promise-with-resolvers.html
window.Modernizr.addTest("promisewithresolvers", () => typeof window.Promise?.withResolvers === "function");
// ES2018: https://262.ecma-international.org/9.0/#sec-get-regexp.prototype.dotAll
window.Modernizr.addTest(
"regexpdotall",
() => window.RegExp?.prototype && !!Object.getOwnPropertyDescriptor(window.RegExp.prototype, "dotAll")?.get,
);
// ES2019: http://262.ecma-international.org/10.0/#sec-object.fromentries
window.Modernizr.addTest("objectfromentries", () => typeof window.Object?.fromEntries === "function");
// ES2024: https://402.ecma-international.org/9.0/#sec-intl.segmenter
// The built-in modernizer 'intl' check only checks for the presence of the Intl object, not the Segmenter,
// and older Firefox has the former but not the latter, so we add our own.
// This is polyfilled now, but we still want to show the warning because we want to remove the polyfill
// at some point.
window.Modernizr.addTest("intlsegmenter", () => typeof window.Intl?.Segmenter === "function");
// Basic test for WebAssembly support. We could also try instantiating a simple module,
// although this would start to make (more) assumptions about how rust-crypto loads its wasm.
window.Modernizr.addTest("wasm", () => typeof WebAssembly === "object" && typeof WebAssembly.Module === "function");
// Check that the session is in a secure context otherwise most Crypto & WebRTC APIs will be unavailable
// https://developer.mozilla.org/en-US/docs/Web/API/Window/isSecureContext
window.Modernizr.addTest("securecontext", () => window.isSecureContext);
const featureList = Object.keys(window.Modernizr) as Array<keyof ModernizrStatic>;
let featureComplete = true;
for (const feature of featureList) {
if (window.Modernizr[feature] === undefined) {
logger.error(
"Looked for feature '%s' but Modernizr has no results for this. " + "Has it been configured correctly?",
feature,
);
return false;
}
if (window.Modernizr[feature] === false) {
logger.error("Browser missing feature: '%s'", feature);
// toggle flag rather than return early so we log all missing features rather than just the first.
featureComplete = false;
}
}
return featureComplete;
}
const supportedBrowser = checkBrowserFeatures();
// React depends on Map & Set which we check for using modernizr's es6collections
// if modernizr fails we may not have a functional react to show the error message.
// try in react but fallback to an `alert`
// We start loading stuff but don't block on it until as late as possible to allow
// the browser to use as much parallelism as it can.
// Load parallelism is based on research in https://github.com/element-hq/element-web/issues/12253
async function start(): Promise<void> {
if (shouldPolyFillIntlSegmenter()) {
await import(/* webpackChunkName: "intl-segmenter-polyfill" */ "@formatjs/intl-segmenter/polyfill-force.js");
}
// load init.ts async so that its code is not executed immediately and we can catch any exceptions
const {
rageshakePromise,
setupLogStorage,
preparePlatform,
loadConfig,
loadLanguage,
loadTheme,
loadApp,
loadModules,
loadPlugins,
showError,
showIncompatibleBrowser,
_t,
extractErrorMessageFromError,
} = await import(
/* webpackChunkName: "init" */
/* webpackPreload: true */
"./init"
);
// Now perform the next stage of initialisation. This has its own try/catch in which we render
// a react error page on failure.
try {
// give rageshake a chance to load/fail, we don't actually assert rageshake loads, we allow it to fail if no IDB
await settled(rageshakePromise);
const fragparts = parseQsFromFragment(window.location);
// don't try to redirect to the native apps if we're
// verifying a 3pid (but after we've loaded the config)
// or if the user is following a deep link
// (https://github.com/element-hq/element-web/issues/7378)
const preventRedirect = fragparts.params.client_secret || fragparts.location.length > 0;
if (!preventRedirect) {
const isIos = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
const isAndroid = /Android/.test(navigator.userAgent);
if (isIos || isAndroid) {
if (document.cookie.indexOf("element_mobile_redirect_to_guide=false") === -1) {
window.location.href = "mobile_guide/";
return;
}
}
}
// set the platform for react sdk
preparePlatform();
// load config requires the platform to be ready
const loadConfigPromise = loadConfig();
await settled(loadConfigPromise); // wait for it to settle
// keep initialising so that we can show any possible error with as many features (theme, i18n) as possible
// now that the config is ready, try to persist logs
const persistLogsPromise = setupLogStorage();
// Load language after loading config.json so that settingsDefaults.language can be applied
const loadLanguagePromise = loadLanguage();
// as quickly as we possibly can, set a default theme...
const loadThemePromise = loadTheme();
// await things settling so that any errors we have to render have features like i18n running
await settled(loadThemePromise, loadLanguagePromise);
const loadModulesPromise = loadModules();
await settled(loadModulesPromise);
const loadPluginsPromise = loadPlugins();
await settled(loadPluginsPromise);
let acceptBrowser = supportedBrowser;
if (!acceptBrowser && window.localStorage) {
acceptBrowser = Boolean(window.localStorage.getItem("mx_accepts_unsupported_browser"));
}
// ##########################
// error handling begins here
// ##########################
if (!acceptBrowser) {
await new Promise<void>((resolve, reject) => {
logger.error("Browser is missing required features.");
// take to a different landing page to AWOOOOOGA at the user
showIncompatibleBrowser(() => {
if (window.localStorage) {
window.localStorage.setItem("mx_accepts_unsupported_browser", String(true));
}
logger.log("User accepts the compatibility risks.");
resolve();
}).catch(reject);
});
}
try {
// await config here
await loadConfigPromise;
} catch (error) {
// Now that we've loaded the theme (CSS), display the config syntax error if needed.
if (error instanceof SyntaxError) {
// This uses the default brand since the app config is unavailable.
return showError(_t("error|misconfigured"), [
_t("error|invalid_json"),
_t("error|invalid_json_detail", {
message: error.message || _t("error|invalid_json_generic"),
}),
]);
}
return showError(_t("error|cannot_load_config"));
}
// ##################################
// app load critical path starts here
// assert things started successfully
// ##################################
await loadPluginsPromise;
await loadModulesPromise;
await loadThemePromise;
await loadLanguagePromise;
// We don't care if the log persistence made it through successfully, but we do want to
// make sure it had a chance to load before we move on. It's prepared much higher up in
// the process, making this the first time we check that it did something.
await settled(persistLogsPromise);
// Finally, load the app. All of the other react-sdk imports are in this file which causes the skinner to
// run on the components.
await loadApp(fragparts.params);
} catch (err) {
logger.error(err);
// Like the compatibility page, AWOOOOOGA at the user
// This uses the default brand since the app config is unavailable.
await showError(_t("error|misconfigured"), [
extractErrorMessageFromError(err, _t("error|app_launch_unexpected_error")),
]);
}
}
start().catch((err) => {
// If we get here, things have gone terribly wrong and we cannot load the app javascript at all.
// Show a different, very simple iframed-static error page. Or actually, one of two different ones
// depending on whether the browser is supported (ie. we think we should be able to load but
// failed) or unsupported (where we tried anyway and, lo and behold, we failed).
logger.error(err);
// show the static error in an iframe to not lose any context / console data
// with some basic styling to make the iframe full page
document.body.style.removeProperty("height");
const iframe = document.createElement("iframe");
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore - typescript seems to only like the IE syntax for iframe sandboxing
iframe["sandbox"] = "";
iframe.src = supportedBrowser ? "static/unable-to-load.html" : "static/incompatible-browser.html";
iframe.style.width = "100%";
iframe.style.height = "100%";
iframe.style.position = "absolute";
iframe.style.top = "0";
iframe.style.left = "0";
iframe.style.right = "0";
iframe.style.bottom = "0";
iframe.style.border = "0";
document.getElementById("matrixchat")?.appendChild(iframe);
});
+160
View File
@@ -0,0 +1,160 @@
/*
Copyright 2018-2024 New Vector Ltd.
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2017 Vector Creations Ltd
Copyright 2015, 2016 OpenMarket 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 { createRoot } from "react-dom/client";
import React, { StrictMode } from "react";
import { logger } from "matrix-js-sdk/src/logger";
import { ModuleLoader } from "@element-hq/element-web-module-api";
import type { QueryDict } from "matrix-js-sdk/src/utils";
import * as languageHandler from "../languageHandler";
import SettingsStore from "../settings/SettingsStore";
import PlatformPeg from "../PlatformPeg";
import SdkConfig from "../SdkConfig";
import { setTheme } from "../theme";
import { ModuleRunner } from "../modules/ModuleRunner";
import type MatrixChat from "../components/structures/MatrixChat";
import ElectronPlatform from "./platform/ElectronPlatform";
import PWAPlatform from "./platform/PWAPlatform";
import WebPlatform from "./platform/WebPlatform";
import { initRageshake, initRageshakeStore } from "./rageshakesetup";
import { ModuleApi } from "../modules/Api.ts";
export const rageshakePromise = initRageshake();
export function preparePlatform(): void {
if (window.electron) {
logger.log("Using Electron platform");
PlatformPeg.set(new ElectronPlatform());
} else if (window.matchMedia("(display-mode: standalone)").matches) {
logger.log("Using PWA platform");
PlatformPeg.set(new PWAPlatform());
} else {
logger.log("Using Web platform");
PlatformPeg.set(new WebPlatform());
}
}
export function setupLogStorage(): Promise<void> {
if (SdkConfig.get().bug_report_endpoint_url) {
return initRageshakeStore();
}
logger.warn("No bug report endpoint set - logs will not be persisted");
return Promise.resolve();
}
export async function loadConfig(): Promise<void> {
// XXX: We call this twice, once here and once in MatrixChat as a prop. We call it here to ensure
// granular settings are loaded correctly and to avoid duplicating the override logic for the theme.
//
// Note: this isn't called twice for some wrappers, like the Jitsi wrapper.
const platformConfig = await PlatformPeg.get()?.getConfig();
if (platformConfig) {
SdkConfig.put(platformConfig);
} else {
SdkConfig.reset();
}
}
export async function loadLanguage(): Promise<void> {
const prefLang = SettingsStore.getValue("language", null, /*excludeDefault=*/ true);
let langs: string[] = [];
if (!prefLang) {
languageHandler.getLanguagesFromBrowser().forEach((l) => {
langs.push(...languageHandler.getNormalizedLanguageKeys(l));
});
} else {
langs = [prefLang];
}
try {
await languageHandler.setLanguage(...langs);
document.documentElement.setAttribute("lang", languageHandler.getCurrentLanguage());
} catch (e) {
logger.error("Unable to set language", e);
}
}
export async function loadTheme(): Promise<void> {
return setTheme();
}
export async function loadApp(fragParams: QueryDict): Promise<void> {
// load app.js async so that its code is not executed immediately and we can catch any exceptions
const module = await import(
/* webpackChunkName: "element-web-app" */
/* webpackPreload: true */
"./app"
);
function setWindowMatrixChat(matrixChat: MatrixChat): void {
window.matrixChat = matrixChat;
}
const app = await module.loadApp(fragParams, setWindowMatrixChat);
const root = createRoot(document.getElementById("matrixchat")!);
root.render(app);
}
export async function showError(title: string, messages?: string[]): Promise<void> {
const { ErrorView } = await import(
/* webpackChunkName: "error-view" */
"../async-components/structures/ErrorView"
);
const root = createRoot(document.getElementById("matrixchat")!);
root.render(
<StrictMode>
<ErrorView title={title} messages={messages} />
</StrictMode>,
);
}
export async function showIncompatibleBrowser(onAccept: () => void): Promise<void> {
const { UnsupportedBrowserView } = await import(
/* webpackChunkName: "error-view" */
"../async-components/structures/ErrorView"
);
const root = createRoot(document.getElementById("matrixchat")!);
root.render(
<StrictMode>
<UnsupportedBrowserView onAccept={onAccept} />
</StrictMode>,
);
}
/**
* @deprecated in favour of the plugin system
*/
export async function loadModules(): Promise<void> {
const { INSTALLED_MODULES } = await import("../modules.js");
for (const InstalledModule of INSTALLED_MODULES) {
ModuleRunner.instance.registerModule((api) => new InstalledModule(api));
}
}
export async function loadPlugins(): Promise<void> {
// Add React to the global namespace, this is part of the new Module API contract to avoid needing
// every single module to ship its own copy of React. This also makes it easier to access via the console
// and incidentally means we can forget our React imports in JSX files without penalty.
window.React = React;
const modules = SdkConfig.get("modules");
if (!modules?.length) return;
const moduleLoader = new ModuleLoader(ModuleApi.instance);
window.mxModuleLoader = moduleLoader;
for (const src of modules) {
// We need to instruct webpack to not mangle this import as it is not available at compile time
const module = await import(/* webpackIgnore: true */ src);
await moduleLoader.load(module);
}
await moduleLoader.start();
}
export { _t } from "../languageHandler";
export { extractErrorMessageFromError } from "../components/views/dialogs/ErrorDialog";
+24
View File
@@ -0,0 +1,24 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Jitsi Widget</title>
</head>
<body>
<div id="jitsiContainer"><!-- the js will put the conference here --></div>
<div id="joinButtonContainer">
<div class="joinConferenceFloating">
<div class="joinConferencePrompt">
<%= require('!!raw-loader!@vector-im/compound-design-tokens/icons/video-call-solid.svg').default %>
<!-- TODO: i18n -->
<h2>Jitsi Video Conference</h2>
<div id="widgetActionContainer">
<button type="button" id="joinButton">Join Conference</button>
</div>
</div>
</div>
</div>
<!-- This script is not webpacked, and the script is downloaded at build time -->
<script src="./jitsi_external_api.min.js"></script>
</body>
</html>
+104
View File
@@ -0,0 +1,104 @@
/*
Copyright 2020-2024 New Vector 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.
*/
/* TODO: Match the user's theme: https://github.com/element-hq/element-web/issues/12794 */
@import url("@vector-im/compound-design-tokens/assets/web/css/compound-design-tokens.css");
@font-face {
font-family: "Nunito";
font-style: normal;
font-weight: 400;
src: url("/res/fonts/Nunito/Nunito-Regular.ttf") format("truetype");
}
$dark-fg: #edf3ff;
$dark-bg: #363c43;
$light-fg: #2e2f32;
$light-bg: #fff;
body {
font-family: Nunito, Arial, Helvetica, sans-serif;
background-color: $dark-bg;
color: $dark-fg;
}
body.theme-light {
background-color: $light-bg;
color: $light-fg;
}
body,
html {
padding: 0;
margin: 0;
}
#jitsiContainer {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
#joinButtonContainer {
display: table;
position: absolute;
height: 100%;
width: 100%;
/* Hidden by default to avoid flashing the prejoin screen at the user when */
/* we're supposed to skip it anyways */
visibility: hidden;
}
.joinConferenceFloating {
display: table-cell;
vertical-align: middle;
}
.joinConferencePrompt {
margin-left: auto;
margin-right: auto;
width: 90%;
text-align: center;
}
#joinButton {
/* Copy of Compound Button styles */
border: 1px solid var(--cpd-color-bg-action-primary-rest);
color: var(--cpd-color-text-on-solid-primary);
background-color: var(--cpd-color-bg-action-primary-rest);
padding-block: var(--cpd-space-2x);
padding-inline: var(--cpd-space-8x);
min-block-size: var(--cpd-space-12x);
text-align: center;
border-radius: 24px;
display: inline-flex;
align-items: center;
justify-content: center;
font: var(--cpd-font-body-lg-semibold);
border: none;
word-break: keep-all;
cursor: pointer;
}
.joinConferencePrompt svg {
$icon-size: 42px;
/* to visually center the form */
margin: -$icon-size auto 0;
color: $dark-fg;
mask-size: $icon-size;
display: block;
width: $icon-size;
height: $icon-size;
}
body.theme-light .joinConferencePrompt svg {
color: $light-fg;
}
+553
View File
@@ -0,0 +1,553 @@
/*
Copyright 2020-2024 New Vector 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 { KJUR } from "jsrsasign";
import {
type IOpenIDCredentials,
type IWidgetApiRequest,
type IWidgetApiRequestData,
type IWidgetApiResponseData,
VideoConferenceCapabilities,
WidgetApi,
type WidgetApiAction,
} from "matrix-widget-api";
import { logger } from "matrix-js-sdk/src/logger";
import type {
JitsiMeetExternalAPIConstructor,
ExternalAPIEventCallbacks,
JitsiMeetExternalAPI as _JitsiMeetExternalAPI,
LogEvent,
VideoMuteStatusChangedEvent,
ExternalAPIOptions as _ExternalAPIOptions,
Config as _Config,
InterfaceConfig as _InterfaceConfig,
} from "jitsi-meet";
import { ElementWidgetActions } from "../../stores/widgets/ElementWidgetActions";
import { type IConfigOptions } from "../../IConfigOptions";
import { SnakedObject } from "../../utils/SnakedObject";
import { ElementWidgetCapabilities } from "../../stores/widgets/ElementWidgetCapabilities";
import { getVectorConfig } from "../getconfig";
interface Config extends _Config {
// Jitsi's types are missing these fields
prejoinConfig?: {
enabled: boolean;
hideDisplayName?: boolean;
hideExtraJoinButtons?: string[];
};
toolbarButtons?: string[];
conferenceInfo?: {
alwaysVIsible?: string[];
autoHide?: string[];
};
disableSelfViewSettings?: boolean;
}
interface InterfaceConfig extends _InterfaceConfig {
// XXX: It is unclear whether this is a typo of TOOLBAR_BUTTONS or if its just really undocumented,
// either way it is missing in types, yet we try and use it
MAIN_TOOLBAR_BUTTONS?: string[];
}
interface ExternalAPIOptions extends _ExternalAPIOptions {
configOverwrite?: Config;
interfaceConfigOverwrite?: InterfaceConfig;
// Jitsi's types are missing these fields
lang?: string;
}
// We have to trick webpack into loading our CSS for us.
// eslint-disable-next-line @typescript-eslint/no-require-imports
require("./index.pcss");
const JITSI_OPENIDTOKEN_JWT_AUTH = "openidtoken-jwt";
// Dev note: we use raw JS without many dependencies to reduce bundle size.
// We do not need all of React to render a Jitsi conference.
declare let JitsiMeetExternalAPI: JitsiMeetExternalAPIConstructor;
let inConference = false;
// Jitsi params
let jitsiDomain: string;
let conferenceId: string;
let displayName: string;
let avatarUrl: string;
let userId: string;
let jitsiAuth: string;
let roomId: string;
let roomName: string;
let startAudioOnly: boolean;
let startWithAudioMuted: boolean | undefined;
let startWithVideoMuted: boolean | undefined;
let isVideoChannel: boolean;
let supportsScreensharing: boolean;
let language: string;
let widgetApi: WidgetApi | undefined;
let meetApi: _JitsiMeetExternalAPI | undefined;
let skipOurWelcomeScreen = false;
async function checkAudioVideoEnabled(): Promise<[audioEnabled: boolean, videoEnabled: boolean]> {
if (!meetApi) return [false, false];
const [audioEnabled, videoEnabled] = (await Promise.all([meetApi.isAudioMuted(), meetApi.isVideoMuted()])).map(
(muted) => !muted,
);
return [audioEnabled, videoEnabled];
}
const setupCompleted = (async (): Promise<string | void> => {
try {
// Queue a config.json lookup asap, so we can use it later on. We want this to be concurrent with
// other setup work and therefore do not block.
const configPromise = getVectorConfig();
// The widget's options are encoded into the fragment to avoid leaking info to the server.
const widgetQuery = new URLSearchParams(window.location.hash.substring(1));
// The widget spec on the other hand requires the widgetId and parentUrl to show up in the regular query string.
const realQuery = new URLSearchParams(window.location.search.substring(1));
const qsParam = (name: string, optional = false): string => {
const vals = widgetQuery.has(name) ? widgetQuery.getAll(name) : realQuery.getAll(name);
if (!optional && vals.length !== 1) {
throw new Error(`Expected singular ${name} in query string`);
}
return vals[0];
};
const parseBooleanOrUndefined = (value: string | undefined): boolean | undefined => {
if (value === undefined) {
return undefined;
}
return value === "true";
};
// If we have these params, expect a widget API to be available (ie. to be in an iframe
// inside a matrix client). Otherwise, assume we're on our own, eg. have been popped
// out into a browser.
const parentUrl = qsParam("parentUrl", true);
const widgetId = qsParam("widgetId", true);
const theme = qsParam("theme", true);
language = qsParam("language", true) ?? "en";
if (theme) {
document.body.classList.add(`theme-${theme.replace(" ", "_")}`);
}
// Set this up as early as possible because Element will be hitting it almost immediately.
let widgetApiReady: Promise<void> | undefined;
if (parentUrl && widgetId) {
const parentOrigin = new URL(qsParam("parentUrl")).origin;
widgetApi = new WidgetApi(qsParam("widgetId"), parentOrigin);
widgetApiReady = new Promise<void>((resolve) => widgetApi!.once("ready", resolve));
widgetApi.requestCapabilities(VideoConferenceCapabilities);
// jitsi cannot work in a popup if auth token is provided because widgetApi is not available there
// so check the token and request the 'requires_client' capability to hide the popup icon in the Element
if (qsParam("auth", true) === "openidtoken-jwt") {
widgetApi.requestCapability(ElementWidgetCapabilities.RequiresClient);
}
widgetApi.start();
const handleAction = (
action: WidgetApiAction,
handler: (request: IWidgetApiRequestData) => Promise<IWidgetApiResponseData | void>,
): void => {
widgetApi!.on(`action:${action}`, async (ev: CustomEvent<IWidgetApiRequest>) => {
ev.preventDefault();
await setupCompleted;
let response: IWidgetApiResponseData;
try {
response = (await handler(ev.detail.data)) ?? {};
} catch (e) {
if (e instanceof Error) {
response = { error: { message: e.message } };
} else {
throw e;
}
}
widgetApi!.transport.reply(ev.detail, response);
});
};
handleAction(ElementWidgetActions.JoinCall, async ({ audioInput, videoInput }) => {
void joinConference(audioInput as string | null, videoInput as string | null);
});
handleAction(ElementWidgetActions.HangupCall, async ({ force }) => {
if (force === true) {
meetApi?.dispose();
void notifyHangup();
meetApi = undefined;
closeConference();
} else {
meetApi?.executeCommand("hangup");
}
});
handleAction(ElementWidgetActions.DeviceMute, async (params) => {
if (!meetApi) return;
const [audioEnabled, videoEnabled] = await checkAudioVideoEnabled();
if (Object.keys(params).length === 0) {
// Handle query
return {
audio_enabled: audioEnabled,
video_enabled: videoEnabled,
};
}
if (params.audio_enabled !== audioEnabled) {
meetApi.executeCommand("toggleAudio");
}
if (params.video_enabled !== videoEnabled) {
meetApi.executeCommand("toggleVideo");
}
return params;
});
handleAction(ElementWidgetActions.TileLayout, async () => {
meetApi?.executeCommand("setTileView", true);
});
handleAction(ElementWidgetActions.SpotlightLayout, async () => {
meetApi?.executeCommand("setTileView", false);
});
handleAction(ElementWidgetActions.StartLiveStream, async ({ rtmpStreamKey }) => {
if (!meetApi) throw new Error("Conference not joined");
meetApi.executeCommand("startRecording", {
mode: "stream",
// this looks like it should be rtmpStreamKey but we may be on too old
// a version of jitsi meet
//rtmpStreamKey,
youtubeStreamKey: rtmpStreamKey,
});
});
} else {
logger.warn("No parent URL or no widget ID - assuming no widget API is available");
}
// Populate the Jitsi params now
jitsiDomain = qsParam("conferenceDomain");
conferenceId = qsParam("conferenceId");
displayName = qsParam("displayName", true);
avatarUrl = qsParam("avatarUrl", true); // http not mxc
userId = qsParam("userId");
jitsiAuth = qsParam("auth", true);
roomId = qsParam("roomId", true);
roomName = qsParam("roomName", true);
startAudioOnly = qsParam("isAudioOnly", true) === "true";
startWithAudioMuted = parseBooleanOrUndefined(qsParam("startWithAudioMuted", true));
startWithVideoMuted = parseBooleanOrUndefined(qsParam("startWithVideoMuted", true));
isVideoChannel = qsParam("isVideoChannel", true) === "true";
supportsScreensharing = qsParam("supportsScreensharing", true) === "true";
// We've reached the point where we have to wait for the config, so do that then parse it.
const instanceConfig = new SnakedObject<IConfigOptions>((await configPromise) ?? <IConfigOptions>{});
const jitsiConfig = instanceConfig.get("jitsi_widget");
if (jitsiConfig) {
skipOurWelcomeScreen = new SnakedObject(jitsiConfig).get("skip_built_in_welcome_screen") ?? false;
}
// Either reveal the prejoin screen, or skip straight to Jitsi depending on the config.
// We don't set up the call yet though as this might lead to failure without the widget API.
toggleConferenceVisibility(skipOurWelcomeScreen);
if (widgetApi) {
await widgetApiReady;
}
// Now that everything should be set up, skip to the Jitsi splash screen if needed
if (skipOurWelcomeScreen) {
skipToJitsiSplashScreen();
}
enableJoinButton(); // always enable the button
} catch (e) {
logger.error("Error setting up Jitsi widget", e);
document.getElementById("widgetActionContainer")!.innerText = "Failed to load Jitsi widget";
}
})();
function enableJoinButton(): void {
document.getElementById("joinButton")!.onclick = (): Promise<void> => joinConference();
}
function switchVisibleContainers(): void {
inConference = !inConference;
// Our welcome screen is managed by other code, so just don't switch to it ever
// if we're not supposed to.
if (!skipOurWelcomeScreen) {
toggleConferenceVisibility(inConference);
}
}
function toggleConferenceVisibility(inConference: boolean): void {
document.getElementById("jitsiContainer")!.style.visibility = inConference ? "unset" : "hidden";
document.getElementById("joinButtonContainer")!.style.visibility = inConference ? "hidden" : "unset";
}
function skipToJitsiSplashScreen(): void {
// really just a function alias for self-documenting code
void joinConference();
}
/**
* Create a JWT token for jitsi openidtoken-jwt auth
*
* See https://github.com/matrix-org/prosody-mod-auth-matrix-user-verification
*/
function createJWTToken(openIdToken: IOpenIDCredentials): string {
// Header
const header = { alg: "HS256", typ: "JWT" };
// Payload
const payload = {
// As per Jitsi token auth, `iss` needs to be set to something agreed between
// JWT generating side and Prosody config. Since we have no configuration for
// the widgets, we can't set one anywhere. Using the Jitsi domain here probably makes sense.
iss: jitsiDomain,
sub: jitsiDomain,
aud: `https://${jitsiDomain}`,
room: "*",
context: {
matrix: {
token: openIdToken.access_token,
room_id: roomId,
server_name: openIdToken.matrix_server_name,
},
user: {
avatar: avatarUrl,
name: displayName,
},
},
};
// Sign JWT
// The secret string here is irrelevant, we're only using the JWT
// to transport data to Prosody in the Jitsi stack.
return KJUR.jws.JWS.sign("HS256", JSON.stringify(header), JSON.stringify(payload), "notused");
}
async function notifyHangup(errorMessage?: string): Promise<void> {
if (widgetApi) {
// We send the hangup event before setAlwaysOnScreen, because the latter
// can cause the receiving side to instantly stop listening.
try {
await widgetApi.transport.send(ElementWidgetActions.HangupCall, { errorMessage });
} finally {
await widgetApi.setAlwaysOnScreen(false);
}
}
}
function closeConference(): void {
switchVisibleContainers();
document.getElementById("jitsiContainer")!.innerHTML = "";
if (skipOurWelcomeScreen) {
skipToJitsiSplashScreen();
}
}
// Converts from IETF language tags used by Element (en-US) to the format used
// by Jitsi (enUS)
function normalizeLanguage(language: string): string {
const [lang, variant] = language.replace("_", "-").split("-");
if (!variant || lang === variant) {
return lang;
}
return lang + variant.toUpperCase();
}
function mapLanguage(language: string): string {
// Element and Jitsi don't agree how to interpret en, so we go with Elements
// interpretation to stay consistent
switch (language) {
case "en":
return "enGB";
case "enUS":
return "en";
default:
return language;
}
}
// event handler bound in HTML
// An audio input of undefined instructs Jitsi to start unmuted with whatever
// audio input it can find, while an input of null instructs it to start muted,
// and a non-nullish input specifies the label of a specific device to use.
// Same for video inputs.
async function joinConference(audioInput?: string | null, videoInput?: string | null): Promise<void> {
let jwt: string | undefined;
if (jitsiAuth === JITSI_OPENIDTOKEN_JWT_AUTH) {
// See https://github.com/matrix-org/prosody-mod-auth-matrix-user-verification
const openIdToken = await widgetApi?.requestOpenIDConnectToken();
logger.log("Got OpenID Connect token");
if (!openIdToken?.access_token) {
// eslint-disable-line camelcase
// We've failing to get a token, don't try to init conference
logger.warn("Expected to have an OpenID credential, cannot initialize widget.");
document.getElementById("widgetActionContainer")!.innerText = "Failed to load Jitsi widget";
return;
}
jwt = createJWTToken(openIdToken);
}
switchVisibleContainers();
logger.warn(
"[Jitsi Widget] The next few errors about failing to parse URL parameters are fine if " +
"they mention 'external_api' or 'jitsi' in the stack. They're just Jitsi Meet trying to parse " +
"our fragment values and not recognizing the options.",
);
const options: ExternalAPIOptions = {
width: "100%",
height: "100%",
parentNode: document.querySelector("#jitsiContainer") ?? undefined,
roomName: conferenceId,
devices: {
audioInput,
videoInput,
},
userInfo: {
displayName,
email: userId,
},
interfaceConfigOverwrite: {
SHOW_JITSI_WATERMARK: false,
SHOW_WATERMARK_FOR_GUESTS: false,
MAIN_TOOLBAR_BUTTONS: [],
VIDEO_LAYOUT_FIT: "height",
},
configOverwrite: {
subject: roomName,
startAudioOnly,
startWithAudioMuted: audioInput === null ? true : startWithAudioMuted,
startWithVideoMuted: videoInput === null ? true : startWithVideoMuted,
// Request some log levels for inclusion in rageshakes
// Ideally we would capture all possible log levels, but this can
// cause Jitsi Meet to try to post various circular data structures
// back over the iframe API, and therefore end up crashing
// https://github.com/jitsi/jitsi-meet/issues/11585
apiLogLevels: ["warn", "error"],
} as any,
jwt: jwt,
lang: mapLanguage(normalizeLanguage(language)),
};
// Video channel widgets need some more tailored config options
if (isVideoChannel) {
// We don't skip jitsi's prejoin screen for video rooms.
options.configOverwrite!.prejoinConfig = { enabled: true };
// Use a simplified set of toolbar buttons
options.configOverwrite!.toolbarButtons = ["microphone", "camera", "tileview", "hangup"];
// Note: We can hide the screenshare button in video rooms but not in
// normal conference calls, since in video rooms we control exactly what
// set of controls appear, but in normal calls we need to leave that up
// to the deployment's configuration.
// https://github.com/element-hq/element-web/issues/4880#issuecomment-940002464
if (supportsScreensharing) options.configOverwrite!.toolbarButtons.splice(2, 0, "desktop");
// Hide all top bar elements
options.configOverwrite!.conferenceInfo = { autoHide: [] };
// Remove the ability to hide your own tile, since we're hiding the
// settings button which would be the only way to get it back
options.configOverwrite!.disableSelfViewSettings = true;
}
meetApi = new JitsiMeetExternalAPI(jitsiDomain, options);
// fires once when user joins the conference
// (regardless of video on or off)
meetApi.on("videoConferenceJoined", onVideoConferenceJoined);
meetApi.on("videoConferenceLeft", onVideoConferenceLeft);
meetApi.on("readyToClose", closeConference as ExternalAPIEventCallbacks["readyToClose"]);
meetApi.on("errorOccurred", onErrorOccurred);
meetApi.on("audioMuteStatusChanged", onMuteStatusChanged);
meetApi.on("videoMuteStatusChanged", onVideoMuteStatusChanged);
(["videoConferenceJoined", "participantJoined", "participantLeft"] as const).forEach((event) => {
meetApi!.on(event, updateParticipants);
});
// Patch logs into rageshakes
meetApi.on("log", onLog);
}
const onVideoConferenceJoined = (): void => {
// Although we set our displayName with the userInfo option above, that
// option has a bug where it causes the name to be the HTML encoding of
// what was actually intended. So, we use the displayName command to at
// least ensure that the name is correct after entering the meeting.
// https://github.com/jitsi/jitsi-meet/issues/11664
// We can't just use these commands immediately after creating the
// iframe, because there's *another* bug where they can crash Jitsi by
// racing with its startup process.
if (displayName) meetApi?.executeCommand("displayName", displayName);
// This doesn't have a userInfo equivalent, so has to be set via commands
if (avatarUrl) meetApi?.executeCommand("avatarUrl", avatarUrl);
if (widgetApi) {
// ignored promise because we don't care if it works
// noinspection JSIgnoredPromiseFromCall
void widgetApi.setAlwaysOnScreen(true);
void widgetApi.transport.send(ElementWidgetActions.JoinCall, {});
}
// Video rooms should start in tile mode
if (isVideoChannel) meetApi?.executeCommand("setTileView", true);
};
const onVideoConferenceLeft = (): void => {
void notifyHangup();
meetApi = undefined;
};
const onErrorOccurred = ({ error }: Parameters<ExternalAPIEventCallbacks["errorOccurred"]>[0]): void => {
if (error.isFatal) {
// We got disconnected. Since Jitsi Meet might send us back to the
// prejoin screen, we're forced to act as if we hung up entirely.
void notifyHangup(error.message);
meetApi = undefined;
closeConference();
}
};
const onMuteStatusChanged = async (): Promise<void> => {
if (!meetApi) return;
const [audioEnabled, videoEnabled] = await checkAudioVideoEnabled();
void widgetApi?.transport.send(ElementWidgetActions.DeviceMute, {
audio_enabled: audioEnabled,
video_enabled: videoEnabled,
});
};
const onVideoMuteStatusChanged = ({ muted }: VideoMuteStatusChangedEvent): void => {
if (muted) {
// Jitsi Meet always sends a "video muted" event directly before
// hanging up, which we need to ignore by padding the timeout here,
// otherwise the React SDK will mistakenly think the user turned off
// their video by hand
setTimeout(() => onMuteStatusChanged, 200);
} else {
void onMuteStatusChanged();
}
};
const updateParticipants = (): void => {
void widgetApi?.transport.send(ElementWidgetActions.CallParticipants, {
participants: meetApi?.getParticipantsInfo(),
});
};
const onLog = ({ logLevel, args }: LogEvent): void =>
(parent as unknown as typeof global).mx_rage_logger?.log(logLevel, ...args);
+19
View File
@@ -0,0 +1,19 @@
/*
Copyright 2024 New Vector 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.
*/
/**
* Because we've been saving a lot of additional logger data in the localStorage for no particular reason
* we need to, hopefully, unbrick user's devices by geting rid of unnecessary data.
* */
if (window.localStorage) {
Object.keys(window.localStorage).forEach((key) => {
if (key.startsWith("loglevel:")) {
window.localStorage.removeItem(key);
}
});
}
@@ -0,0 +1,46 @@
<svg id="livetype" xmlns="http://www.w3.org/2000/svg" width="119.66407" height="40" viewBox="0 0 119.66407 40">
<title>Download_on_the_App_Store_Badge_US-UK_RGB_blk_4SVG_092917</title>
<g>
<g>
<g>
<path d="M110.13477,0H9.53468c-.3667,0-.729,0-1.09473.002-.30615.002-.60986.00781-.91895.0127A13.21476,13.21476,0,0,0,5.5171.19141a6.66509,6.66509,0,0,0-1.90088.627A6.43779,6.43779,0,0,0,1.99757,1.99707,6.25844,6.25844,0,0,0,.81935,3.61816a6.60119,6.60119,0,0,0-.625,1.90332,12.993,12.993,0,0,0-.1792,2.002C.00587,7.83008.00489,8.1377,0,8.44434V31.5586c.00489.3105.00587.6113.01515.9219a12.99232,12.99232,0,0,0,.1792,2.0019,6.58756,6.58756,0,0,0,.625,1.9043A6.20778,6.20778,0,0,0,1.99757,38.001a6.27445,6.27445,0,0,0,1.61865,1.1787,6.70082,6.70082,0,0,0,1.90088.6308,13.45514,13.45514,0,0,0,2.0039.1768c.30909.0068.6128.0107.91895.0107C8.80567,40,9.168,40,9.53468,40H110.13477c.3594,0,.7246,0,1.084-.002.3047,0,.6172-.0039.9219-.0107a13.279,13.279,0,0,0,2-.1768,6.80432,6.80432,0,0,0,1.9082-.6308,6.27742,6.27742,0,0,0,1.6172-1.1787,6.39482,6.39482,0,0,0,1.1816-1.6143,6.60413,6.60413,0,0,0,.6191-1.9043,13.50643,13.50643,0,0,0,.1856-2.0019c.0039-.3106.0039-.6114.0039-.9219.0078-.3633.0078-.7246.0078-1.0938V9.53613c0-.36621,0-.72949-.0078-1.09179,0-.30664,0-.61426-.0039-.9209a13.5071,13.5071,0,0,0-.1856-2.002,6.6177,6.6177,0,0,0-.6191-1.90332,6.46619,6.46619,0,0,0-2.7988-2.7998,6.76754,6.76754,0,0,0-1.9082-.627,13.04394,13.04394,0,0,0-2-.17676c-.3047-.00488-.6172-.01074-.9219-.01269-.3594-.002-.7246-.002-1.084-.002Z" style="fill: #a6a6a6"/>
<path d="M8.44483,39.125c-.30468,0-.602-.0039-.90429-.0107a12.68714,12.68714,0,0,1-1.86914-.1631,5.88381,5.88381,0,0,1-1.65674-.5479,5.40573,5.40573,0,0,1-1.397-1.0166,5.32082,5.32082,0,0,1-1.02051-1.3965,5.72186,5.72186,0,0,1-.543-1.6572,12.41351,12.41351,0,0,1-.1665-1.875c-.00634-.2109-.01464-.9131-.01464-.9131V8.44434S.88185,7.75293.8877,7.5498a12.37039,12.37039,0,0,1,.16553-1.87207,5.7555,5.7555,0,0,1,.54346-1.6621A5.37349,5.37349,0,0,1,2.61183,2.61768,5.56543,5.56543,0,0,1,4.01417,1.59521a5.82309,5.82309,0,0,1,1.65332-.54394A12.58589,12.58589,0,0,1,7.543.88721L8.44532.875H111.21387l.9131.0127a12.38493,12.38493,0,0,1,1.8584.16259,5.93833,5.93833,0,0,1,1.6709.54785,5.59374,5.59374,0,0,1,2.415,2.41993,5.76267,5.76267,0,0,1,.5352,1.64892,12.995,12.995,0,0,1,.1738,1.88721c.0029.2832.0029.5874.0029.89014.0079.375.0079.73193.0079,1.09179V30.4648c0,.3633,0,.7178-.0079,1.0752,0,.3252,0,.6231-.0039.9297a12.73126,12.73126,0,0,1-.1709,1.8535,5.739,5.739,0,0,1-.54,1.67,5.48029,5.48029,0,0,1-1.0156,1.3857,5.4129,5.4129,0,0,1-1.3994,1.0225,5.86168,5.86168,0,0,1-1.668.5498,12.54218,12.54218,0,0,1-1.8692.1631c-.2929.0068-.5996.0107-.8974.0107l-1.084.002Z"/>
</g>
<g id="_Group_" data-name="&lt;Group&gt;">
<g id="_Group_2" data-name="&lt;Group&gt;">
<g id="_Group_3" data-name="&lt;Group&gt;">
<path id="_Path_" data-name="&lt;Path&gt;" d="M24.76888,20.30068a4.94881,4.94881,0,0,1,2.35656-4.15206,5.06566,5.06566,0,0,0-3.99116-2.15768c-1.67924-.17626-3.30719,1.00483-4.1629,1.00483-.87227,0-2.18977-.98733-3.6085-.95814a5.31529,5.31529,0,0,0-4.47292,2.72787c-1.934,3.34842-.49141,8.26947,1.3612,10.97608.9269,1.32535,2.01018,2.8058,3.42763,2.7533,1.38706-.05753,1.9051-.88448,3.5794-.88448,1.65876,0,2.14479.88448,3.591.8511,1.48838-.02416,2.42613-1.33124,3.32051-2.66914a10.962,10.962,0,0,0,1.51842-3.09251A4.78205,4.78205,0,0,1,24.76888,20.30068Z" style="fill: #fff"/>
<path id="_Path_2" data-name="&lt;Path&gt;" d="M22.03725,12.21089a4.87248,4.87248,0,0,0,1.11452-3.49062,4.95746,4.95746,0,0,0-3.20758,1.65961,4.63634,4.63634,0,0,0-1.14371,3.36139A4.09905,4.09905,0,0,0,22.03725,12.21089Z" style="fill: #fff"/>
</g>
</g>
<g>
<path d="M42.30227,27.13965h-4.7334l-1.13672,3.35645H34.42727l4.4834-12.418h2.083l4.4834,12.418H43.438ZM38.0591,25.59082h3.752l-1.84961-5.44727h-.05176Z" style="fill: #fff"/>
<path d="M55.15969,25.96973c0,2.81348-1.50586,4.62109-3.77832,4.62109a3.0693,3.0693,0,0,1-2.84863-1.584h-.043v4.48438h-1.8584V21.44238H48.4302v1.50586h.03418a3.21162,3.21162,0,0,1,2.88281-1.60059C53.645,21.34766,55.15969,23.16406,55.15969,25.96973Zm-1.91016,0c0-1.833-.94727-3.03809-2.39258-3.03809-1.41992,0-2.375,1.23047-2.375,3.03809,0,1.82422.95508,3.0459,2.375,3.0459C52.30227,29.01563,53.24953,27.81934,53.24953,25.96973Z" style="fill: #fff"/>
<path d="M65.12453,25.96973c0,2.81348-1.50586,4.62109-3.77832,4.62109a3.0693,3.0693,0,0,1-2.84863-1.584h-.043v4.48438h-1.8584V21.44238H58.395v1.50586h.03418A3.21162,3.21162,0,0,1,61.312,21.34766C63.60988,21.34766,65.12453,23.16406,65.12453,25.96973Zm-1.91016,0c0-1.833-.94727-3.03809-2.39258-3.03809-1.41992,0-2.375,1.23047-2.375,3.03809,0,1.82422.95508,3.0459,2.375,3.0459C62.26711,29.01563,63.21438,27.81934,63.21438,25.96973Z" style="fill: #fff"/>
<path d="M71.71047,27.03613c.1377,1.23145,1.334,2.04,2.96875,2.04,1.56641,0,2.69336-.80859,2.69336-1.91895,0-.96387-.67969-1.541-2.28906-1.93652l-1.60937-.3877c-2.28027-.55078-3.33887-1.61719-3.33887-3.34766,0-2.14258,1.86719-3.61426,4.51855-3.61426,2.624,0,4.42285,1.47168,4.4834,3.61426h-1.876c-.1123-1.23926-1.13672-1.9873-2.63379-1.9873s-2.52148.75684-2.52148,1.8584c0,.87793.6543,1.39453,2.25488,1.79l1.36816.33594c2.54785.60254,3.60645,1.626,3.60645,3.44238,0,2.32324-1.85059,3.77832-4.79395,3.77832-2.75391,0-4.61328-1.4209-4.7334-3.667Z" style="fill: #fff"/>
<path d="M83.34621,19.2998v2.14258h1.72168v1.47168H83.34621v4.99121c0,.77539.34473,1.13672,1.10156,1.13672a5.80752,5.80752,0,0,0,.61133-.043v1.46289a5.10351,5.10351,0,0,1-1.03223.08594c-1.833,0-2.54785-.68848-2.54785-2.44434V22.91406H80.16262V21.44238H81.479V19.2998Z" style="fill: #fff"/>
<path d="M86.065,25.96973c0-2.84863,1.67773-4.63867,4.29395-4.63867,2.625,0,4.29492,1.79,4.29492,4.63867,0,2.85645-1.66113,4.63867-4.29492,4.63867C87.72609,30.6084,86.065,28.82617,86.065,25.96973Zm6.69531,0c0-1.9541-.89551-3.10742-2.40137-3.10742s-2.40039,1.16211-2.40039,3.10742c0,1.96191.89453,3.10645,2.40039,3.10645S92.76027,27.93164,92.76027,25.96973Z" style="fill: #fff"/>
<path d="M96.18606,21.44238h1.77246v1.541h.043a2.1594,2.1594,0,0,1,2.17773-1.63574,2.86616,2.86616,0,0,1,.63672.06934v1.73828a2.59794,2.59794,0,0,0-.835-.1123,1.87264,1.87264,0,0,0-1.93652,2.083v5.37012h-1.8584Z" style="fill: #fff"/>
<path d="M109.3843,27.83691c-.25,1.64355-1.85059,2.77148-3.89844,2.77148-2.63379,0-4.26855-1.76465-4.26855-4.5957,0-2.83984,1.64355-4.68164,4.19043-4.68164,2.50488,0,4.08008,1.7207,4.08008,4.46582v.63672h-6.39453v.1123a2.358,2.358,0,0,0,2.43555,2.56445,2.04834,2.04834,0,0,0,2.09082-1.27344Zm-6.28223-2.70215h4.52637a2.1773,2.1773,0,0,0-2.2207-2.29785A2.292,2.292,0,0,0,103.10207,25.13477Z" style="fill: #fff"/>
</g>
</g>
</g>
<g id="_Group_4" data-name="&lt;Group&gt;">
<g>
<path d="M37.82619,8.731a2.63964,2.63964,0,0,1,2.80762,2.96484c0,1.90625-1.03027,3.002-2.80762,3.002H35.67092V8.731Zm-1.22852,5.123h1.125a1.87588,1.87588,0,0,0,1.96777-2.146,1.881,1.881,0,0,0-1.96777-2.13379h-1.125Z" style="fill: #fff"/>
<path d="M41.68068,12.44434a2.13323,2.13323,0,1,1,4.24707,0,2.13358,2.13358,0,1,1-4.24707,0Zm3.333,0c0-.97607-.43848-1.54687-1.208-1.54687-.77246,0-1.207.5708-1.207,1.54688,0,.98389.43457,1.55029,1.207,1.55029C44.57522,13.99463,45.01369,13.42432,45.01369,12.44434Z" style="fill: #fff"/>
<path d="M51.57326,14.69775h-.92187l-.93066-3.31641h-.07031l-.92676,3.31641h-.91309l-1.24121-4.50293h.90137l.80664,3.436h.06641l.92578-3.436h.85254l.92578,3.436h.07031l.80273-3.436h.88867Z" style="fill: #fff"/>
<path d="M53.85354,10.19482H54.709v.71533h.06641a1.348,1.348,0,0,1,1.34375-.80225,1.46456,1.46456,0,0,1,1.55859,1.6748v2.915h-.88867V12.00586c0-.72363-.31445-1.0835-.97168-1.0835a1.03294,1.03294,0,0,0-1.0752,1.14111v2.63428h-.88867Z" style="fill: #fff"/>
<path d="M59.09377,8.437h.88867v6.26074h-.88867Z" style="fill: #fff"/>
<path d="M61.21779,12.44434a2.13346,2.13346,0,1,1,4.24756,0,2.1338,2.1338,0,1,1-4.24756,0Zm3.333,0c0-.97607-.43848-1.54687-1.208-1.54687-.77246,0-1.207.5708-1.207,1.54688,0,.98389.43457,1.55029,1.207,1.55029C64.11232,13.99463,64.5508,13.42432,64.5508,12.44434Z" style="fill: #fff"/>
<path d="M66.4009,13.42432c0-.81055.60352-1.27783,1.6748-1.34424l1.21973-.07031v-.38867c0-.47559-.31445-.74414-.92187-.74414-.49609,0-.83984.18213-.93848.50049h-.86035c.09082-.77344.81836-1.26953,1.83984-1.26953,1.12891,0,1.76563.562,1.76563,1.51318v3.07666h-.85547v-.63281h-.07031a1.515,1.515,0,0,1-1.35254.707A1.36026,1.36026,0,0,1,66.4009,13.42432Zm2.89453-.38477v-.37646l-1.09961.07031c-.62012.0415-.90137.25244-.90137.64941,0,.40527.35156.64111.835.64111A1.0615,1.0615,0,0,0,69.29543,13.03955Z" style="fill: #fff"/>
<path d="M71.34816,12.44434c0-1.42285.73145-2.32422,1.86914-2.32422a1.484,1.484,0,0,1,1.38086.79h.06641V8.437h.88867v6.26074h-.85156v-.71143h-.07031a1.56284,1.56284,0,0,1-1.41406.78564C72.0718,14.772,71.34816,13.87061,71.34816,12.44434Zm.918,0c0,.95508.4502,1.52979,1.20313,1.52979.749,0,1.21191-.583,1.21191-1.52588,0-.93848-.46777-1.52979-1.21191-1.52979C72.72121,10.91846,72.26613,11.49707,72.26613,12.44434Z" style="fill: #fff"/>
<path d="M79.23,12.44434a2.13323,2.13323,0,1,1,4.24707,0,2.13358,2.13358,0,1,1-4.24707,0Zm3.333,0c0-.97607-.43848-1.54687-1.208-1.54687-.77246,0-1.207.5708-1.207,1.54688,0,.98389.43457,1.55029,1.207,1.55029C82.12453,13.99463,82.563,13.42432,82.563,12.44434Z" style="fill: #fff"/>
<path d="M84.66945,10.19482h.85547v.71533h.06641a1.348,1.348,0,0,1,1.34375-.80225,1.46456,1.46456,0,0,1,1.55859,1.6748v2.915H87.605V12.00586c0-.72363-.31445-1.0835-.97168-1.0835a1.03294,1.03294,0,0,0-1.0752,1.14111v2.63428h-.88867Z" style="fill: #fff"/>
<path d="M93.51516,9.07373v1.1416h.97559v.74854h-.97559V13.2793c0,.47168.19434.67822.63672.67822a2.96657,2.96657,0,0,0,.33887-.02051v.74023a2.9155,2.9155,0,0,1-.4834.04541c-.98828,0-1.38184-.34766-1.38184-1.21582v-2.543h-.71484v-.74854h.71484V9.07373Z" style="fill: #fff"/>
<path d="M95.70461,8.437h.88086v2.48145h.07031a1.3856,1.3856,0,0,1,1.373-.80664,1.48339,1.48339,0,0,1,1.55078,1.67871v2.90723H98.69v-2.688c0-.71924-.335-1.0835-.96289-1.0835a1.05194,1.05194,0,0,0-1.13379,1.1416v2.62988h-.88867Z" style="fill: #fff"/>
<path d="M104.76125,13.48193a1.828,1.828,0,0,1-1.95117,1.30273A2.04531,2.04531,0,0,1,100.73,12.46045a2.07685,2.07685,0,0,1,2.07617-2.35254c1.25293,0,2.00879.856,2.00879,2.27V12.688h-3.17969v.0498a1.1902,1.1902,0,0,0,1.19922,1.29,1.07934,1.07934,0,0,0,1.07129-.5459Zm-3.126-1.45117h2.27441a1.08647,1.08647,0,0,0-1.1084-1.1665A1.15162,1.15162,0,0,0,101.63527,12.03076Z" style="fill: #fff"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

@@ -0,0 +1,53 @@
<svg width="375" height="178" viewBox="0 0 375 178" fill="none" preserveAspectRatio="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_382_1634)">
<rect width="375" height="178" fill="white"/>
<g filter="url(#filter0_f_382_1634)">
<ellipse cx="187.5" cy="645.488" rx="403.5" ry="612.805" fill="url(#paint0_linear_382_1634)"/>
</g>
<g filter="url(#filter1_f_382_1634)">
<ellipse cx="187.5" cy="650.529" rx="385.3" ry="585.164" fill="#101317"/>
</g>
</g>
<defs>
<filter id="filter0_f_382_1634" x="-248.683" y="-6.48499e-05" width="872.367" height="1290.98" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="16.3415" result="effect1_foregroundBlur_382_1634"/>
</filter>
<filter id="filter1_f_382_1634" x="-230.483" y="32.6826" width="835.966" height="1235.69" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="16.3415" result="effect1_foregroundBlur_382_1634"/>
</filter>
<linearGradient id="paint0_linear_382_1634" x1="141.636" y1="133.796" x2="141.636" y2="32.6829" gradientUnits="userSpaceOnUse">
<stop stop-color="#062993"/>
<stop offset="0.040404" stop-color="#02389D"/>
<stop offset="0.0808081" stop-color="#0045A6"/>
<stop offset="0.121212" stop-color="#0051AD"/>
<stop offset="0.161616" stop-color="#005DB4"/>
<stop offset="0.20202" stop-color="#0069BA"/>
<stop offset="0.242424" stop-color="#0075BB"/>
<stop offset="0.282828" stop-color="#0081BB"/>
<stop offset="0.323232" stop-color="#008CB9"/>
<stop offset="0.363636" stop-color="#0098B7"/>
<stop offset="0.40404" stop-color="#00A3B3"/>
<stop offset="0.444444" stop-color="#00AEAD"/>
<stop offset="0.484848" stop-color="#00B8A4"/>
<stop offset="0.525253" stop-color="#00C2A0"/>
<stop offset="0.565657" stop-color="#00CC99"/>
<stop offset="0.606061" stop-color="#3AD396"/>
<stop offset="0.646465" stop-color="#5DD898"/>
<stop offset="0.686869" stop-color="#79DD99"/>
<stop offset="0.727273" stop-color="#92E29B"/>
<stop offset="0.767677" stop-color="#A8E69F"/>
<stop offset="0.808081" stop-color="#BBEAA5"/>
<stop offset="0.848485" stop-color="#CDEEAE"/>
<stop offset="0.888889" stop-color="#DCF2B9"/>
<stop offset="0.929293" stop-color="#EAF6C7"/>
<stop offset="0.969697" stop-color="#F5FBD5"/>
</linearGradient>
<clipPath id="clip0_382_1634">
<rect width="375" height="178" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

@@ -0,0 +1,8 @@
<svg width="149" height="32" viewBox="0 0 149 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.3571 32C25.1936 32 32.3571 24.8366 32.3571 16C32.3571 7.16344 25.1936 0 16.3571 0C7.5205 0 0.357056 7.16344 0.357056 16C0.357056 24.8366 7.5205 32 16.3571 32Z" fill="#0DBD8B"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.4318 7.45512C13.4318 6.80928 13.9565 6.28573 14.6037 6.28573C18.9901 6.28573 22.546 9.83425 22.546 14.2116C22.546 14.8574 22.0214 15.381 21.3742 15.381C20.727 15.381 20.2023 14.8574 20.2023 14.2116C20.2023 11.1259 17.6957 8.62452 14.6037 8.62452C13.9565 8.62452 13.4318 8.10096 13.4318 7.45512Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M24.8996 13.0422C25.5467 13.0422 26.0714 13.5658 26.0714 14.2116C26.0714 18.589 22.5155 22.1375 18.129 22.1375C17.4819 22.1375 16.9572 21.6139 16.9572 20.9681C16.9572 20.3223 17.4819 19.7987 18.129 19.7987C21.2211 19.7987 23.7277 17.2973 23.7277 14.2116C23.7277 13.5658 24.2524 13.0422 24.8996 13.0422Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.3008 24.5448C19.3008 25.1906 18.7762 25.7142 18.129 25.7142C13.7426 25.7142 10.1867 22.1656 10.1867 17.7883C10.1867 17.1425 10.7113 16.6189 11.3585 16.6189C12.0057 16.6189 12.5303 17.1425 12.5303 17.7883C12.5303 20.8739 15.0369 23.3754 18.129 23.3754C18.7762 23.3754 19.3008 23.8989 19.3008 24.5448Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.81455 18.9578C7.16737 18.9578 6.64272 18.4342 6.64272 17.7884C6.64272 13.411 10.1986 9.86251 14.5851 9.86251C15.2323 9.86251 15.7569 10.3861 15.7569 11.0319C15.7569 11.6777 15.2323 12.2013 14.5851 12.2013C11.493 12.2013 8.98638 14.7027 8.98638 17.7884C8.98638 18.4342 8.46173 18.9578 7.81455 18.9578Z" fill="white"/>
<path d="M56.3571 17.2858H45.0714C45.2048 18.4667 45.6333 19.4096 46.3571 20.1144C47.0809 20.8001 48.0333 21.1429 49.2143 21.1429C49.9952 21.1429 50.7 20.9525 51.3286 20.5715C51.9571 20.1905 52.4048 19.6763 52.6714 19.0286H56.1C55.6429 20.5334 54.7857 21.7525 53.5286 22.6858C52.2905 23.6001 50.8238 24.0572 49.1286 24.0572C46.919 24.0572 45.1286 23.3239 43.7571 21.8572C42.4048 20.3905 41.7286 18.5334 41.7286 16.2858C41.7286 14.0953 42.4143 12.2572 43.7857 10.7715C45.1571 9.28578 46.9286 8.54293 49.1 8.54293C51.2714 8.54293 53.0238 9.27626 54.3571 10.7429C55.7095 12.1905 56.3857 14.0191 56.3857 16.2286L56.3571 17.2858ZM49.1 11.3144C48.0333 11.3144 47.1476 11.6286 46.4429 12.2572C45.7381 12.8858 45.3 13.7239 45.1286 14.7715H53.0143C52.8619 13.7239 52.4429 12.8858 51.7571 12.2572C51.0714 11.6286 50.1857 11.3144 49.1 11.3144ZM58.7254 19.2858V2.28578H62.1254V19.3429C62.1254 20.1048 62.5445 20.4858 63.3826 20.4858L63.9826 20.4572V23.6858C63.6588 23.7429 63.3159 23.7715 62.954 23.7715C61.4873 23.7715 60.4112 23.4001 59.7254 22.6572C59.0588 21.9144 58.7254 20.7905 58.7254 19.2858ZM79.817 17.2858H68.5312C68.6646 18.4667 69.0932 19.4096 69.817 20.1144C70.5408 20.8001 71.4932 21.1429 72.6741 21.1429C73.4551 21.1429 74.1598 20.9525 74.7884 20.5715C75.417 20.1905 75.8646 19.6763 76.1312 19.0286H79.5598C79.1027 20.5334 78.2455 21.7525 76.9884 22.6858C75.7503 23.6001 74.2836 24.0572 72.5884 24.0572C70.3789 24.0572 68.5884 23.3239 67.217 21.8572C65.8646 20.3905 65.1884 18.5334 65.1884 16.2858C65.1884 14.0953 65.8741 12.2572 67.2455 10.7715C68.617 9.28578 70.3884 8.54293 72.5598 8.54293C74.7312 8.54293 76.4836 9.27626 77.817 10.7429C79.1693 12.1905 79.8455 14.0191 79.8455 16.2286L79.817 17.2858ZM72.5598 11.3144C71.4932 11.3144 70.6074 11.6286 69.9027 12.2572C69.1979 12.8858 68.7598 13.7239 68.5884 14.7715H76.4741C76.3217 13.7239 75.9027 12.8858 75.217 12.2572C74.5312 11.6286 73.6455 11.3144 72.5598 11.3144ZM95.1567 15.2001V23.7144H91.7567V14.8286C91.7567 12.581 90.8234 11.4572 88.9567 11.4572C87.9472 11.4572 87.1376 11.781 86.5281 12.4286C85.9376 13.0763 85.6424 13.962 85.6424 15.0858V23.7144H82.2424V8.88578H85.3853V10.8572C85.7472 10.1905 86.2996 9.63816 87.0424 9.20007C87.7853 8.76197 88.7091 8.54293 89.8138 8.54293C91.871 8.54293 93.3567 9.32388 94.271 10.8858C95.5281 9.32388 97.2043 8.54293 99.2996 8.54293C101.033 8.54293 102.366 9.08578 103.3 10.1715C104.233 11.2382 104.7 12.6477 104.7 14.4001V23.7144H101.3V14.8286C101.3 12.581 100.366 11.4572 98.4996 11.4572C97.471 11.4572 96.6519 11.7905 96.0424 12.4572C95.4519 13.1048 95.1567 14.0191 95.1567 15.2001ZM121.608 17.2858H110.323C110.456 18.4667 110.884 19.4096 111.608 20.1144C112.332 20.8001 113.284 21.1429 114.465 21.1429C115.246 21.1429 115.951 20.9525 116.58 20.5715C117.208 20.1905 117.656 19.6763 117.923 19.0286H121.351C120.894 20.5334 120.037 21.7525 118.78 22.6858C117.542 23.6001 116.075 24.0572 114.38 24.0572C112.17 24.0572 110.38 23.3239 109.008 21.8572C107.656 20.3905 106.98 18.5334 106.98 16.2858C106.98 14.0953 107.665 12.2572 109.037 10.7715C110.408 9.28578 112.18 8.54293 114.351 8.54293C116.523 8.54293 118.275 9.27626 119.608 10.7429C120.961 12.1905 121.637 14.0191 121.637 16.2286L121.608 17.2858ZM114.351 11.3144C113.284 11.3144 112.399 11.6286 111.694 12.2572C110.989 12.8858 110.551 13.7239 110.38 14.7715H118.265C118.113 13.7239 117.694 12.8858 117.008 12.2572C116.323 11.6286 115.437 11.3144 114.351 11.3144ZM127.177 8.88578V10.8572C127.519 10.2096 128.081 9.66674 128.862 9.22864C129.662 8.7715 130.624 8.54293 131.748 8.54293C133.5 8.54293 134.853 9.07626 135.805 10.1429C136.777 11.2096 137.262 12.6286 137.262 14.4001V23.7144H133.862V14.8286C133.862 13.781 133.615 12.962 133.119 12.3715C132.643 11.762 131.91 11.4572 130.919 11.4572C129.834 11.4572 128.977 11.781 128.348 12.4286C127.738 13.0763 127.434 13.9715 127.434 15.1144V23.7144H124.034V8.88578H127.177ZM147.192 20.6858V23.6286C146.773 23.7429 146.182 23.8001 145.421 23.8001C142.525 23.8001 141.078 22.3429 141.078 19.4286V11.6001H138.821V8.88578H141.078V5.02864H144.478V8.88578H147.249V11.6001H144.478V19.0858C144.478 20.2477 145.03 20.8286 146.135 20.8286L147.192 20.6858Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 5.8 KiB

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 135 40" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-miterlimit:2;">
<g transform="matrix(1,0,0,1,0,-1)">
<g>
<path d="M130,41L4.999,41C2.249,41 0,38.75 0,36L0,6C0,3.25 2.249,1 4.999,1L130,1C132.75,1 135,3.25 135,6L135,36C135,38.75 132.75,41 130,41Z" style="fill:rgb(3,4,4);fill-rule:nonzero;"/>
<path d="M130,1L4.999,1C2.249,1 0,3.25 0,6L0,36C0,38.75 2.249,41 4.999,41L130,41C132.75,41 135,38.75 135,36L135,6C135,3.25 132.75,1 130,1ZM130,1.8C132.316,1.8 134.2,3.685 134.2,6L134.2,36C134.2,38.316 132.316,40.201 130,40.201L4.999,40.201C2.684,40.201 0.8,38.316 0.8,36L0.8,6C0.8,3.685 2.684,1.8 4.999,1.8L130,1.8Z" style="fill:rgb(167,165,166);fill-rule:nonzero;"/>
<path d="M47.376,10.791L44.468,10.791L44.468,11.512L46.647,11.512C46.588,12.098 46.353,12.559 45.96,12.894C45.566,13.229 45.063,13.397 44.468,13.397C43.814,13.397 43.261,13.171 42.809,12.718C42.365,12.257 42.138,11.688 42.138,11C42.138,10.313 42.365,9.743 42.809,9.282C43.261,8.83 43.814,8.604 44.468,8.604C44.803,8.604 45.122,8.662 45.415,8.788C45.708,8.914 45.943,9.09 46.127,9.316L46.68,8.763C46.429,8.478 46.11,8.26 45.717,8.101C45.323,7.942 44.912,7.866 44.468,7.866C43.596,7.866 42.859,8.168 42.256,8.771C41.652,9.375 41.351,10.12 41.351,11C41.351,11.88 41.652,12.626 42.256,13.229C42.859,13.833 43.596,14.134 44.468,14.134C45.381,14.134 46.11,13.841 46.672,13.246C47.166,12.752 47.418,12.081 47.418,11.243C47.418,11.101 47.401,10.95 47.376,10.791Z" style="fill:white;fill-rule:nonzero;"/>
<path d="M48.524,8L48.524,14L52.027,14L52.027,13.263L49.295,13.263L49.295,11.361L51.758,11.361L51.758,10.64L49.295,10.64L49.295,8.738L52.027,8.738L52.027,8L48.524,8Z" style="fill:white;fill-rule:nonzero;"/>
<path d="M56.953,8.738L56.953,8L52.83,8L52.83,8.738L54.506,8.738L54.506,14L55.277,14L55.277,8.738L56.953,8.738Z" style="fill:white;fill-rule:nonzero;"/>
<rect x="59.937" y="8" width="0.771" height="6" style="fill:white;fill-rule:nonzero;"/>
<path d="M65.803,8.738L65.803,8L61.68,8L61.68,8.738L63.356,8.738L63.356,14L64.127,14L64.127,8.738L65.803,8.738Z" style="fill:white;fill-rule:nonzero;"/>
<path d="M73.605,8.78C73.01,8.168 72.281,7.866 71.409,7.866C70.538,7.866 69.808,8.168 69.213,8.771C68.618,9.366 68.325,10.112 68.325,11C68.325,11.889 68.618,12.634 69.213,13.229C69.808,13.833 70.538,14.134 71.409,14.134C72.272,14.134 73.01,13.833 73.605,13.229C74.2,12.634 74.493,11.889 74.493,11C74.493,10.12 74.2,9.375 73.605,8.78ZM69.767,9.282C70.211,8.83 70.755,8.604 71.409,8.604C72.063,8.604 72.607,8.83 73.043,9.282C73.487,9.727 73.705,10.305 73.705,11C73.705,11.696 73.487,12.274 73.043,12.718C72.607,13.171 72.063,13.397 71.409,13.397C70.755,13.397 70.211,13.171 69.767,12.718C69.331,12.266 69.113,11.696 69.113,11C69.113,10.305 69.331,9.735 69.767,9.282Z" style="fill:white;fill-rule:nonzero;"/>
<path d="M76.345,10.263L76.312,9.106L76.345,9.106L79.396,14L80.2,14L80.2,8L79.429,8L79.429,11.512L79.463,12.668L79.429,12.668L76.513,8L75.575,8L75.575,14L76.345,14L76.345,10.263Z" style="fill:white;fill-rule:nonzero;"/>
<path d="M47.376,10.791L44.468,10.791L44.468,11.512L46.647,11.512C46.588,12.098 46.353,12.559 45.96,12.894C45.566,13.229 45.063,13.397 44.468,13.397C43.814,13.397 43.261,13.171 42.809,12.718C42.365,12.257 42.138,11.688 42.138,11C42.138,10.313 42.365,9.743 42.809,9.282C43.261,8.83 43.814,8.604 44.468,8.604C44.803,8.604 45.122,8.662 45.415,8.788C45.708,8.914 45.943,9.09 46.127,9.316L46.68,8.763C46.429,8.478 46.11,8.26 45.717,8.101C45.323,7.942 44.912,7.866 44.468,7.866C43.596,7.866 42.859,8.168 42.256,8.771C41.652,9.375 41.351,10.12 41.351,11C41.351,11.88 41.652,12.626 42.256,13.229C42.859,13.833 43.596,14.134 44.468,14.134C45.381,14.134 46.11,13.841 46.672,13.246C47.166,12.752 47.418,12.081 47.418,11.243C47.418,11.101 47.401,10.95 47.376,10.791ZM48.524,8L48.524,14L52.027,14L52.027,13.263L49.295,13.263L49.295,11.361L51.758,11.361L51.758,10.64L49.295,10.64L49.295,8.738L52.027,8.738L52.027,8L48.524,8ZM56.953,8.738L56.953,8L52.831,8L52.831,8.738L54.507,8.738L54.507,14L55.277,14L55.277,8.738L56.953,8.738ZM60.708,8L59.937,8L59.937,14L60.708,14L60.708,8ZM65.803,8.738L65.803,8L61.68,8L61.68,8.738L63.356,8.738L63.356,14L64.127,14L64.127,8.738L65.803,8.738ZM73.605,8.78C73.01,8.168 72.281,7.866 71.409,7.866C70.537,7.866 69.808,8.168 69.213,8.771C68.618,9.366 68.325,10.112 68.325,11C68.325,11.889 68.618,12.634 69.213,13.229C69.808,13.833 70.537,14.134 71.409,14.134C72.272,14.134 73.01,13.833 73.605,13.229C74.2,12.634 74.493,11.889 74.493,11C74.493,10.12 74.2,9.375 73.605,8.78ZM69.767,9.282C70.211,8.83 70.755,8.604 71.409,8.604C72.063,8.604 72.607,8.83 73.043,9.282C73.487,9.727 73.705,10.305 73.705,11C73.705,11.696 73.487,12.274 73.043,12.718C72.607,13.171 72.063,13.397 71.409,13.397C70.755,13.397 70.211,13.171 69.767,12.718C69.331,12.266 69.113,11.696 69.113,11C69.113,10.305 69.331,9.735 69.767,9.282ZM76.346,10.263L76.312,9.106L76.346,9.106L79.396,14L80.2,14L80.2,8L79.429,8L79.429,11.512L79.463,12.668L79.429,12.668L76.513,8L75.575,8L75.575,14L76.346,14L76.346,10.263Z" style="fill:none;stroke:white;stroke-width:0.2px;"/>
<path d="M106.936,31.001L108.802,31.001L108.802,18.499L106.936,18.499L106.936,31.001ZM123.743,23.002L121.604,28.422L121.54,28.422L119.32,23.002L117.31,23.002L120.64,30.578L118.741,34.791L120.687,34.791L125.818,23.002L123.743,23.002ZM113.16,29.581C112.55,29.581 111.697,29.275 111.697,28.519C111.697,27.554 112.759,27.184 113.675,27.184C114.495,27.184 114.882,27.361 115.38,27.602C115.235,28.76 114.238,29.581 113.16,29.581ZM113.386,22.729C112.035,22.729 110.636,23.324 110.057,24.643L111.713,25.334C112.067,24.643 112.726,24.418 113.418,24.418C114.383,24.418 115.364,24.997 115.38,26.026L115.38,26.155C115.042,25.962 114.318,25.672 113.434,25.672C111.648,25.672 109.831,26.653 109.831,28.487C109.831,30.16 111.295,31.237 112.935,31.237C114.189,31.237 114.882,30.674 115.315,30.015L115.38,30.015L115.38,30.98L117.182,30.98L117.182,26.187C117.182,23.967 115.524,22.729 113.386,22.729ZM101.854,24.524L99.2,24.524L99.2,20.239L101.854,20.239C103.249,20.239 104.041,21.394 104.041,22.382C104.041,23.351 103.249,24.524 101.854,24.524ZM101.806,18.499L97.334,18.499L97.334,31.001L99.2,31.001L99.2,26.264L101.806,26.264C103.874,26.264 105.907,24.767 105.907,22.382C105.907,19.997 103.874,18.499 101.806,18.499ZM77.424,29.583C76.135,29.583 75.056,28.503 75.056,27.022C75.056,25.523 76.135,24.428 77.424,24.428C78.697,24.428 79.696,25.523 79.696,27.022C79.696,28.503 78.697,29.583 77.424,29.583ZM79.567,23.703L79.502,23.703C79.083,23.203 78.278,22.752 77.263,22.752C75.136,22.752 73.187,24.621 73.187,27.022C73.187,29.406 75.136,31.258 77.263,31.258C78.278,31.258 79.083,30.807 79.502,30.292L79.567,30.292L79.567,30.904C79.567,32.531 78.697,33.401 77.295,33.401C76.152,33.401 75.443,32.58 75.153,31.887L73.526,32.563C73.993,33.691 75.233,35.077 77.295,35.077C79.487,35.077 81.339,33.788 81.339,30.646L81.339,23.01L79.567,23.01L79.567,23.703ZM82.628,31.001L84.497,31.001L84.497,18.499L82.628,18.499L82.628,31.001ZM87.251,26.876C87.204,25.233 88.525,24.396 89.475,24.396C90.216,24.396 90.844,24.766 91.054,25.297L87.251,26.876ZM93.051,25.459C92.697,24.508 91.618,22.752 89.411,22.752C87.219,22.752 85.399,24.476 85.399,27.005C85.399,29.39 87.204,31.258 89.62,31.258C91.569,31.258 92.697,30.066 93.165,29.374L91.714,28.407C91.231,29.116 90.571,29.583 89.62,29.583C88.669,29.583 87.993,29.148 87.558,28.294L93.245,25.942L93.051,25.459ZM47.743,24.057L47.743,25.861L52.061,25.861C51.932,26.876 51.594,27.617 51.078,28.133C50.45,28.761 49.467,29.454 47.743,29.454C45.085,29.454 43.007,27.312 43.007,24.653C43.007,21.995 45.085,19.852 47.743,19.852C49.177,19.852 50.224,20.416 50.998,21.141L52.27,19.868C51.191,18.837 49.757,18.048 47.743,18.048C44.102,18.048 41.041,21.012 41.041,24.653C41.041,28.294 44.102,31.258 47.743,31.258C49.708,31.258 51.191,30.614 52.351,29.406C53.543,28.213 53.914,26.538 53.914,25.185C53.914,24.766 53.881,24.379 53.817,24.057L47.743,24.057ZM58.822,29.583C57.533,29.583 56.421,28.52 56.421,27.005C56.421,25.475 57.533,24.428 58.822,24.428C60.111,24.428 61.223,25.475 61.223,27.005C61.223,28.52 60.111,29.583 58.822,29.583ZM58.822,22.752C56.47,22.752 54.553,24.541 54.553,27.005C54.553,29.454 56.47,31.258 58.822,31.258C61.174,31.258 63.091,29.454 63.091,27.005C63.091,24.541 61.174,22.752 58.822,22.752ZM68.135,29.583C66.847,29.583 65.735,28.52 65.735,27.005C65.735,25.475 66.847,24.428 68.135,24.428C69.424,24.428 70.536,25.475 70.536,27.005C70.536,28.52 69.424,29.583 68.135,29.583ZM68.135,22.752C65.783,22.752 63.866,24.541 63.866,27.005C63.866,29.454 65.783,31.258 68.135,31.258C70.488,31.258 72.405,29.454 72.405,27.005C72.405,24.541 70.488,22.752 68.135,22.752Z" style="fill:white;fill-rule:nonzero;"/>
<path d="M20.717,20.425L10.07,31.725C10.071,31.727 10.071,31.729 10.072,31.731C10.398,32.959 11.519,33.862 12.849,33.862C13.381,33.862 13.88,33.718 14.308,33.466L14.342,33.446L26.326,26.531L20.717,20.425Z" style="fill:rgb(235,67,53);fill-rule:nonzero;"/>
<path d="M31.488,18.501L31.478,18.494L26.304,15.494L20.475,20.681L26.324,26.529L31.471,23.56C32.373,23.073 32.985,22.122 32.985,21.025C32.985,19.936 32.381,18.989 31.488,18.501Z" style="fill:rgb(250,188,19);fill-rule:nonzero;"/>
<path d="M10.07,10.278C10.006,10.514 9.972,10.761 9.972,11.018L9.972,30.985C9.972,31.242 10.005,31.49 10.07,31.725L21.083,20.714L10.07,10.278Z" style="fill:rgb(84,125,191);fill-rule:nonzero;"/>
<path d="M20.795,21.001L26.306,15.492L14.335,8.552C13.9,8.291 13.393,8.141 12.849,8.141C11.519,8.141 10.397,9.046 10.07,10.275L10.07,10.278L20.795,21.001Z" style="fill:rgb(48,168,81);fill-rule:nonzero;"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 9.9 KiB

+183
View File
@@ -0,0 +1,183 @@
/*
Copyright 2025 New Vector 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 url("@vector-im/compound-design-tokens/assets/web/css/compound-design-tokens.css");
html {
min-height: 100%;
position: relative;
}
body {
background: var(--cpd-color-bg-canvas-default);
max-width: 680px;
margin: var(--cpd-space-0x) auto;
padding-bottom: 178px; /* Match the height of mx_BottomGradient */
font-family: var(--cpd-font-family-sans);
font-size: var(--cpd-font-size-body-lg); /* Design says 16px, this is 17px */
color: var(--cpd-color-text-primary);
}
hr {
border: none;
height: var(--cpd-border-width-1);
background-color: var(
--cpd-color-bg-subtle-primary /* Design uses Border token from "Compound Marketing" set, but this matches. */
);
color: var(
--cpd-color-bg-subtle-primary /* Design uses Border token from "Compound Marketing" set, but this matches. */
);
margin: 0;
}
p {
margin: var(--cpd-space-1x) var(--cpd-space-0x);
padding: var(--cpd-space-0x);
}
.mx_Button {
border: 0;
border-radius: 100px;
min-width: 80px;
background-color: var(--cpd-color-bg-action-primary-rest);
color: var(--cpd-color-text-on-solid-primary);
cursor: pointer;
padding: 12px 22px;
word-break: break-word;
text-decoration: none;
}
#deep_link_button {
margin-top: 12px;
display: inline-block;
width: auto;
box-sizing: border-box;
}
.mx_StoreLinks {
margin: 15px 0 12px 0;
}
.mx_StoreBadge {
text-decoration: none !important;
margin: 16px 16px 16px 0px;
}
#f_droid_link {
color: var(--cpd-color-text-action-accent);
font-weight: bold;
text-decoration: none;
}
#f_droid_link:visited {
color: var(--cpd-color-text-action-accent);
}
.mx_HomePage_header {
color: var(--cpd-color-text-secondary);
align-items: center;
justify-content: center;
text-align: center;
padding-top: 48px;
padding-bottom: 48px;
}
.mx_HomePage_header #header_title {
margin-top: 8px;
margin-bottom: 0px;
}
.mx_HomePage h3 {
margin-top: 30px;
}
.mx_HomePage_col {
display: flex;
flex-direction: row;
}
.mx_HomePage_row {
flex: 1 1 0;
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: flex-start;
}
.mx_HomePage_container {
margin: 10px 20px;
}
.mx_HomePage_errorContainer {
display: none; /* shown in JS if needed */
margin: 20px;
border: var(--cpd-border-width-1) solid var(--cpd-color-border-critical-primary);
background-color: var(--cpd-color-bg-critical-subtle);
padding: 5px;
}
.mx_HomePage_container h1,
.mx_HomePage_container h2,
.mx_HomePage_container h3,
.mx_HomePage_container h4 {
font-weight: var(--cpd-font-weight-semibold);
font-size: var(--cpd-font-size-body-lg); /* Design says 16px, this is 17px */
margin-bottom: 8px;
margin-top: 4px;
}
.mx_Spacer {
margin-top: 48px;
}
.mx_DesktopLink {
color: var(--cpd-color-text-action-accent);
font-weight: var(--cpd-font-weight-semibold);
text-decoration: none;
}
/*
* The bottom gradient is a full-width background image that stretches horizontally across the page.
* It is positioned pinned to the bottom of the viewport unless the content is taller than the viewport,
* in which case it will be pinned to the bottom of the content.
*/
.mx_BottomGradient {
position: absolute;
bottom: 0;
left: 0;
right: 0;
width: 100vw;
height: 178px; /* Match the height of assets/bottom-gradient.svg so the gradient only stretches horizontally */
background-image: url("./assets/bottom-gradient.svg");
background-size: 100% 100%;
background-repeat: no-repeat;
z-index: -1;
margin-left: calc(50% - 50vw); /* Center the gradient regardless of body width */
}
.mx_HomePage_step_number {
display: flex;
align-items: flex-start;
margin-right: 8px;
}
.mx_HomePage_step_number span {
display: flex;
align-items: center;
justify-content: center;
width: var(--cpd-space-6x);
height: var(--cpd-space-6x);
border-radius: 50%;
border: var(--cpd-border-width-1) solid var(--cpd-color-bg-subtle-primary); /* Not a border token, but matches the Design (Border token from the "Compound Marketing" set). */
background-color: transparent;
color: var(--cpd-color-text-secondary);
font-size: var(--cpd-font-size-body-md); /* Design says 14px, this is 15px */
}
#step2_description {
color: var(--cpd-color-text-secondary);
}
+105
View File
@@ -0,0 +1,105 @@
<!doctype html>
<!--
Please note: This page is similar to the one at mobile.element.io so any changes made here can potentially
be applied to that site as well. They still share the mobile-apps.ts file for updating the app metadata.
-->
<html lang="en">
<head>
<title>Element Mobile Guide</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="apple-itunes-app" content="app-id=id1631335820" />
</head>
<body>
<div class="mx_HomePage_errorContainer">
<!-- populated by JS if needed -->
</div>
<div class="mx_HomePage_container">
<header>
<div class="mx_HomePage_header">
<div>
<img src="./assets/element-logo.svg" alt="Element Logo" width="148px" height="32px" />
</div>
<div>
<p id="header_title">Join Element.</p>
</div>
</div>
<hr />
</header>
<main>
<div class="mx_HomePage_col mx_Spacer">
<p>
The <a id="back_to_element_button" href="#" class="mx_DesktopLink">desktop site</a> does
<b>not</b> work on mobile, please download the app.
</p>
</div>
<div class="mx_HomePage_col mx_Spacer">
<div class="mx_HomePage_row">
<div class="mx_HomePage_step_number">
<span>1</span>
</div>
<div>
<h1 id="step1_heading">Download Element</h1>
<div class="mx_StoreLinks">
<a
href="https://apps.apple.com/app/element-x-secure-chat-call/id1631335820"
target="_blank"
class="mx_StoreBadge"
id="app_store_link"
>
<img
src="./assets/app-store-badge.svg"
alt="Download on the App Store."
width="120px"
height="40px"
/>
</a>
<a
href="https://play.google.com/store/apps/details?id=io.element.android.x"
target="_blank"
class="mx_StoreBadge"
id="play_store_link"
>
<img
src="./assets/google-play-badge.svg"
alt="Get it on Google Play."
width="135px"
height="40px"
/>
</a>
</div>
<p id="f_droid_section">
Also available on
<a
href="https://f-droid.org/packages/io.element.android.x"
target="_blank"
class="mx_ClearDecoration"
id="f_droid_link"
>F-Droid</a
>
</p>
</div>
</div>
</div>
<div id="step2_container" class="mx_HomePage_col mx_Spacer" style="display: none">
<div class="mx_HomePage_row">
<div class="mx_HomePage_step_number">
<span>2</span>
</div>
<div>
<h1>Come back here to sign in</h1>
<p id="step2_description">This will set up your app</p>
<a class="mx_Button" id="deep_link_button" href="#">Sign in</a>
</div>
</div>
</div>
</main>
</div>
<footer>
<div class="mx_BottomGradient"></div>
</footer>
</body>
</html>
+137
View File
@@ -0,0 +1,137 @@
/*
Copyright 2024 New Vector 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 "./index.css";
import "@fontsource/inter/400.css";
import "@fontsource/inter/600.css";
import { logger } from "matrix-js-sdk/src/logger";
import { getVectorConfig } from "../getconfig";
import { MobileAppVariant, mobileApps, updateMobilePage } from "./mobile-apps.ts";
function onBackToElementClick(): void {
// Cookie should expire in 4 hours
document.cookie = "element_mobile_redirect_to_guide=false;path=/;max-age=14400";
window.location.href = "../";
}
// NEVER pass user-controlled content to this function! Hardcoded strings only please.
function renderConfigError(message: string): void {
const contactMsg =
"If this is unexpected, please contact your system administrator " + "or technical support representative.";
message = `<h2>Error loading Element</h2><p>${message}</p><p>${contactMsg}</p>`;
const toHide = document.getElementsByClassName("mx_HomePage_container");
const errorContainers = document.getElementsByClassName(
"mx_HomePage_errorContainer",
) as HTMLCollectionOf<HTMLDivElement>;
for (const e of toHide) {
// We have to clear the content because .style.display='none'; doesn't work
// due to an !important in the CSS.
e.innerHTML = "";
}
for (const e of errorContainers) {
e.style.display = "block";
e.innerHTML = message;
}
}
async function initPage(): Promise<void> {
const config = await getVectorConfig("..");
// We manually parse the config similar to how validateServerConfig works because
// calling that function pulls in roughly 4mb of JS we don't use.
const wkConfig = config?.["default_server_config"]; // overwritten later under some conditions
let serverName = config?.["default_server_name"];
const defaultHsUrl = config?.["default_hs_url"];
const defaultIsUrl = config?.["default_is_url"];
const appVariant = (config?.["mobile_guide_app_variant"] as MobileAppVariant) ?? MobileAppVariant.X;
const metadata = mobileApps[appVariant] ?? mobileApps[MobileAppVariant.X]; // Additional fallback in case mobile_guide_app_variant has an unexpected value.
const incompatibleOptions = [wkConfig, serverName, defaultHsUrl].filter((i) => !!i);
if (defaultHsUrl && (wkConfig || serverName)) {
return renderConfigError(
"Invalid configuration: a default_hs_url can't be specified along with default_server_name " +
"or default_server_config",
);
}
if (incompatibleOptions.length < 1) {
return renderConfigError("Invalid configuration: no default server specified.");
}
let hsUrl: string | undefined;
let isUrl: string | undefined;
if (!serverName && typeof wkConfig?.["m.homeserver"]?.["base_url"] === "string") {
hsUrl = wkConfig["m.homeserver"]["base_url"];
serverName = wkConfig["m.homeserver"]["server_name"];
if (typeof wkConfig["m.identity_server"]?.["base_url"] === "string") {
isUrl = wkConfig["m.identity_server"]["base_url"];
}
}
if (serverName) {
// We also do our own minimal .well-known validation to avoid pulling in the js-sdk
try {
const result = await fetch(`https://${serverName}/.well-known/matrix/client`);
const wkConfig = await result.json();
if (wkConfig?.["m.homeserver"]) {
hsUrl = wkConfig["m.homeserver"]["base_url"];
if (wkConfig["m.identity_server"]) {
isUrl = wkConfig["m.identity_server"]["base_url"];
}
}
} catch (e) {
if (wkConfig?.["m.homeserver"]) {
hsUrl = wkConfig["m.homeserver"]["base_url"] || undefined;
if (wkConfig["m.identity_server"]) {
isUrl = wkConfig["m.identity_server"]["base_url"] || undefined;
}
} else {
logger.error(e);
return renderConfigError("Unable to fetch homeserver configuration");
}
}
}
if (defaultHsUrl) {
hsUrl = defaultHsUrl;
isUrl = defaultIsUrl;
}
if (!hsUrl) {
return renderConfigError("Unable to locate homeserver");
}
if (hsUrl && !hsUrl.endsWith("/")) hsUrl += "/";
if (isUrl && !isUrl.endsWith("/")) isUrl += "/";
let deepLinkUrl = `https://mobile.element.io${metadata.deepLinkPath}`;
if (metadata.usesLegacyDeepLink) {
deepLinkUrl += `?hs_url=${encodeURIComponent(hsUrl)}`;
if (isUrl) {
deepLinkUrl += `&is_url=${encodeURIComponent(isUrl)}`;
}
} else if (serverName) {
deepLinkUrl += `?account_provider=${serverName}`;
}
// Not part of updateMobilePage as the link is only shown on mobile_guide and not on mobile.element.io
document.getElementById("back_to_element_button")!.onclick = onBackToElementClick;
updateMobilePage(metadata, deepLinkUrl, serverName ?? hsUrl);
}
void initPage();
@@ -0,0 +1,88 @@
/*
Copyright 2025 New Vector 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.
*/
/*
* Shared code that is used by the mobile guide and the mobile.element.io site.
*/
export enum MobileAppVariant {
Classic = "element-classic",
X = "element",
Pro = "element-pro",
}
export interface MobileAppMetadata {
name: string;
appleAppId: string;
appStoreUrl: string;
playStoreUrl: string;
fDroidUrl?: string;
deepLinkPath: string;
usesLegacyDeepLink: boolean;
isProApp: boolean;
}
export const mobileApps: Record<MobileAppVariant, MobileAppMetadata> = {
[MobileAppVariant.Classic]: {
name: "Element",
appleAppId: "id1083446067",
appStoreUrl: "https://apps.apple.com/app/element-messenger/id1083446067",
playStoreUrl: "https://play.google.com/store/apps/details?id=im.vector.app",
fDroidUrl: "https://f-droid.org/packages/im.vector.app",
deepLinkPath: "",
usesLegacyDeepLink: true,
isProApp: false,
},
[MobileAppVariant.X]: {
name: "Element X",
appleAppId: "id1631335820",
appStoreUrl: "https://apps.apple.com/app/element-x-secure-chat-call/id1631335820",
playStoreUrl: "https://play.google.com/store/apps/details?id=io.element.android.x",
fDroidUrl: "https://f-droid.org/packages/io.element.android.x",
deepLinkPath: "/element",
usesLegacyDeepLink: false,
isProApp: false,
},
[MobileAppVariant.Pro]: {
name: "Element Pro",
appleAppId: "id6502951615",
appStoreUrl: "https://apps.apple.com/app/element-pro-for-work/id6502951615",
playStoreUrl: "https://play.google.com/store/apps/details?id=io.element.enterprise",
deepLinkPath: "/element-pro",
usesLegacyDeepLink: false,
isProApp: true,
},
};
export function updateMobilePage(metadata: MobileAppMetadata, deepLinkUrl: string, server: string | undefined): void {
const appleMeta = document.querySelector('meta[name="apple-itunes-app"]') as Element;
appleMeta.setAttribute("content", `app-id=${metadata.appleAppId}`);
if (server) {
(document.getElementById("header_title") as HTMLHeadingElement).innerText = `Join ${server} on Element`;
}
(document.getElementById("app_store_link") as HTMLAnchorElement).href = metadata.appStoreUrl;
(document.getElementById("play_store_link") as HTMLAnchorElement).href = metadata.playStoreUrl;
if (metadata.fDroidUrl) {
(document.getElementById("f_droid_link") as HTMLAnchorElement).href = metadata.fDroidUrl;
} else {
document.getElementById("f_droid_section")!.style.display = "none";
}
const step1Heading = document.getElementById("step1_heading")!;
step1Heading.innerHTML = step1Heading!.innerHTML.replace("Element", metadata.name);
// Step 2 is only shown on the mobile guide, not on mobile.element.io
if (document.getElementById("step2_container")) {
document.getElementById("step2_container")!.style.display = "block";
if (metadata.isProApp) {
document.getElementById("step2_description")!.innerHTML = "Use your work email to join";
}
(document.getElementById("deep_link_button") as HTMLAnchorElement).href = deepLinkUrl;
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,571 @@
/*
Copyright 2024-2025 New Vector Ltd.
Copyright 2022 Šimon Brandner <simon.bra.ag@gmail.com>
Copyright 2018-2021 New Vector Ltd
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2016 Aviral Dasgupta
Copyright 2016 OpenMarket 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 {
type MatrixClient,
type Room,
type MatrixEvent,
type OidcRegistrationClientMetadata,
} from "matrix-js-sdk/src/matrix";
import React from "react";
import { logger } from "matrix-js-sdk/src/logger";
import { uniqueId } from "lodash";
import BasePlatform, { UpdateCheckStatus, type UpdateStatus } from "../../BasePlatform";
import type BaseEventIndexManager from "../../indexing/BaseEventIndexManager";
import dis from "../../dispatcher/dispatcher";
import SdkConfig from "../../SdkConfig";
import { type IConfigOptions } from "../../IConfigOptions";
import * as rageshake from "../../rageshake/rageshake";
import Modal from "../../Modal";
import InfoDialog from "../../components/views/dialogs/InfoDialog";
import Spinner from "../../components/views/elements/Spinner";
import { Action } from "../../dispatcher/actions";
import { type ActionPayload } from "../../dispatcher/payloads";
import { showToast as showUpdateToast } from "../../toasts/UpdateToast";
import { type CheckUpdatesPayload } from "../../dispatcher/payloads/CheckUpdatesPayload";
import ToastStore from "../../stores/ToastStore";
import GenericExpiringToast from "../../components/views/toasts/GenericExpiringToast";
import { BreadcrumbsStore } from "../../stores/BreadcrumbsStore";
import { UPDATE_EVENT } from "../../stores/AsyncStore";
import { avatarUrlForRoom, getInitialLetter } from "../../Avatar";
import DesktopCapturerSourcePicker from "../../components/views/elements/DesktopCapturerSourcePicker";
import { MatrixClientPeg } from "../../MatrixClientPeg";
import { SeshatIndexManager } from "./SeshatIndexManager";
import { IPCManager } from "./IPCManager";
import { _t } from "../../languageHandler";
import { BadgeOverlayRenderer } from "../../favicon";
import GenericToast from "../../components/views/toasts/GenericToast.tsx";
interface SquirrelUpdate {
releaseNotes: string;
releaseName: string;
releaseDate: Date;
updateURL: string;
}
const SSO_ID_KEY = "element-desktop-ssoid";
function platformFriendlyName(): string {
// used to use window.process but the same info is available here
if (navigator.userAgent.includes("Macintosh")) {
return "macOS";
} else if (navigator.userAgent.includes("FreeBSD")) {
return "FreeBSD";
} else if (navigator.userAgent.includes("OpenBSD")) {
return "OpenBSD";
} else if (navigator.userAgent.includes("SunOS")) {
return "SunOS";
} else if (navigator.userAgent.includes("Windows")) {
return "Windows";
} else if (navigator.userAgent.includes("Linux")) {
return "Linux";
} else {
return "Unknown";
}
}
function getUpdateCheckStatus(status: boolean | string): UpdateStatus {
if (status === true) {
return { status: UpdateCheckStatus.Downloading };
} else if (status === false) {
return { status: UpdateCheckStatus.NotAvailable };
} else {
return {
status: UpdateCheckStatus.Error,
detail: status,
};
}
}
export default class ElectronPlatform extends BasePlatform {
private readonly ipc = new IPCManager("ipcCall", "ipcReply");
private readonly eventIndexManager: BaseEventIndexManager = new SeshatIndexManager();
public readonly initialised: Promise<void>;
private readonly electron: Electron;
private protocol!: string;
private sessionId!: string;
private badgeOverlayRenderer?: BadgeOverlayRenderer;
private config!: IConfigOptions;
private supportedSettings?: Record<string, boolean>;
private clientStartedPromiseWithResolvers = Promise.withResolvers<void>();
public constructor() {
super();
if (!window.electron) {
throw new Error("Cannot instantiate ElectronPlatform, window.electron is not set");
}
this.electron = window.electron;
/*
IPC Call `check_updates` returns:
true if there is an update available
false if there is not
or the error if one is encountered
*/
this.electron.on("check_updates", (event, status) => {
dis.dispatch<CheckUpdatesPayload>({
action: Action.CheckUpdates,
...getUpdateCheckStatus(status),
});
});
// `userAccessToken` (IPC) is requested by the main process when appending authentication
// to media downloads. A reply is sent over the same channel.
this.electron.on("userAccessToken", () => {
this.electron.send("userAccessToken", MatrixClientPeg.get()?.getAccessToken());
});
// `homeserverUrl` (IPC) is requested by the main process. A reply is sent over the same channel.
this.electron.on("homeserverUrl", () => {
this.electron.send("homeserverUrl", MatrixClientPeg.get()?.getHomeserverUrl());
});
// `serverSupportedVersions` is requested by the main process when it needs to know if the
// server supports a particular version. This is primarily used to detect authenticated media
// support. A reply is sent over the same channel.
this.electron.on("serverSupportedVersions", async () => {
this.electron.send("serverSupportedVersions", await MatrixClientPeg.get()?.getVersions());
});
// try to flush the rageshake logs to indexeddb before quit.
this.electron.on("before-quit", function () {
logger.log("element-desktop closing");
rageshake.flush();
});
this.electron.on("update-downloaded", this.onUpdateDownloaded);
this.electron.on("preferences", () => {
dis.fire(Action.ViewUserSettings);
});
this.electron.on("userDownloadCompleted", (ev, { id, name }) => {
const key = `DOWNLOAD_TOAST_${id}`;
const onAccept = (): void => {
this.electron.send("userDownloadAction", { id, open: true });
ToastStore.sharedInstance().dismissToast(key);
};
const onDismiss = (): void => {
this.electron.send("userDownloadAction", { id });
};
ToastStore.sharedInstance().addOrReplaceToast({
key,
title: _t("download_completed"),
props: {
description: name,
primaryLabel: _t("action|open"),
onPrimaryClick: onAccept,
dismissLabel: _t("action|dismiss"),
onDismiss,
numSeconds: 10,
},
component: GenericExpiringToast,
priority: 99,
});
});
this.electron.on("openDesktopCapturerSourcePicker", async () => {
const { finished } = Modal.createDialog(DesktopCapturerSourcePicker);
const [source] = await finished;
// getDisplayMedia promise does not return if no dummy is passed here as source
await this.ipc.call("callDisplayMediaCallback", source ?? { id: "", name: "", thumbnailURL: "" });
});
this.electron.on("showToast", async (ev, { title, description, priority = 40 }) => {
await this.clientStartedPromiseWithResolvers.promise;
const key = uniqueId("electron_showToast_");
const onPrimaryClick = (): void => {
ToastStore.sharedInstance().dismissToast(key);
};
ToastStore.sharedInstance().addOrReplaceToast({
key,
title,
props: {
description,
primaryLabel: _t("action|dismiss"),
onPrimaryClick,
},
component: GenericToast,
priority,
});
});
BreadcrumbsStore.instance.on(UPDATE_EVENT, this.onBreadcrumbsUpdate);
this.initialised = this.initialise();
}
protected onAction(payload: ActionPayload): void {
super.onAction(payload);
// Whitelist payload actions, no point sending most across
if (["call_state"].includes(payload.action)) {
this.electron.send("app_onAction", payload);
}
if (payload.action === Action.ClientStarted) {
this.clientStartedPromiseWithResolvers.resolve();
}
}
private async initialise(): Promise<void> {
const { protocol, sessionId, config, supportedSettings, supportsBadgeOverlay } =
await this.electron.initialise();
this.protocol = protocol;
this.sessionId = sessionId;
this.config = config;
this.supportedSettings = supportedSettings;
if (supportsBadgeOverlay) {
this.badgeOverlayRenderer = new BadgeOverlayRenderer();
}
}
public async getConfig(): Promise<IConfigOptions | undefined> {
await this.initialised;
return this.config;
}
private onBreadcrumbsUpdate = (): void => {
const rooms = BreadcrumbsStore.instance.rooms.slice(0, 7).map((r) => ({
roomId: r.roomId,
avatarUrl: avatarUrlForRoom(
r,
Math.floor(60 * window.devicePixelRatio),
Math.floor(60 * window.devicePixelRatio),
"crop",
),
initial: getInitialLetter(r.name),
}));
void this.ipc.call("breadcrumbs", rooms);
};
private onUpdateDownloaded = async (ev: Event, { releaseNotes, releaseName }: SquirrelUpdate): Promise<void> => {
dis.dispatch<CheckUpdatesPayload>({
action: Action.CheckUpdates,
status: UpdateCheckStatus.Ready,
});
if (this.shouldShowUpdate(releaseName)) {
showUpdateToast(await this.getAppVersion(), releaseName, releaseNotes);
}
};
public getHumanReadableName(): string {
return "Electron Platform"; // no translation required: only used for analytics
}
/**
* Return true if platform supports multi-language
* spell-checking, otherwise false.
*/
public supportsSpellCheckSettings(): boolean {
return true;
}
public allowOverridingNativeContextMenus(): boolean {
return true;
}
public setNotificationCount(count: number): void {
if (this.notificationCount === count) return;
super.setNotificationCount(count);
if (this.badgeOverlayRenderer) {
this.badgeOverlayRenderer
.render(count)
.then((buffer) => {
this.electron.send("setBadgeCount", count, buffer);
})
.catch((ex) => {
logger.warn("Unable to generate badge overlay", ex);
});
} else {
this.electron.send("setBadgeCount", count);
}
}
public setErrorStatus(errorDidOccur: boolean): void {
if (!this.badgeOverlayRenderer) {
super.setErrorStatus(errorDidOccur);
return;
}
// Check before calling super so we don't override the previous state.
if (this.errorDidOccur !== errorDidOccur) {
super.setErrorStatus(errorDidOccur);
let promise: Promise<ArrayBuffer | null>;
if (errorDidOccur) {
promise = this.badgeOverlayRenderer.render(this.notificationCount || "×", "#f00");
} else {
promise = this.badgeOverlayRenderer.render(this.notificationCount);
}
promise
.then((buffer) => {
this.electron.send("setBadgeCount", this.notificationCount, buffer, errorDidOccur);
})
.catch((ex) => {
logger.warn("Unable to generate badge overlay", ex);
});
}
}
public supportsNotifications(): boolean {
return true;
}
public maySendNotifications(): boolean {
return true;
}
public displayNotification(
title: string,
msg: string,
avatarUrl: string,
room: Room,
ev?: MatrixEvent,
): Notification {
// GNOME notification spec parses HTML tags for styling...
// Electron Docs state all supported linux notification systems follow this markup spec
// https://github.com/electron/electron/blob/master/docs/tutorial/desktop-environment-integration.md#linux
// maybe we should pass basic styling (italics, bold, underline) through from MD
// we only have to strip out < and > as the spec doesn't include anything about things like &amp;
// so we shouldn't assume that all implementations will treat those properly. Very basic tag parsing is done.
if (navigator.userAgent.includes("Linux")) {
msg = msg.replace(/</g, "&lt;").replace(/>/g, "&gt;");
}
const notification = super.displayNotification(title, msg, avatarUrl, room, ev);
const handler = notification.onclick as () => void;
notification.onclick = (): void => {
handler?.();
void this.ipc.call("focusWindow");
};
return notification;
}
public loudNotification(ev: MatrixEvent, room: Room): void {
this.electron.send("loudNotification");
}
public needsUrlTooltips(): boolean {
return true;
}
public async getAppVersion(): Promise<string> {
return this.ipc.call("getAppVersion");
}
public supportsSetting(settingName?: string): boolean {
if (settingName === undefined) return true;
return this.supportedSettings?.[settingName] === true;
}
public async getSettingValue(settingName: string): Promise<any> {
await this.initialised;
return this.electron.getSettingValue(settingName);
}
public async setSettingValue(settingName: string, value: any): Promise<void> {
await this.initialised;
return this.electron.setSettingValue(settingName, value);
}
public async canSelfUpdate(): Promise<boolean> {
const feedUrl = await this.ipc.call("getUpdateFeedUrl");
return Boolean(feedUrl);
}
public startUpdateCheck(): void {
super.startUpdateCheck();
this.electron.send("check_updates");
}
public installUpdate(): void {
// IPC to the main process to install the update, since quitAndInstall
// doesn't fire the before-quit event so the main process needs to know
// it should exit.
this.electron.send("install_update");
}
public getDefaultDeviceDisplayName(): string {
const brand = SdkConfig.get().brand;
return _t("desktop_default_device_name", {
brand,
platformName: platformFriendlyName(),
});
}
public requestNotificationPermission(): Promise<string> {
return Promise.resolve("granted");
}
public reload(): void {
window.location.reload();
}
public getEventIndexingManager(): BaseEventIndexManager | null {
return this.eventIndexManager;
}
public async setLanguage(preferredLangs: string[]): Promise<any> {
return this.ipc.call("setLanguage", preferredLangs);
}
public setSpellCheckEnabled(enabled: boolean): void {
this.ipc.call("setSpellCheckEnabled", enabled).catch((error) => {
logger.log("Failed to send setSpellCheckEnabled IPC to Electron");
logger.error(error);
});
}
public async getSpellCheckEnabled(): Promise<boolean> {
return this.ipc.call("getSpellCheckEnabled");
}
public setSpellCheckLanguages(preferredLangs: string[]): void {
this.ipc.call("setSpellCheckLanguages", preferredLangs).catch((error) => {
logger.log("Failed to send setSpellCheckLanguages IPC to Electron");
logger.error(error);
});
}
public async getSpellCheckLanguages(): Promise<string[]> {
return this.ipc.call("getSpellCheckLanguages");
}
public async getDesktopCapturerSources(options: GetSourcesOptions): Promise<Array<DesktopCapturerSource>> {
return this.ipc.call("getDesktopCapturerSources", options);
}
public supportsDesktopCapturer(): boolean {
return true;
}
public supportsJitsiScreensharing(): boolean {
// See https://github.com/element-hq/element-web/issues/4880
return false;
}
public async getAvailableSpellCheckLanguages(): Promise<string[]> {
return this.ipc.call("getAvailableSpellCheckLanguages");
}
public getSSOCallbackUrl(fragmentAfterLogin?: string): URL {
const url = super.getSSOCallbackUrl(fragmentAfterLogin);
url.protocol = "element";
url.searchParams.set(SSO_ID_KEY, this.sessionId);
return url;
}
public startSingleSignOn(
mxClient: MatrixClient,
loginType: "sso" | "cas",
fragmentAfterLogin: string,
idpId?: string,
): void {
// this will get intercepted by electron-main will-navigate
super.startSingleSignOn(mxClient, loginType, fragmentAfterLogin, idpId);
Modal.createDialog(InfoDialog, {
title: _t("auth|sso_complete_in_browser_dialog_title"),
description: <Spinner />,
});
}
public navigateForwardBack(back: boolean): void {
void this.ipc.call(back ? "navigateBack" : "navigateForward");
}
public overrideBrowserShortcuts(): boolean {
return true;
}
public async getPickleKey(userId: string, deviceId: string): Promise<string | null> {
try {
return await this.ipc.call("getPickleKey", userId, deviceId);
} catch {
// if we can't connect to the password storage, assume there's no
// pickle key
return null;
}
}
public async createPickleKey(userId: string, deviceId: string): Promise<string | null> {
try {
return await this.ipc.call("createPickleKey", userId, deviceId);
} catch {
// if we can't connect to the password storage, assume there's no
// pickle key
return null;
}
}
public async destroyPickleKey(userId: string, deviceId: string): Promise<void> {
try {
await this.ipc.call("destroyPickleKey", userId, deviceId);
} catch {}
}
public async clearStorage(): Promise<void> {
try {
await super.clearStorage();
await this.ipc.call("clearStorage");
} catch {}
}
public get baseUrl(): string {
// This configuration is element-desktop specific so the types here do not know about it
return (SdkConfig.get() as unknown as Record<string, string>)["web_base_url"] ?? "https://app.element.io";
}
public get defaultOidcClientUri(): string {
// Default to element.io as our scheme `io.element.desktop` is within its scope on default MAS policies
return "https://element.io";
}
public async getOidcClientMetadata(): Promise<OidcRegistrationClientMetadata> {
const baseMetadata = await super.getOidcClientMetadata();
return {
...baseMetadata,
applicationType: "native",
};
}
public getOidcClientState(): string {
return `:${SSO_ID_KEY}:${this.sessionId}`;
}
/**
* The URL to return to after a successful OIDC authentication
*/
public getOidcCallbackUrl(): URL {
const url = super.getOidcCallbackUrl();
url.protocol = this.protocol;
// Trim the double slash into a single slash to comply with https://datatracker.ietf.org/doc/html/rfc8252#section-7.1
if (url.href.startsWith(`${url.protocol}//`)) {
url.href = url.href.replace("://", ":/");
}
return url;
}
public checkSessionLockFree(): boolean {
return true;
}
public async getSessionLock(_onNewInstance: () => Promise<void>): Promise<boolean> {
return true;
}
}
@@ -0,0 +1,66 @@
/*
Copyright 2022-2024 New Vector 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 { logger } from "matrix-js-sdk/src/logger";
import { type ElectronChannel } from "../../@types/global";
interface IPCPayload {
id?: number;
error?: string | { message: string };
reply?: any;
}
export class IPCManager {
private pendingIpcCalls: { [ipcCallId: number]: PromiseWithResolvers<any> } = {};
private nextIpcCallId = 0;
public constructor(
private readonly sendChannel: ElectronChannel = "ipcCall",
private readonly recvChannel: ElectronChannel = "ipcReply",
) {
if (!window.electron) {
throw new Error("Cannot instantiate ElectronPlatform, window.electron is not set");
}
window.electron.on(this.recvChannel, this.onIpcReply);
}
public async call(name: string, ...args: any[]): Promise<any> {
// TODO this should be moved into the preload.js file.
const ipcCallId = ++this.nextIpcCallId;
const deferred = Promise.withResolvers<any>();
this.pendingIpcCalls[ipcCallId] = deferred;
// Maybe add a timeout to these? Probably not necessary.
window.electron!.send(this.sendChannel, { id: ipcCallId, name, args });
return deferred.promise;
}
private onIpcReply = (_ev: Event, payload: IPCPayload): void => {
if (payload.id === undefined) {
logger.warn("Ignoring IPC reply with no ID");
return;
}
if (this.pendingIpcCalls[payload.id] === undefined) {
logger.warn("Unknown IPC payload ID: " + payload.id);
return;
}
const callbacks = this.pendingIpcCalls[payload.id];
delete this.pendingIpcCalls[payload.id];
if (payload.error) {
// `seshat.ts` sends a JavaScript object with a `message` property. Turn it into a proper Error.
let error = payload.error;
if (typeof error === "object" && error.message) {
error = new Error(error.message);
}
callbacks.reject(error);
} else {
callbacks.resolve(payload.reply);
}
};
}
@@ -0,0 +1,22 @@
/*
Copyright 2020-2024 New Vector 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 { logger } from "matrix-js-sdk/src/logger";
import WebPlatform from "./WebPlatform";
export default class PWAPlatform extends WebPlatform {
public setNotificationCount(count: number): void {
if (!navigator.setAppBadge) return super.setNotificationCount(count);
if (this.notificationCount === count) return;
this.notificationCount = count;
navigator.setAppBadge(count).catch((e) => {
logger.error("Failed to update PWA app badge", e);
});
}
}
@@ -0,0 +1,102 @@
/*
Copyright 2022-2024 New Vector 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.
*/
// eslint-disable-next-line no-restricted-imports
import {
type IMatrixProfile,
type IEventWithRoomId as IMatrixEvent,
type IResultRoomEvents,
} from "matrix-js-sdk/src/@types/search";
import BaseEventIndexManager, {
type ICrawlerCheckpoint,
type IEventAndProfile,
type IIndexStats,
type ISearchArgs,
type ILoadArgs,
} from "../../indexing/BaseEventIndexManager";
import { IPCManager } from "./IPCManager";
export class SeshatIndexManager extends BaseEventIndexManager {
private readonly ipc = new IPCManager("seshat", "seshatReply");
public async supportsEventIndexing(): Promise<boolean> {
return this.ipc.call("supportsEventIndexing");
}
public async initEventIndex(userId: string, deviceId: string): Promise<void> {
return this.ipc.call("initEventIndex", userId, deviceId);
}
public async addEventToIndex(ev: IMatrixEvent, profile: IMatrixProfile): Promise<void> {
return this.ipc.call("addEventToIndex", ev, profile);
}
public async deleteEvent(eventId: string): Promise<boolean> {
return this.ipc.call("deleteEvent", eventId);
}
public async isEventIndexEmpty(): Promise<boolean> {
return this.ipc.call("isEventIndexEmpty");
}
public async isRoomIndexed(roomId: string): Promise<boolean> {
return this.ipc.call("isRoomIndexed", roomId);
}
public async commitLiveEvents(): Promise<void> {
return this.ipc.call("commitLiveEvents");
}
public async searchEventIndex(searchConfig: ISearchArgs): Promise<IResultRoomEvents> {
return this.ipc.call("searchEventIndex", searchConfig);
}
public async addHistoricEvents(
events: IEventAndProfile[],
checkpoint: ICrawlerCheckpoint | null,
oldCheckpoint: ICrawlerCheckpoint | null,
): Promise<boolean> {
return this.ipc.call("addHistoricEvents", events, checkpoint, oldCheckpoint);
}
public async addCrawlerCheckpoint(checkpoint: ICrawlerCheckpoint): Promise<void> {
return this.ipc.call("addCrawlerCheckpoint", checkpoint);
}
public async removeCrawlerCheckpoint(checkpoint: ICrawlerCheckpoint): Promise<void> {
return this.ipc.call("removeCrawlerCheckpoint", checkpoint);
}
public async loadFileEvents(args: ILoadArgs): Promise<IEventAndProfile[]> {
return this.ipc.call("loadFileEvents", args);
}
public async loadCheckpoints(): Promise<ICrawlerCheckpoint[]> {
return this.ipc.call("loadCheckpoints");
}
public async closeEventIndex(): Promise<void> {
return this.ipc.call("closeEventIndex");
}
public async getStats(): Promise<IIndexStats> {
return this.ipc.call("getStats");
}
public async getUserVersion(): Promise<number> {
return this.ipc.call("getUserVersion");
}
public async setUserVersion(version: number): Promise<void> {
return this.ipc.call("setUserVersion", version);
}
public async deleteEventIndex(): Promise<void> {
return this.ipc.call("deleteEventIndex");
}
}
+280
View File
@@ -0,0 +1,280 @@
/*
Copyright 2017-2024 New Vector Ltd.
Copyright 2016 Aviral Dasgupta
Copyright 2016 OpenMarket 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 UAParser from "ua-parser-js";
import { logger } from "matrix-js-sdk/src/logger";
import { MatrixClientPeg } from "../../MatrixClientPeg";
import BasePlatform, { UpdateCheckStatus, type UpdateStatus } from "../../BasePlatform";
import dis from "../../dispatcher/dispatcher";
import { hideToast as hideUpdateToast, showToast as showUpdateToast } from "../../toasts/UpdateToast";
import { Action } from "../../dispatcher/actions";
import { type CheckUpdatesPayload } from "../../dispatcher/payloads/CheckUpdatesPayload";
import { parseQs } from "../url_utils";
import { _t } from "../../languageHandler";
import ToastStore from "../../stores/ToastStore.ts";
import GenericToast from "../../components/views/toasts/GenericToast.tsx";
import SdkConfig from "../../SdkConfig.ts";
import type { ActionPayload } from "../../dispatcher/payloads.ts";
import * as SessionLock from "../../utils/SessionLock.ts";
const POKE_RATE_MS = 10 * 60 * 1000; // 10 min
function getNormalizedAppVersion(version: string): string {
// if version looks like semver with leading v, strip it (matches scripts/normalize-version.sh)
const semVerRegex = /^v\d+.\d+.\d+(-.+)?$/;
if (semVerRegex.test(version)) {
return version.substring(1);
}
return version;
}
export default class WebPlatform extends BasePlatform {
private static readonly VERSION = process.env.VERSION!; // baked in by Webpack
private readonly registerServiceWorkerPromise: Promise<void>;
public constructor() {
super();
// Register the service worker in the background
this.registerServiceWorkerPromise = this.registerServiceWorker();
this.registerServiceWorkerPromise.catch((e) => {
console.error("Error registering/updating service worker:", e);
});
}
protected onAction(payload: ActionPayload): void {
super.onAction(payload);
switch (payload.action) {
case Action.ClientStarted:
// Defer drawing the toast until the client is started as the lifecycle methods reset the ToastStore right before
this.registerServiceWorkerPromise.catch(this.handleServiceWorkerRegistrationError);
break;
}
}
private async registerServiceWorker(): Promise<void> {
// sw.js is exported by webpack, sourced from `/src/serviceworker/index.ts`
const registration = await navigator.serviceWorker.register("sw.js");
if (!registration) {
throw new Error("Service worker registration failed");
}
navigator.serviceWorker.addEventListener("message", this.onServiceWorkerPostMessage);
await registration.update();
}
private handleServiceWorkerRegistrationError = (): void => {
const key = "service_worker_error";
const brand = SdkConfig.get().brand;
ToastStore.sharedInstance().addOrReplaceToast({
key,
title: _t("service_worker_error|title"),
props: {
description: _t("service_worker_error|description", { brand }),
primaryLabel: _t("action|ok"),
onPrimaryClick: () => {
ToastStore.sharedInstance().dismissToast(key);
},
},
component: GenericToast,
priority: 95,
});
};
private onServiceWorkerPostMessage = (event: MessageEvent): void => {
try {
if (event.data?.["type"] === "userinfo" && event.data?.["responseKey"]) {
const userId = localStorage.getItem("mx_user_id");
const deviceId = localStorage.getItem("mx_device_id");
const homeserver = MatrixClientPeg.get()?.getHomeserverUrl();
event.source!.postMessage({
responseKey: event.data["responseKey"],
userId,
deviceId,
homeserver,
});
}
} catch (e) {
console.error("Error responding to service worker: ", e);
}
};
public getHumanReadableName(): string {
return "Web Platform"; // no translation required: only used for analytics
}
/**
* Returns true if the platform supports displaying
* notifications, otherwise false.
*/
public supportsNotifications(): boolean {
return Boolean(window.Notification);
}
/**
* Returns true if the application currently has permission
* to display notifications. Otherwise false.
*/
public maySendNotifications(): boolean {
return window.Notification.permission === "granted";
}
/**
* Requests permission to send notifications. Returns
* a promise that is resolved when the user has responded
* to the request. The promise has a single string argument
* that is 'granted' if the user allowed the request or
* 'denied' otherwise.
*/
public requestNotificationPermission(): Promise<string> {
// annoyingly, the latest spec says this returns a
// promise, but this is only supported in Chrome 46
// and Firefox 47, so adapt the callback API.
return new Promise(function (resolve, reject) {
window.Notification.requestPermission((result) => {
resolve(result);
}).catch(reject);
});
}
private async getMostRecentVersion(): Promise<string> {
const res = await fetch("version", {
method: "GET",
cache: "no-cache",
});
if (res.ok) {
const text = await res.text();
return getNormalizedAppVersion(text.trim());
}
return Promise.reject({ status: res.status });
}
public getAppVersion(): Promise<string> {
return Promise.resolve(getNormalizedAppVersion(WebPlatform.VERSION));
}
public startUpdater(): void {
// Poll for an update immediately, and reload the page now if we're out of date
// already as we've just initialised an old version of the app somehow.
//
// Forcibly reloading the page aims to avoid users interacting at all with the old
// and potentially broken version of the app.
//
// Ideally, loading an old copy would be impossible with the
// cache-control: nocache HTTP header set, but Firefox doesn't always obey it :/
console.log("startUpdater, current version is " + getNormalizedAppVersion(WebPlatform.VERSION));
void this.pollForUpdate((version: string, newVersion: string) => {
const query = parseQs(location);
if (query.updated) {
console.log("Update reloaded but still on an old version, stopping");
// We just reloaded already and are still on the old version!
// Show the toast rather than reload in a loop.
showUpdateToast(version, newVersion);
return;
}
// Set updated as a cachebusting query param and reload the page.
const url = new URL(window.location.href);
url.searchParams.set("updated", newVersion);
console.log("Update reloading to " + url.toString());
window.location.href = url.toString();
});
setInterval(() => this.pollForUpdate(showUpdateToast, hideUpdateToast), POKE_RATE_MS);
}
public async canSelfUpdate(): Promise<boolean> {
return true;
}
// Exported for tests
public pollForUpdate = (
showUpdate: (currentVersion: string, mostRecentVersion: string) => void,
showNoUpdate?: () => void,
): Promise<UpdateStatus> => {
return this.getMostRecentVersion().then(
(mostRecentVersion) => {
const currentVersion = getNormalizedAppVersion(WebPlatform.VERSION);
if (currentVersion !== mostRecentVersion) {
if (this.shouldShowUpdate(mostRecentVersion)) {
console.log("Update available to " + mostRecentVersion + ", will notify user");
showUpdate(currentVersion, mostRecentVersion);
} else {
console.log("Update available to " + mostRecentVersion + " but won't be shown");
}
return { status: UpdateCheckStatus.Ready };
} else {
console.log("No update available, already on " + mostRecentVersion);
showNoUpdate?.();
}
return { status: UpdateCheckStatus.NotAvailable };
},
(err) => {
logger.error("Failed to poll for update", err);
return {
status: UpdateCheckStatus.Error,
detail: err.message || (err.status ? err.status.toString() : "Unknown Error"),
};
},
);
};
public startUpdateCheck(): void {
super.startUpdateCheck();
void this.pollForUpdate(showUpdateToast, hideUpdateToast).then((updateState) => {
dis.dispatch<CheckUpdatesPayload>({
action: Action.CheckUpdates,
...updateState,
});
});
}
public installUpdate(): void {
window.location.reload();
}
public getDefaultDeviceDisplayName(): string {
// strip query-string and fragment from uri
const url = new URL(window.location.href);
// `appName` in the format `develop.element.io/abc/xyz`
const appName = [
url.host,
url.pathname.replace(/\/$/, ""), // Remove trailing slash if present
].join("");
const ua = new UAParser();
const browserName = ua.getBrowser().name || "unknown browser";
let osName = ua.getOS().name || "unknown OS";
// Stylise the value from the parser to match Apple's current branding.
if (osName === "Mac OS") osName = "macOS";
return _t("web_default_device_name", {
appName,
browserName,
osName,
});
}
public reload(): void {
window.location.reload();
}
public checkSessionLockFree(): boolean {
return SessionLock.checkSessionLockFree();
}
public async getSessionLock(onNewInstance: () => Promise<void>): Promise<boolean> {
return SessionLock.getSessionLock(onNewInstance);
}
}
+94
View File
@@ -0,0 +1,94 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2020 The Matrix.org Foundation C.I.C.
Copyright 2018 New Vector 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.
*/
/*
* Separate file that sets up rageshake logging when imported.
* This is necessary so that rageshake logging is set up before
* anything else. Webpack puts all import statements at the top
* of the file before any code, so imports will always be
* evaluated first. Other imports can cause other code to be
* evaluated (eg. the loglevel library in js-sdk, which if set
* up before rageshake causes some js-sdk logging to be missing
* from the rageshake.)
*/
import { logger } from "matrix-js-sdk/src/logger";
import * as rageshake from "../rageshake/rageshake";
import SdkConfig from "../SdkConfig";
import sendBugReport, { loadBugReport } from "../rageshake/submit-rageshake";
import { BugReportEndpointURLLocal } from "../IConfigOptions";
export function initRageshake(): Promise<void> {
// we manually check persistence for rageshakes ourselves
const prom = rageshake.init(/*setUpPersistence=*/ false);
prom.then(
async () => {
logger.log("Initialised rageshake.");
logger.log(
"To fix line numbers in Chrome: " +
"Meatball menu → Settings → Ignore list → Add /rageshake\\.ts & /logger\\.ts$",
);
window.addEventListener("beforeunload", () => {
logger.log("element-web closing");
// try to flush the logs to indexeddb
rageshake.flush();
});
await rageshake.cleanup();
},
(err) => {
logger.error("Failed to initialise rageshake: " + err);
},
);
return prom;
}
export function initRageshakeStore(): Promise<void> {
return rageshake.tryInitStorage();
}
window.mxSendRageshake = async function (text: string, withLogs = true): Promise<void> {
const url = SdkConfig.get().bug_report_endpoint_url;
if (!url) {
logger.error("Cannot send a rageshake - no bug_report_endpoint_url configured");
return;
}
if (!text || !text.trim()) {
logger.error("Cannot send a rageshake without a message - please tell us what went wrong");
return;
}
if (url === BugReportEndpointURLLocal) {
try {
const tape = await loadBugReport({
userText: text,
sendLogs: withLogs,
progressCallback: logger.log.bind(console),
});
const blob = new Blob([new Uint8Array(tape.out)], { type: "application/gzip" });
const url = URL.createObjectURL(blob);
logger.log(`Your logs are available at ${url}`);
} catch (err) {
logger.error("Failed to load bug report", err);
}
} else {
try {
await sendBugReport(url, {
userText: text,
sendLogs: withLogs,
progressCallback: logger.log.bind(console),
});
logger.log("Bug report sent!");
} catch (err) {
logger.error("Failed to send bug report", err);
}
}
};
+102
View File
@@ -0,0 +1,102 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
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.
*/
// Parse the given window.location and return parameters that can be used when calling
// MatrixChat.showScreen(screen, params)
import { logger } from "matrix-js-sdk/src/logger";
import { type QueryDict } from "matrix-js-sdk/src/utils";
import { parseQsFromFragment } from "./url_utils";
let lastLocationHashSet: string | null = null;
export function getScreenFromLocation(location: Location): { screen: string; params: QueryDict } {
const fragparts = parseQsFromFragment(location);
return {
screen: fragparts.location.substring(1),
params: fragparts.params,
};
}
// Here, we do some crude URL analysis to allow
// deep-linking.
function routeUrl(location: Location): void {
if (!window.matrixChat) return;
logger.log("Routing URL ", location.href);
const s = getScreenFromLocation(location);
window.matrixChat.showScreen(s.screen, s.params);
}
function onHashChange(): void {
if (decodeURIComponent(window.location.hash) === lastLocationHashSet) {
// we just set this: no need to route it!
return;
}
routeUrl(window.location);
}
// This will be called whenever the SDK changes screens,
// so a web page can update the URL bar appropriately.
export function onNewScreen(screen: string, replaceLast = false): void {
logger.log("newscreen " + screen);
const hash = "#/" + screen;
lastLocationHashSet = hash;
// if the new hash is a substring of the old one then we are stripping fields e.g `via` so replace history
if (
screen.startsWith("room/") &&
window.location.hash.includes("/$") === hash.includes("/$") && // only if both did or didn't contain event link
window.location.hash.startsWith(hash)
) {
replaceLast = true;
}
if (replaceLast) {
window.location.replace(hash);
} else {
window.location.assign(hash);
}
}
export function init(): void {
window.addEventListener("hashchange", onHashChange);
}
const ScreenAfterLoginStorageKey = "mx_screen_after_login";
function getStoredInitialScreenAfterLogin(): ReturnType<typeof getScreenFromLocation> | undefined {
const screenAfterLogin = sessionStorage.getItem(ScreenAfterLoginStorageKey);
return screenAfterLogin ? JSON.parse(screenAfterLogin) : undefined;
}
function setInitialScreenAfterLogin(screenAfterLogin?: ReturnType<typeof getScreenFromLocation>): void {
if (screenAfterLogin?.screen) {
sessionStorage.setItem(ScreenAfterLoginStorageKey, JSON.stringify(screenAfterLogin));
}
}
/**
* Get the initial screen to be displayed after login,
* for example when trying to view a room via a link before logging in
*
* If the current URL has a screen set that in session storage
* Then retrieve the screen from session storage and return it
* Using session storage allows us to remember login fragments from when returning from OIDC login
* @returns screen and params or undefined
*/
export function getInitialScreenAfterLogin(location: Location): ReturnType<typeof getScreenFromLocation> | undefined {
const screenAfterLogin = getScreenFromLocation(location);
if (screenAfterLogin.screen || screenAfterLogin.params) {
setInitialScreenAfterLogin(screenAfterLogin);
}
const storedScreenAfterLogin = getStoredInitialScreenAfterLogin();
return storedScreenAfterLogin;
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,195 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<head>
<style type="text/css">
/* By default, hide the custom IS stuff - enabled in JS */
#custom_is,
#is_url {
display: none;
}
html {
height: 100%;
}
body {
background: #f9fafb;
max-width: 680px;
margin: auto;
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif,
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
}
.mx_Button {
border: 0;
border-radius: 4px;
font-size: 18px;
margin-left: 4px;
margin-right: 4px;
min-width: 80px;
background-color: #03b381;
color: #fff;
cursor: pointer;
padding: 12px 22px;
word-break: break-word;
text-decoration: none;
}
.mx_Center {
justify-content: center;
}
.mx_ClearDecoration {
text-decoration: none !important;
}
.mx_HomePage_header {
color: #2e2f32;
display: flex;
align-items: center;
justify-content: center;
}
.mx_HomePage h3 {
margin-top: 30px;
}
.mx_HomePage_col {
display: flex;
flex-direction: row;
}
.mx_HomePage_row {
flex: 1 1 0;
display: flex;
flex-direction: row;
flex-wrap: wrap;
}
.mx_HomePage_logo {
margin-right: 20px;
margin-top: -10px;
}
.mx_HomePage_container {
display: block !important;
margin: 10px 20px;
}
.mx_HomePage_errorContainer {
display: none; /* shown in JS if needed */
margin: 20px;
border: 1px solid red;
background-color: #ffb9b9;
padding: 5px;
}
.mx_HomePage_container h1,
.mx_HomePage_container h2,
.mx_HomePage_container h3,
.mx_HomePage_container h4 {
font-weight: 600;
margin-bottom: 32px;
}
.mx_Spacer {
margin-top: 24px;
}
.mx_FooterLink {
color: ##0dbd8b;
text-decoration: none;
}
.mx_Subtext {
font-size: 14px;
}
.mx_SubtextTop {
margin-top: 32px;
}
@media screen and (max-width: 1120px) {
body {
font-size: 20px;
}
h1 {
font-size: 20px;
}
h4 {
font-size: 16px;
}
.mx_Button {
font-size: 18px;
padding: 14px 28px;
}
.mx_HomePage_header {
justify-content: left;
}
.mx_Spacer {
margin-top: 24px;
}
}
</style>
<meta name="apple-itunes-app" content="app-id=id1083446067" />
</head>
<body>
<div class="mx_HomePage_errorContainer">
<!-- populated by JS if needed -->
</div>
<div class="mx_HomePage_container">
<div class="mx_HomePage_header">
<span class="mx_HomePage_logo">
<svg width="34" height="42" viewBox="0 0 34 42" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M13.08 8.68C13.08 7.75216 13.8321 7 14.76 7C20.9456 7 25.96 12.0144 25.96 18.2C25.96 19.1278 25.2078 19.88 24.28 19.88C23.3521 19.88 22.6 19.1278 22.6 18.2C22.6 13.8701 19.0899 10.36 14.76 10.36C13.8321 10.36 13.08 9.60784 13.08 8.68Z"
fill="#0DBD8B"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M20.92 33.32C20.92 34.2478 20.1679 35 19.24 35C13.0544 35 8.04001 29.9856 8.04001 23.8C8.04001 22.8722 8.79217 22.12 9.72001 22.12C10.6478 22.12 11.4 22.8722 11.4 23.8C11.4 28.1299 14.9101 31.64 19.24 31.64C20.1679 31.64 20.92 32.3922 20.92 33.32Z"
fill="#0DBD8B"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M4.68 24.9199C3.75216 24.9199 3 24.1678 3 23.2399C3 17.0543 8.01441 12.0399 14.2 12.0399C15.1278 12.0399 15.88 12.7921 15.88 13.7199C15.88 14.6478 15.1278 15.3999 14.2 15.3999C9.87009 15.3999 6.36 18.91 6.36 23.2399C6.36 24.1678 5.60784 24.9199 4.68 24.9199Z"
fill="#0DBD8B"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M29.32 17.0801C30.2478 17.0801 31 17.8322 31 18.7601C31 24.9457 25.9856 29.9601 19.8 29.9601C18.8722 29.9601 18.12 29.2079 18.12 28.2801C18.12 27.3522 18.8722 26.6001 19.8 26.6001C24.1299 26.6001 27.64 23.09 27.64 18.7601C27.64 17.8322 28.3922 17.0801 29.32 17.0801Z"
fill="#0DBD8B"
/>
</svg>
</span>
<h1>Unable to load</h1>
</div>
<div class="mx_HomePage_col">
<div class="mx_HomePage_row">
<div>
<h2 id="step1_heading">Element can't load</h2>
<p>Something went wrong and Element was unable to load.</p>
</div>
</div>
</div>
<div class="mx_HomePage_row mx_Center mx_Spacer">
<p class="mx_Spacer">
<a href="https://element.io" target="_blank" class="mx_FooterLink"> Go to element.io </a>
</p>
</div>
</div>
</body>
+36
View File
@@ -0,0 +1,36 @@
/*
Copyright 2018-2024 New Vector 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 { type QueryDict, decodeParams } from "matrix-js-sdk/src/utils";
// We want to support some name / value pairs in the fragment
// so we're re-using query string like format
//
export function parseQsFromFragment(location: Location): { location: string; params: QueryDict } {
// if we have a fragment, it will start with '#', which we need to drop.
// (if we don't, this will return '').
const fragment = location.hash.substring(1);
// our fragment may contain a query-param-like section. we need to fish
// this out *before* URI-decoding because the params may contain ? and &
// characters which are only URI-encoded once.
const hashparts = fragment.split("?");
const result = {
location: decodeURIComponent(hashparts[0]),
params: <QueryDict>{},
};
if (hashparts.length > 1) {
result.params = decodeParams(hashparts[1]);
}
return result;
}
export function parseQs(location: Location): QueryDict {
return decodeParams(location.search.substring(1));
}