Hide some components rather than unmounting (#2271)
Hide some components rather than unmounting them to allow to show again quickly and keep the view state such as the scrolled offset.gh/stable
parent
72c984e105
commit
cf845fed38
|
@ -60,7 +60,7 @@ class StatusList extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
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 loadMore = '';
|
||||||
let scrollableArea = '';
|
let scrollableArea = '';
|
||||||
|
@ -98,25 +98,22 @@ class StatusList extends React.PureComponent {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (trackScroll) {
|
|
||||||
return (
|
return (
|
||||||
<ScrollContainer scrollKey='status-list'>
|
<ScrollContainer scrollKey={scrollKey} shouldUpdateScroll={shouldUpdateScroll}>
|
||||||
{scrollableArea}
|
{scrollableArea}
|
||||||
</ScrollContainer>
|
</ScrollContainer>
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
return scrollableArea;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
StatusList.propTypes = {
|
StatusList.propTypes = {
|
||||||
|
scrollKey: PropTypes.string.isRequired,
|
||||||
statusIds: ImmutablePropTypes.list.isRequired,
|
statusIds: ImmutablePropTypes.list.isRequired,
|
||||||
onScrollToBottom: PropTypes.func,
|
onScrollToBottom: PropTypes.func,
|
||||||
onScrollToTop: PropTypes.func,
|
onScrollToTop: PropTypes.func,
|
||||||
onScroll: PropTypes.func,
|
onScroll: PropTypes.func,
|
||||||
trackScroll: PropTypes.bool,
|
shouldUpdateScroll: PropTypes.func,
|
||||||
isLoading: PropTypes.bool,
|
isLoading: PropTypes.bool,
|
||||||
isUnread: PropTypes.bool,
|
isUnread: PropTypes.bool,
|
||||||
hasMore: PropTypes.bool,
|
hasMore: PropTypes.bool,
|
||||||
|
|
|
@ -99,6 +99,125 @@ addLocaleData([
|
||||||
...id,
|
...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 {
|
class Mastodon extends React.Component {
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
@ -160,18 +279,12 @@ class Mastodon extends React.Component {
|
||||||
<IntlProvider locale={locale} messages={getMessagesForLocale(locale)}>
|
<IntlProvider locale={locale} messages={getMessagesForLocale(locale)}>
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<Router history={browserHistory} render={applyRouterMiddleware(useScroll())}>
|
<Router history={browserHistory} render={applyRouterMiddleware(useScroll())}>
|
||||||
<Route path='/' component={UI}>
|
<Route path='/' component={Container}>
|
||||||
<IndexRedirect to="/getting-started" />
|
<IndexRedirect to="/getting-started" />
|
||||||
|
|
||||||
<Route path='getting-started' component={GettingStarted} />
|
<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='timelines/tag/:id' component={HashtagTimeline} />
|
||||||
|
|
||||||
<Route path='notifications' component={Notifications} />
|
|
||||||
<Route path='favourites' component={FavouritedStatuses} />
|
|
||||||
|
|
||||||
<Route path='statuses/new' component={Compose} />
|
<Route path='statuses/new' component={Compose} />
|
||||||
<Route path='statuses/:statusId' component={Status} />
|
<Route path='statuses/:statusId' component={Status} />
|
||||||
<Route path='statuses/:statusId/reblogs' component={Reblogs} />
|
<Route path='statuses/:statusId/reblogs' component={Reblogs} />
|
||||||
|
|
|
@ -62,6 +62,7 @@ class AccountTimeline extends React.PureComponent {
|
||||||
|
|
||||||
<StatusList
|
<StatusList
|
||||||
prepend={<HeaderContainer accountId={this.props.params.accountId} />}
|
prepend={<HeaderContainer accountId={this.props.params.accountId} />}
|
||||||
|
scrollKey='account_timeline'
|
||||||
statusIds={statusIds}
|
statusIds={statusIds}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
hasMore={hasMore}
|
hasMore={hasMore}
|
||||||
|
|
|
@ -77,7 +77,7 @@ class CommunityTimeline extends React.PureComponent {
|
||||||
return (
|
return (
|
||||||
<Column icon='users' active={hasUnread} heading={intl.formatMessage(messages.title)}>
|
<Column icon='users' active={hasUnread} heading={intl.formatMessage(messages.title)}>
|
||||||
<ColumnBackButtonSlim />
|
<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>
|
</Column>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,7 +47,7 @@ class Favourites extends React.PureComponent {
|
||||||
return (
|
return (
|
||||||
<Column icon='star' heading={intl.formatMessage(messages.heading)}>
|
<Column icon='star' heading={intl.formatMessage(messages.heading)}>
|
||||||
<ColumnBackButtonSlim />
|
<ColumnBackButtonSlim />
|
||||||
<StatusList statusIds={statusIds} me={me} onScrollToBottom={this.handleScrollToBottom} />
|
<StatusList {...this.props} onScrollToBottom={this.handleScrollToBottom} />
|
||||||
</Column>
|
</Column>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -71,7 +71,7 @@ class HashtagTimeline extends React.PureComponent {
|
||||||
return (
|
return (
|
||||||
<Column icon='hashtag' active={hasUnread} heading={id}>
|
<Column icon='hashtag' active={hasUnread} heading={id}>
|
||||||
<ColumnBackButtonSlim />
|
<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>
|
</Column>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,7 +22,7 @@ class HomeTimeline extends React.PureComponent {
|
||||||
return (
|
return (
|
||||||
<Column icon='home' active={hasUnread} heading={intl.formatMessage(messages.title)}>
|
<Column icon='home' active={hasUnread} heading={intl.formatMessage(messages.title)}>
|
||||||
<ColumnSettingsContainer />
|
<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>
|
</Column>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -80,7 +80,7 @@ class Notifications extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { intl, notifications, trackScroll, isLoading, isUnread } = this.props;
|
const { intl, notifications, shouldUpdateScroll, isLoading, isUnread } = this.props;
|
||||||
|
|
||||||
let loadMore = '';
|
let loadMore = '';
|
||||||
let scrollableArea = '';
|
let scrollableArea = '';
|
||||||
|
@ -113,25 +113,15 @@ class Notifications extends React.PureComponent {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (trackScroll) {
|
|
||||||
return (
|
return (
|
||||||
<Column icon='bell' active={isUnread} heading={intl.formatMessage(messages.title)}>
|
<Column icon='bell' active={isUnread} heading={intl.formatMessage(messages.title)}>
|
||||||
<ColumnSettingsContainer />
|
<ColumnSettingsContainer />
|
||||||
<ClearColumnButton onClick={this.handleClear} />
|
<ClearColumnButton onClick={this.handleClear} />
|
||||||
<ScrollContainer scrollKey='notifications'>
|
<ScrollContainer scrollKey='notifications' shouldUpdateScroll={shouldUpdateScroll}>
|
||||||
{scrollableArea}
|
{scrollableArea}
|
||||||
</ScrollContainer>
|
</ScrollContainer>
|
||||||
</Column>
|
</Column>
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<Column icon='bell' active={isUnread} heading={intl.formatMessage(messages.title)}>
|
|
||||||
<ColumnSettingsContainer />
|
|
||||||
<ClearColumnButton onClick={this.handleClear} />
|
|
||||||
{scrollableArea}
|
|
||||||
</Column>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -139,7 +129,7 @@ class Notifications extends React.PureComponent {
|
||||||
Notifications.propTypes = {
|
Notifications.propTypes = {
|
||||||
notifications: ImmutablePropTypes.list.isRequired,
|
notifications: ImmutablePropTypes.list.isRequired,
|
||||||
dispatch: PropTypes.func.isRequired,
|
dispatch: PropTypes.func.isRequired,
|
||||||
trackScroll: PropTypes.bool,
|
shouldUpdateScroll: PropTypes.func,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
isLoading: PropTypes.bool,
|
isLoading: PropTypes.bool,
|
||||||
isUnread: PropTypes.bool
|
isUnread: PropTypes.bool
|
||||||
|
|
|
@ -77,7 +77,7 @@ class PublicTimeline extends React.PureComponent {
|
||||||
return (
|
return (
|
||||||
<Column icon='globe' active={hasUnread} heading={intl.formatMessage(messages.title)}>
|
<Column icon='globe' active={hasUnread} heading={intl.formatMessage(messages.title)}>
|
||||||
<ColumnBackButtonSlim />
|
<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>
|
</Column>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,6 +40,8 @@ const makeMapStateToProps = () => {
|
||||||
const getStatusIds = makeGetStatusIds();
|
const getStatusIds = makeGetStatusIds();
|
||||||
|
|
||||||
const mapStateToProps = (state, props) => ({
|
const mapStateToProps = (state, props) => ({
|
||||||
|
scrollKey: props.scrollKey,
|
||||||
|
shouldUpdateScroll: props.shouldUpdateScroll,
|
||||||
statusIds: getStatusIds(state, props),
|
statusIds: getStatusIds(state, props),
|
||||||
isLoading: state.getIn(['timelines', props.type, 'isLoading'], true),
|
isLoading: state.getIn(['timelines', props.type, 'isLoading'], true),
|
||||||
isUnread: state.getIn(['timelines', props.type, 'unread']) > 0,
|
isUnread: state.getIn(['timelines', props.type, 'unread']) > 0,
|
||||||
|
|
|
@ -127,9 +127,9 @@ class UI extends React.PureComponent {
|
||||||
mountedColumns = (
|
mountedColumns = (
|
||||||
<ColumnsArea>
|
<ColumnsArea>
|
||||||
<Compose withHeader={true} />
|
<Compose withHeader={true} />
|
||||||
<HomeTimeline trackScroll={false} />
|
<HomeTimeline shouldUpdateScroll={() => false} />
|
||||||
<Notifications trackScroll={false} />
|
<Notifications shouldUpdateScroll={() => false} />
|
||||||
{children}
|
<div style={{display: 'flex', flex: '1 1 auto', position: 'relative'}}>{children}</div>
|
||||||
</ColumnsArea>
|
</ColumnsArea>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -89,11 +89,11 @@
|
||||||
border: none;
|
border: none;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 100ms ease-in;
|
transition: color 100ms ease-in;
|
||||||
|
|
||||||
&:hover, &:active, &:focus {
|
&:hover, &:active, &:focus {
|
||||||
color: lighten($color1, 33%);
|
color: lighten($color1, 33%);
|
||||||
transition: all 200ms ease-out;
|
transition: color 200ms ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.disabled {
|
&.disabled {
|
||||||
|
@ -152,11 +152,11 @@
|
||||||
padding: 0 3px;
|
padding: 0 3px;
|
||||||
line-height: 27px;
|
line-height: 27px;
|
||||||
outline: 0;
|
outline: 0;
|
||||||
transition: all 100ms ease-in;
|
transition: color 100ms ease-in;
|
||||||
|
|
||||||
&:hover, &:active, &:focus {
|
&:hover, &:active, &:focus {
|
||||||
color: lighten($color1, 26%);
|
color: lighten($color1, 26%);
|
||||||
transition: all 200ms ease-out;
|
transition: color 200ms ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.disabled {
|
&.disabled {
|
||||||
|
@ -1100,6 +1100,7 @@ a.status__content__spoiler-link {
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (min-width: 360px) {
|
@media screen and (min-width: 360px) {
|
||||||
|
@ -1257,11 +1258,11 @@ a.status__content__spoiler-link {
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|
||||||
a {
|
a {
|
||||||
transition: all 100ms ease-in;
|
transition: background 100ms ease-in;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: lighten($color1, 3%);
|
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 {
|
.logo-container {
|
||||||
max-width: 400px;
|
max-width: 400px;
|
||||||
margin: 100px auto;
|
margin: 100px auto;
|
||||||
|
@ -40,7 +50,7 @@
|
||||||
|
|
||||||
img {
|
img {
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
transition: all 0.8s ease;
|
transition: opacity 0.8s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
|
|
Reference in New Issue