Merge pull request #176 from vector-im/bwindels/extract-platform
Extract platform code
27
index.html
@ -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>
|
||||
|
@ -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));
|
||||
|
@ -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";
|
@ -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();
|
||||
});
|
||||
}
|
||||
|
@ -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() {
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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() {
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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() {
|
||||
|
@ -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}};
|
120
src/main.js
@ -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}`);
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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,16 +163,15 @@ 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();
|
||||
this._session = new Session({
|
||||
storage: this._storage,
|
||||
sessionInfo: filteredSessionInfo,
|
||||
hsApi: this._requestScheduler.hsApi,
|
||||
olm,
|
||||
clock: this._clock,
|
||||
olmWorker,
|
||||
cryptoDriver: this._cryptoDriver,
|
||||
platform: this._platform,
|
||||
mediaRepository: new MediaRepository(sessionInfo.homeServer)
|
||||
});
|
||||
await this._session.load();
|
||||
@ -293,8 +289,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;
|
||||
}
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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() {
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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.decrypt(
|
||||
const plaintextBytes = await this._crypto.aes.decrypt(
|
||||
aesKey, base64.decode(encryptedData.iv), ciphertextBytes);
|
||||
|
||||
return textDecoder.decode(plaintextBytes);
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
);
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
7
src/platform/web/LegacyPlatform.js
Normal 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});
|
||||
}
|
130
src/platform/web/Platform.js
Normal 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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
@ -196,7 +196,7 @@ class CryptoAESDriver {
|
||||
}
|
||||
|
||||
|
||||
class CryptoLegacyAESDriver {
|
||||
class AESLegacyCrypto {
|
||||
constructor(aesjs) {
|
||||
this._aesjs = aesjs;
|
||||
}
|
||||
@ -221,20 +221,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);
|
||||
}
|
||||
|
||||
/**
|
@ -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 => {
|
@ -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) {
|
38
src/platform/web/dom/request/common.js
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
@ -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) {
|
@ -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) {
|
@ -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) {
|
@ -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;
|
@ -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(),
|
@ -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);
|
||||
}
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 657 B After Width: | Height: | Size: 657 B |
Before Width: | Height: | Size: 212 B After Width: | Height: | Size: 212 B |
Before Width: | Height: | Size: 307 B After Width: | Height: | Size: 307 B |
Before Width: | Height: | Size: 621 B After Width: | Height: | Size: 621 B |
Before Width: | Height: | Size: 307 B After Width: | Height: | Size: 307 B |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 785 B After Width: | Height: | Size: 785 B |
Before Width: | Height: | Size: 621 B After Width: | Height: | Size: 621 B |
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |