diff options
author | Akihiko Odaki <akihiko.odaki.4i@stu.hosei.ac.jp> | 2018-03-24 23:25:15 +0900 |
---|---|---|
committer | Eugen Rochko <eugen@zeonfederated.com> | 2018-03-24 15:25:15 +0100 |
commit | 9a1a55ce526c956ac6b35897d483c316b7ad4394 (patch) | |
tree | 340ef7a6f4de39a8deae2255287fd4b8ea3be563 /app | |
parent | 59657e24b9737cb2b38ea6b0f9e99192908b15df (diff) |
Allow clients to fetch statuses made while they were offline (#6876)
Diffstat (limited to 'app')
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/ |