logo

mastofe

My custom branche(s) on git.pleroma.social/pleroma/mastofe
commit: fe398a098e9990ee3146e70be9e2cda6227274b8
parent: 28384c1771ccaa600e429f41cb2e19234961a9bd
Author: Akihiko Odaki <akihiko.odaki.4i@stu.hosei.ac.jp>
Date:   Sat, 24 Mar 2018 21:06:27 +0900

Store objects to IndexedDB (#6826)


Diffstat:

Mapp/javascript/mastodon/actions/accounts.js42+++++++++++++++++++++++++++++++++++++-----
Mapp/javascript/mastodon/actions/blocks.js3+++
Mapp/javascript/mastodon/actions/compose.js2++
Mapp/javascript/mastodon/actions/favourites.js3+++
Aapp/javascript/mastodon/actions/importer/index.js76++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aapp/javascript/mastodon/actions/importer/normalizer.js46++++++++++++++++++++++++++++++++++++++++++++++
Mapp/javascript/mastodon/actions/interactions.js39+++++++++++++++++++++------------------
Mapp/javascript/mastodon/actions/lists.js14+++++++++-----
Mapp/javascript/mastodon/actions/mutes.js3+++
Mapp/javascript/mastodon/actions/notifications.js22++++++++++++++++------
Mapp/javascript/mastodon/actions/pin_statuses.js2++
Mapp/javascript/mastodon/actions/search.js11+++++++++--
Mapp/javascript/mastodon/actions/statuses.js65++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
Mapp/javascript/mastodon/actions/store.js2++
Mapp/javascript/mastodon/actions/timelines.js5+++++
Aapp/javascript/mastodon/db/async.js28++++++++++++++++++++++++++++
Aapp/javascript/mastodon/db/modifier.js93+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mapp/javascript/mastodon/reducers/accounts.js125++++---------------------------------------------------------------------------
Mapp/javascript/mastodon/reducers/accounts_counters.js112+++----------------------------------------------------------------------------
Mapp/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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 { + importFetchedAccount, + importFetchedAccounts, + importFetchedStatus, + importFetchedStatuses, +} from './importer'; import { defineMessages } from 'react-intl'; export const NOTIFICATIONS_UPDATE = 'NOTIFICATIONS_UPDATE'; @@ -41,11 +47,12 @@ export function updateNotifications(notification, intlMessages, intlLocale) { const showAlert = getState().getIn(['settings', 'notifications', 'alerts', notification.type], true); const playSound = getState().getIn(['settings', 'notifications', 'sounds', notification.type], true); + dispatch(importFetchedAccount(notification.account)); + dispatch(importFetchedStatus(notification.status)); + dispatch({ type: NOTIFICATIONS_UPDATE, notification, - account: notification.account, - status: notification.status, meta: playSound ? { sound: 'boop' } : undefined, }); @@ -89,6 +96,9 @@ export function refreshNotifications() { api(getState).get('/api/v1/notifications', { params }).then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); + dispatch(importFetchedAccounts(response.data.map(item => item.account))); + dispatch(importFetchedStatuses(response.data.map(item => item.status).filter(status => !!status))); + dispatch(refreshNotificationsSuccess(response.data, skipLoading, next ? next.uri : null)); fetchRelatedRelationships(dispatch, response.data); }).catch(error => { @@ -108,8 +118,6 @@ export function refreshNotificationsSuccess(notifications, skipLoading, next) { return { type: NOTIFICATIONS_REFRESH_SUCCESS, notifications, - accounts: notifications.map(item => item.account), - statuses: notifications.map(item => item.status).filter(status => !!status), skipLoading, next, }; @@ -141,6 +149,10 @@ export function expandNotifications() { api(getState).get('/api/v1/notifications', { params }).then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); + + dispatch(importFetchedAccounts(response.data.map(item => item.account))); + dispatch(importFetchedStatuses(response.data.map(item => item.status).filter(status => !!status))); + dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null)); fetchRelatedRelationships(dispatch, response.data); }).catch(error => { @@ -159,8 +171,6 @@ export function expandNotificationsSuccess(notifications, next) { return { type: NOTIFICATIONS_EXPAND_SUCCESS, notifications, - accounts: notifications.map(item => item.account), - statuses: notifications.map(item => item.status).filter(status => !!status), next, }; }; diff --git a/app/javascript/mastodon/actions/pin_statuses.js b/app/javascript/mastodon/actions/pin_statuses.js @@ -1,4 +1,5 @@ import api from '../api'; +import { importFetchedStatuses } from './importer'; export const PINNED_STATUSES_FETCH_REQUEST = 'PINNED_STATUSES_FETCH_REQUEST'; export const PINNED_STATUSES_FETCH_SUCCESS = 'PINNED_STATUSES_FETCH_SUCCESS'; @@ -11,6 +12,7 @@ export function fetchPinnedStatuses() { dispatch(fetchPinnedStatusesRequest()); api(getState).get(`/api/v1/accounts/${me}/statuses`, { params: { pinned: true } }).then(response => { + dispatch(importFetchedStatuses(response.data)); dispatch(fetchPinnedStatusesSuccess(response.data, null)); }).catch(error => { dispatch(fetchPinnedStatusesFail(error)); diff --git a/app/javascript/mastodon/actions/search.js b/app/javascript/mastodon/actions/search.js @@ -1,5 +1,6 @@ import api from '../api'; import { fetchRelationships } from './accounts'; +import { importFetchedAccounts, importFetchedStatuses } from './importer'; export const SEARCH_CHANGE = 'SEARCH_CHANGE'; export const SEARCH_CLEAR = 'SEARCH_CLEAR'; @@ -38,6 +39,14 @@ export function submitSearch() { resolve: true, }, }).then(response => { + if (response.data.accounts) { + dispatch(importFetchedAccounts(response.data.accounts)); + } + + if (response.data.statuses) { + dispatch(importFetchedStatuses(response.data.statuses)); + } + dispatch(fetchSearchSuccess(response.data)); dispatch(fetchRelationships(response.data.accounts.map(item => item.id))); }).catch(error => { @@ -56,8 +65,6 @@ export function fetchSearchSuccess(results) { return { type: SEARCH_FETCH_SUCCESS, results, - accounts: results.accounts, - statuses: results.statuses, }; }; diff --git a/app/javascript/mastodon/actions/statuses.js b/app/javascript/mastodon/actions/statuses.js @@ -1,7 +1,10 @@ import api from '../api'; +import asyncDB from '../db/async'; +import { evictStatus } from '../db/modifier'; import { deleteFromTimelines } from './timelines'; import { fetchStatusCard } from './cards'; +import { importFetchedStatus, importFetchedStatuses, importAccount, importStatus } from './importer'; export const STATUS_FETCH_REQUEST = 'STATUS_FETCH_REQUEST'; export const STATUS_FETCH_SUCCESS = 'STATUS_FETCH_SUCCESS'; @@ -34,6 +37,48 @@ export function fetchStatusRequest(id, skipLoading) { }; }; +function getFromDB(dispatch, getState, accountIndex, index, id) { + return new Promise((resolve, reject) => { + const request = index.get(id); + + request.onerror = reject; + + request.onsuccess = () => { + const promises = []; + + if (!request.result) { + reject(); + return; + } + + dispatch(importStatus(request.result)); + + if (getState().getIn(['accounts', request.result.account], null) === null) { + promises.push(new Promise((accountResolve, accountReject) => { + const accountRequest = accountIndex.get(request.result.account); + + accountRequest.onerror = accountReject; + accountRequest.onsuccess = () => { + if (!request.result) { + accountReject(); + return; + } + + dispatch(importAccount(accountRequest.result)); + accountResolve(); + }; + })); + } + + if (request.result.reblog && getState().getIn(['statuses', request.result.reblog], null) === null) { + promises.push(getFromDB(dispatch, getState, accountIndex, index, request.result.reblog)); + } + + resolve(Promise.all(promises)); + }; + }); +} + export function fetchStatus(id) { return (dispatch, getState) => { const skipLoading = getState().getIn(['statuses', id], null) !== null; @@ -47,18 +92,26 @@ export function fetchStatus(id) { dispatch(fetchStatusRequest(id, skipLoading)); - api(getState).get(`/api/v1/statuses/${id}`).then(response => { - dispatch(fetchStatusSuccess(response.data, skipLoading)); - }).catch(error => { + asyncDB.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(() => { + dispatch(fetchStatusSuccess(skipLoading)); + }, () => api(getState).get(`/api/v1/statuses/${id}`).then(response => { + dispatch(importFetchedStatus(response.data)); + dispatch(fetchStatusSuccess(skipLoading)); + })).catch(error => { dispatch(fetchStatusFail(id, error, skipLoading)); }); }; }; -export function fetchStatusSuccess(status, skipLoading) { +export function fetchStatusSuccess(skipLoading) { return { type: STATUS_FETCH_SUCCESS, - status, skipLoading, }; }; @@ -78,6 +131,7 @@ export function deleteStatus(id) { dispatch(deleteStatusRequest(id)); api(getState).delete(`/api/v1/statuses/${id}`).then(() => { + evictStatus(id); dispatch(deleteStatusSuccess(id)); dispatch(deleteFromTimelines(id)); }).catch(error => { @@ -113,6 +167,7 @@ export function fetchContext(id) { dispatch(fetchContextRequest(id)); api(getState).get(`/api/v1/statuses/${id}/context`).then(response => { + dispatch(importFetchedStatuses(response.data.ancestors.concat(response.data.descendants))); dispatch(fetchContextSuccess(id, response.data.ancestors, response.data.descendants)); }).catch(error => { diff --git a/app/javascript/mastodon/actions/store.js b/app/javascript/mastodon/actions/store.js @@ -1,5 +1,6 @@ import { Iterable, fromJS } from 'immutable'; import { hydrateCompose } from './compose'; +import { importFetchedAccounts } from './importer'; export const STORE_HYDRATE = 'STORE_HYDRATE'; export const STORE_HYDRATE_LAZY = 'STORE_HYDRATE_LAZY'; @@ -18,5 +19,6 @@ export function hydrateStore(rawState) { }); dispatch(hydrateCompose()); + dispatch(importFetchedAccounts(Object.values(rawState.accounts))); }; }; diff --git a/app/javascript/mastodon/actions/timelines.js b/app/javascript/mastodon/actions/timelines.js @@ -1,3 +1,4 @@ +import { importFetchedStatus, importFetchedStatuses } from './importer'; import api, { getLinks } from '../api'; import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; @@ -44,6 +45,8 @@ export function updateTimeline(timeline, status) { } } + dispatch(importFetchedStatus(status)); + dispatch({ type: TIMELINE_UPDATE, timeline, @@ -109,6 +112,7 @@ export function refreshTimeline(timelineId, path, params = {}) { dispatch(refreshTimelineSuccess(timelineId, [], skipLoading, null, true)); } else { const next = getLinks(response).refs.find(link => link.rel === 'next'); + dispatch(importFetchedStatuses(response.data)); dispatch(refreshTimelineSuccess(timelineId, response.data, skipLoading, next ? next.uri : null, false)); } }).catch(error => { @@ -152,6 +156,7 @@ export function expandTimeline(timelineId, path, params = {}) { api(getState).get(path, { params }).then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); + dispatch(importFetchedStatuses(response.data)); dispatch(expandTimelineSuccess(timelineId, response.data, next ? next.uri : null)); }).catch(error => { dispatch(expandTimelineFail(timelineId, error)); diff --git a/app/javascript/mastodon/db/async.js b/app/javascript/mastodon/db/async.js @@ -0,0 +1,28 @@ +import { me } from '../initial_state'; + +export default new Promise((resolve, reject) => { + // 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 (!me || !('getAll' in IDBObjectStore.prototype)) { + reject(); + return; + } + + const request = indexedDB.open('mastodon:' + me); + + 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/db/modifier.js b/app/javascript/mastodon/db/modifier.js @@ -0,0 +1,93 @@ +import asyncDB from './async'; + +const limit = 1024; + +function put(name, objects, callback) { + asyncDB.then(db => { + const putTransaction = db.transaction(name, 'readwrite'); + const putStore = putTransaction.objectStore(name); + const putIndex = putStore.index('id'); + + objects.forEach(object => { + function add() { + putStore.add(object); + } + + putIndex.getKey(object.id).onsuccess = retrieval => { + if (retrieval.target.result) { + putStore.delete(retrieval.target.result).onsuccess = add; + } else { + add(); + } + }; + }); + + putTransaction.oncomplete = () => { + const readTransaction = db.transaction(name, 'readonly'); + const readStore = readTransaction.objectStore(name); + + readStore.count().onsuccess = count => { + const excess = count.target.result - limit; + + if (excess > 0) { + readStore.getAll(null, excess).onsuccess = + retrieval => callback(retrieval.target.result.map(({ id }) => id)); + } + }; + }; + }); +} + +export function evictAccounts(ids) { + asyncDB.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(id => { + accountsMovedIndex.getAllKeys(id).onsuccess = + ({ target }) => evict(target.result); + + statusesIndex.getAll(id).onsuccess = + ({ target }) => evictStatuses(target.result.map(({ id }) => id)); + + accountsIdIndex.getKey(id).onsuccess = + ({ target }) => target.result && accounts.delete(target.result); + }); + } + + evict(ids); + }); +} + +export function evictStatus(id) { + return evictStatuses([id]); +} + +export function evictStatuses(ids) { + asyncDB.then(db => { + const store = db.transaction('statuses', 'readwrite').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); + }); + }); +} + +export function putAccounts(records) { + put('accounts', records, evictAccounts); +} + +export function putStatuses(records) { + put('statuses', records, evictStatuses); +} diff --git a/app/javascript/mastodon/reducers/accounts.js b/app/javascript/mastodon/reducers/accounts.js @@ -1,56 +1,7 @@ -import { - ACCOUNT_FETCH_SUCCESS, - FOLLOWERS_FETCH_SUCCESS, - FOLLOWERS_EXPAND_SUCCESS, - FOLLOWING_FETCH_SUCCESS, - FOLLOWING_EXPAND_SUCCESS, - FOLLOW_REQUESTS_FETCH_SUCCESS, - FOLLOW_REQUESTS_EXPAND_SUCCESS, -} from '../actions/accounts'; -import { - BLOCKS_FETCH_SUCCESS, - BLOCKS_EXPAND_SUCCESS, -} from '../actions/blocks'; -import { - MUTES_FETCH_SUCCESS, - MUTES_EXPAND_SUCCESS, -} from '../actions/mutes'; -import { COMPOSE_SUGGESTIONS_READY } from '../actions/compose'; -import { - REBLOG_SUCCESS, - UNREBLOG_SUCCESS, - FAVOURITE_SUCCESS, - UNFAVOURITE_SUCCESS, - REBLOGS_FETCH_SUCCESS, - FAVOURITES_FETCH_SUCCESS, -} from '../actions/interactions'; -import { - TIMELINE_REFRESH_SUCCESS, - TIMELINE_UPDATE, - TIMELINE_EXPAND_SUCCESS, -} from '../actions/timelines'; -import { - STATUS_FETCH_SUCCESS, - CONTEXT_FETCH_SUCCESS, -} from '../actions/statuses'; -import { SEARCH_FETCH_SUCCESS } from '../actions/search'; -import { - NOTIFICATIONS_UPDATE, - NOTIFICATIONS_REFRESH_SUCCESS, - NOTIFICATIONS_EXPAND_SUCCESS, -} from '../actions/notifications'; -import { - FAVOURITED_STATUSES_FETCH_SUCCESS, - FAVOURITED_STATUSES_EXPAND_SUCCESS, -} from '../actions/favourites'; -import { - LIST_ACCOUNTS_FETCH_SUCCESS, - LIST_EDITOR_SUGGESTIONS_READY, -} from '../actions/lists'; -import { STORE_HYDRATE } from '../actions/store'; -import emojify from '../features/emoji/emoji'; +import { ACCOUNT_IMPORT, ACCOUNTS_IMPORT } from '../actions/importer'; import { Map as ImmutableMap, fromJS } from 'immutable'; -import escapeTextContentForBrowser from 'escape-html'; + +const initialState = ImmutableMap(); const normalizeAccount = (state, account) => { account = { ...account }; @@ -59,15 +10,6 @@ const normalizeAccount = (state, account) => { delete account.following_count; delete account.statuses_count; - 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); - - if (account.moved) { - state = normalizeAccount(state, account.moved); - account.moved = account.moved.id; - } - return state.set(account.id, fromJS(account)); }; @@ -79,67 +21,12 @@ const normalizeAccounts = (state, accounts) => { return state; }; -const normalizeAccountFromStatus = (state, status) => { - state = normalizeAccount(state, status.account); - - if (status.reblog && status.reblog.account) { - state = normalizeAccount(state, status.reblog.account); - } - - return state; -}; - -const normalizeAccountsFromStatuses = (state, statuses) => { - statuses.forEach(status => { - state = normalizeAccountFromStatus(state, status); - }); - - return state; -}; - -const initialState = ImmutableMap(); - export default function accounts(state = initialState, action) { switch(action.type) { - case STORE_HYDRATE: - return normalizeAccounts(state, Object.values(action.state.get('accounts').toJS())); - case ACCOUNT_FETCH_SUCCESS: - case NOTIFICATIONS_UPDATE: + case ACCOUNT_IMPORT: return normalizeAccount(state, action.account); - case FOLLOWERS_FETCH_SUCCESS: - case FOLLOWERS_EXPAND_SUCCESS: - case FOLLOWING_FETCH_SUCCESS: - case FOLLOWING_EXPAND_SUCCESS: - case REBLOGS_FETCH_SUCCESS: - case FAVOURITES_FETCH_SUCCESS: - case COMPOSE_SUGGESTIONS_READY: - case FOLLOW_REQUESTS_FETCH_SUCCESS: - case FOLLOW_REQUESTS_EXPAND_SUCCESS: - case BLOCKS_FETCH_SUCCESS: - case BLOCKS_EXPAND_SUCCESS: - case MUTES_FETCH_SUCCESS: - case MUTES_EXPAND_SUCCESS: - case LIST_ACCOUNTS_FETCH_SUCCESS: - case LIST_EDITOR_SUGGESTIONS_READY: - return action.accounts ? normalizeAccounts(state, action.accounts) : state; - case NOTIFICATIONS_REFRESH_SUCCESS: - case NOTIFICATIONS_EXPAND_SUCCESS: - case SEARCH_FETCH_SUCCESS: - return normalizeAccountsFromStatuses(normalizeAccounts(state, action.accounts), action.statuses); - case TIMELINE_REFRESH_SUCCESS: - case TIMELINE_EXPAND_SUCCESS: - case CONTEXT_FETCH_SUCCESS: - case FAVOURITED_STATUSES_FETCH_SUCCESS: - case FAVOURITED_STATUSES_EXPAND_SUCCESS: - return normalizeAccountsFromStatuses(state, action.statuses); - case REBLOG_SUCCESS: - case FAVOURITE_SUCCESS: - case UNREBLOG_SUCCESS: - case UNFAVOURITE_SUCCESS: - return normalizeAccountFromStatus(state, action.response); - case TIMELINE_UPDATE: - case STATUS_FETCH_SUCCESS: - return normalizeAccountFromStatus(state, action.status); + case ACCOUNTS_IMPORT: + return normalizeAccounts(state, action.accounts); default: return state; } diff --git a/app/javascript/mastodon/reducers/accounts_counters.js b/app/javascript/mastodon/reducers/accounts_counters.js @@ -1,55 +1,8 @@ import { - ACCOUNT_FETCH_SUCCESS, - FOLLOWERS_FETCH_SUCCESS, - FOLLOWERS_EXPAND_SUCCESS, - FOLLOWING_FETCH_SUCCESS, - FOLLOWING_EXPAND_SUCCESS, - FOLLOW_REQUESTS_FETCH_SUCCESS, - FOLLOW_REQUESTS_EXPAND_SUCCESS, ACCOUNT_FOLLOW_SUCCESS, ACCOUNT_UNFOLLOW_SUCCESS, } from '../actions/accounts'; -import { - BLOCKS_FETCH_SUCCESS, - BLOCKS_EXPAND_SUCCESS, -} from '../actions/blocks'; -import { - MUTES_FETCH_SUCCESS, - MUTES_EXPAND_SUCCESS, -} from '../actions/mutes'; -import { COMPOSE_SUGGESTIONS_READY } from '../actions/compose'; -import { - REBLOG_SUCCESS, - UNREBLOG_SUCCESS, - FAVOURITE_SUCCESS, - UNFAVOURITE_SUCCESS, - REBLOGS_FETCH_SUCCESS, - FAVOURITES_FETCH_SUCCESS, -} from '../actions/interactions'; -import { - TIMELINE_REFRESH_SUCCESS, - TIMELINE_UPDATE, - TIMELINE_EXPAND_SUCCESS, -} from '../actions/timelines'; -import { - STATUS_FETCH_SUCCESS, - CONTEXT_FETCH_SUCCESS, -} from '../actions/statuses'; -import { SEARCH_FETCH_SUCCESS } from '../actions/search'; -import { - NOTIFICATIONS_UPDATE, - NOTIFICATIONS_REFRESH_SUCCESS, - NOTIFICATIONS_EXPAND_SUCCESS, -} from '../actions/notifications'; -import { - FAVOURITED_STATUSES_FETCH_SUCCESS, - FAVOURITED_STATUSES_EXPAND_SUCCESS, -} from '../actions/favourites'; -import { - LIST_ACCOUNTS_FETCH_SUCCESS, - LIST_EDITOR_SUGGESTIONS_READY, -} from '../actions/lists'; -import { STORE_HYDRATE } from '../actions/store'; +import { ACCOUNT_IMPORT, ACCOUNTS_IMPORT } from '../actions/importer'; import { Map as ImmutableMap, fromJS } from 'immutable'; const normalizeAccount = (state, account) => state.set(account.id, fromJS({ @@ -66,71 +19,14 @@ const normalizeAccounts = (state, accounts) => { return state; }; -const normalizeAccountFromStatus = (state, status) => { - state = normalizeAccount(state, status.account); - - if (status.reblog && status.reblog.account) { - state = normalizeAccount(state, status.reblog.account); - } - - return state; -}; - -const normalizeAccountsFromStatuses = (state, statuses) => { - statuses.forEach(status => { - state = normalizeAccountFromStatus(state, status); - }); - - return state; -}; - const initialState = ImmutableMap(); export default function accountsCounters(state = initialState, action) { switch(action.type) { - case STORE_HYDRATE: - return state.merge(action.state.get('accounts').map(item => fromJS({ - followers_count: item.get('followers_count'), - following_count: item.get('following_count'), - statuses_count: item.get('statuses_count'), - }))); - case ACCOUNT_FETCH_SUCCESS: - case NOTIFICATIONS_UPDATE: + case ACCOUNT_IMPORT: return normalizeAccount(state, action.account); - case FOLLOWERS_FETCH_SUCCESS: - case FOLLOWERS_EXPAND_SUCCESS: - case FOLLOWING_FETCH_SUCCESS: - case FOLLOWING_EXPAND_SUCCESS: - case REBLOGS_FETCH_SUCCESS: - case FAVOURITES_FETCH_SUCCESS: - case COMPOSE_SUGGESTIONS_READY: - case FOLLOW_REQUESTS_FETCH_SUCCESS: - case FOLLOW_REQUESTS_EXPAND_SUCCESS: - case BLOCKS_FETCH_SUCCESS: - case BLOCKS_EXPAND_SUCCESS: - case MUTES_FETCH_SUCCESS: - case MUTES_EXPAND_SUCCESS: - case LIST_ACCOUNTS_FETCH_SUCCESS: - case LIST_EDITOR_SUGGESTIONS_READY: - return action.accounts ? normalizeAccounts(state, action.accounts) : state; - case NOTIFICATIONS_REFRESH_SUCCESS: - case NOTIFICATIONS_EXPAND_SUCCESS: - case SEARCH_FETCH_SUCCESS: - return normalizeAccountsFromStatuses(normalizeAccounts(state, action.accounts), action.statuses); - case TIMELINE_REFRESH_SUCCESS: - case TIMELINE_EXPAND_SUCCESS: - case CONTEXT_FETCH_SUCCESS: - case FAVOURITED_STATUSES_FETCH_SUCCESS: - case FAVOURITED_STATUSES_EXPAND_SUCCESS: - return normalizeAccountsFromStatuses(state, action.statuses); - case REBLOG_SUCCESS: - case FAVOURITE_SUCCESS: - case UNREBLOG_SUCCESS: - case UNFAVOURITE_SUCCESS: - return normalizeAccountFromStatus(state, action.response); - case TIMELINE_UPDATE: - case STATUS_FETCH_SUCCESS: - return normalizeAccountFromStatus(state, action.status); + case ACCOUNTS_IMPORT: + return normalizeAccounts(state, action.accounts); case ACCOUNT_FOLLOW_SUCCESS: return action.alreadyFollowing ? state : state.updateIn([action.relationship.id, 'followers_count'], num => num + 1); diff --git a/app/javascript/mastodon/reducers/statuses.js b/app/javascript/mastodon/reducers/statuses.js @@ -1,87 +1,25 @@ import { REBLOG_REQUEST, - REBLOG_SUCCESS, REBLOG_FAIL, - UNREBLOG_SUCCESS, FAVOURITE_REQUEST, - FAVOURITE_SUCCESS, FAVOURITE_FAIL, - UNFAVOURITE_SUCCESS, - PIN_SUCCESS, - UNPIN_SUCCESS, } from '../actions/interactions'; import { - STATUS_FETCH_SUCCESS, - CONTEXT_FETCH_SUCCESS, STATUS_MUTE_SUCCESS, STATUS_UNMUTE_SUCCESS, STATUS_REVEAL, STATUS_HIDE, } from '../actions/statuses'; import { - TIMELINE_REFRESH_SUCCESS, - TIMELINE_UPDATE, TIMELINE_DELETE, - TIMELINE_EXPAND_SUCCESS, } from '../actions/timelines'; -import { - NOTIFICATIONS_UPDATE, - NOTIFICATIONS_REFRESH_SUCCESS, - NOTIFICATIONS_EXPAND_SUCCESS, -} from '../actions/notifications'; -import { - FAVOURITED_STATUSES_FETCH_SUCCESS, - FAVOURITED_STATUSES_EXPAND_SUCCESS, -} from '../actions/favourites'; -import { - PINNED_STATUSES_FETCH_SUCCESS, -} from '../actions/pin_statuses'; -import { SEARCH_FETCH_SUCCESS } from '../actions/search'; -import emojify from '../features/emoji/emoji'; +import { STATUS_IMPORT, STATUSES_IMPORT } from '../actions/importer'; import { Map as ImmutableMap, fromJS } from 'immutable'; -import escapeTextContentForBrowser from 'escape-html'; - -const domParser = new DOMParser(); - -const normalizeStatus = (state, status) => { - if (!status) { - return state; - } - - const normalStatus = { ...status }; - normalStatus.account = status.account.id; - - if (status.reblog && status.reblog.id) { - state = normalizeStatus(state, status.reblog); - normalStatus.reblog = status.reblog.id; - } - - // Only calculate these values when status first encountered - // Otherwise keep the ones already in the reducer - if (!state.has(status.id)) { - 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; - }, {}); +const importStatus = (state, status) => state.set(status.id, fromJS(status)); - 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 state.update(status.id, ImmutableMap(), map => map.mergeDeep(fromJS(normalStatus))); -}; - -const normalizeStatuses = (state, statuses) => { - statuses.forEach(status => { - state = normalizeStatus(state, status); - }); - - return state; -}; +const importStatuses = (state, statuses) => + state.withMutations(mutable => statuses.forEach(status => importStatus(mutable, status))); const deleteStatus = (state, id, references) => { references.forEach(ref => { @@ -95,17 +33,10 @@ const initialState = ImmutableMap(); export default function statuses(state = initialState, action) { switch(action.type) { - case TIMELINE_UPDATE: - case STATUS_FETCH_SUCCESS: - case NOTIFICATIONS_UPDATE: - return normalizeStatus(state, action.status); - case REBLOG_SUCCESS: - case UNREBLOG_SUCCESS: - case FAVOURITE_SUCCESS: - case UNFAVOURITE_SUCCESS: - case PIN_SUCCESS: - case UNPIN_SUCCESS: - return normalizeStatus(state, action.response); + case STATUS_IMPORT: + return importStatus(state, action.status); + case STATUSES_IMPORT: + return importStatuses(state, action.statuses); case FAVOURITE_REQUEST: return state.setIn([action.status.get('id'), 'favourited'], true); case FAVOURITE_FAIL: @@ -126,16 +57,6 @@ export default function statuses(state = initialState, action) { return state.withMutations(map => { action.ids.forEach(id => map.setIn([id, 'hidden'], true)); }); - case TIMELINE_REFRESH_SUCCESS: - case TIMELINE_EXPAND_SUCCESS: - case CONTEXT_FETCH_SUCCESS: - case NOTIFICATIONS_REFRESH_SUCCESS: - case NOTIFICATIONS_EXPAND_SUCCESS: - case FAVOURITED_STATUSES_FETCH_SUCCESS: - case FAVOURITED_STATUSES_EXPAND_SUCCESS: - case PINNED_STATUSES_FETCH_SUCCESS: - case SEARCH_FETCH_SUCCESS: - return normalizeStatuses(state, action.statuses); case TIMELINE_DELETE: return deleteStatus(state, action.id, action.references); default: