Merge branch 'master' into bwindels/decrypt-images

This commit is contained in:
Bruno Windels 2020-10-26 17:08:29 +01:00
commit 3ed5ea8b0b
139 changed files with 372 additions and 372 deletions

View file

@ -1,7 +1,7 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no">
<meta name="application-name" content="Hydrogen Chat"/>
<meta name="apple-mobile-web-app-capable" content="yes">
@ -9,25 +9,26 @@
<meta name="apple-mobile-web-app-title" content="Hydrogen Chat">
<meta name="description" content="A matrix chat application">
<link rel="apple-touch-icon" href="assets/icon-maskable.png">
<link rel="stylesheet" type="text/css" href="src/ui/web/css/main.css">
<link rel="stylesheet" type="text/css" href="src/ui/web/css/themes/element/theme.css" title="Element Theme">
<link rel="alternate stylesheet" type="text/css" href="src/ui/web/css/themes/bubbles/theme.css" title="Bubbles Theme">
</head>
<body class="hydrogen">
<link rel="stylesheet" type="text/css" href="src/platform/web/ui/css/main.css">
<link rel="stylesheet" type="text/css" href="src/platform/web/ui/css/themes/element/theme.css" title="Element Theme">
<link rel="alternate stylesheet" type="text/css" href="src/platform/web/ui/css/themes/bubbles/theme.css" title="Bubbles Theme">
</head>
<body class="hydrogen">
<script id="version" type="disabled">
window.HYDROGEN_VERSION = "%%VERSION%%";
window.HYDROGEN_GLOBAL_HASH = "%%GLOBAL_HASH%%";
</script>
<script id="main" type="module">
import {main} from "./src/main.js";
main(document.body, {
<script id="main" type="module">
import {main} from "./src/main.js";
import {Platform} from "./src/platform/web/Platform.js";
main(new Platform(document.body, {
worker: "src/worker.js",
olm: {
wasm: "lib/olm/olm.wasm",
legacyBundle: "lib/olm/olm_legacy.js",
wasmBundle: "lib/olm/olm.js",
}
});
</script>
</body>
}));
</script>
</body>
</html>

View file

