diff --git a/Gemfile b/Gemfile
index 47d94199486..5033e290d08 100644
--- a/Gemfile
+++ b/Gemfile
@@ -13,11 +13,11 @@ gem 'pg', '~> 0.20'
gem 'pghero', '~> 1.7'
gem 'dotenv-rails', '~> 2.2'
-gem 'aws-sdk', '~> 2.10', require: false
+gem 'aws-sdk-s3', '~> 1.8', require: false
gem 'fog-core', '~> 1.45'
gem 'fog-local', '~> 0.4', require: false
gem 'fog-openstack', '~> 0.1', require: false
-gem 'paperclip', '~> 5.1'
+gem 'paperclip', '~> 6.0'
gem 'paperclip-av-transcoder', '~> 0.6'
gem 'posix-spawn'
gem 'streamio-ffmpeg', '~> 3.0'
diff --git a/Gemfile.lock b/Gemfile.lock
index f30cd518ec2..72f70629559 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -57,13 +57,18 @@ GEM
encryptor (~> 3.0.0)
av (0.9.0)
cocaine (~> 0.5.3)
- aws-sdk (2.10.100)
- aws-sdk-resources (= 2.10.100)
- aws-sdk-core (2.10.100)
+ aws-partitions (1.70.0)
+ aws-sdk-core (3.17.0)
+ aws-partitions (~> 1.0)
aws-sigv4 (~> 1.0)
jmespath (~> 1.0)
- aws-sdk-resources (2.10.100)
- aws-sdk-core (= 2.10.100)
+ aws-sdk-kms (1.5.0)
+ aws-sdk-core (~> 3)
+ aws-sigv4 (~> 1.0)
+ aws-sdk-s3 (1.8.2)
+ aws-sdk-core (~> 3)
+ aws-sdk-kms (~> 1)
+ aws-sigv4 (~> 1.0)
aws-sigv4 (1.0.2)
bcrypt (3.1.11)
better_errors (2.4.0)
@@ -238,7 +243,7 @@ GEM
httplog (0.99.7)
colorize
rack
- i18n (0.9.3)
+ i18n (0.9.5)
concurrent-ruby (~> 1.0)
i18n-tasks (0.9.19)
activesupport (>= 4.0.2)
@@ -344,12 +349,12 @@ GEM
http (~> 3.0)
nokogiri (~> 1.8)
ox (2.8.2)
- paperclip (5.2.1)
+ paperclip (6.0.0)
activemodel (>= 4.2.0)
activesupport (>= 4.2.0)
- cocaine (~> 0.5.5)
mime-types
mimemagic (~> 0.3.0)
+ terrapin (~> 0.6.0)
paperclip-av-transcoder (0.6.4)
av (~> 0.9.0)
paperclip (>= 2.5.2)
@@ -555,6 +560,8 @@ GEM
temple (0.8.0)
terminal-table (1.8.0)
unicode-display_width (~> 1.1, >= 1.1.1)
+ terrapin (0.6.0)
+ climate_control (>= 0.0.3, < 1.0)
thor (0.20.0)
thread (0.2.2)
thread_safe (0.3.6)
@@ -578,7 +585,7 @@ GEM
tty-screen (0.6.4)
twitter-text (1.14.7)
unf (~> 0.1.0)
- tzinfo (1.2.4)
+ tzinfo (1.2.5)
thread_safe (~> 0.1)
tzinfo-data (1.2017.3)
tzinfo (>= 1.0.0)
@@ -615,7 +622,7 @@ DEPENDENCIES
active_record_query_trace (~> 1.5)
addressable (~> 2.5)
annotate (~> 2.7)
- aws-sdk (~> 2.10)
+ aws-sdk-s3 (~> 1.8)
better_errors (~> 2.4)
binding_of_caller (~> 0.7)
bootsnap
@@ -675,7 +682,7 @@ DEPENDENCIES
omniauth-saml (~> 1.10)
ostatus2 (~> 2.0)
ox (~> 2.8)
- paperclip (~> 5.1)
+ paperclip (~> 6.0)
paperclip-av-transcoder (~> 0.6)
parallel_tests (~> 2.17)
pg (~> 0.20)
diff --git a/app/helpers/jsonld_helper.rb b/app/helpers/jsonld_helper.rb
index 9530ad9f30f..957a2cbc982 100644
--- a/app/helpers/jsonld_helper.rb
+++ b/app/helpers/jsonld_helper.rb
@@ -60,9 +60,9 @@ module JsonLdHelper
end
def fetch_resource_without_id_validation(uri)
- response = build_request(uri).perform
- return if response.code != 200
- body_to_json(response.to_s)
+ build_request(uri).perform do |response|
+ response.code == 200 ? body_to_json(response.to_s) : nil
+ end
end
def body_to_json(body)
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..5e7cdd2706a 100644
--- a/app/javascript/mastodon/actions/compose.js
+++ b/app/javascript/mastodon/actions/compose.js
@@ -4,13 +4,8 @@ import { throttle } from 'lodash';
import { search as emojiSearch } from '../features/emoji/emoji_mart_search_light';
import { tagHistory } from '../settings';
import { useEmoji } from './emojis';
-
-import {
- updateTimeline,
- refreshHomeTimeline,
- refreshCommunityTimeline,
- refreshPublicTimeline,
-} from './timelines';
+import { importFetchedAccounts } from './importer';
+import { updateTimeline } from './timelines';
let cancelFetchComposeSuggestionsAccounts;
@@ -124,19 +119,17 @@ export function submitCompose() {
// To make the app more responsive, immediately get the status into the columns
- const insertOrRefresh = (timelineId, refreshAction) => {
- if (getState().getIn(['timelines', timelineId, 'online'])) {
+ const insertIfOnline = (timelineId) => {
+ if (getState().getIn(['timelines', timelineId, 'items', 0]) !== null) {
dispatch(updateTimeline(timelineId, { ...response.data }));
- } else if (getState().getIn(['timelines', timelineId, 'loaded'])) {
- dispatch(refreshAction());
}
};
- insertOrRefresh('home', refreshHomeTimeline);
+ insertIfOnline('home');
if (response.data.in_reply_to_id === null && response.data.visibility === 'public') {
- insertOrRefresh('community', refreshCommunityTimeline);
- insertOrRefresh('public', refreshPublicTimeline);
+ insertIfOnline('community');
+ insertIfOnline('public');
}
}).catch(function (error) {
dispatch(submitComposeFail(error));
@@ -282,6 +275,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(/
/g, '\n').replace(/<\/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..7267b85bd02 100644
--- a/app/javascript/mastodon/actions/notifications.js
+++ b/app/javascript/mastodon/actions/notifications.js
@@ -1,15 +1,16 @@
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';
-export const NOTIFICATIONS_REFRESH_REQUEST = 'NOTIFICATIONS_REFRESH_REQUEST';
-export const NOTIFICATIONS_REFRESH_SUCCESS = 'NOTIFICATIONS_REFRESH_SUCCESS';
-export const NOTIFICATIONS_REFRESH_FAIL = 'NOTIFICATIONS_REFRESH_FAIL';
-
export const NOTIFICATIONS_EXPAND_REQUEST = 'NOTIFICATIONS_EXPAND_REQUEST';
export const NOTIFICATIONS_EXPAND_SUCCESS = 'NOTIFICATIONS_EXPAND_SUCCESS';
export const NOTIFICATIONS_EXPAND_FAIL = 'NOTIFICATIONS_EXPAND_FAIL';
@@ -41,11 +42,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,
});
@@ -67,73 +69,14 @@ export function updateNotifications(notification, intlMessages, intlLocale) {
const excludeTypesFromSettings = state => state.getIn(['settings', 'notifications', 'shows']).filter(enabled => !enabled).keySeq().toJS();
-export function refreshNotifications() {
+export function expandNotifications({ maxId } = {}) {
return (dispatch, getState) => {
- const params = {};
- const ids = getState().getIn(['notifications', 'items']);
-
- let skipLoading = false;
-
- if (ids.size > 0) {
- params.since_id = ids.first().get('id');
- }
-
- if (getState().getIn(['notifications', 'loaded'])) {
- skipLoading = true;
- }
-
- params.exclude_types = excludeTypesFromSettings(getState());
-
- dispatch(refreshNotificationsRequest(skipLoading));
-
- api(getState).get('/api/v1/notifications', { params }).then(response => {
- const next = getLinks(response).refs.find(link => link.rel === 'next');
-
- dispatch(refreshNotificationsSuccess(response.data, skipLoading, next ? next.uri : null));
- fetchRelatedRelationships(dispatch, response.data);
- }).catch(error => {
- dispatch(refreshNotificationsFail(error, skipLoading));
- });
- };
-};
-
-export function refreshNotificationsRequest(skipLoading) {
- return {
- type: NOTIFICATIONS_REFRESH_REQUEST,
- skipLoading,
- };
-};
-
-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,
- };
-};
-
-export function refreshNotificationsFail(error, skipLoading) {
- return {
- type: NOTIFICATIONS_REFRESH_FAIL,
- error,
- skipLoading,
- };
-};
-
-export function expandNotifications() {
- return (dispatch, getState) => {
- const items = getState().getIn(['notifications', 'items'], ImmutableList());
-
- if (getState().getIn(['notifications', 'isLoading']) || items.size === 0) {
+ if (getState().getIn(['notifications', 'isLoading'])) {
return;
}
const params = {
- max_id: items.last().get('id'),
- limit: 20,
+ max_id: maxId,
exclude_types: excludeTypesFromSettings(getState()),
};
@@ -141,6 +84,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 +106,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
index 3f40f6c2d55..77abba7b5ab 100644
--- 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
index 73cb106ec4f..882c1709e97 100644
--- 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
index 073f09883fa..dcd813dd94a 100644
--- 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
index 2dd94a99832..34dcafc5106 100644
--- 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/streaming.js b/app/javascript/mastodon/actions/streaming.js
index c22152edde3..f76510cdb16 100644
--- a/app/javascript/mastodon/actions/streaming.js
+++ b/app/javascript/mastodon/actions/streaming.js
@@ -2,11 +2,10 @@ import { connectStream } from '../stream';
import {
updateTimeline,
deleteFromTimelines,
- refreshHomeTimeline,
- connectTimeline,
+ expandHomeTimeline,
disconnectTimeline,
} from './timelines';
-import { updateNotifications, refreshNotifications } from './notifications';
+import { updateNotifications, expandNotifications } from './notifications';
import { getLocale } from '../locales';
const { messages } = getLocale();
@@ -16,10 +15,6 @@ export function connectTimelineStream (timelineId, path, pollingRefresh = null)
return connectStream (path, pollingRefresh, (dispatch, getState) => {
const locale = getState().getIn(['meta', 'locale']);
return {
- onConnect() {
- dispatch(connectTimeline(timelineId));
- },
-
onDisconnect() {
dispatch(disconnectTimeline(timelineId));
},
@@ -42,8 +37,8 @@ export function connectTimelineStream (timelineId, path, pollingRefresh = null)
}
function refreshHomeTimelineAndNotification (dispatch) {
- dispatch(refreshHomeTimeline());
- dispatch(refreshNotifications());
+ dispatch(expandHomeTimeline());
+ dispatch(expandNotifications());
}
export const connectUserStream = () => connectTimelineStream('home', 'user', refreshHomeTimelineAndNotification);
diff --git a/app/javascript/mastodon/actions/timelines.js b/app/javascript/mastodon/actions/timelines.js
index f0ab16a2d76..5be07126d51 100644
--- a/app/javascript/mastodon/actions/timelines.js
+++ b/app/javascript/mastodon/actions/timelines.js
@@ -1,35 +1,20 @@
+import { importFetchedStatus, importFetchedStatuses } from './importer';
import api, { getLinks } from '../api';
-import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
+import { Map as ImmutableMap } from 'immutable';
export const TIMELINE_UPDATE = 'TIMELINE_UPDATE';
export const TIMELINE_DELETE = 'TIMELINE_DELETE';
-export const TIMELINE_REFRESH_REQUEST = 'TIMELINE_REFRESH_REQUEST';
-export const TIMELINE_REFRESH_SUCCESS = 'TIMELINE_REFRESH_SUCCESS';
-export const TIMELINE_REFRESH_FAIL = 'TIMELINE_REFRESH_FAIL';
-
export const TIMELINE_EXPAND_REQUEST = 'TIMELINE_EXPAND_REQUEST';
export const TIMELINE_EXPAND_SUCCESS = 'TIMELINE_EXPAND_SUCCESS';
export const TIMELINE_EXPAND_FAIL = 'TIMELINE_EXPAND_FAIL';
export const TIMELINE_SCROLL_TOP = 'TIMELINE_SCROLL_TOP';
-export const TIMELINE_CONNECT = 'TIMELINE_CONNECT';
export const TIMELINE_DISCONNECT = 'TIMELINE_DISCONNECT';
export const TIMELINE_CONTEXT_UPDATE = 'CONTEXT_UPDATE';
-export function refreshTimelineSuccess(timeline, statuses, skipLoading, next, partial) {
- return {
- type: TIMELINE_REFRESH_SUCCESS,
- timeline,
- statuses,
- skipLoading,
- next,
- partial,
- };
-};
-
export function updateTimeline(timeline, status) {
return (dispatch, getState) => {
const references = status.reblog ? getState().get('statuses').filter((item, itemId) => (itemId === status.reblog.id || item.get('reblog') === status.reblog.id)).map((_, itemId) => itemId) : [];
@@ -44,6 +29,8 @@ export function updateTimeline(timeline, status) {
}
}
+ dispatch(importFetchedStatus(status));
+
dispatch({
type: TIMELINE_UPDATE,
timeline,
@@ -77,95 +64,34 @@ export function deleteFromTimelines(id) {
};
};
-export function refreshTimelineRequest(timeline, skipLoading) {
- return {
- type: TIMELINE_REFRESH_REQUEST,
- timeline,
- skipLoading,
- };
-};
-
-export function refreshTimeline(timelineId, path, params = {}) {
- return function (dispatch, getState) {
- const timeline = getState().getIn(['timelines', timelineId], ImmutableMap());
-
- if (timeline.get('isLoading') || (timeline.get('online') && !timeline.get('isPartial'))) {
- return;
- }
-
- const ids = timeline.get('items', ImmutableList());
- const newestId = ids.size > 0 ? ids.first() : null;
-
- let skipLoading = timeline.get('loaded');
-
- if (newestId !== null) {
- params.since_id = newestId;
- }
-
- dispatch(refreshTimelineRequest(timelineId, skipLoading));
-
- api(getState).get(path, { params }).then(response => {
- if (response.status === 206) {
- dispatch(refreshTimelineSuccess(timelineId, [], skipLoading, null, true));
- } else {
- const next = getLinks(response).refs.find(link => link.rel === 'next');
- dispatch(refreshTimelineSuccess(timelineId, response.data, skipLoading, next ? next.uri : null, false));
- }
- }).catch(error => {
- dispatch(refreshTimelineFail(timelineId, error, skipLoading));
- });
- };
-};
-
-export const refreshHomeTimeline = () => refreshTimeline('home', '/api/v1/timelines/home');
-export const refreshPublicTimeline = () => refreshTimeline('public', '/api/v1/timelines/public');
-export const refreshCommunityTimeline = () => refreshTimeline('community', '/api/v1/timelines/public', { local: true });
-export const refreshAccountTimeline = (accountId, withReplies) => refreshTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies });
-export const refreshAccountFeaturedTimeline = accountId => refreshTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true });
-export const refreshAccountMediaTimeline = accountId => refreshTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { only_media: true });
-export const refreshHashtagTimeline = hashtag => refreshTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`);
-export const refreshListTimeline = id => refreshTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`);
-
-export function refreshTimelineFail(timeline, error, skipLoading) {
- return {
- type: TIMELINE_REFRESH_FAIL,
- timeline,
- error,
- skipLoading,
- skipAlert: error.response && error.response.status === 404,
- };
-};
-
export function expandTimeline(timelineId, path, params = {}) {
return (dispatch, getState) => {
const timeline = getState().getIn(['timelines', timelineId], ImmutableMap());
- const ids = timeline.get('items', ImmutableList());
- if (timeline.get('isLoading') || ids.size === 0) {
+ if (timeline.get('isLoading')) {
return;
}
- params.max_id = ids.last();
- params.limit = 10;
-
dispatch(expandTimelineRequest(timelineId));
api(getState).get(path, { params }).then(response => {
const next = getLinks(response).refs.find(link => link.rel === 'next');
- dispatch(expandTimelineSuccess(timelineId, response.data, next ? next.uri : null));
+ dispatch(importFetchedStatuses(response.data));
+ dispatch(expandTimelineSuccess(timelineId, response.data, next ? next.uri : null, response.code === 206));
}).catch(error => {
dispatch(expandTimelineFail(timelineId, error));
});
};
};
-export const expandHomeTimeline = () => expandTimeline('home', '/api/v1/timelines/home');
-export const expandPublicTimeline = () => expandTimeline('public', '/api/v1/timelines/public');
-export const expandCommunityTimeline = () => expandTimeline('community', '/api/v1/timelines/public', { local: true });
-export const expandAccountTimeline = (accountId, withReplies) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies });
-export const expandAccountMediaTimeline = accountId => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { only_media: true });
-export const expandHashtagTimeline = hashtag => expandTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`);
-export const expandListTimeline = id => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`);
+export const expandHomeTimeline = ({ maxId } = {}) => expandTimeline('home', '/api/v1/timelines/home', { max_id: maxId });
+export const expandPublicTimeline = ({ maxId } = {}) => expandTimeline('public', '/api/v1/timelines/public', { max_id: maxId });
+export const expandCommunityTimeline = ({ maxId } = {}) => expandTimeline('community', '/api/v1/timelines/public', { local: true, max_id: maxId });
+export const expandAccountTimeline = (accountId, { maxId, withReplies } = {}) => expandTimeline(`account:${accountId}${withReplies ? ':with_replies' : ''}`, `/api/v1/accounts/${accountId}/statuses`, { exclude_replies: !withReplies, max_id: maxId });
+export const expandAccountFeaturedTimeline = accountId => expandTimeline(`account:${accountId}:pinned`, `/api/v1/accounts/${accountId}/statuses`, { pinned: true });
+export const expandAccountMediaTimeline = (accountId, { maxId } = {}) => expandTimeline(`account:${accountId}:media`, `/api/v1/accounts/${accountId}/statuses`, { max_id: maxId, only_media: true });
+export const expandHashtagTimeline = (hashtag, { maxId } = {}) => expandTimeline(`hashtag:${hashtag}`, `/api/v1/timelines/tag/${hashtag}`, { max_id: maxId });
+export const expandListTimeline = (id, { maxId } = {}) => expandTimeline(`list:${id}`, `/api/v1/timelines/list/${id}`, { max_id: maxId });
export function expandTimelineRequest(timeline) {
return {
@@ -174,12 +100,13 @@ export function expandTimelineRequest(timeline) {
};
};
-export function expandTimelineSuccess(timeline, statuses, next) {
+export function expandTimelineSuccess(timeline, statuses, next, partial) {
return {
type: TIMELINE_EXPAND_SUCCESS,
timeline,
statuses,
next,
+ partial,
};
};
@@ -199,13 +126,6 @@ export function scrollTopTimeline(timeline, top) {
};
};
-export function connectTimeline(timeline) {
- return {
- type: TIMELINE_CONNECT,
- timeline,
- };
-};
-
export function disconnectTimeline(timeline) {
return {
type: TIMELINE_DISCONNECT,
diff --git a/app/javascript/mastodon/components/load_more.js b/app/javascript/mastodon/components/load_more.js
index c4c8c94a2a4..389c3e1e115 100644
--- a/app/javascript/mastodon/components/load_more.js
+++ b/app/javascript/mastodon/components/load_more.js
@@ -6,6 +6,7 @@ export default class LoadMore extends React.PureComponent {
static propTypes = {
onClick: PropTypes.func,
+ disabled: PropTypes.bool,
visible: PropTypes.bool,
}
@@ -14,10 +15,10 @@ export default class LoadMore extends React.PureComponent {
}
render() {
- const { visible } = this.props;
+ const { disabled, visible } = this.props;
return (
-