Merge branch 'develop' into superkenvery/webaudioapi

This commit is contained in:
Michael Telatynski 2024-06-20 17:19:10 +01:00 committed by GitHub
commit 6821a35444
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
224 changed files with 22605 additions and 19617 deletions

View file

@ -23,7 +23,6 @@ import "matrix-js-sdk/src/browser-index";
import React, { ReactElement } from "react";
import PlatformPeg from "matrix-react-sdk/src/PlatformPeg";
import { UserFriendlyError } from "matrix-react-sdk/src/languageHandler";
import AutoDiscoveryUtils from "matrix-react-sdk/src/utils/AutoDiscoveryUtils";
import { AutoDiscovery, ClientConfig } from "matrix-js-sdk/src/autodiscovery";
import * as Lifecycle from "matrix-react-sdk/src/Lifecycle";
@ -34,11 +33,13 @@ import { createClient } from "matrix-js-sdk/src/matrix";
import { SnakedObject } from "matrix-react-sdk/src/utils/SnakedObject";
import MatrixChat from "matrix-react-sdk/src/components/structures/MatrixChat";
import { ValidatedServerConfig } from "matrix-react-sdk/src/utils/ValidatedServerConfig";
import { QueryDict, encodeParams } from "matrix-js-sdk/src/utils";
import { WrapperLifecycle, WrapperOpts } from "@matrix-org/react-sdk-module-api/lib/lifecycles/WrapperLifecycle";
import { ModuleRunner } from "matrix-react-sdk/src/modules/ModuleRunner";
import { parseQs } from "./url_utils";
import VectorBasePlatform from "./platform/VectorBasePlatform";
import { getScreenFromLocation, init as initRouting, onNewScreen } from "./routing";
import { getInitialScreenAfterLogin, getScreenFromLocation, init as initRouting, onNewScreen } from "./routing";
import { UserFriendlyError } from "../languageHandler";
// add React and ReactPerf to the global namespace, to make them easier to access via the console
// this incidentally means we can forget our React imports in JSX files without penalty.
@ -48,44 +49,23 @@ logger.log(`Application is running in ${process.env.NODE_ENV} mode`);
window.matrixLogger = logger;
// We use this to work out what URL the SDK should
// pass through when registering to allow the user to
// click back to the client having registered.
// It's up to us to recognise if we're loaded with
// this URL and tell MatrixClient to resume registration.
//
// If we're in electron, we should never pass through a file:// URL otherwise
// the identity server will try to 302 the browser to it, which breaks horribly.
// so in that instance, hardcode to use app.element.io for now instead.
function makeRegistrationUrl(params: QueryDict): string {
let url: string;
if (window.location.protocol === "vector:") {
url = "https://app.element.io/#/register";
} else {
url = window.location.protocol + "//" + window.location.host + window.location.pathname + "#/register";
}
const encodedParams = encodeParams(params);
if (encodedParams) {
url += "?" + encodedParams;
}
return url;
}
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; a little nasty but let's redirect to
// clear them.
// 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("loginToken");
url.searchParams.delete("state");
url.searchParams.delete("code");
logger.log(`Redirecting to ${url.href} to drop loginToken from queryparams`);
logger.log(`Redirecting to ${url.href} to drop delegated authentication params from queryparams`);
window.history.replaceState(null, "", url.href);
}
export async function loadApp(fragParams: {}): Promise<ReactElement> {
export async function loadApp(fragParams: {}, matrixChatRef: React.Ref<MatrixChat>): Promise<ReactElement> {
initRouting();
const platform = PlatformPeg.get();
@ -109,9 +89,14 @@ export async function loadApp(fragParams: {}): Promise<ReactElement> {
// 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;
}
if (!hasPossibleToken && !isReturningFromSso && autoRedirect) {
logger.log("Bypassing app load to redirect to SSO");
const tempCli = createClient({
@ -129,18 +114,25 @@ export async function loadApp(fragParams: {}): Promise<ReactElement> {
const defaultDeviceName =
snakedConfig.get("default_device_display_name") ?? platform?.getDefaultDeviceDisplayName();
const initialScreenAfterLogin = getInitialScreenAfterLogin(window.location);
const wrapperOpts: WrapperOpts = { Wrapper: React.Fragment };
ModuleRunner.instance.invoke(WrapperLifecycle.Wrapper, wrapperOpts);
return (
<MatrixChat
onNewScreen={onNewScreen}
makeRegistrationUrl={makeRegistrationUrl}
config={config}
realQueryParams={params}
startingFragmentQueryParams={fragParams}
enableGuest={!config.disable_guests}
onTokenLoginCompleted={onTokenLoginCompleted}
initialScreenAfterLogin={getScreenFromLocation(window.location)}
defaultDeviceDisplayName={defaultDeviceName}
/>
<wrapperOpts.Wrapper>
<MatrixChat
ref={matrixChatRef}
onNewScreen={onNewScreen}
config={config}
realQueryParams={params}
startingFragmentQueryParams={fragParams}
enableGuest={!config.disable_guests}
onTokenLoginCompleted={onTokenLoginCompleted}
initialScreenAfterLogin={initialScreenAfterLogin}
defaultDeviceDisplayName={defaultDeviceName}
/>
</wrapperOpts.Wrapper>
);
}
@ -165,16 +157,13 @@ async function verifyServerConfig(): Promise<IConfigOptions> {
const isUrl = config["default_is_url"];
const incompatibleOptions = [wkConfig, serverName, hsUrl].filter((i) => !!i);
if (incompatibleOptions.length > 1) {
if (hsUrl && (wkConfig || serverName)) {
// noinspection ExceptionCaughtLocallyJS
throw new UserFriendlyError(
"Invalid configuration: can only specify one of default_server_config, default_server_name, " +
"or default_hs_url.",
);
throw new UserFriendlyError("error|invalid_configuration_mixed_server");
}
if (incompatibleOptions.length < 1) {
// noinspection ExceptionCaughtLocallyJS
throw new UserFriendlyError("Invalid configuration: no default server specified.");
throw new UserFriendlyError("error|invalid_configuration_no_server");
}
if (hsUrl) {
@ -197,7 +186,7 @@ async function verifyServerConfig(): Promise<IConfigOptions> {
}
let discoveryResult: ClientConfig | undefined;
if (wkConfig) {
if (!serverName && wkConfig) {
logger.log("Config uses a default_server_config - validating object");
discoveryResult = await AutoDiscovery.fromDiscoveryConfig(wkConfig);
}
@ -209,9 +198,13 @@ async function verifyServerConfig(): Promise<IConfigOptions> {
"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 = AutoDiscoveryUtils.buildValidatedConfigFromDiscovery(serverName, discoveryResult, true);
validatedConfig = await AutoDiscoveryUtils.buildValidatedConfigFromDiscovery(serverName, discoveryResult, true);
} catch (e) {
const { hsUrl, isUrl, userId } = await Lifecycle.getStoredSessionVars();
if (hsUrl && userId) {

View file

@ -49,19 +49,27 @@
<% }
} %>
<% for (var i=0; i < htmlWebpackPlugin.tags.headTags.length; i++) {
var tag = htmlWebpackPlugin.tags.headTags[i];
var path = tag.attributes && tag.attributes.href;
if (path.includes("/Inter/")) { %>
<link rel="preload" as="font" href=".<%= path %>" crossorigin="anonymous"/>
<% 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? -->
<section id="matrixchat" style="height: 100%;" class="notranslate"></section>
<script src="<%= htmlWebpackPlugin.files.js.find(entry => entry.includes("bundle.js")) %>"></script>
<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('matrix-react-sdk/res/img/warning.svg').default %>" aria-hidden alt="" width="24" height="23" style="visibility: hidden; position: absolute; top: 0px; left: 0px;"/>
@ -76,5 +84,11 @@
<audio id="remoteAudio"></audio>
<!-- 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>

View file

@ -28,7 +28,6 @@ import "./modernizr";
// 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
require("gfm.css/gfm.css");
require("katex/dist/katex.css");
/**
@ -99,7 +98,7 @@ const supportedBrowser = checkBrowserFeatures();
// 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/vector-im/element-web/issues/12253
// Load parallelism is based on research in https://github.com/element-hq/element-web/issues/12253
async function start(): Promise<void> {
// load init.ts async so that its code is not executed immediately and we can catch any exceptions
const {
@ -130,7 +129,7 @@ async function start(): Promise<void> {
// 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/vector-im/element-web/issues/7378)
// (https://github.com/element-hq/element-web/issues/7378)
const preventRedirect = fragparts.params.client_secret || fragparts.location.length > 0;
if (!preventRedirect) {
@ -177,7 +176,7 @@ async function start(): Promise<void> {
// error handling begins here
// ##########################
if (!acceptBrowser) {
await new Promise<void>((resolve) => {
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(() => {
@ -186,7 +185,7 @@ async function start(): Promise<void> {
}
logger.log("User accepts the compatibility risks.");
resolve();
});
}).catch(reject);
});
}
@ -197,17 +196,14 @@ async function start(): Promise<void> {
// 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("Your Element is misconfigured"), [
_t(
"Your Element configuration contains invalid JSON. " +
"Please correct the problem and reload the page.",
),
_t("The message from the parser is: %(message)s", {
message: error.message || _t("Invalid JSON"),
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("Unable to load config file: please refresh the page to try again."));
return showError(_t("error|cannot_load_config"));
}
// ##################################
@ -231,8 +227,8 @@ async function start(): Promise<void> {
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("Your Element is misconfigured"), [
extractErrorMessageFromError(err, _t("Unexpected error preparing the app. See console for details.")),
await showError(_t("error|misconfigured"), [
extractErrorMessageFromError(err, _t("error|app_launch_unexpected_error")),
]);
}
}

View file

@ -30,6 +30,7 @@ import SdkConfig from "matrix-react-sdk/src/SdkConfig";
import { setTheme } from "matrix-react-sdk/src/theme";
import { logger } from "matrix-js-sdk/src/logger";
import { ModuleRunner } from "matrix-react-sdk/src/modules/ModuleRunner";
import MatrixChat from "matrix-react-sdk/src/components/structures/MatrixChat";
import ElectronPlatform from "./platform/ElectronPlatform";
import PWAPlatform from "./platform/PWAPlatform";
@ -137,7 +138,7 @@ export async function loadLanguage(): Promise<void> {
}
export async function loadTheme(): Promise<void> {
setTheme();
return setTheme();
}
export async function loadApp(fragParams: {}): Promise<void> {
@ -147,7 +148,10 @@ export async function loadApp(fragParams: {}): Promise<void> {
/* webpackPreload: true */
"./app"
);
window.matrixChat = ReactDOM.render(await module.loadApp(fragParams), document.getElementById("matrixchat"));
function setWindowMatrixChat(matrixChat: MatrixChat): void {
window.matrixChat = matrixChat;
}
ReactDOM.render(await module.loadApp(fragParams, setWindowMatrixChat), document.getElementById("matrixchat"));
}
export async function showError(title: string, messages?: string[]): Promise<void> {
@ -184,4 +188,4 @@ export async function loadModules(): Promise<void> {
}
}
export const _t = languageHandler._t;
export { _t } from "../languageHandler";

View file

@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
/* TODO: Match the user's theme: https://github.com/vector-im/element-web/issues/12794 */
/* TODO: Match the user's theme: https://github.com/element-hq/element-web/issues/12794 */
@font-face {
font-family: "Nunito";

View file

@ -37,9 +37,40 @@ import type {
AudioMuteStatusChangedEvent,
LogEvent,
VideoMuteStatusChangedEvent,
ExternalAPIOptions as _ExternalAPIOptions,
Config as _Config,
InterfaceConfig as _InterfaceConfig,
} from "jitsi-meet";
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.
require("./index.pcss");
@ -146,17 +177,17 @@ const setupCompleted = (async (): Promise<string | void> => {
}
}
await widgetApi!.transport.reply(ev.detail, response);
widgetApi!.transport.reply(ev.detail, response);
});
};
handleAction(ElementWidgetActions.JoinCall, async ({ audioInput, videoInput }) => {
joinConference(audioInput as string | null, videoInput as string | null);
void joinConference(audioInput as string | null, videoInput as string | null);
});
handleAction(ElementWidgetActions.HangupCall, async ({ force }) => {
if (force === true) {
meetApi?.dispose();
notifyHangup();
void notifyHangup();
meetApi = undefined;
closeConference();
} else {
@ -261,14 +292,12 @@ function switchVisibleContainers(): void {
function toggleConferenceVisibility(inConference: boolean): void {
document.getElementById("jitsiContainer")!.style.visibility = inConference ? "unset" : "hidden";
// Video rooms have a separate UI for joining, so they should never show our join button
document.getElementById("joinButtonContainer")!.style.visibility =
inConference || isVideoChannel ? "hidden" : "unset";
document.getElementById("joinButtonContainer")!.style.visibility = inConference ? "hidden" : "unset";
}
function skipToJitsiSplashScreen(): void {
// really just a function alias for self-documenting code
joinConference();
void joinConference();
}
/**
@ -382,7 +411,7 @@ async function joinConference(audioInput?: string | null, videoInput?: string |
"our fragment values and not recognizing the options.",
);
const options = {
const options: ExternalAPIOptions = {
width: "100%",
height: "100%",
parentNode: document.querySelector("#jitsiContainer") ?? undefined,
@ -419,22 +448,21 @@ async function joinConference(audioInput?: string | null, videoInput?: string |
// Video channel widgets need some more tailored config options
if (isVideoChannel) {
// Ensure that we skip Jitsi Meet's native prejoin screen, for
// deployments that have it enabled
options.configOverwrite.prejoinConfig = { enabled: false };
// 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"];
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/vector-im/element-web/issues/4880#issuecomment-940002464
if (supportsScreensharing) options.configOverwrite.toolbarButtons.splice(2, 0, "desktop");
// 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: [] };
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;
options.configOverwrite!.disableSelfViewSettings = true;
}
meetApi = new JitsiMeetExternalAPI(jitsiDomain, options);
@ -472,8 +500,8 @@ const onVideoConferenceJoined = (): void => {
if (widgetApi) {
// ignored promise because we don't care if it works
// noinspection JSIgnoredPromiseFromCall
widgetApi.setAlwaysOnScreen(true);
widgetApi.transport.send(ElementWidgetActions.JoinCall, {});
void widgetApi.setAlwaysOnScreen(true);
void widgetApi.transport.send(ElementWidgetActions.JoinCall, {});
}
// Video rooms should start in tile mode
@ -481,7 +509,7 @@ const onVideoConferenceJoined = (): void => {
};
const onVideoConferenceLeft = (): void => {
notifyHangup();
void notifyHangup();
meetApi = undefined;
};
@ -489,7 +517,7 @@ const onErrorOccurred = ({ error }: Parameters<ExternalAPIEventCallbacks["errorO
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.
notifyHangup(error.message);
void notifyHangup(error.message);
meetApi = undefined;
closeConference();
}
@ -497,7 +525,7 @@ const onErrorOccurred = ({ error }: Parameters<ExternalAPIEventCallbacks["errorO
const onAudioMuteStatusChanged = ({ muted }: AudioMuteStatusChangedEvent): void => {
const action = muted ? ElementWidgetActions.MuteAudio : ElementWidgetActions.UnmuteAudio;
widgetApi?.transport.send(action, {});
void widgetApi?.transport.send(action, {});
};
const onVideoMuteStatusChanged = ({ muted }: VideoMuteStatusChangedEvent): void => {
@ -507,15 +535,15 @@ const onVideoMuteStatusChanged = ({ muted }: VideoMuteStatusChangedEvent): void
// otherwise the React SDK will mistakenly think the user turned off
// their video by hand
setTimeout(() => {
if (meetApi) widgetApi?.transport.send(ElementWidgetActions.MuteVideo, {});
if (meetApi) void widgetApi?.transport.send(ElementWidgetActions.MuteVideo, {});
}, 200);
} else {
widgetApi?.transport.send(ElementWidgetActions.UnmuteVideo, {});
void widgetApi?.transport.send(ElementWidgetActions.UnmuteVideo, {});
}
};
const updateParticipants = (): void => {
widgetApi?.transport.send(ElementWidgetActions.CallParticipants, {
void widgetApi?.transport.send(ElementWidgetActions.CallParticipants, {
participants: meetApi?.getParticipantsInfo(),
});
};

View file

@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<title>Element Mobile Guide</title>

View file

@ -44,10 +44,10 @@ async function initPage(): Promise<void> {
const defaultIsUrl = config?.["default_is_url"];
const incompatibleOptions = [wkConfig, serverName, defaultHsUrl].filter((i) => !!i);
if (incompatibleOptions.length > 1) {
if (defaultHsUrl && (wkConfig || serverName)) {
return renderConfigError(
"Invalid configuration: can only specify one of default_server_config, default_server_name, " +
"or default_hs_url.",
"Invalid configuration: a default_hs_url can't be specified along with default_server_name " +
"or default_server_config",
);
}
if (incompatibleOptions.length < 1) {
@ -57,7 +57,7 @@ async function initPage(): Promise<void> {
let hsUrl: string | undefined;
let isUrl: string | undefined;
if (typeof wkConfig?.["m.homeserver"]?.["base_url"] === "string") {
if (!serverName && typeof wkConfig?.["m.homeserver"]?.["base_url"] === "string") {
hsUrl = wkConfig["m.homeserver"]["base_url"];
if (typeof wkConfig["m.identity_server"]?.["base_url"] === "string") {
@ -78,8 +78,16 @@ async function initPage(): Promise<void> {
}
}
} catch (e) {
logger.error(e);
return renderConfigError("Unable to fetch homeserver configuration");
if (wkConfig && 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");
}
}
}
@ -112,4 +120,4 @@ async function initPage(): Promise<void> {
}
}
initPage();
void initPage();

View file

@ -21,7 +21,6 @@ limitations under the License.
import { UpdateCheckStatus, UpdateStatus } from "matrix-react-sdk/src/BasePlatform";
import BaseEventIndexManager from "matrix-react-sdk/src/indexing/BaseEventIndexManager";
import dis from "matrix-react-sdk/src/dispatcher/dispatcher";
import { _t } from "matrix-react-sdk/src/languageHandler";
import SdkConfig from "matrix-react-sdk/src/SdkConfig";
import { IConfigOptions } from "matrix-react-sdk/src/IConfigOptions";
import * as rageshake from "matrix-react-sdk/src/rageshake/rageshake";
@ -43,10 +42,13 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { BreadcrumbsStore } from "matrix-react-sdk/src/stores/BreadcrumbsStore";
import { UPDATE_EVENT } from "matrix-react-sdk/src/stores/AsyncStore";
import { avatarUrlForRoom, getInitialLetter } from "matrix-react-sdk/src/Avatar";
import DesktopCapturerSourcePicker from "matrix-react-sdk/src/components/views/elements/DesktopCapturerSourcePicker";
import { OidcRegistrationClientMetadata } from "matrix-js-sdk/src/matrix";
import VectorBasePlatform from "./VectorBasePlatform";
import { SeshatIndexManager } from "./SeshatIndexManager";
import { IPCManager } from "./IPCManager";
import { _t } from "../../languageHandler";
interface SquirrelUpdate {
releaseNotes: string;
@ -55,6 +57,8 @@ interface SquirrelUpdate {
updateURL: string;
}
const SSO_ID_KEY = "element-desktop-ssoid";
const isMac = navigator.platform.toUpperCase().includes("MAC");
function platformFriendlyName(): string {
@ -149,12 +153,12 @@ export default class ElectronPlatform extends VectorBasePlatform {
ToastStore.sharedInstance().addOrReplaceToast({
key,
title: _t("Download Completed"),
title: _t("download_completed"),
props: {
description: name,
acceptLabel: _t("Open"),
acceptLabel: _t("action|open"),
onAccept,
dismissLabel: _t("Dismiss"),
dismissLabel: _t("action|dismiss"),
onDismiss,
numSeconds: 10,
},
@ -163,7 +167,14 @@ export default class ElectronPlatform extends VectorBasePlatform {
});
});
this.ipc.call("startSSOFlow", this.ssoID);
window.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: "" });
});
void this.ipc.call("startSSOFlow", this.ssoID);
BreadcrumbsStore.instance.on(UPDATE_EVENT, this.onBreadcrumbsUpdate);
}
@ -183,7 +194,7 @@ export default class ElectronPlatform extends VectorBasePlatform {
),
initial: getInitialLetter(r.name),
}));
this.ipc.call("breadcrumbs", rooms);
void this.ipc.call("breadcrumbs", rooms);
};
private onUpdateDownloaded = async (ev: Event, { releaseNotes, releaseName }: SquirrelUpdate): Promise<void> => {
@ -249,7 +260,7 @@ export default class ElectronPlatform extends VectorBasePlatform {
const handler = notification.onclick as Function;
notification.onclick = (): void => {
handler?.();
this.ipc.call("focusWindow");
void this.ipc.call("focusWindow");
};
return notification;
@ -304,7 +315,7 @@ export default class ElectronPlatform extends VectorBasePlatform {
public getDefaultDeviceDisplayName(): string {
const brand = SdkConfig.get().brand;
return _t("%(brand)s Desktop: %(platformName)s", {
return _t("desktop_default_device_name", {
brand,
platformName: platformFriendlyName(),
});
@ -357,7 +368,7 @@ export default class ElectronPlatform extends VectorBasePlatform {
}
public supportsJitsiScreensharing(): boolean {
// See https://github.com/vector-im/element-web/issues/4880
// See https://github.com/element-hq/element-web/issues/4880
return false;
}
@ -365,10 +376,10 @@ export default class ElectronPlatform extends VectorBasePlatform {
return this.ipc.call("getAvailableSpellCheckLanguages");
}
public getSSOCallbackUrl(fragmentAfterLogin: string): URL {
public getSSOCallbackUrl(fragmentAfterLogin?: string): URL {
const url = super.getSSOCallbackUrl(fragmentAfterLogin);
url.protocol = "element";
url.searchParams.set("element-desktop-ssoid", this.ssoID);
url.searchParams.set(SSO_ID_KEY, this.ssoID);
return url;
}
@ -381,13 +392,13 @@ export default class ElectronPlatform extends VectorBasePlatform {
// this will get intercepted by electron-main will-navigate
super.startSingleSignOn(mxClient, loginType, fragmentAfterLogin, idpId);
Modal.createDialog(InfoDialog, {
title: _t("Go to your browser to complete Sign In"),
title: _t("auth|sso_complete_in_browser_dialog_title"),
description: <Spinner />,
});
}
public navigateForwardBack(back: boolean): void {
this.ipc.call(back ? "navigateBack" : "navigateForward");
void this.ipc.call(back ? "navigateBack" : "navigateForward");
}
public overrideBrowserShortcuts(): boolean {
@ -426,4 +437,35 @@ export default class ElectronPlatform extends VectorBasePlatform {
await this.ipc.call("clearStorage");
} catch (e) {}
}
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.ssoID}`;
}
/**
* The URL to return to after a successful OIDC authentication
*/
public getOidcCallbackUrl(): URL {
const url = super.getOidcCallbackUrl();
url.protocol = "io.element.desktop";
return url;
}
}

View file

@ -18,11 +18,11 @@ limitations under the License.
*/
import BasePlatform from "matrix-react-sdk/src/BasePlatform";
import { _t } from "matrix-react-sdk/src/languageHandler";
import type { IConfigOptions } from "matrix-react-sdk/src/IConfigOptions";
import { getVectorConfig } from "../getconfig";
import Favicon from "../../favicon";
import { _t } from "../../languageHandler";
/**
* Vector-specific extensions to the BasePlatform template
@ -41,7 +41,7 @@ export default abstract class VectorBasePlatform extends BasePlatform {
/**
* Delay creating the `Favicon` instance until first use (on the first notification) as
* it uses canvas, which can trigger a permission prompt in Firefox's resist fingerprinting mode.
* See https://github.com/vector-im/element-web/issues/9605.
* See https://github.com/element-hq/element-web/issues/9605.
*/
public get favicon(): Favicon {
if (this._favicon) {
@ -85,6 +85,6 @@ export default abstract class VectorBasePlatform extends BasePlatform {
* device Vector is running on
*/
public getDefaultDeviceDisplayName(): string {
return _t("Unknown device");
return _t("unknown_device");
}
}

View file

@ -1,7 +1,7 @@
/*
Copyright 2016 Aviral Dasgupta
Copyright 2016 OpenMarket Ltd
Copyright 2017-2020 New Vector Ltd
Copyright 2017-2020, 2024 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@ -18,7 +18,6 @@ limitations under the License.
import { UpdateCheckStatus, UpdateStatus } from "matrix-react-sdk/src/BasePlatform";
import dis from "matrix-react-sdk/src/dispatcher/dispatcher";
import { _t } from "matrix-react-sdk/src/languageHandler";
import { hideToast as hideUpdateToast, showToast as showUpdateToast } from "matrix-react-sdk/src/toasts/UpdateToast";
import { Action } from "matrix-react-sdk/src/dispatcher/actions";
import { CheckUpdatesPayload } from "matrix-react-sdk/src/dispatcher/payloads/CheckUpdatesPayload";
@ -27,6 +26,7 @@ import { logger } from "matrix-js-sdk/src/logger";
import VectorBasePlatform from "./VectorBasePlatform";
import { parseQs } from "../url_utils";
import { _t } from "../../languageHandler";
const POKE_RATE_MS = 10 * 60 * 1000; // 10 min
@ -44,9 +44,41 @@ export default class WebPlatform extends VectorBasePlatform {
public constructor() {
super();
// Register service worker if available on this platform
if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("sw.js");
// Register the service worker in the background
this.tryRegisterServiceWorker().catch((e) => console.error("Error registering/updating service worker:", e));
}
private async tryRegisterServiceWorker(): Promise<void> {
if (!("serviceWorker" in navigator)) {
return; // not available on this platform - don't try to register the service worker
}
// sw.js is exported by webpack, sourced from `/src/serviceworker/index.ts`
const registration = await navigator.serviceWorker.register("sw.js");
if (!registration) {
// Registration didn't work for some reason - assume failed and ignore.
// This typically happens in Jest.
return;
}
await registration.update();
navigator.serviceWorker.addEventListener("message", this.onServiceWorkerPostMessage.bind(this));
}
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");
event.source!.postMessage({
responseKey: event.data["responseKey"],
userId,
deviceId,
});
}
} catch (e) {
console.error("Error responding to service worker: ", e);
}
}
@ -81,10 +113,10 @@ export default class WebPlatform extends VectorBasePlatform {
// 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) {
return new Promise(function (resolve, reject) {
window.Notification.requestPermission((result) => {
resolve(result);
});
}).catch(reject);
});
}
@ -116,7 +148,7 @@ export default class WebPlatform extends VectorBasePlatform {
// 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));
this.pollForUpdate((version: string, newVersion: string) => {
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");
@ -175,7 +207,7 @@ export default class WebPlatform extends VectorBasePlatform {
public startUpdateCheck(): void {
super.startUpdateCheck();
this.pollForUpdate(showUpdateToast, hideUpdateToast).then((updateState) => {
void this.pollForUpdate(showUpdateToast, hideUpdateToast).then((updateState) => {
dis.dispatch<CheckUpdatesPayload>({
action: Action.CheckUpdates,
...updateState,
@ -202,7 +234,7 @@ export default class WebPlatform extends VectorBasePlatform {
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("%(appName)s: %(browserName)s on %(osName)s", {
return _t("web_default_device_name", {
appName,
browserName,
osName,

View file

@ -35,10 +35,11 @@ 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\\.js$",
"To fix line numbers in Chrome: " +
"Meatball menu → Settings → Ignore list → Add /rageshake\\.ts & /logger\\.ts$",
);
window.addEventListener("beforeunload", () => {
@ -47,7 +48,7 @@ export function initRageshake(): Promise<void> {
rageshake.flush();
});
rageshake.cleanup();
await rageshake.cleanup();
},
(err) => {
logger.error("Failed to initialise rageshake: " + err);

View file

@ -76,3 +76,36 @@ export function onNewScreen(screen: string, replaceLast = false): void {
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;
}