@ -1,6 +1,6 @@
{
"name": "hydrogen-web",
"version": "0.1.19",
"version": "0.1.20",
"description": "A javascript matrix client prototype, trying to minize RAM usage by offloading as much as possible to IndexedDB",
"main": "index.js",
"directories": {

View file

@ -45,12 +45,12 @@ import flexbugsFixes from "postcss-flexbugs-fixes";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const projectDir = path.join(__dirname, "../");
const cssSrcDir = path.join(projectDir, "src/ui/web/css/");
const cssSrcDir = path.join(projectDir, "src/platform/web/ui/css/");
const program = new commander.Command();
program
const parameters = new commander.Command();
parameters
.option("--modern-only", "don't make a legacy build")
program.parse(process.argv);
parameters.parse(process.argv);
async function build({modernOnly}) {
// get version number
@ -70,10 +70,13 @@ async function build({modernOnly}) {
// copy olm assets
const olmAssets = await copyFolder(path.join(projectDir, "lib/olm/"), assets.directory);
assets.addSubMap(olmAssets);
await assets.write(`hydrogen.js`, await buildJs("src/main.js"));
await assets.write(`hydrogen.js`, await buildJs("src/main.js", ["src/platform/web/Platform.js"]));
if (!modernOnly) {
await assets.write(`hydrogen-legacy.js`, await buildJsLegacy("src/main.js", ['src/legacy-polyfill.js', 'src/legacy-extras.js']));
await assets.write(`worker.js`, await buildJsLegacy("src/worker.js", ['src/worker-polyfill.js']));
await assets.write(`hydrogen-legacy.js`, await buildJsLegacy("src/main.js", [
'src/platform/web/legacy-polyfill.js',
'src/platform/web/LegacyPlatform.js'
]));
await assets.write(`worker.js`, await buildJsLegacy("src/platform/web/worker/main.js", ['src/platform/web/worker/polyfill.js']));
}
// creates the directories where the theme css bundles are placed in,
// and writes to assets, so the build bundles can translate them, so do it first
@ -82,7 +85,7 @@ async function build({modernOnly}) {
await buildManifest(assets);
// all assets have been added, create a hash from all assets name to cache unhashed files like index.html
assets.addToHashForAll("index.html", devHtml);
let swSource = await fs.readFile(path.join(projectDir, "src/service-worker.template.js"), "utf8");
let swSource = await fs.readFile(path.join(projectDir, "src/platform/web/service-worker.template.js"), "utf8");
assets.addToHashForAll("sw.js", swSource);
const globalHash = assets.hashForAll();
@ -148,12 +151,12 @@ async function buildHtml(doc, version, globalHash, modernOnly, assets) {
}
});
const mainScripts = [
`<script type="module">import {main} from "./${assets.resolve(`hydrogen.js`)}"; main(document.body, ${pathsJSON});</script>`
`<script type="module">import {main, Platform} from "./${assets.resolve(`hydrogen.js`)}"; main(new Platform(document.body, ${pathsJSON}));</script>`
];
if (!modernOnly) {
mainScripts.push(
`<script type="text/javascript" nomodule src="${assets.resolve(`hydrogen-legacy.js`)}"></script>`,
`<script type="text/javascript" nomodule>hydrogenBundle.main(document.body, ${pathsJSON}, hydrogenBundle.legacyExtras);</script>`
`<script type="text/javascript" nomodule>hydrogen.main(new hydrogen.Platform(document.body, ${pathsJSON}));</script>`
);
}
doc("script#main").replaceWith(mainScripts.join(""));
@ -168,16 +171,16 @@ async function buildHtml(doc, version, globalHash, modernOnly, assets) {
await assets.writeUnhashed("index.html", doc.html());
}
async function buildJs(inputFile) {
async function buildJs(mainFile, extraFiles = []) {
// create js bundle
const bundle = await rollup({
input: inputFile,
plugins: [removeJsComments({comments: "none"})]
input: extraFiles.concat(mainFile),
plugins: [multi(), removeJsComments({comments: "none"})]
});
const {output} = await bundle.generate({
format: 'es',
// TODO: can remove this?
name: `hydrogenBundle`
name: `hydrogen`
});
const code = output[0].code;
return code;
@ -214,7 +217,7 @@ async function buildJsLegacy(mainFile, extraFiles = []) {
const bundle = await rollup(rollupConfig);
const {output} = await bundle.generate({
format: 'iife',
name: `hydrogenBundle`
name: `hydrogen`
});
const code = output[0].code;
return code;
@ -460,4 +463,4 @@ class AssetMap {
}
}
build(program).catch(err => console.error(err));
build(parameters).catch(err => console.error(err));

View file

@ -1,17 +0,0 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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.
*/
export {WebPlatform as Platform} from "./ui/web/WebPlatform.js";

View file

@ -23,10 +23,7 @@ import {ViewModel} from "./ViewModel.js";
export class RootViewModel extends ViewModel {
constructor(options) {
super(options);
const {createSessionContainer, sessionInfoStorage, storageFactory} = options;
this._createSessionContainer = createSessionContainer;
this._sessionInfoStorage = sessionInfoStorage;
this._storageFactory = storageFactory;
this._createSessionContainer = options.createSessionContainer;
this._error = null;
this._sessionPickerViewModel = null;
this._sessionLoadViewModel = null;
@ -73,7 +70,7 @@ export class RootViewModel extends ViewModel {
if (restoreUrlIfAtDefault) {
this.urlCreator.pushUrl(restoreUrlIfAtDefault);
} else {
const sessionInfos = await this._sessionInfoStorage.getAll();
const sessionInfos = await this.platform.sessionInfoStorage.getAll();
if (sessionInfos.length === 0) {
this.navigation.push("login");
} else if (sessionInfos.length === 1) {
@ -90,10 +87,7 @@ export class RootViewModel extends ViewModel {
async _showPicker() {
this._setSection(() => {
this._sessionPickerViewModel = new SessionPickerViewModel(this.childOptions({
sessionInfoStorage: this._sessionInfoStorage,
storageFactory: this._storageFactory,
}));
this._sessionPickerViewModel = new SessionPickerViewModel(this.childOptions());
});
try {
await this._sessionPickerViewModel.load();
@ -125,11 +119,7 @@ export class RootViewModel extends ViewModel {
_showSession(sessionContainer) {
this._setSection(() => {
this._sessionViewModel = new SessionViewModel(this.childOptions({
sessionContainer,
updateService: this.getOption("updateService"),
estimateStorageUsage: this.getOption("estimateStorageUsage"),
}));
this._sessionViewModel = new SessionViewModel(this.childOptions({sessionContainer}));
this._sessionViewModel.start();
});
}

View file

@ -130,9 +130,6 @@ class SessionItemViewModel extends ViewModel {
export class SessionPickerViewModel extends ViewModel {
constructor(options) {
super(options);
const {storageFactory, sessionInfoStorage} = options;
this._storageFactory = storageFactory;
this._sessionInfoStorage = sessionInfoStorage;
this._sessions = new SortedArray((s1, s2) => s1.id.localeCompare(s2.id));
this._loadViewModel = null;
this._error = null;
@ -140,7 +137,7 @@ export class SessionPickerViewModel extends ViewModel {
// this loads all the sessions
async load() {
const sessions = await this._sessionInfoStorage.getAll();
const sessions = await this.platform.sessionInfoStorage.getAll();
this._sessions.setManyUnsorted(sessions.map(s => {
return new SessionItemViewModel(this.childOptions({sessionInfo: s}), this);
}));
@ -152,8 +149,8 @@ export class SessionPickerViewModel extends ViewModel {
}
async _exportData(id) {
const sessionInfo = await this._sessionInfoStorage.get(id);
const stores = await this._storageFactory.export(id);
const sessionInfo = await this.platform.sessionInfoStorage.get(id);
const stores = await this.platform.storageFactory.export(id);
const data = {sessionInfo, stores};
return data;
}
@ -164,8 +161,8 @@ export class SessionPickerViewModel extends ViewModel {
const {sessionInfo} = data;
sessionInfo.comment = `Imported on ${new Date().toLocaleString()} from id ${sessionInfo.id}.`;
sessionInfo.id = this._createSessionContainer().createNewSessionId();
await this._storageFactory.import(sessionInfo.id, data.stores);
await this._sessionInfoStorage.add(sessionInfo);
await this.platform.storageFactory.import(sessionInfo.id, data.stores);
await this.platform.sessionInfoStorage.add(sessionInfo);
this._sessions.set(new SessionItemViewModel(sessionInfo, this));
} catch (err) {
alert(err.message);
@ -175,13 +172,13 @@ export class SessionPickerViewModel extends ViewModel {
async delete(id) {
const idx = this._sessions.array.findIndex(s => s.id === id);
await this._sessionInfoStorage.delete(id);
await this._storageFactory.delete(id);
await this.platform.sessionInfoStorage.delete(id);
await this.platform.storageFactory.delete(id);
this._sessions.remove(idx);
}
async clear(id) {
await this._storageFactory.delete(id);
await this.platform.storageFactory.delete(id);
}
get sessions() {

View file

@ -30,8 +30,8 @@ export class ViewModel extends EventEmitter {
}
childOptions(explicitOptions) {
const {navigation, urlCreator, clock} = this._options;
return Object.assign({navigation, urlCreator, clock}, explicitOptions);
const {navigation, urlCreator, platform} = this._options;
return Object.assign({navigation, urlCreator, platform}, explicitOptions);
}
// makes it easier to pass through dependencies of a sub-view model
@ -100,8 +100,12 @@ export class ViewModel extends EventEmitter {
}
}
get platform() {
return this._options.platform;
}
get clock() {
return this._options.clock;
return this._options.platform.clock;
}
/**

View file

@ -188,9 +188,7 @@ export class SessionViewModel extends ViewModel {
}
if (settingsOpen) {
this._settingsViewModel = this.track(new SettingsViewModel(this.childOptions({
updateService: this.getOption("updateService"),
session: this._sessionContainer.session,
estimateStorageUsage: this.getOption("estimateStorageUsage")
})));
this._settingsViewModel.load();
}

View file

@ -43,7 +43,7 @@ export class TimelineViewModel extends ViewModel {
// once we support sending messages we could do
// timeline.entries.concat(timeline.pendingEvents)
// for an ObservableList that also contains local echos
this._tiles = new TilesCollection(timeline.entries, tilesCreator({room, ownUserId, clock: this.clock}));
this._tiles = new TilesCollection(timeline.entries, tilesCreator({room, ownUserId, platform: this.platform}));
}
async load() {

View file

@ -21,7 +21,6 @@ export class MessageTile extends SimpleTile {
constructor(options) {
super(options);
this._mediaRepository = options.mediaRepository;
this._clock = options.clock;
this._isOwn = this._entry.sender === options.ownUserId;
this._date = this._entry.timestamp ? new Date(this._entry.timestamp) : null;
this._isContinuation = false;
@ -88,8 +87,8 @@ export class MessageTile extends SimpleTile {
let isContinuation = false;
if (prev && prev instanceof MessageTile && prev.sender === this.sender) {
// timestamp is null for pending events
const myTimestamp = this._entry.timestamp || this._clock.now();
const otherTimestamp = prev._entry.timestamp || this._clock.now();
const myTimestamp = this._entry.timestamp || this.clock.now();
const otherTimestamp = prev._entry.timestamp || this.clock.now();
// other message was sent less than 5min ago
isContinuation = (myTimestamp - otherTimestamp) < (5 * 60 * 1000);
}

View file

@ -23,9 +23,9 @@ import {RoomMemberTile} from "./tiles/RoomMemberTile.js";
import {EncryptedEventTile} from "./tiles/EncryptedEventTile.js";
import {EncryptionEnabledTile} from "./tiles/EncryptionEnabledTile.js";
export function tilesCreator({room, ownUserId, clock}) {
export function tilesCreator({room, ownUserId, platform}) {
return function tilesCreator(entry, emitUpdate) {
const options = {entry, emitUpdate, ownUserId, clock,
const options = {entry, emitUpdate, ownUserId, platform,
mediaRepository: room.mediaRepository};
if (entry.isGap) {
return new GapTile(options, room);

View file

@ -35,12 +35,11 @@ export class SettingsViewModel extends ViewModel {
this._session = session;
this._sessionBackupViewModel = this.track(new SessionBackupViewModel(this.childOptions({session})));
this._closeUrl = this.urlCreator.urlUntilSegment("session");
this._estimateStorageUsage = options.estimateStorageUsage;
this._estimate = null;
}
async load() {
this._estimate = await this._estimateStorageUsage();
this._estimate = await this.platform.estimateStorageUsage();
this.emitChange("");
}
@ -61,18 +60,19 @@ export class SettingsViewModel extends ViewModel {
}
get version() {
if (this._updateService) {
return `${this._updateService.version} (${this._updateService.buildHash})`;
const {updateService} = this.platform;
if (updateService) {
return `${updateService.version} (${updateService.buildHash})`;
}
return this.i18n`development version`;
}
checkForUpdate() {
this._updateService?.checkForUpdate();
this.platform.updateService?.checkForUpdate();
}
get showUpdateButton() {
return !!this._updateService;
return !!this.platform.updateService;
}
get sessionBackupViewModel() {

View file

@ -1,6 +0,0 @@
import aesjs from "../lib/aes-js/index.js";
import {hkdf} from "./utils/crypto/hkdf.js";
// these are run-time dependencies that are only needed for the legacy bundle.
// they are exported here and passed into main to make them available to the app.
export const legacyExtras = {crypto:{aesjs, hkdf}};

View file

@ -16,82 +16,14 @@ limitations under the License.
*/
// import {RecordRequester, ReplayRequester} from "./matrix/net/request/replay.js";
import {createFetchRequest} from "./matrix/net/request/fetch.js";
import {xhrRequest} from "./matrix/net/request/xhr.js";
import {SessionContainer} from "./matrix/SessionContainer.js";
import {StorageFactory} from "./matrix/storage/idb/StorageFactory.js";
import {SessionInfoStorage} from "./matrix/sessioninfo/localstorage/SessionInfoStorage.js";
import {RootViewModel} from "./domain/RootViewModel.js";
import {createNavigation, createRouter} from "./domain/navigation/index.js";
import {RootView} from "./ui/web/RootView.js";
import {Clock} from "./ui/web/dom/Clock.js";
import {ServiceWorkerHandler} from "./ui/web/dom/ServiceWorkerHandler.js";
import {History} from "./ui/web/dom/History.js";
import {OnlineStatus} from "./ui/web/dom/OnlineStatus.js";
import {CryptoDriver} from "./ui/web/dom/CryptoDriver.js";
import {estimateStorageUsage} from "./ui/web/dom/StorageEstimate.js";
import {WorkerPool} from "./utils/WorkerPool.js";
import {OlmWorker} from "./matrix/e2ee/OlmWorker.js";
function addScript(src) {
return new Promise(function (resolve, reject) {
var s = document.createElement("script");
s.setAttribute("src", src );
s.onload=resolve;
s.onerror=reject;
document.body.appendChild(s);
});
}
async function loadOlm(olmPaths) {
// make crypto.getRandomValues available without
// a prefix on IE11, needed by olm to work
if (window.msCrypto && !window.crypto) {
window.crypto = window.msCrypto;
}
if (olmPaths) {
if (window.WebAssembly) {
await addScript(olmPaths.wasmBundle);
await window.Olm.init({locateFile: () => olmPaths.wasm});
} else {
await addScript(olmPaths.legacyBundle);
await window.Olm.init();
}
return window.Olm;
}
return null;
}
// make path relative to basePath,
// assuming it and basePath are relative to document
function relPath(path, basePath) {
const idx = basePath.lastIndexOf("/");
const dir = idx === -1 ? "" : basePath.slice(0, idx);
const dirCount = dir.length ? dir.split("/").length : 0;
return "../".repeat(dirCount) + path;
}
async function loadOlmWorker(paths) {
const workerPool = new WorkerPool(paths.worker, 4);
await workerPool.init();
const path = relPath(paths.olm.legacyBundle, paths.worker);
await workerPool.sendAll({type: "load_olm", path});
const olmWorker = new OlmWorker(workerPool);
return olmWorker;
}
// Don't use a default export here, as we use multiple entries during legacy build,
// which does not support default exports,
// see https://github.com/rollup/plugins/tree/master/packages/multi-entry
export async function main(container, paths, legacyExtras) {
export async function main(platform) {
try {
// TODO: add .legacy to .hydrogen (container) in (legacy)platform.createAndMountRootView; and use .hydrogen:not(.legacy) if needed for modern stuff
const isIE11 = !!window.MSInputMethodContext && !!document.documentMode;
if (isIE11) {
document.body.className += " ie11";
} else {
document.body.className += " not-ie11";
}
// to replay:
// const fetchLog = await (await fetch("/fetchlogs/constrainterror.json")).json();
// const replay = new ReplayRequester(fetchLog, {delay: false});
@ -101,61 +33,25 @@ export async function main(container, paths, legacyExtras) {
// const recorder = new RecordRequester(createFetchRequest(clock.createTimeout));
// const request = recorder.request;
// window.getBrawlFetchLog = () => recorder.log();
const clock = new Clock();
let request;
if (typeof fetch === "function") {
request = createFetchRequest(clock.createTimeout);
} else {
request = xhrRequest;
}
const navigation = createNavigation();
const sessionInfoStorage = new SessionInfoStorage("hydrogen_sessions_v1");
let serviceWorkerHandler;
if (paths.serviceWorker && "serviceWorker" in navigator) {
serviceWorkerHandler = new ServiceWorkerHandler({navigation});
serviceWorkerHandler.registerAndStart(paths.serviceWorker);
}
const storageFactory = new StorageFactory(serviceWorkerHandler);
const olmPromise = loadOlm(paths.olm);
// if wasm is not supported, we'll want
// to run some olm operations in a worker (mainly for IE11)
let workerPromise;
if (!window.WebAssembly) {
workerPromise = loadOlmWorker(paths);
}
const urlRouter = createRouter({navigation, history: new History()});
platform.setNavigation(navigation);
const urlRouter = createRouter({navigation, history: platform.history});
urlRouter.attach();
const olmPromise = platform.loadOlm();
const workerPromise = platform.loadOlmWorker();
const vm = new RootViewModel({
createSessionContainer: () => {
return new SessionContainer({
random: Math.random,
onlineStatus: new OnlineStatus(),
storageFactory,
sessionInfoStorage,
request,
clock,
cryptoDriver: new CryptoDriver(legacyExtras?.crypto),
olmPromise,
workerPromise,
});
return new SessionContainer({platform, olmPromise, workerPromise});
},
sessionInfoStorage,
storageFactory,
clock,
platform,
// the only public interface of the router is to create urls,
// so we call it that in the view models
urlCreator: urlRouter,
navigation,
updateService: serviceWorkerHandler,
estimateStorageUsage
});
window.__hydrogenViewModel = vm;
await vm.load();
// TODO: replace with platform.createAndMountRootView(vm, container);
const view = new RootView(vm);
container.appendChild(view.mount());
platform.createAndMountRootView(vm);
} catch(err) {
console.error(`${err.message}:\n${err.stack}`);
}

View file

@ -41,8 +41,8 @@ const PICKLE_KEY = "DEFAULT_KEY";
export class Session {
// sessionInfo contains deviceId, userId and homeServer
constructor({clock, storage, hsApi, sessionInfo, olm, olmWorker, cryptoDriver, mediaRepository}) {
this._clock = clock;
constructor({storage, hsApi, sessionInfo, olm, olmWorker, platform, mediaRepository}) {
this._platform = platform;
this._storage = storage;
this._hsApi = hsApi;
this._mediaRepository = mediaRepository;
@ -61,7 +61,6 @@ export class Session {
this._megolmDecryption = null;
this._getSyncToken = () => this.syncToken;
this._olmWorker = olmWorker;
this._cryptoDriver = cryptoDriver;
this._sessionBackup = null;
this._hasSecretStorageKey = new ObservableValue(null);
@ -106,7 +105,7 @@ export class Session {
pickleKey: PICKLE_KEY,
olm: this._olm,
storage: this._storage,
now: this._clock.now,
now: this._platform.clock.now,
ownUserId: this._user.id,
senderKeyLock
});
@ -115,7 +114,7 @@ export class Session {
pickleKey: PICKLE_KEY,
olm: this._olm,
storage: this._storage,
now: this._clock.now,
now: this._platform.clock.now,
ownUserId: this._user.id,
olmUtil: this._olmUtil,
senderKeyLock
@ -125,7 +124,7 @@ export class Session {
pickleKey: PICKLE_KEY,
olm: this._olm,
storage: this._storage,
now: this._clock.now,
now: this._platform.clock.now,
ownDeviceId: this._sessionInfo.deviceId,
});
this._megolmDecryption = new MegOlmDecryption({
@ -166,7 +165,7 @@ export class Session {
this.needsSessionBackup.set(true)
}
},
clock: this._clock
clock: this._platform.clock
});
}
@ -185,7 +184,7 @@ export class Session {
if (this._sessionBackup) {
return false;
}
const key = await ssssKeyFromCredential(type, credential, this._storage, this._cryptoDriver, this._olm);
const key = await ssssKeyFromCredential(type, credential, this._storage, this._platform.crypto, this._olm);
// and create session backup, which needs to read from accountData
const readTxn = this._storage.readTxn([
this._storage.storeNames.accountData,
@ -207,7 +206,7 @@ export class Session {
}
async _createSessionBackup(ssssKey, txn) {
const secretStorage = new SecretStorage({key: ssssKey, cryptoDriver: this._cryptoDriver});
const secretStorage = new SecretStorage({key: ssssKey, crypto: this._platform.crypto});
this._sessionBackup = await SessionBackup.fromSecretStorage({olm: this._olm, secretStorage, hsApi: this._hsApi, txn});
if (this._sessionBackup) {
for (const room of this._rooms.values()) {
@ -363,7 +362,7 @@ export class Session {
pendingEvents,
user: this._user,
createRoomEncryption: this._createRoomEncryption,
clock: this._clock
clock: this._platform.clock
});
this._rooms.add(roomId, room);
return room;

View file

@ -44,13 +44,8 @@ export const LoginFailure = createEnum(
);
export class SessionContainer {
constructor({clock, random, onlineStatus, request, storageFactory, sessionInfoStorage, olmPromise, workerPromise, cryptoDriver}) {
this._random = random;
this._clock = clock;
this._onlineStatus = onlineStatus;
this._request = request;
this._storageFactory = storageFactory;
this._sessionInfoStorage = sessionInfoStorage;
constructor({platform, olmPromise, workerPromise}) {
this._platform = platform;
this._sessionStartedByReconnector = false;
this._status = new ObservableValue(LoadStatus.NotLoading);
this._error = null;
@ -63,11 +58,10 @@ export class SessionContainer {
this._requestScheduler = null;
this._olmPromise = olmPromise;
this._workerPromise = workerPromise;
this._cryptoDriver = cryptoDriver;
}
createNewSessionId() {
return (Math.floor(this._random() * Number.MAX_SAFE_INTEGER)).toString();
return (Math.floor(this._platform.random() * Number.MAX_SAFE_INTEGER)).toString();
}
get sessionId() {
@ -80,7 +74,7 @@ export class SessionContainer {
}
this._status.set(LoadStatus.Loading);
try {
const sessionInfo = await this._sessionInfoStorage.get(sessionId);
const sessionInfo = await this._platform.sessionInfoStorage.get(sessionId);
if (!sessionInfo) {
throw new Error("Invalid session id: " + sessionId);
}
@ -96,9 +90,11 @@ export class SessionContainer {
return;
}
this._status.set(LoadStatus.Login);
const clock = this._platform.clock;
let sessionInfo;
try {
const hsApi = new HomeServerApi({homeServer, request: this._request, createTimeout: this._clock.createTimeout});
const request = this._platform.request;
const hsApi = new HomeServerApi({homeServer, request, createTimeout: clock.createTimeout});
const loginData = await hsApi.passwordLogin(username, password, "Hydrogen").response();
const sessionId = this.createNewSessionId();
sessionInfo = {
@ -107,9 +103,9 @@ export class SessionContainer {
userId: loginData.user_id,
homeServer: homeServer,
accessToken: loginData.access_token,
lastUsed: this._clock.now()
lastUsed: clock.now()
};
await this._sessionInfoStorage.add(sessionInfo);
await this._platform.sessionInfoStorage.add(sessionInfo);
} catch (err) {
this._error = err;
if (err instanceof HomeServerError) {
@ -139,22 +135,23 @@ export class SessionContainer {
}
async _loadSessionInfo(sessionInfo, isNewLogin) {
const clock = this._platform.clock;
this._sessionStartedByReconnector = false;
this._status.set(LoadStatus.Loading);
this._reconnector = new Reconnector({
onlineStatus: this._onlineStatus,
retryDelay: new ExponentialRetryDelay(this._clock.createTimeout),
createMeasure: this._clock.createMeasure
onlineStatus: this._platform.onlineStatus,
retryDelay: new ExponentialRetryDelay(clock.createTimeout),
createMeasure: clock.createMeasure
});
const hsApi = new HomeServerApi({
homeServer: sessionInfo.homeServer,
accessToken: sessionInfo.accessToken,
request: this._request,
request: this._platform.request,
reconnector: this._reconnector,
createTimeout: this._clock.createTimeout
createTimeout: clock.createTimeout
});
this._sessionId = sessionInfo.id;
this._storage = await this._storageFactory.create(sessionInfo.id);
this._storage = await this._platform.storageFactory.create(sessionInfo.id);
// no need to pass access token to session
const filteredSessionInfo = {
deviceId: sessionInfo.deviceId,
@ -166,22 +163,21 @@ export class SessionContainer {
if (this._workerPromise) {
olmWorker = await this._workerPromise;
}
this._requestScheduler = new RequestScheduler({hsApi, clock: this._clock});
this._requestScheduler = new RequestScheduler({hsApi, clock});
this._requestScheduler.start();
const mediaRepository = new MediaRepository({
homeServer: sessionInfo.homeServer,
cryptoDriver: this._cryptoDriver,
request: this._request,
crypto: this._platform.crypto,
request: this._platform.request,
});
this._session = new Session({
storage: this._storage,
sessionInfo: filteredSessionInfo,
hsApi: this._requestScheduler.hsApi,
olm,
clock: this._clock,
olmWorker,
cryptoDriver: this._cryptoDriver,
mediaRepository
mediaRepository,
platform: this._platform,
});
await this._session.load();
if (isNewLogin) {
@ -298,8 +294,8 @@ export class SessionContainer {
// if one fails, don't block the other from trying
// also, run in parallel
await Promise.all([
this._storageFactory.delete(this._sessionId),
this._sessionInfoStorage.delete(this._sessionId),
this._platform.storageFactory.delete(this._sessionId),
this._platform.sessionInfoStorage.delete(this._sessionId),
]);
this._sessionId = null;
}

View file

@ -25,7 +25,7 @@ import base64 from "../../../lib/base64-arraybuffer/index.js";
* @param {string} info.hashes.sha256 Base64 encoded SHA-256 hash of the ciphertext.
* @return {Promise} A promise that resolves with an ArrayBuffer when the attachment is decrypted.
*/
export async function decryptAttachment(cryptoDriver, ciphertextBuffer, info) {
export async function decryptAttachment(crypto, ciphertextBuffer, info) {
if (info === undefined || info.key === undefined || info.iv === undefined
|| info.hashes === undefined || info.hashes.sha256 === undefined) {
throw new Error("Invalid info. Missing info.key, info.iv or info.hashes.sha256 key");
@ -35,7 +35,7 @@ export async function decryptAttachment(cryptoDriver, ciphertextBuffer, info) {
// re-encode to not deal with padded vs unpadded
var expectedSha256base64 = base64.encode(base64.decode(info.hashes.sha256));
// Check the sha256 hash
const digestResult = await cryptoDriver.digest("SHA-256", ciphertextBuffer);
const digestResult = await crypto.digest("SHA-256", ciphertextBuffer);
if (base64.encode(new Uint8Array(digestResult)) != expectedSha256base64) {
throw new Error("Mismatched SHA-256 digest");
}
@ -48,7 +48,7 @@ export async function decryptAttachment(cryptoDriver, ciphertextBuffer, info) {
counterLength = 128;
}
const decryptedBuffer = await cryptoDriver.aes.decryptCTR({
const decryptedBuffer = await crypto.aes.decryptCTR({
jwkKey: info.key,
iv: ivArray,
data: ciphertextBuffer,

View file

@ -18,9 +18,9 @@ import {encodeQueryParams} from "./common.js";
import {decryptAttachment} from "../e2ee/attachment.js";
export class MediaRepository {
constructor({homeServer, cryptoDriver, request}) {
constructor({homeServer, crypto, request}) {
this._homeServer = homeServer;
this._cryptoDriver = cryptoDriver;
this._crypto = crypto;
this._request = request;
}
@ -56,7 +56,7 @@ export class MediaRepository {
async downloadEncryptedFile(fileEntry) {
const url = this.mxcUrl(fileEntry.url);
const {body: encryptedBuffer} = await this._request(url, {format: "buffer", cache: true}).response();
const decryptedBuffer = await decryptAttachment(this._cryptoDriver, encryptedBuffer, fileEntry);
const decryptedBuffer = await decryptAttachment(this._crypto, encryptedBuffer, fileEntry);
return decryptedBuffer;
}
}

View file

@ -26,25 +26,3 @@ export function encodeQueryParams(queryParams) {
})
.join("&");
}
export function addCacheBuster(urlStr, random = Math.random) {
// XHR doesn't have a good way to disable cache,
// so add a random query param
// see https://davidtranscend.com/blog/prevent-ie11-cache-ajax-requests/
if (urlStr.includes("?")) {
urlStr = urlStr + "&";
} else {
urlStr = urlStr + "?";
}
return urlStr + `_cacheBuster=${Math.ceil(random() * Number.MAX_SAFE_INTEGER)}`;
}
export function tests() {
return {
"add cache buster": assert => {
const random = () => 0.5;
assert.equal(addCacheBuster("http://foo", random), "http://foo?_cacheBuster=4503599627370496");
assert.equal(addCacheBuster("http://foo?bar=baz", random), "http://foo?bar=baz&_cacheBuster=4503599627370496");
}
}
}

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {Platform} from "../../../Platform.js";
import {KeyLimits} from "../../storage/common.js";
// key for events in the timelineEvents store
export class EventKey {
@ -25,7 +25,7 @@ export class EventKey {
nextFragmentKey() {
// could take MIN_EVENT_INDEX here if it can't be paged back
return new EventKey(this.fragmentId + 1, Platform.middleStorageKey);
return new EventKey(this.fragmentId + 1, KeyLimits.middleStorageKey);
}
nextKeyForDirection(direction) {
@ -45,19 +45,19 @@ export class EventKey {
}
static get maxKey() {
return new EventKey(Platform.maxStorageKey, Platform.maxStorageKey);
return new EventKey(KeyLimits.maxStorageKey, KeyLimits.maxStorageKey);
}
static get minKey() {
return new EventKey(Platform.minStorageKey, Platform.minStorageKey);
return new EventKey(KeyLimits.minStorageKey, KeyLimits.minStorageKey);
}
static get defaultLiveKey() {
return EventKey.defaultFragmentKey(Platform.minStorageKey);
return EventKey.defaultFragmentKey(KeyLimits.minStorageKey);
}
static defaultFragmentKey(fragmentId) {
return new EventKey(fragmentId, Platform.middleStorageKey);
return new EventKey(fragmentId, KeyLimits.middleStorageKey);
}
toString() {

View file

@ -17,7 +17,7 @@ limitations under the License.
import {BaseEntry} from "./BaseEntry.js";
import {Direction} from "../Direction.js";
import {isValidFragmentId} from "../common.js";
import {Platform} from "../../../../Platform.js";
import {KeyLimits} from "../../../storage/common.js";
export class FragmentBoundaryEntry extends BaseEntry {
constructor(fragment, isFragmentStart, fragmentIdComparer) {
@ -53,9 +53,9 @@ export class FragmentBoundaryEntry extends BaseEntry {
get entryIndex() {
if (this.started) {
return Platform.minStorageKey;
return KeyLimits.minStorageKey;
} else {
return Platform.maxStorageKey;
return KeyLimits.maxStorageKey;
}
}

View file

@ -17,9 +17,9 @@ limitations under the License.
import base64 from "../../../lib/base64-arraybuffer/index.js";
export class SecretStorage {
constructor({key, cryptoDriver}) {
constructor({key, crypto}) {
this._key = key;
this._cryptoDriver = cryptoDriver;
this._crypto = crypto;
}
async readSecret(name, txn) {
@ -44,7 +44,7 @@ export class SecretStorage {
const textEncoder = new TextEncoder();
const textDecoder = new TextDecoder();
// now derive the aes and mac key from the 4s key
const hkdfKey = await this._cryptoDriver.derive.hkdf(
const hkdfKey = await this._crypto.derive.hkdf(
this._key.binaryKey,
new Uint8Array(8).buffer, //zero salt
textEncoder.encode(type), // info
@ -56,7 +56,7 @@ export class SecretStorage {
const ciphertextBytes = base64.decode(encryptedData.ciphertext);
const isVerified = await this._cryptoDriver.hmac.verify(
const isVerified = await this._crypto.hmac.verify(
hmacKey, base64.decode(encryptedData.mac),
ciphertextBytes, "SHA-256");
@ -64,7 +64,7 @@ export class SecretStorage {
throw new Error("Bad MAC");
}
const plaintextBytes = await this._cryptoDriver.aes.decryptCTR({
const plaintextBytes = await this._crypto.aes.decryptCTR({
key: aesKey,
iv: base64.decode(encryptedData.iv),
data: ciphertextBytes

View file

@ -47,14 +47,14 @@ export async function readKey(txn) {
return new Key(new KeyDescription(keyData.id, keyAccountData), keyData.binaryKey);
}
export async function keyFromCredential(type, credential, storage, cryptoDriver, olm) {
export async function keyFromCredential(type, credential, storage, crypto, olm) {
const keyDescription = await readDefaultKeyDescription(storage);
if (!keyDescription) {
throw new Error("Could not find a default secret storage key in account data");
}
let key;
if (type === "phrase") {
key = await keyFromPassphrase(keyDescription, credential, cryptoDriver);
key = await keyFromPassphrase(keyDescription, credential, crypto);
} else if (type === "key") {
key = keyFromRecoveryKey(olm, keyDescription, credential);
} else {

View file

@ -22,10 +22,10 @@ const DEFAULT_BITSIZE = 256;
/**
* @param {KeyDescription} keyDescription
* @param {string} passphrase
* @param {CryptoDriver} cryptoDriver
* @param {Crypto} crypto
* @return {Key}
*/
export async function keyFromPassphrase(keyDescription, passphrase, cryptoDriver) {
export async function keyFromPassphrase(keyDescription, passphrase, crypto) {
const {passphraseParams} = keyDescription;
if (!passphraseParams) {
throw new Error("not a passphrase key");
@ -35,7 +35,7 @@ export async function keyFromPassphrase(keyDescription, passphrase, cryptoDriver
}
// TODO: we should we move this to platform specific code
const textEncoder = new TextEncoder();
const keyBits = await cryptoDriver.derive.pbkdf2(
const keyBits = await crypto.derive.pbkdf2(
textEncoder.encode(passphrase),
passphraseParams.iterations || DEFAULT_ITERATIONS,
// salt is just a random string, not encoded in any way

View file

@ -50,3 +50,20 @@ export class StorageError extends Error {
return "StorageError";
}
}
export const KeyLimits = {
get minStorageKey() {
// for indexeddb, we use unsigned 32 bit integers as keys
return 0;
},
get middleStorageKey() {
// for indexeddb, we use unsigned 32 bit integers as keys
return 0x7FFFFFFF;
},
get maxStorageKey() {
// for indexeddb, we use unsigned 32 bit integers as keys
return 0xFFFFFFFF;
}
}

View file

@ -15,7 +15,7 @@ limitations under the License.
*/
import { encodeUint32, decodeUint32 } from "../utils.js";
import {Platform} from "../../../../Platform.js";
import {KeyLimits} from "../../common.js";
function encodeKey(roomId, queueIndex) {
return `${roomId}|${encodeUint32(queueIndex)}`;
@ -34,8 +34,8 @@ export class PendingEventStore {
async getMaxQueueIndex(roomId) {
const range = IDBKeyRange.bound(
encodeKey(roomId, Platform.minStorageKey),
encodeKey(roomId, Platform.maxStorageKey),
encodeKey(roomId, KeyLimits.minStorageKey),
encodeKey(roomId, KeyLimits.maxStorageKey),
false,
false,
);

View file

@ -17,7 +17,7 @@ limitations under the License.
import {EventKey} from "../../../room/timeline/EventKey.js";
import { StorageError } from "../../common.js";
import { encodeUint32 } from "../utils.js";
import {Platform} from "../../../../Platform.js";
import {KeyLimits} from "../../common.js";
function encodeKey(roomId, fragmentId, eventIndex) {
return `${roomId}|${encodeUint32(fragmentId)}|${encodeUint32(eventIndex)}`;
@ -52,7 +52,7 @@ class Range {
if (this._lower && !this._upper) {
return IDBKeyRange.bound(
encodeKey(roomId, this._lower.fragmentId, this._lower.eventIndex),
encodeKey(roomId, this._lower.fragmentId, Platform.maxStorageKey),
encodeKey(roomId, this._lower.fragmentId, KeyLimits.maxStorageKey),
this._lowerOpen,
false
);
@ -61,7 +61,7 @@ class Range {
// also bound as we don't want to move into another roomId
if (!this._lower && this._upper) {
return IDBKeyRange.bound(
encodeKey(roomId, this._upper.fragmentId, Platform.minStorageKey),
encodeKey(roomId, this._upper.fragmentId, KeyLimits.minStorageKey),
encodeKey(roomId, this._upper.fragmentId, this._upper.eventIndex),
false,
this._upperOpen

View file

@ -15,7 +15,7 @@ limitations under the License.
*/
import { StorageError } from "../../common.js";
import {Platform} from "../../../../Platform.js";
import {KeyLimits} from "../../common.js";
import { encodeUint32 } from "../utils.js";
function encodeKey(roomId, fragmentId) {
@ -30,8 +30,8 @@ export class TimelineFragmentStore {
_allRange(roomId) {
try {
return IDBKeyRange.bound(
encodeKey(roomId, Platform.minStorageKey),
encodeKey(roomId, Platform.maxStorageKey)
encodeKey(roomId, KeyLimits.minStorageKey),
encodeKey(roomId, KeyLimits.maxStorageKey)
);
} catch (err) {
throw new StorageError(`error from IDBKeyRange with roomId ${roomId}`, err);

View file

@ -48,7 +48,7 @@ export async function checkNeedsSyncPromise() {
return needsSyncPromise;
}
// storage keys are defined to be unsigned 32bit numbers in WebPlatform.js, which is assumed by idb
// storage keys are defined to be unsigned 32bit numbers in KeyLimits, which is assumed by idb
export function encodeUint32(n) {
const hex = n.toString(16);
return "0".repeat(8 - hex.length) + hex;

View file

@ -0,0 +1,7 @@
import aesjs from "../../../lib/aes-js/index.js";
import {hkdf} from "../../utils/crypto/hkdf.js";
import {Platform as ModernPlatform} from "./Platform.js";
export function Platform(container, paths) {
return new ModernPlatform(container, paths, {aesjs, hkdf});
}

View file

@ -0,0 +1,130 @@
/*
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 {createFetchRequest} from "./dom/request/fetch.js";
import {xhrRequest} from "./dom/request/xhr.js";
import {StorageFactory} from "../../matrix/storage/idb/StorageFactory.js";
import {SessionInfoStorage} from "../../matrix/sessioninfo/localstorage/SessionInfoStorage.js";
import {OlmWorker} from "../../matrix/e2ee/OlmWorker.js";
import {RootView} from "./ui/RootView.js";
import {Clock} from "./dom/Clock.js";
import {ServiceWorkerHandler} from "./dom/ServiceWorkerHandler.js";
import {History} from "./dom/History.js";
import {OnlineStatus} from "./dom/OnlineStatus.js";
import {Crypto} from "./dom/Crypto.js";
import {estimateStorageUsage} from "./dom/StorageEstimate.js";
import {WorkerPool} from "./dom/WorkerPool.js";
function addScript(src) {
return new Promise(function (resolve, reject) {
var s = document.createElement("script");
s.setAttribute("src", src );
s.onload=resolve;
s.onerror=reject;
document.body.appendChild(s);
});
}
async function loadOlm(olmPaths) {
// make crypto.getRandomValues available without
// a prefix on IE11, needed by olm to work
if (window.msCrypto && !window.crypto) {
window.crypto = window.msCrypto;
}
if (olmPaths) {
if (window.WebAssembly) {
await addScript(olmPaths.wasmBundle);
await window.Olm.init({locateFile: () => olmPaths.wasm});
} else {
await addScript(olmPaths.legacyBundle);
await window.Olm.init();
}
return window.Olm;
}
return null;
}
// make path relative to basePath,
// assuming it and basePath are relative to document
function relPath(path, basePath) {
const idx = basePath.lastIndexOf("/");
const dir = idx === -1 ? "" : basePath.slice(0, idx);
const dirCount = dir.length ? dir.split("/").length : 0;
return "../".repeat(dirCount) + path;
}
async function loadOlmWorker(paths) {
const workerPool = new WorkerPool(paths.worker, 4);
await workerPool.init();
const path = relPath(paths.olm.legacyBundle, paths.worker);
await workerPool.sendAll({type: "load_olm", path});
const olmWorker = new OlmWorker(workerPool);
return olmWorker;
}
export class Platform {
constructor(container, paths, cryptoExtras = null) {
this._paths = paths;
this._container = container;
this.clock = new Clock();
this.history = new History();
this.onlineStatus = new OnlineStatus();
this._serviceWorkerHandler = null;
if (paths.serviceWorker && "serviceWorker" in navigator) {
this._serviceWorkerHandler = new ServiceWorkerHandler();
this._serviceWorkerHandler.registerAndStart(paths.serviceWorker);
}
this.crypto = new Crypto(cryptoExtras);
this.storageFactory = new StorageFactory(this._serviceWorkerHandler);
this.sessionInfoStorage = new SessionInfoStorage("hydrogen_sessions_v1");
this.estimateStorageUsage = estimateStorageUsage;
this.random = Math.random;
if (typeof fetch === "function") {
this.request = createFetchRequest(this.clock.createTimeout);
} else {
this.request = xhrRequest;
}
}
get updateService() {
return this._serviceWorkerHandler;
}
loadOlm() {
return loadOlm(this._paths.olm);
}
async loadOlmWorker() {
if (!window.WebAssembly) {
return await loadOlmWorker(this._paths);
}
}
createAndMountRootView(vm) {
const isIE11 = !!window.MSInputMethodContext && !!document.documentMode;
if (isIE11) {
this._container.className += " legacy";
}
window.__hydrogenViewModel = vm;
const view = new RootView(vm);
this._container.appendChild(view.mount());
}
setNavigation(navigation) {
this._serviceWorkerHandler?.setNavigation(navigation);
}
}

View file

@ -26,7 +26,7 @@ function subtleCryptoResult(promiseOrOp, method) {
}
}
class CryptoHMACDriver {
class HMACCrypto {
constructor(subtleCrypto) {
this._subtleCrypto = subtleCrypto;
}
@ -80,10 +80,10 @@ class CryptoHMACDriver {
}
}
class CryptoDeriveDriver {
constructor(subtleCrypto, cryptoDriver, cryptoExtras) {
class DeriveCrypto {
constructor(subtleCrypto, crypto, cryptoExtras) {
this._subtleCrypto = subtleCrypto;
this._cryptoDriver = cryptoDriver;
this._crypto = crypto;
this._cryptoExtras = cryptoExtras;
}
/**
@ -130,7 +130,7 @@ class CryptoDeriveDriver {
*/
async hkdf(key, salt, info, hash, length) {
if (!this._subtleCrypto.deriveBits) {
return this._cryptoExtras.hkdf(this._cryptoDriver, key, salt, info, hash, length);
return this._cryptoExtras.hkdf(this._crypto, key, salt, info, hash, length);
}
const hkdfkey = await subtleCryptoResult(this._subtleCrypto.importKey(
'raw',
@ -152,7 +152,7 @@ class CryptoDeriveDriver {
}
}
class CryptoAESDriver {
class AESCrypto {
constructor(subtleCrypto) {
this._subtleCrypto = subtleCrypto;
}
@ -200,7 +200,7 @@ class CryptoAESDriver {
}
class CryptoLegacyAESDriver {
class AESLegacyCrypto {
constructor(aesjs) {
this._aesjs = aesjs;
}
@ -237,20 +237,20 @@ function hashName(name) {
return name;
}
export class CryptoDriver {
export class Crypto {
constructor(cryptoExtras) {
const crypto = window.crypto || window.msCrypto;
const subtleCrypto = crypto.subtle || crypto.webkitSubtle;
this._subtleCrypto = subtleCrypto;
// not exactly guaranteeing AES-CTR support
// but in practice IE11 doesn't have this
if (!subtleCrypto.deriveBits && cryptoExtras.aesjs) {
this.aes = new CryptoLegacyAESDriver(cryptoExtras.aesjs);
if (!subtleCrypto.deriveBits && cryptoExtras?.aesjs) {
this.aes = new AESLegacyCrypto(cryptoExtras.aesjs);
} else {
this.aes = new CryptoAESDriver(subtleCrypto);
this.aes = new AESCrypto(subtleCrypto);
}
this.hmac = new CryptoHMACDriver(subtleCrypto);
this.derive = new CryptoDeriveDriver(subtleCrypto, this, cryptoExtras);
this.hmac = new HMACCrypto(subtleCrypto);
this.derive = new DeriveCrypto(subtleCrypto, this, cryptoExtras);
}
/**

View file

@ -19,15 +19,19 @@ limitations under the License.
// - UpdateService (see checkForUpdate method, and should also emit events rather than showing confirm dialog here)
// - ConcurrentAccessBlocker (see preventConcurrentSessionAccess method)
export class ServiceWorkerHandler {
constructor({navigation}) {
constructor() {
this._waitingForReply = new Map();
this._messageIdCounter = 0;
this._navigation = null;
this._registration = null;
this._navigation = navigation;
this._registrationPromise = null;
this._currentController = null;
}
setNavigation(navigation) {
this._navigation = navigation;
}
registerAndStart(path) {
this._registrationPromise = (async () => {
navigator.serviceWorker.addEventListener("message", this);
@ -61,7 +65,7 @@ export class ServiceWorkerHandler {
}
_closeSessionIfNeeded(sessionId) {
const currentSession = this._navigation.path.get("session");
const currentSession = this._navigation?.path.get("session");
if (sessionId && currentSession?.value === sessionId) {
return new Promise(resolve => {
const unsubscribe = this._navigation.pathObservable.subscribe(path => {

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {AbortError} from "./error.js";
import {AbortError} from "../../../utils/error.js";
class WorkerState {
constructor(worker) {

View file

@ -0,0 +1,38 @@
/*
Copyright 2020 Bruno Windels <bruno@windels.cloud>
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.
*/
export function addCacheBuster(urlStr, random = Math.random) {
// XHR doesn't have a good way to disable cache,
// so add a random query param
// see https://davidtranscend.com/blog/prevent-ie11-cache-ajax-requests/
if (urlStr.includes("?")) {
urlStr = urlStr + "&";
} else {
urlStr = urlStr + "?";
}
return urlStr + `_cacheBuster=${Math.ceil(random() * Number.MAX_SAFE_INTEGER)}`;
}
export function tests() {
return {
"add cache buster": assert => {
const random = () => 0.5;
assert.equal(addCacheBuster("http://foo", random), "http://foo?_cacheBuster=4503599627370496");
assert.equal(addCacheBuster("http://foo?bar=baz", random), "http://foo?bar=baz&_cacheBuster=4503599627370496");
}
}
}

View file

@ -18,9 +18,9 @@ limitations under the License.
import {
AbortError,
ConnectionError
} from "../../error.js";
import {abortOnTimeout} from "../timeout.js";
import {addCacheBuster} from "../common.js";
} from "../../../../matrix/error.js";
import {abortOnTimeout} from "./timeout.js";
import {addCacheBuster} from "./common.js";
class RequestResult {
constructor(promise, controller) {

View file

@ -18,7 +18,7 @@ limitations under the License.
import {
AbortError,
ConnectionError
} from "../error.js";
} from "../../../../matrix/error.js";
export function abortOnTimeout(createTimeout, timeoutAmount, requestResult, responsePromise) {

View file

@ -17,8 +17,8 @@ limitations under the License.
import {
AbortError,
ConnectionError
} from "../../error.js";
import {addCacheBuster} from "../common.js";
} from "../../../../matrix/error.js";
import {addCacheBuster} from "./common.js";
class RequestResult {
constructor(promise, xhr) {

View file

@ -15,8 +15,8 @@ limitations under the License.
*/
// polyfills needed for IE11
import Promise from "../lib/es6-promise/index.js";
import {checkNeedsSyncPromise} from "./matrix/storage/idb/utils.js";
import Promise from "../../../lib/es6-promise/index.js";
import {checkNeedsSyncPromise} from "../../matrix/storage/idb/utils.js";
if (typeof window.Promise === "undefined") {
window.Promise = Promise;

View file

@ -14,8 +14,10 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
const container = document.querySelector(".hydrogen");
export function spinner(t, extraClasses = undefined) {
if (document.body.classList.contains("ie11")) {
if (container.classList.contains("legacy")) {
return t.div({className: "spinner"}, [
t.div(),
t.div(),

View file

@ -32,7 +32,7 @@ limitations under the License.
}
}
.not-ie11 .spinner circle {
.hydrogen:not(.legacy) .spinner circle {
transform-origin: 50% 50%;
animation-name: spinner;
animation-duration: 2s;
@ -45,35 +45,35 @@ limitations under the License.
stroke-linecap: butt;
}
.ie11 .spinner {
.hydrogen.legacy .spinner {
display: inline-block;
position: relative;
}
.ie11 .spinner div {
.hydrogen.legacy .spinner div {
box-sizing: border-box;
display: block;
position: absolute;
padding: 2px;
border: 2px solid currentcolor;
border-radius: 50%;
animation: ie-spinner 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
animation: legacy-spinner 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
border-color: currentcolor transparent transparent transparent;
width: var(--size);
height: var(--size);
}
.ie11 .spinner div:nth-child(1) {
.hydrogen.legacy .spinner div:nth-child(1) {
animation-delay: -0.45s;
}
.ie11 .spinner div:nth-child(2) {
.hydrogen.legacy .spinner div:nth-child(2) {
animation-delay: -0.3s;
}
.ie11 .spinner div:nth-child(3) {
.hydrogen.legacy .spinner div:nth-child(3) {
animation-delay: -0.15s;
}
@keyframes ie-spinner {
@keyframes legacy-spinner {
0% {
transform: rotate(0deg);
}

View file

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

Before

Width:  |  Height:  |  Size: 657 B

After

Width:  |  Height:  |  Size: 657 B

View file

Before

Width:  |  Height:  |  Size: 212 B

After

Width:  |  Height:  |  Size: 212 B

View file

Before

Width:  |  Height:  |  Size: 307 B

After

Width:  |  Height:  |  Size: 307 B

View file

Before

Width:  |  Height:  |  Size: 621 B

After

Width:  |  Height:  |  Size: 621 B

View file

Before

Width:  |  Height:  |  Size: 307 B

After

Width:  |  Height:  |  Size: 307 B

View file

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

Before

Width:  |  Height:  |  Size: 785 B

After

Width:  |  Height:  |  Size: 785 B

View file

Before

Width:  |  Height:  |  Size: 621 B

After

Width:  |  Height:  |  Size: 621 B

View file

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Some files were not shown because too many files have changed in this diff Show more