about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/components/actions/accounts.jsx11
-rw-r--r--app/assets/javascripts/components/actions/timelines.jsx11
-rw-r--r--app/assets/javascripts/components/components/column_back_button.jsx40
-rw-r--r--app/assets/javascripts/components/containers/mastodon.jsx4
-rw-r--r--app/assets/javascripts/components/features/account/components/header.jsx6
-rw-r--r--app/assets/javascripts/components/features/account/index.jsx2
-rw-r--r--app/assets/javascripts/components/features/status/index.jsx18
-rw-r--r--app/assets/javascripts/components/reducers/timelines.jsx12
-rw-r--r--app/controllers/api_controller.rb2
-rw-r--r--app/models/feed.rb5
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