Merge branch 'master' into bwindels/decrypt-images
27
index.html
|
@ -1,7 +1,7 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, user-scalable=no">
|
<meta name="viewport" content="width=device-width, user-scalable=no">
|
||||||
<meta name="application-name" content="Hydrogen Chat"/>
|
<meta name="application-name" content="Hydrogen Chat"/>
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
<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="apple-mobile-web-app-title" content="Hydrogen Chat">
|
||||||
<meta name="description" content="A matrix chat application">
|
<meta name="description" content="A matrix chat application">
|
||||||
<link rel="apple-touch-icon" href="assets/icon-maskable.png">
|
<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/platform/web/ui/css/main.css">
|
||||||
<link rel="stylesheet" type="text/css" href="src/ui/web/css/themes/element/theme.css" title="Element Theme">
|
<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/ui/web/css/themes/bubbles/theme.css" title="Bubbles Theme">
|
<link rel="alternate stylesheet" type="text/css" href="src/platform/web/ui/css/themes/bubbles/theme.css" title="Bubbles Theme">
|
||||||
</head>
|
</head>
|
||||||
<body class="hydrogen">
|
<body class="hydrogen">
|
||||||
<script id="version" type="disabled">
|
<script id="version" type="disabled">
|
||||||
window.HYDROGEN_VERSION = "%%VERSION%%";
|
window.HYDROGEN_VERSION = "%%VERSION%%";
|
||||||
window.HYDROGEN_GLOBAL_HASH = "%%GLOBAL_HASH%%";
|
window.HYDROGEN_GLOBAL_HASH = "%%GLOBAL_HASH%%";
|
||||||
</script>
|
</script>
|
||||||
<script id="main" type="module">
|
<script id="main" type="module">
|
||||||
import {main} from "./src/main.js";
|
import {main} from "./src/main.js";
|
||||||
main(document.body, {
|
import {Platform} from "./src/platform/web/Platform.js";
|
||||||
|
main(new Platform(document.body, {
|
||||||
worker: "src/worker.js",
|
worker: "src/worker.js",
|
||||||
olm: {
|
olm: {
|
||||||
wasm: "lib/olm/olm.wasm",
|
wasm: "lib/olm/olm.wasm",
|
||||||
legacyBundle: "lib/olm/olm_legacy.js",
|
legacyBundle: "lib/olm/olm_legacy.js",
|
||||||
wasmBundle: "lib/olm/olm.js",
|
wasmBundle: "lib/olm/olm.js",
|
||||||
}
|
}
|
||||||
});
|
}));
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "hydrogen-web",
|
"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",
|
"description": "A javascript matrix client prototype, trying to minize RAM usage by offloading as much as possible to IndexedDB",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"directories": {
|
"directories": {
|
||||||
|
|
|
@ -45,12 +45,12 @@ import flexbugsFixes from "postcss-flexbugs-fixes";
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = dirname(__filename);
|
const __dirname = dirname(__filename);
|
||||||
const projectDir = path.join(__dirname, "../");
|
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();
|
const parameters = new commander.Command();
|
||||||
program
|
parameters
|
||||||
.option("--modern-only", "don't make a legacy build")
|
.option("--modern-only", "don't make a legacy build")
|
||||||
program.parse(process.argv);
|
parameters.parse(process.argv);
|
||||||
|
|
||||||
async function build({modernOnly}) {
|
async function build({modernOnly}) {
|
||||||
// get version number
|
// get version number
|
||||||
|
@ -70,10 +70,13 @@ async function build({modernOnly}) {
|
||||||
// copy olm assets
|
// copy olm assets
|
||||||
const olmAssets = await copyFolder(path.join(projectDir, "lib/olm/"), assets.directory);
|
const olmAssets = await copyFolder(path.join(projectDir, "lib/olm/"), assets.directory);
|
||||||
assets.addSubMap(olmAssets);
|
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) {
|
if (!modernOnly) {
|
||||||
await assets.write(`hydrogen-legacy.js`, await buildJsLegacy("src/main.js", ['src/legacy-polyfill.js', 'src/legacy-extras.js']));
|
await assets.write(`hydrogen-legacy.js`, await buildJsLegacy("src/main.js", [
|
||||||
await assets.write(`worker.js`, await buildJsLegacy("src/worker.js", ['src/worker-polyfill.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,
|
// 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
|
// 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);
|
await buildManifest(assets);
|
||||||
// all assets have been added, create a hash from all assets name to cache unhashed files like index.html
|
// all assets have been added, create a hash from all assets name to cache unhashed files like index.html
|
||||||
assets.addToHashForAll("index.html", devHtml);
|
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);
|
assets.addToHashForAll("sw.js", swSource);
|
||||||
|
|
||||||
const globalHash = assets.hashForAll();
|
const globalHash = assets.hashForAll();
|
||||||
|
@ -148,12 +151,12 @@ async function buildHtml(doc, version, globalHash, modernOnly, assets) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const mainScripts = [
|
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) {
|
if (!modernOnly) {
|
||||||
mainScripts.push(
|
mainScripts.push(
|
||||||
`<script type="text/javascript" nomodule src="${assets.resolve(`hydrogen-legacy.js`)}"></script>`,
|
`<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(""));
|
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());
|
await assets.writeUnhashed("index.html", doc.html());
|
||||||
}
|
}
|
||||||
|
|
||||||
async function buildJs(inputFile) {
|
async function buildJs(mainFile, extraFiles = []) {
|
||||||
// create js bundle
|
// create js bundle
|
||||||
const bundle = await rollup({
|
const bundle = await rollup({
|
||||||
input: inputFile,
|
input: extraFiles.concat(mainFile),
|
||||||
plugins: [removeJsComments({comments: "none"})]
|
plugins: [multi(), removeJsComments({comments: "none"})]
|
||||||
});
|
});
|
||||||
const {output} = await bundle.generate({
|
const {output} = await bundle.generate({
|
||||||
format: 'es',
|
format: 'es',
|
||||||
// TODO: can remove this?
|
// TODO: can remove this?
|
||||||
name: `hydrogenBundle`
|
name: `hydrogen`
|
||||||
});
|
});
|
||||||
const code = output[0].code;
|
const code = output[0].code;
|
||||||
return code;
|
return code;
|
||||||
|
@ -214,7 +217,7 @@ async function buildJsLegacy(mainFile, extraFiles = []) {
|
||||||
const bundle = await rollup(rollupConfig);
|
const bundle = await rollup(rollupConfig);
|
||||||
const {output} = await bundle.generate({
|
const {output} = await bundle.generate({
|
||||||
format: 'iife',
|
format: 'iife',
|
||||||
name: `hydrogenBundle`
|
name: `hydrogen`
|
||||||
});
|
});
|
||||||
const code = output[0].code;
|
const code = output[0].code;
|
||||||
return 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 {
|
export class RootViewModel extends ViewModel {
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
super(options);
|
super(options);
|
||||||
const {createSessionContainer, sessionInfoStorage, storageFactory} = options;
|
this._createSessionContainer = options.createSessionContainer;
|
||||||
this._createSessionContainer = createSessionContainer;
|
|
||||||
this._sessionInfoStorage = sessionInfoStorage;
|
|
||||||
this._storageFactory = storageFactory;
|
|
||||||
this._error = null;
|
this._error = null;
|
||||||
this._sessionPickerViewModel = null;
|
this._sessionPickerViewModel = null;
|
||||||
this._sessionLoadViewModel = null;
|
this._sessionLoadViewModel = null;
|
||||||
|
@ -73,7 +70,7 @@ export class RootViewModel extends ViewModel {
|
||||||
if (restoreUrlIfAtDefault) {
|
if (restoreUrlIfAtDefault) {
|
||||||
this.urlCreator.pushUrl(restoreUrlIfAtDefault);
|
this.urlCreator.pushUrl(restoreUrlIfAtDefault);
|
||||||
} else {
|
} else {
|
||||||
const sessionInfos = await this._sessionInfoStorage.getAll();
|
const sessionInfos = await this.platform.sessionInfoStorage.getAll();
|
||||||
if (sessionInfos.length === 0) {
|
if (sessionInfos.length === 0) {
|
||||||
this.navigation.push("login");
|
this.navigation.push("login");
|
||||||
} else if (sessionInfos.length === 1) {
|
} else if (sessionInfos.length === 1) {
|
||||||
|
@ -90,10 +87,7 @@ export class RootViewModel extends ViewModel {
|
||||||
|
|
||||||
async _showPicker() {
|
async _showPicker() {
|
||||||
this._setSection(() => {
|
this._setSection(() => {
|
||||||
this._sessionPickerViewModel = new SessionPickerViewModel(this.childOptions({
|
this._sessionPickerViewModel = new SessionPickerViewModel(this.childOptions());
|
||||||
sessionInfoStorage: this._sessionInfoStorage,
|
|
||||||
storageFactory: this._storageFactory,
|
|
||||||
}));
|
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
await this._sessionPickerViewModel.load();
|
await this._sessionPickerViewModel.load();
|
||||||
|
@ -125,11 +119,7 @@ export class RootViewModel extends ViewModel {
|
||||||
|
|
||||||
_showSession(sessionContainer) {
|
_showSession(sessionContainer) {
|
||||||
this._setSection(() => {
|
this._setSection(() => {
|
||||||
this._sessionViewModel = new SessionViewModel(this.childOptions({
|
this._sessionViewModel = new SessionViewModel(this.childOptions({sessionContainer}));
|
||||||
sessionContainer,
|
|
||||||
updateService: this.getOption("updateService"),
|
|
||||||
estimateStorageUsage: this.getOption("estimateStorageUsage"),
|
|
||||||
}));
|
|
||||||
this._sessionViewModel.start();
|
this._sessionViewModel.start();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -130,9 +130,6 @@ class SessionItemViewModel extends ViewModel {
|
||||||
export class SessionPickerViewModel extends ViewModel {
|
export class SessionPickerViewModel extends ViewModel {
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
super(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._sessions = new SortedArray((s1, s2) => s1.id.localeCompare(s2.id));
|
||||||
this._loadViewModel = null;
|
this._loadViewModel = null;
|
||||||
this._error = null;
|
this._error = null;
|
||||||
|
@ -140,7 +137,7 @@ export class SessionPickerViewModel extends ViewModel {
|
||||||
|
|
||||||
// this loads all the sessions
|
// this loads all the sessions
|
||||||
async load() {
|
async load() {
|
||||||
const sessions = await this._sessionInfoStorage.getAll();
|
const sessions = await this.platform.sessionInfoStorage.getAll();
|
||||||
this._sessions.setManyUnsorted(sessions.map(s => {
|
this._sessions.setManyUnsorted(sessions.map(s => {
|
||||||
return new SessionItemViewModel(this.childOptions({sessionInfo: s}), this);
|
return new SessionItemViewModel(this.childOptions({sessionInfo: s}), this);
|
||||||
}));
|
}));
|
||||||
|
@ -152,8 +149,8 @@ export class SessionPickerViewModel extends ViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
async _exportData(id) {
|
async _exportData(id) {
|
||||||
const sessionInfo = await this._sessionInfoStorage.get(id);
|
const sessionInfo = await this.platform.sessionInfoStorage.get(id);
|
||||||
const stores = await this._storageFactory.export(id);
|
const stores = await this.platform.storageFactory.export(id);
|
||||||
const data = {sessionInfo, stores};
|
const data = {sessionInfo, stores};
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
@ -164,8 +161,8 @@ export class SessionPickerViewModel extends ViewModel {
|
||||||
const {sessionInfo} = data;
|
const {sessionInfo} = data;
|
||||||
sessionInfo.comment = `Imported on ${new Date().toLocaleString()} from id ${sessionInfo.id}.`;
|
sessionInfo.comment = `Imported on ${new Date().toLocaleString()} from id ${sessionInfo.id}.`;
|
||||||
sessionInfo.id = this._createSessionContainer().createNewSessionId();
|
sessionInfo.id = this._createSessionContainer().createNewSessionId();
|
||||||
await this._storageFactory.import(sessionInfo.id, data.stores);
|
await this.platform.storageFactory.import(sessionInfo.id, data.stores);
|
||||||
await this._sessionInfoStorage.add(sessionInfo);
|
await this.platform.sessionInfoStorage.add(sessionInfo);
|
||||||
this._sessions.set(new SessionItemViewModel(sessionInfo, this));
|
this._sessions.set(new SessionItemViewModel(sessionInfo, this));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
alert(err.message);
|
alert(err.message);
|
||||||
|
@ -175,13 +172,13 @@ export class SessionPickerViewModel extends ViewModel {
|
||||||
|
|
||||||
async delete(id) {
|
async delete(id) {
|
||||||
const idx = this._sessions.array.findIndex(s => s.id === id);
|
const idx = this._sessions.array.findIndex(s => s.id === id);
|
||||||
await this._sessionInfoStorage.delete(id);
|
await this.platform.sessionInfoStorage.delete(id);
|
||||||
await this._storageFactory.delete(id);
|
await this.platform.storageFactory.delete(id);
|
||||||
this._sessions.remove(idx);
|
this._sessions.remove(idx);
|
||||||
}
|
}
|
||||||
|
|
||||||
async clear(id) {
|
async clear(id) {
|
||||||
await this._storageFactory.delete(id);
|
await this.platform.storageFactory.delete(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
get sessions() {
|
get sessions() {
|
||||||
|
|
|
@ -30,8 +30,8 @@ export class ViewModel extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
childOptions(explicitOptions) {
|
childOptions(explicitOptions) {
|
||||||
const {navigation, urlCreator, clock} = this._options;
|
const {navigation, urlCreator, platform} = this._options;
|
||||||
return Object.assign({navigation, urlCreator, clock}, explicitOptions);
|
return Object.assign({navigation, urlCreator, platform}, explicitOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
// makes it easier to pass through dependencies of a sub-view model
|
// 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() {
|
get clock() {
|
||||||
return this._options.clock;
|
return this._options.platform.clock;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -188,9 +188,7 @@ export class SessionViewModel extends ViewModel {
|
||||||
}
|
}
|
||||||
if (settingsOpen) {
|
if (settingsOpen) {
|
||||||
this._settingsViewModel = this.track(new SettingsViewModel(this.childOptions({
|
this._settingsViewModel = this.track(new SettingsViewModel(this.childOptions({
|
||||||
updateService: this.getOption("updateService"),
|
|
||||||
session: this._sessionContainer.session,
|
session: this._sessionContainer.session,
|
||||||
estimateStorageUsage: this.getOption("estimateStorageUsage")
|
|
||||||
})));
|
})));
|
||||||
this._settingsViewModel.load();
|
this._settingsViewModel.load();
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,7 +43,7 @@ export class TimelineViewModel extends ViewModel {
|
||||||
// once we support sending messages we could do
|
// once we support sending messages we could do
|
||||||
// timeline.entries.concat(timeline.pendingEvents)
|
// timeline.entries.concat(timeline.pendingEvents)
|
||||||
// for an ObservableList that also contains local echos
|
// 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() {
|
async load() {
|
||||||
|
|
|
@ -21,7 +21,6 @@ export class MessageTile extends SimpleTile {
|
||||||
constructor(options) {
|
constructor(options) {
|
||||||
super(options);
|
super(options);
|
||||||
this._mediaRepository = options.mediaRepository;
|
this._mediaRepository = options.mediaRepository;
|
||||||
this._clock = options.clock;
|
|
||||||
this._isOwn = this._entry.sender === options.ownUserId;
|
this._isOwn = this._entry.sender === options.ownUserId;
|
||||||
this._date = this._entry.timestamp ? new Date(this._entry.timestamp) : null;
|
this._date = this._entry.timestamp ? new Date(this._entry.timestamp) : null;
|
||||||
this._isContinuation = false;
|
this._isContinuation = false;
|
||||||
|
@ -88,8 +87,8 @@ export class MessageTile extends SimpleTile {
|
||||||
let isContinuation = false;
|
let isContinuation = false;
|
||||||
if (prev && prev instanceof MessageTile && prev.sender === this.sender) {
|
if (prev && prev instanceof MessageTile && prev.sender === this.sender) {
|
||||||
// timestamp is null for pending events
|
// timestamp is null for pending events
|
||||||
const myTimestamp = this._entry.timestamp || this._clock.now();
|
const myTimestamp = this._entry.timestamp || this.clock.now();
|
||||||
const otherTimestamp = prev._entry.timestamp || this._clock.now();
|
const otherTimestamp = prev._entry.timestamp || this.clock.now();
|
||||||
// other message was sent less than 5min ago
|
// other message was sent less than 5min ago
|
||||||
isContinuation = (myTimestamp - otherTimestamp) < (5 * 60 * 1000);
|
isContinuation = (myTimestamp - otherTimestamp) < (5 * 60 * 1000);
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,9 +23,9 @@ import {RoomMemberTile} from "./tiles/RoomMemberTile.js";
|
||||||
import {EncryptedEventTile} from "./tiles/EncryptedEventTile.js";
|
import {EncryptedEventTile} from "./tiles/EncryptedEventTile.js";
|
||||||
import {EncryptionEnabledTile} from "./tiles/EncryptionEnabledTile.js";
|
import {EncryptionEnabledTile} from "./tiles/EncryptionEnabledTile.js";
|
||||||
|
|
||||||
export function tilesCreator({room, ownUserId, clock}) {
|
export function tilesCreator({room, ownUserId, platform}) {
|
||||||
return function tilesCreator(entry, emitUpdate) {
|
return function tilesCreator(entry, emitUpdate) {
|
||||||
const options = {entry, emitUpdate, ownUserId, clock,
|
const options = {entry, emitUpdate, ownUserId, platform,
|
||||||
mediaRepository: room.mediaRepository};
|
mediaRepository: room.mediaRepository};
|
||||||
if (entry.isGap) {
|
if (entry.isGap) {
|
||||||
return new GapTile(options, room);
|
return new GapTile(options, room);
|
||||||
|
|
|
@ -35,12 +35,11 @@ export class SettingsViewModel extends ViewModel {
|
||||||
this._session = session;
|
this._session = session;
|
||||||
this._sessionBackupViewModel = this.track(new SessionBackupViewModel(this.childOptions({session})));
|
this._sessionBackupViewModel = this.track(new SessionBackupViewModel(this.childOptions({session})));
|
||||||
this._closeUrl = this.urlCreator.urlUntilSegment("session");
|
this._closeUrl = this.urlCreator.urlUntilSegment("session");
|
||||||
this._estimateStorageUsage = options.estimateStorageUsage;
|
|
||||||
this._estimate = null;
|
this._estimate = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async load() {
|
async load() {
|
||||||
this._estimate = await this._estimateStorageUsage();
|
this._estimate = await this.platform.estimateStorageUsage();
|
||||||
this.emitChange("");
|
this.emitChange("");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,18 +60,19 @@ export class SettingsViewModel extends ViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
get version() {
|
get version() {
|
||||||
if (this._updateService) {
|
const {updateService} = this.platform;
|
||||||
return `${this._updateService.version} (${this._updateService.buildHash})`;
|
if (updateService) {
|
||||||
|
return `${updateService.version} (${updateService.buildHash})`;
|
||||||
}
|
}
|
||||||
return this.i18n`development version`;
|
return this.i18n`development version`;
|
||||||
}
|
}
|
||||||
|
|
||||||
checkForUpdate() {
|
checkForUpdate() {
|
||||||
this._updateService?.checkForUpdate();
|
this.platform.updateService?.checkForUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
get showUpdateButton() {
|
get showUpdateButton() {
|
||||||
return !!this._updateService;
|
return !!this.platform.updateService;
|
||||||
}
|
}
|
||||||
|
|
||||||
get sessionBackupViewModel() {
|
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 {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 {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 {RootViewModel} from "./domain/RootViewModel.js";
|
||||||
import {createNavigation, createRouter} from "./domain/navigation/index.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,
|
// Don't use a default export here, as we use multiple entries during legacy build,
|
||||||
// which does not support default exports,
|
// which does not support default exports,
|
||||||
// see https://github.com/rollup/plugins/tree/master/packages/multi-entry
|
// see https://github.com/rollup/plugins/tree/master/packages/multi-entry
|
||||||
export async function main(container, paths, legacyExtras) {
|
export async function main(platform) {
|
||||||
try {
|
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:
|
// to replay:
|
||||||
// const fetchLog = await (await fetch("/fetchlogs/constrainterror.json")).json();
|
// const fetchLog = await (await fetch("/fetchlogs/constrainterror.json")).json();
|
||||||
// const replay = new ReplayRequester(fetchLog, {delay: false});
|
// 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 recorder = new RecordRequester(createFetchRequest(clock.createTimeout));
|
||||||
// const request = recorder.request;
|
// const request = recorder.request;
|
||||||
// window.getBrawlFetchLog = () => recorder.log();
|
// 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 navigation = createNavigation();
|
||||||
const sessionInfoStorage = new SessionInfoStorage("hydrogen_sessions_v1");
|
platform.setNavigation(navigation);
|
||||||
let serviceWorkerHandler;
|
const urlRouter = createRouter({navigation, history: platform.history});
|
||||||
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()});
|
|
||||||
urlRouter.attach();
|
urlRouter.attach();
|
||||||
|
const olmPromise = platform.loadOlm();
|
||||||
|
const workerPromise = platform.loadOlmWorker();
|
||||||
|
|
||||||
const vm = new RootViewModel({
|
const vm = new RootViewModel({
|
||||||
createSessionContainer: () => {
|
createSessionContainer: () => {
|
||||||
return new SessionContainer({
|
return new SessionContainer({platform, olmPromise, workerPromise});
|
||||||
random: Math.random,
|
|
||||||
onlineStatus: new OnlineStatus(),
|
|
||||||
storageFactory,
|
|
||||||
sessionInfoStorage,
|
|
||||||
request,
|
|
||||||
clock,
|
|
||||||
cryptoDriver: new CryptoDriver(legacyExtras?.crypto),
|
|
||||||
olmPromise,
|
|
||||||
workerPromise,
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
sessionInfoStorage,
|
platform,
|
||||||
storageFactory,
|
|
||||||
clock,
|
|
||||||
// the only public interface of the router is to create urls,
|
// the only public interface of the router is to create urls,
|
||||||
// so we call it that in the view models
|
// so we call it that in the view models
|
||||||
urlCreator: urlRouter,
|
urlCreator: urlRouter,
|
||||||
navigation,
|
navigation,
|
||||||
updateService: serviceWorkerHandler,
|
|
||||||
estimateStorageUsage
|
|
||||||
});
|
});
|
||||||
window.__hydrogenViewModel = vm;
|
|
||||||
await vm.load();
|
await vm.load();
|
||||||
// TODO: replace with platform.createAndMountRootView(vm, container);
|
platform.createAndMountRootView(vm);
|
||||||
const view = new RootView(vm);
|
|
||||||
container.appendChild(view.mount());
|
|
||||||
} catch(err) {
|
} catch(err) {
|
||||||
console.error(`${err.message}:\n${err.stack}`);
|
console.error(`${err.message}:\n${err.stack}`);
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,8 +41,8 @@ const PICKLE_KEY = "DEFAULT_KEY";
|
||||||
|
|
||||||
export class Session {
|
export class Session {
|
||||||
// sessionInfo contains deviceId, userId and homeServer
|
// sessionInfo contains deviceId, userId and homeServer
|
||||||
constructor({clock, storage, hsApi, sessionInfo, olm, olmWorker, cryptoDriver, mediaRepository}) {
|
constructor({storage, hsApi, sessionInfo, olm, olmWorker, platform, mediaRepository}) {
|
||||||
this._clock = clock;
|
this._platform = platform;
|
||||||
this._storage = storage;
|
this._storage = storage;
|
||||||
this._hsApi = hsApi;
|
this._hsApi = hsApi;
|
||||||
this._mediaRepository = mediaRepository;
|
this._mediaRepository = mediaRepository;
|
||||||
|
@ -61,7 +61,6 @@ export class Session {
|
||||||
this._megolmDecryption = null;
|
this._megolmDecryption = null;
|
||||||
this._getSyncToken = () => this.syncToken;
|
this._getSyncToken = () => this.syncToken;
|
||||||
this._olmWorker = olmWorker;
|
this._olmWorker = olmWorker;
|
||||||
this._cryptoDriver = cryptoDriver;
|
|
||||||
this._sessionBackup = null;
|
this._sessionBackup = null;
|
||||||
this._hasSecretStorageKey = new ObservableValue(null);
|
this._hasSecretStorageKey = new ObservableValue(null);
|
||||||
|
|
||||||
|
@ -106,7 +105,7 @@ export class Session {
|
||||||
pickleKey: PICKLE_KEY,
|
pickleKey: PICKLE_KEY,
|
||||||
olm: this._olm,
|
olm: this._olm,
|
||||||
storage: this._storage,
|
storage: this._storage,
|
||||||
now: this._clock.now,
|
now: this._platform.clock.now,
|
||||||
ownUserId: this._user.id,
|
ownUserId: this._user.id,
|
||||||
senderKeyLock
|
senderKeyLock
|
||||||
});
|
});
|
||||||
|
@ -115,7 +114,7 @@ export class Session {
|
||||||
pickleKey: PICKLE_KEY,
|
pickleKey: PICKLE_KEY,
|
||||||
olm: this._olm,
|
olm: this._olm,
|
||||||
storage: this._storage,
|
storage: this._storage,
|
||||||
now: this._clock.now,
|
now: this._platform.clock.now,
|
||||||
ownUserId: this._user.id,
|
ownUserId: this._user.id,
|
||||||
olmUtil: this._olmUtil,
|
olmUtil: this._olmUtil,
|
||||||
senderKeyLock
|
senderKeyLock
|
||||||
|
@ -125,7 +124,7 @@ export class Session {
|
||||||
pickleKey: PICKLE_KEY,
|
pickleKey: PICKLE_KEY,
|
||||||
olm: this._olm,
|
olm: this._olm,
|
||||||
storage: this._storage,
|
storage: this._storage,
|
||||||
now: this._clock.now,
|
now: this._platform.clock.now,
|
||||||
ownDeviceId: this._sessionInfo.deviceId,
|
ownDeviceId: this._sessionInfo.deviceId,
|
||||||
});
|
});
|
||||||
this._megolmDecryption = new MegOlmDecryption({
|
this._megolmDecryption = new MegOlmDecryption({
|
||||||
|
@ -166,7 +165,7 @@ export class Session {
|
||||||
this.needsSessionBackup.set(true)
|
this.needsSessionBackup.set(true)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
clock: this._clock
|
clock: this._platform.clock
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -185,7 +184,7 @@ export class Session {
|
||||||
if (this._sessionBackup) {
|
if (this._sessionBackup) {
|
||||||
return false;
|
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
|
// and create session backup, which needs to read from accountData
|
||||||
const readTxn = this._storage.readTxn([
|
const readTxn = this._storage.readTxn([
|
||||||
this._storage.storeNames.accountData,
|
this._storage.storeNames.accountData,
|
||||||
|
@ -207,7 +206,7 @@ export class Session {
|
||||||
}
|
}
|
||||||
|
|
||||||
async _createSessionBackup(ssssKey, txn) {
|
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});
|
this._sessionBackup = await SessionBackup.fromSecretStorage({olm: this._olm, secretStorage, hsApi: this._hsApi, txn});
|
||||||
if (this._sessionBackup) {
|
if (this._sessionBackup) {
|
||||||
for (const room of this._rooms.values()) {
|
for (const room of this._rooms.values()) {
|
||||||
|
@ -363,7 +362,7 @@ export class Session {
|
||||||
pendingEvents,
|
pendingEvents,
|
||||||
user: this._user,
|
user: this._user,
|
||||||
createRoomEncryption: this._createRoomEncryption,
|
createRoomEncryption: this._createRoomEncryption,
|
||||||
clock: this._clock
|
clock: this._platform.clock
|
||||||
});
|
});
|
||||||
this._rooms.add(roomId, room);
|
this._rooms.add(roomId, room);
|
||||||
return room;
|
return room;
|
||||||
|
|
|
@ -44,13 +44,8 @@ export const LoginFailure = createEnum(
|
||||||
);
|
);
|
||||||
|
|
||||||
export class SessionContainer {
|
export class SessionContainer {
|
||||||
constructor({clock, random, onlineStatus, request, storageFactory, sessionInfoStorage, olmPromise, workerPromise, cryptoDriver}) {
|
constructor({platform, olmPromise, workerPromise}) {
|
||||||
this._random = random;
|
this._platform = platform;
|
||||||
this._clock = clock;
|
|
||||||
this._onlineStatus = onlineStatus;
|
|
||||||
this._request = request;
|
|
||||||
this._storageFactory = storageFactory;
|
|
||||||
this._sessionInfoStorage = sessionInfoStorage;
|
|
||||||
this._sessionStartedByReconnector = false;
|
this._sessionStartedByReconnector = false;
|
||||||
this._status = new ObservableValue(LoadStatus.NotLoading);
|
this._status = new ObservableValue(LoadStatus.NotLoading);
|
||||||
this._error = null;
|
this._error = null;
|
||||||
|
@ -63,11 +58,10 @@ export class SessionContainer {
|
||||||
this._requestScheduler = null;
|
this._requestScheduler = null;
|
||||||
this._olmPromise = olmPromise;
|
this._olmPromise = olmPromise;
|
||||||
this._workerPromise = workerPromise;
|
this._workerPromise = workerPromise;
|
||||||
this._cryptoDriver = cryptoDriver;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
createNewSessionId() {
|
createNewSessionId() {
|
||||||
return (Math.floor(this._random() * Number.MAX_SAFE_INTEGER)).toString();
|
return (Math.floor(this._platform.random() * Number.MAX_SAFE_INTEGER)).toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
get sessionId() {
|
get sessionId() {
|
||||||
|
@ -80,7 +74,7 @@ export class SessionContainer {
|
||||||
}
|
}
|
||||||
this._status.set(LoadStatus.Loading);
|
this._status.set(LoadStatus.Loading);
|
||||||
try {
|
try {
|
||||||
const sessionInfo = await this._sessionInfoStorage.get(sessionId);
|
const sessionInfo = await this._platform.sessionInfoStorage.get(sessionId);
|
||||||
if (!sessionInfo) {
|
if (!sessionInfo) {
|
||||||
throw new Error("Invalid session id: " + sessionId);
|
throw new Error("Invalid session id: " + sessionId);
|
||||||
}
|
}
|
||||||
|
@ -96,9 +90,11 @@ export class SessionContainer {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this._status.set(LoadStatus.Login);
|
this._status.set(LoadStatus.Login);
|
||||||
|
const clock = this._platform.clock;
|
||||||
let sessionInfo;
|
let sessionInfo;
|
||||||
try {
|
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 loginData = await hsApi.passwordLogin(username, password, "Hydrogen").response();
|
||||||
const sessionId = this.createNewSessionId();
|
const sessionId = this.createNewSessionId();
|
||||||
sessionInfo = {
|
sessionInfo = {
|
||||||
|
@ -107,9 +103,9 @@ export class SessionContainer {
|
||||||
userId: loginData.user_id,
|
userId: loginData.user_id,
|
||||||
homeServer: homeServer,
|
homeServer: homeServer,
|
||||||
accessToken: loginData.access_token,
|
accessToken: loginData.access_token,
|
||||||
lastUsed: this._clock.now()
|
lastUsed: clock.now()
|
||||||
};
|
};
|
||||||
await this._sessionInfoStorage.add(sessionInfo);
|
await this._platform.sessionInfoStorage.add(sessionInfo);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this._error = err;
|
this._error = err;
|
||||||
if (err instanceof HomeServerError) {
|
if (err instanceof HomeServerError) {
|
||||||
|
@ -139,22 +135,23 @@ export class SessionContainer {
|
||||||
}
|
}
|
||||||
|
|
||||||
async _loadSessionInfo(sessionInfo, isNewLogin) {
|
async _loadSessionInfo(sessionInfo, isNewLogin) {
|
||||||
|
const clock = this._platform.clock;
|
||||||
this._sessionStartedByReconnector = false;
|
this._sessionStartedByReconnector = false;
|
||||||
this._status.set(LoadStatus.Loading);
|
this._status.set(LoadStatus.Loading);
|
||||||
this._reconnector = new Reconnector({
|
this._reconnector = new Reconnector({
|
||||||
onlineStatus: this._onlineStatus,
|
onlineStatus: this._platform.onlineStatus,
|
||||||
retryDelay: new ExponentialRetryDelay(this._clock.createTimeout),
|
retryDelay: new ExponentialRetryDelay(clock.createTimeout),
|
||||||
createMeasure: this._clock.createMeasure
|
createMeasure: clock.createMeasure
|
||||||
});
|
});
|
||||||
const hsApi = new HomeServerApi({
|
const hsApi = new HomeServerApi({
|
||||||
homeServer: sessionInfo.homeServer,
|
homeServer: sessionInfo.homeServer,
|
||||||
accessToken: sessionInfo.accessToken,
|
accessToken: sessionInfo.accessToken,
|
||||||
request: this._request,
|
request: this._platform.request,
|
||||||
reconnector: this._reconnector,
|
reconnector: this._reconnector,
|
||||||
createTimeout: this._clock.createTimeout
|
createTimeout: clock.createTimeout
|
||||||
});
|
});
|
||||||
this._sessionId = sessionInfo.id;
|
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
|
// no need to pass access token to session
|
||||||
const filteredSessionInfo = {
|
const filteredSessionInfo = {
|
||||||
deviceId: sessionInfo.deviceId,
|
deviceId: sessionInfo.deviceId,
|
||||||
|
@ -166,22 +163,21 @@ export class SessionContainer {
|
||||||
if (this._workerPromise) {
|
if (this._workerPromise) {
|
||||||
olmWorker = await this._workerPromise;
|
olmWorker = await this._workerPromise;
|
||||||
}
|
}
|
||||||
this._requestScheduler = new RequestScheduler({hsApi, clock: this._clock});
|
this._requestScheduler = new RequestScheduler({hsApi, clock});
|
||||||
this._requestScheduler.start();
|
this._requestScheduler.start();
|
||||||
const mediaRepository = new MediaRepository({
|
const mediaRepository = new MediaRepository({
|
||||||
homeServer: sessionInfo.homeServer,
|
homeServer: sessionInfo.homeServer,
|
||||||
cryptoDriver: this._cryptoDriver,
|
crypto: this._platform.crypto,
|
||||||
request: this._request,
|
request: this._platform.request,
|
||||||
});
|
});
|
||||||
this._session = new Session({
|
this._session = new Session({
|
||||||
storage: this._storage,
|
storage: this._storage,
|
||||||
sessionInfo: filteredSessionInfo,
|
sessionInfo: filteredSessionInfo,
|
||||||
hsApi: this._requestScheduler.hsApi,
|
hsApi: this._requestScheduler.hsApi,
|
||||||
olm,
|
olm,
|
||||||
clock: this._clock,
|
|
||||||
olmWorker,
|
olmWorker,
|
||||||
cryptoDriver: this._cryptoDriver,
|
mediaRepository,
|
||||||
mediaRepository
|
platform: this._platform,
|
||||||
});
|
});
|
||||||
await this._session.load();
|
await this._session.load();
|
||||||
if (isNewLogin) {
|
if (isNewLogin) {
|
||||||
|
@ -298,8 +294,8 @@ export class SessionContainer {
|
||||||
// if one fails, don't block the other from trying
|
// if one fails, don't block the other from trying
|
||||||
// also, run in parallel
|
// also, run in parallel
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
this._storageFactory.delete(this._sessionId),
|
this._platform.storageFactory.delete(this._sessionId),
|
||||||
this._sessionInfoStorage.delete(this._sessionId),
|
this._platform.sessionInfoStorage.delete(this._sessionId),
|
||||||
]);
|
]);
|
||||||
this._sessionId = null;
|
this._sessionId = null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
* @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.
|
* @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
|
if (info === undefined || info.key === undefined || info.iv === undefined
|
||||||
|| info.hashes === undefined || info.hashes.sha256 === undefined) {
|
|| info.hashes === undefined || info.hashes.sha256 === undefined) {
|
||||||
throw new Error("Invalid info. Missing info.key, info.iv or info.hashes.sha256 key");
|
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
|
// re-encode to not deal with padded vs unpadded
|
||||||
var expectedSha256base64 = base64.encode(base64.decode(info.hashes.sha256));
|
var expectedSha256base64 = base64.encode(base64.decode(info.hashes.sha256));
|
||||||
// Check the sha256 hash
|
// 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) {
|
if (base64.encode(new Uint8Array(digestResult)) != expectedSha256base64) {
|
||||||
throw new Error("Mismatched SHA-256 digest");
|
throw new Error("Mismatched SHA-256 digest");
|
||||||
}
|
}
|
||||||
|
@ -48,7 +48,7 @@ export async function decryptAttachment(cryptoDriver, ciphertextBuffer, info) {
|
||||||
counterLength = 128;
|
counterLength = 128;
|
||||||
}
|
}
|
||||||
|
|
||||||
const decryptedBuffer = await cryptoDriver.aes.decryptCTR({
|
const decryptedBuffer = await crypto.aes.decryptCTR({
|
||||||
jwkKey: info.key,
|
jwkKey: info.key,
|
||||||
iv: ivArray,
|
iv: ivArray,
|
||||||
data: ciphertextBuffer,
|
data: ciphertextBuffer,
|
||||||
|
|
|
@ -18,9 +18,9 @@ import {encodeQueryParams} from "./common.js";
|
||||||
import {decryptAttachment} from "../e2ee/attachment.js";
|
import {decryptAttachment} from "../e2ee/attachment.js";
|
||||||
|
|
||||||
export class MediaRepository {
|
export class MediaRepository {
|
||||||
constructor({homeServer, cryptoDriver, request}) {
|
constructor({homeServer, crypto, request}) {
|
||||||
this._homeServer = homeServer;
|
this._homeServer = homeServer;
|
||||||
this._cryptoDriver = cryptoDriver;
|
this._crypto = crypto;
|
||||||
this._request = request;
|
this._request = request;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,7 +56,7 @@ export class MediaRepository {
|
||||||
async downloadEncryptedFile(fileEntry) {
|
async downloadEncryptedFile(fileEntry) {
|
||||||
const url = this.mxcUrl(fileEntry.url);
|
const url = this.mxcUrl(fileEntry.url);
|
||||||
const {body: encryptedBuffer} = await this._request(url, {format: "buffer", cache: true}).response();
|
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;
|
return decryptedBuffer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,25 +26,3 @@ export function encodeQueryParams(queryParams) {
|
||||||
})
|
})
|
||||||
.join("&");
|
.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.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {Platform} from "../../../Platform.js";
|
import {KeyLimits} from "../../storage/common.js";
|
||||||
|
|
||||||
// key for events in the timelineEvents store
|
// key for events in the timelineEvents store
|
||||||
export class EventKey {
|
export class EventKey {
|
||||||
|
@ -25,7 +25,7 @@ export class EventKey {
|
||||||
|
|
||||||
nextFragmentKey() {
|
nextFragmentKey() {
|
||||||
// could take MIN_EVENT_INDEX here if it can't be paged back
|
// 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) {
|
nextKeyForDirection(direction) {
|
||||||
|
@ -45,19 +45,19 @@ export class EventKey {
|
||||||
}
|
}
|
||||||
|
|
||||||
static get maxKey() {
|
static get maxKey() {
|
||||||
return new EventKey(Platform.maxStorageKey, Platform.maxStorageKey);
|
return new EventKey(KeyLimits.maxStorageKey, KeyLimits.maxStorageKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
static get minKey() {
|
static get minKey() {
|
||||||
return new EventKey(Platform.minStorageKey, Platform.minStorageKey);
|
return new EventKey(KeyLimits.minStorageKey, KeyLimits.minStorageKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
static get defaultLiveKey() {
|
static get defaultLiveKey() {
|
||||||
return EventKey.defaultFragmentKey(Platform.minStorageKey);
|
return EventKey.defaultFragmentKey(KeyLimits.minStorageKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
static defaultFragmentKey(fragmentId) {
|
static defaultFragmentKey(fragmentId) {
|
||||||
return new EventKey(fragmentId, Platform.middleStorageKey);
|
return new EventKey(fragmentId, KeyLimits.middleStorageKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
toString() {
|
toString() {
|
||||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
||||||
import {BaseEntry} from "./BaseEntry.js";
|
import {BaseEntry} from "./BaseEntry.js";
|
||||||
import {Direction} from "../Direction.js";
|
import {Direction} from "../Direction.js";
|
||||||
import {isValidFragmentId} from "../common.js";
|
import {isValidFragmentId} from "../common.js";
|
||||||
import {Platform} from "../../../../Platform.js";
|
import {KeyLimits} from "../../../storage/common.js";
|
||||||
|
|
||||||
export class FragmentBoundaryEntry extends BaseEntry {
|
export class FragmentBoundaryEntry extends BaseEntry {
|
||||||
constructor(fragment, isFragmentStart, fragmentIdComparer) {
|
constructor(fragment, isFragmentStart, fragmentIdComparer) {
|
||||||
|
@ -53,9 +53,9 @@ export class FragmentBoundaryEntry extends BaseEntry {
|
||||||
|
|
||||||
get entryIndex() {
|
get entryIndex() {
|
||||||
if (this.started) {
|
if (this.started) {
|
||||||
return Platform.minStorageKey;
|
return KeyLimits.minStorageKey;
|
||||||
} else {
|
} else {
|
||||||
return Platform.maxStorageKey;
|
return KeyLimits.maxStorageKey;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,9 +17,9 @@ limitations under the License.
|
||||||
import base64 from "../../../lib/base64-arraybuffer/index.js";
|
import base64 from "../../../lib/base64-arraybuffer/index.js";
|
||||||
|
|
||||||
export class SecretStorage {
|
export class SecretStorage {
|
||||||
constructor({key, cryptoDriver}) {
|
constructor({key, crypto}) {
|
||||||
this._key = key;
|
this._key = key;
|
||||||
this._cryptoDriver = cryptoDriver;
|
this._crypto = crypto;
|
||||||
}
|
}
|
||||||
|
|
||||||
async readSecret(name, txn) {
|
async readSecret(name, txn) {
|
||||||
|
@ -44,7 +44,7 @@ export class SecretStorage {
|
||||||
const textEncoder = new TextEncoder();
|
const textEncoder = new TextEncoder();
|
||||||
const textDecoder = new TextDecoder();
|
const textDecoder = new TextDecoder();
|
||||||
// now derive the aes and mac key from the 4s key
|
// 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,
|
this._key.binaryKey,
|
||||||
new Uint8Array(8).buffer, //zero salt
|
new Uint8Array(8).buffer, //zero salt
|
||||||
textEncoder.encode(type), // info
|
textEncoder.encode(type), // info
|
||||||
|
@ -56,7 +56,7 @@ export class SecretStorage {
|
||||||
|
|
||||||
const ciphertextBytes = base64.decode(encryptedData.ciphertext);
|
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),
|
hmacKey, base64.decode(encryptedData.mac),
|
||||||
ciphertextBytes, "SHA-256");
|
ciphertextBytes, "SHA-256");
|
||||||
|
|
||||||
|
@ -64,7 +64,7 @@ export class SecretStorage {
|
||||||
throw new Error("Bad MAC");
|
throw new Error("Bad MAC");
|
||||||
}
|
}
|
||||||
|
|
||||||
const plaintextBytes = await this._cryptoDriver.aes.decryptCTR({
|
const plaintextBytes = await this._crypto.aes.decryptCTR({
|
||||||
key: aesKey,
|
key: aesKey,
|
||||||
iv: base64.decode(encryptedData.iv),
|
iv: base64.decode(encryptedData.iv),
|
||||||
data: ciphertextBytes
|
data: ciphertextBytes
|
||||||
|
|
|
@ -47,14 +47,14 @@ export async function readKey(txn) {
|
||||||
return new Key(new KeyDescription(keyData.id, keyAccountData), keyData.binaryKey);
|
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);
|
const keyDescription = await readDefaultKeyDescription(storage);
|
||||||
if (!keyDescription) {
|
if (!keyDescription) {
|
||||||
throw new Error("Could not find a default secret storage key in account data");
|
throw new Error("Could not find a default secret storage key in account data");
|
||||||
}
|
}
|
||||||
let key;
|
let key;
|
||||||
if (type === "phrase") {
|
if (type === "phrase") {
|
||||||
key = await keyFromPassphrase(keyDescription, credential, cryptoDriver);
|
key = await keyFromPassphrase(keyDescription, credential, crypto);
|
||||||
} else if (type === "key") {
|
} else if (type === "key") {
|
||||||
key = keyFromRecoveryKey(olm, keyDescription, credential);
|
key = keyFromRecoveryKey(olm, keyDescription, credential);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -22,10 +22,10 @@ const DEFAULT_BITSIZE = 256;
|
||||||
/**
|
/**
|
||||||
* @param {KeyDescription} keyDescription
|
* @param {KeyDescription} keyDescription
|
||||||
* @param {string} passphrase
|
* @param {string} passphrase
|
||||||
* @param {CryptoDriver} cryptoDriver
|
* @param {Crypto} crypto
|
||||||
* @return {Key}
|
* @return {Key}
|
||||||
*/
|
*/
|
||||||
export async function keyFromPassphrase(keyDescription, passphrase, cryptoDriver) {
|
export async function keyFromPassphrase(keyDescription, passphrase, crypto) {
|
||||||
const {passphraseParams} = keyDescription;
|
const {passphraseParams} = keyDescription;
|
||||||
if (!passphraseParams) {
|
if (!passphraseParams) {
|
||||||
throw new Error("not a passphrase key");
|
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
|
// TODO: we should we move this to platform specific code
|
||||||
const textEncoder = new TextEncoder();
|
const textEncoder = new TextEncoder();
|
||||||
const keyBits = await cryptoDriver.derive.pbkdf2(
|
const keyBits = await crypto.derive.pbkdf2(
|
||||||
textEncoder.encode(passphrase),
|
textEncoder.encode(passphrase),
|
||||||
passphraseParams.iterations || DEFAULT_ITERATIONS,
|
passphraseParams.iterations || DEFAULT_ITERATIONS,
|
||||||
// salt is just a random string, not encoded in any way
|
// salt is just a random string, not encoded in any way
|
||||||
|
|
|
@ -50,3 +50,20 @@ export class StorageError extends Error {
|
||||||
return "StorageError";
|
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 { encodeUint32, decodeUint32 } from "../utils.js";
|
||||||
import {Platform} from "../../../../Platform.js";
|
import {KeyLimits} from "../../common.js";
|
||||||
|
|
||||||
function encodeKey(roomId, queueIndex) {
|
function encodeKey(roomId, queueIndex) {
|
||||||
return `${roomId}|${encodeUint32(queueIndex)}`;
|
return `${roomId}|${encodeUint32(queueIndex)}`;
|
||||||
|
@ -34,8 +34,8 @@ export class PendingEventStore {
|
||||||
|
|
||||||
async getMaxQueueIndex(roomId) {
|
async getMaxQueueIndex(roomId) {
|
||||||
const range = IDBKeyRange.bound(
|
const range = IDBKeyRange.bound(
|
||||||
encodeKey(roomId, Platform.minStorageKey),
|
encodeKey(roomId, KeyLimits.minStorageKey),
|
||||||
encodeKey(roomId, Platform.maxStorageKey),
|
encodeKey(roomId, KeyLimits.maxStorageKey),
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
||||||
import {EventKey} from "../../../room/timeline/EventKey.js";
|
import {EventKey} from "../../../room/timeline/EventKey.js";
|
||||||
import { StorageError } from "../../common.js";
|
import { StorageError } from "../../common.js";
|
||||||
import { encodeUint32 } from "../utils.js";
|
import { encodeUint32 } from "../utils.js";
|
||||||
import {Platform} from "../../../../Platform.js";
|
import {KeyLimits} from "../../common.js";
|
||||||
|
|
||||||
function encodeKey(roomId, fragmentId, eventIndex) {
|
function encodeKey(roomId, fragmentId, eventIndex) {
|
||||||
return `${roomId}|${encodeUint32(fragmentId)}|${encodeUint32(eventIndex)}`;
|
return `${roomId}|${encodeUint32(fragmentId)}|${encodeUint32(eventIndex)}`;
|
||||||
|
@ -52,7 +52,7 @@ class Range {
|
||||||
if (this._lower && !this._upper) {
|
if (this._lower && !this._upper) {
|
||||||
return IDBKeyRange.bound(
|
return IDBKeyRange.bound(
|
||||||
encodeKey(roomId, this._lower.fragmentId, this._lower.eventIndex),
|
encodeKey(roomId, this._lower.fragmentId, this._lower.eventIndex),
|
||||||
encodeKey(roomId, this._lower.fragmentId, Platform.maxStorageKey),
|
encodeKey(roomId, this._lower.fragmentId, KeyLimits.maxStorageKey),
|
||||||
this._lowerOpen,
|
this._lowerOpen,
|
||||||
false
|
false
|
||||||
);
|
);
|
||||||
|
@ -61,7 +61,7 @@ class Range {
|
||||||
// also bound as we don't want to move into another roomId
|
// also bound as we don't want to move into another roomId
|
||||||
if (!this._lower && this._upper) {
|
if (!this._lower && this._upper) {
|
||||||
return IDBKeyRange.bound(
|
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),
|
encodeKey(roomId, this._upper.fragmentId, this._upper.eventIndex),
|
||||||
false,
|
false,
|
||||||
this._upperOpen
|
this._upperOpen
|
||||||
|
|
|
@ -15,7 +15,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { StorageError } from "../../common.js";
|
import { StorageError } from "../../common.js";
|
||||||
import {Platform} from "../../../../Platform.js";
|
import {KeyLimits} from "../../common.js";
|
||||||
import { encodeUint32 } from "../utils.js";
|
import { encodeUint32 } from "../utils.js";
|
||||||
|
|
||||||
function encodeKey(roomId, fragmentId) {
|
function encodeKey(roomId, fragmentId) {
|
||||||
|
@ -30,8 +30,8 @@ export class TimelineFragmentStore {
|
||||||
_allRange(roomId) {
|
_allRange(roomId) {
|
||||||
try {
|
try {
|
||||||
return IDBKeyRange.bound(
|
return IDBKeyRange.bound(
|
||||||
encodeKey(roomId, Platform.minStorageKey),
|
encodeKey(roomId, KeyLimits.minStorageKey),
|
||||||
encodeKey(roomId, Platform.maxStorageKey)
|
encodeKey(roomId, KeyLimits.maxStorageKey)
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw new StorageError(`error from IDBKeyRange with roomId ${roomId}`, err);
|
throw new StorageError(`error from IDBKeyRange with roomId ${roomId}`, err);
|
||||||
|
|
|
@ -48,7 +48,7 @@ export async function checkNeedsSyncPromise() {
|
||||||
return needsSyncPromise;
|
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) {
|
export function encodeUint32(n) {
|
||||||
const hex = n.toString(16);
|
const hex = n.toString(16);
|
||||||
return "0".repeat(8 - hex.length) + hex;
|
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) {
|
constructor(subtleCrypto) {
|
||||||
this._subtleCrypto = subtleCrypto;
|
this._subtleCrypto = subtleCrypto;
|
||||||
}
|
}
|
||||||
|
@ -80,10 +80,10 @@ class CryptoHMACDriver {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class CryptoDeriveDriver {
|
class DeriveCrypto {
|
||||||
constructor(subtleCrypto, cryptoDriver, cryptoExtras) {
|
constructor(subtleCrypto, crypto, cryptoExtras) {
|
||||||
this._subtleCrypto = subtleCrypto;
|
this._subtleCrypto = subtleCrypto;
|
||||||
this._cryptoDriver = cryptoDriver;
|
this._crypto = crypto;
|
||||||
this._cryptoExtras = cryptoExtras;
|
this._cryptoExtras = cryptoExtras;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
|
@ -130,7 +130,7 @@ class CryptoDeriveDriver {
|
||||||
*/
|
*/
|
||||||
async hkdf(key, salt, info, hash, length) {
|
async hkdf(key, salt, info, hash, length) {
|
||||||
if (!this._subtleCrypto.deriveBits) {
|
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(
|
const hkdfkey = await subtleCryptoResult(this._subtleCrypto.importKey(
|
||||||
'raw',
|
'raw',
|
||||||
|
@ -152,7 +152,7 @@ class CryptoDeriveDriver {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class CryptoAESDriver {
|
class AESCrypto {
|
||||||
constructor(subtleCrypto) {
|
constructor(subtleCrypto) {
|
||||||
this._subtleCrypto = subtleCrypto;
|
this._subtleCrypto = subtleCrypto;
|
||||||
}
|
}
|
||||||
|
@ -200,7 +200,7 @@ class CryptoAESDriver {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class CryptoLegacyAESDriver {
|
class AESLegacyCrypto {
|
||||||
constructor(aesjs) {
|
constructor(aesjs) {
|
||||||
this._aesjs = aesjs;
|
this._aesjs = aesjs;
|
||||||
}
|
}
|
||||||
|
@ -237,20 +237,20 @@ function hashName(name) {
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CryptoDriver {
|
export class Crypto {
|
||||||
constructor(cryptoExtras) {
|
constructor(cryptoExtras) {
|
||||||
const crypto = window.crypto || window.msCrypto;
|
const crypto = window.crypto || window.msCrypto;
|
||||||
const subtleCrypto = crypto.subtle || crypto.webkitSubtle;
|
const subtleCrypto = crypto.subtle || crypto.webkitSubtle;
|
||||||
this._subtleCrypto = subtleCrypto;
|
this._subtleCrypto = subtleCrypto;
|
||||||
// not exactly guaranteeing AES-CTR support
|
// not exactly guaranteeing AES-CTR support
|
||||||
// but in practice IE11 doesn't have this
|
// but in practice IE11 doesn't have this
|
||||||
if (!subtleCrypto.deriveBits && cryptoExtras.aesjs) {
|
if (!subtleCrypto.deriveBits && cryptoExtras?.aesjs) {
|
||||||
this.aes = new CryptoLegacyAESDriver(cryptoExtras.aesjs);
|
this.aes = new AESLegacyCrypto(cryptoExtras.aesjs);
|
||||||
} else {
|
} else {
|
||||||
this.aes = new CryptoAESDriver(subtleCrypto);
|
this.aes = new AESCrypto(subtleCrypto);
|
||||||
}
|
}
|
||||||
this.hmac = new CryptoHMACDriver(subtleCrypto);
|
this.hmac = new HMACCrypto(subtleCrypto);
|
||||||
this.derive = new CryptoDeriveDriver(subtleCrypto, this, cryptoExtras);
|
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)
|
// - UpdateService (see checkForUpdate method, and should also emit events rather than showing confirm dialog here)
|
||||||
// - ConcurrentAccessBlocker (see preventConcurrentSessionAccess method)
|
// - ConcurrentAccessBlocker (see preventConcurrentSessionAccess method)
|
||||||
export class ServiceWorkerHandler {
|
export class ServiceWorkerHandler {
|
||||||
constructor({navigation}) {
|
constructor() {
|
||||||
this._waitingForReply = new Map();
|
this._waitingForReply = new Map();
|
||||||
this._messageIdCounter = 0;
|
this._messageIdCounter = 0;
|
||||||
|
this._navigation = null;
|
||||||
this._registration = null;
|
this._registration = null;
|
||||||
this._navigation = navigation;
|
|
||||||
this._registrationPromise = null;
|
this._registrationPromise = null;
|
||||||
this._currentController = null;
|
this._currentController = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setNavigation(navigation) {
|
||||||
|
this._navigation = navigation;
|
||||||
|
}
|
||||||
|
|
||||||
registerAndStart(path) {
|
registerAndStart(path) {
|
||||||
this._registrationPromise = (async () => {
|
this._registrationPromise = (async () => {
|
||||||
navigator.serviceWorker.addEventListener("message", this);
|
navigator.serviceWorker.addEventListener("message", this);
|
||||||
|
@ -61,7 +65,7 @@ export class ServiceWorkerHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
_closeSessionIfNeeded(sessionId) {
|
_closeSessionIfNeeded(sessionId) {
|
||||||
const currentSession = this._navigation.path.get("session");
|
const currentSession = this._navigation?.path.get("session");
|
||||||
if (sessionId && currentSession?.value === sessionId) {
|
if (sessionId && currentSession?.value === sessionId) {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
const unsubscribe = this._navigation.pathObservable.subscribe(path => {
|
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.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {AbortError} from "./error.js";
|
import {AbortError} from "../../../utils/error.js";
|
||||||
|
|
||||||
class WorkerState {
|
class WorkerState {
|
||||||
constructor(worker) {
|
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 {
|
import {
|
||||||
AbortError,
|
AbortError,
|
||||||
ConnectionError
|
ConnectionError
|
||||||
} from "../../error.js";
|
} from "../../../../matrix/error.js";
|
||||||
import {abortOnTimeout} from "../timeout.js";
|
import {abortOnTimeout} from "./timeout.js";
|
||||||
import {addCacheBuster} from "../common.js";
|
import {addCacheBuster} from "./common.js";
|
||||||
|
|
||||||
class RequestResult {
|
class RequestResult {
|
||||||
constructor(promise, controller) {
|
constructor(promise, controller) {
|
|
@ -18,7 +18,7 @@ limitations under the License.
|
||||||
import {
|
import {
|
||||||
AbortError,
|
AbortError,
|
||||||
ConnectionError
|
ConnectionError
|
||||||
} from "../error.js";
|
} from "../../../../matrix/error.js";
|
||||||
|
|
||||||
|
|
||||||
export function abortOnTimeout(createTimeout, timeoutAmount, requestResult, responsePromise) {
|
export function abortOnTimeout(createTimeout, timeoutAmount, requestResult, responsePromise) {
|
|
@ -17,8 +17,8 @@ limitations under the License.
|
||||||
import {
|
import {
|
||||||
AbortError,
|
AbortError,
|
||||||
ConnectionError
|
ConnectionError
|
||||||
} from "../../error.js";
|
} from "../../../../matrix/error.js";
|
||||||
import {addCacheBuster} from "../common.js";
|
import {addCacheBuster} from "./common.js";
|
||||||
|
|
||||||
class RequestResult {
|
class RequestResult {
|
||||||
constructor(promise, xhr) {
|
constructor(promise, xhr) {
|
|
@ -15,8 +15,8 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// polyfills needed for IE11
|
// polyfills needed for IE11
|
||||||
import Promise from "../lib/es6-promise/index.js";
|
import Promise from "../../../lib/es6-promise/index.js";
|
||||||
import {checkNeedsSyncPromise} from "./matrix/storage/idb/utils.js";
|
import {checkNeedsSyncPromise} from "../../matrix/storage/idb/utils.js";
|
||||||
|
|
||||||
if (typeof window.Promise === "undefined") {
|
if (typeof window.Promise === "undefined") {
|
||||||
window.Promise = Promise;
|
window.Promise = Promise;
|
|
@ -14,8 +14,10 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
const container = document.querySelector(".hydrogen");
|
||||||
|
|
||||||
export function spinner(t, extraClasses = undefined) {
|
export function spinner(t, extraClasses = undefined) {
|
||||||
if (document.body.classList.contains("ie11")) {
|
if (container.classList.contains("legacy")) {
|
||||||
return t.div({className: "spinner"}, [
|
return t.div({className: "spinner"}, [
|
||||||
t.div(),
|
t.div(),
|
||||||
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%;
|
transform-origin: 50% 50%;
|
||||||
animation-name: spinner;
|
animation-name: spinner;
|
||||||
animation-duration: 2s;
|
animation-duration: 2s;
|
||||||
|
@ -45,35 +45,35 @@ limitations under the License.
|
||||||
stroke-linecap: butt;
|
stroke-linecap: butt;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ie11 .spinner {
|
.hydrogen.legacy .spinner {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ie11 .spinner div {
|
.hydrogen.legacy .spinner div {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
display: block;
|
display: block;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
border: 2px solid currentcolor;
|
border: 2px solid currentcolor;
|
||||||
border-radius: 50%;
|
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;
|
border-color: currentcolor transparent transparent transparent;
|
||||||
width: var(--size);
|
width: var(--size);
|
||||||
height: var(--size);
|
height: var(--size);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ie11 .spinner div:nth-child(1) {
|
.hydrogen.legacy .spinner div:nth-child(1) {
|
||||||
animation-delay: -0.45s;
|
animation-delay: -0.45s;
|
||||||
}
|
}
|
||||||
.ie11 .spinner div:nth-child(2) {
|
.hydrogen.legacy .spinner div:nth-child(2) {
|
||||||
animation-delay: -0.3s;
|
animation-delay: -0.3s;
|
||||||
}
|
}
|
||||||
.ie11 .spinner div:nth-child(3) {
|
.hydrogen.legacy .spinner div:nth-child(3) {
|
||||||
animation-delay: -0.15s;
|
animation-delay: -0.15s;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes ie-spinner {
|
@keyframes legacy-spinner {
|
||||||
0% {
|
0% {
|
||||||
transform: rotate(0deg);
|
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 |