diff --git a/app/javascript/mastodon/actions/accounts.js b/app/javascript/mastodon/actions/accounts.js
index cb2c682a4..455cfed60 100644
--- a/app/javascript/mastodon/actions/accounts.js
+++ b/app/javascript/mastodon/actions/accounts.js
@@ -1,5 +1,4 @@
import api, { getLinks } from '../api';
-import openDB from '../storage/db';
import { importAccount, importFetchedAccount, importFetchedAccounts } from './importer';
export const ACCOUNT_FETCH_REQUEST = 'ACCOUNT_FETCH_REQUEST';
@@ -74,24 +73,6 @@ export const FOLLOW_REQUEST_REJECT_REQUEST = 'FOLLOW_REQUEST_REJECT_REQUEST';
export const FOLLOW_REQUEST_REJECT_SUCCESS = 'FOLLOW_REQUEST_REJECT_SUCCESS';
export const FOLLOW_REQUEST_REJECT_FAIL = 'FOLLOW_REQUEST_REJECT_FAIL';
-function getFromDB(dispatch, getState, index, id) {
- return new Promise((resolve, reject) => {
- const request = index.get(id);
-
- request.onerror = reject;
-
- request.onsuccess = () => {
- if (!request.result) {
- reject();
- return;
- }
-
- dispatch(importAccount(request.result));
- resolve(request.result.moved && getFromDB(dispatch, getState, index, request.result.moved));
- };
- });
-}
-
export function fetchAccount(id) {
return (dispatch, getState) => {
dispatch(fetchRelationships([id]));
@@ -102,17 +83,8 @@ export function fetchAccount(id) {
dispatch(fetchAccountRequest(id));
- openDB().then(db => getFromDB(
- dispatch,
- getState,
- db.transaction('accounts', 'read').objectStore('accounts').index('id'),
- id,
- ).then(() => db.close(), error => {
- db.close();
- throw error;
- })).catch(() => api(getState).get(`/api/v1/accounts/${id}`).then(response => {
+ api(getState).get(`/api/v1/accounts/${id}`).then(response => {
dispatch(importFetchedAccount(response.data));
- })).then(() => {
dispatch(fetchAccountSuccess());
}).catch(error => {
dispatch(fetchAccountFail(id, error));
diff --git a/app/javascript/mastodon/actions/crypto.js b/app/javascript/mastodon/actions/crypto.js
new file mode 100644
index 000000000..7b8ab01ae
--- /dev/null
+++ b/app/javascript/mastodon/actions/crypto.js
@@ -0,0 +1,111 @@
+import api, { getLinks } from '../api';
+import Olm from 'olm';
+import olmModule from 'olm/olm.wasm';
+import CryptoStore from './crypto/store';
+
+export const CRYPTO_INITIALIZE_REQUEST = 'CRYPTO_INITIALIZE_REQUEST';
+export const CRYPTO_INITIALIZE_SUCCESS = 'CRYPTO_INITIALIZE_SUCCESS';
+export const CRYPTO_INITIALIZE_FAIL = 'CRYPTO_INITIALIZE_FAIL ';
+
+const cryptoStore = new CryptoStore();
+
+const loadOlm = () => Olm.init({
+
+ locateFile: path => {
+ if (path.endsWith('.wasm')) {
+ return olmModule;
+ }
+
+ return path;
+ },
+
+});
+
+const getRandomBytes = size => {
+ const array = new Uint8Array(size);
+ crypto.getRandomValues(array);
+ return array.buffer;
+};
+
+const generateDeviceId = () => {
+ const id = new Uint16Array(getRandomBytes(2))[0];
+ return id & 0x3fff;
+};
+
+export const initializeCryptoRequest = () => ({
+ type: CRYPTO_INITIALIZE_REQUEST,
+});
+
+export const initializeCryptoSuccess = account => ({
+ type: CRYPTO_INITIALIZE_SUCCESS,
+ account,
+});
+
+export const initializeCryptoFail = error => ({
+ type: CRYPTO_INITIALIZE_FAIL,
+ error,
+});
+
+export const initializeCrypto = () => (dispatch, getState) => {
+ dispatch(initializeCryptoRequest());
+
+ loadOlm().then(() => {
+ return cryptoStore.getAccount();
+ }).then(account => {
+ dispatch(initializeCryptoSuccess(account));
+ }).catch(err => {
+ console.error(err);
+ dispatch(initializeCryptoFail(err));
+ });
+};
+
+export const enableCrypto = () => (dispatch, getState) => {
+ dispatch(initializeCryptoRequest());
+
+ loadOlm().then(() => {
+ const deviceId = generateDeviceId();
+ const account = new Olm.Account();
+
+ account.create();
+ account.generate_one_time_keys(10);
+
+ const deviceName = 'Browser';
+ const identityKeys = JSON.parse(account.identity_keys());
+ const oneTimeKeys = JSON.parse(account.one_time_keys());
+
+ return cryptoStore.storeAccount(account).then(api(getState).post('/api/v1/crypto/keys/upload', {
+ device: {
+ device_id: deviceId,
+ name: deviceName,
+ fingerprint_key: identityKeys.ed25519,
+ identity_key: identityKeys.curve25519,
+ },
+
+ one_time_keys: Object.keys(oneTimeKeys.curve25519).map(key => ({
+ key_id: key,
+ key: oneTimeKeys.curve25519[key],
+ signature: account.sign(oneTimeKeys.curve25519[key]),
+ })),
+ })).then(() => {
+ account.mark_keys_as_published();
+ }).then(() => {
+ return cryptoStore.storeAccount(account);
+ }).then(() => {
+ dispatch(initializeCryptoSuccess(account));
+ });
+ }).catch(err => {
+ console.error(err);
+ dispatch(initializeCryptoFail(err));
+ });
+};
+
+const MESSAGE_PREKEY = 0;
+
+export const receiveCrypto = encryptedMessage => (dispatch, getState) => {
+ const { account_id, device_id, type, body } = encryptedMessage;
+ const deviceKey = `${account_id}:${device_id}`;
+
+ cryptoStore.decryptMessage(deviceKey, type, body).then(payloadString => {
+ console.log(encryptedMessage, payloadString);
+ });
+};
diff --git a/app/javascript/mastodon/actions/crypto/store.js b/app/javascript/mastodon/actions/crypto/store.js
new file mode 100644
index 000000000..438b3464b
--- /dev/null
+++ b/app/javascript/mastodon/actions/crypto/store.js
@@ -0,0 +1,110 @@
+import Dexie from 'dexie';
+import Olm from 'olm';
+
+const MESSAGE_TYPE_PREKEY = 0;
+
+export default class CryptoStore {
+
+ constructor() {
+ this.pickleKey = 'DEFAULT_KEY';
+
+ this.db = new Dexie('mastodon-crypto');
+
+ this.db.version(1).stores({
+ accounts: '',
+ sessions: 'deviceKey,sessionId',
+ });
+ }
+
+ // FIXME: Need to call free() on returned accounts at some point
+ // but it needs to happen *after* you're done using them
+ getAccount () {
+ return this.db.accounts.get('-').then(pickledAccount => {
+ if (typeof pickledAccount === 'undefined') {
+ return null;
+ }
+
+ const account = new Olm.Account();
+
+ account.unpickle(this.pickleKey, pickledAccount);
+
+ return account;
+ });
+ }
+
+ storeAccount (account) {
+ return this.db.accounts.put(account.pickle(this.pickleKey), '-');
+ }
+
+ storeSession (deviceKey, session) {
+ return this.db.sessions.put({
+ deviceKey,
+ sessionId: session.session_id(),
+ pickledSession: session.pickle(this.pickleKey),
+ });
+ }
+
+ createInboundSession (deviceKey, type, body) {
+ return this.getAccount().then(account => {
+ const session = new Olm.Session();
+
+ let payloadString;
+
+ try {
+ session.create_inbound(account, body);
+ account.remove_one_time_keys(session);
+ this.storeAccount(account);
+
+ payloadString = session.decrypt(type, body);
+
+ this.storeSession(deviceKey, session);
+ } finally {
+ session.free();
+ account.free();
+ }
+
+ return payloadString;
+ });
+ }
+
+ // FIXME: Need to call free() on returned sessions at some point
+ // but it needs to happen *after* you're done using them
+ getSessionsForDevice (deviceKey) {
+ return this.db.sessions.where('deviceKey').equals(deviceKey).toArray().then(sessions => sessions.map(sessionData => {
+ const session = new Olm.Session();
+
+ session.unpickle(this.pickleKey, sessionData.pickledSession);
+
+ return session;
+ }));
+ }
+
+ decryptMessage (deviceKey, type, body) {
+ return this.getSessionsForDevice(deviceKey).then(sessions => {
+ let payloadString;
+
+ sessions.forEach(session => {
+ try {
+ payloadString = this.decryptMessageForSession(deviceKey, session, type, body);
+ } catch (e) {
+ console.error(e);
+ }
+ });
+
+ if (typeof payloadString !== 'undefined') {
+ return payloadString;
+ }
+
+ if (type === MESSAGE_TYPE_PREKEY) {
+ return this.createInboundSession(deviceKey, type, body);
+ }
+ });
+ }
+
+ decryptMessageForSession (deviceKey, session, type, body) {
+ const payloadString = session.decrypt(type, body);
+ this.storeSession(deviceKey, session);
+ return payloadString;
+ }
+
+}
diff --git a/app/javascript/mastodon/actions/statuses.js b/app/javascript/mastodon/actions/statuses.js
index e565e0b0a..b236c9cf3 100644
--- a/app/javascript/mastodon/actions/statuses.js
+++ b/app/javascript/mastodon/actions/statuses.js
@@ -1,6 +1,4 @@
import api from '../api';
-import openDB from '../storage/db';
-import { evictStatus } from '../storage/modifier';
import { deleteFromTimelines } from './timelines';
import { importFetchedStatus, importFetchedStatuses, importAccount, importStatus, importFetchedAccount } from './importer';
@@ -94,23 +92,10 @@ export function fetchStatus(id) {
dispatch(fetchStatusRequest(id, skipLoading));
- openDB().then(db => {
- const transaction = db.transaction(['accounts', 'statuses'], 'read');
- const accountIndex = transaction.objectStore('accounts').index('id');
- const index = transaction.objectStore('statuses').index('id');
-
- return getFromDB(dispatch, getState, accountIndex, index, id).then(() => {
- db.close();
- }, error => {
- db.close();
- throw error;
- });
- }).then(() => {
- dispatch(fetchStatusSuccess(skipLoading));
- }, () => api(getState).get(`/api/v1/statuses/${id}`).then(response => {
+ api(getState).get(`/api/v1/statuses/${id}`).then(response => {
dispatch(importFetchedStatus(response.data));
dispatch(fetchStatusSuccess(skipLoading));
- })).catch(error => {
+ }).catch(error => {
dispatch(fetchStatusFail(id, error, skipLoading));
});
};
@@ -152,7 +137,6 @@ export function deleteStatus(id, routerHistory, withRedraft = false) {
dispatch(deleteStatusRequest(id));
api(getState).delete(`/api/v1/statuses/${id}`).then(response => {
- evictStatus(id);
dispatch(deleteStatusSuccess(id));
dispatch(deleteFromTimelines(id));
dispatch(importFetchedAccount(response.data.account));
diff --git a/app/javascript/mastodon/actions/streaming.js b/app/javascript/mastodon/actions/streaming.js
index 7cecff66e..527b90ab3 100644
--- a/app/javascript/mastodon/actions/streaming.js
+++ b/app/javascript/mastodon/actions/streaming.js
@@ -8,6 +8,7 @@ import {
} from './timelines';
import { updateNotifications, expandNotifications } from './notifications';
import { updateConversations } from './conversations';
+import { receiveCrypto } from './crypto';
import {
fetchAnnouncements,
updateAnnouncements,
@@ -59,6 +60,9 @@ export function connectTimelineStream (timelineId, path, pollingRefresh = null,
case 'announcement.delete':
dispatch(deleteAnnouncement(data.payload));
break;
+ case 'encrypted_message':
+ dispatch(receiveCrypto(JSON.parse(data.payload)));
+ break;
}
},
};
diff --git a/app/javascript/mastodon/features/direct_timeline/index.js b/app/javascript/mastodon/features/direct_timeline/index.js
index 5ce795760..8b25f3194 100644
--- a/app/javascript/mastodon/features/direct_timeline/index.js
+++ b/app/javascript/mastodon/features/direct_timeline/index.js
@@ -3,17 +3,21 @@ import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import Column from '../../components/column';
import ColumnHeader from '../../components/column_header';
-import { mountConversations, unmountConversations, expandConversations } from '../../actions/conversations';
+import { mountConversations } from '../../actions/conversations';
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
-import { connectDirectStream } from '../../actions/streaming';
+import { initializeCrypto, enableCrypto } from 'mastodon/actions/crypto';
import ConversationsListContainer from './containers/conversations_list_container';
const messages = defineMessages({
title: { id: 'column.direct', defaultMessage: 'Direct messages' },
});
-export default @connect()
+const mapStateToProps = state => ({
+ enabled: state.getIn(['crypto', 'enabled']),
+});
+
+export default @connect(mapStateToProps)
@injectIntl
class DirectTimeline extends React.PureComponent {
@@ -24,6 +28,7 @@ class DirectTimeline extends React.PureComponent {
intl: PropTypes.object.isRequired,
hasUnread: PropTypes.bool,
multiColumn: PropTypes.bool,
+ enabled: PropTypes.bool,
};
handlePin = () => {
@@ -49,29 +54,23 @@ class DirectTimeline extends React.PureComponent {
const { dispatch } = this.props;
dispatch(mountConversations());
- dispatch(expandConversations());
- this.disconnect = dispatch(connectDirectStream());
+ dispatch(initializeCrypto());
}
componentWillUnmount () {
this.props.dispatch(unmountConversations());
-
- if (this.disconnect) {
- this.disconnect();
- this.disconnect = null;
- }
}
setRef = c => {
this.column = c;
}
- handleLoadMore = maxId => {
- this.props.dispatch(expandConversations({ maxId }));
+ handleEnableCrypto = () => {
+ this.props.dispatch(enableCrypto());
}
render () {
- const { intl, hasUnread, columnId, multiColumn, shouldUpdateScroll } = this.props;
+ const { intl, hasUnread, columnId, multiColumn, shouldUpdateScroll, enabled } = this.props;
const pinned = !!columnId;
return (
@@ -87,14 +86,9 @@ class DirectTimeline extends React.PureComponent {
multiColumn={multiColumn}
/>
- }
- shouldUpdateScroll={shouldUpdateScroll}
- />
+ {!enabled && }
+
+ {enabled && Crypto enabled}
);
}
diff --git a/app/javascript/mastodon/reducers/crypto.js b/app/javascript/mastodon/reducers/crypto.js
new file mode 100644
index 000000000..e68e45ca9
--- /dev/null
+++ b/app/javascript/mastodon/reducers/crypto.js
@@ -0,0 +1,15 @@
+import { CRYPTO_INITIALIZE_SUCCESS } from 'mastodon/actions/crypto';
+import { Map as ImmutableMap } from 'immutable';
+
+const initialState = ImmutableMap({
+ enabled: false,
+});
+
+export default function crypto (state = initialState, action) {
+ switch(action.type) {
+ case CRYPTO_INITIALIZE_SUCCESS:
+ return state.set('enabled', action.account !== null);
+ default:
+ return state;
+ }
+};
diff --git a/app/javascript/mastodon/reducers/index.js b/app/javascript/mastodon/reducers/index.js
index 3823bb05e..3cf6e9169 100644
--- a/app/javascript/mastodon/reducers/index.js
+++ b/app/javascript/mastodon/reducers/index.js
@@ -36,6 +36,7 @@ import trends from './trends';
import missed_updates from './missed_updates';
import announcements from './announcements';
import markers from './markers';
+import crypto from './crypto';
const reducers = {
announcements,
@@ -75,6 +76,7 @@ const reducers = {
trends,
missed_updates,
markers,
+ crypto,
};
export default combineReducers(reducers);
diff --git a/app/javascript/mastodon/storage/db.js b/app/javascript/mastodon/storage/db.js
deleted file mode 100644
index 377a792a7..000000000
--- a/app/javascript/mastodon/storage/db.js
+++ /dev/null
@@ -1,27 +0,0 @@
-export default () => new Promise((resolve, reject) => {
- // ServiceWorker is required to synchronize the login state.
- // Microsoft Edge 17 does not support getAll according to:
- // Catalog of standard and vendor APIs across browsers - Microsoft Edge Development
- // https://developer.microsoft.com/en-us/microsoft-edge/platform/catalog/?q=specName%3Aindexeddb
- if (!('caches' in self && 'getAll' in IDBObjectStore.prototype)) {
- reject();
- return;
- }
-
- const request = indexedDB.open('mastodon');
-
- request.onerror = reject;
- request.onsuccess = ({ target }) => resolve(target.result);
-
- request.onupgradeneeded = ({ target }) => {
- const accounts = target.result.createObjectStore('accounts', { autoIncrement: true });
- const statuses = target.result.createObjectStore('statuses', { autoIncrement: true });
-
- accounts.createIndex('id', 'id', { unique: true });
- accounts.createIndex('moved', 'moved');
-
- statuses.createIndex('id', 'id', { unique: true });
- statuses.createIndex('account', 'account');
- statuses.createIndex('reblog', 'reblog');
- };
-});
diff --git a/app/javascript/mastodon/storage/modifier.js b/app/javascript/mastodon/storage/modifier.js
deleted file mode 100644
index 9fadabef4..000000000
--- a/app/javascript/mastodon/storage/modifier.js
+++ /dev/null
@@ -1,211 +0,0 @@
-import openDB from './db';
-
-const accountAssetKeys = ['avatar', 'avatar_static', 'header', 'header_static'];
-const storageMargin = 8388608;
-const storeLimit = 1024;
-
-// navigator.storage is not present on:
-// Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.100 Safari/537.36 Edge/16.16299
-// estimate method is not present on Chrome 57.0.2987.98 on Linux.
-export const storageFreeable = 'storage' in navigator && 'estimate' in navigator.storage;
-
-function openCache() {
- // ServiceWorker and Cache API is not available on iOS 11
- // https://webkit.org/status/#specification-service-workers
- return self.caches ? caches.open('mastodon-system') : Promise.reject();
-}
-
-function printErrorIfAvailable(error) {
- if (error) {
- console.warn(error);
- }
-}
-
-function put(name, objects, onupdate, oncreate) {
- return openDB().then(db => (new Promise((resolve, reject) => {
- const putTransaction = db.transaction(name, 'readwrite');
- const putStore = putTransaction.objectStore(name);
- const putIndex = putStore.index('id');
-
- objects.forEach(object => {
- putIndex.getKey(object.id).onsuccess = retrieval => {
- function addObject() {
- putStore.add(object);
- }
-
- function deleteObject() {
- putStore.delete(retrieval.target.result).onsuccess = addObject;
- }
-
- if (retrieval.target.result) {
- if (onupdate) {
- onupdate(object, retrieval.target.result, putStore, deleteObject);
- } else {
- deleteObject();
- }
- } else {
- if (oncreate) {
- oncreate(object, addObject);
- } else {
- addObject();
- }
- }
- };
- });
-
- putTransaction.oncomplete = () => {
- const readTransaction = db.transaction(name, 'readonly');
- const readStore = readTransaction.objectStore(name);
- const count = readStore.count();
-
- count.onsuccess = () => {
- const excess = count.result - storeLimit;
-
- if (excess > 0) {
- const retrieval = readStore.getAll(null, excess);
-
- retrieval.onsuccess = () => resolve(retrieval.result);
- retrieval.onerror = reject;
- } else {
- resolve([]);
- }
- };
-
- count.onerror = reject;
- };
-
- putTransaction.onerror = reject;
- })).then(resolved => {
- db.close();
- return resolved;
- }, error => {
- db.close();
- throw error;
- }));
-}
-
-function evictAccountsByRecords(records) {
- return openDB().then(db => {
- const transaction = db.transaction(['accounts', 'statuses'], 'readwrite');
- const accounts = transaction.objectStore('accounts');
- const accountsIdIndex = accounts.index('id');
- const accountsMovedIndex = accounts.index('moved');
- const statuses = transaction.objectStore('statuses');
- const statusesIndex = statuses.index('account');
-
- function evict(toEvict) {
- toEvict.forEach(record => {
- openCache()
- .then(cache => accountAssetKeys.forEach(key => cache.delete(records[key])))
- .catch(printErrorIfAvailable);
-
- accountsMovedIndex.getAll(record.id).onsuccess = ({ target }) => evict(target.result);
-
- statusesIndex.getAll(record.id).onsuccess =
- ({ target }) => evictStatusesByRecords(target.result);
-
- accountsIdIndex.getKey(record.id).onsuccess =
- ({ target }) => target.result && accounts.delete(target.result);
- });
- }
-
- evict(records);
-
- db.close();
- }).catch(printErrorIfAvailable);
-}
-
-export function evictStatus(id) {
- evictStatuses([id]);
-}
-
-export function evictStatuses(ids) {
- return openDB().then(db => {
- const transaction = db.transaction('statuses', 'readwrite');
- const store = transaction.objectStore('statuses');
- const idIndex = store.index('id');
- const reblogIndex = store.index('reblog');
-
- ids.forEach(id => {
- reblogIndex.getAllKeys(id).onsuccess =
- ({ target }) => target.result.forEach(reblogKey => store.delete(reblogKey));
-
- idIndex.getKey(id).onsuccess =
- ({ target }) => target.result && store.delete(target.result);
- });
-
- db.close();
- }).catch(printErrorIfAvailable);
-}
-
-function evictStatusesByRecords(records) {
- return evictStatuses(records.map(({ id }) => id));
-}
-
-export function putAccounts(records, avatarStatic) {
- const avatarKey = avatarStatic ? 'avatar_static' : 'avatar';
- const newURLs = [];
-
- put('accounts', records, (newRecord, oldKey, store, oncomplete) => {
- store.get(oldKey).onsuccess = ({ target }) => {
- accountAssetKeys.forEach(key => {
- const newURL = newRecord[key];
- const oldURL = target.result[key];
-
- if (newURL !== oldURL) {
- openCache()
- .then(cache => cache.delete(oldURL))
- .catch(printErrorIfAvailable);
- }
- });
-
- const newURL = newRecord[avatarKey];
- const oldURL = target.result[avatarKey];
-
- if (newURL !== oldURL) {
- newURLs.push(newURL);
- }
-
- oncomplete();
- };
- }, (newRecord, oncomplete) => {
- newURLs.push(newRecord[avatarKey]);
- oncomplete();
- }).then(records => Promise.all([
- evictAccountsByRecords(records),
- openCache().then(cache => cache.addAll(newURLs)),
- ])).then(freeStorage, error => {
- freeStorage();
- throw error;
- }).catch(printErrorIfAvailable);
-}
-
-export function putStatuses(records) {
- put('statuses', records)
- .then(evictStatusesByRecords)
- .catch(printErrorIfAvailable);
-}
-
-export function freeStorage() {
- return storageFreeable && navigator.storage.estimate().then(({ quota, usage }) => {
- if (usage + storageMargin < quota) {
- return null;
- }
-
- return openDB().then(db => new Promise((resolve, reject) => {
- const retrieval = db.transaction('accounts', 'readonly').objectStore('accounts').getAll(null, 1);
-
- retrieval.onsuccess = () => {
- if (retrieval.result.length > 0) {
- resolve(evictAccountsByRecords(retrieval.result).then(freeStorage));
- } else {
- resolve(caches.delete('mastodon-system'));
- }
- };
-
- retrieval.onerror = reject;
-
- db.close();
- }));
- });
-}
diff --git a/config/routes.rb b/config/routes.rb
index 349db0934..a16f89687 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -340,22 +340,22 @@ Rails.application.routes.draw do
end
end
- # namespace :crypto do
- # resources :deliveries, only: :create
+ namespace :crypto do
+ resources :deliveries, only: :create
- # namespace :keys do
- # resource :upload, only: [:create]
- # resource :query, only: [:create]
- # resource :claim, only: [:create]
- # resource :count, only: [:show]
- # end
+ namespace :keys do
+ resource :upload, only: [:create]
+ resource :query, only: [:create]
+ resource :claim, only: [:create]
+ resource :count, only: [:show]
+ end
- # resources :encrypted_messages, only: [:index] do
- # collection do
- # post :clear
- # end
- # end
- # end
+ resources :encrypted_messages, only: [:index] do
+ collection do
+ post :clear
+ end
+ end
+ end
resources :conversations, only: [:index, :destroy] do
member do
diff --git a/config/webpack/rules/index.js b/config/webpack/rules/index.js
index bbf6b0187..78c0a4704 100644
--- a/config/webpack/rules/index.js
+++ b/config/webpack/rules/index.js
@@ -2,6 +2,7 @@ const babel = require('./babel');
const css = require('./css');
const file = require('./file');
const nodeModules = require('./node_modules');
+const wasm = require('./wasm');
// Webpack loaders are processed in reverse order
// https://webpack.js.org/concepts/loaders/#loader-features
@@ -11,4 +12,5 @@ module.exports = {
css,
nodeModules,
babel,
+ wasm,
};
diff --git a/config/webpack/rules/wasm.js b/config/webpack/rules/wasm.js
new file mode 100644
index 000000000..f2ad679c3
--- /dev/null
+++ b/config/webpack/rules/wasm.js
@@ -0,0 +1,8 @@
+module.exports = {
+ test: /\.wasm$/,
+ type: "javascript/auto",
+ loader: "file-loader",
+ options: {
+ name: '[name]-[hash].[ext]'
+ }
+};
diff --git a/config/webpack/shared.js b/config/webpack/shared.js
index 15a209253..dfe7ebead 100644
--- a/config/webpack/shared.js
+++ b/config/webpack/shared.js
@@ -109,5 +109,8 @@ module.exports = {
// Called by http-link-header in an API we never use, increases
// bundle size unnecessarily
Buffer: false,
+
+ // Called by olm
+ fs: 'empty',
},
};
diff --git a/package.json b/package.json
index 37c6d97d3..5ee1bd7a1 100644
--- a/package.json
+++ b/package.json
@@ -88,6 +88,7 @@
"css-loader": "^3.6.0",
"cssnano": "^4.1.10",
"detect-passive-events": "^1.0.2",
+ "dexie": "^3.0.1",
"dotenv": "^8.2.0",
"emoji-mart": "Gargron/emoji-mart#build",
"es6-symbol": "^3.1.3",
@@ -117,6 +118,7 @@
"object-fit-images": "^3.2.3",
"object.values": "^1.1.1",
"offline-plugin": "^5.0.7",
+ "olm": "https://packages.matrix.org/npm/olm/olm-3.1.4.tgz",
"path-complete-extname": "^1.0.0",
"pg": "^6.4.0",
"postcss-loader": "^3.0.0",
diff --git a/yarn.lock b/yarn.lock
index 5449ae10f..0f6eed6b3 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3811,6 +3811,11 @@ detect-passive-events@^1.0.2:
resolved "https://registry.yarnpkg.com/detect-passive-events/-/detect-passive-events-1.0.4.tgz#6ed477e6e5bceb79079735dcd357789d37f9a91a"
integrity sha1-btR35uW863kHlzXc01d4nTf5qRo=
+dexie@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/dexie/-/dexie-3.0.1.tgz#faafeb94be0d5e18b25d700546a2c05725511cfc"
+ integrity sha512-/s4KzlaerQnCad/uY1ZNdFckTrbdMVhLlziYQzz62Ff9Ick1lHGomvTXNfwh4ApEZATyXRyVk5F6/y8UU84B0w==
+
diff-sequences@^25.2.6:
version "25.2.6"
resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-25.2.6.tgz#5f467c00edd35352b7bca46d7927d60e687a76dd"
@@ -7658,6 +7663,10 @@ offline-plugin@^5.0.7:
minimatch "^3.0.3"
slash "^1.0.0"
+"olm@https://packages.matrix.org/npm/olm/olm-3.1.4.tgz":
+ version "3.1.4"
+ resolved "https://packages.matrix.org/npm/olm/olm-3.1.4.tgz#0f03128b7d3b2f614d2216409a1dfccca765fdb3"
+
on-finished@~2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947"