Compare commits
1 commit
main
...
feature-e2
Author | SHA1 | Date | |
---|---|---|---|
|
53b716f382 |
16 changed files with 298 additions and 320 deletions
|
@ -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));
|
||||
|
|
111
app/javascript/mastodon/actions/crypto.js
Normal file
111
app/javascript/mastodon/actions/crypto.js
Normal file
|
@ -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);
|
||||
});
|
||||
};
|
110
app/javascript/mastodon/actions/crypto/store.js
Normal file
110
app/javascript/mastodon/actions/crypto/store.js
Normal file
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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));
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
||||
<ConversationsListContainer
|
||||
trackScroll={!pinned}
|
||||
scrollKey={`direct_timeline-${columnId}`}
|
||||
timelineId='direct'
|
||||
onLoadMore={this.handleLoadMore}
|
||||
emptyMessage={<FormattedMessage id='empty_column.direct' defaultMessage="You don't have any direct messages yet. When you send or receive one, it will show up here." />}
|
||||
shouldUpdateScroll={shouldUpdateScroll}
|
||||
/>
|
||||
{!enabled && <button onClick={this.handleEnableCrypto}>Enable crypto</button>}
|
||||
|
||||
{enabled && <span>Crypto enabled</span>}
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
|
15
app/javascript/mastodon/reducers/crypto.js
Normal file
15
app/javascript/mastodon/reducers/crypto.js
Normal file
|
@ -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;
|
||||
}
|
||||
};
|
|
@ -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);
|
||||
|
|
|
@ -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');
|
||||
};
|
||||
});
|
|
@ -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();
|
||||
}));
|
||||
});
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
8
config/webpack/rules/wasm.js
Normal file
8
config/webpack/rules/wasm.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
module.exports = {
|
||||
test: /\.wasm$/,
|
||||
type: "javascript/auto",
|
||||
loader: "file-loader",
|
||||
options: {
|
||||
name: '[name]-[hash].[ext]'
|
||||
}
|
||||
};
|
|
@ -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',
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue