commit: cf845fed3824d3e3587ce9b2ad752c2b3f0a2a76
parent: 72c984e1057306d1e4df49871b9fb658fd7cbcc6
Author: Akihiko Odaki <akihiko.odaki.4i@stu.hosei.ac.jp>
Date: Mon, 24 Apr 2017 11:49:08 +0900
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.
Diffstat:
13 files changed, 167 insertions(+), 53 deletions(-)
diff --git a/app/assets/javascripts/components/components/status_list.jsx b/app/assets/javascripts/components/components/status_list.jsx
@@ -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,
diff --git a/app/assets/javascripts/components/containers/mastodon.jsx b/app/assets/javascripts/components/containers/mastodon.jsx
@@ -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} />
diff --git a/app/assets/javascripts/components/features/account_timeline/index.jsx b/app/assets/javascripts/components/features/account_timeline/index.jsx
@@ -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}
diff --git a/app/assets/javascripts/components/features/community_timeline/index.jsx b/app/assets/javascripts/components/features/community_timeline/index.jsx
@@ -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>
);
}
diff --git a/app/assets/javascripts/components/features/favourited_statuses/index.jsx b/app/assets/javascripts/components/features/favourited_statuses/index.jsx
@@ -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>
);
}
diff --git a/app/assets/javascripts/components/features/hashtag_timeline/index.jsx b/app/assets/javascripts/components/features/hashtag_timeline/index.jsx
@@ -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>
);
}
diff --git a/app/assets/javascripts/components/features/home_timeline/index.jsx b/app/assets/javascripts/components/features/home_timeline/index.jsx
@@ -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>
);
}
diff --git a/app/assets/javascripts/components/features/notifications/index.jsx b/app/assets/javascripts/components/features/notifications/index.jsx
@@ -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
diff --git a/app/assets/javascripts/components/features/public_timeline/index.jsx b/app/assets/javascripts/components/features/public_timeline/index.jsx
@@ -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>
);
}
diff --git a/app/assets/javascripts/components/features/ui/containers/status_list_container.jsx b/app/assets/javascripts/components/features/ui/containers/status_list_container.jsx
@@ -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,
diff --git a/app/assets/javascripts/components/features/ui/index.jsx b/app/assets/javascripts/components/features/ui/index.jsx
@@ -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>
);
}
diff --git a/app/assets/stylesheets/components.scss b/app/assets/stylesheets/components.scss
@@ -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;
}
}
}
diff --git a/app/assets/stylesheets/containers.scss b/app/assets/stylesheets/containers.scss
@@ -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 {