about summary refs log tree commit diff
diff options
context:
space:
mode:
authorEugen Rochko <eugen@zeonfederated.com>2017-01-30 21:40:55 +0100
committerEugen Rochko <eugen@zeonfederated.com>2017-01-30 21:44:11 +0100
commitf21e7d6ac06556671c2663ce2879442c60230b32 (patch)
tree68260b8383038daf6f315093064a2dbdd7b3944b
parenta2a85e85491110461cbc938abd0f2687f0e51612 (diff)
Make profile header scroll along with contents. AccountTimeline, Followers and Following are no longer
nested inside a common parent (<Account>), instead they all embed <HeaderContainer />
-rw-r--r--app/assets/javascripts/components/actions/compose.jsx14
-rw-r--r--app/assets/javascripts/components/components/autosuggest_textarea.jsx65
-rw-r--r--app/assets/javascripts/components/components/status_list.jsx7
-rw-r--r--app/assets/javascripts/components/containers/mastodon.jsx9
-rw-r--r--app/assets/javascripts/components/containers/status_container.jsx5
-rw-r--r--app/assets/javascripts/components/features/account/index.jsx109
-rw-r--r--app/assets/javascripts/components/features/account_timeline/components/header.jsx59
-rw-r--r--app/assets/javascripts/components/features/account_timeline/containers/header_container.jsx45
-rw-r--r--app/assets/javascripts/components/features/account_timeline/index.jsx23
-rw-r--r--app/assets/javascripts/components/features/followers/index.jsx32
-rw-r--r--app/assets/javascripts/components/features/following/index.jsx32
-rw-r--r--app/assets/javascripts/components/features/status/components/action_bar.jsx6
-rw-r--r--app/assets/javascripts/components/features/status/index.jsx8
-rw-r--r--app/assets/stylesheets/components.scss6
14 files changed, 234 insertions, 186 deletions
diff --git a/app/assets/javascripts/components/actions/compose.jsx b/app/assets/javascripts/components/actions/compose.jsx
index 6d0188166..e11d1e537 100644
--- a/app/assets/javascripts/components/actions/compose.jsx
+++ b/app/assets/javascripts/components/actions/compose.jsx
@@ -54,10 +54,16 @@ export function cancelReplyCompose() {
   };
 };
 
