summaryrefslogtreecommitdiffstats
path: root/app
diff options
context:
space:
mode:
authorAkihiko Odaki <akihiko.odaki.4i@stu.hosei.ac.jp>2018-03-24 23:25:15 +0900
committerEugen Rochko <eugen@zeonfederated.com>2018-03-24 15:25:15 +0100
commit9a1a55ce526c956ac6b35897d483c316b7ad4394 (patch)
tree340ef7a6f4de39a8deae2255287fd4b8ea3be563 /app
parent59657e24b9737cb2b38ea6b0f9e99192908b15df (diff)
Allow clients to fetch statuses made while they were offline (#6876)
Diffstat (limited to 'app')
-rw-r--r--app/javascript/mastodon/actions/compose.js20
-rw-r--r--app/javascript/mastodon/actions/streaming.js9
-rw-r--r--app/javascript/mastodon/actions/timelines.js111
-rw-r--r--app/javascript/mastodon/components/load_more.js5
-rw-r--r--app/javascript/mastodon/components/scrollable_list.js6
-rw-r--r--app/javascript/mastodon/components/status_list.js37
-rw-r--r--app/javascript/mastodon/features/account_gallery/index.js51
-rw-r--r--app/javascript/mastodon/features/account_timeline/index.js18
-rw-r--r--app/javascript/mastodon/features/community_timeline/index.js13
-rw-r--r--app/javascript/mastodon/features/hashtag_timeline/index.js15
-rw-r--r--app/javascript/mastodon/features/home_timeline/index.js12
-rw-r--r--app/javascript/mastodon/features/list_timeline/index.js10
-rw-r--r--app/javascript/mastodon/features/public_timeline/index.js13
-rw-r--r--app/javascript/mastodon/features/standalone/community_timeline/index.js13
-rw-r--r--app/javascript/mastodon/features/standalone/hashtag_timeline/index.js13
-rw-r--r--app/javascript/mastodon/features/standalone/public_timeline/index.js13
-rw-r--r--app/javascript/mastodon/features/ui/components/report_modal.js6
-rw-r--r--app/javascript/mastodon/features/ui/containers/status_list_container.js6
-rw-r--r--app/javascript/mastodon/features/ui/index.js4
-rw-r--r--app/javascript/mastodon/reducers/statuses.js4
-rw-r--r--app/javascript/mastodon/reducers/timelines.js65
-rw-r--r--app/javascript/mastodon/stream.js6
22 files changed, 191 insertions, 259 deletions
diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js
index 8e13209b839..5e7cdd2706a 100644
--- a/app/javascript/mastodon/actions/compose.js
+++ b/app/javascript/mastodon/actions/compose.js
@@ -5,13 +5,7 @@ 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,
- refreshHomeTimeline,
- refreshCommunityTimeline,
- refreshPublicTimeline,
-} from './timelines';
+import { updateTimeline } from './timelines';
let cancelFetchComposeSuggestionsAccounts;
@@ -125,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));
diff --git a/app/javascript/mastodon/actions/streaming.js b/app/javascript/mastodon/actions/streaming.js
index c22152edde3..3ac6b8a09a8 100644
--- a/app/javascript/mastodon/actions/streaming.js
+++ b/app/javascript/mastodon/actions/streaming.js
@@ -2,8 +2,7 @@ import { connectStream } from '../stream';
import {
updateTimeline,
deleteFromTimelines,
- refreshHomeTimeline,
- connectTimeline,
+ expandHomeTimeline,
disconnectTimeline,
} from './timelines';
import { updateNotifications, refreshNotifications } from './notifications';
@@ -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,7 +37,7 @@ export function connectTimelineStream (timelineId, path, pollingRefresh = null)
}
function refreshHomeTimelineAndNotification (dispatch) {
- dispatch(refreshHomeTimeline());
+ dispatch(expandHomeTimeline());
dispatch(refreshNotifications());
}
diff --git a/app/javascript/mastodon/actions/timelines.js b/app/javascript/mastodon/actions/timelines.js
index e5748b4e76d..5be07126d51 100644
--- a/app/javascript/mastodon/actions/timelines.js
+++ b/app/javascript/mastodon/actions/timelines.js
@@ -1,36 +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) : [];
@@ -80,97 +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(importFetchedStatuses(response.data));
- 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(importFetchedStatuses(response.data));
- dispatch(expandTimelineSuccess(timelineId, response.data, next ? next.uri : null));
+ 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 {
@@ -179,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,
};
};
@@ -204,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 (
- <button className='load-more' disabled={!visible} style={{ visibility: visible ? 'visible' : 'hidden' }} onClick={this.props.onClick}>
+ <button className='load-more' disabled={disabled || !visible} style={{ visibility: visible ? 'visible' : 'hidden' }} onClick={this.props.onClick}>
<FormattedMessage id='status.load_more' defaultMessage='Load more' />
</button>
);
diff --git a/app/javascript/mastodon/components/scrollable_list.js b/app/javascript/mastodon/components/scrollable_list.js
index ac3e404df4f..ee07106f7f2 100644
--- a/app/javascript/mastodon/components/scrollable_list.js
+++ b/app/javascript/mastodon/components/scrollable_list.js
@@ -17,7 +17,7 @@ export default class ScrollableList extends PureComponent {
static propTypes = {
scrollKey: PropTypes.string.isRequired,
- onLoadMore: PropTypes.func.isRequired,
+ onLoadMore: PropTypes.func,
onScrollToTop: PropTypes.func,
onScroll: PropTypes.func,
trackScroll: PropTypes.bool,
@@ -148,11 +148,11 @@ export default class ScrollableList extends PureComponent {
}
render () {
- const { children, scrollKey, trackScroll, shouldUpdateScroll, isLoading, hasMore, prepend, emptyMessage } = this.props;
+ const { children, scrollKey, trackScroll, shouldUpdateScroll, isLoading, hasMore, prepend, emptyMessage, onLoadMore } = this.props;
const { fullscreen } = this.state;
const childrenCount = React.Children.count(children);
- const loadMore = (hasMore && childrenCount > 0) ? <LoadMore visible={!isLoading} onClick={this.handleLoadMore} /> : null;
+ const loadMore = (hasMore && childrenCount > 0 && onLoadMore) ? <LoadMore visible={!isLoading} onClick={this.handleLoadMore} /> : null;
let scrollableArea = null;
if (isLoading || childrenCount > 0 || !emptyMessage) {
diff --git a/app/javascript/mastodon/components/status_list.js b/app/javascript/mastodon/components/status_list.js
index 3bebf702cf7..8c2673f3016 100644
--- a/app/javascript/mastodon/components/status_list.js
+++ b/app/javascript/mastodon/components/status_list.js
@@ -1,11 +1,31 @@
+import { debounce } from 'lodash';
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import StatusContainer from '../containers/status_container';
import ImmutablePureComponent from 'react-immutable-pure-component';
+import LoadMore from './load_more';
import ScrollableList from './scrollable_list';
import { FormattedMessage } from 'react-intl';
+class LoadGap extends ImmutablePureComponent {
+
+ static propTypes = {
+ disabled: PropTypes.bool,
+ maxId: PropTypes.string,
+ onClick: PropTypes.func.isRequired,
+ };
+
+ handleClick = () => {
+ this.props.onClick(this.props.maxId);
+ }
+
+ render () {
+ return <LoadMore onClick={this.handleClick} disabled={this.props.disabled} />;
+ }
+
+}
+
export default class StatusList extends ImmutablePureComponent {
static propTypes = {
@@ -38,6 +58,10 @@ export default class StatusList extends ImmutablePureComponent {
this._selectChild(elementIndex);
}
+ handleLoadOlder = debounce(() => {
+ this.props.onLoadMore(this.props.statusIds.last());
+ }, 300, { leading: true })
+
_selectChild (index) {
const element = this.node.node.querySelector(`article:nth-of-type(${index + 1}) .focusable`);
@@ -51,7 +75,7 @@ export default class StatusList extends ImmutablePureComponent {
}
render () {
- const { statusIds, featuredStatusIds, ...other } = this.props;
+ const { statusIds, featuredStatusIds, onLoadMore, ...other } = this.props;
const { isLoading, isPartial } = other;
if (isPartial) {
@@ -70,7 +94,14 @@ export default class StatusList extends ImmutablePureComponent {
}
let scrollableContent = (isLoading || statusIds.size > 0) ? (
- statusIds.map(statusId => (
+ statusIds.map((statusId, index) => statusId === null ? (
+ <LoadGap
+ key={'gap:' + statusIds.get(index + 1)}
+ disabled={isLoading}
+ maxId={index > 0 ? statusIds.get(index - 1) : null}
+ onClick={onLoadMore}
+ />
+ ) : (
<StatusContainer
key={statusId}
id={statusId}
@@ -93,7 +124,7 @@ export default class StatusList extends ImmutablePureComponent {
}
return (
- <ScrollableList {...other} ref={this.setRef}>
+ <ScrollableList {...other} onLoadMore={onLoadMore && this.handleLoadOlder} ref={this.setRef}>
{scrollableContent}
</ScrollableList>
);
diff --git a/app/javascript/mastodon/features/account_gallery/index.js b/app/javascript/mastodon/features/account_gallery/index.js
index 4b408256a18..9a40d139cb8 100644
--- a/app/javascript/mastodon/features/account_gallery/index.js
+++ b/app/javascript/mastodon/features/account_gallery/index.js
@@ -3,7 +3,7 @@ import { connect } from 'react-redux';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import { fetchAccount } from '../../actions/accounts';
-import { refreshAccountMediaTimeline, expandAccountMediaTimeline } from '../../actions/timelines';
+import { expandAccountMediaTimeline } from '../../actions/timelines';
import LoadingIndicator from '../../components/loading_indicator';
import Column from '../ui/components/column';
import ColumnBackButton from '../../components/column_back_button';
@@ -17,9 +17,31 @@ import LoadMore from '../../components/load_more';
const mapStateToProps = (state, props) => ({
medias: getAccountGallery(state, props.params.accountId),
isLoading: state.getIn(['timelines', `account:${props.params.accountId}:media`, 'isLoading']),
- hasMore: !!state.getIn(['timelines', `account:${props.params.accountId}:media`, 'next']),
+ hasMore: state.getIn(['timelines', `account:${props.params.accountId}:media`, 'hasMore']),
});
+class LoadMoreMedia extends ImmutablePureComponent {
+
+ static propTypes = {
+ maxId: PropTypes.string,
+ onLoadMore: PropTypes.func.isRequired,
+ };
+
+ handleLoadMore = () => {
+ this.props.onLoadMore(this.props.maxId);
+ }
+
+ render () {
+ return (
+ <LoadMore
+ disabled={this.props.disabled}
+ onLoadMore={this.handleLoadMore}
+ />
+ );
+ }
+
+}
+
@connect(mapStateToProps)
export default class AccountGallery extends ImmutablePureComponent {
@@ -33,19 +55,19 @@ export default class AccountGallery extends ImmutablePureComponent {
componentDidMount () {
this.props.dispatch(fetchAccount(this.props.params.accountId));
- this.props.dispatch(refreshAccountMediaTimeline(this.props.params.accountId));
+ this.props.dispatch(expandAccountMediaTimeline(this.props.params.accountId));
}
componentWillReceiveProps (nextProps) {
if (nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) {
this.props.dispatch(fetchAccount(nextProps.params.accountId));
- this.props.dispatch(refreshAccountMediaTimeline(this.props.params.accountId));
+ this.props.dispatch(expandAccountMediaTimeline(this.props.params.accountId));
}
}
handleScrollToBottom = () => {
if (this.props.hasMore) {
- this.props.dispatch(expandAccountMediaTimeline(this.props.params.accountId));
+ this.handleLoadMore(this.props.medias.last().get('id'));
}
}
@@ -58,7 +80,11 @@ export default class AccountGallery extends ImmutablePureComponent {
}
}
- handleLoadMore = (e) => {
+ handleLoadMore = maxId => {
+ this.props.dispatch(expandAccountMediaTimeline(this.props.params.accountId, { maxId }));
+ };
+
+ handleLoadOlder = (e) => {
e.preventDefault();
this.handleScrollToBottom();
}
@@ -66,7 +92,7 @@ export default class AccountGallery extends ImmutablePureComponent {
render () {
const { medias, isLoading, hasMore } = this.props;
- let loadMore = null;
+ let loadOlder = null;
if (!medias && isLoading) {
return (
@@ -77,7 +103,7 @@ export default class AccountGallery extends ImmutablePureComponent {
}
if (!isLoading && medias.size > 0 && hasMore) {
- loadMore = <LoadMore onClick={this.handleLoadMore} />;
+ loadOlder = <LoadMore onClick={this.handleLoadOlder} />;
}
return (
@@ -89,13 +115,18 @@ export default class AccountGallery extends ImmutablePureComponent {
<HeaderContainer accountId={this.props.params.accountId} />
<div className='account-gallery__container'>
- {medias.map(media => (
+ {medias.map((media, index) => media === null ? (
+ <LoadMoreMedia
+ key={'more:' + medias.getIn(index + 1, 'id')}
+ maxId={index > 0 ? medias.getIn(index - 1, 'id') : null}
+ />
+ ) : (
<MediaItem
key={media.get('id')}
media={media}
/>
))}
- {loadMore}
+ {loadOlder}
</div>
</div>
</ScrollContainer>
diff --git a/app/javascript/mastodon/features/account_timeline/index.js b/app/javascript/mastodon/features/account_timeline/index.js
index 5e21cf7c6fa..d329bac5c19 100644
--- a/app/javascript/mastodon/features/account_timeline/index.js
+++ b/