commit: f21e7d6ac06556671c2663ce2879442c60230b32
parent: a2a85e85491110461cbc938abd0f2687f0e51612
Author: Eugen Rochko <eugen@zeonfederated.com>
Date: Mon, 30 Jan 2017 21:40:55 +0100
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 />
Diffstat:
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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;