Merge branch 'next-scrollable-list' into 'next'

Next: Reimplement ScrollableList with Virtuoso

See merge request soapbox-pub/soapbox-fe!1249
revert-5af0e40a
Alex Gleason 2 years ago
commit 0a1cc033af

@ -1,17 +0,0 @@
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,
};
}

@ -1,131 +0,0 @@
import { is } from 'immutable';
import PropTypes from 'prop-types';
import React from 'react';
import getRectFromEntry from '../features/ui/util/get_rect_from_entry';
import scheduleIdleTask from '../features/ui/util/schedule_idle_task';
// Diff these props in the "rendered" state
const updateOnPropsForRendered = ['id', 'index', 'listLength'];
// Diff these props in the "unrendered" state
const updateOnPropsForUnrendered = ['id', 'index', 'listLength', 'cachedHeight'];
export default class IntersectionObserverArticle extends React.Component {
static propTypes = {
intersectionObserverWrapper: PropTypes.object.isRequired,
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
index: 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,
};
state = {
isHidden: false, // set to true in requestIdleCallback to trigger un-render
isIntersecting: true,
}
shouldComponentUpdate(nextProps, nextState) {
const isUnrendered = !this.state.isIntersecting && (this.state.isHidden || this.props.cachedHeight);
const willBeUnrendered = !nextState.isIntersecting && (nextState.isHidden || nextProps.cachedHeight);
if (!!isUnrendered !== !!willBeUnrendered) {
// If we're going from rendered to unrendered (or vice versa) then update
return true;
}
// Otherwise, diff based on props
const propsToDiff = isUnrendered ? updateOnPropsForUnrendered : updateOnPropsForRendered;
return !propsToDiff.every(prop => is(nextProps[prop], this.props[prop]));
}
componentDidMount() {
const { intersectionObserverWrapper, id } = this.props;
intersectionObserverWrapper.observe(
id,
this.node,
this.handleIntersection,
);
this.componentMounted = true;
}
componentWillUnmount() {
const { intersectionObserverWrapper, id } = this.props;
intersectionObserverWrapper.unobserve(id, this.node);
this.componentMounted = false;
}
handleIntersection = (entry) => {
this.entry = entry;
scheduleIdleTask(this.calculateHeight);
this.setState(this.updateStateAfterIntersection);
}
updateStateAfterIntersection = (prevState) => {
if (prevState.isIntersecting !== false && !this.entry.isIntersecting) {
scheduleIdleTask(this.hideIfNotIntersecting);
}
return {
isIntersecting: this.entry.isIntersecting,
isHidden: false,
};
}
calculateHeight = () => {
const { onHeightChange, saveHeightKey, id } = this.props;
// save the height of the fully-rendered element (this is expensive
// on Chrome, where we need to fall back to getBoundingClientRect)
this.height = getRectFromEntry(this.entry).height;
if (onHeightChange && saveHeightKey) {
onHeightChange(saveHeightKey, id, this.height);
}
}
hideIfNotIntersecting = () => {
if (!this.componentMounted) {
return;
}
// When the browser gets a chance, test if we're still not intersecting,
// and if so, set our isHidden to true to trigger an unrender. The point of
// this is to save DOM nodes and avoid using up too much memory.
this.setState((prevState) => ({ isHidden: !prevState.isIntersecting }));
}
handleRef = (node) => {
this.node = node;
}
render() {
const { children, id, index, listLength, cachedHeight } = this.props;
const { isIntersecting, isHidden } = this.state;
if (!isIntersecting && (isHidden || cachedHeight)) {
return (
<article
ref={this.handleRef}
aria-posinset={index + 1}
aria-setsize={listLength}
style={{ height: `${this.height || cachedHeight}px`, opacity: 0, overflow: 'hidden' }}
data-id={id}
tabIndex='0'
>
{children && React.cloneElement(children, { hidden: true })}
</article>
);
}
return (
<article ref={this.handleRef} aria-posinset={index + 1} aria-setsize={listLength} data-id={id} tabIndex='0'>
{children && React.cloneElement(children, { hidden: false })}
</article>
);
}
}

