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.
This commit is contained in:
Akihiko Odaki 2017-04-24 11:49:08 +09:00 committed by Eugen
parent 72c984e105
commit cf845fed38
13 changed files with 167 additions and 53 deletions

View file

@ -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={scrollKey} shouldUpdateScroll={shouldUpdateScroll}>
<ScrollContainer scrollKey='status-list'> {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,

View file

@ -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} />

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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' shouldUpdateScroll={shouldUpdateScroll}>
<ScrollContainer scrollKey='notifications'>
{scrollableArea}
</ScrollContainer>
</Column>
);
} else {
return (
<Column icon='bell' active={isUnread} heading={intl.formatMessage(messages.title)}>
<ColumnSettingsContainer />
<ClearColumnButton onClick={this.handleClear} />
{scrollableArea} {scrollableArea}
</Column> </ScrollContainer>
); </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

View file

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

View file

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

View file

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

View file

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

View file

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