nested inside a common parent (<Account>), instead they all embed <HeaderContainer />master
@@ -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) { | |||
@@ -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); |
@@ -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; |
@@ -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); |
@@ -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) { | |||
.account__header__avatar, .account__header .account__header__content { | |||
display: none; | |||
} | |||
} | |||
.account__header__content { | |||
word-wrap: break-word; | |||
font-weight: 400; | |||