virtualized chat tweaks for rendering and performance

This commit is contained in:
Chad Pilkey 2020-01-21 21:08:48 +00:00
parent e3f33ab613
commit 66cfdc96da
6 changed files with 139 additions and 167 deletions

View File

@ -9,6 +9,7 @@ import ChatService from './service';
const CHAT_CONFIG = Meteor.settings.public.chat;
const PUBLIC_CHAT_KEY = CHAT_CONFIG.public_id;
const CHAT_CLEAR = CHAT_CONFIG.system_messages_keys.chat_clear;
const SYSTEM_CHAT_TYPE = CHAT_CONFIG.type_system;
const ROLE_MODERATOR = Meteor.settings.public.user.role_moderator;
const CONNECTION_STATUS = 'online';
@ -78,30 +79,33 @@ export default injectIntl(withTracker(({ intl }) => {
sender: null,
};
const moderatorTime = time + 1;
const moderatorId = `moderator-msg-${moderatorTime}`;
let moderatorMsg;
if (amIModerator && welcomeProp.modOnlyMessage) {
const moderatorTime = time + 1;
const moderatorId = `moderator-msg-${moderatorTime}`;
const moderatorMsg = {
id: moderatorId,
content: [{
moderatorMsg = {
id: moderatorId,
text: welcomeProp.modOnlyMessage,
content: [{
id: moderatorId,
text: welcomeProp.modOnlyMessage,
time: moderatorTime,
}],
time: moderatorTime,
}],
time: moderatorTime,
sender: null,
};
sender: null,
};
}
const messagesBeforeWelcomeMsg = ChatService.reduceAndMapGroupMessages(
const messagesBeforeWelcomeMsg = ChatService.reduceAndDontMapGroupMessages(
messages.filter(message => message.timestamp < time),
);
const messagesAfterWelcomeMsg = ChatService.reduceAndMapGroupMessages(
const messagesAfterWelcomeMsg = ChatService.reduceAndDontMapGroupMessages(
messages.filter(message => message.timestamp >= time),
);
const messagesFormated = messagesBeforeWelcomeMsg
.concat(welcomeMsg)
.concat(amIModerator ? moderatorMsg : [])
.concat(moderatorMsg || [])
.concat(messagesAfterWelcomeMsg);
messages = messagesFormated.sort((a, b) => (a.time - b.time));
@ -134,7 +138,7 @@ export default injectIntl(withTracker(({ intl }) => {
}
messages = messages.map((message) => {
if (message.sender) return message;
if (message.sender && message.sender !== SYSTEM_CHAT_TYPE) return message;
return {
...message,

View File

@ -8,14 +8,13 @@ import {
List, AutoSizer, CellMeasurer, CellMeasurerCache,
} from 'react-virtualized';
import { styles } from './styles';
import MessageListItem from './message-list-item/component';
import MessageListItemContainer from './message-list-item/container';
const propTypes = {
messages: PropTypes.arrayOf(PropTypes.object).isRequired,
scrollPosition: PropTypes.number,
chatId: PropTypes.string.isRequired,
hasUnreadMessages: PropTypes.bool.isRequired,
partnerIsLoggedOut: PropTypes.bool.isRequired,
handleScrollUpdate: PropTypes.func.isRequired,
intl: PropTypes.shape({
formatMessage: PropTypes.func.isRequired,
@ -42,22 +41,11 @@ const intlMessages = defineMessages({
});
class MessageList extends Component {
static getDerivedStateFromProps(props, state) {
const { messages: propMessages } = props;
const { messages: stateMessages } = state;
if (propMessages.length !== 3 && propMessages.length < stateMessages.length) return null;
return {
messages: propMessages,
};
}
constructor(props) {
super(props);
this.cache = new CellMeasurerCache({
fixedWidth: true,
minWidth: 75,
minHeight: 18,
});
this.shouldScrollBottom = false;
@ -74,10 +62,11 @@ class MessageList extends Component {
shouldScrollToBottom: true,
shouldScrollToPosition: false,
scrollPosition: 0,
messages: [],
};
this.listRef = null;
this.lastWidth = 0;
}
componentDidMount() {
@ -87,52 +76,16 @@ class MessageList extends Component {
this.scrollTo(scrollPosition);
}
componentWillReceiveProps(nextProps) {
const {
chatId,
} = this.props;
if (chatId !== nextProps.chatId) {
const { scrollArea } = this.state;
this.handleScrollUpdate(scrollArea.scrollTop, scrollArea);
}
}
shouldComponentUpdate(nextProps, nextState) {
const {
chatId,
hasUnreadMessages,
partnerIsLoggedOut,
} = this.props;
const {
scrollArea,
} = this.state;
if (!scrollArea && nextState.scrollArea) return true;
const switchingCorrespondent = chatId !== nextProps.chatId;
const hasNewUnreadMessages = hasUnreadMessages !== nextProps.hasUnreadMessages;
// check if the messages include <user has left the meeting>
const lastMessage = nextProps.messages[nextProps.messages.length - 1];
if (lastMessage) {
const userLeftIsDisplayed = lastMessage.id.includes('partner-disconnected');
if (!(partnerIsLoggedOut && userLeftIsDisplayed)) return true;
}
if (switchingCorrespondent || hasNewUnreadMessages) return true;
return false;
}
componentDidUpdate(prevProps, prevState) {
componentDidUpdate(prevProps) {
const {
scrollPosition,
chatId,
messages,
} = this.props;
const {
scrollPosition: prevScrollPosition,
messages: prevMessages,
chatId: prevChatId,
} = prevProps;
const {
@ -140,35 +93,36 @@ class MessageList extends Component {
shouldScrollToPosition,
scrollPosition: scrollPositionState,
shouldScrollToBottom,
messages,
} = this.state;
const { messages: prevMessages } = prevState;
const compareChatId = prevProps.chatId !== chatId;
if (compareChatId) {
if (prevChatId !== chatId) {
this.cache.clearAll();
setTimeout(() => this.scrollTo(scrollPosition), 300);
} else if (prevMessages && messages) {
if (prevMessages.length > messages.length) {
// the chat has been cleared
this.cache.clearAll();
} else {
prevMessages.forEach((prevMessage, index) => {
const newMessage = messages[index];
if (newMessage.content.length > prevMessage.content.length
|| newMessage.id !== prevMessage.id) {
this.resizeRow(index);
}
});
}
}
if (!shouldScrollToBottom && !scrollPosition && prevScrollPosition) {
this.scrollToBottom();
}
const prevLength = prevProps.messages && !!prevProps.messages.length
&& prevProps.messages[prevProps.messages.length - 1].content.length;
const currentLength = messages && !!messages.length
&& messages[messages.length - 1].content.length;
if (!compareChatId && (prevLength !== currentLength && currentLength > prevLength)) {
this.resizeRow(messages.length - 1);
}
if (shouldScrollToPosition && scrollArea.scrollTop === scrollPositionState) {
this.setState({ shouldScrollToPosition: false });
}
if (prevMessages.length < messages.length) {
this.resizeRow(prevMessages.length - 1);
// this.resizeRow(prevMessages.length - 1);
// messages.forEach((i, idx) => this.resizeRow(idx));
}
}
@ -217,7 +171,7 @@ class MessageList extends Component {
this.cache.clear(idx);
if (this.listRef) {
this.listRef.recomputeRowHeights(idx);
this.listRef.forceUpdate();
// this.listRef.forceUpdate();
}
}
@ -242,6 +196,7 @@ class MessageList extends Component {
} = this.props;
const { scrollArea } = this.state;
const message = messages[index];
return (
<CellMeasurer
key={key}
@ -250,28 +205,21 @@ class MessageList extends Component {
parent={parent}
rowIndex={index}
>
{
({ measure }) => (
<span
style={style}
onLoad={measure}
key={key}
>
<MessageListItem
style={style}
handleReadMessage={handleReadMessage}
key={message.id}
messages={message.content}
user={message.sender}
time={message.time}
chatAreaId={id}
lastReadMessageTime={lastReadMessageTime}
deferredMeasurementCache={this.cache}
scrollArea={scrollArea}
/>
</span>
)
}
<span
style={style}
key={key}
>
<MessageListItemContainer
style={style}
handleReadMessage={handleReadMessage}
key={key}
message={message}
messageId={message.id}
chatAreaId={id}
lastReadMessageTime={lastReadMessageTime}
scrollArea={scrollArea}
/>
</span>
</CellMeasurer>
);
}
@ -294,6 +242,7 @@ class MessageList extends Component {
className={styles.unreadButton}
color="primary"
size="sm"
key="unread-messages"
label={intl.formatMessage(intlMessages.moreMessages)}
onClick={this.scrollToBottom}
/>
@ -305,39 +254,28 @@ class MessageList extends Component {
render() {
const {
intl,
id,
messages,
} = this.props;
const {
scrollArea,
shouldScrollToBottom,
shouldScrollToPosition,
scrollPosition,
messages,
} = this.state;
const isEmpty = messages.length === 0;
return (
<div className={styles.messageListWrapper}>
<div
style={
{
height: '100%',
width: '100%',
[<div className={styles.messageListWrapper} key="chat-list">
<AutoSizer>
{({ height, width }) => {
if (width !== this.lastWidth) {
this.lastWidth = width;
this.cache.clearAll();
}
}
role="log"
id={id}
aria-live="polite"
aria-atomic="false"
aria-relevant="additions"
aria-label={isEmpty ? intl.formatMessage(intlMessages.emptyLogLabel) : ''}
>
<AutoSizer>
{({ height, width }) => (
return (
<List
ref={(ref) => {
if (ref != null) {
if (ref !== null) {
this.listRef = ref;
if (!scrollArea) {
@ -351,7 +289,7 @@ class MessageList extends Component {
rowCount={messages.length}
height={height}
width={width}
overscanRowCount={15}
overscanRowCount={5}
deferredMeasurementCache={this.cache}
onScroll={this.handleScrollChange}
scrollToIndex={shouldScrollToBottom ? messages.length - 1 : undefined}
@ -360,13 +298,13 @@ class MessageList extends Component {
&& (scrollArea && scrollArea.scrollHeight >= scrollPosition)
? scrollPosition : undefined
}
scrollToAlignment="start"
scrollToAlignment="end"
/>
)}
</AutoSizer>
</div>
{this.renderUnreadNotification()}
</div>
);
}}
</AutoSizer>
</div>,
this.renderUnreadNotification()]
);
}
}

View File

@ -45,21 +45,24 @@ class MessageListItem extends Component {
scrollArea,
messages,
user,
messageId,
} = this.props;
const {
scrollArea: nextScrollArea,
messages: nextMessages,
user: nextUser,
messageId: nextMessageId,
} = nextProps;
if (!scrollArea && nextScrollArea) return true;
const hasNewMessage = messages.length !== nextMessages.length;
const hasIdChanged = messageId !== nextMessageId;
const hasUserChanged = user && nextUser
&& (user.isModerator !== nextUser.isModerator || user.isOnline !== nextUser.isOnline);
return hasNewMessage || hasUserChanged;
return hasNewMessage || hasIdChanged || hasUserChanged;
}
renderSystemMessage() {
@ -71,27 +74,22 @@ class MessageListItem extends Component {
return (
<div className={styles.item}>
<div className={styles.wrapper} ref={(ref) => { this.item = ref; }}>
<div className={styles.messages}>
<span>
{messages.map(message => (
message.text !== ''
? (
<Message
className={(message.id ? styles.systemMessage : null)}
key={_.uniqueId('id-')}
text={message.text}
time={message.time}
chatAreaId={chatAreaId}
handleReadMessage={handleReadMessage}
/>
) : null
))}
</span>
</div>
<div className={styles.messages}>
{messages.map(message => (
message.text !== ''
? (
<Message
className={(message.id ? styles.systemMessage : styles.systemMessageNoBorder)}
key={message.id ? message.id : _.uniqueId('id-')}
text={message.text}
time={message.time}
chatAreaId={chatAreaId}
handleReadMessage={handleReadMessage}
/>
) : null
))}
</div>
</div>
);
}
@ -117,7 +115,7 @@ class MessageListItem extends Component {
return (
<div className={styles.item}>
<div className={styles.wrapper} ref={(ref) => { this.item = ref; }}>
<div className={styles.wrapper}>
<div className={styles.avatarWrapper}>
<UserAvatar
className={styles.avatar}

View File

@ -0,0 +1,22 @@
import React, { PureComponent } from 'react';
import { withTracker } from 'meteor/react-meteor-data';
import MessageListItem from './component';
import ChatService from '../../service';
class MessageListItemContainer extends PureComponent {
render() {
return (
<MessageListItem {...this.props} />
);
}
}
export default withTracker(({ message }) => {
const mappedMessage = ChatService.mapGroupMessage(message);
return {
messages: mappedMessage.content,
user: mappedMessage.sender,
time: mappedMessage.time,
};
})(MessageListItemContainer);

View File

@ -3,16 +3,12 @@
:root {
--systemMessage-background-color: #F9FBFC;
--systemMessage-border-color: #C5CDD4;
--systemMessage-font-color: var(--color-dark-grey);
}
.item {
margin: 1rem 0 1rem 0;
padding: calc(var(--line-height-computed) / 4) 0 calc(var(--line-height-computed) / 2) 0;
font-size: var(--font-size-base);
margin-bottom: var(--line-height-computed);
&:last-child {
margin-bottom: 0 !important;
}
}
.wrapper {
@ -33,7 +29,16 @@
border-radius: var(--border-radius);
font-weight: var(--btn-font-weight);
padding: var(--font-size-base);
margin-bottom: var(--line-height-computed);
//margin-bottom: var(--line-height-computed);
color: var(--systemMessage-font-color);
margin-top: 0px;
margin-bottom: 0px;
}
.systemMessageNoBorder {
color: var(--systemMessage-font-color);
margin-top: 0px;
margin-bottom: 0px;
}
.avatarWrapper {

View File

@ -37,13 +37,13 @@ const getWelcomeProp = () => Meetings.findOne({ meetingId: Auth.meetingID },
const mapGroupMessage = (message) => {
const mappedMessage = {
id: message._id,
id: message._id || message.id,
content: message.content,
time: message.timestamp,
time: message.timestamp || message.time,
sender: null,
};
if (message.sender !== SYSTEM_CHAT_TYPE) {
if (message.sender && message.sender !== SYSTEM_CHAT_TYPE) {
const sender = Users.findOne({ userId: message.sender },
{
fields: {
@ -97,6 +97,9 @@ const reduceGroupMessages = (previous, current) => {
const reduceAndMapGroupMessages = messages => (messages
.reduce(reduceGroupMessages, []).map(mapGroupMessage));
const reduceAndDontMapGroupMessages = messages => (messages
.reduce(reduceGroupMessages, []));
const getPublicGroupMessages = () => {
const publicGroupMessages = GroupChatMsg.find({
meetingId: Auth.meetingID,
@ -128,7 +131,7 @@ const getPrivateGroupMessages = () => {
}, { sort: ['timestamp'] }).fetch();
}
return reduceAndMapGroupMessages(messages, []);
return reduceAndDontMapGroupMessages(messages, []);
};
const isChatLocked = (receiverID) => {
@ -322,7 +325,9 @@ const getLastMessageTimestampFromChatList = activeChats => activeChats
.reduce(maxNumberReducer, 0);
export default {
mapGroupMessage,
reduceAndMapGroupMessages,
reduceAndDontMapGroupMessages,
getPublicGroupMessages,
getPrivateGroupMessages,
getUser,