diff --git a/src/components/views/elements/LazyRenderList.js b/src/components/views/elements/LazyRenderList.js
index d7d2a0ab99..0fc0ef6733 100644
--- a/src/components/views/elements/LazyRenderList.js
+++ b/src/components/views/elements/LazyRenderList.js
@@ -15,9 +15,7 @@ limitations under the License.
*/
import React from "react";
-
-const OVERFLOW_ITEMS = 20;
-const OVERFLOW_MARGIN = 5;
+import PropTypes from 'prop-types';
class ItemRange {
constructor(topCount, renderCount, bottomCount) {
@@ -27,11 +25,22 @@ class ItemRange {
}
contains(range) {
+ // don't contain empty ranges
+ // as it will prevent clearing the list
+ // once it is scrolled far enough out of view
+ if (!range.renderCount && this.renderCount) {
+ return false;
+ }
return range.topCount >= this.topCount &&
(range.topCount + range.renderCount) <= (this.topCount + this.renderCount);
}
expand(amount) {
+ // don't expand ranges that won't render anything
+ if (this.renderCount === 0) {
+ return this;
+ }
+
const topGrow = Math.min(amount, this.topCount);
const bottomGrow = Math.min(amount, this.bottomCount);
return new ItemRange(
@@ -40,52 +49,80 @@ class ItemRange {
this.bottomCount - bottomGrow,
);
}
+
+ totalSize() {
+ return this.topCount + this.renderCount + this.bottomCount;
+ }
}
export default class LazyRenderList extends React.Component {
- constructor(props) {
- super(props);
- const renderRange = LazyRenderList.getVisibleRangeFromProps(props).expand(OVERFLOW_ITEMS);
- this.state = {renderRange};
+ static getDerivedStateFromProps(props, state) {
+ const range = LazyRenderList.getVisibleRangeFromProps(props);
+ const intersectRange = range.expand(props.overflowMargin);
+ const renderRange = range.expand(props.overflowItems);
+ const listHasChangedSize = !!state && renderRange.totalSize() !== state.renderRange.totalSize();
+ // only update render Range if the list has shrunk/grown and we need to adjust padding OR
+ // if the new range + overflowMargin isn't contained by the old anymore
+ if (listHasChangedSize || !state || !state.renderRange.contains(intersectRange)) {
+ return {renderRange};
+ }
+ return null;
}
static getVisibleRangeFromProps(props) {
const {items, itemHeight, scrollTop, height} = props;
const length = items ? items.length : 0;
- const topCount = Math.max(0, Math.floor(scrollTop / itemHeight));
+ const topCount = Math.min(Math.max(0, Math.floor(scrollTop / itemHeight)), length);
const itemsAfterTop = length - topCount;
- const renderCount = Math.min(Math.ceil(height / itemHeight), itemsAfterTop);
+ const visibleItems = height !== 0 ? Math.ceil(height / itemHeight) : 0;
+ const renderCount = Math.min(visibleItems, itemsAfterTop);
const bottomCount = itemsAfterTop - renderCount;
return new ItemRange(topCount, renderCount, bottomCount);
}
- componentWillReceiveProps(props) {
- const state = this.state;
- const range = LazyRenderList.getVisibleRangeFromProps(props);
- const intersectRange = range.expand(OVERFLOW_MARGIN);
-
- const prevSize = this.props.items ? this.props.items.length : 0;
- const listHasChangedSize = props.items.length !== prevSize;
- // only update renderRange if the list has shrunk/grown and we need to adjust padding or
- // if the new range isn't contained by the old anymore
- if (listHasChangedSize || !state.renderRange || !state.renderRange.contains(intersectRange)) {
- this.setState({renderRange: range.expand(OVERFLOW_ITEMS)});
- }
- }
-
render() {
const {itemHeight, items, renderItem} = this.props;
-
const {renderRange} = this.state;
- const paddingTop = renderRange.topCount * itemHeight;
- const paddingBottom = renderRange.bottomCount * itemHeight;
+ const {topCount, renderCount, bottomCount} = renderRange;
+
+ const paddingTop = topCount * itemHeight;
+ const paddingBottom = bottomCount * itemHeight;
const renderedItems = (items || []).slice(
- renderRange.topCount,
- renderRange.topCount + renderRange.renderCount,
+ topCount,
+ topCount + renderCount,
);
- return (
- { renderedItems.map(renderItem) }
-
);
+ const element = this.props.element || "div";
+ const elementProps = {
+ "style": {paddingTop: `${paddingTop}px`, paddingBottom: `${paddingBottom}px`},
+ "className": this.props.className,
+ };
+ return React.createElement(element, elementProps, renderedItems.map(renderItem));
}
}
+
+LazyRenderList.defaultProps = {
+ overflowItems: 20,
+ overflowMargin: 5,
+};
+
+LazyRenderList.propTypes = {
+ // height in pixels of the component returned by `renderItem`
+ itemHeight: PropTypes.number.isRequired,
+ // function to turn an element of `items` into a react component
+ renderItem: PropTypes.func.isRequired,
+ // scrollTop of the viewport (minus the height of any content above this list like other `LazyRenderList`s)
+ scrollTop: PropTypes.number.isRequired,
+ // the height of the viewport this content is scrolled in
+ height: PropTypes.number.isRequired,
+ // all items for the list. These should not be react components, see `renderItem`.
+ items: PropTypes.array,
+ // the amount of items to scroll before causing a rerender,
+ // should typically be less than `overflowItems` unless applying
+ // margins in the parent component when using multiple LazyRenderList in one viewport.
+ // use 0 to only rerender when items will come into view.
+ overflowMargin: PropTypes.number,
+ // the amount of items to add at the top and bottom to render,
+ // so not every scroll of causes a rerender.
+ overflowItems: PropTypes.number,
+};
diff --git a/src/components/views/emojipicker/Category.js b/src/components/views/emojipicker/Category.js
index ba48c8842b..ba525b76e2 100644
--- a/src/components/views/emojipicker/Category.js
+++ b/src/components/views/emojipicker/Category.js
@@ -16,9 +16,11 @@ limitations under the License.
import React from 'react';
import PropTypes from 'prop-types';
-
+import { CATEGORY_HEADER_HEIGHT, EMOJI_HEIGHT, EMOJIS_PER_ROW } from "./EmojiPicker";
import sdk from '../../../index';
+const OVERFLOW_ROWS = 3;
+
class Category extends React.PureComponent {
static propTypes = {
emojis: PropTypes.arrayOf(PropTypes.object).isRequired,
@@ -30,22 +32,54 @@ class Category extends React.PureComponent {
selectedEmojis: PropTypes.instanceOf(Set),
};
+ _renderEmojiRow = (rowIndex) => {
+ const { onClick, onMouseEnter, onMouseLeave, selectedEmojis, emojis } = this.props;
+ const emojisForRow = emojis.slice(rowIndex * 8, (rowIndex + 1) * 8);
+ const Emoji = sdk.getComponent("emojipicker.Emoji");
+ return ({
+ emojisForRow.map(emoji =>
+ )
+ }
);
+ };
+
render() {
- const { onClick, onMouseEnter, onMouseLeave, emojis, name, selectedEmojis } = this.props;
+ const { emojis, name, heightBefore, viewportHeight, scrollTop } = this.props;
if (!emojis || emojis.length === 0) {
return null;
}
+ const rows = new Array(Math.ceil(emojis.length / EMOJIS_PER_ROW));
+ for (let counter = 0; counter < rows.length; ++counter) {
+ rows[counter] = counter;
+ }
+ const LazyRenderList = sdk.getComponent('elements.LazyRenderList');
+
+ const viewportTop = scrollTop;
+ const viewportBottom = viewportTop + viewportHeight;
+ const listTop = heightBefore + CATEGORY_HEADER_HEIGHT;
+ const listBottom = listTop + (rows.length * EMOJI_HEIGHT);
+ const top = Math.max(viewportTop, listTop);
+ const bottom = Math.min(viewportBottom, listBottom);
+ // the viewport height and scrollTop passed to the LazyRenderList
+ // is capped at the intersection with the real viewport, so lists
+ // out of view are passed height 0, so they won't render any items.
+ const localHeight = Math.max(0, bottom - top);
+ const localScrollTop = Math.max(0, scrollTop - listTop);
- const Emoji = sdk.getComponent("emojipicker.Emoji");
return (
{name}
-
- {emojis.map(emoji => )}
-
+
+
);
}
diff --git a/src/components/views/emojipicker/EmojiPicker.js b/src/components/views/emojipicker/EmojiPicker.js
index 6d34804187..48713b90d8 100644
--- a/src/components/views/emojipicker/EmojiPicker.js
+++ b/src/components/views/emojipicker/EmojiPicker.js
@@ -58,6 +58,10 @@ EMOJIBASE.forEach(emoji => {
emoji.filterString = `${emoji.annotation}\n${emoji.shortcodes.join('\n')}}\n${emoji.emoticon || ''}`;
});
+export const CATEGORY_HEADER_HEIGHT = 22;
+export const EMOJI_HEIGHT = 37;
+export const EMOJIS_PER_ROW = 8;
+
class EmojiPicker extends React.Component {
static propTypes = {
onChoose: PropTypes.func.isRequired,
@@ -71,6 +75,11 @@ class EmojiPicker extends React.Component {
this.state = {
filter: "",
previewEmoji: null,
+ scrollTop: 0,
+ // initial estimation of height, dialog is hardcoded to 450px height.
+ // should be enough to never have blank rows of emojis as
+ // 3 rows of overflow are also rendered. The actual value is updated on scroll.
+ viewportHeight: 280,
};
this.recentlyUsed = recent.get().map(unicode => DATA_BY_EMOJI[unicode]);
@@ -145,10 +154,20 @@ class EmojiPicker extends React.Component {
this.updateVisibility = this.updateVisibility.bind(this);
}
+ onScroll = () => {
+ const body = this.bodyRef.current;
+ this.setState({
+ scrollTop: body.scrollTop,
+ viewportHeight: body.clientHeight,
+ });
+ this.updateVisibility();
+ };
+
updateVisibility() {
- const rect = this.bodyRef.current.getBoundingClientRect();
+ const body = this.bodyRef.current;
+ const rect = body.getBoundingClientRect();
for (const cat of this.categories) {
- const elem = this.bodyRef.current.querySelector(`[data-category-id="${cat.id}"]`);
+ const elem = body.querySelector(`[data-category-id="${cat.id}"]`);
if (!elem) {
cat.visible = false;
cat.ref.current.classList.remove("mx_EmojiPicker_anchor_visible");
@@ -210,23 +229,36 @@ class EmojiPicker extends React.Component {
}
}
+ _categoryHeightForEmojiCount(count) {
+ if (count === 0) {
+ return 0;
+ }
+ return CATEGORY_HEADER_HEIGHT + (Math.ceil(count / EMOJIS_PER_ROW) * EMOJI_HEIGHT);
+ }
+
render() {
const Header = sdk.getComponent("emojipicker.Header");
const Search = sdk.getComponent("emojipicker.Search");
const Category = sdk.getComponent("emojipicker.Category");
const Preview = sdk.getComponent("emojipicker.Preview");
const QuickReactions = sdk.getComponent("emojipicker.QuickReactions");
+ let heightBefore = 0;
return (
-
- {this.categories.map(category => (
-
+ {this.categories.map(category => {
+ const emojis = this.memoizedDataByCategory[category.id];
+ const categoryElement = (
- ))}
+ selectedEmojis={this.props.selectedEmojis} />);
+ const height = this._categoryHeightForEmojiCount(emojis.length);
+ heightBefore += height;
+ return categoryElement;
+ })}
{this.state.previewEmoji || !this.props.showQuickReactions
?