-export function mentionCompose(account) {
-  return {
-    type: COMPOSE_MENTION,
-    account: account
+export function mentionCompose(account, router) {
+  return (dispatch, getState) => {
+    dispatch({
+      type: COMPOSE_MENTION,
+      account: account
+    });
+
+    if (!getState().getIn(['compose', 'mounted'])) {
+      router.push('/statuses/new');
+    }
   };
 };
 
diff --git a/app/assets/javascripts/components/components/autosuggest_textarea.jsx b/app/assets/javascripts/components/components/autosuggest_textarea.jsx
index 81ec7a236..4e4c2090c 100644
--- a/app/assets/javascripts/components/components/autosuggest_textarea.jsx
+++ b/app/assets/javascripts/components/components/autosuggest_textarea.jsx
@@ -56,7 +56,7 @@ const AutosuggestTextarea = React.createClass({
   onChange (e) {
     const [ tokenStart, token ] = textAtCursorMatchesToken(e.target.value, e.target.selectionStart);
 
-    if (token != null && this.state.lastToken !== token) {
+    if (token !== null && this.state.lastToken !== token) {
       this.setState({ lastToken: token, selectedSuggestion: 0, tokenStart });
       this.props.onSuggestionsFetchRequested(token);
     } else if (token === null) {
@@ -77,37 +77,37 @@ const AutosuggestTextarea = React.createClass({
     }
 
     switch(e.key) {
-      case 'Escape':
-        if (!suggestionsHidden) {
-          e.preventDefault();
-          this.setState({ suggestionsHidden: true });
-        }
-
-        break;
-      case 'ArrowDown':
-        if (suggestions.size > 0 && !suggestionsHidden) {
-          e.preventDefault();
-          this.setState({ selectedSuggestion: Math.min(selectedSuggestion + 1, suggestions.size - 1) });
-        }
-
-        break;
-      case 'ArrowUp':
-        if (suggestions.size > 0 && !suggestionsHidden) {
-          e.preventDefault();
-          this.setState({ selectedSuggestion: Math.max(selectedSuggestion - 1, 0) });
-        }
-
-        break;
-      case 'Enter':
-      case 'Tab':
-        // Select suggestion
-        if (this.state.lastToken != null && suggestions.size > 0 && !suggestionsHidden) {
-          e.preventDefault();
-          e.stopPropagation();
-          this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestions.get(selectedSuggestion));
-        }
-
-        break;
+    case 'Escape':
+      if (!suggestionsHidden) {
+        e.preventDefault();
+        this.setState({ suggestionsHidden: true });
+      }
+
+      break;
+    case 'ArrowDown':
+      if (suggestions.size > 0 && !suggestionsHidden) {
+        e.preventDefault();
+        this.setState({ selectedSuggestion: Math.min(selectedSuggestion + 1, suggestions.size - 1) });
+      }
+
+      break;
+    case 'ArrowUp':
+      if (suggestions.size > 0 && !suggestionsHidden) {
+        e.preventDefault();
+        this.setState({ selectedSuggestion: Math.max(selectedSuggestion - 1, 0) });
+      }
+
+      break;
+    case 'Enter':
+    case 'Tab':
+      // Select suggestion
+      if (this.state.lastToken !== null && suggestions.size > 0 && !suggestionsHidden) {
+        e.preventDefault();
+        e.stopPropagation();
+        this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestions.get(selectedSuggestion));
+      }
+
+      break;
     }
 
     if (e.defaultPrevented || !this.props.onKeyDown) {
@@ -184,6 +184,7 @@ const AutosuggestTextarea = React.createClass({
           className={className}
           disabled={disabled}
           placeholder={placeholder}
+          autoFocus={true}
           value={value}
           onChange={this.onChange}
           onKeyDown={this.onKeyDown}
diff --git a/app/assets/javascripts/components/components/status_list.jsx b/app/assets/javascripts/components/components/status_list.jsx
index 8223a312c..0e64f0ee6 100644
--- a/app/assets/javascripts/components/components/status_list.jsx
+++ b/app/assets/javascripts/components/components/status_list.jsx
@@ -13,7 +13,8 @@ const StatusList = React.createClass({
     onScrollToTop: React.PropTypes.func,
     onScroll: React.PropTypes.func,
     trackScroll: React.PropTypes.bool,
-    isLoading: React.PropTypes.bool
+    isLoading: React.PropTypes.bool,
+    prepend: React.PropTypes.node
   },
 
   getDefaultProps () {
@@ -70,7 +71,7 @@ const StatusList = React.createClass({
   },
 
   render () {
-    const { statusIds, onScrollToBottom, trackScroll, isLoading } = this.props;
+    const { statusIds, onScrollToBottom, trackScroll, isLoading, prepend } = this.props;
 
     let loadMore = '';
 
@@ -81,6 +82,8 @@ const StatusList = React.createClass({
     const scrollableArea = (
       <div className='scrollable' ref={this.setRef}>
         <div>
+          {prepend}
+
           {statusIds.map((statusId) => {
             return <StatusContainer key={statusId} id={statusId} />;
           })}
diff --git a/app/assets/javascripts/components/containers/mastodon.jsx b/app/assets/javascripts/components/containers/mastodon.jsx
index 5f4b2cf79..839f7267e 100644
--- a/app/assets/javascripts/components/containers/mastodon.jsx
+++ b/app/assets/javascripts/components/containers/mastodon.jsx
@@ -18,7 +18,6 @@ import {
 } from 'react-router';
 import { useScroll } from 'react-router-scroll';
 import UI from '../features/ui';
-import Account from '../features/account';
 import Status from '../features/status';
 import GettingStarted from '../features/getting_started';
 import PublicTimeline from '../features/public_timeline';
@@ -121,11 +120,9 @@ const Mastodon = React.createClass({
               <Route path='statuses/:statusId/reblogs' component={Reblogs} />
               <Route path='statuses/:statusId/favourites' component={Favourites} />
 
-              <Route path='accounts/:accountId' component={Account}>
-                <IndexRoute component={AccountTimeline} />
-                <Route path='followers' component={Followers} />
-                <Route path='following' component={Following} />
-              </Route>
+              <Route path='accounts/:accountId' component={AccountTimeline} />
+              <Route path='accounts/:accountId/followers' component={Followers} />
+              <Route path='accounts/:accountId/following' component={Following} />
 
               <Route path='follow_requests' component={FollowRequests} />
               <Route path='*' component={GenericNotFound} />
diff --git a/app/assets/javascripts/components/containers/status_container.jsx b/app/assets/javascripts/components/containers/status_container.jsx
index ad2be03d1..1704a8cc2 100644
--- a/app/assets/javascripts/components/containers/status_container.jsx
+++ b/app/assets/javascripts/components/containers/status_container.jsx
@@ -88,10 +88,7 @@ const mapDispatchToProps = (dispatch) => ({
   },
 
   onMention (account, router) {
-    dispatch(mentionCompose(account));
-    if (isMobile(window.innerWidth)) {
-      router.push('/statuses/new');
-    }
+    dispatch(mentionCompose(account, router));
   },
 
   onOpenMedia (url) {
diff --git a/app/assets/javascripts/components/features/account/index.jsx b/app/assets/javascripts/components/features/account/index.jsx
deleted file mode 100644
index 3a9b48f21..000000000
--- a/app/assets/javascripts/components/features/account/index.jsx
+++ /dev/null
@@ -1,109 +0,0 @@
-import { connect }           from 'react-redux';
-import PureRenderMixin       from 'react-addons-pure-render-mixin';
-import ImmutablePropTypes    from 'react-immutable-proptypes';
-import {
-  fetchAccount,
-  followAccount,
-  unfollowAccount,
-  blockAccount,
-  unblockAccount,
-  fetchAccountTimeline,
-  expandAccountTimeline
-}                            from '../../actions/accounts';
-import { mentionCompose }    from '../../actions/compose';
-import Header                from './components/header';
-import {
-  getAccountTimeline,
-  makeGetAccount
-}                            from '../../selectors';
-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';
-import { isMobile } from '../../is_mobile'
-
-const makeMapStateToProps = () => {
-  const getAccount = makeGetAccount();
-
-  const mapStateToProps = (state, props) => ({
-    account: getAccount(state, Number(props.params.accountId)),
-    me: state.getIn(['meta', 'me'])
-  });
-
-  return mapStateToProps;
-};
-
-const Account = React.createClass({
-
-  contextTypes: {
-    router: React.PropTypes.object
-  },
-
-  propTypes: {
-    params: React.PropTypes.object.isRequired,
-    dispatch: React.PropTypes.func.isRequired,
-    account: ImmutablePropTypes.map,
-    me: React.PropTypes.number.isRequired,
-    children: React.PropTypes.node
-  },
-
-  mixins: [PureRenderMixin],
-
-  componentWillMount () {
-    this.props.dispatch(fetchAccount(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)));
-    }
-  },
-
-  handleFollow () {
-    if (this.props.account.getIn(['relationship', 'following'])) {
-      this.props.dispatch(unfollowAccount(this.props.account.get('id')));
-    } else {
-      this.props.dispatch(followAccount(this.props.account.get('id')));
-    }
-  },
-
-  handleBlock () {
-    if (this.props.account.getIn(['relationship', 'blocking'])) {
-      this.props.dispatch(unblockAccount(this.props.account.get('id')));
-    } else {
-      this.props.dispatch(blockAccount(this.props.account.get('id')));
-    }
-  },
-
-  handleMention () {
-    this.props.dispatch(mentionCompose(this.props.account));
-    if (isMobile(window.innerWidth)) {
-      this.context.router.push('/statuses/new');
-    }
-  },
-
-  render () {
-    const { account, me } = this.props;
-
-    if (account === null) {
-      return (
-        <Column>
-          <LoadingIndicator />
-        </Column>
-      );
-    }
-
-    return (
-      <Column>
-        <ColumnBackButton />
-        <Header account={account} me={me} onFollow={this.handleFollow} />
-        <ActionBar account={account} me={me} onBlock={this.handleBlock} onMention={this.handleMention} />
-
-        {this.props.children}
-      </Column>
-    );
-  }
-
-});
-
-export default connect(makeMapStateToProps)(Account);
diff --git a/app/assets/javascripts/components/features/account_timeline/components/header.jsx b/app/assets/javascripts/components/features/account_timeline/components/header.jsx
new file mode 100644
index 000000000..ff3e8af2d
--- /dev/null
+++ b/app/assets/javascripts/components/features/account_timeline/components/header.jsx
@@ -0,0 +1,59 @@
+import PureRenderMixin from 'react-addons-pure-render-mixin';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import InnerHeader from '../../account/components/header';
+import ActionBar from '../../account/components/action_bar';
+
+const Header = React.createClass({
+  contextTypes: {
+    router: React.PropTypes.object
+  },
+
+  propTypes: {
+    account: ImmutablePropTypes.map.isRequired,
+    me: React.PropTypes.number.isRequired,
+    onFollow: React.PropTypes.func.isRequired,
+    onBlock: React.PropTypes.func.isRequired,
+    onMention: React.PropTypes.func.isRequired
+  },
+
+  mixins: [PureRenderMixin],
+
+  handleFollow () {
+    this.props.onFollow(this.props.account);
+  },
+
+  handleBlock () {
+    this.props.onBlock(this.props.account);
+  },
+
+  handleMention () {
+    this.props.onMention(this.props.account, this.context.router);
+  },
+
+  render () {
+    const { account, me } = this.props;
+
+    if (!account) {
+      return null;
+    }
+
+    return (
+      <div>
+        <InnerHeader
+          account={account}
+          me={me}
+          onFollow={this.handleFollow}
+        />
+
+        <ActionBar
+          account={account}
+          me={me}
+          onBlock={this.handleBlock}
+          onMention={this.handleMention}
+        />
+      </div>
+    );
+  }
+});
+
+export default Header;
diff --git a/app/assets/javascripts/components/features/account_timeline/containers/header_container.jsx b/app/assets/javascripts/components/features/account_timeline/containers/header_container.jsx
new file mode 100644
index 000000000..dca826596
--- /dev/null
+++ b/app/assets/javascripts/components/features/account_timeline/containers/header_container.jsx
@@ -0,0 +1,45 @@
+import { connect } from 'react-redux';
+import { makeGetAccount } from '../../../selectors';
+import Header from '../components/header';
+import {
+  followAccount,
+  unfollowAccount,
+  blockAccount,
+  unblockAccount
+} from '../../../actions/accounts';
+import { mentionCompose } from '../../../actions/compose';
+
+const makeMapStateToProps = () => {
+  const getAccount = makeGetAccount();
+
+  const mapStateToProps = (state, { accountId }) => ({
+    account: getAccount(state, Number(accountId)),
+    me: state.getIn(['meta', 'me'])
+  });
+
+  return mapStateToProps;
+};
+
+const mapDispatchToProps = dispatch => ({
+  onFollow (account) {
+    if (account.getIn(['relationship', 'following'])) {
+      dispatch(unfollowAccount(account.get('id')));
+    } else {
+      dispatch(followAccount(account.get('id')));
+    }
+  },
+
+  onBlock (account) {
+    if (account.getIn(['relationship', 'blocking'])) {
+      dispatch(unblockAccount(account.get('id')));
+    } else {
+      dispatch(blockAccount(account.get('id')));
+    }
+  },
+
+  onMention (account, router) {
+    dispatch(mentionCompose(account, router));
+  }
+});
+
+export default connect(makeMapStateToProps, mapDispatchToProps)(Header);
diff --git a/app/assets/javascripts/components/features/account_timeline/index.jsx b/app/assets/javascripts/components/features/account_timeline/index.jsx
index 5c09839f7..6e2356dc1 100644
--- a/app/assets/javascripts/components/features/account_timeline/index.jsx
+++ b/app/assets/javascripts/components/features/account_timeline/index.jsx
@@ -7,6 +7,9 @@ import {
 } from '../../actions/accounts';
 import StatusList from '../../components/status_list';
 import LoadingIndicator from '../../components/loading_indicator';
+import Column from '../ui/components/column';
+import HeaderContainer from './containers/header_container';
+import ColumnBackButton from '../../components/column_back_button';
 
 const mapStateToProps = (state, props) => ({
   statusIds: state.getIn(['timelines', 'accounts_timelines', Number(props.params.accountId), 'items']),
@@ -44,10 +47,26 @@ const AccountTimeline = React.createClass({
     const { statusIds, isLoading, me } = this.props;
 
     if (!statusIds) {
-      return <LoadingIndicator />;
+      return (
+        <Column>
+          <LoadingIndicator />
+        </Column>
+      );
     }
 
-    return <StatusList statusIds={statusIds} isLoading={isLoading} me={me} onScrollToBottom={this.handleScrollToBottom} />
+    return (
+      <Column>
+        <ColumnBackButton />
+
+        <StatusList
+          prepend={<HeaderContainer accountId={this.props.params.accountId} />}
+          statusIds={statusIds}
+          isLoading={isLoading}
+          me={me}
+          onScrollToBottom={this.handleScrollToBottom}
+        />
+      </Column>
+    );
   }
 
 });
diff --git a/app/assets/javascripts/components/features/followers/index.jsx b/app/assets/javascripts/components/features/followers/index.jsx
index 38755d862..b96516813 100644
--- a/app/assets/javascripts/components/features/followers/index.jsx
+++ b/app/assets/javascripts/components/features/followers/index.jsx
@@ -8,6 +8,10 @@ import {
 } from '../../actions/accounts';
 import { ScrollContainer } from 'react-router-scroll';
 import AccountContainer from '../../containers/account_container';
+import Column from '../ui/components/column';
+import HeaderContainer from '../account_timeline/containers/header_container';
+import LoadMore from '../../components/load_more';
+import ColumnBackButton from '../../components/column_back_button';
 
 const mapStateToProps = (state, props) => ({
   accountIds: state.getIn(['user_lists', 'followers', Number(props.params.accountId), 'items'])
@@ -41,21 +45,35 @@ const Followers = React.createClass({
     }
   },
 
+  handleLoadMore (e) {
+    e.preventDefault();
+    this.props.dispatch(expandFollowing(Number(this.props.params.accountId)));
+  },
+
   render () {
     const { accountIds } = this.props;
 
     if (!accountIds) {
-      return <LoadingIndicator />;
+      return (
+        <Column>
+          <LoadingIndicator />
+        </Column>
+      );
     }
 
     return (
-      <ScrollContainer scrollKey='followers'>
-        <div className='scrollable' onScroll={this.handleScroll}>
-          <div>
-            {accountIds.map(id => <AccountContainer key={id} id={id} withNote={false} />)}
+      <Column>
+        <ColumnBackButton />
+        <ScrollContainer scrollKey='followers'>
+          <div className='scrollable' onScroll={this.handleScroll}>
+            <div>
+              <HeaderContainer accountId={this.props.params.accountId} />
+              {accountIds.map(id => <AccountContainer key={id} id={id} withNote={false} />)}
+              <LoadMore onClick={this.handleLoadMore} />
+            </div>
           </div>
-        </div>
-      </ScrollContainer>
+        </ScrollContainer>
+      </Column>
     );
   }
 
diff --git a/app/assets/javascripts/components/features/following/index.jsx b/app/assets/javascripts/components/features/following/index.jsx
index c4ec7bb67..559911a7d 100644
--- a/app/assets/javascripts/components/features/following/index.jsx
+++ b/app/assets/javascripts/components/features/following/index.jsx
@@ -8,6 +8,10 @@ import {
 } from '../../actions/accounts';
 import { ScrollContainer } from 'react-router-scroll';
 import AccountContainer from '../../containers/account_container';
+import Column from '../ui/components/column';
+import HeaderContainer from '../account_timeline/containers/header_container';
+import LoadMore from '../../components/load_more';
+import ColumnBackButton from '../../components/column_back_button';
 
 const mapStateToProps = (state, props) => ({
   accountIds: state.getIn(['user_lists', 'following', Number(props.params.accountId), 'items'])
@@ -41,21 +45,35 @@ const Following = React.createClass({
     }
   },
 
+  handleLoadMore (e) {
+    e.preventDefault();
+    this.props.dispatch(expandFollowing(Number(this.props.params.accountId)));
+  },
+
   render () {
     const { accountIds } = this.props;
 
     if (!accountIds) {
-      return <LoadingIndicator />;
+      return (
+        <Column>
+          <LoadingIndicator />
+        </Column>
+      );
     }
 
     return (
-      <ScrollContainer scrollKey='following'>
-        <div className='scrollable' onScroll={this.handleScroll}>
-          <div>
-            {accountIds.map(id => <AccountContainer key={id} id={id} withNote={false} />)}
+      <Column>
+        <ColumnBackButton />
+        <ScrollContainer scrollKey='following'>
+          <div className='scrollable' onScroll={this.handleScroll}>
+            <div>
+              <HeaderContainer accountId={this.props.params.accountId} />
+              {accountIds.map(id => <AccountContainer key={id} id={id} withNote={false} />)}
+              <LoadMore onClick={this.handleLoadMore} />
+            </div>
           </div>
-        </div>
-      </ScrollContainer>
+        </ScrollContainer>
+      </Column>
     );
   }
 
diff --git a/app/assets/javascripts/components/features/status/components/action_bar.jsx b/app/assets/javascripts/components/features/status/components/action_bar.jsx
index 3f8a0457d..2f152e919 100644
--- a/app/assets/javascripts/components/features/status/components/action_bar.jsx
+++ b/app/assets/javascripts/components/features/status/components/action_bar.jsx
@@ -14,6 +14,10 @@ const messages = defineMessages({
 
 const ActionBar = React.createClass({
 
+  contextTypes: {
+    router: React.PropTypes.object
+  },
+
   propTypes: {
     status: ImmutablePropTypes.map.isRequired,
     onReply: React.PropTypes.func.isRequired,
@@ -43,7 +47,7 @@ const ActionBar = React.createClass({
   },
 
   handleMentionClick () {
-    this.props.onMention(this.props.status.get('account'));
+    this.props.onMention(this.props.status.get('account'), this.context.router);
   },
 
   render () {
diff --git a/app/assets/javascripts/components/features/status/index.jsx b/app/assets/javascripts/components/features/status/index.jsx
index 389549849..993c649d2 100644
--- a/app/assets/javascripts/components/features/status/index.jsx
+++ b/app/assets/javascripts/components/features/status/index.jsx
@@ -80,12 +80,8 @@ const Status = React.createClass({
     this.props.dispatch(deleteStatus(status.get('id')));
   },
 
-  handleMentionClick (account) {
-    this.props.dispatch(mentionCompose(account));
-
-    if (isMobile(window.innerWidth)) {
-      this.context.router.push('/statuses/new');
-    }
+  handleMentionClick (account, router) {
+    this.props.dispatch(mentionCompose(account, router));
   },
 
   handleOpenMedia (url) {
diff --git a/app/assets/stylesheets/components.scss b/app/assets/stylesheets/components.scss
index 6810fae12..471fff11e 100644
--- a/app/assets/stylesheets/components.scss
+++ b/app/assets/stylesheets/components.scss
@@ -169,12 +169,6 @@
   }
 }
 
-@media screen and (max-height: 480px) {
-  .account__header__avatar, .account__header .account__header__content {
-    display: none;
-  }
-}
-
 .account__header__content {
   word-wrap: break-word;
   font-weight: 400;