@ -1,347 +0,0 @@
import classNames from 'classnames';
import { List as ImmutableList } from 'immutable';
import { throttle } from 'lodash';
import PropTypes from 'prop-types';
import React, { PureComponent } from 'react';
import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';
import { getSettings } from 'soapbox/actions/settings';
import PullToRefresh from 'soapbox/components/pull-to-refresh';
import IntersectionObserverArticleContainer from '../containers/intersection_observer_article_container';
import IntersectionObserverWrapper from '../features/ui/util/intersection_observer_wrapper';
import LoadMore from './load_more';
import MoreFollows from './more_follows';
import { Spinner, Text } from './ui';
const MOUSE_IDLE_DELAY = 300;
const mapStateToProps = state => {
const settings = getSettings(state);
return {
autoload: settings.get('autoloadMore'),
};
};
export default @connect(mapStateToProps, null, null, { forwardRef: true })
@withRouter
class ScrollableList extends PureComponent {
static propTypes = {
scrollKey: PropTypes.string.isRequired,
onLoadMore: PropTypes.func,
isLoading: PropTypes.bool,
showLoading: PropTypes.bool,
hasMore: PropTypes.bool,
diffCount: PropTypes.number,
prepend: PropTypes.node,
alwaysPrepend: PropTypes.bool,
emptyMessage: PropTypes.node,
children: PropTypes.node,
onScrollToTop: PropTypes.func,
onScroll: PropTypes.func,
placeholderComponent: PropTypes.object,
placeholderCount: PropTypes.number,
autoload: PropTypes.bool,
onRefresh: PropTypes.func,
className: PropTypes.string,
location: PropTypes.object,
};
state = {
cachedMediaWidth: 250, // Default media/card width using default theme
};
intersectionObserverWrapper = new IntersectionObserverWrapper();
mouseIdleTimer = null;
mouseMovedRecently = false;
lastScrollWasSynthetic = false;
scrollToTopOnMouseIdle = false;
setScrollTop = newScrollTop => {
if (this.documentElement.scrollTop !== newScrollTop) {
this.lastScrollWasSynthetic = true;
this.documentElement.scrollTop = newScrollTop;
}
};
clearMouseIdleTimer = () => {
if (this.mouseIdleTimer === null) {
return;
}
clearTimeout(this.mouseIdleTimer);
this.mouseIdleTimer = null;
};
handleMouseMove = throttle(() => {
// As long as the mouse keeps moving, clear and restart the idle timer.
this.clearMouseIdleTimer();
this.mouseIdleTimer = setTimeout(this.handleMouseIdle, MOUSE_IDLE_DELAY);
if (!this.mouseMovedRecently && this.documentElement.scrollTop === 0) {
// Only set if we just started moving and are scrolled to the top.
this.scrollToTopOnMouseIdle = true;
}
// Save setting this flag for last, so we can do the comparison above.
this.mouseMovedRecently = true;
}, MOUSE_IDLE_DELAY / 2);
handleMouseIdle = () => {
if (this.scrollToTopOnMouseIdle) {
this.setScrollTop(0);
}
this.mouseMovedRecently = false;
this.scrollToTopOnMouseIdle = false;
}
componentDidMount() {
this.window = window;
this.documentElement = document.scrollingElement || document.documentElement;
this.attachScrollListener();
this.attachIntersectionObserver();
// Handle initial scroll position
this.handleScroll();
}
getScrollPosition = () => {
if (this.documentElement && (this.documentElement.scrollTop > 0 || this.mouseMovedRecently)) {
return { height: this.documentElement.scrollHeight, top: this.documentElement.scrollTop };
} else {
return undefined;
}
}
updateScrollBottom = (snapshot) => {
const newScrollTop = this.documentElement.scrollHeight - snapshot;
this.setScrollTop(newScrollTop);
}
componentDidUpdate(prevProps, prevState, snapshot) {
// Reset the scroll position when a new child comes in in order not to
// jerk the scrollbar around if you're already scrolled down the page.
if (snapshot !== null) {
this.setScrollTop(this.documentElement.scrollHeight - snapshot);
}
}
attachScrollListener() {
this.window.addEventListener('scroll', this.handleScroll);
this.window.addEventListener('wheel', this.handleWheel);
}
detachScrollListener() {
this.window.removeEventListener('scroll', this.handleScroll);
this.window.removeEventListener('wheel', this.handleWheel);
}
handleScroll = throttle(() => {
const { autoload } = this.props;
if (this.window) {
const { scrollTop, scrollHeight } = this.documentElement;
const { innerHeight } = this.window;
const offset = scrollHeight - scrollTop - innerHeight;
if (autoload && 400 > offset && this.props.onLoadMore && this.props.hasMore && !this.props.isLoading) {
this.props.onLoadMore();
}
if (scrollTop < 100 && this.props.onScrollToTop) {
this.props.onScrollToTop();
} else if (this.props.onScroll) {
this.props.onScroll();
}
if (!this.lastScrollWasSynthetic) {
// If the last scroll wasn't caused by setScrollTop(), assume it was
// intentional and cancel any pending scroll reset on mouse idle
this.scrollToTopOnMouseIdle = false;
}
this.lastScrollWasSynthetic = false;
}
}, 150, {
trailing: true,
});
handleWheel = throttle(() => {
this.scrollToTopOnMouseIdle = false;
}, 150, {
trailing: true,
});
getSnapshotBeforeUpdate(prevProps) {
const someItemInserted = React.Children.count(prevProps.children) > 0 &&
React.Children.count(prevProps.children) < React.Children.count(this.props.children) &&
this.getFirstChildKey(prevProps) !== this.getFirstChildKey(this.props);
if (someItemInserted && (this.documentElement.scrollTop > 0 || this.mouseMovedRecently)) {
return this.documentElement.scrollHeight - this.documentElement.scrollTop;
} else {
return null;
}
}
cacheMediaWidth = (width) => {
if (width && this.state.cachedMediaWidth !== width) {
this.setState({ cachedMediaWidth: width });
}
}
componentWillUnmount() {
this.clearMouseIdleTimer();
this.detachScrollListener();
this.detachIntersectionObserver();
}
attachIntersectionObserver() {
this.intersectionObserverWrapper.connect();
}
detachIntersectionObserver() {
this.intersectionObserverWrapper.disconnect();
}
getFirstChildKey(props) {
const { children } = props;
let firstChild = children;
if (children instanceof ImmutableList) {
firstChild = children.get(0);
} else if (Array.isArray(children)) {
firstChild = children[0];
}
return firstChild && firstChild.key;
}
handleLoadMore = e => {
e.preventDefault();
this.props.onLoadMore();
}
getMoreFollows = () => {
const { scrollKey, isLoading, diffCount, hasMore } = this.props;
const isMoreFollows = ['followers', 'following'].some(k => k === scrollKey);
if (!(diffCount && isMoreFollows)) return null;
if (hasMore) return null;
return (
<MoreFollows visible={!isLoading} count={diffCount} type={scrollKey} />
);
}
setRef = c => {
this.node = c;
}
renderLoading = () => {
const { className, prepend, placeholderComponent: Placeholder, placeholderCount } = this.props;
if (Placeholder && placeholderCount > 0) {
return (
<div role='feed' className={className}>
{Array(placeholderCount).fill().map((_, i) => (
<Placeholder key={i} />
))}
</div>
);
}
return (
<div className={classNames('slist slist--flex', className)}>
<div role='feed' className='item-list'>
{prepend}
</div>
<div className='slist__append'>
<Spinner />
</div>
</div>
);
}
renderEmptyMessage = () => {
const { className, prepend, alwaysPrepend, emptyMessage } = this.props;
return (
<div className={classNames('mt-2', className)} ref={this.setRef}>
{alwaysPrepend && prepend}
<div className='bg-primary-50 dark:bg-slate-700 mt-2 rounded-lg text-center p-8'>
<Text>{emptyMessage}</Text>
</div>
</div>
);
}
renderFeed = () => {
const { className, children, scrollKey, isLoading, hasMore, prepend, onLoadMore, onRefresh, placeholderComponent: Placeholder } = this.props;
const childrenCount = React.Children.count(children);
const trackScroll = true; //placeholder
const loadMore = (hasMore && onLoadMore) ? <LoadMore visible={!isLoading} onClick={this.handleLoadMore} /> : null;
const feed = (
<div ref={this.setRef} onMouseMove={this.handleMouseMove}>
<div role='feed' className={className}>
{prepend}
{React.Children.map(children, (child, index) => (
<IntersectionObserverArticleContainer
key={child.key}
id={child.key}
index={index}
listLength={childrenCount}
intersectionObserverWrapper={this.intersectionObserverWrapper}
saveHeightKey={trackScroll ? `${this.props.location.key}:${scrollKey}` : null}
>
{React.cloneElement(child, {
getScrollPosition: this.getScrollPosition,
updateScrollBottom: this.updateScrollBottom,
cachedMediaWidth: this.state.cachedMediaWidth,
cacheMediaWidth: this.cacheMediaWidth,
})}
</IntersectionObserverArticleContainer>
))}
{(isLoading && Placeholder) && (
<Placeholder />
)}
{this.getMoreFollows()}
{loadMore}
</div>
</div>
);
if (onRefresh) {
return (
<PullToRefresh onRefresh={onRefresh}>
{feed}
</PullToRefresh>
);
} else {
return feed;
}
}
render() {
const { children, showLoading, isLoading, hasMore, emptyMessage } = this.props;
const childrenCount = React.Children.count(children);
if (showLoading) {
return this.renderLoading();
} else if (isLoading || childrenCount > 0 || hasMore || !emptyMessage) {
return this.renderFeed();
} else {
return this.renderEmptyMessage();
}
}
}

