Commit f21e7d6a authored by Eugen Rochko's avatar Eugen Rochko

Make profile header scroll along with contents. AccountTimeline, Followers and...

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 />
parent a2a85e85
......@@ -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');
}
};
};
......
......@@ -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}
......
......@@ -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} />;
})}
......
......@@ -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} />
......
......@@ -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) {
......
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);
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;
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);
......@@ -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>
);
}
});
......
......@@ -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>
);
}
......
......@@ -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>
);
}
......
......@@ -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 () {
......
......@@ -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) {
......
......@@ -169,12 +169,6 @@
}
}
@media screen and (max-height: 480px) {