Merge branch 'develop' of github.com:vector-im/element-web into t3chguy/querystring

 Conflicts:
	package.json
	src/@types/global.d.ts
	src/vector/app.tsx
	src/vector/jitsi/index.ts
	src/vector/platform/WebPlatform.ts
	yarn.lock
This commit is contained in:
Michael Telatynski 2021-07-16 12:45:37 +01:00
commit b03b4582c0
266 changed files with 11675 additions and 10340 deletions

View file

@ -19,30 +19,28 @@ limitations under the License.
*/
import React from 'react';
// add React and ReactPerf to the global namespace, to make them easier to
// access via the console
global.React = React;
// 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.
window.React = React;
import * as sdk from 'matrix-react-sdk';
import PlatformPeg from 'matrix-react-sdk/src/PlatformPeg';
import * as VectorConferenceHandler from 'matrix-react-sdk/src/VectorConferenceHandler';
import {_td, newTranslatableError} from 'matrix-react-sdk/src/languageHandler';
import { _td, newTranslatableError } from 'matrix-react-sdk/src/languageHandler';
import AutoDiscoveryUtils from 'matrix-react-sdk/src/utils/AutoDiscoveryUtils';
import {AutoDiscovery} from "matrix-js-sdk/src/autodiscovery";
import { AutoDiscovery } from "matrix-js-sdk/src/autodiscovery";
import * as Lifecycle from "matrix-react-sdk/src/Lifecycle";
import {parseQs, parseQsFromFragment} from './url_utils';
import {MatrixClientPeg} from 'matrix-react-sdk/src/MatrixClientPeg';
import type MatrixChatType from "matrix-react-sdk/src/components/structures/MatrixChat";
import SdkConfig from "matrix-react-sdk/src/SdkConfig";
import CallHandler from 'matrix-react-sdk/src/CallHandler';
import { parseQs, parseQsFromFragment } from './url_utils';
import VectorBasePlatform from "./platform/VectorBasePlatform";
import { createClient } from "matrix-js-sdk/src/matrix";
let lastLocationHashSet = null;
let lastLocationHashSet: string = null;
// Parse the given window.location and return parameters that can be used when calling
// MatrixChat.showScreen(screen, params)
function getScreenFromLocation(location) {
function getScreenFromLocation(location: Location) {
const fragparts = parseQsFromFragment(location);
return {
screen: fragparts.location.substring(1),
@ -52,15 +50,15 @@ function getScreenFromLocation(location) {
// Here, we do some crude URL analysis to allow
// deep-linking.
function routeUrl(location) {
function routeUrl(location: Location) {
if (!window.matrixChat) return;
console.log("Routing URL ", location.href);
const s = getScreenFromLocation(location);
window.matrixChat.showScreen(s.screen, s.params);
(window.matrixChat as MatrixChatType).showScreen(s.screen, s.params);
}
function onHashChange(ev) {
function onHashChange(ev: HashChangeEvent) {
if (decodeURIComponent(window.location.hash) === lastLocationHashSet) {
// we just set this: no need to route it!
return;
@ -70,11 +68,16 @@ function onHashChange(ev) {
// This will be called whenever the SDK changes screens,
// so a web page can update the URL bar appropriately.
function onNewScreen(screen) {
console.log("newscreen "+screen);
function onNewScreen(screen: string, replaceLast = false) {
console.log("newscreen " + screen);
const hash = '#/' + screen;
lastLocationHashSet = hash;
window.location.hash = hash;
if (replaceLast) {
window.location.replace(hash);
} else {
window.location.assign(hash);
}
}
// We use this to work out what URL the SDK should
@ -85,11 +88,11 @@ function onNewScreen(screen) {
//
// 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 riot.im/app for now instead.
function makeRegistrationUrl(params) {
// so in that instance, hardcode to use app.element.io for now instead.
function makeRegistrationUrl(params: object) {
let url;
if (window.location.protocol === "vector:") {
url = 'https://riot.im/app/#/register';
url = 'https://app.element.io/#/register';
} else {
url = (
window.location.protocol + '//' +
@ -116,27 +119,15 @@ function onTokenLoginCompleted() {
// 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.
const parsedUrl = new URL(window.location);
parsedUrl.search = "";
const formatted = parsedUrl.toString();
console.log("Redirecting to " + formatted + " to drop loginToken from queryparams");
window.location.href = formatted;
const url = new URL(window.location.href);
url.searchParams.delete("loginToken");
console.log(`Redirecting to ${url.href} to drop loginToken from queryparams`);
window.history.replaceState(null, "", url.href);
}
export async function loadApp(fragParams: {}) {
// XXX: the way we pass the path to the worker script from webpack via html in body's dataset is a hack
// but alternatives seem to require changing the interface to passing Workers to js-sdk
const vectorIndexeddbWorkerScript = document.body.dataset.vectorIndexeddbWorkerScript;
if (!vectorIndexeddbWorkerScript) {
// If this is missing, something has probably gone wrong with
// the bundling. The js-sdk will just fall back to accessing
// indexeddb directly with no worker script, but we want to
// make sure the indexeddb script is present, so fail hard.
throw newTranslatableError(_td("Missing indexeddb worker script!"));
}
MatrixClientPeg.setIndexedDbWorkerScript(vectorIndexeddbWorkerScript);
CallHandler.setConferenceHandler(VectorConferenceHandler);
window.addEventListener('hashchange', onHashChange);
const platform = PlatformPeg.get();
@ -146,15 +137,34 @@ export async function loadApp(fragParams: {}) {
const urlWithoutQuery = window.location.protocol + '//' + window.location.host + window.location.pathname;
console.log("Vector starting at " + urlWithoutQuery);
platform.startUpdater();
(platform as VectorBasePlatform).startUpdater();
// Don't bother loading the app until the config is verified
const config = await verifyServerConfig();
// 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;
const autoRedirect = config['sso_immediate_redirect'] === true;
if (!hasPossibleToken && !isReturningFromSso && autoRedirect) {
console.log("Bypassing app load to redirect to SSO");
const tempCli = createClient({
baseUrl: config['validated_server_config'].hsUrl,
idBaseUrl: config['validated_server_config'].isUrl,
});
PlatformPeg.get().startSingleSignOn(tempCli, "sso", `/${getScreenFromLocation(window.location).screen}`);
// 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.
return;
}
const MatrixChat = sdk.getComponent('structures.MatrixChat');
return <MatrixChat
onNewScreen={onNewScreen}
makeRegistrationUrl={makeRegistrationUrl}
ConferenceHandler={VectorConferenceHandler}
config={config}
realQueryParams={params}
startingFragmentQueryParams={fragParams}
@ -234,12 +244,12 @@ async function verifyServerConfig() {
validatedConfig = AutoDiscoveryUtils.buildValidatedConfigFromDiscovery(serverName, discoveryResult, true);
} catch (e) {
const {hsUrl, isUrl, userId} = Lifecycle.getLocalStorageSessionVars();
const { hsUrl, isUrl, userId } = await Lifecycle.getStoredSessionVars();
if (hsUrl && userId) {
console.error(e);
console.warn("A session was found - suppressing config error and using the session's homeserver");
console.log("Using pre-existing hsUrl and isUrl: ", {hsUrl, isUrl});
console.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
@ -254,7 +264,7 @@ async function verifyServerConfig() {
// Add the newly built config to the actual config for use by the app
console.log("Updating SdkConfig with validated discovery information");
SdkConfig.add({"validated_server_config": validatedConfig});
SdkConfig.add({ "validated_server_config": validatedConfig });
return SdkConfig.get();
}

View file

@ -18,7 +18,7 @@ import request from 'browser-request';
// 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: string='') {
export async function getVectorConfig(relativeLocation='') {
if (relativeLocation !== '' && !relativeLocation.endsWith('/')) relativeLocation += '/';
const specificConfigPromise = getConfig(`${relativeLocation}config.${document.domain}.json`);
@ -55,7 +55,7 @@ function getConfig(configJsonFilename: string): Promise<{}> {
resolve({});
}
}
reject({err: err, response: response});
reject({ err: err, response: response });
return;
}
@ -65,7 +65,7 @@ function getConfig(configJsonFilename: string): Promise<{}> {
// loading from the filesystem (see above).
resolve(JSON.parse(body));
} catch (e) {
reject({err: e});
reject({ err: e });
}
},
);

View file

@ -2,26 +2,27 @@
<html lang="en" style="height: 100%;">
<head>
<meta charset="utf-8">
<title>Riot</title>
<link rel="apple-touch-icon" sizes="57x57" href="<%= require('../../res/vector-icons/apple-touch-icon-57x57.png') %>">
<link rel="apple-touch-icon" sizes="60x60" href="<%= require('../../res/vector-icons/apple-touch-icon-60x60.png') %>">
<link rel="apple-touch-icon" sizes="72x72" href="<%= require('../../res/vector-icons/apple-touch-icon-72x72.png') %>">
<link rel="apple-touch-icon" sizes="76x76" href="<%= require('../../res/vector-icons/apple-touch-icon-76x76.png') %>">
<link rel="apple-touch-icon" sizes="114x114" href="<%= require('../../res/vector-icons/apple-touch-icon-114x114.png') %>">
<link rel="apple-touch-icon" sizes="120x120" href="<%= require('../../res/vector-icons/apple-touch-icon-120x120.png') %>">
<link rel="apple-touch-icon" sizes="144x144" href="<%= require('../../res/vector-icons/apple-touch-icon-144x144.png') %>">
<link rel="apple-touch-icon" sizes="152x152" href="<%= require('../../res/vector-icons/apple-touch-icon-152x152.png') %>">
<link rel="apple-touch-icon" sizes="180x180" href="<%= require('../../res/vector-icons/apple-touch-icon-180x180.png') %>">
<title>Element</title>
<link rel="apple-touch-icon" sizes="57x57" href="<%= require('../../res/vector-icons/apple-touch-icon-57.png') %>">
<link rel="apple-touch-icon" sizes="60x60" href="<%= require('../../res/vector-icons/apple-touch-icon-60.png') %>">
<link rel="apple-touch-icon" sizes="72x72" href="<%= require('../../res/vector-icons/apple-touch-icon-72.png') %>">
<link rel="apple-touch-icon" sizes="76x76" href="<%= require('../../res/vector-icons/apple-touch-icon-76.png') %>">
<link rel="apple-touch-icon" sizes="114x114" href="<%= require('../../res/vector-icons/apple-touch-icon-114.png') %>">
<link rel="apple-touch-icon" sizes="120x120" href="<%= require('../../res/vector-icons/apple-touch-icon-120.png') %>">
<link rel="apple-touch-icon" sizes="144x144" href="<%= require('../../res/vector-icons/apple-touch-icon-144.png') %>">
<link rel="apple-touch-icon" sizes="152x152" href="<%= require('../../res/vector-icons/apple-touch-icon-152.png') %>">
<link rel="apple-touch-icon" sizes="180x180" href="<%= require('../../res/vector-icons/apple-touch-icon-180.png') %>">
<link rel="manifest" href="manifest.json">
<meta name="referrer" content="no-referrer">
<link rel="shortcut icon" href="<%= require('../../res/vector-icons/favicon.ico') %>">
<meta name="apple-mobile-web-app-title" content="Riot">
<meta name="application-name" content="Riot">
<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="msapplication-TileColor" content="#da532c">
<meta name="msapplication-TileImage" content="<%= require('../../res/vector-icons/mstile-144x144.png') %>">
<meta name="msapplication-TileImage" content="<%= require('../../res/vector-icons/mstile-150.png') %>">
<meta name="msapplication-config" content="<%= require('../../res/vector-icons/browserconfig.xml') %>">
<meta name="theme-color" content="#ffffff">
<meta property="og:image" content="<%= htmlWebpackPlugin.options.vars.og_image_url %>" />
<meta property="og:image" content="<%= og_image_url %>" />
<meta http-equiv="Content-Security-Policy" content="
default-src 'none';
style-src 'self' 'unsafe-inline';
@ -34,7 +35,6 @@
worker-src 'self';
frame-src * blob: data:;
form-action 'self';
object-src 'self';
manifest-src 'self';
">
<% for (var i=0; i < htmlWebpackPlugin.files.css.length; i++) {
@ -48,14 +48,34 @@
<link rel="stylesheet" href="<%= file %>">
<% }
} %>
<% 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.indexOf("Inter") !== -1) { %>
<link rel="preload" as="font" href="<%= path %>" crossorigin="anonymous"/>
<% }
} %>
</head>
<body style="height: 100%;" data-vector-indexeddb-worker-script="<%= htmlWebpackPlugin.files.chunks['indexeddb-worker'].entry %>">
<noscript>Sorry, Riot requires JavaScript to be enabled.</noscript> <!-- TODO: Translate this? -->
<section id="matrixchat" style="height: 100%; overflow: auto;"></section>
<script src="<%= htmlWebpackPlugin.files.chunks['bundle'].entry %>"></script>
<body
style="height: 100%; margin: 0;"
data-vector-recorder-worklet-script="<%= htmlWebpackPlugin.files.js.find(entry => entry.includes("recorder-worklet.js")) %>"
>
<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>
<!-- Legacy supporting Prefetch images -->
<img src="<%= require('matrix-react-sdk/res/img/warning.svg') %>" width="24" height="23" style="visibility: hidden; position: absolute; top: 0px; left: 0px;"/>
<img src="<%= require('matrix-react-sdk/res/img/e2e/warning.svg') %>" width="24" height="23" style="visibility: hidden; position: absolute; top: 0px; left: 0px;"/>
<img src="<%= require('matrix-react-sdk/res/img/feather-customised/warning-triangle.svg') %>" width="24" height="23" style="visibility: hidden; position: absolute; top: 0px; left: 0px;"/>
<img src="<%= require('matrix-react-sdk/res/img/format/bold.svg') %>" width="25" height="22" style="visibility: hidden; position: absolute; top: 0px; left: 0px;"/>
<img src="<%= require('matrix-react-sdk/res/img/format/code.svg') %>" width="25" height="22" style="visibility: hidden; position: absolute; top: 0px; left: 0px;"/>
<img src="<%= require('matrix-react-sdk/res/img/format/italics.svg') %>" width="25" height="22" style="visibility: hidden; position: absolute; top: 0px; left: 0px;"/>
<img src="<%= require('matrix-react-sdk/res/img/format/quote.svg') %>" width="25" height="22" style="visibility: hidden; position: absolute; top: 0px; left: 0px;"/>
<img src="<%= require('matrix-react-sdk/res/img/format/strikethrough.svg') %>" width="25" height="22" style="visibility: hidden; position: absolute; top: 0px; left: 0px;"/>
<audio id="messageAudio">
<source src="media/message.ogg" type="audio/ogg" />
<source src="media/message.mp3" type="audio/mpeg" />

View file

@ -23,16 +23,12 @@ limitations under the License.
// in webpack.config.js
require('gfm.css/gfm.css');
require('highlight.js/styles/github.css');
require('katex/dist/katex.css');
// 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 { parseQsFromFragment } from "./url_utils";
import './modernizr';
// load service worker if available on this platform
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('sw.js');
}
async function settled(...promises: Array<Promise<any>>) {
for (const prom of promises) {
try {
@ -49,14 +45,23 @@ function checkBrowserFeatures() {
return false;
}
// custom checks atop Modernizr because it doesn't have ES2018/ES2019 checks in it for some features we depend on,
// Modernizr requires rules to be lowercase with no punctuation:
// ES2018: http://www.ecma-international.org/ecma-262/9.0/#sec-promise.prototype.finally
// Custom checks atop Modernizr because it doesn't have ES2018/ES2019 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", () =>
window.Promise && window.Promise.prototype && typeof window.Promise.prototype.finally === "function");
// ES2019: http://www.ecma-international.org/ecma-262/10.0/#sec-object.fromentries
typeof window.Promise?.prototype?.finally === "function");
// ES2020: http://262.ecma-international.org/#sec-promise.allsettled
window.Modernizr.addTest("promiseallsettled", () =>
typeof window.Promise?.allSettled === "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", () =>
window.Object && typeof window.Object.fromEntries === "function");
typeof window.Object?.fromEntries === "function");
const featureList = Object.keys(window.Modernizr);
@ -78,21 +83,19 @@ function checkBrowserFeatures() {
return featureComplete;
}
let acceptBrowser = checkBrowserFeatures();
if (!acceptBrowser && window.localStorage) {
acceptBrowser = Boolean(window.localStorage.getItem("mx_accepts_unsupported_browser"));
}
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/vector-im/riot-web/issues/12253
// Load parallelism is based on research in https://github.com/vector-im/element-web/issues/12253
async function start() {
// load init.ts async so that its code is not executed immediately and we can catch any exceptions
const {
rageshakePromise,
setupLogStorage,
preparePlatform,
loadOlm,
loadConfig,
@ -109,21 +112,22 @@ async function start() {
"./init");
try {
await settled(rageshakePromise); // give rageshake a chance to load/fail
// 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/vector-im/riot-web/issues/7378)
// (https://github.com/vector-im/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("riot_mobile_redirect_to_guide=false") === -1) {
if (document.cookie.indexOf("element_mobile_redirect_to_guide=false") === -1) {
window.location.href = "mobile_guide/";
return;
}
@ -138,6 +142,9 @@ async function start() {
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...
@ -147,11 +154,16 @@ async function start() {
// await things settling so that any errors we have to render have features like i18n running
await settled(loadSkinPromise, loadThemePromise, loadLanguagePromise);
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(resolve => {
await new Promise<void>(resolve => {
console.error("Browser is missing required features.");
// take to a different landing page to AWOOOOOGA at the user
showIncompatibleBrowser(() => {
@ -170,9 +182,14 @@ async function start() {
} catch (error) {
// Now that we've loaded the theme (CSS), display the config syntax error if needed.
if (error.err && error.err instanceof SyntaxError) {
return showError(_t("Your Riot is misconfigured"), [
_t("Your Riot configuration contains invalid JSON. Please correct the problem and reload the page."),
_t("The message from the parser is: %(message)s", { message: error.err.message || _t("Invalid JSON")}),
// 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.err.message || _t("Invalid JSON") },
),
]);
}
return showError(_t("Unable to load config file: please refresh the page to try again."));
@ -182,26 +199,46 @@ async function start() {
// app load critical path starts here
// assert things started successfully
// ##################################
await rageshakePromise;
await loadOlmPromise;
await loadSkinPromise;
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) {
console.error(err);
// Like the compatibility page, AWOOOOOGA at the user
await showError(_t("Your Riot is misconfigured"), [
// This uses the default brand since the app config is unavailable.
await showError(_t("Your Element is misconfigured"), [
err.translatedMessage || _t("Unexpected error preparing the app. See console for details."),
]);
}
}
start().catch(err => {
console.error(err);
if (!acceptBrowser) {
// TODO redirect to static incompatible browser page
}
// show the static error in an iframe to not lose any context / console data
// with some basic styling to make the iframe full page
delete document.body.style.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);
});

View file

@ -14,8 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {IndexedDBStoreWorker} from 'matrix-js-sdk/src/indexeddb-worker.js';
import { IndexedDBStoreWorker } from 'matrix-js-sdk/src/indexeddb-worker';
const remoteWorker = new IndexedDBStoreWorker(postMessage);
const remoteWorker = new IndexedDBStoreWorker(postMessage as InstanceType<typeof Worker>["postMessage"]);
global.onmessage = remoteWorker.onMessage;

View file

@ -1,8 +1,8 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2017 Vector Creations Ltd
Copyright 2018, 2019, 2020 New Vector Ltd
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2018 - 2021 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.
@ -17,36 +17,47 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import olmWasmPath from "olm/olm.wasm";
import Olm from 'olm';
import olmWasmPath from "@matrix-org/olm/olm.wasm";
import Olm from '@matrix-org/olm';
import * as ReactDOM from "react-dom";
import * as React from "react";
import * as languageHandler from "matrix-react-sdk/src/languageHandler";
import SettingsStore from "matrix-react-sdk/src/settings/SettingsStore";
import ElectronPlatform from "./platform/ElectronPlatform";
import PWAPlatform from "./platform/PWAPlatform";
import WebPlatform from "./platform/WebPlatform";
import PlatformPeg from "matrix-react-sdk/src/PlatformPeg";
import SdkConfig from "matrix-react-sdk/src/SdkConfig";
import {setTheme} from "matrix-react-sdk/src/theme";
import { initRageshake } from "./rageshakesetup";
import { setTheme } from "matrix-react-sdk/src/theme";
import { initRageshake, initRageshakeStore } from "./rageshakesetup";
export const rageshakePromise = initRageshake();
export function preparePlatform() {
if (window.ipcRenderer) {
if (window.electron) {
console.log("Using Electron platform");
const plaf = new ElectronPlatform();
PlatformPeg.set(plaf);
PlatformPeg.set(new ElectronPlatform());
} else if (window.matchMedia('(display-mode: standalone)').matches) {
console.log("Using PWA platform");
PlatformPeg.set(new PWAPlatform());
} else {
console.log("Using Web platform");
PlatformPeg.set(new WebPlatform());
}
}
export function setupLogStorage() {
if (SdkConfig.get().bug_report_endpoint_url) {
return initRageshakeStore();
}
console.warn("No bug report endpoint set - logs will not be persisted");
return Promise.resolve();
}
export async function loadConfig() {
// 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.
@ -122,8 +133,9 @@ export async function loadSkin() {
/* webpackPreload: true */
"matrix-react-sdk"),
import(
/* webpackChunkName: "riot-web-component-index" */
/* webpackChunkName: "element-web-component-index" */
/* webpackPreload: true */
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore - this module is generated so may fail lint
"../component-index"),
]);
@ -138,7 +150,7 @@ export async function loadTheme() {
export async function loadApp(fragParams: {}) {
// load app.js async so that its code is not executed immediately and we can catch any exceptions
const module = await import(
/* webpackChunkName: "riot-web-app" */
/* webpackChunkName: "element-web-app" */
/* webpackPreload: true */
"./app");
window.matrixChat = ReactDOM.render(await module.loadApp(fragParams),
@ -148,18 +160,16 @@ export async function loadApp(fragParams: {}) {
export async function showError(title: string, messages?: string[]) {
const ErrorView = (await import(
/* webpackChunkName: "error-view" */
/* webpackPreload: true */
"../components/structures/ErrorView")).default;
"../async-components/structures/ErrorView")).default;
window.matrixChat = ReactDOM.render(<ErrorView title={title} messages={messages} />,
document.getElementById('matrixchat'));
}
export async function showIncompatibleBrowser(onAccept) {
const CompatibilityPage = (await import(
/* webpackChunkName: "compatibility-page" */
/* webpackPreload: true */
"matrix-react-sdk/src/components/structures/CompatibilityPage")).default;
window.matrixChat = ReactDOM.render(<CompatibilityPage onAccept={onAccept} />,
const CompatibilityView = (await import(
/* webpackChunkName: "compatibility-view" */
"../async-components/structures/CompatibilityView")).default;
window.matrixChat = ReactDOM.render(<CompatibilityView onAccept={onAccept} />,
document.getElementById('matrixchat'));
}

View file

@ -9,9 +9,12 @@
<div id="joinButtonContainer">
<div class="joinConferenceFloating">
<div class="joinConferencePrompt">
<span class="icon"><!-- managed by CSS --></span>
<!-- TODO: i18n -->
<h2>Jitsi Video Conference</h2>
<button type="button" id="joinButton">Join Conference</button>
<div id="widgetActionContainer">
<button type="button" id="joinButton">Join Conference</button>
</div>
</div>
</div>
</div>

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/riot-web/issues/12794
// TODO: Match the user's theme: https://github.com/vector-im/element-web/issues/12794
@font-face {
font-family: 'Nunito';
@ -23,10 +23,19 @@ limitations under the License.
src: url('~matrix-react-sdk/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: #181b21;
color: #edf3ff;
background-color: $dark-bg;
color: $dark-fg;
}
body.theme-light {
background-color: $light-bg;
color: $light-fg;
}
body, html {
@ -73,3 +82,26 @@ body, html {
background-color: #03b381;
border: 0;
}
.icon {
$icon-size: 42px;
margin-top: -$icon-size; // to visually center the form
&::before {
content: '';
background-size: contain;
background-color: $dark-fg;
mask-repeat: no-repeat;
mask-position: center;
mask-image: url("~matrix-react-sdk/res/img/element-icons/call/video-call.svg");
mask-size: $icon-size;
display: block;
width: $icon-size;
height: $icon-size;
margin: 0 auto; // center
}
}
body.theme-light .icon::before {
background-color: $light-fg;
}

View file

@ -18,12 +18,21 @@ limitations under the License.
require("./index.scss");
import { parseQs, parseQsFromFragment } from "../url_utils";
import { Capability, WidgetApi } from "matrix-react-sdk/src/widgets/WidgetApi";
import { KJUR } from 'jsrsasign';
import {
IOpenIDCredentials,
IWidgetApiRequest,
VideoConferenceCapabilities,
WidgetApi,
} from "matrix-widget-api";
import { ElementWidgetActions } from "matrix-react-sdk/src/stores/widgets/ElementWidgetActions";
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 var JitsiMeetExternalAPI: any;
declare let JitsiMeetExternalAPI: any;
let inConference = false;
@ -33,10 +42,15 @@ let conferenceId: string;
let displayName: string;
let avatarUrl: string;
let userId: string;
let jitsiAuth: string;
let roomId: string;
let openIdToken: IOpenIDCredentials;
let roomName: string;
let widgetApi: WidgetApi;
let meetApi: any; // JitsiMeetExternalAPI
(async function () {
(async function() {
try {
// The widget's options are encoded into the fragment to avoid leaking info to the server. The widget
// spec on the other hand requires the widgetId and parentUrl to show up in the regular query string.
@ -54,13 +68,33 @@ let widgetApi: WidgetApi;
// out into a browser.
const parentUrl = qsParam('parentUrl', true);
const widgetId = qsParam('widgetId', true);
const theme = qsParam('theme', true);
// Set this up as early as possible because Riot will be hitting it almost immediately.
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 readyPromise: Promise<[void, void]>;
if (parentUrl && widgetId) {
widgetApi = new WidgetApi(qsParam('parentUrl'), qsParam('widgetId'), [
Capability.AlwaysOnScreen,
const parentOrigin = new URL(qsParam('parentUrl')).origin;
widgetApi = new WidgetApi(qsParam("widgetId"), parentOrigin);
widgetApi.requestCapabilities(VideoConferenceCapabilities);
readyPromise = Promise.all([
new Promise<void>(resolve => {
widgetApi.once(`action:${ElementWidgetActions.ClientReady}`, ev => {
ev.preventDefault();
widgetApi.transport.reply(ev.detail, {});
resolve();
});
}),
new Promise<void>(resolve => {
widgetApi.once("ready", () => resolve());
}),
]);
widgetApi.expectingExplicitReady = true;
widgetApi.start();
} else {
console.warn("No parent URL or no widget ID - assuming no widget API is available");
}
// Populate the Jitsi params now
@ -69,40 +103,130 @@ let widgetApi: WidgetApi;
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);
if (widgetApi) {
await widgetApi.waitReady();
await readyPromise;
await widgetApi.setAlwaysOnScreen(false); // start off as detachable from the screen
// See https://github.com/matrix-org/prosody-mod-auth-matrix-user-verification
if (jitsiAuth === JITSI_OPENIDTOKEN_JWT_AUTH) {
// Request credentials, give callback to continue when received
openIdToken = await widgetApi.requestOpenIDConnectToken();
console.log("Got OpenID Connect token");
}
// TODO: register widgetApi listeners for PTT controls (https://github.com/vector-im/element-web/issues/12795)
widgetApi.on(`action:${ElementWidgetActions.HangupCall}`,
(ev: CustomEvent<IWidgetApiRequest>) => {
if (meetApi) meetApi.executeCommand('hangup');
widgetApi.transport.reply(ev.detail, {}); // ack
},
);
widgetApi.on(`action:${ElementWidgetActions.StartLiveStream}`,
(ev: CustomEvent<IWidgetApiRequest>) => {
if (meetApi) {
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: ev.detail.data.rtmpStreamKey,
youtubeStreamKey: ev.detail.data.rtmpStreamKey,
});
widgetApi.transport.reply(ev.detail, {}); // ack
} else {
widgetApi.transport.reply(ev.detail, { error: { message: "Conference not joined" } });
}
},
);
}
// TODO: register widgetApi listeners for PTT controls (https://github.com/vector-im/riot-web/issues/12795)
document.getElementById("joinButton").onclick = () => joinConference();
enableJoinButton(); // always enable the button
} catch (e) {
console.error("Error setting up Jitsi widget", e);
document.getElementById("jitsiContainer").innerText = "Failed to load Jitsi widget";
switchVisibleContainers();
document.getElementById("widgetActionContainer").innerText = "Failed to load Jitsi widget";
}
})();
function enableJoinButton() {
document.getElementById("joinButton").onclick = () => joinConference();
}
function switchVisibleContainers() {
inConference = !inConference;
document.getElementById("jitsiContainer").style.visibility = inConference ? 'unset' : 'hidden';
document.getElementById("joinButtonContainer").style.visibility = inConference ? 'hidden' : 'unset';
}
/**
* Create a JWT token fot jitsi openidtoken-jwt auth
*
* See https://github.com/matrix-org/prosody-mod-auth-matrix-user-verification
*/
function createJWTToken() {
// 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',
);
}
function joinConference() { // event handler bound in HTML
let jwt;
if (jitsiAuth === JITSI_OPENIDTOKEN_JWT_AUTH) {
if (!openIdToken?.access_token) { // eslint-disable-line camelcase
// We've failing to get a token, don't try to init conference
console.warn('Expected to have an OpenID credential, cannot initialize widget.');
document.getElementById("widgetActionContainer").innerText = "Failed to load Jitsi widget";
return;
}
jwt = createJWTToken();
}
switchVisibleContainers();
// noinspection JSIgnoredPromiseFromCall
if (widgetApi) widgetApi.setAlwaysOnScreen(true); // ignored promise because we don't care if it works
if (widgetApi) {
// ignored promise because we don't care if it works
// noinspection JSIgnoredPromiseFromCall
widgetApi.setAlwaysOnScreen(true);
}
console.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 meetApi = new JitsiMeetExternalAPI(jitsiDomain, {
const options = {
width: "100%",
height: "100%",
parentNode: document.querySelector("#jitsiContainer"),
@ -113,17 +237,25 @@ function joinConference() { // event handler bound in HTML
MAIN_TOOLBAR_BUTTONS: [],
VIDEO_LAYOUT_FIT: "height",
},
});
jwt: jwt,
};
meetApi = new JitsiMeetExternalAPI(jitsiDomain, options);
if (displayName) meetApi.executeCommand("displayName", displayName);
if (avatarUrl) meetApi.executeCommand("avatarUrl", avatarUrl);
if (userId) meetApi.executeCommand("email", userId);
if (roomName) meetApi.executeCommand("subject", roomName);
meetApi.on("readyToClose", () => {
switchVisibleContainers();
// noinspection JSIgnoredPromiseFromCall
if (widgetApi) widgetApi.setAlwaysOnScreen(false); // ignored promise because we don't care if it works
if (widgetApi) {
// ignored promise because we don't care if it works
// noinspection JSIgnoredPromiseFromCall
widgetApi.setAlwaysOnScreen(false);
}
document.getElementById("jitsiContainer").innerHTML = "";
meetApi = null;
});
}

View file

@ -10,11 +10,7 @@
}
body {
background: #c5e0f7;
background: -moz-linear-gradient(top, #c5e0f7 0%, #ffffff 100%);
background: -webkit-linear-gradient(top, #c5e0f7 0%,#ffffff 100%);
background: linear-gradient(to bottom, #c5e0f7 0%,#ffffff 100%);
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#c5e0f7', endColorstr='#ffffff',GradientType=0 );
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";
@ -100,7 +96,7 @@ body {
}
.mx_FooterLink {
color: #368BD6;
color: #0dbd8b;
text-decoration: none;
}
@ -151,25 +147,21 @@ body {
<div class="mx_HomePage_container">
<div class="mx_HomePage_header">
<span class="mx_HomePage_logo">
<svg width="34px" height="42px" viewBox="0 0 34 42" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="blue" transform="translate(1.000000, 1.000000)">
<path d="M11.6756011,11.3799998 L11.6756011,17.149751 L17.5348252,17.1437188 C17.6092313,17.1437188 17.6771226,17.1417081 17.744671,17.1373516 C19.2852547,17.0358104 20.4894678,15.7727453 20.4894678,14.2620269 C20.4894678,12.6725555 19.1669592,11.3799998 17.5406542,11.3799998 L11.6756011,11.3799998 Z M5.88083947,39.8778856 C2.68069092,39.8778856 0.0860778277,37.342372 0.0860778277,34.2143673 L0.0860778277,23.4205057 C0.0651618241,23.2247959 0.0538388139,23.025735 0.0538388139,22.8243282 C0.0535037238,22.6192351 0.0644760535,22.416823 0.0860778277,22.2170918 L0.0860778277,5.71648153 C0.0860778277,2.58847681 2.68069092,0.0526281121 5.88083947,0.0526281121 L17.5406542,0.0526281121 C25.5573126,0.0526281121 32.079334,6.42693471 32.079334,14.2620269 C32.079334,21.7113966 26.1261594,27.9382507 18.5264495,28.4382489 C18.2034515,28.4600316 17.8705099,28.4710906 17.5406542,28.4710906 L11.6756011,28.4767876 L11.6756011,34.2143673 C11.6756011,37.342372 9.08133089,39.8778856 5.88083947,39.8778856 L5.88083947,39.8778856 Z" id="Fill-1" fill="#A2DDEF"></path>
<path d="M11.6756011,11.3799998 L11.6756011,17.149751 L17.5348252,17.1437188 C17.6092313,17.1437188 17.6771226,17.1417081 17.744671,17.1373516 C19.2852547,17.0358104 20.4894678,15.7727453 20.4894678,14.2620269 C20.4894678,12.6725555 19.1669592,11.3799998 17.5406542,11.3799998 L11.6756011,11.3799998 Z M5.88083947,39.8778856 C2.68069092,39.8778856 0.0860778277,37.342372 0.0860778277,34.2143673 L0.0860778277,23.4205057 C0.0651618241,23.2247959 0.0538388139,23.025735 0.0538388139,22.8243282 C0.0535037238,22.6192351 0.0644760535,22.416823 0.0860778277,22.2170918 L0.0860778277,5.71648153 C0.0860778277,2.58847681 2.68069092,0.0526281121 5.88083947,0.0526281121 L17.5406542,0.0526281121 C25.5573126,0.0526281121 32.079334,6.42693471 32.079334,14.2620269 C32.079334,21.7113966 26.1261594,27.9382507 18.5264495,28.4382489 C18.2034515,28.4600316 17.8705099,28.4710906 17.5406542,28.4710906 L11.6756011,28.4767876 L11.6756011,34.2143673 C11.6756011,37.342372 9.08133089,39.8778856 5.88083947,39.8778856 Z" id="Stroke-3" stroke="#368BD6" stroke-width="1.02344117"></path>
<path d="M5.88087375,34.2142667 L5.88087375,5.716381 L17.5406885,5.716381 C22.3695423,5.716381 26.2842638,9.54243948 26.2842638,14.2619264 C26.2842638,18.7857035 22.6877398,22.488438 18.1373089,22.7876997 C17.939807,22.8007693 17.7412764,22.8074717 17.5406885,22.8074717 L5.84864254,22.8188658" id="Stroke-5" stroke="#368BD6" stroke-width="1.02344117" stroke-linecap="round"></path>
<path d="M22.882533,19.5375774 L31.0723484,30.9604582 C32.909185,33.5221111 32.2731328,37.0539347 29.6524604,38.8484992 C28.640263,39.5418613 27.480282,39.8746349 26.3319591,39.8746349 C24.505752,39.8746349 22.709033,39.0334852 21.5812832,37.4607697 L13.3914677,26.0378889 C11.5549741,23.476236 12.1910263,19.9444124 14.8116987,18.1498479 C17.432371,16.3539429 21.0460393,16.9759245 22.882533,19.5375774 Z M10.6558259,2.46823596 C11.5442417,3.70717248 11.8854126,5.21051822 11.6165905,6.69945383 C11.3474256,8.1893948 10.5004989,9.48731234 9.23182325,10.3549365 C6.61252242,12.1461499 2.98925341,11.5224926 1.15515992,8.96452603 C0.266744094,7.72558951 -0.0744267831,6.22257889 0.194738181,4.73297304 C0.463560259,3.24336719 1.31048696,1.94511453 2.57950547,1.07782546 C5.19880631,-0.713387872 8.82173243,-0.08973062 10.6558259,2.46823596 Z" id="Combined-Shape" fill="#368BD6"></path>
</g>
</g>
</svg>
<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>
<p>Set up Riot on iOS or Android</p>
<p>Set up Element on iOS or Android</p>
</div>
<div class="mx_HomePage_col">
<div class="mx_HomePage_row">
<div>
<h2 id="step1_heading">Install the app</h2>
<p><strong>iOS</strong> (iPhone or iPad)</p>
<a href="https://itunes.apple.com/app/riot-im/id1083446067?mt=8" target="_blank" class="mx_ClearDecoration">
<a href="https://apps.apple.com/app/vector/id1083446067" target="_blank" class="mx_ClearDecoration">
<svg width="144px" height="48px" viewBox="0 0 120 40" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<desc>Download on the App Store.</desc>
<defs></defs>
@ -268,7 +260,8 @@ body {
</g>
</g>
</svg>
<a href="https://f-droid.org/repository/browse/?fdid=im.vector.alpha" target="_blank" class="mx_ClearDecoration">
</a>
<a href="https://f-droid.org/repository/browse/?fdid=im.vector.app" target="_blank" class="mx_ClearDecoration">
<svg width="164px" height="48px" viewBox="0 0 157 46" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<desc>Get it on F-Droid.</desc>
<defs>
@ -330,16 +323,16 @@ body {
<div class="mx_HomePage_row">
<div>
<h2>2: Configure your app</h2>
<a class="mx_Button" id="configure_riot_button" href="#">Configure</a>
<a class="mx_Button" id="configure_element_button" href="#">Configure</a>
<p class="mx_Subtext mx_SubtextTop">Tap the button above, or manually enable <em>Use custom server</em> and enter:</p>
<p class="mx_Subtext">Homeserver: <em id="hs_url"></em></p>
<p class="mx_Subtext" id="custom_is">Identity Server: <em id="is_url"></em></p>
<p class="mx_Subtext" id="custom_is">Identity server: <em id="is_url"></em></p>
</div>
</div>
</div>
<div class="mx_HomePage_row mx_Center mx_Spacer">
<p class="mx_Spacer">
<a id="back_to_riot_button" href="#" class="mx_FooterLink">
<a id="back_to_element_button" href="#" class="mx_FooterLink">
Go to Desktop Site
</a>
</p>

View file

@ -1,19 +1,21 @@
import {getVectorConfig} from '../getconfig';
import { getVectorConfig } from '../getconfig';
function onBackToRiotClick() {
function onBackToElementClick(): void {
// Cookie should expire in 4 hours
document.cookie = 'riot_mobile_redirect_to_guide=false;path=/;max-age=14400';
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) {
function renderConfigError(message: string): void {
const contactMsg = "If this is unexpected, please contact your system administrator " +
"or technical support representative.";
message = `<h2>Error loading Riot</h2><p>${message}</p><p>${contactMsg}</p>`;
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");
const errorContainers = document.getElementsByClassName(
"mx_HomePage_errorContainer",
) as HTMLCollectionOf<HTMLDialogElement>;
for (const e of toHide) {
// We have to clear the content because .style.display='none'; doesn't work
@ -26,10 +28,10 @@ function renderConfigError(message) {
}
}
async function initPage() {
document.getElementById('back_to_riot_button').onclick = onBackToRiotClick;
async function initPage(): Promise<void> {
document.getElementById('back_to_element_button').onclick = onBackToElementClick;
let config = await getVectorConfig('..');
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.
@ -92,8 +94,8 @@ async function initPage() {
if (isUrl && !isUrl.endsWith('/')) isUrl += '/';
if (hsUrl !== 'https://matrix.org/') {
document.getElementById('configure_riot_button').href =
"https://riot.im/config/config?hs_url=" + encodeURIComponent(hsUrl) +
(document.getElementById('configure_element_button') as HTMLAnchorElement).href =
"https://mobile.element.io?hs_url=" + encodeURIComponent(hsUrl) +
"&is_url=" + encodeURIComponent(isUrl);
document.getElementById('step1_heading').innerHTML= '1: Install the app';
document.getElementById('step2_container').style.display = 'block';

File diff suppressed because one or more lines are too long

View file

@ -1,437 +0,0 @@
// @flow
/*
Copyright 2016 Aviral Dasgupta
Copyright 2016 OpenMarket Ltd
Copyright 2018 New Vector Ltd
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import VectorBasePlatform, {updateCheckStatusEnum} from './VectorBasePlatform';
import BaseEventIndexManager from 'matrix-react-sdk/src/indexing/BaseEventIndexManager';
import dis from 'matrix-react-sdk/src/dispatcher';
import { _t, _td } from 'matrix-react-sdk/src/languageHandler';
import * as rageshake from 'matrix-react-sdk/src/rageshake/rageshake';
import {MatrixClient} from "matrix-js-sdk";
import Modal from "matrix-react-sdk/src/Modal";
import InfoDialog from "matrix-react-sdk/src/components/views/dialogs/InfoDialog";
import Spinner from "matrix-react-sdk/src/components/views/elements/Spinner";
import {Categories, Modifiers, registerShortcut} from "matrix-react-sdk/src/accessibility/KeyboardShortcuts";
import {Key} from "matrix-react-sdk/src/Keyboard";
import React from "react";
const ipcRenderer = window.ipcRenderer;
const isMac = navigator.platform.toUpperCase().includes('MAC');
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 _onAction(payload: Object) {
// Whitelist payload actions, no point sending most across
if (['call_state'].includes(payload.action)) {
ipcRenderer.send('app_onAction', payload);
}
}
function getUpdateCheckStatus(status) {
if (status === true) {
return { status: updateCheckStatusEnum.DOWNLOADING };
} else if (status === false) {
return { status: updateCheckStatusEnum.NOTAVAILABLE };
} else {
return {
status: updateCheckStatusEnum.ERROR,
detail: status,
};
}
}
class SeshatIndexManager extends BaseEventIndexManager {
constructor() {
super();
this._pendingIpcCalls = {};
this._nextIpcCallId = 0;
ipcRenderer.on('seshatReply', this._onIpcReply.bind(this));
}
async _ipcCall(name: string, ...args: []): Promise<{}> {
// TODO this should be moved into the preload.js file.
const ipcCallId = ++this._nextIpcCallId;
return new Promise((resolve, reject) => {
this._pendingIpcCalls[ipcCallId] = {resolve, reject};
window.ipcRenderer.send('seshat', {id: ipcCallId, name, args});
});
}
_onIpcReply(ev: {}, payload: {}) {
if (payload.id === undefined) {
console.warn("Ignoring IPC reply with no ID");
return;
}
if (this._pendingIpcCalls[payload.id] === undefined) {
console.warn("Unknown IPC payload ID: " + payload.id);
return;
}
const callbacks = this._pendingIpcCalls[payload.id];
delete this._pendingIpcCalls[payload.id];
if (payload.error) {
callbacks.reject(payload.error);
} else {
callbacks.resolve(payload.reply);
}
}
async supportsEventIndexing(): Promise<boolean> {
return this._ipcCall('supportsEventIndexing');
}
async initEventIndex(): Promise<> {
return this._ipcCall('initEventIndex');
}
async addEventToIndex(ev: MatrixEvent, profile: MatrixProfile): Promise<> {
return this._ipcCall('addEventToIndex', ev, profile);
}
async deleteEvent(eventId: string): Promise<boolean> {
return this._ipcCall('deleteEvent', eventId);
}
async isEventIndexEmpty(): Promise<boolean> {
return this._ipcCall('isEventIndexEmpty');
}
async commitLiveEvents(): Promise<> {
return this._ipcCall('commitLiveEvents');
}
async searchEventIndex(searchConfig: SearchConfig): Promise<SearchResult> {
return this._ipcCall('searchEventIndex', searchConfig);
}
async addHistoricEvents(
events: [HistoricEvent],
checkpoint: CrawlerCheckpoint | null,
oldCheckpoint: CrawlerCheckpoint | null,
): Promise<> {
return this._ipcCall('addHistoricEvents', events, checkpoint, oldCheckpoint);
}
async addCrawlerCheckpoint(checkpoint: CrawlerCheckpoint): Promise<> {
return this._ipcCall('addCrawlerCheckpoint', checkpoint);
}
async removeCrawlerCheckpoint(checkpoint: CrawlerCheckpoint): Promise<> {
return this._ipcCall('removeCrawlerCheckpoint', checkpoint);
}
async loadFileEvents(args): Promise<[EventAndProfile]> {
return this._ipcCall('loadFileEvents', args);
}
async loadCheckpoints(): Promise<[CrawlerCheckpoint]> {
return this._ipcCall('loadCheckpoints');
}
async closeEventIndex(): Promise<> {
return this._ipcCall('closeEventIndex');
}
async getStats(): Promise<> {
return this._ipcCall('getStats');
}
async deleteEventIndex(): Promise<> {
return this._ipcCall('deleteEventIndex');
}
}
export default class ElectronPlatform extends VectorBasePlatform {
constructor() {
super();
this._pendingIpcCalls = {};
this._nextIpcCallId = 0;
this.eventIndexManager = new SeshatIndexManager();
dis.register(_onAction);
/*
IPC Call `check_updates` returns:
true if there is an update available
false if there is not
or the error if one is encountered
*/
ipcRenderer.on('check_updates', (event, status) => {
if (!this.showUpdateCheck) return;
dis.dispatch({
action: 'check_updates',
value: getUpdateCheckStatus(status),
});
this.showUpdateCheck = false;
});
// try to flush the rageshake logs to indexeddb before quit.
ipcRenderer.on('before-quit', function() {
console.log('riot-desktop closing');
rageshake.flush();
});
ipcRenderer.on('ipcReply', this._onIpcReply.bind(this));
ipcRenderer.on('update-downloaded', this.onUpdateDownloaded.bind(this));
ipcRenderer.on('preferences', () => {
dis.dispatch({ action: 'view_user_settings' });
});
this.startUpdateCheck = this.startUpdateCheck.bind(this);
this.stopUpdateCheck = this.stopUpdateCheck.bind(this);
// register Mac specific shortcuts
if (isMac) {
registerShortcut(Categories.NAVIGATION, {
keybinds: [{
modifiers: [Modifiers.COMMAND],
key: Key.COMMA,
}],
description: _td("Open user settings"),
});
}
}
async getConfig(): Promise<{}> {
return this._ipcCall('getConfig');
}
async onUpdateDownloaded(ev, updateInfo) {
dis.dispatch({
action: 'new_version',
currentVersion: await this.getAppVersion(),
newVersion: updateInfo,
releaseNotes: updateInfo.releaseNotes,
});
}
getHumanReadableName(): string {
return 'Electron Platform'; // no translation required: only used for analytics
}
setNotificationCount(count: number) {
if (this.notificationCount === count) return;
super.setNotificationCount(count);
ipcRenderer.send('setBadgeCount', count);
}
supportsNotifications(): boolean {
return true;
}
maySendNotifications(): boolean {
return true;
}
displayNotification(title: string, msg: string, avatarUrl: string, room: Object): 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;');
}
// Notifications in Electron use the HTML5 notification API
const notifBody = {
body: msg,
silent: true, // we play our own sounds
};
if (avatarUrl) notifBody['icon'] = avatarUrl;
const notification = new global.Notification(title, notifBody);
notification.onclick = () => {
dis.dispatch({
action: 'view_room',
room_id: room.roomId,
});
global.focus();
this._ipcCall('focusWindow');
};
return notification;
}
loudNotification(ev: Event, room: Object) {
ipcRenderer.send('loudNotification');
}
clearNotification(notif: Notification) {
notif.close();
}
async getAppVersion(): Promise<string> {
return this._ipcCall('getAppVersion');
}
supportsAutoLaunch(): boolean {
return true;
}
async getAutoLaunchEnabled(): boolean {
return this._ipcCall('getAutoLaunchEnabled');
}
async setAutoLaunchEnabled(enabled: boolean): void {
return this._ipcCall('setAutoLaunchEnabled', enabled);
}
supportsAutoHideMenuBar(): boolean {
// This is irelevant on Mac as Menu bars don't live in the app window
return !isMac;
}
async getAutoHideMenuBarEnabled(): boolean {
return this._ipcCall('getAutoHideMenuBarEnabled');
}
async setAutoHideMenuBarEnabled(enabled: boolean): void {
return this._ipcCall('setAutoHideMenuBarEnabled', enabled);
}
supportsMinimizeToTray(): boolean {
// Things other than Mac support tray icons
return !isMac;
}
async getMinimizeToTrayEnabled(): boolean {
return this._ipcCall('getMinimizeToTrayEnabled');
}
async setMinimizeToTrayEnabled(enabled: boolean): void {
return this._ipcCall('setMinimizeToTrayEnabled', enabled);
}
async canSelfUpdate(): boolean {
const feedUrl = await this._ipcCall('getUpdateFeedUrl');
return Boolean(feedUrl);
}
startUpdateCheck() {
if (this.showUpdateCheck) return;
super.startUpdateCheck();
ipcRenderer.send('check_updates');
}
installUpdate() {
// 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.
ipcRenderer.send('install_update');
}
getDefaultDeviceDisplayName(): string {
return _t('Riot Desktop on %(platformName)s', { platformName: platformFriendlyName() });
}
screenCaptureErrorString(): ?string {
return null;
}
requestNotificationPermission(): Promise<string> {
return Promise.resolve('granted');
}
reload() {
// we used to remote to the main process to get it to
// reload the webcontents, but in practice this is unnecessary:
// the normal way works fine.
window.location.reload(false);
}
async _ipcCall(name, ...args) {
const ipcCallId = ++this._nextIpcCallId;
return new Promise((resolve, reject) => {
this._pendingIpcCalls[ipcCallId] = {resolve, reject};
window.ipcRenderer.send('ipcCall', {id: ipcCallId, name, args});
// Maybe add a timeout to these? Probably not necessary.
});
}
_onIpcReply(ev, payload) {
if (payload.id === undefined) {
console.warn("Ignoring IPC reply with no ID");
return;
}
if (this._pendingIpcCalls[payload.id] === undefined) {
console.warn("Unknown IPC payload ID: " + payload.id);
return;
}
const callbacks = this._pendingIpcCalls[payload.id];
delete this._pendingIpcCalls[payload.id];
if (payload.error) {
callbacks.reject(payload.error);
} else {
callbacks.resolve(payload.reply);
}
}
getEventIndexingManager(): BaseEventIndexManager | null {
return this.eventIndexManager;
}
setLanguage(preferredLangs: string[]) {
this._ipcCall('setLanguage', preferredLangs).catch(error => {
console.log("Failed to send setLanguage IPC to Electron");
console.error(error);
});
}
getSSOCallbackUrl(hsUrl: string, isUrl: string): URL {
const url = super.getSSOCallbackUrl(hsUrl, isUrl);
url.protocol = "riot";
return url;
}
startSingleSignOn(mxClient: MatrixClient, loginType: "sso" | "cas") {
super.startSingleSignOn(mxClient, loginType); // this will get intercepted by electron-main will-navigate
Modal.createTrackedDialog('Electron', 'SSO', InfoDialog, {
title: _t("Go to your browser to complete Sign In"),
description: <Spinner />,
});
}
}

View file

@ -0,0 +1,634 @@
/*
Copyright 2016 Aviral Dasgupta
Copyright 2016 OpenMarket Ltd
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2018 - 2021 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.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { UpdateCheckStatus } from "matrix-react-sdk/src/BasePlatform";
import BaseEventIndexManager, {
ICrawlerCheckpoint,
IEventAndProfile,
IIndexStats,
ISearchArgs,
} from 'matrix-react-sdk/src/indexing/BaseEventIndexManager';
import dis from 'matrix-react-sdk/src/dispatcher/dispatcher';
import { _t, _td } from 'matrix-react-sdk/src/languageHandler';
import SdkConfig from 'matrix-react-sdk/src/SdkConfig';
import * as rageshake from 'matrix-react-sdk/src/rageshake/rageshake';
import { MatrixClient } from "matrix-js-sdk/src/client";
import { Room } from "matrix-js-sdk/src/models/room";
import Modal from "matrix-react-sdk/src/Modal";
import InfoDialog from "matrix-react-sdk/src/components/views/dialogs/InfoDialog";
import Spinner from "matrix-react-sdk/src/components/views/elements/Spinner";
import {
Categories,
CMD_OR_CTRL,
DIGITS,
Modifiers,
registerShortcut,
} from "matrix-react-sdk/src/accessibility/KeyboardShortcuts";
import { isOnlyCtrlOrCmdKeyEvent, Key } from "matrix-react-sdk/src/Keyboard";
import React from "react";
import { randomString } from "matrix-js-sdk/src/randomstring";
import { Action } from "matrix-react-sdk/src/dispatcher/actions";
import { ActionPayload } from "matrix-react-sdk/src/dispatcher/payloads";
import { SwitchSpacePayload } from "matrix-react-sdk/src/dispatcher/payloads/SwitchSpacePayload";
import { showToast as showUpdateToast } from "matrix-react-sdk/src/toasts/UpdateToast";
import { CheckUpdatesPayload } from "matrix-react-sdk/src/dispatcher/payloads/CheckUpdatesPayload";
import ToastStore from "matrix-react-sdk/src/stores/ToastStore";
import GenericExpiringToast from "matrix-react-sdk/src/components/views/toasts/GenericExpiringToast";
import SettingsStore from 'matrix-react-sdk/src/settings/SettingsStore';
import { IMatrixProfile, IEventWithRoomId as IMatrixEvent, IResultRoomEvents } from "matrix-js-sdk/src/@types/search";
import VectorBasePlatform from './VectorBasePlatform';
const electron = window.electron;
const isMac = navigator.platform.toUpperCase().includes('MAC');
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 _onAction(payload: ActionPayload) {
// Whitelist payload actions, no point sending most across
if (['call_state'].includes(payload.action)) {
electron.send('app_onAction', payload);
}
}
function getUpdateCheckStatus(status: boolean | string) {
if (status === true) {
return { status: UpdateCheckStatus.Downloading };
} else if (status === false) {
return { status: UpdateCheckStatus.NotAvailable };
} else {
return {
status: UpdateCheckStatus.Error,
detail: status,
};
}
}
interface IPCPayload {
id?: number;
error?: string;
reply?: any;
}
class SeshatIndexManager extends BaseEventIndexManager {
private pendingIpcCalls: Record<number, { resolve, reject }> = {};
private nextIpcCallId = 0;
constructor() {
super();
electron.on('seshatReply', this._onIpcReply);
}
async _ipcCall(name: string, ...args: any[]): Promise<any> {
// TODO this should be moved into the preload.js file.
const ipcCallId = ++this.nextIpcCallId;
return new Promise((resolve, reject) => {
this.pendingIpcCalls[ipcCallId] = { resolve, reject };
window.electron.send('seshat', { id: ipcCallId, name, args });
});
}
_onIpcReply = (ev: {}, payload: IPCPayload) => {
if (payload.id === undefined) {
console.warn("Ignoring IPC reply with no ID");
return;
}
if (this.pendingIpcCalls[payload.id] === undefined) {
console.warn("Unknown IPC payload ID: " + payload.id);
return;
}
const callbacks = this.pendingIpcCalls[payload.id];
delete this.pendingIpcCalls[payload.id];
if (payload.error) {
callbacks.reject(payload.error);
} else {
callbacks.resolve(payload.reply);
}
};
async supportsEventIndexing(): Promise<boolean> {
return this._ipcCall('supportsEventIndexing');
}
async initEventIndex(userId: string, deviceId: string): Promise<void> {
return this._ipcCall('initEventIndex', userId, deviceId);
}
async addEventToIndex(ev: IMatrixEvent, profile: IMatrixProfile): Promise<void> {
return this._ipcCall('addEventToIndex', ev, profile);
}
async deleteEvent(eventId: string): Promise<boolean> {
return this._ipcCall('deleteEvent', eventId);
}
async isEventIndexEmpty(): Promise<boolean> {
return this._ipcCall('isEventIndexEmpty');
}
async isRoomIndexed(roomId: string): Promise<boolean> {
return this._ipcCall('isRoomIndexed', roomId);
}
async commitLiveEvents(): Promise<void> {
return this._ipcCall('commitLiveEvents');
}
async searchEventIndex(searchConfig: ISearchArgs): Promise<IResultRoomEvents> {
return this._ipcCall('searchEventIndex', searchConfig);
}
async addHistoricEvents(
events: IEventAndProfile[],
checkpoint: ICrawlerCheckpoint | null,
oldCheckpoint: ICrawlerCheckpoint | null,
): Promise<boolean> {
return this._ipcCall('addHistoricEvents', events, checkpoint, oldCheckpoint);
}
async addCrawlerCheckpoint(checkpoint: ICrawlerCheckpoint): Promise<void> {
return this._ipcCall('addCrawlerCheckpoint', checkpoint);
}
async removeCrawlerCheckpoint(checkpoint: ICrawlerCheckpoint): Promise<void> {
return this._ipcCall('removeCrawlerCheckpoint', checkpoint);
}
async loadFileEvents(args): Promise<IEventAndProfile[]> {
return this._ipcCall('loadFileEvents', args);
}
async loadCheckpoints(): Promise<ICrawlerCheckpoint[]> {
return this._ipcCall('loadCheckpoints');
}
async closeEventIndex(): Promise<void> {
return this._ipcCall('closeEventIndex');
}
async getStats(): Promise<IIndexStats> {
return this._ipcCall('getStats');
}
async getUserVersion(): Promise<number> {
return this._ipcCall('getUserVersion');
}
async setUserVersion(version: number): Promise<void> {
return this._ipcCall('setUserVersion', version);
}
async deleteEventIndex(): Promise<void> {
return this._ipcCall('deleteEventIndex');
}
}
export default class ElectronPlatform extends VectorBasePlatform {
private eventIndexManager: BaseEventIndexManager = new SeshatIndexManager();
private pendingIpcCalls: Record<number, { resolve, reject }> = {};
private nextIpcCallId = 0;
// this is the opaque token we pass to the HS which when we get it in our callback we can resolve to a profile
private ssoID: string = randomString(32);
constructor() {
super();
dis.register(_onAction);
/*
IPC Call `check_updates` returns:
true if there is an update available
false if there is not
or the error if one is encountered
*/
electron.on('check_updates', (event, status) => {
dis.dispatch<CheckUpdatesPayload>({
action: Action.CheckUpdates,
...getUpdateCheckStatus(status),
});
});
// try to flush the rageshake logs to indexeddb before quit.
electron.on('before-quit', function() {
console.log('element-desktop closing');
rageshake.flush();
});
electron.on('ipcReply', this._onIpcReply);
electron.on('update-downloaded', this.onUpdateDownloaded);
electron.on('preferences', () => {
dis.fire(Action.ViewUserSettings);
});
electron.on('userDownloadCompleted', (ev, { path, name }) => {
const onAccept = () => {
electron.send('userDownloadOpen', { path });
};
ToastStore.sharedInstance().addOrReplaceToast({
key: `DOWNLOAD_TOAST_${path}`,
title: _t("Download Completed"),
props: {
description: name,
acceptLabel: _t("Open"),
onAccept,
dismissLabel: _t("Dismiss"),
numSeconds: 10,
},
component: GenericExpiringToast,
priority: 99,
});
});
// register OS-specific shortcuts
registerShortcut(Categories.NAVIGATION, {
keybinds: [{
modifiers: [CMD_OR_CTRL],
key: DIGITS,
}],
description: _td("Switch to space by number"),
});
if (isMac) {
registerShortcut(Categories.NAVIGATION, {
keybinds: [{
modifiers: [Modifiers.COMMAND],
key: Key.COMMA,
}],
description: _td("Open user settings"),
});
registerShortcut(Categories.NAVIGATION, {
keybinds: [{
modifiers: [Modifiers.COMMAND],
key: Key.SQUARE_BRACKET_LEFT,
}, {
modifiers: [Modifiers.COMMAND],
key: Key.SQUARE_BRACKET_RIGHT,
}],
description: _td("Previous/next recently visited room or community"),
});
} else {
registerShortcut(Categories.NAVIGATION, {
keybinds: [{
modifiers: [Modifiers.ALT],
key: Key.ARROW_LEFT,
}, {
modifiers: [Modifiers.ALT],
key: Key.ARROW_RIGHT,
}],
description: _td("Previous/next recently visited room or community"),
});
}
this._ipcCall("startSSOFlow", this.ssoID);
}
async getConfig(): Promise<{}> {
return this._ipcCall('getConfig');
}
onUpdateDownloaded = async (ev, { releaseNotes, releaseName }) => {
dis.dispatch<CheckUpdatesPayload>({
action: Action.CheckUpdates,
status: UpdateCheckStatus.Ready,
});
if (this.shouldShowUpdate(releaseName)) {
showUpdateToast(await this.getAppVersion(), releaseName, releaseNotes);
}
};
getHumanReadableName(): string {
return 'Electron Platform'; // no translation required: only used for analytics
}
/**
* Return true if platform supports multi-language
* spell-checking, otherwise false.
*/
supportsMultiLanguageSpellCheck(): boolean {
// Electron uses OS spell checking on macOS, so no need for in-app options
if (isMac) return false;
return true;
}
setNotificationCount(count: number) {
if (this.notificationCount === count) return;
super.setNotificationCount(count);
electron.send('setBadgeCount', count);
}
supportsNotifications(): boolean {
return true;
}
maySendNotifications(): boolean {
return true;
}
displayNotification(title: string, msg: string, avatarUrl: string, room: Room): 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;');
}
// Notifications in Electron use the HTML5 notification API
const notifBody = {
body: msg,
silent: true, // we play our own sounds
};
if (avatarUrl) notifBody['icon'] = avatarUrl;
const notification = new window.Notification(title, notifBody);
notification.onclick = () => {
dis.dispatch({
action: 'view_room',
room_id: room.roomId,
});
window.focus();
this._ipcCall('focusWindow');
};
return notification;
}
loudNotification(ev: Event, room: Object) {
electron.send('loudNotification');
}
async getAppVersion(): Promise<string> {
return this._ipcCall('getAppVersion');
}
supportsAutoLaunch(): boolean {
return true;
}
async getAutoLaunchEnabled(): Promise<boolean> {
return this._ipcCall('getAutoLaunchEnabled');
}
async setAutoLaunchEnabled(enabled: boolean): Promise<void> {
return this._ipcCall('setAutoLaunchEnabled', enabled);
}
supportsWarnBeforeExit(): boolean {
return true;
}
async shouldWarnBeforeExit(): Promise<boolean> {
return this._ipcCall('shouldWarnBeforeExit');
}
async setWarnBeforeExit(enabled: boolean): Promise<void> {
return this._ipcCall('setWarnBeforeExit', enabled);
}
supportsAutoHideMenuBar(): boolean {
// This is irelevant on Mac as Menu bars don't live in the app window
return !isMac;
}
async getAutoHideMenuBarEnabled(): Promise<boolean> {
return this._ipcCall('getAutoHideMenuBarEnabled');
}
async setAutoHideMenuBarEnabled(enabled: boolean): Promise<void> {
return this._ipcCall('setAutoHideMenuBarEnabled', enabled);
}
supportsMinimizeToTray(): boolean {
// Things other than Mac support tray icons
return !isMac;
}
async getMinimizeToTrayEnabled(): Promise<boolean> {
return this._ipcCall('getMinimizeToTrayEnabled');
}
async setMinimizeToTrayEnabled(enabled: boolean): Promise<void> {
return this._ipcCall('setMinimizeToTrayEnabled', enabled);
}
async canSelfUpdate(): Promise<boolean> {
const feedUrl = await this._ipcCall('getUpdateFeedUrl');
return Boolean(feedUrl);
}
startUpdateCheck() {
super.startUpdateCheck();
electron.send('check_updates');
}
installUpdate() {
// 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.
electron.send('install_update');
}
getDefaultDeviceDisplayName(): string {
const brand = SdkConfig.get().brand;
return _t('%(brand)s Desktop (%(platformName)s)', {
brand,
platformName: platformFriendlyName(),
});
}
screenCaptureErrorString(): string | null {
return null;
}
requestNotificationPermission(): Promise<string> {
return Promise.resolve('granted');
}
reload() {
// we used to remote to the main process to get it to
// reload the webcontents, but in practice this is unnecessary:
// the normal way works fine.
window.location.reload(false);
}
async _ipcCall(name: string, ...args: any[]): Promise<any> {
const ipcCallId = ++this.nextIpcCallId;
return new Promise((resolve, reject) => {
this.pendingIpcCalls[ipcCallId] = { resolve, reject };
window.electron.send('ipcCall', { id: ipcCallId, name, args });
// Maybe add a timeout to these? Probably not necessary.
});
}
_onIpcReply = (ev, payload) => {
if (payload.id === undefined) {
console.warn("Ignoring IPC reply with no ID");
return;
}
if (this.pendingIpcCalls[payload.id] === undefined) {
console.warn("Unknown IPC payload ID: " + payload.id);
return;
}
const callbacks = this.pendingIpcCalls[payload.id];
delete this.pendingIpcCalls[payload.id];
if (payload.error) {
callbacks.reject(payload.error);
} else {
callbacks.resolve(payload.reply);
}
};
getEventIndexingManager(): BaseEventIndexManager | null {
return this.eventIndexManager;
}
async setLanguage(preferredLangs: string[]) {
return this._ipcCall('setLanguage', preferredLangs);
}
setSpellCheckLanguages(preferredLangs: string[]) {
this._ipcCall('setSpellCheckLanguages', preferredLangs).catch(error => {
console.log("Failed to send setSpellCheckLanguages IPC to Electron");
console.error(error);
});
}
async getSpellCheckLanguages(): Promise<string[]> {
return this._ipcCall('getSpellCheckLanguages');
}
async getAvailableSpellCheckLanguages(): Promise<string[]> {
return this._ipcCall('getAvailableSpellCheckLanguages');
}
getSSOCallbackUrl(fragmentAfterLogin: string): URL {
const url = super.getSSOCallbackUrl(fragmentAfterLogin);
url.protocol = "element";
url.searchParams.set("element-desktop-ssoid", this.ssoID);
return url;
}
startSingleSignOn(mxClient: MatrixClient, loginType: "sso" | "cas", fragmentAfterLogin: string, idpId?: string) {
// this will get intercepted by electron-main will-navigate
super.startSingleSignOn(mxClient, loginType, fragmentAfterLogin, idpId);
Modal.createTrackedDialog('Electron', 'SSO', InfoDialog, {
title: _t("Go to your browser to complete Sign In"),
description: <Spinner />,
});
}
private navigateForwardBack(back: boolean) {
this._ipcCall(back ? "navigateBack" : "navigateForward");
}
private navigateToSpace(num: number) {
dis.dispatch<SwitchSpacePayload>({
action: Action.SwitchSpace,
num,
});
}
onKeyDown(ev: KeyboardEvent): boolean {
let handled = false;
switch (ev.key) {
case Key.SQUARE_BRACKET_LEFT:
case Key.SQUARE_BRACKET_RIGHT:
if (isMac && ev.metaKey && !ev.altKey && !ev.ctrlKey && !ev.shiftKey) {
this.navigateForwardBack(ev.key === Key.SQUARE_BRACKET_LEFT);
handled = true;
}
break;
case Key.ARROW_LEFT:
case Key.ARROW_RIGHT:
if (!isMac && ev.altKey && !ev.metaKey && !ev.ctrlKey && !ev.shiftKey) {
this.navigateForwardBack(ev.key === Key.ARROW_LEFT);
handled = true;
}
break;
}
if (!handled &&
// ideally we would use SpaceStore.spacesEnabled here but importing SpaceStore in this platform
// breaks skinning as the platform is instantiated prior to the skin being loaded
SettingsStore.getValue("feature_spaces") &&
ev.code.startsWith("Digit") &&
isOnlyCtrlOrCmdKeyEvent(ev)
) {
const spaceNumber = ev.code.slice(5); // Cut off the first 5 characters - "Digit"
this.navigateToSpace(parseInt(spaceNumber, 10));
handled = true;
}
return handled;
}
async getPickleKey(userId: string, deviceId: string): Promise<string | null> {
try {
return await this._ipcCall('getPickleKey', userId, deviceId);
} catch (e) {
// if we can't connect to the password storage, assume there's no
// pickle key
return null;
}
}
async createPickleKey(userId: string, deviceId: string): Promise<string | null> {
try {
return await this._ipcCall('createPickleKey', userId, deviceId);
} catch (e) {
// if we can't connect to the password storage, assume there's no
// pickle key
return null;
}
}
async destroyPickleKey(userId: string, deviceId: string): Promise<void> {
try {
await this._ipcCall('destroyPickleKey', userId, deviceId);
} catch (e) {}
}
}

View file

@ -0,0 +1,29 @@
/*
Copyright 2020 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.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import WebPlatform from "./WebPlatform";
export default class PWAPlatform extends WebPlatform {
setNotificationCount(count: number) {
if (!navigator.setAppBadge) return super.setNotificationCount(count);
if (this.notificationCount === count) return;
this.notificationCount = count;
navigator.setAppBadge(count).catch(e => {
console.error("Failed to update PWA app badge", e);
});
}
}

View file

@ -1,176 +0,0 @@
// @flow
/*
Copyright 2016 Aviral Dasgupta
Copyright 2016 OpenMarket Ltd
Copyright 2018 New Vector Ltd
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import BasePlatform from 'matrix-react-sdk/src/BasePlatform';
import { _t } from 'matrix-react-sdk/src/languageHandler';
import dis from 'matrix-react-sdk/src/dispatcher';
import {getVectorConfig} from "../getconfig";
import Favico from 'favico.js';
export const updateCheckStatusEnum = {
CHECKING: 'CHECKING',
ERROR: 'ERROR',
NOTAVAILABLE: 'NOTAVAILABLE',
DOWNLOADING: 'DOWNLOADING',
READY: 'READY',
};
/**
* Vector-specific extensions to the BasePlatform template
*/
export default class VectorBasePlatform extends BasePlatform {
constructor() {
super();
this.showUpdateCheck = false;
this.startUpdateCheck = this.startUpdateCheck.bind(this);
this.stopUpdateCheck = this.stopUpdateCheck.bind(this);
}
async getConfig(): Promise<{}> {
return getVectorConfig();
}
getHumanReadableName(): string {
return 'Vector Base Platform'; // no translation required: only used for analytics
}
/**
* Delay creating the `Favico` 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/riot-web/issues/9605.
*/
get favicon() {
if (this._favicon) {
return this._favicon;
}
// The 'animations' are really low framerate and look terrible.
// Also it re-starts the animation every time you set the badge,
// and we set the state each time, even if the value hasn't changed,
// so we'd need to fix that if enabling the animation.
this._favicon = new Favico({ animation: 'none' });
return this._favicon;
}
_updateFavicon() {
try {
// This needs to be in in a try block as it will throw
// if there are more than 100 badge count changes in
// its internal queue
let bgColor = "#d00";
let notif = this.notificationCount;
if (this.errorDidOccur) {
notif = notif || "×";
bgColor = "#f00";
}
const doUpdate = () => {
this.favicon.badge(notif, {
bgColor: bgColor,
});
};
doUpdate();
// HACK: Workaround for Chrome 78+ and dependency incompatibility.
// The library we use doesn't appear to work in Chrome 78, likely due to their
// changes surrounding tab behaviour. Tabs went through a bit of a redesign and
// restructuring in Chrome 78, so it's not terribly surprising that the library
// doesn't work correctly. The library we use hasn't been updated in years and
// does not look easy to fix/fork ourselves - we might as well write our own that
// doesn't include animation/webcam/etc support. However, that's a bit difficult
// so for now we'll just trigger the update twice.
//
// Note that trying to reproduce the problem in isolation doesn't seem to work:
// see https://gist.github.com/turt2live/5ab87919918adbfd7cfb8f1ad10f2409 for
// an example (you'll need your own web server to host that).
if (window.chrome) {
doUpdate();
}
} catch (e) {
console.warn(`Failed to set badge count: ${e.message}`);
}
}
setNotificationCount(count: number) {
if (this.notificationCount === count) return;
super.setNotificationCount(count);
this._updateFavicon();
}
setErrorStatus(errorDidOccur: boolean) {
if (this.errorDidOccur === errorDidOccur) return;
super.setErrorStatus(errorDidOccur);
this._updateFavicon();
}
/**
* Begin update polling, if applicable
*/
startUpdater() {
}
/**
* Whether we can call checkForUpdate on this platform build
*/
async canSelfUpdate(): boolean {
return false;
}
startUpdateCheck() {
this.showUpdateCheck = true;
dis.dispatch({
action: 'check_updates',
value: { status: updateCheckStatusEnum.CHECKING },
});
}
stopUpdateCheck() {
this.showUpdateCheck = false;
dis.dispatch({
action: 'check_updates',
value: false,
});
}
getUpdateCheckStatusEnum() {
return updateCheckStatusEnum;
}
/**
* Update the currently running app to the latest available
* version and replace this instance of the app with the
* new version.
*/
installUpdate() {
}
/**
* Get a sensible default display name for the
* device Vector is running on
*/
getDefaultDeviceDisplayName(): string {
return _t("Unknown device");
}
}

View file

@ -0,0 +1,89 @@
/*
Copyright 2016 Aviral Dasgupta
Copyright 2016 OpenMarket Ltd
Copyright 2018, 2020 New Vector Ltd
Copyright 2019 Michael Telatynski <7t3chguy@gmail.com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import BasePlatform from 'matrix-react-sdk/src/BasePlatform';
import { _t } from 'matrix-react-sdk/src/languageHandler';
import { getVectorConfig } from "../getconfig";
import Favicon from "../../favicon";
/**
* Vector-specific extensions to the BasePlatform template
*/
export default abstract class VectorBasePlatform extends BasePlatform {
protected _favicon: Favicon;
async getConfig(): Promise<{}> {
return getVectorConfig();
}
getHumanReadableName(): string {
return 'Vector Base Platform'; // no translation required: only used for analytics
}
/**
* 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.
*/
get favicon() {
if (this._favicon) {
return this._favicon;
}
return this._favicon = new Favicon();
}
_updateFavicon() {
let bgColor = "#d00";
let notif: string | number = this.notificationCount;
if (this.errorDidOccur) {
notif = notif || "×";
bgColor = "#f00";
}
this.favicon.badge(notif, { bgColor });
}
setNotificationCount(count: number) {
if (this.notificationCount === count) return;
super.setNotificationCount(count);
this._updateFavicon();
}
setErrorStatus(errorDidOccur: boolean) {
if (this.errorDidOccur === errorDidOccur) return;
super.setErrorStatus(errorDidOccur);
this._updateFavicon();
}
/**
* Begin update polling, if applicable
*/
startUpdater() {
}
/**
* Get a sensible default display name for the
* device Vector is running on
*/
getDefaultDeviceDisplayName(): string {
return _t("Unknown device");
}
}

View file

@ -1,8 +1,7 @@
// @flow
/*
Copyright 2016 Aviral Dasgupta
Copyright 2016 OpenMarket Ltd
Copyright 2017-2020 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.
@ -17,22 +16,29 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import VectorBasePlatform, {updateCheckStatusEnum} from './VectorBasePlatform';
import VectorBasePlatform from './VectorBasePlatform';
import { UpdateCheckStatus } from "matrix-react-sdk/src/BasePlatform";
import request from 'browser-request';
import dis from 'matrix-react-sdk/src/dispatcher.js';
import dis from 'matrix-react-sdk/src/dispatcher/dispatcher';
import { _t } from 'matrix-react-sdk/src/languageHandler';
import { Room } from "matrix-js-sdk/src/models/room";
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';
import UAParser from 'ua-parser-js';
const POKE_RATE_MS = 10 * 60 * 1000; // 10 min
export default class WebPlatform extends VectorBasePlatform {
private runningVersion: string = null;
constructor() {
super();
this.runningVersion = null;
this.startUpdateCheck = this.startUpdateCheck.bind(this);
this.stopUpdateCheck = this.stopUpdateCheck.bind(this);
// Register service worker if available on this platform
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('sw.js');
}
}
getHumanReadableName(): string {
@ -44,7 +50,7 @@ export default class WebPlatform extends VectorBasePlatform {
* notifications, otherwise false.
*/
supportsNotifications(): boolean {
return Boolean(global.Notification);
return Boolean(window.Notification);
}
/**
@ -52,7 +58,7 @@ export default class WebPlatform extends VectorBasePlatform {
* to display notifications. Otherwise false.
*/
maySendNotifications(): boolean {
return global.Notification.permission === 'granted';
return window.Notification.permission === 'granted';
}
/**
@ -67,29 +73,31 @@ export default class WebPlatform extends VectorBasePlatform {
// promise, but this is only supported in Chrome 46
// and Firefox 47, so adapt the callback API.
return new Promise(function(resolve, reject) {
global.Notification.requestPermission((result) => {
window.Notification.requestPermission((result) => {
resolve(result);
});
});
}
displayNotification(title: string, msg: string, avatarUrl: string, room: Object) {
displayNotification(title: string, msg: string, avatarUrl: string, room: Room) {
const notifBody = {
body: msg,
tag: "vector",
silent: true, // we play our own sounds
};
if (avatarUrl) notifBody['icon'] = avatarUrl;
const notification = new global.Notification(title, notifBody);
const notification = new window.Notification(title, notifBody);
notification.onclick = function() {
dis.dispatch({
action: 'view_room',
room_id: room.roomId,
});
global.focus();
window.focus();
notification.close();
};
return notification;
}
_getVersion(): Promise<string> {
@ -129,45 +137,42 @@ export default class WebPlatform extends VectorBasePlatform {
startUpdater() {
this.pollForUpdate();
setInterval(this.pollForUpdate.bind(this), POKE_RATE_MS);
setInterval(this.pollForUpdate, POKE_RATE_MS);
}
async canSelfUpdate(): boolean {
async canSelfUpdate(): Promise<boolean> {
return true;
}
pollForUpdate() {
pollForUpdate = () => {
return this._getVersion().then((ver) => {
if (this.runningVersion === null) {
this.runningVersion = ver;
} else if (this.runningVersion !== ver) {
dis.dispatch({
action: 'new_version',
currentVersion: this.runningVersion,
newVersion: ver,
});
// Return to skip a MatrixChat state update
return;
if (this.shouldShowUpdate(ver)) {
showUpdateToast(this.runningVersion, ver);
}
return { status: UpdateCheckStatus.Ready };
} else {
hideUpdateToast();
}
return { status: updateCheckStatusEnum.NOTAVAILABLE };
return { status: UpdateCheckStatus.NotAvailable };
}, (err) => {
console.error("Failed to poll for update", err);
return {
status: updateCheckStatusEnum.ERROR,
status: UpdateCheckStatus.Error,
detail: err.message || err.status ? err.status.toString() : 'Unknown Error',
};
});
}
};
startUpdateCheck() {
if (this.showUpdateCheck) return;
super.startUpdateCheck();
this.pollForUpdate().then((updateState) => {
if (!this.showUpdateCheck) return;
if (!updateState) return;
dis.dispatch({
action: 'check_updates',
value: updateState,
dis.dispatch<CheckUpdatesPayload>({
action: Action.CheckUpdates,
...updateState,
});
});
}
@ -178,20 +183,29 @@ export default class WebPlatform extends VectorBasePlatform {
getDefaultDeviceDisplayName(): string {
// strip query-string and fragment from uri
const u = new URL(window.location);
u.search = "";
u.hash = "";
const appName = u.toString();
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";
const osName = ua.getOS().name || "unknown os";
return _t('%(appName)s via %(browserName)s on %(osName)s', {appName, browserName, osName});
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, %(osName)s)', {
appName,
browserName,
osName,
});
}
screenCaptureErrorString(): ?string {
screenCaptureErrorString(): string | null {
// it won't work at all if you're not on HTTPS so whine whine whine
if (!global.window || global.window.location.protocol !== "https:") {
if (window.location.protocol !== "https:") {
return _t("You need to be using HTTPS to place a screen-sharing call.");
}
return null;

View file

@ -31,14 +31,15 @@ import SdkConfig from "matrix-react-sdk/src/SdkConfig";
import sendBugReport from "matrix-react-sdk/src/rageshake/submit-rageshake";
export function initRageshake() {
const prom = rageshake.init();
// we manually check persistence for rageshakes ourselves
const prom = rageshake.init(/*setUpPersistence=*/false);
prom.then(() => {
console.log("Initialised rageshake.");
console.log("To fix line numbers in Chrome: " +
"Meatball menu → Settings → Blackboxing → Add /rageshake\\.js$");
"Meatball menu → Settings → Ignore list → Add /rageshake\\.js$");
window.addEventListener('beforeunload', (e) => {
console.log('riot-web closing');
console.log('element-web closing');
// try to flush the logs to indexeddb
rageshake.flush();
});
@ -50,13 +51,23 @@ export function initRageshake() {
return prom;
}
export function initRageshakeStore() {
return rageshake.tryInitStorage();
}
window.mxSendRageshake = function(text: string, withLogs?: boolean) {
const url = SdkConfig.get().bug_report_endpoint_url;
if (!url) {
console.error("Cannot send a rageshake - no bug_report_endpoint_url configured");
return;
}
if (withLogs === undefined) withLogs = true;
if (!text || !text.trim()) {
console.error("Cannot send a rageshake without a message - please tell us what went wrong");
return;
}
sendBugReport(SdkConfig.get().bug_report_endpoint_url, {
sendBugReport(url, {
userText: text,
sendLogs: withLogs,
progressCallback: console.log.bind(console),

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,183 @@
<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_header {
align-items: center;
}
.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>