Fix height cache (#4909)
This commit is contained in:
parent
081f907f90
commit
60944d5dca
17
app/javascript/mastodon/actions/height_cache.js
Normal file
17
app/javascript/mastodon/actions/height_cache.js
Normal 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,
|
||||||
|
};
|
||||||
|
};
|
|
@ -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,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
|
@ -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'
|
||||||
>
|
>
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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);
|
|
@ -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));
|
||||||
|
|
|
@ -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, {
|
||||||
|
|
23
app/javascript/mastodon/reducers/height_cache.js
Normal file
23
app/javascript/mastodon/reducers/height_cache.js
Normal 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;
|
||||||
|
}
|
||||||
|
};
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
Reference in a new issue