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 />
This commit is contained in:
Eugen Rochko 2017-01-30 21:40:55 +01:00
parent a2a85e8549
commit f21e7d6ac0
14 changed files with 230 additions and 182 deletions

View file

@ -54,10 +54,16 @@ export function cancelReplyCompose() {
}; };
}; };
export function mentionCompose(account) { export function mentionCompose(account, router) {
return { return (dispatch, getState) => {
type: COMPOSE_MENTION, dispatch({
account: account type: COMPOSE_MENTION,
account: account
});
if (!getState().getIn(['compose', 'mounted'])) {
router.push('/statuses/new');
}
}; };
}; };

View file

@ -56,7 +56,7 @@ const AutosuggestTextarea = React.createClass({
onChange (e) { onChange (e) {
const [ tokenStart, token ] = textAtCursorMatchesToken(e.target.value, e.target.selectionStart); 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.setState({ lastToken: token, selectedSuggestion: 0, tokenStart });
this.props.onSuggestionsFetchRequested(token); this.props.onSuggestionsFetchRequested(token);
} else if (token === null) { } else if (token === null) {
@ -77,37 +77,37 @@ const AutosuggestTextarea = React.createClass({
} }
switch(e.key) { switch(e.key) {
case 'Escape': case 'Escape':
if (!suggestionsHidden) { if (!suggestionsHidden) {
e.preventDefault(); e.preventDefault();
this.setState({ suggestionsHidden: true }); this.setState({ suggestionsHidden: true });
} }
break; break;
case 'ArrowDown': case 'ArrowDown':
if (suggestions.size > 0 && !suggestionsHidden) { if (suggestions.size > 0 && !suggestionsHidden) {
e.preventDefault(); e.preventDefault();
this.setState({ selectedSuggestion: Math.min(selectedSuggestion + 1, suggestions.size - 1) }); this.setState({ selectedSuggestion: Math.min(selectedSuggestion + 1, suggestions.size - 1) });
} }
break; break;
case 'ArrowUp': case 'ArrowUp':
if (suggestions.size > 0 && !suggestionsHidden) { if (suggestions.size > 0 && !suggestionsHidden) {
e.preventDefault(); e.preventDefault();
this.setState({ selectedSuggestion: Math.max(selectedSuggestion - 1, 0) }); this.setState({ selectedSuggestion: Math.max(selectedSuggestion - 1, 0) });
} }
break; break;
case 'Enter': case 'Enter':
case 'Tab': case 'Tab':
// Select suggestion // Select suggestion
if (this.state.lastToken != null && suggestions.size > 0 && !suggestionsHidden) { if (this.state.lastToken !== null && suggestions.size > 0 && !suggestionsHidden) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestions.get(selectedSuggestion)); this.props.onSuggestionSelected(this.state.tokenStart, this.state.lastToken, suggestions.get(selectedSuggestion));
} }
break; break;
} }
if (e.defaultPrevented || !this.props.onKeyDown) { if (e.defaultPrevented || !this.props.onKeyDown) {
@ -184,6 +184,7 @@ const AutosuggestTextarea = React.createClass({
className={className} className={className}
disabled={disabled} disabled={disabled}
placeholder={placeholder} placeholder={placeholder}
autoFocus={true}
value={value} value={value}
onChange={this.onChange} onChange={this.onChange}
onKeyDown={this.onKeyDown} onKeyDown={this.onKeyDown}

View file

@ -13,7 +13,8 @@ const StatusList = React.createClass({
onScrollToTop: React.PropTypes.func, onScrollToTop: React.PropTypes.func,
onScroll: React.PropTypes.func, onScroll: React.PropTypes.func,
trackScroll: React.PropTypes.bool, trackScroll: React.PropTypes.bool,
isLoading: React.PropTypes.bool isLoading: React.PropTypes.bool,
prepend: React.PropTypes.node
}, },
getDefaultProps () { getDefaultProps () {
@ -70,7 +71,7 @@ const StatusList = React.createClass({
}, },
render () { render () {
const { statusIds, onScrollToBottom, trackScroll, isLoading } = this.props; const { statusIds, onScrollToBottom, trackScroll, isLoading, prepend } = this.props;
let loadMore = ''; let loadMore = '';
@ -81,6 +82,8 @@ const StatusList = React.createClass({
const scrollableArea = ( const scrollableArea = (
<div className='scrollable' ref={this.setRef}> <div className='scrollable' ref={this.setRef}>
<div> <div>
{prepend}
{statusIds.map((statusId) => { {statusIds.map((statusId) => {
return <StatusContainer key={statusId} id={statusId} />; return <StatusContainer key={statusId} id={statusId} />;
})} })}

View file

@ -18,7 +18,6 @@ import {
} from 'react-router'; } from 'react-router';
import { useScroll } from 'react-router-scroll'; import { useScroll } from 'react-router-scroll';
import UI from '../features/ui'; import UI from '../features/ui';
import Account from '../features/account';
import Status from '../features/status'; import Status from '../features/status';
import GettingStarted from '../features/getting_started'; import GettingStarted from '../features/getting_started';
import PublicTimeline from '../features/public_timeline'; 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/reblogs' component={Reblogs} />
<Route path='statuses/:statusId/favourites' component={Favourites} /> <Route path='statuses/:statusId/favourites' component={Favourites} />
<Route path='accounts/:accountId' component={Account}> <Route path='accounts/:accountId' component={AccountTimeline} />
<IndexRoute component={AccountTimeline} /> <Route path='accounts/:accountId/followers' component={Followers} />
<Route path='followers' component={Followers} /> <Route path='accounts/:accountId/following' component={Following} />
<Route path='following' component={Following} />
</Route>
<Route path='follow_requests' component={FollowRequests} /> <Route path='follow_requests' component={FollowRequests} />
<Route path='*' component={GenericNotFound} /> <Route path='*' component={GenericNotFound} />

View file

@ -88,10 +88,7 @@ const mapDispatchToProps = (dispatch) => ({
}, },
onMention (account, router) { onMention (account, router) {
dispatch(mentionCompose(account)); dispatch(mentionCompose(account, router));
if (isMobile(window.innerWidth)) {
router.push('/statuses/new');
}
}, },
onOpenMedia (url) { onOpenMedia (url) {

View file

@ -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);

View file

@ -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;

View file

@ -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);

View file

@ -7,6 +7,9 @@ import {
} from '../../actions/accounts'; } from '../../actions/accounts';
import StatusList from '../../components/status_list'; import StatusList from '../../components/status_list';
import LoadingIndicator from '../../components/loading_indicator'; 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) => ({ const mapStateToProps = (state, props) => ({
statusIds: state.getIn(['timelines', 'accounts_timelines', Number(props.params.accountId), 'items']), 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; const { statusIds, isLoading, me } = this.props;
if (!statusIds) { 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>
);
} }
}); });

View file

@ -8,6 +8,10 @@ import {
} from '../../actions/accounts'; } from '../../actions/accounts';
import { ScrollContainer } from 'react-router-scroll'; import { ScrollContainer } from 'react-router-scroll';
import AccountContainer from '../../containers/account_container'; 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) => ({ const mapStateToProps = (state, props) => ({
accountIds: state.getIn(['user_lists', 'followers', Number(props.params.accountId), 'items']) 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 () { render () {
const { accountIds } = this.props; const { accountIds } = this.props;
if (!accountIds) { if (!accountIds) {
return <LoadingIndicator />; return (
<Column>
<LoadingIndicator />
</Column>
);
} }
return ( return (
<ScrollContainer scrollKey='followers'> <Column>
<div className='scrollable' onScroll={this.handleScroll}> <ColumnBackButton />
<div> <ScrollContainer scrollKey='followers'>
{accountIds.map(id => <AccountContainer key={id} id={id} withNote={false} />)} <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>
</div> </ScrollContainer>
</ScrollContainer> </Column>
); );
} }

View file

@ -8,6 +8,10 @@ import {
} from '../../actions/accounts'; } from '../../actions/accounts';
import { ScrollContainer } from 'react-router-scroll'; import { ScrollContainer } from 'react-router-scroll';
import AccountContainer from '../../containers/account_container'; 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) => ({ const mapStateToProps = (state, props) => ({
accountIds: state.getIn(['user_lists', 'following', Number(props.params.accountId), 'items']) 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 () { render () {
const { accountIds } = this.props; const { accountIds } = this.props;
if (!accountIds) { if (!accountIds) {
return <LoadingIndicator />; return (
<Column>
<LoadingIndicator />
</Column>
);
} }
return ( return (
<ScrollContainer scrollKey='following'> <Column>
<div className='scrollable' onScroll={this.handleScroll}> <ColumnBackButton />
<div> <ScrollContainer scrollKey='following'>
{accountIds.map(id => <AccountContainer key={id} id={id} withNote={false} />)} <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>
</div> </ScrollContainer>
</ScrollContainer> </Column>
); );
} }

View file

@ -14,6 +14,10 @@ const messages = defineMessages({
const ActionBar = React.createClass({ const ActionBar = React.createClass({
contextTypes: {
router: React.PropTypes.object
},
propTypes: { propTypes: {
status: ImmutablePropTypes.map.isRequired, status: ImmutablePropTypes.map.isRequired,
onReply: React.PropTypes.func.isRequired, onReply: React.PropTypes.func.isRequired,
@ -43,7 +47,7 @@ const ActionBar = React.createClass({
}, },
handleMentionClick () { handleMentionClick () {
this.props.onMention(this.props.status.get('account')); this.props.onMention(this.props.status.get('account'), this.context.router);
}, },
render () { render () {

View file

@ -80,12 +80,8 @@ const Status = React.createClass({
this.props.dispatch(deleteStatus(status.get('id'))); this.props.dispatch(deleteStatus(status.get('id')));
}, },
handleMentionClick (account) { handleMentionClick (account, router) {
this.props.dispatch(mentionCompose(account)); this.props.dispatch(mentionCompose(account, router));
if (isMobile(window.innerWidth)) {
this.context.router.push('/statuses/new');
}
}, },
handleOpenMedia (url) { handleOpenMedia (url) {

View file

@ -169,12 +169,6 @@
} }
} }
@media screen and (max-height: 480px) {
.account__header__avatar, .account__header .account__header__content {
display: none;
}
}
.account__header__content { .account__header__content {
word-wrap: break-word; word-wrap: break-word;
font-weight: 400; font-weight: 400;