forked from fedi/mastodon
WIP
This commit is contained in:
parent
a8b6524b43
commit
53b716f382
|
@ -1,5 +1,4 @@
|
||||||
import api, { getLinks } from '../api';
|
import api, { getLinks } from '../api';
|
||||||
import openDB from '../storage/db';
|
|
||||||
import { importAccount, importFetchedAccount, importFetchedAccounts } from './importer';
|
import { importAccount, importFetchedAccount, importFetchedAccounts } from './importer';
|
||||||
|
|
||||||
export const ACCOUNT_FETCH_REQUEST = 'ACCOUNT_FETCH_REQUEST';
|
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_SUCCESS = 'FOLLOW_REQUEST_REJECT_SUCCESS';
|
||||||
export const FOLLOW_REQUEST_REJECT_FAIL = 'FOLLOW_REQUEST_REJECT_FAIL';
|
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) {
|
export function fetchAccount(id) {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
dispatch(fetchRelationships([id]));
|
dispatch(fetchRelationships([id]));
|
||||||
|
@ -102,17 +83,8 @@ export function fetchAccount(id) {
|
||||||
|
|
||||||
dispatch(fetchAccountRequest(id));
|
dispatch(fetchAccountRequest(id));
|
||||||
|
|
||||||
openDB().then(db => getFromDB(
|
api(getState).get(`/api/v1/accounts/${id}`).then(response => {
|
||||||
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 => {
|
|
||||||
dispatch(importFetchedAccount(response.data));
|
dispatch(importFetchedAccount(response.data));
|
||||||
})).then(() => {
|
|
||||||
dispatch(fetchAccountSuccess());
|
dispatch(fetchAccountSuccess());
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
dispatch(fetchAccountFail(id, 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 api from '../api';
|
||||||
import openDB from '../storage/db';
|
|
||||||
import { evictStatus } from '../storage/modifier';
|
|
||||||
|
|
||||||
import { deleteFromTimelines } from './timelines';
|
import { deleteFromTimelines } from './timelines';
|
||||||
import { importFetchedStatus, importFetchedStatuses, importAccount, importStatus, importFetchedAccount } from './importer';
|
import { importFetchedStatus, importFetchedStatuses, importAccount, importStatus, importFetchedAccount } from './importer';
|
||||||
|
@ -94,23 +92,10 @@ export function fetchStatus(id) {
|
||||||
|
|
||||||
dispatch(fetchStatusRequest(id, skipLoading));
|
dispatch(fetchStatusRequest(id, skipLoading));
|
||||||
|
|
||||||
openDB().then(db => {
|
api(getState).get(`/api/v1/statuses/${id}`).then(response => {
|
||||||
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 => {
|
|
||||||
dispatch(importFetchedStatus(response.data));
|
dispatch(importFetchedStatus(response.data));
|
||||||
dispatch(fetchStatusSuccess(skipLoading));
|
dispatch(fetchStatusSuccess(skipLoading));
|
||||||
})).catch(error => {
|
}).catch(error => {
|
||||||
dispatch(fetchStatusFail(id, error, skipLoading));
|
dispatch(fetchStatusFail(id, error, skipLoading));
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -152,7 +137,6 @@ export function deleteStatus(id, routerHistory, withRedraft = false) {
|
||||||
dispatch(deleteStatusRequest(id));
|
dispatch(deleteStatusRequest(id));
|
||||||
|
|
||||||
api(getState).delete(`/api/v1/statuses/${id}`).then(response => {
|
api(getState).delete(`/api/v1/statuses/${id}`).then(response => {
|
||||||
evictStatus(id);
|
|
||||||
dispatch(deleteStatusSuccess(id));
|
dispatch(deleteStatusSuccess(id));
|
||||||
dispatch(deleteFromTimelines(id));
|
dispatch(deleteFromTimelines(id));
|
||||||
dispatch(importFetchedAccount(response.data.account));
|
dispatch(importFetchedAccount(response.data.account));
|
||||||
|
|
|
@ -8,6 +8,7 @@ import {
|
||||||
} from './timelines';
|
} from './timelines';
|
||||||
import { updateNotifications, expandNotifications } from './notifications';
|
import { updateNotifications, expandNotifications } from './notifications';
|
||||||
import { updateConversations } from './conversations';
|
import { updateConversations } from './conversations';
|
||||||
|
import { receiveCrypto } from './crypto';
|
||||||
import {
|
import {
|
||||||
fetchAnnouncements,
|
fetchAnnouncements,
|
||||||
updateAnnouncements,
|
updateAnnouncements,
|
||||||
|
@ -59,6 +60,9 @@ export function connectTimelineStream (timelineId, path, pollingRefresh = null,
|
||||||
case 'announcement.delete':
|
case 'announcement.delete':
|
||||||
dispatch(deleteAnnouncement(data.payload));
|
dispatch(deleteAnnouncement(data.payload));
|
||||||
break;
|
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 PropTypes from 'prop-types';
|
||||||
import Column from '../../components/column';
|
import Column from '../../components/column';
|
||||||
import ColumnHeader from '../../components/column_header';
|
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 { addColumn, removeColumn, moveColumn } from '../../actions/columns';
|
||||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
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';
|
import ConversationsListContainer from './containers/conversations_list_container';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
title: { id: 'column.direct', defaultMessage: 'Direct messages' },
|
title: { id: 'column.direct', defaultMessage: 'Direct messages' },
|
||||||
});
|
});
|
||||||
|
|
||||||
export default @connect()
|
const mapStateToProps = state => ({
|
||||||
|
enabled: state.getIn(['crypto', 'enabled']),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default @connect(mapStateToProps)
|
||||||
@injectIntl
|
@injectIntl
|
||||||
class DirectTimeline extends React.PureComponent {
|
class DirectTimeline extends React.PureComponent {
|
||||||
|
|
||||||
|
@ -24,6 +28,7 @@ class DirectTimeline extends React.PureComponent {
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
hasUnread: PropTypes.bool,
|
hasUnread: PropTypes.bool,
|
||||||
multiColumn: PropTypes.bool,
|
multiColumn: PropTypes.bool,
|
||||||
|
enabled: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
handlePin = () => {
|
handlePin = () => {
|
||||||
|
@ -49,29 +54,23 @@ class DirectTimeline extends React.PureComponent {
|
||||||
const { dispatch } = this.props;
|
const { dispatch } = this.props;
|
||||||
|
|
||||||
dispatch(mountConversations());
|
dispatch(mountConversations());
|
||||||
dispatch(expandConversations());
|
dispatch(initializeCrypto());
|
||||||
this.disconnect = dispatch(connectDirectStream());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount () {
|
componentWillUnmount () {
|
||||||
this.props.dispatch(unmountConversations());
|
this.props.dispatch(unmountConversations());
|
||||||
|
|
||||||
if (this.disconnect) {
|
|
||||||
this.disconnect();
|
|
||||||
this.disconnect = null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setRef = c => {
|
setRef = c => {
|
||||||
this.column = c;
|
this.column = c;
|
||||||
}
|
}
|
||||||
|
|
||||||
handleLoadMore = maxId => {
|
handleEnableCrypto = () => {
|
||||||
this.props.dispatch(expandConversations({ maxId }));
|
this.props.dispatch(enableCrypto());
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { intl, hasUnread, columnId, multiColumn, shouldUpdateScroll } = this.props;
|
const { intl, hasUnread, columnId, multiColumn, shouldUpdateScroll, enabled } = this.props;
|
||||||
const pinned = !!columnId;
|
const pinned = !!columnId;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -87,14 +86,9 @@ class DirectTimeline extends React.PureComponent {
|
||||||
multiColumn={multiColumn}
|
multiColumn={multiColumn}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ConversationsListContainer
|
{!enabled && <button onClick={this.handleEnableCrypto}>Enable crypto</button>}
|
||||||
trackScroll={!pinned}
|
|
||||||
scrollKey={`direct_timeline-${columnId}`}
|
{enabled && <span>Crypto enabled</span>}
|
||||||
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}
|
|
||||||
/>
|
|
||||||
</Column>
|
</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 missed_updates from './missed_updates';
|
||||||
import announcements from './announcements';
|
import announcements from './announcements';
|
||||||
import markers from './markers';
|
import markers from './markers';
|
||||||
|
import crypto from './crypto';
|
||||||
|
|
||||||
const reducers = {
|
const reducers = {
|
||||||
announcements,
|
announcements,
|
||||||
|
@ -75,6 +76,7 @@ const reducers = {
|
||||||
trends,
|
trends,
|
||||||
missed_updates,
|
missed_updates,
|
||||||
markers,
|
markers,
|
||||||
|
crypto,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default combineReducers(reducers);
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
# namespace :crypto do
|
namespace :crypto do
|
||||||
# resources :deliveries, only: :create
|
resources :deliveries, only: :create
|
||||||
|
|
||||||
# namespace :keys do
|
namespace :keys do
|
||||||
# resource :upload, only: [:create]
|
resource :upload, only: [:create]
|
||||||
# resource :query, only: [:create]
|
resource :query, only: [:create]
|
||||||
# resource :claim, only: [:create]
|
resource :claim, only: [:create]
|
||||||
# resource :count, only: [:show]
|
resource :count, only: [:show]
|
||||||
# end
|
end
|
||||||
|
|
||||||
# resources :encrypted_messages, only: [:index] do
|
resources :encrypted_messages, only: [:index] do
|
||||||
# collection do
|
collection do
|
||||||
# post :clear
|
post :clear
|
||||||
# end
|
end
|
||||||
# end
|
end
|
||||||
# end
|
end
|
||||||
|
|
||||||
resources :conversations, only: [:index, :destroy] do
|
resources :conversations, only: [:index, :destroy] do
|
||||||
member do
|
member do
|
||||||
|
|
|
@ -2,6 +2,7 @@ const babel = require('./babel');
|
||||||
const css = require('./css');
|
const css = require('./css');
|
||||||
const file = require('./file');
|
const file = require('./file');
|
||||||
const nodeModules = require('./node_modules');
|
const nodeModules = require('./node_modules');
|
||||||
|
const wasm = require('./wasm');
|
||||||
|
|
||||||
// Webpack loaders are processed in reverse order
|
// Webpack loaders are processed in reverse order
|
||||||
// https://webpack.js.org/concepts/loaders/#loader-features
|
// https://webpack.js.org/concepts/loaders/#loader-features
|
||||||
|
@ -11,4 +12,5 @@ module.exports = {
|
||||||
css,
|
css,
|
||||||
nodeModules,
|
nodeModules,
|
||||||
babel,
|
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
|
// Called by http-link-header in an API we never use, increases
|
||||||
// bundle size unnecessarily
|
// bundle size unnecessarily
|
||||||
Buffer: false,
|
Buffer: false,
|
||||||
|
|
||||||
|
// Called by olm
|
||||||
|
fs: 'empty',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -88,6 +88,7 @@
|
||||||
"css-loader": "^3.6.0",
|
"css-loader": "^3.6.0",
|
||||||
"cssnano": "^4.1.10",
|
"cssnano": "^4.1.10",
|
||||||
"detect-passive-events": "^1.0.2",
|
"detect-passive-events": "^1.0.2",
|
||||||
|
"dexie": "^3.0.1",
|
||||||
"dotenv": "^8.2.0",
|
"dotenv": "^8.2.0",
|
||||||
"emoji-mart": "Gargron/emoji-mart#build",
|
"emoji-mart": "Gargron/emoji-mart#build",
|
||||||
"es6-symbol": "^3.1.3",
|
"es6-symbol": "^3.1.3",
|
||||||
|
@ -117,6 +118,7 @@
|
||||||
"object-fit-images": "^3.2.3",
|
"object-fit-images": "^3.2.3",
|
||||||
"object.values": "^1.1.1",
|
"object.values": "^1.1.1",
|
||||||
"offline-plugin": "^5.0.7",
|
"offline-plugin": "^5.0.7",
|
||||||
|
"olm": "https://packages.matrix.org/npm/olm/olm-3.1.4.tgz",
|
||||||
"path-complete-extname": "^1.0.0",
|
"path-complete-extname": "^1.0.0",
|
||||||
"pg": "^6.4.0",
|
"pg": "^6.4.0",
|
||||||
"postcss-loader": "^3.0.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"
|
resolved "https://registry.yarnpkg.com/detect-passive-events/-/detect-passive-events-1.0.4.tgz#6ed477e6e5bceb79079735dcd357789d37f9a91a"
|
||||||
integrity sha1-btR35uW863kHlzXc01d4nTf5qRo=
|
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:
|
diff-sequences@^25.2.6:
|
||||||
version "25.2.6"
|
version "25.2.6"
|
||||||
resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-25.2.6.tgz#5f467c00edd35352b7bca46d7927d60e687a76dd"
|
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"
|
minimatch "^3.0.3"
|
||||||
slash "^1.0.0"
|
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:
|
on-finished@~2.3.0:
|
||||||
version "2.3.0"
|
version "2.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947"
|
resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947"
|
||||||
|
|
Loading…
Reference in a new issue