diff options
author | Akihiko Odaki <akihiko.odaki.4i@stu.hosei.ac.jp> | 2017-04-24 11:49:08 +0900 |
---|---|---|
committer | Eugen <eugen@zeonfederated.com> | 2017-04-24 04:49:08 +0200 |
commit | cf845fed3824d3e3587ce9b2ad752c2b3f0a2a76 (patch) | |
tree | fca5dab5974340cb98eaecd22ac41733f5c8cdd6 | |
parent | 72c984e1057306d1e4df49871b9fb658fd7cbcc6 (diff) |
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.
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 index dc2a9509d..517c8fe5d 100644 --- 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 index e2b91e5dd..c85a353ee 100644 --- 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 index 4987a2364..a06de3d21 100644 --- 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 index c2d8bf2ed..3877888ba 100644 --- 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 index d6f53bf33..bc45ace51 100644 --- 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 index 5c091e17f..0575e9214 100644 --- 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 index 6b986171e..52b94690d 100644 --- 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 index 14c00b9ce..da3ce2f62 100644 --- 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 index fa7a2db8e..53be13686 100644 --- 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 index 4c33f2b61..1599000b5 100644 --- 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 index 1f35842d9..c92b9751b 100644 --- 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 index feab81366..0c8379be4 100644 --- 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 index 43705b19c..6f339f998 100644 --- 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 { |