From 66cfdc96dac8fa0eacdf4743b0bef3a18344429c Mon Sep 17 00:00:00 2001
From: Chad Pilkey <capilkey@gmail.com>
Date: Tue, 21 Jan 2020 21:08:48 +0000
Subject: [PATCH] virtualized chat tweaks for rendering and performance

---
 .../imports/ui/components/chat/container.jsx  |  32 ++--
 .../chat/message-list/component.jsx           | 180 ++++++------------
 .../message-list-item/component.jsx           |  40 ++--
 .../message-list-item/container.jsx           |  22 +++
 .../message-list-item/styles.scss             |  19 +-
 .../imports/ui/components/chat/service.js     |  13 +-
 6 files changed, 139 insertions(+), 167 deletions(-)
 create mode 100644 bigbluebutton-html5/imports/ui/components/chat/message-list/message-list-item/container.jsx

diff --git a/bigbluebutton-html5/imports/ui/components/chat/container.jsx b/bigbluebutton-html5/imports/ui/components/chat/container.jsx
index a790298ddc..7dfe11426d 100755
--- a/bigbluebutton-html5/imports/ui/components/chat/container.jsx
+++ b/bigbluebutton-html5/imports/ui/components/chat/container.jsx
@@ -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,
diff --git a/bigbluebutton-html5/imports/ui/components/chat/message-list/component.jsx b/bigbluebutton-html5/imports/ui/components/chat/message-list/component.jsx
index d17355b9fe..2fb1352e41 100644
--- a/bigbluebutton-html5/imports/ui/components/chat/message-list/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/chat/message-list/component.jsx
@@ -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()]
     );
   }
 }
diff --git a/bigbluebutton-html5/imports/ui/components/chat/message-list/message-list-item/component.jsx b/bigbluebutton-html5/imports/ui/components/chat/message-list/message-list-item/component.jsx
index c3afcca86c..3b84e86d07 100644
--- a/bigbluebutton-html5/imports/ui/components/chat/message-list/message-list-item/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/chat/message-list/message-list-item/component.jsx
@@ -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}
diff --git a/bigbluebutton-html5/imports/ui/components/chat/message-list/message-list-item/container.jsx b/bigbluebutton-html5/imports/ui/components/chat/message-list/message-list-item/container.jsx
new file mode 100644
index 0000000000..50bf4d9f15
--- /dev/null
+++ b/bigbluebutton-html5/imports/ui/components/chat/message-list/message-list-item/container.jsx
@@ -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);
diff --git a/bigbluebutton-html5/imports/ui/components/chat/message-list/message-list-item/styles.scss b/bigbluebutton-html5/imports/ui/components/chat/message-list/message-list-item/styles.scss
index 579b6d4883..492fd1cc65 100755
--- a/bigbluebutton-html5/imports/ui/components/chat/message-list/message-list-item/styles.scss
+++ b/bigbluebutton-html5/imports/ui/components/chat/message-list/message-list-item/styles.scss
@@ -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 {
diff --git a/bigbluebutton-html5/imports/ui/components/chat/service.js b/bigbluebutton-html5/imports/ui/components/chat/service.js
index 64cb0ea531..e4f3f5c0a1 100755
--- a/bigbluebutton-html5/imports/ui/components/chat/service.js
+++ b/bigbluebutton-html5/imports/ui/components/chat/service.js
@@ -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,
-- 
GitLab