From f21e7d6ac06556671c2663ce2879442c60230b32 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Mon, 30 Jan 2017 21:40:55 +0100 Subject: [PATCH] Make profile header scroll along with contents. AccountTimeline, Followers and Following are no longer nested inside a common parent (), instead they all embed --- .../components/actions/compose.jsx | 14 ++- .../components/autosuggest_textarea.jsx | 57 ++++----- .../components/components/status_list.jsx | 7 +- .../components/containers/mastodon.jsx | 9 +- .../containers/status_container.jsx | 5 +- .../components/features/account/index.jsx | 109 ------------------ .../account_timeline/components/header.jsx | 59 ++++++++++ .../containers/header_container.jsx | 45 ++++++++ .../features/account_timeline/index.jsx | 23 +++- .../components/features/followers/index.jsx | 32 +++-- .../components/features/following/index.jsx | 32 +++-- .../features/status/components/action_bar.jsx | 6 +- .../components/features/status/index.jsx | 8 +- app/assets/stylesheets/components.scss | 6 - 14 files changed, 230 insertions(+), 182 deletions(-) delete mode 100644 app/assets/javascripts/components/features/account/index.jsx create mode 100644 app/assets/javascripts/components/features/account_timeline/components/header.jsx create mode 100644 app/assets/javascripts/components/features/account_timeline/containers/header_container.jsx 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 }); - } + 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 '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 '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 '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; + 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 = (
+ {prepend} + {statusIds.map((statusId) => { return ; })} 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({ - - - - - + + + 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 ( - - - - ); - } - - return ( - - -
- - - {this.props.children} - - ); - } - -}); - -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 ( +
+ + + +
+ ); + } +}); + +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 ; + return ( + + + + ); } - return + return ( + + + + } + statusIds={statusIds} + isLoading={isLoading} + me={me} + onScrollToBottom={this.handleScrollToBottom} + /> + + ); } }); 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 ; + return ( + + + + ); } return ( - -
-
- {accountIds.map(id => )} + + + +
+
+ + {accountIds.map(id => )} + +
-
- + + ); } 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 ; + return ( + + + + ); } return ( - -
-
- {accountIds.map(id => )} + + + +
+
+ + {accountIds.map(id => )} + +
-
- + + ); } 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;