summaryrefslogtreecommitdiffstats
path: root/app
diff options
context:
space:
mode:
authorAkihiko Odaki <akihiko.odaki.4i@stu.hosei.ac.jp>2018-03-24 21:06:27 +0900
committerEugen Rochko <eugen@zeonfederated.com>2018-03-24 13:06:27 +0100
commitfe398a098e9990ee3146e70be9e2cda6227274b8 (patch)
treed4fcb38f3dfa7b448907153ea4c9182088dc797c /app
parent28384c1771ccaa600e429f41cb2e19234961a9bd (diff)
Store objects to IndexedDB (#6826)
Diffstat (limited to 'app')
-rw-r--r--app/javascript/mastodon/actions/accounts.js42
-rw-r--r--app/javascript/mastodon/actions/blocks.js3
-rw-r--r--app/javascript/mastodon/actions/compose.js2
-rw-r--r--app/javascript/mastodon/actions/favourites.js3
-rw-r--r--app/javascript/mastodon/actions/importer/index.js76
-rw-r--r--app/javascript/mastodon/actions/importer/normalizer.js46
-rw-r--r--app/javascript/mastodon/actions/interactions.js39
-rw-r--r--app/javascript/mastodon/actions/lists.js14
-rw-r--r--app/javascript/mastodon/actions/mutes.js3
-rw-r--r--app/javascript/mastodon/actions/notifications.js22
-rw-r--r--app/javascript/mastodon/actions/pin_statuses.js2
-rw-r--r--app/javascript/mastodon/actions/search.js11
-rw-r--r--app/javascript/mastodon/actions/statuses.js65
-rw-r--r--app/javascript/mastodon/actions/store.js2
-rw-r--r--app/javascript/mastodon/actions/timelines.js5
-rw-r--r--app/javascript/mastodon/db/async.js28
-rw-r--r--app/javascript/mastodon/db/modifier.js93
-rw-r--r--app/javascript/mastodon/reducers/accounts.js125
-rw-r--r--app/javascript/mastodon/reducers/accounts_counters.js112
-rw-r--r--app/javascript/mastodon/reducers/statuses.js95
20 files changed, 433 insertions, 355 deletions
diff --git a/app/javascript/mastodon/actions/accounts.js b/app/javascript/mastodon/actions/accounts.js
index f63325658d6..1d1947acadd 100644
--- a/app/javascript/mastodon/actions/accounts.js
+++ b/app/javascript/mastodon/actions/accounts.js
@@ -1,4 +1,6 @@
import api, { getLinks } from '../api';
+import asyncDB from '../db/async';
+import { importAccount, importFetchedAccount, importFetchedAccounts } from './importer';
export const ACCOUNT_FETCH_REQUEST = 'ACCOUNT_FETCH_REQUEST';
export const ACCOUNT_FETCH_SUCCESS = 'ACCOUNT_FETCH_SUCCESS';
@@ -64,6 +66,24 @@ 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]));
@@ -74,9 +94,16 @@ export function fetchAccount(id) {
dispatch(fetchAccountRequest(id));
- api(getState).get(`/api/v1/accounts/${id}`).then(response => {
- dispatch(fetchAccountSuccess(response.data));
- }).catch(error => {
+ asyncDB.then(db => getFromDB(
+ dispatch,
+ getState,
+ db.transaction('accounts', 'read').objectStore('accounts').index('id'),
+ id
+ )).catch(() => api(getState).get(`/api/v1/accounts/${id}`).then(response => {
+ dispatch(importFetchedAccount(response.data));
+ })).then(() => {
+ dispatch(fetchAccountSuccess());
+ }, error => {
dispatch(fetchAccountFail(id, error));
});
};
@@ -89,10 +116,9 @@ export function fetchAccountRequest(id) {
};
};
-export function fetchAccountSuccess(account) {
+export function fetchAccountSuccess() {
return {
type: ACCOUNT_FETCH_SUCCESS,
- account,
};
};
@@ -319,6 +345,7 @@ export function fetchFollowers(id) {
api(getState).get(`/api/v1/accounts/${id}/followers`).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
+ dispatch(importFetchedAccounts(response.data));
dispatch(fetchFollowersSuccess(id, response.data, next ? next.uri : null));
dispatch(fetchRelationships(response.data.map(item => item.id)));
}).catch(error => {
@@ -364,6 +391,7 @@ export function expandFollowers(id) {
api(getState).get(url).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
+ dispatch(importFetchedAccounts(response.data));
dispatch(expandFollowersSuccess(id, response.data, next ? next.uri : null));
dispatch(fetchRelationships(response.data.map(item => item.id)));
}).catch(error => {
@@ -403,6 +431,7 @@ export function fetchFollowing(id) {
api(getState).get(`/api/v1/accounts/${id}/following`).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
+ dispatch(importFetchedAccounts(response.data));
dispatch(fetchFollowingSuccess(id, response.data, next ? next.uri : null));
dispatch(fetchRelationships(response.data.map(item => item.id)));
}).catch(error => {
@@ -448,6 +477,7 @@ export function expandFollowing(id) {
api(getState).get(url).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
+ dispatch(importFetchedAccounts(response.data));
dispatch(expandFollowingSuccess(id, response.data, next ? next.uri : null));
dispatch(fetchRelationships(response.data.map(item => item.id)));
}).catch(error => {
@@ -529,6 +559,7 @@ export function fetchFollowRequests() {
api(getState).get('/api/v1/follow_requests').then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
+ dispatch(importFetchedAccounts(response.data));
dispatch(fetchFollowRequestsSuccess(response.data, next ? next.uri : null));
}).catch(error => dispatch(fetchFollowRequestsFail(error)));
};
@@ -567,6 +598,7 @@ export function expandFollowRequests() {
api(getState).get(url).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
+ dispatch(importFetchedAccounts(response.data));
dispatch(expandFollowRequestsSuccess(response.data, next ? next.uri : null));
}).catch(error => dispatch(expandFollowRequestsFail(error)));
};
diff --git a/app/javascript/mastodon/actions/blocks.js b/app/javascript/mastodon/actions/blocks.js
index 553283a71f5..7000f5a71cb 100644
--- a/app/javascript/mastodon/actions/blocks.js
+++ b/app/javascript/mastodon/actions/blocks.js
@@ -1,5 +1,6 @@
import api, { getLinks } from '../api';
import { fetchRelationships } from './accounts';
+import { importFetchedAccounts } from './importer';
export const BLOCKS_FETCH_REQUEST = 'BLOCKS_FETCH_REQUEST';
export const BLOCKS_FETCH_SUCCESS = 'BLOCKS_FETCH_SUCCESS';
@@ -15,6 +16,7 @@ export function fetchBlocks() {
api(getState).get('/api/v1/blocks').then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
+ dispatch(importFetchedAccounts(response.data));
dispatch(fetchBlocksSuccess(response.data, next ? next.uri : null));
dispatch(fetchRelationships(response.data.map(item => item.id)));
}).catch(error => dispatch(fetchBlocksFail(error)));
@@ -54,6 +56,7 @@ export function expandBlocks() {
api(getState).get(url).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
+ dispatch(importFetchedAccounts(response.data));
dispatch(expandBlocksSuccess(response.data, next ? next.uri : null));
dispatch(fetchRelationships(response.data.map(item => item.id)));
}).catch(error => dispatch(expandBlocksFail(error)));
diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js
index 1371f22b27a..8e13209b839 100644
--- a/app/javascript/mastodon/actions/compose.js
+++ b/app/javascript/mastodon/actions/compose.js
@@ -4,6 +4,7 @@ import { throttle } from 'lodash';
import { search as emojiSearch } from '../features/emoji/emoji_mart_search_light';
import { tagHistory } from '../settings';
import { useEmoji } from './emojis';
+import { importFetchedAccounts } from './importer';
import {
updateTimeline,
@@ -282,6 +283,7 @@ const fetchComposeSuggestionsAccounts = throttle((dispatch, getState, token) =>
limit: 4,
},
}).then(response => {
+ dispatch(importFetchedAccounts(response.data));
dispatch(readyComposeSuggestionsAccounts(token, response.data));
});
}, 200, { leading: true, trailing: true });
diff --git a/app/javascript/mastodon/actions/favourites.js b/app/javascript/mastodon/actions/favourites.js
index 93094c52616..124cf8c44e3 100644
--- a/app/javascript/mastodon/actions/favourites.js
+++ b/app/javascript/mastodon/actions/favourites.js
@@ -1,4 +1,5 @@
import api, { getLinks } from '../api';
+import { importFetchedStatuses } from './importer';
export const FAVOURITED_STATUSES_FETCH_REQUEST = 'FAVOURITED_STATUSES_FETCH_REQUEST';
export const FAVOURITED_STATUSES_FETCH_SUCCESS = 'FAVOURITED_STATUSES_FETCH_SUCCESS';
@@ -18,6 +19,7 @@ export function fetchFavouritedStatuses() {
api(getState).get('/api/v1/favourites').then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
+ dispatch(importFetchedStatuses(response.data));
dispatch(fetchFavouritedStatusesSuccess(response.data, next ? next.uri : null));
}).catch(error => {
dispatch(fetchFavouritedStatusesFail(error));
@@ -58,6 +60,7 @@ export function expandFavouritedStatuses() {
api(getState).get(url).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
+ dispatch(importFetchedStatuses(response.data));
dispatch(expandFavouritedStatusesSuccess(response.data, next ? next.uri : null));
}).catch(error => {
dispatch(expandFavouritedStatusesFail(error));
diff --git a/app/javascript/mastodon/actions/importer/index.js b/app/javascript/mastodon/actions/importer/index.js
new file mode 100644
index 00000000000..d1ea40c3607
--- /dev/null
+++ b/app/javascript/mastodon/actions/importer/index.js
@@ -0,0 +1,76 @@
+import { putAccounts, putStatuses } from '../../db/modifier';
+import { normalizeAccount, normalizeStatus } from './normalizer';
+
+export const ACCOUNT_IMPORT = 'ACCOUNT_IMPORT';
+export const ACCOUNTS_IMPORT = 'ACCOUNTS_IMPORT';
+export const STATUS_IMPORT = 'STATUS_IMPORT';
+export const STATUSES_IMPORT = 'STATUSES_IMPORT';
+
+function pushUnique(array, object) {
+ if (array.every(element => element.id !== object.id)) {
+ array.push(object);
+ }
+}
+
+export function importAccount(account) {
+ return { type: ACCOUNT_IMPORT, account };
+}
+
+export function importAccounts(accounts) {
+ return { type: ACCOUNTS_IMPORT, accounts };
+}
+
+export function importStatus(status) {
+ return { type: STATUS_IMPORT, status };
+}
+
+export function importStatuses(statuses) {
+ return { type: STATUSES_IMPORT, statuses };
+}
+
+export function importFetchedAccount(account) {
+ return importFetchedAccounts([account]);
+}
+
+export function importFetchedAccounts(accounts) {
+ const normalAccounts = [];
+
+ function processAccount(account) {
+ pushUnique(normalAccounts, normalizeAccount(account));
+
+ if (account.moved) {
+ processAccount(account);
+ }
+ }
+
+ accounts.forEach(processAccount);
+ putAccounts(normalAccounts);
+
+ return importAccounts(normalAccounts);
+}
+
+export function importFetchedStatus(status) {
+ return importFetchedStatuses([status]);
+}
+
+export function importFetchedStatuses(statuses) {
+ return (dispatch, getState) => {
+ const accounts = [];
+ const normalStatuses = [];
+
+ function processStatus(status) {
+ pushUnique(normalStatuses, normalizeStatus(status, getState().getIn(['statuses', status.id])));
+ pushUnique(accounts, status.account);
+
+ if (status.reblog && status.reblog.id) {
+ processStatus(status.reblog);
+ }
+ }
+
+ statuses.forEach(processStatus);
+ putStatuses(normalStatuses);
+
+ dispatch(importFetchedAccounts(accounts));
+ dispatch(importStatuses(normalStatuses));
+ };
+}
diff --git a/app/javascript/mastodon/actions/importer/normalizer.js b/app/javascript/mastodon/actions/importer/normalizer.js
new file mode 100644
index 00000000000..c88f6946fe8
--- /dev/null
+++ b/app/javascript/mastodon/actions/importer/normalizer.js
@@ -0,0 +1,46 @@
+import escapeTextContentForBrowser from 'escape-html';
+import emojify from '../../features/emoji/emoji';
+
+const domParser = new DOMParser();
+
+export function normalizeAccount(account) {
+ account = { ...account };
+
+ const displayName = account.display_name.length === 0 ? account.username : account.display_name;
+ account.display_name_html = emojify(escapeTextContentForBrowser(displayName));
+ account.note_emojified = emojify(account.note);
+
+ return account;
+}
+
+export function normalizeStatus(status, normalOldStatus) {
+ const normalStatus = { ...status };
+ normalStatus.account = status.account.id;
+
+ if (status.reblog && status.reblog.id) {
+ normalStatus.reblog = status.reblog.id;
+ }
+
+ // Only calculate these values when status first encountered
+ // Otherwise keep the ones already in the reducer
+ if (normalOldStatus) {
+ normalStatus.search_index = normalOldStatus.get('search_index');
+ normalStatus.contentHtml = normalOldStatus.get('contentHtml');
+ normalStatus.spoilerHtml = normalOldStatus.get('spoilerHtml');
+ normalStatus.hidden = normalOldStatus.get('hidden');
+ } else {
+ const searchContent = [status.spoiler_text, status.content].join('\n\n').replace(/<br\s*\/?>/g, '\n').replace(/<\/p><p>/g, '\n\n');
+
+ const emojiMap = normalStatus.emojis.reduce((obj, emoji) => {
+ obj[`:${emoji.shortcode}:`] = emoji;
+ return obj;
+ }, {});
+
+ normalStatus.search_index = domParser.parseFromString(searchContent, 'text/html').documentElement.textContent;
+ normalStatus.contentHtml = emojify(normalStatus.content, emojiMap);
+ normalStatus.spoilerHtml = emojify(escapeTextContentForBrowser(normalStatus.spoiler_text || ''), emojiMap);
+ normalStatus.hidden = normalStatus.sensitive;
+ }
+
+ return normalStatus;
+}
diff --git a/app/javascript/mastodon/actions/interactions.js b/app/javascript/mastodon/actions/interactions.js
index 10e66910af9..2dc4c574cd4 100644
--- a/app/javascript/mastodon/actions/interactions.js
+++ b/app/javascript/mastodon/actions/interactions.js
@@ -1,4 +1,5 @@
import api from '../api';
+import { importFetchedAccounts, importFetchedStatus } from './importer';
export const REBLOG_REQUEST = 'REBLOG_REQUEST';
export const REBLOG_SUCCESS = 'REBLOG_SUCCESS';
@@ -39,7 +40,8 @@ export function reblog(status) {
api(getState).post(`/api/v1/statuses/${status.get('id')}/reblog`).then(function (response) {
// The reblog API method returns a new status wrapped around the original. In this case we are only
// interested in how the original is modified, hence passing it skipping the wrapper
- dispatch(reblogSuccess(status, response.data.reblog));
+ dispatch(importFetchedStatus(response.data.reblog));
+ dispatch(reblogSuccess(status));
}).catch(function (error) {
dispatch(reblogFail(status, error));
});
@@ -51,7 +53,8 @@ export function unreblog(status) {
dispatch(unreblogRequest(status));
api(getState).post(`/api/v1/statuses/${status.get('id')}/unreblog`).then(response => {
- dispatch(unreblogSuccess(status, response.data));
+ dispatch(importFetchedStatus(response.data));
+ dispatch(unreblogSuccess(status));
}).catch(error => {
dispatch(unreblogFail(status, error));
});
@@ -66,11 +69,10 @@ export function reblogRequest(status) {
};
};
-export function reblogSuccess(status, response) {
+export function reblogSuccess(status) {
return {
type: REBLOG_SUCCESS,
status: status,
- response: response,
skipLoading: true,
};
};
@@ -92,11 +94,10 @@ export function unreblogRequest(status) {
};
};
-export function unreblogSuccess(status, response) {
+export function unreblogSuccess(status) {
return {
type: UNREBLOG_SUCCESS,
status: status,
- response: response,
skipLoading: true,
};
};
@@ -115,7 +116,8 @@ export function favourite(status) {
dispatch(favouriteRequest(status));
api(getState).post(`/api/v1/statuses/${status.get('id')}/favourite`).then(function (response) {
- dispatch(favouriteSuccess(status, response.data));
+ dispatch(importFetchedStatus(response.data));
+ dispatch(favouriteSuccess(status));
}).catch(function (error) {
dispatch(favouriteFail(status, error));
});
@@ -127,7 +129,8 @@ export function unfavourite(status) {
dispatch(unfavouriteRequest(status));
api(getState).post(`/api/v1/statuses/${status.get('id')}/unfavourite`).then(response => {
- dispatch(unfavouriteSuccess(status, response.data));
+ dispatch(importFetchedStatus(response.data));
+ dispatch(unfavouriteSuccess(status));
}).catch(error => {
dispatch(unfavouriteFail(status, error));
});
@@ -142,11 +145,10 @@ export function favouriteRequest(status) {
};
};
-export function favouriteSuccess(status, response) {
+export function favouriteSuccess(status) {
return {
type: FAVOURITE_SUCCESS,
status: status,
- response: response,
skipLoading: true,
};
};
@@ -168,11 +170,10 @@ export function unfavouriteRequest(status) {
};
};
-export function unfavouriteSuccess(status, response) {
+export function unfavouriteSuccess(status) {
return {
type: UNFAVOURITE_SUCCESS,
status: status,
- response: response,
skipLoading: true,
};
};
@@ -191,6 +192,7 @@ export function fetchReblogs(id) {
dispatch(fetchReblogsRequest(id));
api(getState).get(`/api/v1/statuses/${id}/reblogged_by`).then(response => {
+ dispatch(importFetchedAccounts(response.data));
dispatch(fetchReblogsSuccess(id, response.data));
}).catch(error => {
dispatch(fetchReblogsFail(id, error));
@@ -225,6 +227,7 @@ export function fetchFavourites(id) {
dispatch(fetchFavouritesRequest(id));
api(getState).get(`/api/v1/statuses/${id}/favourited_by`).then(response => {
+ dispatch(importFetchedAccounts(response.data));
dispatch(fetchFavouritesSuccess(id, response.data));
}).catch(error => {
dispatch(fetchFavouritesFail(id, error));
@@ -259,7 +262,8 @@ export function pin(status) {
dispatch(pinRequest(status));
api(getState).post(`/api/v1/statuses/${status.get('id')}/pin`).then(response => {
- dispatch(pinSuccess(status, response.data));
+ dispatch(importFetchedStatus(response.data));
+ dispatch(pinSuccess(status));
}).catch(error => {
dispatch(pinFail(status, error));
});
@@ -274,11 +278,10 @@ export function pinRequest(status) {
};
};
-export function pinSuccess(status, response) {
+export function pinSuccess(status) {
return {
type: PIN_SUCCESS,
status,
- response,
skipLoading: true,
};
};
@@ -297,7 +300,8 @@ export function unpin (status) {
dispatch(unpinRequest(status));
api(getState).post(`/api/v1/statuses/${status.get('id')}/unpin`).then(response => {
- dispatch(unpinSuccess(status, response.data));
+ dispatch(importFetchedStatus(response.data));
+ dispatch(unpinSuccess(status));
}).catch(error => {
dispatch(unpinFail(status, error));
});
@@ -312,11 +316,10 @@ export function unpinRequest(status) {
};
};
-export function unpinSuccess(status, response) {
+export function unpinSuccess(status) {
return {
type: UNPIN_SUCCESS,
status,
- response,
skipLoading: true,
};
};
diff --git a/app/javascript/mastodon/actions/lists.js b/app/javascript/mastodon/actions/lists.js
index 4c8f9b186ef..12d60e3a380 100644
--- a/app/javascript/mastodon/actions/lists.js
+++ b/app/javascript/mastodon/actions/lists.js
@@ -1,4 +1,5 @@
import api from '../api';
+import { importFetchedAccounts } from './importer';
export const LIST_FETCH_REQUEST = 'LIST_FETCH_REQUEST';
export const LIST_FETCH_SUCCESS = 'LIST_FETCH_SUCCESS';
@@ -200,9 +201,10 @@ export const deleteListFail = (id, error) => ({
export const fetchListAccounts = listId => (dispatch, getState) => {
dispatch(fetchListAccountsRequest(listId));
- api(getState).get(`/api/v1/lists/${listId}/accounts`, { params: { limit: 0 } })
- .then(({ data }) => dispatch(fetchListAccountsSuccess(listId, data)))
- .catch(err => dispatch(fetchListAccountsFail(listId, err)));
+ api(getState).get(`/api/v1/lists/${listId}/accounts`, { params: { limit: 0 } }).then(({ data }) => {
+ dispatch(importFetchedAccounts(data));
+ dispatch(fetchListAccountsSuccess(listId, data));
+ }).catch(err => dispatch(fetchListAccountsFail(listId, err)));
};
export const fetchListAccountsRequest = id => ({
@@ -231,8 +233,10 @@ export const fetchListSuggestions = q => (dispatch, getState) => {
following: true,
};
- api(getState).get('/api/v1/accounts/search', { params })
- .then(({ data }) => dispatch(fetchListSuggestionsReady(q, data)));
+ api(getState).get('/api/v1/accounts/search', { params }).then(({ data }) => {
+ dispatch(importFetchedAccounts(data));
+ dispatch(fetchListSuggestionsReady(q, data));
+ });
};
export const fetchListSuggestionsReady = (query, accounts) => ({
diff --git a/app/javascript/mastodon/actions/mutes.js b/app/javascript/mastodon/actions/mutes.js
index daa76a8f7c2..9f645faee17 100644
--- a/app/javascript/mastodon/actions/mutes.js
+++ b/app/javascript/mastodon/actions/mutes.js
@@ -1,5 +1,6 @@
import api, { getLinks } from '../api';
import { fetchRelationships } from './accounts';
+import { importFetchedAccounts } from './importer';
import { openModal } from './modal';
export const MUTES_FETCH_REQUEST = 'MUTES_FETCH_REQUEST';
@@ -19,6 +20,7 @@ export function fetchMutes() {
api(getState).get('/api/v1/mutes').then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
+ dispatch(importFetchedAccounts(response.data));
dispatch(fetchMutesSuccess(response.data, next ? next.uri : null));
dispatch(fetchRelationships(response.data.map(item => item.id)));
}).catch(error => dispatch(fetchMutesFail(error)));
@@ -58,6 +60,7 @@ export function expandMutes() {
api(getState).get(url).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
+ dispatch(importFetchedAccounts(response.data));
dispatch(expandMutesSuccess(response.data, next ? next.uri : null));
dispatch(fetchRelationships(response.data.map(item => item.id)));
}).catch(error => dispatch(expandMutesFail(error)));
diff --git a/app/javascript/mastodon/actions/notifications.js b/app/javascript/mastodon/actions/notifications.js
index cf9242d0fbf..a664cd97877 100644
--- a/app/javascript/mastodon/actions/notifications.js
+++ b/app/javascript/mastodon/actions/notifications.js
@@ -2,6 +2,12 @@ import api, { getLinks } from '../api';
import { List as ImmutableList } from 'immutable';
import IntlMessageFormat from 'intl-messageformat';
import { fetchRelationships } from './accounts';
+import