Fix height cache (#4909)

This commit is contained in:
abcang 2017-09-13 17:24:33 +09:00 committed by Eugen Rochko
parent 081f907f90
commit 60944d5dca
10 changed files with 93 additions and 61 deletions

View File

@ -0,0 +1,17 @@
export const HEIGHT_CACHE_SET = 'HEIGHT_CACHE_SET';
export const HEIGHT_CACHE_CLEAR = 'HEIGHT_CACHE_CLEAR';
export function setHeight (key, id, height) {
return {
type: HEIGHT_CACHE_SET,
key,
id,
height,
};
};
export function clearHeight () {
return {
type: HEIGHT_CACHE_CLEAR,
};
};

View File

@ -23,9 +23,6 @@ export const STATUS_UNMUTE_REQUEST = 'STATUS_UNMUTE_REQUEST';
export const STATUS_UNMUTE_SUCCESS = 'STATUS_UNMUTE_SUCCESS'; export const STATUS_UNMUTE_SUCCESS = 'STATUS_UNMUTE_SUCCESS';
export const STATUS_UNMUTE_FAIL = 'STATUS_UNMUTE_FAIL'; export const STATUS_UNMUTE_FAIL = 'STATUS_UNMUTE_FAIL';
export const STATUS_SET_HEIGHT = 'STATUS_SET_HEIGHT';
export const STATUSES_CLEAR_HEIGHT = 'STATUSES_CLEAR_HEIGHT';
export function fetchStatusRequest(id, skipLoading) { export function fetchStatusRequest(id, skipLoading) {
return { return {
type: STATUS_FETCH_REQUEST, type: STATUS_FETCH_REQUEST,
@ -218,17 +215,3 @@ export function unmuteStatusFail(id, error) {
error, error,
}; };
}; };
export function setStatusHeight (id, height) {
return {
type: STATUS_SET_HEIGHT,
id,
height,
};
};
export function clearStatusesHeight () {
return {
type: STATUSES_CLEAR_HEIGHT,
};
};

View File

@ -7,10 +7,13 @@ import getRectFromEntry from '../features/ui/util/get_rect_from_entry';
export default class IntersectionObserverArticle extends ImmutablePureComponent { export default class IntersectionObserverArticle extends ImmutablePureComponent {
static propTypes = { static propTypes = {
intersectionObserverWrapper: PropTypes.object, intersectionObserverWrapper: PropTypes.object.isRequired,
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
index: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), index: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
listLength: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), listLength: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
saveHeightKey: PropTypes.string,
cachedHeight: PropTypes.number,
onHeightChange: PropTypes.func,
children: PropTypes.node, children: PropTypes.node,
}; };
@ -34,13 +37,10 @@ export default class IntersectionObserverArticle extends ImmutablePureComponent
} }
componentDidMount () { componentDidMount () {
if (!this.props.intersectionObserverWrapper) { const { intersectionObserverWrapper, id } = this.props;
// TODO: enable IntersectionObserver optimization for notification statuses.
// These are managed in notifications/index.js rather than status_list.js intersectionObserverWrapper.observe(
return; id,
}
this.props.intersectionObserverWrapper.observe(
this.props.id,
this.node, this.node,
this.handleIntersection this.handleIntersection
); );
@ -49,20 +49,21 @@ export default class IntersectionObserverArticle extends ImmutablePureComponent
} }
componentWillUnmount () { componentWillUnmount () {
if (this.props.intersectionObserverWrapper) { const { intersectionObserverWrapper, id } = this.props;
this.props.intersectionObserverWrapper.unobserve(this.props.id, this.node); intersectionObserverWrapper.unobserve(id, this.node);
}
this.componentMounted = false; this.componentMounted = false;
} }
handleIntersection = (entry) => { handleIntersection = (entry) => {
const { onHeightChange, saveHeightKey, id } = this.props;
if (this.node && this.node.children.length !== 0) { if (this.node && this.node.children.length !== 0) {
// save the height of the fully-rendered element // save the height of the fully-rendered element
this.height = getRectFromEntry(entry).height; this.height = getRectFromEntry(entry).height;
if (this.props.onHeightChange) { if (onHeightChange && saveHeightKey) {
this.props.onHeightChange(this.props.status, this.height); onHeightChange(saveHeightKey, id, this.height);
} }
} }
@ -94,16 +95,16 @@ export default class IntersectionObserverArticle extends ImmutablePureComponent
} }
render () { render () {
const { children, id, index, listLength } = this.props; const { children, id, index, listLength, cachedHeight } = this.props;
const { isIntersecting, isHidden } = this.state; const { isIntersecting, isHidden } = this.state;
if (!isIntersecting && isHidden) { if (!isIntersecting && (isHidden || cachedHeight)) {
return ( return (
<article <article
ref={this.handleRef} ref={this.handleRef}
aria-posinset={index} aria-posinset={index}
aria-setsize={listLength} aria-setsize={listLength}
style={{ height: `${this.height}px`, opacity: 0, overflow: 'hidden' }} style={{ height: `${this.height || cachedHeight}px`, opacity: 0, overflow: 'hidden' }}
data-id={id} data-id={id}
tabIndex='0' tabIndex='0'
> >

View File

@ -1,7 +1,7 @@
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import { ScrollContainer } from 'react-router-scroll'; import { ScrollContainer } from 'react-router-scroll';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import IntersectionObserverArticle from './intersection_observer_article'; import IntersectionObserverArticleContainer from '../containers/intersection_observer_article_container';
import LoadMore from './load_more'; import LoadMore from './load_more';
import IntersectionObserverWrapper from '../features/ui/util/intersection_observer_wrapper'; import IntersectionObserverWrapper from '../features/ui/util/intersection_observer_wrapper';
import { throttle } from 'lodash'; import { throttle } from 'lodash';
@ -9,6 +9,10 @@ import { List as ImmutableList } from 'immutable';
export default class ScrollableList extends PureComponent { export default class ScrollableList extends PureComponent {
static contextTypes = {
router: PropTypes.object,
};
static propTypes = { static propTypes = {
scrollKey: PropTypes.string.isRequired, scrollKey: PropTypes.string.isRequired,
onScrollToBottom: PropTypes.func, onScrollToBottom: PropTypes.func,
@ -173,9 +177,16 @@ export default class ScrollableList extends PureComponent {
{prepend} {prepend}
{React.Children.map(this.props.children, (child, index) => ( {React.Children.map(this.props.children, (child, index) => (
<IntersectionObserverArticle key={child.key} id={child.key} index={index} listLength={childrenCount} intersectionObserverWrapper={this.intersectionObserverWrapper}> <IntersectionObserverArticleContainer
key={child.key}
id={child.key}
index={index}
listLength={childrenCount}
intersectionObserverWrapper={this.intersectionObserverWrapper}
saveHeightKey={trackScroll ? `${this.context.router.route.location.key}:${scrollKey}` : null}
>
{child} {child}
</IntersectionObserverArticle> </IntersectionObserverArticleContainer>
))} ))}
{loadMore} {loadMore}

View File

@ -0,0 +1,17 @@
import { connect } from 'react-redux';
import IntersectionObserverArticle from '../components/intersection_observer_article';
import { setHeight } from '../actions/height_cache';
const makeMapStateToProps = (state, props) => ({
cachedHeight: state.getIn(['height_cache', props.saveHeightKey, props.id]),
});
const mapDispatchToProps = (dispatch) => ({
onHeightChange (key, id, height) {
dispatch(setHeight(key, id, height));
},
});
export default connect(makeMapStateToProps, mapDispatchToProps)(IntersectionObserverArticle);

View File

@ -18,7 +18,7 @@ import {
blockAccount, blockAccount,
muteAccount, muteAccount,
} from '../actions/accounts'; } from '../actions/accounts';
import { muteStatus, unmuteStatus, deleteStatus, setStatusHeight } from '../actions/statuses'; import { muteStatus, unmuteStatus, deleteStatus } from '../actions/statuses';
import { initReport } from '../actions/reports'; import { initReport } from '../actions/reports';
import { openModal } from '../actions/modal'; import { openModal } from '../actions/modal';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
@ -138,10 +138,6 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
} }
}, },
onHeightChange (status, height) {
dispatch(setStatusHeight(status.get('id'), height));
},
}); });
export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Status)); export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Status));

