dispatch/client/js/components/pages/Chat/MessageBox.js

307 lines
7.4 KiB
JavaScript
Raw Normal View History

import React, { PureComponent, createRef } from 'react';
import { VariableSizeList as List } from 'react-window';
import AutoSizer from 'react-virtualized-auto-sizer';
2017-05-02 21:21:25 +00:00
import debounce from 'lodash/debounce';
2018-12-14 13:24:23 +00:00
import { formatDate, measureScrollBarWidth } from 'utils';
import { getScrollPos, saveScrollPos } from 'utils/scrollPosition';
import { windowHeight } from 'utils/size';
2016-02-16 21:43:25 +00:00
import Message from './Message';
2015-12-28 23:34:32 +00:00
const fetchThreshold = 600;
// The amount of time in ms that needs to pass without any
// scroll events happening before adding messages to the top,
// this is done to prevent the scroll from jumping all over the place
2018-11-22 08:51:26 +00:00
const scrollbackDebounce = 150;
2015-12-28 23:34:32 +00:00
const scrollBarWidth = `${measureScrollBarWidth()}px`;
const hasSameLastMessage = (m1, m2) => {
if (m1.length === 0 || m2.length === 0) {
if (m1.length === 0 && m2.length === 0) {
return true;
}
return false;
}
return m1[m1.length - 1].id === m2[m2.length - 1].id;
};
2018-12-14 13:24:23 +00:00
2017-03-23 19:38:27 +00:00
export default class MessageBox extends PureComponent {
2018-12-14 13:24:23 +00:00
state = { topDate: '' };
2019-02-08 08:10:06 +00:00
list = createRef();
2019-02-08 08:10:06 +00:00
outer = createRef();
addMore = debounce(() => {
const { tab, onAddMore } = this.props;
this.ready = true;
2020-06-15 08:58:51 +00:00
onAddMore(tab.network, tab.name);
}, scrollbackDebounce);
constructor(props) {
super(props);
this.loadScrollPos();
}
componentDidUpdate(prevProps) {
const { messages } = this.props;
if (prevProps.tab !== this.props.tab) {
this.loadScrollPos(true);
}
if (this.nextScrollTop > 0) {
this.list.current.scrollTo(this.nextScrollTop);
this.nextScrollTop = 0;
} else if (
this.bottom &&
!hasSameLastMessage(messages, prevProps.messages)
) {
this.list.current.scrollToItem(messages.length + 1);
}
}
componentWillUnmount() {
this.saveScrollPos();
}
getSnapshotBeforeUpdate(prevProps) {
if (prevProps.messages !== this.props.messages) {
this.list.current.resetAfterIndex(0);
2017-05-02 21:21:25 +00:00
}
if (prevProps.tab !== this.props.tab) {
this.saveScrollPos();
this.bottom = false;
2017-03-23 19:38:27 +00:00
}
2015-12-28 23:34:32 +00:00
if (prevProps.messages[0] !== this.props.messages[0]) {
const { messages, hasMoreMessages } = this.props;
if (prevProps.tab === this.props.tab && prevProps.messages.length > 0) {
const addedMessages = messages.length - prevProps.messages.length;
2017-05-02 21:21:25 +00:00
let addedHeight = 0;
for (let i = 0; i < addedMessages; i++) {
addedHeight += messages[i].height;
2017-05-02 21:21:25 +00:00
}
this.nextScrollTop = addedHeight + this.outer.current.scrollTop;
if (!hasMoreMessages) {
this.nextScrollTop -= 93;
}
2017-05-02 21:21:25 +00:00
}
this.loading = false;
this.ready = false;
2016-02-16 21:43:25 +00:00
}
return null;
2016-02-16 21:43:25 +00:00
}
getRowHeight = index => {
const { messages, hasMoreMessages } = this.props;
2017-05-02 21:21:25 +00:00
if (index === 0) {
if (hasMoreMessages) {
2017-05-02 21:21:25 +00:00
return 100;
}
return 7;
}
if (index === messages.length + 1) {
return 7;
2017-05-02 21:21:25 +00:00
}
return messages[index - 1].height;
};
2016-02-16 21:43:25 +00:00
getItemKey = index => {
const { messages } = this.props;
if (index === 0) {
return 'top';
}
if (index === messages.length + 1) {
return 'bottom';
}
return messages[index - 1].id;
};
updateScrollKey = () => {
const { tab } = this.props;
2020-06-15 08:58:51 +00:00
this.scrollKey = `msg:${tab.network}:${tab.name}`;
return this.scrollKey;
};
loadScrollPos = scroll => {
const pos = getScrollPos(this.updateScrollKey());
2018-11-22 07:46:19 +00:00
const { messages } = this.props;
if (pos >= 0) {
this.bottom = false;
if (scroll) {
this.list.current.scrollTo(pos);
} else {
this.initialScrollTop = pos;
}
} else {
this.bottom = true;
if (scroll) {
2018-11-22 07:46:19 +00:00
this.list.current.scrollToItem(messages.length + 1);
} else if (messages.length > 0) {
let totalHeight = 14;
2018-11-22 07:46:19 +00:00
for (let i = 0; i < messages.length; i++) {
totalHeight += messages[i].height;
}
const messageBoxHeight = windowHeight() - 100;
if (totalHeight > messageBoxHeight) {
this.initialScrollTop = totalHeight;
2018-11-22 07:46:19 +00:00
}
}
}
};
saveScrollPos = () => {
if (this.bottom) {
saveScrollPos(this.scrollKey, -1);
} else {
saveScrollPos(this.scrollKey, this.scrollTop);
}
};
2015-12-28 23:34:32 +00:00
fetchMore = () => {
2017-05-02 21:21:25 +00:00
this.loading = true;
this.props.onFetchMore();
};
handleScroll = ({ scrollOffset, scrollDirection }) => {
if (
!this.loading &&
this.props.hasMoreMessages &&
scrollOffset <= fetchThreshold &&
scrollDirection === 'backward'
) {
this.fetchMore();
}
if (this.loading && !this.ready) {
if (this.mouseDown) {
this.ready = true;
this.shouldAdd = true;
} else {
this.addMore();
2017-05-02 21:21:25 +00:00
}
}
const { clientHeight, scrollHeight } = this.outer.current;
this.scrollTop = scrollOffset;
this.bottom = scrollOffset + clientHeight >= scrollHeight - 20;
2017-05-02 21:21:25 +00:00
};
2018-12-14 13:24:23 +00:00
handleItemsRendered = ({ visibleStartIndex }) => {
const startIndex = visibleStartIndex === 0 ? 0 : visibleStartIndex - 1;
const firstVisibleMessage = this.props.messages[startIndex];
if (firstVisibleMessage && firstVisibleMessage.date) {
this.setState({ topDate: formatDate(firstVisibleMessage.date) });
} else {
this.setState({ topDate: '' });
}
};
2017-05-02 21:21:25 +00:00
handleMouseDown = () => {
this.mouseDown = true;
};
handleMouseUp = () => {
this.mouseDown = false;
if (this.shouldAdd) {
const { tab, onAddMore } = this.props;
this.shouldAdd = false;
2020-06-15 08:58:51 +00:00
onAddMore(tab.network, tab.name);
2017-05-02 21:21:25 +00:00
}
2017-03-23 19:38:27 +00:00
};
2016-02-16 21:43:25 +00:00
2017-04-20 03:32:22 +00:00
renderMessage = ({ index, style }) => {
const { messages } = this.props;
2017-05-02 21:21:25 +00:00
if (index === 0) {
if (this.props.hasMoreMessages) {
return (
<div className="messagebox-top-indicator" style={style}>
2017-05-02 21:21:25 +00:00
Loading messages...
</div>
);
}
return null;
}
if (index === messages.length + 1) {
return null;
2017-05-02 21:21:25 +00:00
}
const { coloredNicks, onNickClick } = this.props;
2018-04-25 03:36:27 +00:00
const message = messages[index - 1];
2015-12-28 23:34:32 +00:00
2016-02-16 21:43:25 +00:00
return (
<div style={style}>
<Message
message={message}
coloredNick={coloredNicks}
onNickClick={onNickClick}
/>
</div>
2016-02-16 21:43:25 +00:00
);
};
render() {
2018-12-14 13:24:23 +00:00
const { messages, hideTopDate } = this.props;
const { topDate } = this.state;
const dateContainerStyle = {
right: scrollBarWidth
};
2015-12-28 23:34:32 +00:00
return (
2017-05-02 21:21:25 +00:00
<div
className="messagebox"
onMouseDown={this.handleMouseDown}
onMouseUp={this.handleMouseUp}
>
2018-12-14 13:24:23 +00:00
<div
className="messagebox-topdate-container"
style={dateContainerStyle}
>
{!hideTopDate && topDate && (
<span className="messagebox-topdate">{topDate}</span>
)}
</div>
<AutoSizer>
2017-03-23 19:38:27 +00:00
{({ width, height }) => (
<List
ref={this.list}
outerRef={this.outer}
2017-03-23 19:38:27 +00:00
width={width}
height={height}
2018-12-14 13:24:23 +00:00
itemCount={messages.length + 2}
itemKey={this.getItemKey}
itemSize={this.getRowHeight}
estimatedItemSize={32}
initialScrollOffset={this.initialScrollTop}
2017-03-23 19:38:27 +00:00
onScroll={this.handleScroll}
2018-12-14 13:24:23 +00:00
onItemsRendered={this.handleItemsRendered}
className="messagebox-window"
2018-11-14 07:33:01 +00:00
overscanCount={5}
>
{this.renderMessage}
</List>
2017-03-23 19:38:27 +00:00
)}
</AutoSizer>
2015-12-28 23:34:32 +00:00
</div>
);
}
}