diff options
21 files changed, 217 insertions, 594 deletions
diff --git a/app/javascript/mastodon/actions/accounts.js b/app/javascript/mastodon/actions/accounts.js index 405032460..a862798f9 100644 --- a/app/javascript/mastodon/actions/accounts.js +++ b/app/javascript/mastodon/actions/accounts.js @@ -29,22 +29,6 @@ export const ACCOUNT_UNMUTE_REQUEST = 'ACCOUNT_UNMUTE_REQUEST'; export const ACCOUNT_UNMUTE_SUCCESS = 'ACCOUNT_UNMUTE_SUCCESS'; export const ACCOUNT_UNMUTE_FAIL = 'ACCOUNT_UNMUTE_FAIL'; -export const ACCOUNT_TIMELINE_FETCH_REQUEST = 'ACCOUNT_TIMELINE_FETCH_REQUEST'; -export const ACCOUNT_TIMELINE_FETCH_SUCCESS = 'ACCOUNT_TIMELINE_FETCH_SUCCESS'; -export const ACCOUNT_TIMELINE_FETCH_FAIL = 'ACCOUNT_TIMELINE_FETCH_FAIL'; - -export const ACCOUNT_TIMELINE_EXPAND_REQUEST = 'ACCOUNT_TIMELINE_EXPAND_REQUEST'; -export const ACCOUNT_TIMELINE_EXPAND_SUCCESS = 'ACCOUNT_TIMELINE_EXPAND_SUCCESS'; -export const ACCOUNT_TIMELINE_EXPAND_FAIL = 'ACCOUNT_TIMELINE_EXPAND_FAIL'; - -export const ACCOUNT_MEDIA_TIMELINE_FETCH_REQUEST = 'ACCOUNT_MEDIA_TIMELINE_FETCH_REQUEST'; -export const ACCOUNT_MEDIA_TIMELINE_FETCH_SUCCESS = 'ACCOUNT_MEDIA_TIMELINE_FETCH_SUCCESS'; -export const ACCOUNT_MEDIA_TIMELINE_FETCH_FAIL = 'ACCOUNT_MEDIA_TIMELINE_FETCH_FAIL'; - -export const ACCOUNT_MEDIA_TIMELINE_EXPAND_REQUEST = 'ACCOUNT_MEDIA_TIMELINE_EXPAND_REQUEST'; -export const ACCOUNT_MEDIA_TIMELINE_EXPAND_SUCCESS = 'ACCOUNT_MEDIA_TIMELINE_EXPAND_SUCCESS'; -export const ACCOUNT_MEDIA_TIMELINE_EXPAND_FAIL = 'ACCOUNT_MEDIA_TIMELINE_EXPAND_FAIL'; - export const FOLLOWERS_FETCH_REQUEST = 'FOLLOWERS_FETCH_REQUEST'; export const FOLLOWERS_FETCH_SUCCESS = 'FOLLOWERS_FETCH_SUCCESS'; export const FOLLOWERS_FETCH_FAIL = 'FOLLOWERS_FETCH_FAIL'; @@ -99,99 +83,6 @@ export function fetchAccount(id) { }; }; -export function fetchAccountTimeline(id, replace = false) { - return (dispatch, getState) => { - const ids = getState().getIn(['timelines', 'accounts_timelines', id, 'items'], Immutable.List()); - const newestId = ids.size > 0 ? ids.first() : null; - - let params = {}; - let skipLoading = false; - - replace = replace || newestId === null; - - if (!replace) { - params.since_id = newestId; - skipLoading = true; - } - - dispatch(fetchAccountTimelineRequest(id, skipLoading)); - - api(getState).get(`/api/v1/accounts/${id}/statuses`, { params }).then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - dispatch(fetchAccountTimelineSuccess(id, response.data, replace, skipLoading, next)); - }).catch(error => { - dispatch(fetchAccountTimelineFail(id, error, skipLoading)); - }); - }; -}; - -export function fetchAccountMediaTimeline(id, replace = false) { - return (dispatch, getState) => { - const ids = getState().getIn(['timelines', 'accounts_media_timelines', id, 'items'], Immutable.List()); - const newestId = ids.size > 0 ? ids.first() : null; - - let params = { only_media: 'true', limit: 12 }; - let skipLoading = false; - - replace = replace || newestId === null; - - if (!replace) { - params.since_id = newestId; - skipLoading = true; - } - - dispatch(fetchAccountMediaTimelineRequest(id, skipLoading)); - - api(getState).get(`/api/v1/accounts/${id}/statuses`, { params }).then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - dispatch(fetchAccountMediaTimelineSuccess(id, response.data, replace, skipLoading, next)); - }).catch(error => { - dispatch(fetchAccountMediaTimelineFail(id, error, skipLoading)); - }); - }; -}; - -export function expandAccountTimeline(id) { - return (dispatch, getState) => { - const lastId = getState().getIn(['timelines', 'accounts_timelines', id, 'items'], Immutable.List()).last(); - - dispatch(expandAccountTimelineRequest(id)); - - api(getState).get(`/api/v1/accounts/${id}/statuses`, { - params: { - limit: 10, - max_id: lastId, - }, - }).then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - dispatch(expandAccountTimelineSuccess(id, response.data, next)); - }).catch(error => { - dispatch(expandAccountTimelineFail(id, error)); - }); - }; -}; - -export function expandAccountMediaTimeline(id) { - return (dispatch, getState) => { - const lastId = getState().getIn(['timelines', 'accounts_media_timelines', id, 'items'], Immutable.List()).last(); - - dispatch(expandAccountMediaTimelineRequest(id)); - - api(getState).get(`/api/v1/accounts/${id}/statuses`, { - params: { - limit: 12, - only_media: 'true', - max_id: lastId, - }, - }).then(response => { - const next = getLinks(response).refs.find(link => link.rel === 'next'); - dispatch(expandAccountMediaTimelineSuccess(id, response.data, next)); - }).catch(error => { - dispatch(expandAccountMediaTimelineFail(id, error)); - }); - }; -}; - export function fetchAccountRequest(id) { return { type: ACCOUNT_FETCH_REQUEST, @@ -281,112 +172,6 @@ export function unfollowAccountFail(error) { }; }; -export function fetchAccountTimelineRequest(id, skipLoading) { - return { - type: ACCOUNT_TIMELINE_FETCH_REQUEST, - id, - skipLoading, - }; -}; - -export function fetchAccountTimelineSuccess(id, statuses, replace, skipLoading, next) { - return { - type: ACCOUNT_TIMELINE_FETCH_SUCCESS, - id, - statuses, - replace, - skipLoading, - next, - }; -}; - -export function fetchAccountTimelineFail(id, error, skipLoading) { - return { - type: ACCOUNT_TIMELINE_FETCH_FAIL, - id, - error, - skipLoading, - skipAlert: error.response.status === 404, - }; -}; - -export function fetchAccountMediaTimelineRequest(id, skipLoading) { - return { - type: ACCOUNT_MEDIA_TIMELINE_FETCH_REQUEST, - id, - skipLoading, - }; -}; - -export function fetchAccountMediaTimelineSuccess(id, statuses, replace, skipLoading, next) { - return { - type: ACCOUNT_MEDIA_TIMELINE_FETCH_SUCCESS, - id, - statuses, - replace, - skipLoading, - next, - }; -}; - -export function fetchAccountMediaTimelineFail(id, error, skipLoading) { - return { - type: ACCOUNT_MEDIA_TIMELINE_FETCH_FAIL, - id, - error, - skipLoading, - skipAlert: error.response.status === 404, - }; -}; - -export function expandAccountTimelineRequest(id) { - return { - type: ACCOUNT_TIMELINE_EXPAND_REQUEST, - id, - }; -}; - -export function expandAccountTimelineSuccess(id, statuses, next) { - return { - type: ACCOUNT_TIMELINE_EXPAND_SUCCESS, - id, - statuses, - next, - }; -}; - -export function expandAccountTimelineFail(id, error) { - return { - type: ACCOUNT_TIMELINE_EXPAND_FAIL, - id, - error, - }; -}; - -export function expandAccountMediaTimelineRequest(id) { - return { - type: ACCOUNT_MEDIA_TIMELINE_EXPAND_REQUEST, - id, - }; -}; - -export function expandAccountMediaTimelineSuccess(id, statuses, next) { - return { - type: ACCOUNT_MEDIA_TIMELINE_EXPAND_SUCCESS, - id, - statuses, - next, - }; -}; - -export function expandAccountMediaTimelineFail(id, error) { - return { - type: ACCOUNT_MEDIA_TIMELINE_EXPAND_FAIL, - id, - error, - }; -}; - export function blockAccount(id) { return (dispatch, getState) => { dispatch(blockAccountRequest(id)); diff --git a/app/javascript/mastodon/actions/notifications.js b/app/javascript/mastodon/actions/notifications.js index 51b06ed32..d3de2d871 100644 --- a/app/javascript/mastodon/actions/notifications.js +++ b/app/javascript/mastodon/actions/notifications.js @@ -124,25 +124,22 @@ export function refreshNotificationsFail(error, skipLoading) { export function expandNotifications() { return (dispatch, getState) => { - const url = getState().getIn(['notifications', 'next'], null); - const lastId = getState().getIn(['notifications', 'items']).last(); + const items = getState().getIn(['notifications', 'items'], Immutable.List()); - if (url === null || getState().getIn(['notifications', 'isLoading'])) { + if (getState().getIn(['notifications', 'isLoading']) || items.size === 0) { return; } - dispatch(expandNotificationsRequest()); - const params = { - max_id: lastId, + max_id: items.last().get('id'), limit: 20, + exclude_types: excludeTypesFromSettings(getState()), }; - params.exclude_types = excludeTypesFromSettings(getState()); + dispatch(expandNotificationsRequest()); - api(getState).get(url, params).then(response => { + api(getState).get('/api/v1/notifications', { params }).then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); - dispatch(expandNotificationsSuccess(response.data, next ? next.uri : null)); fetchRelatedRelationships(dispatch, response.data); }).catch(error => { diff --git a/app/javascript/mastodon/actions/timelines.js b/app/javascript/mastodon/actions/timelines.js index 98e441cd1..cb4410eba 100644 --- a/app/javascript/mastodon/actions/timelines.js +++ b/app/javascript/mastodon/actions/timelines.js @@ -56,91 +56,89 @@ export function deleteFromTimelines(id) { }; }; -export function refreshTimelineRequest(timeline, id, skipLoading) { +export function refreshTimelineRequest(timeline, skipLoading) { return { type: TIMELINE_REFRESH_REQUEST, timeline, - id, skipLoading, }; }; -export function refreshTimeline(timeline, id = null) { +export function refreshTimeline(timelineId, path, params = {}) { return function (dispatch, getState) { - if (getState().getIn(['timelines', timeline, 'isLoading'])) { + const timeline = getState().getIn(['timelines', timelineId], Immutable.Map()); + + if (timeline.get('isLoading') || timeline.get('online')) { return; } - const ids = getState().getIn(['timelines', timeline, 'items'], Immutable.List()); + const ids = timeline.get('items', Immutable.List()); const newestId = ids.size > 0 ? ids.first() : null; - let params = getState().getIn(['timelines', timeline, 'params'], {}); - const path = getState().getIn(['timelines', timeline, 'path'])(id); - - let skipLoading = false; - if (newestId !== null && getState().getIn(['timelines', timeline, 'loaded']) && (id === null || getState().getIn(['timelines', timeline, 'id']) === id)) { - if (id === null && getState().getIn(['timelines', timeline, 'online'])) { - // Skip refreshing when timeline is live anyway - return; - } + let skipLoading = timeline.get('loaded'); - params = { ...params, since_id: newestId }; - skipLoading = true; - } else if (getState().getIn(['timelines', timeline, 'loaded'])) { - skipLoading = true; + if (newestId !== null) { + params.since_id = newestId; } - dispatch(refreshTimelineRequest(timeline, id, skipLoading)); + dispatch(refreshTimelineRequest(timelineId, skipLoading)); api(getState).get(path, { params }).then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); - dispatch(refreshTimelineSuccess(timeline, response.data, skipLoading, next ? next.uri : null)); + dispatch(refreshTimelineSuccess(timelineId, response.data, skipLoading, next ? next.uri : null)); }).catch(error => { - dispatch(refreshTimelineFail(timeline, error, skipLoading)); + 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 => refreshTimeline(`account:${accountId}`, `/api/v1/accounts/${accountId}/statuses`); +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 function refreshTimelineFail(timeline, error, skipLoading) { return { type: TIMELINE_REFRESH_FAIL, timeline, error, skipLoading, + skipAlert: error.response.status === 404, }; }; -export function expandTimeline(timeline) { +export function expandTimeline(timelineId, path, params = {}) { return (dispatch, getState) => { - if (getState().getIn(['timelines', timeline, 'isLoading'])) { - return; - } + const timeline = getState().getIn(['timelines', timelineId], Immutable.Map()); + const ids = timeline.get('items', Immutable.List()); - if (getState().getIn(['timelines', timeline, 'items']).size === 0) { + if (timeline.get('isLoading') || ids.size === 0) { return; } - const path = getState().getIn(['timelines', timeline, 'path'])(getState().getIn(['timelines', timeline, 'id'])); - const params = getState().getIn(['timelines', timeline, 'params'], {}); - const lastId = getState().getIn(['timelines', timeline, 'items']).last(); + params.max_id = ids.last(); + params.limit = 10; - dispatch(expandTimelineRequest(timeline)); + dispatch(expandTimelineRequest(timelineId)); - api(getState).get(path, { - params: { - ...params, - max_id: lastId, - limit: 10, - }, - }).then(response => { + api(getState).get(path, { params }).then(response => { const next = getLinks(response).refs.find(link => link.rel === 'next'); - dispatch(expandTimelineSuccess(timeline, response.data, next ? next.uri : null)); + dispatch(expandTimelineSuccess(timelineId, response.data, next ? next.uri : null)); }).catch(error => { - dispatch(expandTimelineFail(timeline, 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 => expandTimeline(`account:${accountId}`, `/api/v1/accounts/${accountId}/statuses`); +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 function expandTimelineRequest(timeline) { return { type: TIMELINE_EXPAND_REQUEST, diff --git a/app/javascript/mastodon/containers/mastodon.js b/app/javascript/mastodon/containers/mastodon.js index 6d1c3f983..5e009cfa3 100644 --- a/app/javascript/mastodon/containers/mastodon.js +++ b/app/javascript/mastodon/containers/mastodon.js @@ -6,7 +6,7 @@ import { refreshTimelineSuccess, updateTimeline, deleteFromTimelines, - refreshTimeline, + refreshHomeTimeline, connectTimeline, disconnectTimeline, } from '../actions/timelines'; @@ -65,7 +65,7 @@ class Mastodon extends React.PureComponent { const setupPolling = () => { this.polling = setInterval(() => { - store.dispatch(refreshTimeline('home')); + store.dispatch(refreshHomeTimeline()); store.dispatch(refreshNotifications()); }, 20000); }; @@ -104,7 +104,7 @@ class Mastodon extends React.PureComponent { reconnected () { clearPolling(); store.dispatch(connectTimeline('home')); - store.dispatch(refreshTimeline('home')); + store.dispatch(refreshHomeTimeline()); store.dispatch(refreshNotifications()); }, diff --git a/app/javascript/mastodon/features/account_gallery/index.js b/app/javascript/mastodon/features/account_gallery/index.js index a15419ac7..fcbee3c89 100644 --- a/app/javascript/mastodon/features/account_gallery/index.js +++ b/app/javascript/mastodon/features/account_gallery/index.js @@ -2,11 +2,8 @@ import React from 'react'; import { connect } from 'react-redux'; import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; -import { - fetchAccount, - fetchAccountMediaTimeline, - expandAccountMediaTimeline, -} from '../../actions/accounts'; +import { fetchAccount } from '../../actions/accounts'; +import { refreshAccountMediaTimeline, expandAccountMediaTimeline } from '../../actions/timelines'; import LoadingIndicator from '../../components/loading_indicator'; import Column from '../ui/components/column'; import ColumnBackButton from '../../components/column_back_button'; @@ -21,8 +18,8 @@ import LoadMore from '../../components/load_more'; const mapStateToProps = (state, props) => ({ medias: getAccountGallery(state, Number(props.params.accountId)), - isLoading: state.getIn(['timelines', 'accounts_media_timelines', Number(props.params.accountId), 'isLoading']), - hasMore: !!state.getIn(['timelines', 'accounts_media_timelines', Number(props.params.accountId), 'next']), + isLoading: state.getIn(['timelines', `account:${Number(props.params.accountId)}:media`, 'isLoading']), + hasMore: !!state.getIn(['timelines', `account:${Number(props.params.accountId)}:media`, 'next']), autoPlayGif: state.getIn(['meta', 'auto_play_gif']), }); @@ -39,13 +36,13 @@ class AccountGallery extends ImmutablePureComponent { componentDidMount () { this.props.dispatch(fetchAccount(Number(this.props.params.accountId))); - this.props.dispatch(fetchAccountMediaTimeline(Number(this.props.params.accountId))); + this.props.dispatch(refreshAccountMediaTimeline(Number(this.props.params.accountId))); } componentWillReceiveProps (nextProps) { if (nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) { this.props.dispatch(fetchAccount(Number(nextProps.params.accountId))); - this.props.dispatch(fetchAccountMediaTimeline(Number(this.props.params.accountId))); + this.props.dispatch(refreshAccountMediaTimeline(Number(this.props.params.accountId))); } } diff --git a/app/javascript/mastodon/features/account_timeline/index.js b/app/javascript/mastodon/features/account_timeline/index.js index 299f9ad5c..1aab8f130 100644 --- a/app/javascript/mastodon/features/account_timeline/index.js +++ b/app/javascript/mastodon/features/account_timeline/index.js @@ -2,11 +2,8 @@ import React from 'react'; import { connect } from 'react-redux'; import ImmutablePropTypes from 'react-immutable-proptypes'; import PropTypes from 'prop-types'; -import { - fetchAccount, - fetchAccountTimeline, - expandAccountTimeline, -} from '../../actions/accounts'; +import { fetchAccount } from '../../actions/accounts'; +import { refreshAccountTimeline, expandAccountTimeline } from '../../actions/timelines'; import StatusList from '../../components/status_list'; import LoadingIndicator from '../../components/loading_indicator'; import Column from '../ui/components/column'; @@ -16,9 +13,9 @@ import Immutable from 'immutable'; import ImmutablePureComponent from 'react-immutable-pure-component'; const mapStateToProps = (state, props) => ({ - statusIds: state.getIn(['timelines', 'accounts_timelines', Number(props.params.accountId), 'items'], Immutable.List()), - isLoading: state.getIn(['timelines', 'accounts_timelines', Number(props.params.accountId), 'isLoading']), - hasMore: !!state.getIn(['timelines', 'accounts_timelines', Number(props.params.accountId), 'next']), + statusIds: state.getIn(['timelines', `account:${Number(props.params.accountId)}`, 'items'], Immutable.List()), + isLoading: state.getIn(['timelines', `account:${Number(props.params.accountId)}`, 'isLoading']), + hasMore: !!state.getIn(['timelines', `account:${Number(props.params.accountId)}`, 'next']), me: state.getIn(['meta', 'me']), }); @@ -35,13 +32,13 @@ class AccountTimeline extends ImmutablePureComponent { componentWillMount () { this.props.dispatch(fetchAccount(Number(this.props.params.accountId))); - this.props.dispatch(fetchAccountTimeline(Number(this.props.params.accountId))); + this.props.dispatch(refreshAccountTimeline(Number(this.props.params.accountId))); } componentWillReceiveProps (nextProps) { if (nextProps.params.accountId !== this.props.params.accountId && nextProps.params.accountId) { this.props.dispatch(fetchAccount(Number(nextProps.params.accountId))); - this.props.dispatch(fetchAccountTimeline(Number(nextProps.params.accountId))); + this.props.dispatch(refreshAccountTimeline(Number(nextProps.params.accountId))); } } diff --git a/app/javascript/mastodon/features/community_timeline/index.js b/app/javascript/mastodon/features/community_timeline/index.js index 44ea4dddd..4fbe67038 100644 --- a/app/javascript/mastodon/features/community_timeline/index.js +++ b/app/javascript/mastodon/features/community_timeline/index.js @@ -5,7 +5,8 @@ import StatusListContainer from '../ui/containers/status_list_container'; import Column from '../../components/column'; import ColumnHeader from '../../components/column_header'; import { - refreshTimeline, + refreshCommunityTimeline, + expandCommunityTimeline, updateTimeline, deleteFromTimelines, connectTimeline, @@ -61,7 +62,7 @@ class CommunityTimeline extends React.PureComponent { componentDidMount () { const { dispatch, streamingAPIBaseURL, accessToken } = this.props; - dispatch(refreshTimeline('community')); + dispatch(refreshCommunityTimeline()); if (typeof this._subscription !== 'undefined') { return; @@ -106,6 +107,10 @@ class CommunityTimeline extends React.PureComponent { this.column = c; } + handleLoadMore = () => { + this.props.dispatch(expandCommunityTimeline()); + } + render () { const { intl, hasUnread, columnId, multiColumn } = this.props; const pinned = !!columnId; @@ -126,10 +131,10 @@ class CommunityTimeline extends React.PureComponent { </ColumnHeader> <StatusListContainer - {...this.props} trackScroll={!pinned} scrollKey={`community_timeline-${columnId}`} - type='community' + timelineId='community' + loadMore={this.handleLoadMore} emptyMessage={<FormattedMessage id='empty_column.community' defaultMessage='The local timeline is empty. Write something publicly to get the ball rolling!' />} /> </Column> diff --git a/app/javascript/mastodon/features/hashtag_timeline/index.js b/app/javascript/mastodon/features/hashtag_timeline/index.js index 05e6d1996..ba6dbe6e5 100644 --- a/app/javascript/mastodon/features/hashtag_timeline/index.js +++ b/app/javascript/mastodon/features/hashtag_timeline/index.js @@ -5,7 +5,8 @@ import StatusListContainer from '../ui/containers/status_list_container'; import Column from '../../components/column'; import ColumnHeader from '../../components/column_header'; import { - refreshTimeline, + refreshHashtagTimeline, + expandHashtagTimeline, updateTimeline, deleteFromTimelines, } from '../../actions/timelines'; @@ -81,13 +82,13 @@ class HashtagTimeline extends React.PureComponent { const { dispatch } = this.props; const { id } = this.props.params; - dispatch(refreshTimeline('tag', id)); + dispatch(refreshHashtagTimeline(id)); this._subscribe(dispatch, id); } componentWillReceiveProps (nextProps) { if (nextProps.params.id !== this.props.params.id) { - this.props.dispatch(refreshTimeline('tag', nextProps.params.id)); + this.props.dispatch(refreshHashtagTimeline(nextProps.params.id)); this._unsubscribe(); this._subscribe(this.props.dispatch, nextProps.params.id); } @@ -101,6 +102,10 @@ class HashtagTimeline extends React.PureComponent { this.column = c; } + handleLoadMore = () => { + this.props.dispatch(expandHashtagTimeline(this.props.params.id)); + } + render () { const { hasUnread, columnId, multiColumn } = this.props; const { id } = this.props.params; @@ -123,8 +128,8 @@ class HashtagTimeline extends React.PureComponent { <StatusListContainer trackScroll={!pinned} scrollKey={`hashtag_timeline-${columnId}`} - type='tag' - id={id} + timelineId={`hashtag:${id}`} + loadMore={this.handleLoadMore} emptyMessage={<FormattedMessage id='empty_column.hashtag' defaultMessage='There is nothing in this hashtag yet.' />} /> </Column> diff --git a/app/javascript/mastodon/features/home_timeline/index.js b/app/javascript/mastodon/features/home_timeline/index.js index 7439bf3a1..6d3968751 100644 --- a/app/javascript/mastodon/features/home_timeline/index.js +++ b/app/javascript/mastodon/features/home_timeline/index.js @@ -1,5 +1,6 @@ import React from 'react'; import { connect } from 'react-redux'; +import { expandHomeTimeline } from '../../actions/timelines'; import PropTypes from 'prop-types'; import StatusListContainer from '../ui/containers/status_list_container'; import Column from '../../components/column'; @@ -52,6 +53,10 @@ class HomeTimeline extends React.PureComponent { this.column = c; } + handleLoadMore = () => { + this.props.dispatch(expandHomeTimeline()); + } + render () { const { intl, hasUnread, hasFollows, columnId, multiColumn } = this.props; const pinned = !!columnId; @@ -80,10 +85,10 @@ class HomeTimeline extends React.PureComponent { </ColumnHeader> <StatusListContainer - {...this.props} trackScroll={!pinned} scrollKey={`home_timeline-${columnId}`} - type='home' + loadMore={this.handleLoadMore} + timelineId='home' emptyMessage={emptyMessage} /> </Column> diff --git a/app/javascript/mastodon/features/public_timeline/index.js b/app/javascript/mastodon/features/public_timeline/index.js index 2e4fd32a4..02ddb418f 100644 --- a/app/javascript/mastodon/features/public_timeline/index.js +++ b/app/javascript/mastodon/features/public_timeline/index.js @@ -5,7 +5,8 @@ import StatusListContainer from '../ui/containers/status_list_container'; import Column from '../../components/column'; import ColumnHeader from '../../components/column_header'; import { - refreshTimeline, + refreshPublicTimeline, + expandPublicTimeline, updateTimeline, deleteFromTimelines, connectTimeline, @@ -61,7 +62,7 @@ class PublicTimeline extends React.PureComponent { componentDidMount () { const { dispatch, streamingAPIBaseURL, accessToken } = this.props; - dispatch(refreshTimeline('public')); + dispatch(refreshPublicTimeline()); if (typeof this._subscription !== 'undefined') { return; @@ -106,6 +107,10 @@ class PublicTimeline extends React.PureComponent { this.column = c; } + handleLoadMore = () => { + this.props.dispatch(expandPublicTimeline()); + } + render () { const { intl, columnId, hasUnread, multiColumn } = this.props; const pinned = !!columnId; @@ -126,8 +131,8 @@ class PublicTimeline extends React.PureComponent { </ColumnHeader> <StatusListContainer - {...this.props} - type='public' + timelineId='public' + loadMore={this.handleLoadMore} trackScroll={!pinned} scrollKey={`public_timeline-${columnId}`} emptyMessage={<FormattedMessage id='empty_column.public' defaultMessage='There is nothing here! Write something publicly, or manually follow users from other instances to fill it up' />} diff --git a/app/javascript/mastodon/features/report/index.js b/app/javascript/mastodon/features/report/index.js index 25e601f1a..217802b5c 100644 --- a/app/javascript/mastodon/features/report/index.js +++ b/app/javascript/mastodon/features/report/index.js @@ -1,7 +1,7 @@ import React from 'react'; import { connect } from 'react-redux'; import { cancelReport, changeReportComment, submitReport } from '../../actions/reports'; -import { fetchAccountTimeline } from '../../actions/accounts'; +import { refreshAccountTimeline } from '../../actions/timelines'; import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import Column from '../ui/components/column'; @@ -28,7 +28,7 @@ const makeMapStateToProps = () => { isSubmitting: state.getIn(['reports', 'new', 'isSubmitting']), account: getAccount(state, accountId), comment: state.getIn(['reports', 'new', 'comment']), - statusIds: Immutable.OrderedSet(state.getIn(['timelines', 'accounts_timelines', accountId, 'items'])).union(state.getIn(['reports', 'new', 'status_ids'])), + statusIds: Immutable.OrderedSet(state.getIn(['timelines', `account:${accountId}`, 'items'])).union(state.getIn(['reports', 'new', 'status_ids'])), }; }; @@ -61,12 +61,12 @@ class Report extends React.PureComponent { return; } - this.props.dispatch(fetchAccountTimeline(this.props.account.get('id'))); + this.props.dispatch(refreshAccountTimeline(this.props.account.get('id'))); } componentWillReceiveProps (nextProps) { if (this.props.account !== nextProps.account && nextProps.account) { - this.props.dispatch(fetchAccountTimeline(nextProps.account.get('id'))); + this.props.dispatch(refreshAccountTimeline(nextProps.account.get('id'))); } } diff --git a/app/javascript/mastodon/features/status/index.js b/app/javascript/mastodon/features/status/index.js index 7fc55b795..f8a87ccb1 100644 --- a/app/javascript/mastodon/features/status/index.js +++ b/app/javascript/mastodon/features/status/index.js @@ -44,8 +44,8 @@ const makeMapStateToProps = () => { const mapStateToProps = (state, props) => ({ status: getStatus(state, Number(props.params.statusId)), - ancestorsIds: state.getIn(['timelines', 'ancestors', Number(props.params.statusId)]), - descendantsIds: state.getIn(['timelines', 'descendants', Number(props.params.statusId)]), + ancestorsIds: state.getIn(['contexts', 'ancestors', Number(props.params.statusId)]), + descendantsIds: state.getIn(['contexts', 'descendants', Number(props.params.statusId)]), me: state.getIn(['meta', 'me']), boostModal: state.getIn(['meta', 'boost_modal']), deleteModal: state.getIn(['meta', 'delete_modal']), diff --git a/app/javascript/mastodon/features/ui/containers/status_list_container.js b/app/javascript/mastodon/features/ui/containers/status_list_container.js index cbdd2d12d..8d8dc9ba9 100644 --- a/app/javascript/mastodon/features/ui/containers/status_list_container.js +++ b/app/javascript/mastodon/features/ui/containers/status_list_container.js @@ -1,6 +1,6 @@ import { connect } from 'react-redux'; import StatusList from '../../../components/status_list'; -import { expandTimeline, scrollTopTimeline } from '../../../actions/timelines'; +import { scrollTopTimeline } from '../../../actions/timelines'; import Immutable from 'immutable'; import { createSelector } from 'reselect'; import { debounce } from 'lodash'; @@ -39,31 +39,29 @@ const makeGetStatusIds = () => createSelector([ const makeMapStateToProps = () => { const getStatusIds = makeGetStatusIds(); - const mapStateToProps = (state, props) => ({ - scrollKey: props.scrollKey, - shouldUpdateScroll: props.shouldUpdateScroll, - statusIds: getStatusIds(state, props), - isLoading: state.getIn(['timelines', props.type, 'isLoading'], true), - isUnread: state.getIn(['timelines', props.type, 'unread']) > 0, - hasMore: !!state.getIn(['timelines', props.type, 'next']), + const mapStateToProps = (state, { timelineId }) => ({ + statusIds: getStatusIds(state, { type: timelineId }), + isLoading: state.getIn(['timelines', timelineId, 'isLoading'], true), + isUnread: state.getIn(['timelines', timelineId, 'unread']) > 0, + hasMore: !!state.getIn(['timelines', timelineId, 'next']), }); return mapStateToProps; }; -const mapDispatchToProps = (dispatch, { type, id }) => ({ +const mapDispatchToProps = (dispatch, { timelineId, loadMore }) => ({ onScrollToBottom: debounce(() => { - dispatch(scrollTopTimeline(type, false)); - dispatch(expandTimeline(type, id)); + dispatch(scrollTopTimeline(timelineId, false)); + loadMore(); }, 300, { leading: true }), onScrollToTop: debounce(() => { - dispatch(scrollTopTimeline(type, true)); + dispatch(scrollTopTimeline(timelineId, true)); }, 100), onScroll: debounce(() => { - dispatch(scrollTopTimeline(type, false)); + dispatch(scrollTopTimeline(timelineId, false)); }, 100), }); diff --git a/app/javascript/mastodon/features/ui/index.js b/app/javascript/mastodon/features/ui/index.js index 5e70c888c..fe775b434 100644 --- a/app/javascript/mastodon/features/ui/index.js +++ b/app/javascript/mastodon/features/ui/index.js @@ -8,7 +8,7 @@ import { connect } from 'react-redux'; import { isMobile } from '../../is_mobile'; import { debounce } from 'lodash'; import { uploadCompose } from '../../actions/compose'; -import { refreshTimeline } from '../../actions/timelines'; +import { refreshHomeTimeline } from '../../actions/timelines'; import { refreshNotifications } from '../../actions/notifications'; import UploadArea from './components/upload_area'; import ColumnsAreaContainer from './containers/columns_area_container'; @@ -95,7 +95,7 @@ class UI extends React.PureComponent { document.addEventListener('dragleave', this.handleDragLeave, false); document.addEventListener('dragend', this.handleDragEnd, false); - this.props.dispatch(refreshTimeline('home')); + this.props.dispatch(refreshHomeTimeline()); this.props.dispatch(refreshNotifications()); } diff --git a/app/javascript/mastodon/reducers/accounts.js b/app/javascript/mastodon/reducers/accounts.js index d4d9ad62e..7b7074317 100644 --- a/app/javascript/mastodon/reducers/accounts.js +++ b/app/javascript/mastodon/reducers/accounts.js @@ -4,8 +4,6 @@ import { FOLLOWERS_EXPAND_SUCCESS, FOLLOWING_FETCH_SUCCESS, FOLLOWING_EXPAND_SUCCESS, - ACCOUNT_TIMELINE_FETCH_SUCCESS, - ACCOUNT_TIMELINE_EXPAND_SUCCESS, FOLLOW_REQUESTS_FETCH_SUCCESS, FOLLOW_REQUESTS_EXPAND_SUCCESS, } from '../actions/accounts'; @@ -113,8 +111,6 @@ export default function accounts(state = initialState, action) { return normalizeAccountsFromStatuses(normalizeAccounts(state, action.accounts), action.statuses); case TIMELINE_REFRESH_SUCCESS: case TIMELINE_EXPAND_SUCCESS: - case ACCOUNT_TIMELINE_FETCH_SUCCESS: - case ACCOUNT_TIMELINE_EXPAND_SUCCESS: case CONTEXT_FETCH_SUCCESS: case FAVOURITED_STATUSES_FETCH_SUCCESS: case FAVOURITED_STATUSES_EXPAND_SUCCESS: diff --git a/app/javascript/mastodon/reducers/accounts_counters.js b/app/javascript/mastodon/reducers/accounts_counters.js index ea631ceca..eb8a4f83d 100644 --- a/app/javascript/mastodon/reducers/accounts_counters.js +++ b/app/javascript/mastodon/reducers/accounts_counters.js @@ -4,8 +4,6 @@ import { FOLLOWERS_EXPAND_SUCCESS, FOLLOWING_FETCH_SUCCESS, FOLLOWING_EXPAND_SUCCESS, - ACCOUNT_TIMELINE_FETCH_SUCCESS, - ACCOUNT_TIMELINE_EXPAND_SUCCESS, FOLLOW_REQUESTS_FETCH_SUCCESS, FOLLOW_REQUESTS_EXPAND_SUCCESS, ACCOUNT_FOLLOW_SUCCESS, @@ -115,8 +113,6 @@ export default function accountsCounters(state = initialState, action) { return normalizeAccountsFromStatuses(normalizeAccounts(state, action.accounts), action.statuses); case TIMELINE_REFRESH_SUCCESS: case TIMELINE_EXPAND_SUCCESS: - case ACCOUNT_TIMELINE_FETCH_SUCCESS: - case ACCOUNT_TIMELINE_EXPAND_SUCCESS: case CONTEXT_FETCH_SUCCESS: case FAVOURITED_STATUSES_FETCH_SUCCESS: case FAVOURITED_STATUSES_EXPAND_SUCCESS: diff --git a/app/javascript/mastodon/reducers/contexts.js b/app/javascript/mastodon/reducers/contexts.js new file mode 100644 index 000000000..8a24f5f7a --- /dev/null +++ b/app/javascript/mastodon/reducers/contexts.js @@ -0,0 +1,43 @@ +import { CONTEXT_FETCH_SUCCESS } from '../actions/statuses'; +import { TIMELINE_DELETE } from '../actions/timelines'; +import Immutable from 'immutable'; + +const initialState = Immutable.Map({ + ancestors: Immutable.Map(), + descendants: Immutable.Map(), +}); + +const normalizeContext = (state, id, ancestors, descendants) => { + const ancestorsIds = ancestors.map(ancestor => ancestor.get('id')); + const descendantsIds = descendants.map(descendant => descendant.get('id')); + + return state.withMutations(map => { + map.setIn(['ancestors', id], ancestorsIds); + map.setIn(['descendants', id], descendantsIds); + }); +}; + +const deleteFromContexts = (state, id) => { + state.getIn(['descendants', id], Immutable.List()).forEach(descendantId => { + state = state.updateIn(['ancestors', descendantId], Immutable.List(), list => list.filterNot(itemId => itemId === id)); + }); + + state.getIn(['ancestors', id], Immutable.List()).forEach(ancestorId => { + state = state.updateIn(['descendants', ancestorId], Immutable.List(), list => list.filterNot(itemId => itemId === id)); + }); + + state = state.deleteIn(['descendants', id]).deleteIn(['ancestors', id]); + + return state; +}; + +export default function contexts(state = initialState, action) { + switch(action.type) { + case CONTEXT_FETCH_SUCCESS: + return normalizeContext(state, action.id, Immutable.fromJS(action.ancestors), Immutable.fromJS(action.descendants)); + case TIMELINE_DELETE: + return deleteFromContexts(state, action.id); + default: + return state; + } +}; diff --git a/app/javascript/mastodon/reducers/index.js b/app/javascript/mastodon/reducers/index.js index c4fe28ea7..be402a16b 100644 --- a/app/javascript/mastodon/reducers/index.js +++ b/app/javascript/mastodon/reducers/index.js @@ -17,6 +17,7 @@ import settings from './settings'; import status_lists from './status_lists'; import cards from './cards'; import reports from './reports'; +import contexts from './contexts'; export default combineReducers({ timelines, @@ -37,4 +38,5 @@ export default combineReducers({ settings, cards, reports, + contexts, }); diff --git a/app/javascript/mastodon/reducers/statuses.js b/app/javascript/mastodon/reducers/statuses.js index 7bc3710c4..691135ff7 100644 --- a/app/javascript/mastodon/reducers/statuses.js +++ b/app/javascript/mastodon/reducers/statuses.js @@ -21,10 +21,6 @@ import { TIMELINE_EXPAND_SUCCESS, } from '../actions/timelines'; import { - ACCOUNT_TIMELINE_FETCH_SUCCESS, - ACCOUNT_TIMELINE_EXPAND_SUCCESS, - ACCOUNT_MEDIA_TIMELINE_FETCH_SUCCESS, - ACCOUNT_MEDIA_TIMELINE_EXPAND_SUCCESS, ACCOUNT_BLOCK_SUCCESS, } from '../actions/accounts'; import { @@ -113,10 +109,6 @@ export default function statuses(state = initialState, action) { return state.setIn([action.id, 'muted'], false); case TIMELINE_REFRESH_SUCCESS: case TIMELINE_EXPAND_SUCCESS: - case ACCOUNT_TIMELINE_FETCH_SUCCESS: - case ACCOUNT_TIMELINE_EXPAND_SUCCESS: - case ACCOUNT_MEDIA_TIMELINE_FETCH_SUCCESS: - case ACCOUNT_MEDIA_TIMELINE_EXPAND_SUCCESS: case CONTEXT_FETCH_SUCCESS: case NOTIFICATIONS_REFRESH_SUCCESS: case NOTIFICATIONS_EXPAND_SUCCESS: diff --git a/app/javascript/mastodon/reducers/timelines.js b/app/javascript/mastodon/reducers/timelines.js index ab756b854..2bc1c8050 100644 --- a/app/javascript/mastodon/reducers/timelines.js +++ b/app/javascript/mastodon/reducers/timelines.js @@ -18,228 +18,79 @@ import { UNFAVOURITE_SUCCESS, } from '../actions/interactions'; import { - ACCOUNT_TIMELINE_FETCH_REQUEST, - ACCOUNT_TIMELINE_FETCH_SUCCESS, - ACCOUNT_TIMELINE_FETCH_FAIL, - ACCOUNT_TIMELINE_EXPAND_REQUEST, - ACCOUNT_TIMELINE_EXPAND_SUCCESS, - ACCOUNT_TIMELINE_EXPAND_FAIL, - ACCOUNT_MEDIA_TIMELINE_FETCH_REQUEST, - ACCOUNT_MEDIA_TIMELINE_FETCH_SUCCESS, - ACCOUNT_MEDIA_TIMELINE_FETCH_FAIL, - ACCOUNT_MEDIA_TIMELINE_EXPAND_REQUEST, - ACCOUNT_MEDIA_TIMELINE_EXPAND_SUCCESS, - ACCOUNT_MEDIA_TIMELINE_EXPAND_FAIL, ACCOUNT_BLOCK_SUCCESS, ACCOUNT_MUTE_SUCCESS, } from '../actions/accounts'; -import { - CONTEXT_FETCH_SUCCESS, -} from '../actions/statuses'; import Immutable from 'immutable'; -const initialState = Immutable.Map({ - home: Immutable.Map({ - path: () => '/api/v1/timelines/home', - next: null, - isLoading: false, - online: false, - loaded: false, - top: true, - unread: 0, - items: Immutable.List(), - }), - - public: Immutable.Map({ - path: () => '/api/v1/timelines/public', - next: null, - isLoading: false, - online: false, - loaded: false, - top: true, - unread: 0, - items: Immutable.List(), - }), - - community: Immutable.Map({ - path: () => '/api/v1/timelines/public', - next: null, - params: { local: true }, - isLoading: false, - online: false, - loaded: false, - top: true, - unread: 0, - items: Immutable.List(), - }), +const initialState = Immutable.Map(); - tag: Immutable.Map({ - path: (id) => `/api/v1/timelines/tag/${id}`, - next: null, - isLoading: false, - id: null, - loaded: false, - top: true, - unread: 0, - items: Immutable.List(), - }), - - accounts_timelines: Immutable.Map(), - accounts_media_timelines: Immutable.Map(), - ancestors: Immutable.Map(), - descendants: Immutable.Map(), +const initialTimeline = Immutable.Map({ + unread: 0, + online: false, + top: true, + loaded: false, + isLoading: false, + next: false, + items: Immutable.List(), }); -const normalizeStatus = (state, status) => { - return state; -}; - const normalizeTimeline = (state, timeline, statuses, next) => { - let ids = Immutable.List(); - const loaded = state.getIn([timeline, 'loaded']); - - statuses.forEach((status, i) => { - state = normalizeStatus(state, status); - ids = ids.set(i, status.get('id')); - }); - - state = state.setIn([timeline, 'loaded'], true); - state = state.setIn([timeline, 'isLoading'], false); - - if (state.getIn([timeline, 'next']) === null) { - state = state.setIn([timeline, 'next'], next); - } - - return state.updateIn([timeline, 'items'], Immutable.List(), list => (loaded ? ids.concat(list) : ids)); + const ids = Immutable.List(statuses.map(status => status.get('id'))); + const wasLoaded = state.getIn([timeline, 'loaded']); + const hadNext = state.getIn([timeline, 'next']); + const oldIds = state.getIn([timeline, 'items'], Immutable.List()); + + return state.update(timeline, initialTimeline, map => map.withMutations(mMap => { + mMap.set('loaded', true); + mMap.set('isLoading', false); + if (!hadNext) mMap.set('next', next); + mMap.set('items', wasLoaded ? ids.concat(oldIds) : ids); + })); }; const appendNormalizedTimeline = (state, timeline, statuses, next) => { - let moreIds = Immutable.List(); - - statuses.forEach((status, i) => { - state = normalizeStatus(state, status); - moreIds = moreIds.set(i, status.get('id')); - }); - - state = state.setIn([timeline, 'isLoading'], false); - state = state.setIn([timeline, 'next'], next); - - return state.updateIn([timeline, 'items'], Immutable.List(), list => list.concat(moreIds)); -}; - -const normalizeAccountTimeline = (state, accountId, statuses, replace, next) => { - let ids = Immutable.List(); - - statuses.forEach((status, i) => { - state = normalizeStatus(state, status); - ids = ids.set(i, status.get('id')); - }); - - return state.updateIn(['accounts_timelines', accountId], Immutable.Map(), map => map - .set('isLoading', false) - .set('loaded', true) - .update('next', null, v => replace ? next : v) - .update('items', Immutable.List(), list => (replace ? ids : ids.concat(list)))); -}; - -const normalizeAccountMediaTimeline = (state, accountId, statuses, replace, next) => { - let ids = Immutable.List(); - - statuses.forEach((status, i) => { - state = normalizeStatus(state, status); - ids = ids.set(i, status.get('id')); - }); - - return state.updateIn(['accounts_media_timelines', accountId], Immutable.Map(), map => map - .set('isLoading', false) - .update('next', null, v => replace ? next : v) - .update('items', Immutable.List(), list => (replace ? ids : ids.concat(list)))); -}; - -const appendNormalizedAccountTimeline = (state, accountId, statuses, next) => { - let moreIds = Immutable.List([]); - - statuses.forEach((status, i) => { - state = normalizeStatus(state, status); - moreIds = moreIds.set(i, status.get('id')); - }); - - return state.updateIn(['accounts_timelines', accountId], Immutable.Map(), map => map - .set('isLoading', false) - .set('next', next) - .update('items', list => list.concat(moreIds))); -}; - -const appendNormalizedAccountMediaTimeline = (state, accountId, statuses, next) => { - let moreIds = Immutable.List([]); - - statuses.forEach((status, i) => { - state = normalizeStatus(state, status); - moreIds = moreIds.set(i, status.get('id')); - }); - - return state.updateIn(['accounts_media_timelines', accountId], Immutable.Map(), map => map - .set('isLoading', false) - .set('next', next) - .update('items', list => list.concat(moreIds))); + const ids = Immutable.List(statuses.map(status => status.get('id'))); + const oldIds = state.getIn([timeline, 'items'], Immutable.List()); + + return state.update(timeline, initialTimeline, map => map.withMutations(mMap => { + mMap.set('isLoading', false); + mMap.set('next', next); + mMap.set('items', oldIds.concat(ids)); + })); }; const updateTimeline = (state, timeline, status, references) => { - const top = state.getIn([timeline, 'top']); + const top = state.getIn([timeline, 'top']); + const ids = state.getIn([timeline, 'items'], Immutable.List()); + const includesId = ids.includes(status.get('id')); + const unread = state.getIn([timeline, 'unread'], 0); - state = normalizeStatus(state, status); - - if (!top) { - state = state.updateIn([timeline, 'unread'], unread => unread + 1); + if (includesId) { + return state; } - state = state.updateIn([timeline, 'items'], Immutable.List(), list => { - if (top && list.size > 40) { - list = list.take(20); - } - - if (list.includes(status.get('id'))) { - return list; - } - - const reblogOfId = status.getIn(['reblog', 'id'], null); + let newIds = ids; - if (reblogOfId !== null) { - list = list.filterNot(itemId => references.includes(itemId)); - } - - return list.unshift(status.get('id')); - }); - - return state; + return state.update(timeline, initialTimeline, map => map.withMutations(mMap => { + if (!top) mMap.set('unread', unread + 1); + if (top && ids.size > 40) newIds = newIds.take(20); + if (status.getIn(['reblog', 'id'], null) !== null) newIds = newIds.filterNot(item => references.includes(item)); + mMap.set('items', newIds.unshift(status.get('id'))); + })); }; const deleteStatus = (state, id, accountId, references, reblogOf) => { - if (reblogOf) { - // If we are deleting a reblog, just replace reblog with its original - return state.updateIn(['home', 'items'], list => list.map(item => item === id ? reblogOf : item)); - } - - // Remove references from timelines - ['home', 'public', 'community', 'tag'].forEach(function (timeline) { - state = state.updateIn([timeline, 'items'], list => list.filterNot(item => item === id)); + state.keySeq().forEach(timeline => { + state = state.updateIn([timeline, 'items'], list => { + if (reblogOf && !list.includes(reblogOf)) { + return list.map(item => item === id ? reblogOf : item); + } else { + return list.filterNot(item => item === id); + } + }); }); - // Remove references from account timelines - state = state.updateIn(['accounts_timelines', accountId, 'items'], Immutable.List([]), list => list.filterNot(item => item === id)); - state = state.updateIn(['accounts_media_timelines', accountId, 'items'], Immutable.List([]), list => list.filterNot(item => item === id)); - - // Remove references from context - state.getIn(['descendants', id], Immutable.List()).forEach(descendantId => { - state = state.updateIn(['ancestors', descendantId], Immutable.List(), list => list.filterNot(itemId => itemId === id)); - }); - - state.getIn(['ancestors', id], Immutable.List()).forEach(ancestorId => { - state = state.updateIn(['descendants', ancestorId], Immutable.List(), list => list.filterNot(itemId => itemId === id)); - }); - - state = state.deleteIn(['descendants', id]).deleteIn(['ancestors', id]); - // Remove reblogs of deleted status references.forEach(ref => { state = deleteStatus(state, ref[0], ref[1], []); @@ -257,54 +108,27 @@ const filterTimelines = (state, relationship, statuses) => { } references = statuses.filter(item => item.get('reblog') === status.get('id')).map(item => [item.get('id'), item.get('account')]); - state = deleteStatus(state, status.get('id'), status.get('account'), references); - }); - - return state; -}; - -const normalizeContext = (state, id, ancestors, descendants) => { - const ancestorsIds = ancestors.map(ancestor => ancestor.get('id')); - const descendantsIds = descendants.map(descendant => descendant.get('id')); - - return state.withMutations(map => { - map.setIn(['ancestors', id], ancestorsIds); - map.setIn(['descendants', id], descendantsIds); + state = deleteStatus(state, status.get('id'), status.get('account'), references); }); -}; - -const resetTimeline = (state, timeline, id) => { - if (timeline === 'tag' && typeof id !== 'undefined' && state.getIn([timeline, 'id']) !== id) { - state = state.update(timeline, map => map - .set('id', id) - .set('isLoading', true) - .set('loaded', false) - .set('next', null) - .set('top', true) - .update('items', list => list.clear())); - } else { - state = state.setIn([timeline, 'isLoading'], true); - } return state; }; const updateTop = (state, timeline, top) => { - if (top) { - state = state.setIn([timeline, 'unread'], 0); - } - - return state.setIn([timeline, 'top'], top); + return state.update(timeline, initialTimeline, map => map.withMutations(mMap => { + if (top) mMap.set('unread', 0); + mMap.set('top', top); + })); }; export default function timelines(state = initialState, action) { switch(action.type) { case TIMELINE_REFRESH_REQUEST: case TIMELINE_EXPAND_REQUEST: - return resetTimeline(state, action.timeline, action.id); + return state.update(action.timeline, initialTimeline, map => map.set('isLoading', true)); case TIMELINE_REFRESH_FAIL: case TIMELINE_EXPAND_FAIL: - return state.setIn([action.timeline, 'isLoading'], false); + return state.update(action.timeline, initialTimeline, map => map.set('isLoading', false)); case TIMELINE_REFRESH_SUCCESS: return normalizeTimeline(state, action.timeline, Immutable.fromJS(action.statuses), action.next); case TIMELINE_EXPAND_SUCCESS: @@ -313,37 +137,15 @@ export default function timelines(state = initialState, action) { return updateTimeline(state, action.timeline, Immutable.fromJS(action.status), action.references); case TIMELINE_DELETE: return deleteStatus(state, action.id, action.accountId, action.references, action.reblogOf); - case CONTEXT_FETCH_SUCCESS: - return normalizeContext(state, action.id, Immutable.fromJS(action.ancestors), Immutable.fromJS(action.descendants)); - case ACCOUNT_TIMELINE_FETCH_REQUEST: - case ACCOUNT_TIMELINE_EXPAND_REQUEST: - return state.updateIn(['accounts_timelines', action.id], Immutable.Map(), map => map.set('isLoading', true)); - case ACCOUNT_TIMELINE_FETCH_FAIL: - case ACCOUNT_TIMELINE_EXPAND_FAIL: - return state.updateIn(['accounts_timelines', action.id], Immutable.Map(), map => map.set('isLoading', false)); - case ACCOUNT_TIMELINE_FETCH_SUCCESS: - return normalizeAccountTimeline(state, action.id, Immutable.fromJS(action.statuses), action.replace, action.next); - case ACCOUNT_TIMELINE_EXPAND_SUCCESS: - return appendNormalizedAccountTimeline(state, action.id, Immutable.fromJS(action.statuses), action.next); - case ACCOUNT_MEDIA_TIMELINE_FETCH_REQUEST: - case ACCOUNT_MEDIA_TIMELINE_EXPAND_REQUEST: - return state.updateIn(['accounts_media_timelines', action.id], Immutable.Map(), map => map.set('isLoading', true)); - case ACCOUNT_MEDIA_TIMELINE_FETCH_FAIL: - case ACCOUNT_MEDIA_TIMELINE_EXPAND_FAIL: - return state.updateIn(['accounts_media_timelines', action.id], Immutable.Map(), map => map.set('isLoading', false)); - case ACCOUNT_MEDIA_TIMELINE_FETCH_SUCCESS: - return normalizeAccountMediaTimeline(state, action.id, Immutable.fromJS(action.statuses), action.replace, action.next); - case ACCOUNT_MEDIA_TIMELINE_EXPAND_SUCCESS: - return appendNormalizedAccountMediaTimeline(state, action.id, Immutable.fromJS(action.statuses), action.next); case ACCOUNT_BLOCK_SUCCESS: case ACCOUNT_MUTE_SUCCESS: return filterTimelines(state, action.relationship, action.statuses); case TIMELINE_SCROLL_TOP: return updateTop(state, action.timeline, action.top); case TIMELINE_CONNECT: - return state.setIn([action.timeline, 'online'], true); + return state.update(action.timeline, initialTimeline, map => map.set('online', true)); case TIMELINE_DISCONNECT: - return state.setIn([action.timeline, 'online'], false); + return state.update(action.timeline, initialTimeline, map => map.set('online', false)); default: return state; } diff --git a/app/javascript/mastodon/selectors/index.js b/app/javascript/mastodon/selectors/index.js index 0d2ef2ea9..d5d736e2f 100644 --- a/app/javascript/mastodon/selectors/index.js +++ b/app/javascript/mastodon/selectors/index.js @@ -76,7 +76,7 @@ export const makeGetNotification = () => { }; export const getAccountGallery = createSelector([ - (state, id) => state.getIn(['timelines', 'accounts_media_timelines', id, 'items'], Immutable.List()), + (state, id) => state.getIn(['timelines', `account:${id}:media`, 'items'], Immutable.List()), state => state.get('statuses'), ], (statusIds, statuses) => { let medias = Immutable.List(); |