@ -0,0 +1,151 @@
import React from 'react';
import { Virtuoso } from 'react-virtuoso';
import PullToRefresh from 'soapbox/components/pull-to-refresh';
import { Spinner, Text } from './ui';
type Context = {
itemClassName?: string,
listClassName?: string,
}
type VComponent = {
context?: Context,
}
// NOTE: It's crucial to space lists with **padding** instead of margin!
// Pass an `itemClassName` like `pb-3`, NOT a `space-y-3` className
// https://virtuoso.dev/troubleshooting#list-does-not-scroll-to-the-bottom--items-jump-around
const Item: React.FC<VComponent> = ({ context, ...rest }) => (
<div className={context?.itemClassName} {...rest} />
);
// Ensure the className winds up here
const List = React.forwardRef<HTMLDivElement, VComponent>((props, ref) => {
const { context, ...rest } = props;
return <div ref={ref} className={context?.listClassName} {...rest} />;
});
interface IScrollableList {
scrollKey?: string,
onLoadMore?: () => void,
isLoading?: boolean,
showLoading?: boolean,
hasMore?: boolean,
prepend?: React.ReactElement,
alwaysPrepend?: boolean,
emptyMessage?: React.ReactNode,
children: Iterable<React.ReactNode>,
onScrollToTop?: () => void,
onScroll?: () => void,
placeholderComponent?: React.ComponentType,
placeholderCount?: number,
onRefresh?: () => Promise<any>,
className?: string,
itemClassName?: string,
}
/** Legacy ScrollableList with Virtuoso for backwards-compatibility */
const ScrollableList: React.FC<IScrollableList> = ({
prepend = null,
alwaysPrepend,
children,
isLoading,
emptyMessage,
showLoading,
onRefresh,
onScroll,
onScrollToTop,
onLoadMore,
className,
itemClassName,
hasMore,
placeholderComponent: Placeholder,
placeholderCount = 0,
}) => {
/** Normalized children */
const elements = Array.from(children || []);
const showPlaceholder = showLoading && Placeholder && placeholderCount > 0;
// NOTE: We are doing some trickery to load a feed of placeholders
// Virtuoso's `EmptyPlaceholder` unfortunately doesn't work for our use-case
const data = showPlaceholder ? Array(placeholderCount).fill('') : elements;
const isEmpty = data.length === 0; // Yes, if it has placeholders it isn't "empty"
// Add a placeholder at the bottom for loading
// (Don't use Virtuoso's `Footer` component because it doesn't preserve its height)
if (hasMore && Placeholder) {
data.push(<Placeholder />);
} else if (hasMore) {
data.push(<Spinner />);
}
/* Render an empty state instead of the scrollable list */
const renderEmpty = (): JSX.Element => {
return (
<div className='mt-2'>
{alwaysPrepend && prepend}
<div className='bg-primary-50 dark:bg-slate-700 mt-2 rounded-lg text-center p-8'>
{isLoading ? (
<Spinner />
) : (
<Text>{emptyMessage}</Text>
)}
</div>
</div>
);
};
/** Render a single item */
const renderItem = (_i: number, element: JSX.Element): JSX.Element => {
if (showPlaceholder) {
return <Placeholder />;
} else {
return element;
}
};
/** Render the actual Virtuoso list */
const renderFeed = (): JSX.Element => (
<Virtuoso
useWindowScroll
className={className}
data={data}
startReached={onScrollToTop}
endReached={onLoadMore}
isScrolling={isScrolling => isScrolling && onScroll && onScroll()}
itemContent={renderItem}
context={{
listClassName: className,
itemClassName,
}}
components={{
Header: () => prepend,
ScrollSeekPlaceholder: Placeholder as any,
EmptyPlaceholder: () => renderEmpty(),
List,
Item,
}}
/>
);
/** Conditionally render inner elements */
const renderBody = (): JSX.Element => {
if (isEmpty) {
return renderEmpty();
} else {
return renderFeed();
}
};
return (
<PullToRefresh onRefresh={onRefresh}>
{renderBody()}
</PullToRefresh>
);
};
export default ScrollableList;

