diff options
10 files changed, 80 insertions, 31 deletions
diff --git a/app/assets/javascripts/components/actions/accounts.jsx b/app/assets/javascripts/components/actions/accounts.jsx index c1c99d6bd..eacbeef06 100644 --- a/app/assets/javascripts/components/actions/accounts.jsx +++ b/app/assets/javascripts/components/actions/accounts.jsx @@ -53,7 +53,7 @@ export function fetchAccount(id) { }; }; -export function fetchAccountTimeline(id) { +export function fetchAccountTimeline(id, replace = false) { return (dispatch, getState) => { dispatch(fetchAccountTimelineRequest(id)); @@ -62,12 +62,12 @@ export function fetchAccountTimeline(id) { let params = ''; - if (newestId !== null) { + if (newestId !== null && !replace) { params = `?since_id=${newestId}`; } api(getState).get(`/api/v1/accounts/${id}/statuses${params}`).then(response => { - dispatch(fetchAccountTimelineSuccess(id, response.data)); + dispatch(fetchAccountTimelineSuccess(id, response.data, replace)); }).catch(error => { dispatch(fetchAccountTimelineFail(id, error)); }); @@ -184,11 +184,12 @@ export function fetchAccountTimelineRequest(id) { }; }; -export function fetchAccountTimelineSuccess(id, statuses) { +export function fetchAccountTimelineSuccess(id, statuses, replace) { return { type: ACCOUNT_TIMELINE_FETCH_SUCCESS, id: id, - statuses: statuses + statuses: statuses, + replace: replace }; }; diff --git a/app/assets/javascripts/components/actions/timelines.jsx b/app/assets/javascripts/components/actions/timelines.jsx index 5258d7103..831065feb 100644 --- a/app/assets/javascripts/components/actions/timelines.jsx +++ b/app/assets/javascripts/components/actions/timelines.jsx @@ -11,11 +11,12 @@ 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 function refreshTimelineSuccess(timeline, statuses) { +export function refreshTimelineSuccess(timeline, statuses, replace) { return { type: TIMELINE_REFRESH_SUCCESS, timeline: timeline, - statuses: statuses + statuses: statuses, + replace: replace }; }; @@ -41,7 +42,7 @@ export function refreshTimelineRequest(timeline) { }; }; -export function refreshTimeline(timeline) { +export function refreshTimeline(timeline, replace = false) { return function (dispatch, getState) { dispatch(refreshTimelineRequest(timeline)); @@ -50,12 +51,12 @@ export function refreshTimeline(timeline) { let params = ''; - if (newestId !== null) { + if (newestId !== null && !replace) { params = `?since_id=${newestId}`; } api(getState).get(`/api/v1/statuses/${timeline}${params}`).then(function (response) { - dispatch(refreshTimelineSuccess(timeline, response.data)); + dispatch(refreshTimelineSuccess(timeline, response.data, replace)); }).catch(function (error) { dispatch(refreshTimelineFail(timeline, error)); }); diff --git a/app/assets/javascripts/components/components/column_back_button.jsx b/app/assets/javascripts/components/components/column_back_button.jsx new file mode 100644 index 000000000..755378ad8 --- /dev/null +++ b/app/assets/javascripts/components/components/column_back_button.jsx @@ -0,0 +1,40 @@ +import PureRenderMixin from 'react-addons-pure-render-mixin'; + +const outerStyle = { + padding: '15px', + fontSize: '16px', + background: '#2f3441', + flex: '0 0 auto', + cursor: 'pointer', + color: '#2b90d9' +}; + +const iconStyle = { + display: 'inline-block', + marginRight: '5px' +}; + +const ColumnBackButton = React.createClass({ + + contextTypes: { + router: React.PropTypes.object + }, + + mixins: [PureRenderMixin], + + handleClick () { + this.context.router.goBack(); + }, + + render () { + return ( + <div onClick={this.handleClick} style={outerStyle}> + <i className='fa fa-fw fa-chevron-left' style={iconStyle} /> + Back + </div> + ); + } + +}); + +export default ColumnBackButton; diff --git a/app/assets/javascripts/components/containers/mastodon.jsx b/app/assets/javascripts/components/containers/mastodon.jsx index 4eb9f83c8..8e1becbda 100644 --- a/app/assets/javascripts/components/containers/mastodon.jsx +++ b/app/assets/javascripts/components/containers/mastodon.jsx @@ -54,9 +54,9 @@ const Mastodon = React.createClass({ return store.dispatch(deleteFromTimelines(data.id)); case 'merge': case 'unmerge': - return store.dispatch(refreshTimeline('home')); + return store.dispatch(refreshTimeline('home', true)); case 'block': - return store.dispatch(refreshTimeline('mentions')); + return store.dispatch(refreshTimeline('mentions', true)); } } diff --git a/app/assets/javascripts/components/features/account/components/header.jsx b/app/assets/javascripts/components/features/account/components/header.jsx index 681b30ca8..d794a0aaf 100644 --- a/app/assets/javascripts/components/features/account/components/header.jsx +++ b/app/assets/javascripts/components/features/account/components/header.jsx @@ -26,16 +26,16 @@ const Header = React.createClass({ return ( <div style={{ flex: '0 0 auto', background: '#2f3441', textAlign: 'center', backgroundImage: `url(${account.get('header')})`, backgroundSize: 'cover', position: 'relative' }}> - <div style={{ background: 'rgba(47, 52, 65, 0.8)', padding: '30px 10px' }}> + <div style={{ background: 'rgba(47, 52, 65, 0.8)', padding: '20px 10px' }}> <a href={account.get('url')} target='_blank' rel='noopener' style={{ display: 'block', color: 'inherit', textDecoration: 'none' }}> - <div style={{ width: '90px', margin: '0 auto', marginBottom: '15px' }}> + <div style={{ width: '90px', margin: '0 auto', marginBottom: '10px' }}> <img src={account.get('avatar')} alt='' style={{ display: 'block', width: '90px', height: '90px', borderRadius: '90px' }} /> </div> <span style={{ display: 'inline-block', color: '#fff', fontSize: '20px', lineHeight: '27px', fontWeight: '500' }}>{displayName}</span> </a> - <span style={{ fontSize: '14px', fontWeight: '400', display: 'block', color: '#2b90d9', marginBottom: '15px' }}>@{account.get('acct')}</span> + <span style={{ fontSize: '14px', fontWeight: '400', display: 'block', color: '#2b90d9', marginBottom: '10px' }}>@{account.get('acct')}</span> <p style={{ color: '#616b86', fontSize: '14px' }}>{account.get('note')}</p> {info} diff --git a/app/assets/javascripts/components/features/account/index.jsx b/app/assets/javascripts/components/features/account/index.jsx index 22e02ff54..83770eb74 100644 --- a/app/assets/javascripts/components/features/account/index.jsx +++ b/app/assets/javascripts/components/features/account/index.jsx @@ -18,6 +18,7 @@ import { import LoadingIndicator from '../../components/loading_indicator'; import ActionBar from './components/action_bar'; import Column from '../ui/components/column'; +import ColumnBackButton from '../../components/column_back_button'; const mapStateToProps = (state, props) => ({ account: getAccount(state, Number(props.params.accountId)), @@ -74,6 +75,7 @@ const Account = React.createClass({ return ( <Column> + <ColumnBackButton /> <Header account={account} me={me} /> <ActionBar account={account} me={me} onFollow={this.handleFollow} onBlock={this.handleBlock} /> diff --git a/app/assets/javascripts/components/features/status/index.jsx b/app/assets/javascripts/components/features/status/index.jsx index cc7a2bfeb..c51fb5d31 100644 --- a/app/assets/javascripts/components/features/status/index.jsx +++ b/app/assets/javascripts/components/features/status/index.jsx @@ -16,6 +16,8 @@ import { getStatusAncestors, getStatusDescendants } from '../../selectors'; +import { ScrollContainer } from 'react-router-scroll'; +import ColumnBackButton from '../../components/column_back_button'; const mapStateToProps = (state, props) => ({ status: getStatus(state, Number(props.params.statusId)), @@ -81,14 +83,18 @@ const Status = React.createClass({ return ( <Column> - <div style={{ overflowY: 'scroll', flex: '1 1 auto' }} className='scrollable'> - <div>{this.renderChildren(ancestors)}</div> + <ColumnBackButton /> - <DetailedStatus status={status} me={me} /> - <ActionBar status={status} me={me} onReply={this.handleReplyClick} onFavourite={this.handleFavouriteClick} onReblog={this.handleReblogClick} onDelete={this.handleDeleteClick} /> + <ScrollContainer scrollKey='thread'> + <div style={{ overflowY: 'scroll', flex: '1 1 auto' }} className='scrollable'> + <div>{this.renderChildren(ancestors)}</div> - <div>{this.renderChildren(descendants)}</div> - </div> + <DetailedStatus status={status} me={me} /> + <ActionBar status={status} me={me} onReply={this.handleReplyClick} onFavourite={this.handleFavouriteClick} onReblog={this.handleReblogClick} onDelete={this.handleDeleteClick} /> + + <div>{this.renderChildren(descendants)}</div> + </div> + </ScrollContainer> </Column> ); } diff --git a/app/assets/javascripts/components/reducers/timelines.jsx b/app/assets/javascripts/components/reducers/timelines.jsx index 06534971d..331cbf59c 100644 --- a/app/assets/javascripts/components/reducers/timelines.jsx +++ b/app/assets/javascripts/components/reducers/timelines.jsx @@ -77,7 +77,7 @@ function normalizeStatus(state, status) { }); }; -function normalizeTimeline(state, timeline, statuses) { +function normalizeTimeline(state, timeline, statuses, replace = false) { let ids = Immutable.List([]); statuses.forEach((status, i) => { @@ -85,7 +85,7 @@ function normalizeTimeline(state, timeline, statuses) { ids = ids.set(i, status.get('id')); }); - return state.update(timeline, list => list.unshift(...ids)); + return state.update(timeline, list => (replace ? ids : list.unshift(...ids))); }; function appendNormalizedTimeline(state, timeline, statuses) { @@ -99,7 +99,7 @@ function appendNormalizedTimeline(state, timeline, statuses) { return state.update(timeline, list => list.push(...moreIds)); }; -function normalizeAccountTimeline(state, accountId, statuses) { +function normalizeAccountTimeline(state, accountId, statuses, replace = false) { let ids = Immutable.List([]); statuses.forEach((status, i) => { @@ -107,7 +107,7 @@ function normalizeAccountTimeline(state, accountId, statuses) { ids = ids.set(i, status.get('id')); }); - return state.updateIn(['accounts_timelines', accountId], Immutable.List([]), list => list.unshift(...ids)); + return state.updateIn(['accounts_timelines', accountId], Immutable.List([]), list => (replace ? ids : list.unshift(...ids))); }; function appendNormalizedAccountTimeline(state, accountId, statuses) { @@ -217,7 +217,7 @@ function normalizeSuggestions(state, accounts) { export default function timelines(state = initialState, action) { switch(action.type) { case TIMELINE_REFRESH_SUCCESS: - return normalizeTimeline(state, action.timeline, Immutable.fromJS(action.statuses)); + return normalizeTimeline(state, action.timeline, Immutable.fromJS(action.statuses), action.replace); case TIMELINE_EXPAND_SUCCESS: return appendNormalizedTimeline(state, action.timeline, Immutable.fromJS(action.statuses)); case TIMELINE_UPDATE: @@ -243,7 +243,7 @@ export default function timelines(state = initialState, action) { case STATUS_FETCH_SUCCESS: return normalizeContext(state, Immutable.fromJS(action.status), Immutable.fromJS(action.context.ancestors), Immutable.fromJS(action.context.descendants)); case ACCOUNT_TIMELINE_FETCH_SUCCESS: - return normalizeAccountTimeline(state, action.id, Immutable.fromJS(action.statuses)); + return normalizeAccountTimeline(state, action.id, Immutable.fromJS(action.statuses), action.replace); case ACCOUNT_TIMELINE_EXPAND_SUCCESS: return appendNormalizedAccountTimeline(state, action.id, Immutable.fromJS(action.statuses)); case SUGGESTIONS_FETCH_SUCCESS: diff --git a/app/controllers/api_controller.rb b/app/controllers/api_controller.rb index c4ad450cf..e29892cbe 100644 --- a/app/controllers/api_controller.rb +++ b/app/controllers/api_controller.rb @@ -37,7 +37,7 @@ class ApiController < ApplicationController end def set_maps(statuses) - status_ids = statuses.flat_map { |s| [s.id, s.reblog_of_id] }.compact + status_ids = statuses.flat_map { |s| [s.id, s.reblog_of_id] }.compact.uniq @reblogs_map = Status.reblogs_map(status_ids, current_user.account) @favourites_map = Status.favourites_map(status_ids, current_user.account) end diff --git a/app/models/feed.rb b/app/models/feed.rb index 4466ea14e..2bc9e980a 100644 --- a/app/models/feed.rb +++ b/app/models/feed.rb @@ -8,13 +8,12 @@ class Feed max_id = '+inf' if max_id.blank? since_id = '-inf' if since_id.blank? unhydrated = redis.zrevrangebyscore(key, "(#{max_id}", "(#{since_id}", limit: [0, limit], with_scores: true).collect(&:last).map(&:to_i) - status_map = {} # If we're after most recent items and none are there, we need to precompute the feed - if unhydrated.empty? && max_id == '+inf' + if unhydrated.empty? && max_id == '+inf' && since_id == '-inf' PrecomputeFeedService.new.call(@type, @account, limit) else - Status.where(id: unhydrated).with_includes.with_counters.each { |status| status_map[status.id] = status } + status_map = Status.where(id: unhydrated).with_includes.with_counters.map { |status| [status.id, status] }.to_h unhydrated.map { |id| status_map[id] }.compact end end |