Hide some components rather than unmounting ()

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
app/assets
javascripts/components
components
containers
features
account_timeline
community_timeline
favourited_statuses
hashtag_timeline
home_timeline
notifications
public_timeline
ui
stylesheets

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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