@ -1,3 +1,4 @@
import classNames from 'classnames';
import { debounce } from 'lodash';
import PropTypes from 'prop-types';
import React from 'react';
@ -222,7 +223,12 @@ export default class StatusList extends ImmutablePureComponent {
placeholderComponent={PlaceholderStatus}
placeholderCount={20}
ref={this.setRef}
className={divideType === 'border' ? 'divide-y divide-solid divide-gray-200 dark:divide-gray-800' : 'sm:space-y-3 divide-y divide-solid divide-gray-200 dark:divide-gray-800 sm:divide-none'}
className={classNames('divide-y divide-solid divide-gray-200 dark:divide-slate-700', {
'sm:divide-none': divideType !== 'border',
})}
itemClassName={classNames({
'sm:pb-3': divideType !== 'border',
})}
{...other}
>
{this.renderScrollableContent()}

@ -1,18 +0,0 @@
import { connect } from 'react-redux';
import { setHeight } from '../actions/height_cache';
import IntersectionObserverArticle from '../components/intersection_observer_article';
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);

@ -116,7 +116,8 @@ class UserIndex extends ImmutablePureComponent {
showLoading={showLoading}
onLoadMore={this.handleLoadMore}
emptyMessage={intl.formatMessage(messages.empty)}
className='mt-4 space-y-4'
className='mt-4'
itemClassName='pb-4'
>
{accountIds.map(id =>
<AccountContainer key={id} id={id} withDate />,

@ -45,7 +45,7 @@ const Blocks: React.FC = () => {
onLoadMore={() => handleLoadMore(dispatch)}
hasMore={hasMore}
emptyMessage={emptyMessage}
className='space-y-4'
itemClassName='pb-4'
>
{accountIds.map((id: string) =>
<AccountContainer key={id} id={id} />,

@ -125,7 +125,7 @@ class Followers extends ImmutablePureComponent {
diffCount={diffCount}
onLoadMore={this.handleLoadMore}
emptyMessage={<FormattedMessage id='account.followers.empty' defaultMessage='No one follows this user yet.' />}
className='space-y-4'
itemClassName='pb-4'
>
{accountIds.map(id =>
<AccountContainer key={id} id={id} withNote={false} />,

@ -125,7 +125,7 @@ class Following extends ImmutablePureComponent {
diffCount={diffCount}
onLoadMore={this.handleLoadMore}
emptyMessage={<FormattedMessage id='account.follows.empty' defaultMessage="This user doesn't follow anyone yet." />}
className='space-y-4'
itemClassName='pb-4'
>
{accountIds.map(id =>
<AccountContainer key={id} id={id} withNote={false} />,

@ -45,7 +45,7 @@ const Mutes: React.FC = () => {
onLoadMore={() => handleLoadMore(dispatch)}
hasMore={hasMore}
emptyMessage={emptyMessage}
className='space-y-4'
itemClassName='pb-4'
>
{accountIds.map((id: string) =>
<AccountContainer key={id} id={id} />,

@ -156,16 +156,18 @@ class Notifications extends React.PureComponent {
? (<FilterBarContainer />)
: null;
const showLoading = isLoading && !notifications || notifications.isEmpty();
const scrollContainer = (
<PullToRefresh onRefresh={this.handleRefresh}>
<Virtuoso
useWindowScroll
data={isLoading ? Array(20).fill() : notifications.toArray()}
data={showLoading ? Array(20).fill() : notifications.toArray()}
startReached={this.handleScrollToTop}
endReached={this.handleLoadOlder}
isScrolling={isScrolling => isScrolling && this.handleScroll()}
itemContent={(_index, notification) => (
isLoading ? (
showLoading ? (
<PlaceholderNotification />
) : (
<NotificationContainer

@ -28,7 +28,7 @@ const BirthdaysModal = ({ onClose }: IBirthdaysModal) => {
<ScrollableList
scrollKey='reblogs'
emptyMessage={emptyMessage}
className='space-y-3'
itemClassName='pb-3'
>
{accountIds.map(id =>
<Account key={id} accountId={id} />,

@ -54,7 +54,7 @@ class FavouritesModal extends React.PureComponent {
<ScrollableList
scrollKey='favourites'
emptyMessage={emptyMessage}
className='space-y-3'
itemClassName='pb-3'
>
{accountIds.map(id =>
<AccountContainer key={id} id={id} />,

@ -61,7 +61,7 @@ class MentionsModal extends React.PureComponent {
body = (
<ScrollableList
scrollKey='mentions'
className='space-y-3'
itemClassName='pb-3'
>
{accountIds.map(id =>
<AccountContainer key={id} id={id} withNote={false} />,

@ -87,7 +87,7 @@ const ReactionsModal: React.FC<IReactionsModal> = ({ onClose, statusId, ...props
<ScrollableList
scrollKey='reactions'
emptyMessage={emptyMessage}
className='space-y-3'
itemClassName='pb-3'
>
{accounts.map((account) =>
<AccountContainer key={`${account.id}-${account.reaction}`} id={account.id} /* reaction={account.reaction} */ />,

@ -72,7 +72,7 @@ class ReblogsModal extends React.PureComponent {
<ScrollableList
scrollKey='reblogs'
emptyMessage={emptyMessage}
className='space-y-3'
itemClassName='pb-3'
>
{accountIds.map(id =>
<AccountContainer key={id} id={id} />,

@ -35,7 +35,6 @@ import { fetchFollowRequests } from '../../actions/accounts';
import { fetchReports, fetchUsers, fetchConfig } from '../../actions/admin';
import { uploadCompose, resetCompose } from '../../actions/compose';
import { fetchFilters } from '../../actions/filters';
import { clearHeight } from '../../actions/height_cache';
import { openModal } from '../../actions/modals';
import { expandNotifications } from '../../actions/notifications';
import { fetchScheduledStatuses } from '../../actions/scheduled_statuses';
@ -187,7 +186,6 @@ class SwitchingColumnsArea extends React.PureComponent {
static propTypes = {
children: PropTypes.node,
location: PropTypes.object,
onLayoutChange: PropTypes.func.isRequired,
soapbox: ImmutablePropTypes.record.isRequired,
features: PropTypes.object.isRequired,
};
@ -205,9 +203,6 @@ class SwitchingColumnsArea extends React.PureComponent {
}
handleResize = debounce(() => {
// The cached heights are no longer accurate, invalidate
this.props.onLayoutChange();
this.setState({ mobile: isMobile(window.innerWidth) });
}, 500, {
trailing: true,
@ -408,11 +403,6 @@ class UI extends React.PureComponent {
mobile: isMobile(window.innerWidth),
};
handleLayoutChange = () => {
// The cached heights are no longer accurate, invalidate
this.props.dispatch(clearHeight());
}
handleDragEnter = (e) => {
e.preventDefault();
@ -756,7 +746,7 @@ class UI extends React.PureComponent {
<div className='z-10 flex flex-col'>
<Navbar />
<SwitchingColumnsArea location={location} onLayoutChange={this.handleLayoutChange} soapbox={soapbox} features={features}>
<SwitchingColumnsArea location={location} soapbox={soapbox} features={features}>
{children}
</SwitchingColumnsArea>

@ -1,21 +0,0 @@
// Get the bounding client rect from an IntersectionObserver entry.
// This is to work around a bug in Chrome: https://crbug.com/737228
let hasBoundingRectBug;
function getRectFromEntry(entry) {
if (typeof hasBoundingRectBug !== 'boolean') {
const boundingRect = entry.target.getBoundingClientRect();
const observerRect = entry.boundingClientRect;
hasBoundingRectBug = boundingRect.height !== observerRect.height ||
boundingRect.top !== observerRect.top ||
boundingRect.width !== observerRect.width ||
boundingRect.bottom !== observerRect.bottom ||
boundingRect.left !== observerRect.left ||
boundingRect.right !== observerRect.right;
}
return hasBoundingRectBug ? entry.target.getBoundingClientRect() : entry.boundingClientRect;
}
export default getRectFromEntry;

@ -1,57 +0,0 @@
// Wrapper for IntersectionObserver in order to make working with it
// a bit easier. We also follow this performance advice:
// "If you need to observe multiple elements, it is both possible and
// advised to observe multiple elements using the same IntersectionObserver
// instance by calling observe() multiple times."
// https://developers.google.com/web/updates/2016/04/intersectionobserver
class IntersectionObserverWrapper {
callbacks = {};
observerBacklog = [];
observer = null;
connect(options) {
const onIntersection = (entries) => {
entries.forEach(entry => {
const id = entry.target.getAttribute('data-id');
if (this.callbacks[id]) {
this.callbacks[id](entry);
}
});
};
this.observer = new IntersectionObserver(onIntersection, options);
this.observerBacklog.forEach(([ id, node, callback ]) => {
this.observe(id, node, callback);
});
this.observerBacklog = null;
}
observe(id, node, callback) {
if (!this.observer) {
this.observerBacklog.push([ id, node, callback ]);
} else {
this.callbacks[id] = callback;
this.observer.observe(node);
}
}
unobserve(id, node) {
if (this.observer) {
delete this.callbacks[id];
this.observer.unobserve(node);
}
}
disconnect() {
if (this.observer) {
this.callbacks = {};
this.observer.disconnect();
this.observer = null;
}
}
}
export default IntersectionObserverWrapper;

@ -1,14 +0,0 @@
import { Map as ImmutableMap } from 'immutable';
import reducer from '../height_cache';
import { HEIGHT_CACHE_CLEAR } from '../height_cache';
describe('height_cache reducer', () => {
it('should return the initial state', () => {
expect(reducer(undefined, {})).toEqual(ImmutableMap());
});
it('should handle HEIGHT_CACHE_CLEAR', () => {
expect(reducer(undefined, { type: HEIGHT_CACHE_CLEAR })).toEqual(ImmutableMap());
});
});

@ -1,24 +0,0 @@
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;
}
}

@ -28,7 +28,6 @@ import group_editor from './group_editor';
import group_lists from './group_lists';
import group_relationships from './group_relationships';
import groups from './groups';
import height_cache from './height_cache';
import identity_proofs from './identity_proofs';
import instance from './instance';
import listAdder from './list_adder';
@ -83,7 +82,6 @@ const reducers = {
compose,
search,
notifications,
height_cache,
custom_emojis,
identity_proofs,
lists,

Loading…
Cancel
Save