Hide some components rather than unmounting them to allow to show again quickly and keep the view state such as the scrolled offset.master
@@ -60,7 +60,7 @@ class StatusList extends React.PureComponent { | |||
} | |||
render () { | |||
const { statusIds, onScrollToBottom, trackScroll, isLoading, isUnread, hasMore, prepend, emptyMessage } = this.props; | |||
const { statusIds, onScrollToBottom, scrollKey, shouldUpdateScroll, isLoading, isUnread, hasMore, prepend, emptyMessage } = this.props; | |||
let loadMore = ''; | |||
let scrollableArea = ''; | |||
@@ -98,25 +98,22 @@ class StatusList extends React.PureComponent { | |||
); | |||
} | |||
if (trackScroll) { | |||
return ( | |||
<ScrollContainer scrollKey='status-list'> | |||
{scrollableArea} | |||
</ScrollContainer> | |||
); | |||
} else { | |||
return scrollableArea; | |||
} | |||
return ( | |||
<ScrollContainer scrollKey={scrollKey} shouldUpdateScroll={shouldUpdateScroll}> | |||
{scrollableArea} | |||
</ScrollContainer> | |||
); | |||
} | |||
} | |||
StatusList.propTypes = { | |||
scrollKey: PropTypes.string.isRequired, | |||
statusIds: ImmutablePropTypes.list.isRequired, | |||
onScrollToBottom: PropTypes.func, | |||
onScrollToTop: PropTypes.func, | |||
onScroll: PropTypes.func, | |||
trackScroll: PropTypes.bool, | |||
shouldUpdateScroll: PropTypes.func, | |||
isLoading: PropTypes.bool, | |||
isUnread: PropTypes.bool, | |||
hasMore: PropTypes.bool, | |||
@@ -99,6 +99,125 @@ addLocaleData([ | |||
...id, | |||
]); | |||
const getTopWhenReplacing = (previous, { location }) => location && location.action === 'REPLACE' && [0, 0]; | |||
const hiddenColumnContainerStyle = { | |||
position: 'absolute', | |||
left: '0', | |||
top: '0', | |||
visibility: 'hidden' | |||
}; | |||
class Container extends React.PureComponent { | |||
constructor(props) { | |||
super(props); | |||
this.state = { | |||
renderedPersistents: [], | |||
unrenderedPersistents: [], | |||
}; | |||
} | |||
componentWillMount () { | |||
this.unlistenHistory = null; | |||
this.setState(() => { | |||
return { | |||
mountImpersistent: false, | |||
renderedPersistents: [], | |||
unrenderedPersistents: [ | |||
{pathname: '/timelines/home', component: HomeTimeline}, | |||
{pathname: '/timelines/public', component: PublicTimeline}, | |||
{pathname: '/timelines/public/local', component: CommunityTimeline}, | |||
{pathname: '/notifications', component: Notifications}, | |||
{pathname: '/favourites', component: FavouritedStatuses} | |||
], | |||
}; | |||
}, () => { | |||
if (this.unlistenHistory) { | |||
return; | |||
} | |||
this.unlistenHistory = browserHistory.listen(location => { | |||
const pathname = location.pathname.replace(/\/$/, '').toLowerCase(); | |||
this.setState(oldState => { | |||
let persistentMatched = false; | |||
const newState = { | |||
renderedPersistents: oldState.renderedPersistents.map(persistent => { | |||
const givenMatched = persistent.pathname === pathname; | |||
if (givenMatched) { | |||
persistentMatched = true; | |||
} | |||
return { | |||
hidden: !givenMatched, | |||
pathname: persistent.pathname, | |||
component: persistent.component | |||
}; | |||
}), | |||
}; | |||
if (!persistentMatched) { | |||
newState.unrenderedPersistents = []; | |||
oldState.unrenderedPersistents.forEach(persistent => { | |||
if (persistent.pathname === pathname) { | |||
persistentMatched = true; | |||
newState.renderedPersistents.push({ | |||
hidden: false, | |||
pathname: persistent.pathname, | |||
component: persistent.component | |||
}); | |||
} else { | |||
newState.unrenderedPersistents.push(persistent); | |||
} | |||
}); | |||
} | |||
newState.mountImpersistent = !persistentMatched; | |||
return newState; | |||
}); | |||
}); | |||
}); | |||
} | |||
componentWillUnmount () { | |||
if (this.unlistenHistory) { | |||
this.unlistenHistory(); | |||
} | |||
this.unlistenHistory = "done"; | |||
} | |||
render () { | |||
// Hide some components rather than unmounting them to allow to show again | |||
// quickly and keep the view state such as the scrolled offset. | |||
const persistentsView = this.state.renderedPersistents.map((persistent) => | |||
<div aria-hidden={persistent.hidden} key={persistent.pathname} className='mastodon-column-container' style={persistent.hidden ? hiddenColumnContainerStyle : null}> | |||
<persistent.component shouldUpdateScroll={persistent.hidden ? Function.prototype : getTopWhenReplacing} /> | |||
</div> | |||
); | |||
return ( | |||
<UI> | |||
{this.state.mountImpersistent && this.props.children} | |||
{persistentsView} | |||
</UI> | |||
); | |||
} | |||
} | |||
Container.propTypes = { | |||
children: PropTypes.node, | |||
}; | |||
class Mastodon extends React.Component { | |||
componentDidMount() { | |||
@@ -160,18 +279,12 @@ class Mastodon extends React.Component { | |||
<IntlProvider locale={locale} messages={getMessagesForLocale(locale)}> | |||
<Provider store={store}> | |||
<Router history={browserHistory} render={applyRouterMiddleware(useScroll())}> | |||
<Route path='/' component={UI}> | |||
<Route path='/' component={Container}> | |||
<IndexRedirect to="/getting-started" /> | |||
<Route path='getting-started' component={GettingStarted} /> | |||
<Route path='timelines/home' component={HomeTimeline} /> | |||
<Route path='timelines/public' component={PublicTimeline} /> | |||
<Route path='timelines/public/local' component={CommunityTimeline} /> | |||
<Route path='timelines/tag/:id' component={HashtagTimeline} /> | |||
<Route path='notifications' component={Notifications} /> | |||
<Route path='favourites' component={FavouritedStatuses} /> | |||
<Route path='statuses/new' component={Compose} /> | |||
<Route path='statuses/:statusId' component={Status} /> | |||
<Route path='statuses/:statusId/reblogs' component={Reblogs} /> | |||
@@ -62,6 +62,7 @@ class AccountTimeline extends React.PureComponent { | |||
<StatusList | |||
prepend={<HeaderContainer accountId={this.props.params.accountId} />} | |||
scrollKey='account_timeline' | |||
statusIds={statusIds} | |||
isLoading={isLoading} | |||
hasMore={hasMore} | |||
@@ -77,7 +77,7 @@ class CommunityTimeline extends React.PureComponent { | |||
return ( | |||
<Column icon='users' active={hasUnread} heading={intl.formatMessage(messages.title)}> | |||
<ColumnBackButtonSlim /> | |||
<StatusListContainer type='community' emptyMessage={<FormattedMessage id='empty_column.community' defaultMessage='The local timeline is empty. Write something publicly to get the ball rolling!' />} /> | |||
<StatusListContainer {...this.props} scrollKey='community_timeline' type='community' emptyMessage={<FormattedMessage id='empty_column.community' defaultMessage='The local timeline is empty. Write something publicly to get the ball rolling!' />} /> | |||
</Column> | |||
); | |||
} | |||
@@ -47,7 +47,7 @@ class Favourites extends React.PureComponent { | |||
return ( | |||
<Column icon='star' heading={intl.formatMessage(messages.heading)}> | |||
<ColumnBackButtonSlim /> | |||
<StatusList statusIds={statusIds} me={me} onScrollToBottom={this.handleScrollToBottom} /> | |||
<StatusList {...this.props} onScrollToBottom={this.handleScrollToBottom} /> | |||
</Column> | |||
); | |||
} | |||
@@ -71,7 +71,7 @@ class HashtagTimeline extends React.PureComponent { | |||
return ( | |||
<Column icon='hashtag' active={hasUnread} heading={id}> | |||
<ColumnBackButtonSlim /> | |||
<StatusListContainer type='tag' id={id} emptyMessage={<FormattedMessage id='empty_column.hashtag' defaultMessage='There is nothing in this hashtag yet.' />} /> | |||
<StatusListContainer scrollKey='hashtag_timeline' type='tag' id={id} emptyMessage={<FormattedMessage id='empty_column.hashtag' defaultMessage='There is nothing in this hashtag yet.' />} /> | |||
</Column> | |||
); | |||
} | |||
@@ -22,7 +22,7 @@ class HomeTimeline extends React.PureComponent { | |||
return ( | |||
<Column icon='home' active={hasUnread} heading={intl.formatMessage(messages.title)}> | |||
<ColumnSettingsContainer /> | |||
<StatusListContainer {...this.props} type='home' emptyMessage={<FormattedMessage id='empty_column.home' defaultMessage="You aren't following anyone yet. Visit {public} or use search to get started and meet other users." values={{ public: <Link to='/timelines/public'><FormattedMessage id='empty_column.home.public_timeline' defaultMessage='the public timeline' /></Link> }} />} /> | |||
<StatusListContainer {...this.props} scrollKey='home_timeline' type='home' emptyMessage={<FormattedMessage id='empty_column.home' defaultMessage="You aren't following anyone yet. Visit {public} or use search to get started and meet other users." values={{ public: <Link to='/timelines/public'><FormattedMessage id='empty_column.home.public_timeline' defaultMessage='the public timeline' /></Link> }} />} /> | |||
</Column> | |||
); | |||
} | |||
@@ -80,7 +80,7 @@ class Notifications extends React.PureComponent { | |||
} | |||
render () { | |||
const { intl, notifications, trackScroll, isLoading, isUnread } = this.props; | |||
const { intl, notifications, shouldUpdateScroll, isLoading, isUnread } = this.props; | |||
let loadMore = ''; | |||
let scrollableArea = ''; | |||
@@ -113,25 +113,15 @@ class Notifications extends React.PureComponent { | |||
); | |||
} | |||
if (trackScroll) { | |||
return ( | |||
<Column icon='bell' active={isUnread} heading={intl.formatMessage(messages.title)}> | |||
<ColumnSettingsContainer /> | |||
<ClearColumnButton onClick={this.handleClear} /> | |||
<ScrollContainer scrollKey='notifications'> | |||
{scrollableArea} | |||
</ScrollContainer> | |||
</Column> | |||
); | |||
} else { | |||
return ( | |||
<Column icon='bell' active={isUnread} heading={intl.formatMessage(messages.title)}> | |||
<ColumnSettingsContainer /> | |||
<ClearColumnButton onClick={this.handleClear} /> | |||
return ( | |||
<Column icon='bell' active={isUnread} heading={intl.formatMessage(messages.title)}> | |||
<ColumnSettingsContainer /> | |||
<ClearColumnButton onClick={this.handleClear} /> | |||
<ScrollContainer scrollKey='notifications' shouldUpdateScroll={shouldUpdateScroll}> | |||
{scrollableArea} | |||
</Column> | |||
); | |||
} | |||
</ScrollContainer> | |||
</Column> | |||
); | |||
} | |||
} | |||
@@ -139,7 +129,7 @@ class Notifications extends React.PureComponent { | |||
Notifications.propTypes = { | |||
notifications: ImmutablePropTypes.list.isRequired, | |||
dispatch: PropTypes.func.isRequired, | |||
trackScroll: PropTypes.bool, | |||
shouldUpdateScroll: PropTypes.func, | |||
intl: PropTypes.object.isRequired, | |||
isLoading: PropTypes.bool, | |||
isUnread: PropTypes.bool | |||
@@ -77,7 +77,7 @@ class PublicTimeline extends React.PureComponent { | |||
return ( | |||
<Column icon='globe' active={hasUnread} heading={intl.formatMessage(messages.title)}> | |||
<ColumnBackButtonSlim /> | |||
<StatusListContainer type='public' emptyMessage={<FormattedMessage id='empty_column.public' defaultMessage='There is nothing here! Write something publicly, or manually follow users from other instances to fill it up' />} /> | |||
<StatusListContainer {...this.props} type='public' scrollKey='public_timeline' emptyMessage={<FormattedMessage id='empty_column.public' defaultMessage='There is nothing here! Write something publicly, or manually follow users from other instances to fill it up' />} /> | |||
</Column> | |||
); | |||
} | |||
@@ -40,6 +40,8 @@ const makeMapStateToProps = () => { | |||
const getStatusIds = makeGetStatusIds(); | |||
const mapStateToProps = (state, props) => ({ | |||
scrollKey: props.scrollKey, | |||
shouldUpdateScroll: props.shouldUpdateScroll, | |||
statusIds: getStatusIds(state, props), | |||
isLoading: state.getIn(['timelines', props.type, 'isLoading'], true), | |||
isUnread: state.getIn(['timelines', props.type, 'unread']) > 0, | |||
@@ -127,9 +127,9 @@ class UI extends React.PureComponent { | |||
mountedColumns = ( | |||
<ColumnsArea> | |||
<Compose withHeader={true} /> | |||
<HomeTimeline trackScroll={false} /> | |||
<Notifications trackScroll={false} /> | |||
{children} | |||
<HomeTimeline shouldUpdateScroll={() => false} /> | |||
<Notifications shouldUpdateScroll={() => false} /> | |||
<div style={{display: 'flex', flex: '1 1 auto', position: 'relative'}}>{children}</div> | |||
</ColumnsArea> | |||
); | |||
} | |||
@@ -89,11 +89,11 @@ | |||
border: none; | |||
background: transparent; | |||
cursor: pointer; | |||
transition: all 100ms ease-in; | |||
transition: color 100ms ease-in; | |||
&:hover, &:active, &:focus { | |||
color: lighten($color1, 33%); | |||
transition: all 200ms ease-out; | |||
transition: color 200ms ease-out; | |||
} | |||
&.disabled { | |||
@@ -152,11 +152,11 @@ | |||
padding: 0 3px; | |||
line-height: 27px; | |||
outline: 0; | |||
transition: all 100ms ease-in; | |||
transition: color 100ms ease-in; | |||
&:hover, &:active, &:focus { | |||
color: lighten($color1, 26%); | |||
transition: all 200ms ease-out; | |||
transition: color 200ms ease-out; | |||
} | |||
&.disabled { | |||
@@ -1100,6 +1100,7 @@ a.status__content__spoiler-link { | |||
flex-direction: row; | |||
justify-content: flex-start; | |||
overflow-x: auto; | |||
position: relative; | |||
} | |||
@media screen and (min-width: 360px) { | |||
@@ -1257,11 +1258,11 @@ a.status__content__spoiler-link { | |||
flex-direction: row; | |||
a { | |||
transition: all 100ms ease-in; | |||
transition: background 100ms ease-in; | |||
&:hover { | |||
background: lighten($color1, 3%); | |||
transition: all 200ms ease-out; | |||
transition: background 200ms ease-out; | |||
} | |||
} | |||
} | |||
@@ -9,6 +9,16 @@ | |||
} | |||
} | |||
.mastodon-column-container { | |||
display: flex; | |||
height: 100%; | |||
width: 100%; | |||
// 707568 - height 100% doesn't work on child of a flex item - chromium - Monorail | |||
// https://bugs.chromium.org/p/chromium/issues/detail?id=707568 | |||
flex: 1 1 auto; | |||
} | |||
.logo-container { | |||
max-width: 400px; | |||
margin: 100px auto; | |||
@@ -40,7 +50,7 @@ | |||
img { | |||
opacity: 0.8; | |||
transition: all 0.8s ease; | |||
transition: opacity 0.8s ease; | |||
} | |||
&:hover { | |||