View File

@ -11,7 +11,7 @@ import { debounce } from 'lodash';
import { uploadCompose } from '../../actions/compose'; import { uploadCompose } from '../../actions/compose';
import { refreshHomeTimeline } from '../../actions/timelines'; import { refreshHomeTimeline } from '../../actions/timelines';
import { refreshNotifications } from '../../actions/notifications'; import { refreshNotifications } from '../../actions/notifications';
import { clearStatusesHeight } from '../../actions/statuses'; import { clearHeight } from '../../actions/height_cache';
import { WrappedSwitch, WrappedRoute } from './util/react_router_helpers'; import { WrappedSwitch, WrappedRoute } from './util/react_router_helpers';
import UploadArea from './components/upload_area'; import UploadArea from './components/upload_area';
import ColumnsAreaContainer from './containers/columns_area_container'; import ColumnsAreaContainer from './containers/columns_area_container';
@ -68,7 +68,7 @@ export default class UI extends React.PureComponent {
handleResize = debounce(() => { handleResize = debounce(() => {
// The cached heights are no longer accurate, invalidate // The cached heights are no longer accurate, invalidate
this.props.dispatch(clearStatusesHeight()); this.props.dispatch(clearHeight());
this.setState({ width: window.innerWidth }); this.setState({ width: window.innerWidth });
}, 500, { }, 500, {

View File

@ -0,0 +1,23 @@
import { Map as ImmutableMap } from 'immutable';
import { HEIGHT_CACHE_SET, HEIGHT_CACHE_CLEAR } from '../actions/height_cache';
const initialState = ImmutableMap();
const setHeight = (state, key, id, height) => {
return state.update(key, ImmutableMap(), map => map.set(id, height));
};
const clearHeights = () => {
return ImmutableMap();
};
export default function statuses(state = initialState, action) {
switch(action.type) {
case HEIGHT_CACHE_SET:
return setHeight(state, action.key, action.id, action.height);
case HEIGHT_CACHE_CLEAR:
return clearHeights();
default:
return state;
}
};

View File

@ -19,6 +19,7 @@ import compose from './compose';
import search from './search'; import search from './search';
import media_attachments from './media_attachments'; import media_attachments from './media_attachments';
import notifications from './notifications'; import notifications from './notifications';
import height_cache from './height_cache';
const reducers = { const reducers = {
timelines, timelines,
@ -41,6 +42,7 @@ const reducers = {
search, search,
media_attachments, media_attachments,
notifications, notifications,
height_cache,
}; };
export default combineReducers(reducers); export default combineReducers(reducers);

View File

@ -15,8 +15,6 @@ import {
CONTEXT_FETCH_SUCCESS, CONTEXT_FETCH_SUCCESS,
STATUS_MUTE_SUCCESS, STATUS_MUTE_SUCCESS,
STATUS_UNMUTE_SUCCESS, STATUS_UNMUTE_SUCCESS,
STATUS_SET_HEIGHT,
STATUSES_CLEAR_HEIGHT,
} from '../actions/statuses'; } from '../actions/statuses';
import { import {
TIMELINE_REFRESH_SUCCESS, TIMELINE_REFRESH_SUCCESS,
@ -95,18 +93,6 @@ const filterStatuses = (state, relationship) => {
return state; return state;
}; };
const setHeight = (state, id, height) => {
return state.update(id, ImmutableMap(), map => map.set('height', height));
};
const clearHeights = (state) => {
state.forEach(status => {
state = state.deleteIn([status.get('id'), 'height']);
});
return state;
};
const initialState = ImmutableMap(); const initialState = ImmutableMap();
export default function statuses(state = initialState, action) { export default function statuses(state = initialState, action) {
@ -148,10 +134,6 @@ export default function statuses(state = initialState, action) {
return deleteStatus(state, action.id, action.references); return deleteStatus(state, action.id, action.references);
case ACCOUNT_BLOCK_SUCCESS: case ACCOUNT_BLOCK_SUCCESS:
return filterStatuses(state, action.relationship); return filterStatuses(state, action.relationship);
case STATUS_SET_HEIGHT:
return setHeight(state, action.id, action.height);
case STATUSES_CLEAR_HEIGHT:
return clearHeights(state);
default: default:
return state; return state;
} }