Generalized the infinite scrollable list (#4697)
parent
938cd2875b
commit
0827c09c44
|
@ -26,6 +26,7 @@ export default class Account extends ImmutablePureComponent {
|
||||||
onBlock: PropTypes.func.isRequired,
|
onBlock: PropTypes.func.isRequired,
|
||||||
onMute: PropTypes.func.isRequired,
|
onMute: PropTypes.func.isRequired,
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
|
hidden: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
handleFollow = () => {
|
handleFollow = () => {
|
||||||
|
@ -41,12 +42,21 @@ export default class Account extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { account, me, intl } = this.props;
|
const { account, me, intl, hidden } = this.props;
|
||||||
|
|
||||||
if (!account) {
|
if (!account) {
|
||||||
return <div />;
|
return <div />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (hidden) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{account.get('display_name')}
|
||||||
|
{account.get('username')}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let buttons;
|
let buttons;
|
||||||
|
|
||||||
if (account.get('id') !== me && account.get('relationship', null) !== null) {
|
if (account.get('id') !== me && account.get('relationship', null) !== null) {
|
||||||
|
|
|
@ -0,0 +1,122 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
|
import scheduleIdleTask from '../features/ui/util/schedule_idle_task';
|
||||||
|
import getRectFromEntry from '../features/ui/util/get_rect_from_entry';
|
||||||
|
|
||||||
|
export default class IntersectionObserverArticle extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
intersectionObserverWrapper: PropTypes.object,
|
||||||
|
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||||
|
index: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||||
|
listLength: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||||
|
children: PropTypes.node,
|
||||||
|
};
|
||||||
|
|
||||||
|
state = {
|
||||||
|
isHidden: false, // set to true in requestIdleCallback to trigger un-render
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldComponentUpdate (nextProps, nextState) {
|
||||||
|
if (!nextState.isIntersecting && nextState.isHidden) {
|
||||||
|
// It's only if we're not intersecting (i.e. offscreen) and isHidden is true
|
||||||
|
// that either "isIntersecting" or "isHidden" matter, and then they're
|
||||||
|
// the only things that matter (and updated ARIA attributes).
|
||||||
|
return this.state.isIntersecting || !this.state.isHidden || nextProps.listLength !== this.props.listLength;
|
||||||
|
} else if (nextState.isIntersecting && !this.state.isIntersecting) {
|
||||||
|
// If we're going from a non-intersecting state to an intersecting state,
|
||||||
|
// (i.e. offscreen to onscreen), then we definitely need to re-render
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Otherwise, diff based on "updateOnProps" and "updateOnStates"
|
||||||
|
return super.shouldComponentUpdate(nextProps, nextState);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
if (!this.props.intersectionObserverWrapper) {
|
||||||
|
// TODO: enable IntersectionObserver optimization for notification statuses.
|
||||||
|
// These are managed in notifications/index.js rather than status_list.js
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.props.intersectionObserverWrapper.observe(
|
||||||
|
this.props.id,
|
||||||
|
this.node,
|
||||||
|
this.handleIntersection
|
||||||
|
);
|
||||||
|
|
||||||
|
this.componentMounted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount () {
|
||||||
|
if (this.props.intersectionObserverWrapper) {
|
||||||
|
this.props.intersectionObserverWrapper.unobserve(this.props.id, this.node);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.componentMounted = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleIntersection = (entry) => {
|
||||||
|
if (this.node && this.node.children.length !== 0) {
|
||||||
|
// save the height of the fully-rendered element
|
||||||
|
this.height = getRectFromEntry(entry).height;
|
||||||
|
|
||||||
|
if (this.props.onHeightChange) {
|
||||||
|
this.props.onHeightChange(this.props.status, this.height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState((prevState) => {
|
||||||
|
if (prevState.isIntersecting && !entry.isIntersecting) {
|
||||||
|
scheduleIdleTask(this.hideIfNotIntersecting);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
isIntersecting: entry.isIntersecting,
|
||||||
|
isHidden: false,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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.
|
||||||
|
// See: https://github.com/tootsuite/mastodon/issues/2900
|
||||||
|
this.setState((prevState) => ({ isHidden: !prevState.isIntersecting }));
|
||||||
|
}
|
||||||
|
|
||||||
|
handleRef = (node) => {
|
||||||
|
this.node = node;
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { children, id, index, listLength } = this.props;
|
||||||
|
const { isIntersecting, isHidden } = this.state;
|
||||||
|
|
||||||
|
if (!isIntersecting && isHidden) {
|
||||||
|
return (
|
||||||
|
<article
|
||||||
|
ref={this.handleRef}
|
||||||
|
aria-posinset={index}
|
||||||
|
aria-setsize={listLength}
|
||||||
|
style={{ height: `${this.height}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} aria-setsize={listLength} data-id={id} tabIndex='0'>
|
||||||
|
{children && React.cloneElement(children, { hidden: false })}
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,179 @@
|
||||||
|
import React, { PureComponent } from 'react';
|
||||||
|
import { ScrollContainer } from 'react-router-scroll';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import IntersectionObserverArticle from './intersection_observer_article';
|
||||||
|
import LoadMore from './load_more';
|
||||||
|
import IntersectionObserverWrapper from '../features/ui/util/intersection_observer_wrapper';
|
||||||
|
import { throttle } from 'lodash';
|
||||||
|
|
||||||
|
export default class ScrollableList extends PureComponent {
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
scrollKey: PropTypes.string.isRequired,
|
||||||
|
onScrollToBottom: PropTypes.func,
|
||||||
|
onScrollToTop: PropTypes.func,
|
||||||
|
onScroll: PropTypes.func,
|
||||||
|
trackScroll: PropTypes.bool,
|
||||||
|
shouldUpdateScroll: PropTypes.func,
|
||||||
|
isLoading: PropTypes.bool,
|
||||||
|
hasMore: PropTypes.bool,
|
||||||
|
prepend: PropTypes.node,
|
||||||
|
emptyMessage: PropTypes.node,
|
||||||
|
children: PropTypes.node,
|
||||||
|
};
|
||||||
|
|
||||||
|
static defaultProps = {
|
||||||
|
trackScroll: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
intersectionObserverWrapper = new IntersectionObserverWrapper();
|
||||||
|
|
||||||
|
handleScroll = throttle(() => {
|
||||||
|
if (this.node) {
|
||||||
|
const { scrollTop, scrollHeight, clientHeight } = this.node;
|
||||||
|
const offset = scrollHeight - scrollTop - clientHeight;
|
||||||
|
this._oldScrollPosition = scrollHeight - scrollTop;
|
||||||
|
|
||||||
|
if (400 > offset && this.props.onScrollToBottom && !this.props.isLoading) {
|
||||||
|
this.props.onScrollToBottom();
|
||||||
|
} else if (scrollTop < 100 && this.props.onScrollToTop) {
|
||||||
|
this.props.onScrollToTop();
|
||||||
|
} else if (this.props.onScroll) {
|
||||||
|
this.props.onScroll();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 150, {
|
||||||
|
trailing: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
componentDidMount () {
|
||||||
|
this.attachScrollListener();
|
||||||
|
this.attachIntersectionObserver();
|
||||||
|
|
||||||
|
// Handle initial scroll posiiton
|
||||||
|
this.handleScroll();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate (prevProps) {
|
||||||
|
// 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 (React.Children.count(prevProps.children) < React.Children.count(this.props.children) && this._oldScrollPosition && this.node.scrollTop > 0) {
|
||||||
|
if (this.getFirstChildKey(prevProps) !== this.getFirstChildKey(this.props)) {
|
||||||
|
const newScrollTop = this.node.scrollHeight - this._oldScrollPosition;
|
||||||
|
if (this.node.scrollTop !== newScrollTop) {
|
||||||
|
this.node.scrollTop = newScrollTop;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this._oldScrollPosition = this.node.scrollHeight - this.node.scrollTop;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount () {
|
||||||
|
this.detachScrollListener();
|
||||||
|
this.detachIntersectionObserver();
|
||||||
|
}
|
||||||
|
|
||||||
|
attachIntersectionObserver () {
|
||||||
|
this.intersectionObserverWrapper.connect({
|
||||||
|
root: this.node,
|
||||||
|
rootMargin: '300% 0px',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
detachIntersectionObserver () {
|
||||||
|
this.intersectionObserverWrapper.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
attachScrollListener () {
|
||||||
|
this.node.addEventListener('scroll', this.handleScroll);
|
||||||
|
}
|
||||||
|
|
||||||
|
detachScrollListener () {
|
||||||
|
this.node.removeEventListener('scroll', this.handleScroll);
|
||||||
|
}
|
||||||
|
|
||||||
|
getFirstChildKey (props) {
|
||||||
|
const { children } = props;
|
||||||
|
const firstChild = Array.isArray(children) ? children[0] : children;
|
||||||
|
return firstChild && firstChild.key;
|
||||||
|
}
|
||||||
|
|
||||||
|
setRef = (c) => {
|
||||||
|
this.node = c;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleLoadMore = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
this.props.onScrollToBottom();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleKeyDown = (e) => {
|
||||||
|
if (['PageDown', 'PageUp'].includes(e.key) || (e.ctrlKey && ['End', 'Home'].includes(e.key))) {
|
||||||
|
const article = (() => {
|
||||||
|
switch (e.key) {
|
||||||
|
case 'PageDown':
|
||||||
|
return e.target.nodeName === 'ARTICLE' && e.target.nextElementSibling;
|
||||||
|
case 'PageUp':
|
||||||
|
return e.target.nodeName === 'ARTICLE' && e.target.previousElementSibling;
|
||||||
|
case 'End':
|
||||||
|
return this.node.querySelector('[role="feed"] > article:last-of-type');
|
||||||
|
case 'Home':
|
||||||
|
return this.node.querySelector('[role="feed"] > article:first-of-type');
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
|
||||||
|
if (article) {
|
||||||
|
e.preventDefault();
|
||||||
|
article.focus();
|
||||||
|
article.scrollIntoView();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render () {
|
||||||
|
const { children, scrollKey, trackScroll, shouldUpdateScroll, isLoading, hasMore, prepend, emptyMessage } = this.props;
|
||||||
|
const childrenCount = React.Children.count(children);
|
||||||
|
|
||||||
|
const loadMore = <LoadMore visible={!isLoading && childrenCount > 0 && hasMore} onClick={this.handleLoadMore} />;
|
||||||
|
let scrollableArea = null;
|
||||||
|
|
||||||
|
if (isLoading || childrenCount > 0 || !emptyMessage) {
|
||||||
|
scrollableArea = (
|
||||||
|
<div className='scrollable' ref={this.setRef}>
|
||||||
|
<div role='feed' className='item-list' onKeyDown={this.handleKeyDown}>
|
||||||
|
{prepend}
|
||||||
|
|
||||||
|
{React.Children.map(this.props.children, (child, index) => (
|
||||||
|
<IntersectionObserverArticle key={child.key} id={child.key} index={index} listLength={childrenCount} intersectionObserverWrapper={this.intersectionObserverWrapper}>
|
||||||
|
{child}
|
||||||
|
</IntersectionObserverArticle>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{loadMore}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
scrollableArea = (
|
||||||
|
<div className='empty-column-indicator' ref={this.setRef}>
|
||||||
|
{emptyMessage}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trackScroll) {
|
||||||
|
return (
|
||||||
|
<ScrollContainer scrollKey={scrollKey} shouldUpdateScroll={shouldUpdateScroll}>
|
||||||
|
{scrollableArea}
|
||||||
|
</ScrollContainer>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return scrollableArea;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -9,13 +9,11 @@ import StatusContent from './status_content';
|
||||||
import StatusActionBar from './status_action_bar';
|
import StatusActionBar from './status_action_bar';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import scheduleIdleTask from '../features/ui/util/schedule_idle_task';
|
|
||||||
import { MediaGallery, VideoPlayer } from '../features/ui/util/async-components';
|
import { MediaGallery, VideoPlayer } from '../features/ui/util/async-components';
|
||||||
|
|
||||||
// We use the component (and not the container) since we do not want
|
// We use the component (and not the container) since we do not want
|
||||||
// to use the progress bar to show download progress
|
// to use the progress bar to show download progress
|
||||||
import Bundle from '../features/ui/components/bundle';
|
import Bundle from '../features/ui/components/bundle';
|
||||||
import getRectFromEntry from '../features/ui/util/get_rect_from_entry';
|
|
||||||
|
|
||||||
export default class Status extends ImmutablePureComponent {
|
export default class Status extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
@ -26,7 +24,6 @@ export default class Status extends ImmutablePureComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
status: ImmutablePropTypes.map,
|
status: ImmutablePropTypes.map,
|
||||||
account: ImmutablePropTypes.map,
|
account: ImmutablePropTypes.map,
|
||||||
wrapped: PropTypes.bool,
|
|
||||||
onReply: PropTypes.func,
|
onReply: PropTypes.func,
|
||||||
onFavourite: PropTypes.func,
|
onFavourite: PropTypes.func,
|
||||||
onReblog: PropTypes.func,
|
onReblog: PropTypes.func,
|
||||||
|
@ -40,14 +37,11 @@ export default class Status extends ImmutablePureComponent {
|
||||||
boostModal: PropTypes.bool,
|
boostModal: PropTypes.bool,
|
||||||
autoPlayGif: PropTypes.bool,
|
autoPlayGif: PropTypes.bool,
|
||||||
muted: PropTypes.bool,
|
muted: PropTypes.bool,
|
||||||
intersectionObserverWrapper: PropTypes.object,
|
hidden: PropTypes.bool,
|
||||||
index: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
|
||||||
listLength: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
isExpanded: false,
|
isExpanded: false,
|
||||||
isHidden: false, // set to true in requestIdleCallback to trigger un-render
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Avoid checking props that are functions (and whose equality will always
|
// Avoid checking props that are functions (and whose equality will always
|
||||||
|
@ -55,91 +49,15 @@ export default class Status extends ImmutablePureComponent {
|
||||||
updateOnProps = [
|
updateOnProps = [
|
||||||
'status',
|
'status',
|
||||||
'account',
|
'account',
|
||||||
'wrapped',
|
|
||||||
'me',
|
'me',
|
||||||
'boostModal',
|
'boostModal',
|
||||||
'autoPlayGif',
|
'autoPlayGif',
|
||||||
'muted',
|
'muted',
|
||||||
'listLength',
|
'hidden',
|
||||||
]
|
]
|
||||||
|
|
||||||
updateOnStates = ['isExpanded']
|
updateOnStates = ['isExpanded']
|
||||||
|
|
||||||
shouldComponentUpdate (nextProps, nextState) {
|
|
||||||
if (!nextState.isIntersecting && nextState.isHidden) {
|
|
||||||
// It's only if we're not intersecting (i.e. offscreen) and isHidden is true
|
|
||||||
// that either "isIntersecting" or "isHidden" matter, and then they're
|
|
||||||
// the only things that matter (and updated ARIA attributes).
|
|
||||||
return this.state.isIntersecting || !this.state.isHidden || nextProps.listLength !== this.props.listLength;
|
|
||||||
} else if (nextState.isIntersecting && !this.state.isIntersecting) {
|
|
||||||
// If we're going from a non-intersecting state to an intersecting state,
|
|
||||||
// (i.e. offscreen to onscreen), then we definitely need to re-render
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
// Otherwise, diff based on "updateOnProps" and "updateOnStates"
|
|
||||||
return super.shouldComponentUpdate(nextProps, nextState);
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount () {
|
|
||||||
if (!this.props.intersectionObserverWrapper) {
|
|
||||||
// TODO: enable IntersectionObserver optimization for notification statuses.
|
|
||||||
// These are managed in notifications/index.js rather than status_list.js
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.props.intersectionObserverWrapper.observe(
|
|
||||||
this.props.id,
|
|
||||||
this.node,
|
|
||||||
this.handleIntersection
|
|
||||||
);
|
|
||||||
|
|
||||||
this.componentMounted = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount () {
|
|
||||||
if (this.props.intersectionObserverWrapper) {
|
|
||||||
this.props.intersectionObserverWrapper.unobserve(this.props.id, this.node);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.componentMounted = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleIntersection = (entry) => {
|
|
||||||
if (this.node && this.node.children.length !== 0) {
|
|
||||||
// save the height of the fully-rendered element
|
|
||||||
this.height = getRectFromEntry(entry).height;
|
|
||||||
|
|
||||||
if (this.props.onHeightChange) {
|
|
||||||
this.props.onHeightChange(this.props.status, this.height);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState((prevState) => {
|
|
||||||
if (prevState.isIntersecting && !entry.isIntersecting) {
|
|
||||||
scheduleIdleTask(this.hideIfNotIntersecting);
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
isIntersecting: entry.isIntersecting,
|
|
||||||
isHidden: false,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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.
|
|
||||||
// See: https://github.com/tootsuite/mastodon/issues/2900
|
|
||||||
this.setState((prevState) => ({ isHidden: !prevState.isIntersecting }));
|
|
||||||
}
|
|
||||||
|
|
||||||
handleRef = (node) => {
|
|
||||||
this.node = node;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleClick = () => {
|
handleClick = () => {
|
||||||
if (!this.context.router) {
|
if (!this.context.router) {
|
||||||
return;
|
return;
|
||||||
|
@ -173,25 +91,19 @@ export default class Status extends ImmutablePureComponent {
|
||||||
let media = null;
|
let media = null;
|
||||||
let statusAvatar;
|
let statusAvatar;
|
||||||
|
|
||||||
// Exclude intersectionObserverWrapper from `other` variable
|
const { status, account, hidden, ...other } = this.props;
|
||||||
// because intersection is managed in here.
|
const { isExpanded } = this.state;
|
||||||
const { status, account, intersectionObserverWrapper, index, listLength, wrapped, ...other } = this.props;
|
|
||||||
const { isExpanded, isIntersecting, isHidden } = this.state;
|
|
||||||
|
|
||||||
if (status === null) {
|
if (status === null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const hasIntersectionObserverWrapper = !!this.props.intersectionObserverWrapper;
|
if (hidden) {
|
||||||
const isHiddenForSure = isIntersecting === false && isHidden;
|
|
||||||
const visibilityUnknownButHeightIsCached = isIntersecting === undefined && status.has('height');
|
|
||||||
|
|
||||||
if (hasIntersectionObserverWrapper && (isHiddenForSure || visibilityUnknownButHeightIsCached)) {
|
|
||||||
return (
|
return (
|
||||||
<article ref={this.handleRef} data-id={status.get('id')} aria-posinset={index} aria-setsize={listLength} tabIndex='0' style={{ height: `${this.height || status.get('height')}px`, opacity: 0, overflow: 'hidden' }}>
|
<div>
|
||||||
{status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])}
|
{status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])}
|
||||||
{status.get('content')}
|
{status.get('content')}
|
||||||
</article>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -199,14 +111,14 @@ export default class Status extends ImmutablePureComponent {
|
||||||
const display_name_html = { __html: status.getIn(['account', 'display_name_html']) };
|
const display_name_html = { __html: status.getIn(['account', 'display_name_html']) };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<article className='status__wrapper' ref={this.handleRef} data-id={status.get('id')} aria-posinset={index} aria-setsize={listLength} tabIndex='0'>
|
<div className='status__wrapper' data-id={status.get('id')} >
|
||||||
<div className='status__prepend'>
|
<div className='status__prepend'>
|
||||||
<div className='status__prepend-icon-wrapper'><i className='fa fa-fw fa-retweet status__prepend-icon' /></div>
|
<div className='status__prepend-icon-wrapper'><i className='fa fa-fw fa-retweet status__prepend-icon' /></div>
|
||||||
<FormattedMessage id='status.reblogged_by' defaultMessage='{name} boosted' values={{ name: <a onClick={this.handleAccountClick} data-id={status.getIn(['account', 'id'])} href={status.getIn(['account', 'url'])} className='status__display-name muted'><strong dangerouslySetInnerHTML={display_name_html} /></a> }} />
|
<FormattedMessage id='status.reblogged_by' defaultMessage='{name} boosted' values={{ name: <a onClick={this.handleAccountClick} data-id={status.getIn(['account', 'id'])} href={status.getIn(['account', 'url'])} className='status__display-name muted'><strong dangerouslySetInnerHTML={display_name_html} /></a> }} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Status {...other} wrapped status={status.get('reblog')} account={status.get('account')} />
|
<Status {...other} status={status.get('reblog')} account={status.get('account')} />
|
||||||
</article>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -235,7 +147,7 @@ export default class Status extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<article aria-posinset={index} aria-setsize={listLength} className={`status ${this.props.muted ? 'muted' : ''} status-${status.get('visibility')}`} data-id={status.get('id')} tabIndex={wrapped ? null : '0'} ref={this.handleRef}>
|
<div className={`status ${this.props.muted ? 'muted' : ''} status-${status.get('visibility')}`} data-id={status.get('id')}>
|
||||||
<div className='status__info'>
|
<div className='status__info'>
|
||||||
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'><RelativeTimestamp timestamp={status.get('created_at')} /></a>
|
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'><RelativeTimestamp timestamp={status.get('created_at')} /></a>
|
||||||
|
|
||||||
|
@ -253,7 +165,7 @@ export default class Status extends ImmutablePureComponent {
|
||||||
{media}
|
{media}
|
||||||
|
|
||||||
<StatusActionBar {...this.props} />
|
<StatusActionBar {...this.props} />
|
||||||
</article>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,9 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import { ScrollContainer } from 'react-router-scroll';
|
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import StatusContainer from '../containers/status_container';
|
import StatusContainer from '../containers/status_container';
|
||||||
import LoadMore from './load_more';
|
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import IntersectionObserverWrapper from '../features/ui/util/intersection_observer_wrapper';
|
import ScrollableList from './scrollable_list';
|
||||||
import { throttle } from 'lodash';
|
|
||||||
|
|
||||||
export default class StatusList extends ImmutablePureComponent {
|
export default class StatusList extends ImmutablePureComponent {
|
||||||
|
|
||||||
|
@ -28,145 +25,21 @@ export default class StatusList extends ImmutablePureComponent {
|
||||||
trackScroll: true,
|
trackScroll: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
intersectionObserverWrapper = new IntersectionObserverWrapper();
|
|
||||||
|
|
||||||
handleScroll = throttle(() => {
|
|
||||||
if (this.node) {
|
|
||||||
const { scrollTop, scrollHeight, clientHeight } = this.node;
|
|
||||||
const offset = scrollHeight - scrollTop - clientHeight;
|
|
||||||
this._oldScrollPosition = scrollHeight - scrollTop;
|
|
||||||
|
|
||||||
if (400 > offset && this.props.onScrollToBottom && !this.props.isLoading) {
|
|
||||||
this.props.onScrollToBottom();
|
|
||||||
} else if (scrollTop < 100 && this.props.onScrollToTop) {
|
|
||||||
this.props.onScrollToTop();
|
|
||||||
} else if (this.props.onScroll) {
|
|
||||||
this.props.onScroll();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, 150, {
|
|
||||||
trailing: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
componentDidMount () {
|
|
||||||
this.attachScrollListener();
|
|
||||||
this.attachIntersectionObserver();
|
|
||||||
|
|
||||||
// Handle initial scroll posiiton
|
|
||||||
this.handleScroll();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate (prevProps) {
|
|
||||||
// Reset the scroll position when a new toot comes in in order not to
|
|
||||||
// jerk the scrollbar around if you're already scrolled down the page.
|
|
||||||
if (prevProps.statusIds.size < this.props.statusIds.size && this._oldScrollPosition && this.node.scrollTop > 0) {
|
|
||||||
if (prevProps.statusIds.first() !== this.props.statusIds.first()) {
|
|
||||||
let newScrollTop = this.node.scrollHeight - this._oldScrollPosition;
|
|
||||||
if (this.node.scrollTop !== newScrollTop) {
|
|
||||||
this.node.scrollTop = newScrollTop;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this._oldScrollPosition = this.node.scrollHeight - this.node.scrollTop;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount () {
|
|
||||||
this.detachScrollListener();
|
|
||||||
this.detachIntersectionObserver();
|
|
||||||
}
|
|
||||||
|
|
||||||
attachIntersectionObserver () {
|
|
||||||
this.intersectionObserverWrapper.connect({
|
|
||||||
root: this.node,
|
|
||||||
rootMargin: '300% 0px',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
detachIntersectionObserver () {
|
|
||||||
this.intersectionObserverWrapper.disconnect();
|
|
||||||
}
|
|
||||||
|
|
||||||
attachScrollListener () {
|
|
||||||
this.node.addEventListener('scroll', this.handleScroll);
|
|
||||||
}
|
|
||||||
|
|
||||||
detachScrollListener () {
|
|
||||||
this.node.removeEventListener('scroll', this.handleScroll);
|
|
||||||
}
|
|
||||||
|
|
||||||
setRef = (c) => {
|
|
||||||
this.node = c;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleLoadMore = (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
this.props.onScrollToBottom();
|
|
||||||
}
|
|
||||||
|
|
||||||
handleKeyDown = (e) => {
|
|
||||||
if (['PageDown', 'PageUp'].includes(e.key) || (e.ctrlKey && ['End', 'Home'].includes(e.key))) {
|
|
||||||
const article = (() => {
|
|
||||||
switch (e.key) {
|
|
||||||
case 'PageDown':
|
|
||||||
return e.target.nodeName === 'ARTICLE' && e.target.nextElementSibling;
|
|
||||||
case 'PageUp':
|
|
||||||
return e.target.nodeName === 'ARTICLE' && e.target.previousElementSibling;
|
|
||||||
case 'End':
|
|
||||||
return this.node.querySelector('[role="feed"] > article:last-of-type');
|
|
||||||
case 'Home':
|
|
||||||
return this.node.querySelector('[role="feed"] > article:first-of-type');
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
|
|
||||||
if (article) {
|
|
||||||
e.preventDefault();
|
|
||||||
article.focus();
|
|
||||||
article.scrollIntoView();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { statusIds, scrollKey, trackScroll, shouldUpdateScroll, isLoading, hasMore, prepend, emptyMessage } = this.props;
|
const { statusIds, ...other } = this.props;
|
||||||
|
const { isLoading } = other;
|
||||||
|
|
||||||
const loadMore = <LoadMore visible={!isLoading && statusIds.size > 0 && hasMore} onClick={this.handleLoadMore} />;
|
const scrollableContent = (isLoading || statusIds.size > 0) ? (
|
||||||
let scrollableArea = null;
|
statusIds.map((statusId) => (
|
||||||
|
<StatusContainer key={statusId} id={statusId} />
|
||||||
|
))
|
||||||
|
) : null;
|
||||||
|
|
||||||
if (isLoading || statusIds.size > 0 || !emptyMessage) {
|
|
||||||
scrollableArea = (
|
|
||||||
<div className='scrollable' ref={this.setRef}>
|
|
||||||
<div role='feed' className='status-list' onKeyDown={this.handleKeyDown}>
|
|
||||||
{prepend}
|
|
||||||
|
|
||||||
{statusIds.map((statusId, index) => {
|
|
||||||
return <StatusContainer key={statusId} id={statusId} index={index} listLength={statusIds.size} intersectionObserverWrapper={this.intersectionObserverWrapper} />;
|
|
||||||
})}
|
|
||||||
|
|
||||||
{loadMore}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
scrollableArea = (
|
|
||||||
<div className='empty-column-indicator' ref={this.setRef}>
|
|
||||||
{emptyMessage}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (trackScroll) {
|
|
||||||
return (
|
return (
|
||||||
<ScrollContainer scrollKey={scrollKey} shouldUpdateScroll={shouldUpdateScroll}>
|
<ScrollableList {...other}>
|
||||||
{scrollableArea}
|
{scrollableContent}
|
||||||
</ScrollContainer>
|
</ScrollableList>
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
return scrollableArea;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@ const messages = defineMessages({
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
statusIds: state.getIn(['status_lists', 'favourites', 'items']),
|
statusIds: state.getIn(['status_lists', 'favourites', 'items']),
|
||||||
|
hasMore: !!state.getIn(['status_lists', 'favourites', 'next']),
|
||||||
});
|
});
|
||||||
|
|
||||||
@connect(mapStateToProps)
|
@connect(mapStateToProps)
|
||||||
|
@ -28,6 +29,7 @@ export default class Favourites extends ImmutablePureComponent {
|
||||||
intl: PropTypes.object.isRequired,
|
intl: PropTypes.object.isRequired,
|
||||||
columnId: PropTypes.string,
|
columnId: PropTypes.string,
|
||||||
multiColumn: PropTypes.bool,
|
multiColumn: PropTypes.bool,
|
||||||
|
hasMore: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
componentWillMount () {
|
componentWillMount () {
|
||||||
|
@ -62,7 +64,7 @@ export default class Favourites extends ImmutablePureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render () {
|
||||||
const { intl, statusIds, columnId, multiColumn } = this.props;
|
const { intl, statusIds, columnId, multiColumn, hasMore } = this.props;
|
||||||
const pinned = !!columnId;
|
const pinned = !!columnId;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -81,6 +83,7 @@ export default class Favourites extends ImmutablePureComponent {
|
||||||
trackScroll={!pinned}
|
trackScroll={!pinned}
|
||||||
statusIds={statusIds}
|
statusIds={statusIds}
|
||||||
scrollKey={`favourited_statuses-${columnId}`}
|
scrollKey={`favourited_statuses-${columnId}`}
|
||||||
|
hasMore={hasMore}
|
||||||
onScrollToBottom={this.handleScrollToBottom}
|
onScrollToBottom={this.handleScrollToBottom}
|
||||||
/>
|
/>
|
||||||
</Column>
|
</Column>
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
import ImmutablePropTypes from 'react-immutable-proptypes';
|
import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import StatusContainer from '../../../containers/status_container';
|
import StatusContainer from '../../../containers/status_container';
|
||||||
import AccountContainer from '../../../containers/account_container';
|
import AccountContainer from '../../../containers/account_container';
|
||||||
|
@ -10,6 +11,7 @@ export default class Notification extends ImmutablePureComponent {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
notification: ImmutablePropTypes.map.isRequired,
|
notification: ImmutablePropTypes.map.isRequired,
|
||||||
|
hidden: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
renderFollow (account, link) {
|
renderFollow (account, link) {
|
||||||
|
@ -23,13 +25,13 @@ export default class Notification extends ImmutablePureComponent {
|
||||||
<FormattedMessage id='notification.follow' defaultMessage='{name} followed you' values={{ name: link }} />
|
<FormattedMessage id='notification.follow' defaultMessage='{name} followed you' values={{ name: link }} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<AccountContainer id={account.get('id')} withNote={false} />
|
<AccountContainer id={account.get('id')} withNote={false} hidden={this.props.hidden} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderMention (notification) {
|
renderMention (notification) {
|
||||||
return <StatusContainer id={notification.get('status')} withDismiss />;
|
return <StatusContainer id={notification.get('status')} withDismiss hidden={this.props.hidden} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderFavourite (notification, link) {
|
renderFavourite (notification, link) {
|
||||||
|
@ -42,7 +44,7 @@ export default class Notification extends ImmutablePureComponent {
|
||||||
<FormattedMessage id='notification.favourite' defaultMessage='{name} favourited your status' values={{ name: link }} />
|
<FormattedMessage id='notification.favourite' defaultMessage='{name} favourited your status' values={{ name: link }} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<StatusContainer id={notification.get('status')} account={notification.get('account')} muted withDismiss />
|
<StatusContainer id={notification.get('status')} account={notification.get('account')} muted withDismiss hidden={!!this.props.hidden} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -57,7 +59,7 @@ export default class Notification extends ImmutablePureComponent {
|
||||||
<FormattedMessage id='notification.reblog' defaultMessage='{name} boosted your status' values={{ name: link }} />
|
<FormattedMessage id='notification.reblog' defaultMessage='{name} boosted your status' values={{ name: link }} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<StatusContainer id={notification.get('status')} account={notification.get('account')} muted withDismiss />
|
<StatusContainer id={notification.get('status')} account={notification.get('account')} muted withDismiss hidden={this.props.hidden} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,13 +7,12 @@ import ColumnHeader from '../../components/column_header';
|
||||||
import { expandNotifications, scrollTopNotifications } from '../../actions/notifications';
|
import { expandNotifications, scrollTopNotifications } from '../../actions/notifications';
|
||||||
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
|
import { addColumn, removeColumn, moveColumn } from '../../actions/columns';
|
||||||
import NotificationContainer from './containers/notification_container';
|
import NotificationContainer from './containers/notification_container';
|
||||||
import { ScrollContainer } from 'react-router-scroll';
|
|
||||||
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
|
||||||
import ColumnSettingsContainer from './containers/column_settings_container';
|
import ColumnSettingsContainer from './containers/column_settings_container';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { List as ImmutableList } from 'immutable';
|
import { List as ImmutableList } from 'immutable';
|
||||||
import LoadMore from '../../components/load_more';
|
|
||||||
import { debounce } from 'lodash';
|
import { debounce } from 'lodash';
|
||||||
|
import ScrollableList from '../../components/scrollable_list';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
title: { id: 'column.notifications', defaultMessage: 'Notifications' },
|
title: { id: 'column.notifications', defaultMessage: 'Notifications' },
|
||||||
|
@ -51,40 +50,18 @@ export default class Notifications extends React.PureComponent {
|
||||||
trackScroll: true,
|
trackScroll: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
dispatchExpandNotifications = debounce(() => {
|
handleScrollToBottom = debounce(() => {
|
||||||
|
this.props.dispatch(scrollTopNotifications(false));
|
||||||
this.props.dispatch(expandNotifications());
|
this.props.dispatch(expandNotifications());
|
||||||
}, 300, { leading: true });
|
}, 300, { leading: true });
|
||||||
|
|
||||||
dispatchScrollToTop = debounce((top) => {
|
handleScrollToTop = debounce(() => {
|
||||||
this.props.dispatch(scrollTopNotifications(top));
|
this.props.dispatch(scrollTopNotifications(true));
|
||||||
}, 100);
|
}, 100);
|
||||||
|
|
||||||
handleScroll = (e) => {
|
handleScroll = debounce(() => {
|
||||||
const { scrollTop, scrollHeight, clientHeight } = e.target;
|
this.props.dispatch(scrollTopNotifications(false));
|
||||||
const offset = scrollHeight - scrollTop - clientHeight;
|
}, 100);
|
||||||
this._oldScrollPosition = scrollHeight - scrollTop;
|
|
||||||
|
|
||||||
if (250 > offset && this.props.hasMore && !this.props.isLoading) {
|
|
||||||
this.dispatchExpandNotifications();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (scrollTop < 100) {
|
|
||||||
this.dispatchScrollToTop(true);
|
|
||||||
} else {
|
|
||||||
this.dispatchScrollToTop(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate (prevProps) {
|
|
||||||
if (this.node.scrollTop > 0 && (prevProps.notifications.size < this.props.notifications.size && prevProps.notifications.first() !== this.props.notifications.first() && !!this._oldScrollPosition)) {
|
|
||||||
this.node.scrollTop = this.node.scrollHeight - this._oldScrollPosition;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleLoadMore = (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
this.dispatchExpandNotifications();
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePin = () => {
|
handlePin = () => {
|
||||||
const { columnId, dispatch } = this.props;
|
const { columnId, dispatch } = this.props;
|
||||||
|
@ -105,10 +82,6 @@ export default class Notifications extends React.PureComponent {
|
||||||
this.column.scrollTop();
|
this.column.scrollTop();
|
||||||
}
|
}
|
||||||
|
|
||||||
setRef = (c) => {
|
|
||||||
this.node = c;
|
|
||||||
}
|
|
||||||
|
|
||||||
setColumnRef = c => {
|
setColumnRef = c => {
|
||||||
this.column = c;
|
this.column = c;
|
||||||
}
|
}
|
||||||
|
@ -116,52 +89,34 @@ export default class Notifications extends React.PureComponent {
|
||||||
render () {
|
render () {
|
||||||
const { intl, notifications, shouldUpdateScroll, isLoading, isUnread, columnId, multiColumn, hasMore } = this.props;
|
const { intl, notifications, shouldUpdateScroll, isLoading, isUnread, columnId, multiColumn, hasMore } = this.props;
|
||||||
const pinned = !!columnId;
|
const pinned = !!columnId;
|
||||||
|
const emptyMessage = <FormattedMessage id='empty_column.notifications' defaultMessage="You don't have any notifications yet. Interact with others to start the conversation." />;
|
||||||
|
|
||||||
let loadMore = '';
|
let scrollableContent = null;
|
||||||
let scrollableArea = '';
|
|
||||||
let unread = '';
|
|
||||||
let scrollContainer = '';
|
|
||||||
|
|
||||||
if (!isLoading && hasMore) {
|
if (isLoading && this.scrollableContent) {
|
||||||
loadMore = <LoadMore onClick={this.handleLoadMore} />;
|
scrollableContent = this.scrollableContent;
|
||||||
}
|
|
||||||
|
|
||||||
if (isUnread) {
|
|
||||||
unread = <div className='notifications__unread-indicator' />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isLoading && this.scrollableArea) {
|
|
||||||
scrollableArea = this.scrollableArea;
|
|
||||||
} else if (notifications.size > 0 || hasMore) {
|
} else if (notifications.size > 0 || hasMore) {
|
||||||
scrollableArea = (
|
scrollableContent = notifications.map((item) => <NotificationContainer key={item.get('id')} notification={item} accountId={item.get('account')} />);
|
||||||
<div className='scrollable' onScroll={this.handleScroll} ref={this.setRef}>
|
|
||||||
{unread}
|
|
||||||
|
|
||||||
<div>
|
|
||||||
{notifications.map(item => <NotificationContainer key={item.get('id')} notification={item} accountId={item.get('account')} />)}
|
|
||||||
{loadMore}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
scrollableArea = (
|
scrollableContent = null;
|
||||||
<div className='empty-column-indicator' ref={this.setRef}>
|
|
||||||
<FormattedMessage id='empty_column.notifications' defaultMessage="You don't have any notifications yet. Interact with others to start the conversation." />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pinned) {
|
this.scrollableContent = scrollableContent;
|
||||||
scrollContainer = scrollableArea;
|
|
||||||
} else {
|
|
||||||
scrollContainer = (
|
|
||||||
<ScrollContainer scrollKey={`notifications-${columnId}`} shouldUpdateScroll={shouldUpdateScroll}>
|
|
||||||
{scrollableArea}
|
|
||||||
</ScrollContainer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.scrollableArea = scrollableArea;
|
const scrollContainer = (
|
||||||
|
<ScrollableList
|
||||||
|
scrollKey={`notifications-${columnId}`}
|
||||||
|
isLoading={isLoading}
|
||||||
|
hasMore={hasMore}
|
||||||
|
emptyMessage={emptyMessage}
|
||||||
|
onScrollToBottom={this.handleScrollToBottom}
|
||||||
|
onScrollToTop={this.handleScrollToTop}
|
||||||
|
onScroll={this.handleScroll}
|
||||||
|
shouldUpdateScroll={shouldUpdateScroll}
|
||||||
|
>
|
||||||
|
{scrollableContent}
|
||||||
|
</ScrollableList>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column ref={this.setColumnRef}>
|
<Column ref={this.setColumnRef}>
|
||||||
|
|
Reference in New Issue