From 927e2167bc42d54de8d6afbe7e2ebd5c96214450 Mon Sep 17 00:00:00 2001
From: Tainan Felipe <tainanfelipe214@gmail.com>
Date: Tue, 9 Mar 2021 17:52:20 -0300
Subject: [PATCH] Re-implements chat sync and clear

---
 .../api/group-chat-msg/server/methods.js      |  4 ++
 .../methods/chatMessageBeforeJoinCounter.js   | 30 ++++++++
 .../server/methods/fetchMessagePerPage.js     | 22 ++++++
 .../api/group-chat-msg/server/publishers.js   |  3 +
 .../imports/ui/components/chat/component.jsx  |  6 ++
 .../imports/ui/components/chat/container.jsx  | 42 +++++++++--
 .../chat/time-window-list/component.jsx       | 25 +++++--
 .../components-data/chat-context/adapter.jsx  | 69 +++++++++++++++++--
 .../components-data/chat-context/context.jsx  | 62 ++++++++++++++---
 .../private/config/settings.yml               |  2 +
 bigbluebutton-html5/private/locales/en.json   |  1 +
 11 files changed, 241 insertions(+), 25 deletions(-)
 create mode 100644 bigbluebutton-html5/imports/api/group-chat-msg/server/methods/chatMessageBeforeJoinCounter.js
 create mode 100644 bigbluebutton-html5/imports/api/group-chat-msg/server/methods/fetchMessagePerPage.js

diff --git a/bigbluebutton-html5/imports/api/group-chat-msg/server/methods.js b/bigbluebutton-html5/imports/api/group-chat-msg/server/methods.js
index d94d732f57..5103e484c7 100644
--- a/bigbluebutton-html5/imports/api/group-chat-msg/server/methods.js
+++ b/bigbluebutton-html5/imports/api/group-chat-msg/server/methods.js
@@ -3,8 +3,12 @@ import sendGroupChatMsg from './methods/sendGroupChatMsg';
 import clearPublicChatHistory from './methods/clearPublicChatHistory';
 import startUserTyping from './methods/startUserTyping';
 import stopUserTyping from './methods/stopUserTyping';
+import chatMessageBeforeJoinCounter from './methods/chatMessageBeforeJoinCounter';
+import fetchMessagePerPage from './methods/fetchMessagePerPage';
 
 Meteor.methods({
+  fetchMessagePerPage,
+  chatMessageBeforeJoinCounter,
   sendGroupChatMsg,
   clearPublicChatHistory,
   startUserTyping,
diff --git a/bigbluebutton-html5/imports/api/group-chat-msg/server/methods/chatMessageBeforeJoinCounter.js b/bigbluebutton-html5/imports/api/group-chat-msg/server/methods/chatMessageBeforeJoinCounter.js
new file mode 100644
index 0000000000..f1e9d1141f
--- /dev/null
+++ b/bigbluebutton-html5/imports/api/group-chat-msg/server/methods/chatMessageBeforeJoinCounter.js
@@ -0,0 +1,30 @@
+import { Meteor } from 'meteor/meteor';
+import { check } from 'meteor/check';
+import GroupChat from '/imports/api/group-chat';
+import { GroupChatMsg } from '/imports/api/group-chat-msg';
+import Users from '/imports/api/users';
+import { extractCredentials } from '/imports/api/common/server/helpers';
+
+const CHAT_CONFIG = Meteor.settings.public.chat;
+const PUBLIC_CHAT_TYPE = CHAT_CONFIG.type_public;
+
+export default function chatMessageBeforeJoinCounter() {
+  const { meetingId, requesterUserId } = extractCredentials(this.userId);
+  const groupChats = GroupChat.find({
+    $or: [
+      { meetingId, access: PUBLIC_CHAT_TYPE },
+      { meetingId, users: { $all: [requesterUserId] } },
+    ],
+  }).fetch();
+
+  const User = Users.findOne({ userId: requesterUserId, meetingId });
+
+  const chatIdWithCounter = groupChats.map((groupChat) => {
+    const msgCount = GroupChatMsg.find({ chatId: groupChat.chatId, timestamp: { $lt: User.authTokenValidatedTime } }).count();
+    return {
+      chatId: groupChat.chatId,
+      count: msgCount,
+    };
+  }).filter(chat => chat.count);
+  return chatIdWithCounter;
+}
diff --git a/bigbluebutton-html5/imports/api/group-chat-msg/server/methods/fetchMessagePerPage.js b/bigbluebutton-html5/imports/api/group-chat-msg/server/methods/fetchMessagePerPage.js
new file mode 100644
index 0000000000..8a00d3d593
--- /dev/null
+++ b/bigbluebutton-html5/imports/api/group-chat-msg/server/methods/fetchMessagePerPage.js
@@ -0,0 +1,22 @@
+import { Meteor } from 'meteor/meteor';
+import GroupChat from '/imports/api/group-chat';
+import { GroupChatMsg } from '/imports/api/group-chat-msg';
+import Users from '/imports/api/users';
+import { extractCredentials } from '/imports/api/common/server/helpers';
+
+const CHAT_CONFIG = Meteor.settings.public.chat;
+const ITENS_PER_PAGE = CHAT_CONFIG.itemsPerPage;
+
+export default function fetchMessagePerPage(chatId, page) {
+  const { meetingId, requesterUserId } = extractCredentials(this.userId);
+  const User = Users.findOne({ userId: requesterUserId, meetingId });
+
+  const messages = GroupChatMsg.find({ chatId, meetingId, timestamp: { $lt: User.authTokenValidatedTime } },
+    {
+      sort: { timestamp: 1 },
+      skip: page > 0 ? ((page - 1) * ITENS_PER_PAGE) : 0,
+      limit: ITENS_PER_PAGE,
+    })
+    .fetch();
+  return messages;
+}
diff --git a/bigbluebutton-html5/imports/api/group-chat-msg/server/publishers.js b/bigbluebutton-html5/imports/api/group-chat-msg/server/publishers.js
index 34e52d2b0d..ede79c838d 100644
--- a/bigbluebutton-html5/imports/api/group-chat-msg/server/publishers.js
+++ b/bigbluebutton-html5/imports/api/group-chat-msg/server/publishers.js
@@ -1,4 +1,5 @@
 import { GroupChatMsg, UsersTyping } from '/imports/api/group-chat-msg';
+import Users from '/imports/api/users';
 import { Meteor } from 'meteor/meteor';
 
 import Logger from '/imports/startup/server/logger';
@@ -19,7 +20,9 @@ function groupChatMsg(chatsIds) {
 
   Logger.debug('Publishing group-chat-msg', { meetingId, userId });
 
+  const User = Users.findOne({ userId });
   const selector = {
+    timestamp: { $gte: User.authTokenValidatedTime },
     $or: [
       { meetingId, chatId: { $eq: PUBLIC_GROUP_CHAT_ID } },
       { chatId: { $in: chatsIds } },
diff --git a/bigbluebutton-html5/imports/ui/components/chat/component.jsx b/bigbluebutton-html5/imports/ui/components/chat/component.jsx
index 924d6caa8c..a39a86d8c0 100755
--- a/bigbluebutton-html5/imports/ui/components/chat/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/chat/component.jsx
@@ -5,6 +5,7 @@ import injectWbResizeEvent from '/imports/ui/components/presentation/resize-wrap
 import Button from '/imports/ui/components/button/component';
 import { Session } from 'meteor/session';
 import withShortcutHelper from '/imports/ui/components/shortcut-help/service';
+import ChatLogger from '/imports/ui/components/chat/chat-logger/ChatLogger';
 import { styles } from './styles.scss';
 import MessageForm from './message-form/container';
 import TimeWindowList from './time-window-list/container';
@@ -45,9 +46,12 @@ const Chat = (props) => {
     timeWindowsValues,
     dispatch,
     count,
+    syncing,
+    syncedPercent,
   } = props;
   const HIDE_CHAT_AK = shortcuts.hidePrivateChat;
   const CLOSE_CHAT_AK = shortcuts.closePrivateChat;
+  ChatLogger.debug('ChatComponent::render', props);
   return (
     <div
       data-test={chatID !== 'public' ? 'privateChat' : 'publicChat'}
@@ -108,6 +112,8 @@ const Chat = (props) => {
           timeWindowsValues,
           dispatch,
           count,
+          syncing,
+          syncedPercent,
         }}
       />
       <MessageForm
diff --git a/bigbluebutton-html5/imports/ui/components/chat/container.jsx b/bigbluebutton-html5/imports/ui/components/chat/container.jsx
index 229f1c98f0..9048cc2736 100755
--- a/bigbluebutton-html5/imports/ui/components/chat/container.jsx
+++ b/bigbluebutton-html5/imports/ui/components/chat/container.jsx
@@ -23,7 +23,8 @@ const DEBOUNCE_TIME = 1000;
 
 const sysMessagesIds = {
   welcomeId: `${SYSTEM_CHAT_TYPE}-welcome-msg`,
-  moderatorId: `${SYSTEM_CHAT_TYPE}-moderator-msg`
+  moderatorId: `${SYSTEM_CHAT_TYPE}-moderator-msg`,
+  syncId: `${SYSTEM_CHAT_TYPE}-sync-msg`
 };
 
 const intlMessages = defineMessages({
@@ -43,11 +44,15 @@ const intlMessages = defineMessages({
     id: 'app.chat.partnerDisconnected',
     description: 'System chat message when the private chat partnet disconnect from the meeting',
   },
+  loading: {
+    id: 'app.chat.loading',
+    description: 'loading message',
+  },
 });
 
 let previousChatId = null;
-let debounceTimeout = null;
-let messages = null;
+let prevSync = false;
+
 let globalAppplyStateToProps = () => { }
 
 const throttledFunc = _.throttle(() => {
@@ -71,6 +76,8 @@ const ChatContainer = (props) => {
     intl,
   } = props;
 
+  ChatLogger.debug('ChatContainer::render::props', props);
+
   const isPublicChat = chatID === PUBLIC_CHAT_KEY;
   const systemMessages = {
     [sysMessagesIds.welcomeId]: {
@@ -118,10 +125,33 @@ const ChatContainer = (props) => {
   const lastMsg = contextChat && (isPublicChat
     ? contextChat.preJoinMessages[lastTimeWindow] || contextChat.posJoinMessages[lastTimeWindow]
     : contextChat.messageGroups[lastTimeWindow]);
+  ChatLogger.debug('ChatContainer::render::chatData',contextChat);
   applyPropsToState = () => {
-    if (!_.isEqualWith(lastMsg, stateLastMsg) || previousChatId !== chatID) {
+    ChatLogger.debug('ChatContainer::applyPropsToState::chatData',lastMsg, stateLastMsg, contextChat?.syncing);
+    if (
+      (lastMsg?.lastTimestamp !== stateLastMsg?.lastTimestamp)
+      || (previousChatId !== chatID)
+      || (prevSync !== contextChat?.syncing)
+      ) {
+      prevSync = contextChat?.syncing;
       const timeWindowsValues = isPublicChat
-        ? [...Object.values(contextChat?.preJoinMessages || {}), ...systemMessagesIds.map((item) => systemMessages[item]),
+        ? [
+          ...(
+            !contextChat?.syncing ? Object.values(contextChat?.preJoinMessages || {}) : [
+              {
+                id: sysMessagesIds.syncId,
+                content: [{
+                  id: 'synced',
+                  text: intl.formatMessage(intlMessages.loading, { 0: contextChat?.syncedPercent}),
+                  time: loginTime + 1,
+                }],
+                key: sysMessagesIds.syncId,
+                time: loginTime + 1,
+                sender: null,
+              }
+            ]
+          )
+          , ...systemMessagesIds.map((item) => systemMessages[item]),
         ...Object.values(contextChat?.posJoinMessages || {})]
         : [...Object.values(contextChat?.messageGroups || {})];
       if (previousChatId !== chatID) {
@@ -144,6 +174,8 @@ const ChatContainer = (props) => {
       timeWindowsValues: stateTimeWindows,
       dispatch: usingChatContext?.dispatch,
       title,
+      syncing: contextChat?.syncing,
+      syncedPercent: contextChat?.syncedPercent,
       chatName,
       contextChat,
     }}>
diff --git a/bigbluebutton-html5/imports/ui/components/chat/time-window-list/component.jsx b/bigbluebutton-html5/imports/ui/components/chat/time-window-list/component.jsx
index f5e1b11bbf..d861f4c2a7 100644
--- a/bigbluebutton-html5/imports/ui/components/chat/time-window-list/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/chat/time-window-list/component.jsx
@@ -93,9 +93,16 @@ class TimeWindowList extends PureComponent {
       setUserSentMessage,
       timeWindowsValues,
       chatId,
+      syncing,
+      syncedPercent,
     } = this.props;
 
-    const {timeWindowsValues: prevTimeWindowsValues, chatId: prevChatId} = prevProps;
+    const {
+      timeWindowsValues: prevTimeWindowsValues,
+      chatId: prevChatId,
+      syncing: prevSyncing,
+      syncedPercent: prevSyncedPercent
+    } = prevProps;
 
     const prevTimeWindowsLength = prevTimeWindowsValues.length;
     const timeWindowsValuesLength = timeWindowsValues.length;
@@ -109,15 +116,23 @@ class TimeWindowList extends PureComponent {
       }  
     }
 
-    if (lastTimeWindow && (chatId !== prevChatId)) {
-      this.listRef.recomputeGridSize();
-    }
-
     if (userSentMessage && !prevProps.userSentMessage){
       this.setState({
         userScrolledBack: false,
       }, ()=> setUserSentMessage(false));
     }
+
+     // this condition exist to the case where the chat has a single message and the chat is cleared
+    // The component List from react-virtualized doesn't have a reference to the list of messages so I need force the update to fix it
+    if (
+      (lastTimeWindow?.id === 'SYSTEM_MESSAGE-PUBLIC_CHAT_CLEAR')
+      || (prevSyncing && !syncing)
+      || (syncedPercent !== prevSyncedPercent)
+      || (chatId !== prevChatId)      
+      ) {
+      this.listRef.forceUpdateGrid();
+    }
+
   }
 
   componentWillUnmount() {
diff --git a/bigbluebutton-html5/imports/ui/components/components-data/chat-context/adapter.jsx b/bigbluebutton-html5/imports/ui/components/components-data/chat-context/adapter.jsx
index a45f32e7a7..ea8b17de86 100644
--- a/bigbluebutton-html5/imports/ui/components/components-data/chat-context/adapter.jsx
+++ b/bigbluebutton-html5/imports/ui/components/components-data/chat-context/adapter.jsx
@@ -1,24 +1,83 @@
-import { useContext, useEffect } from 'react';
+import { useContext, useEffect, useState } from 'react';
 import _ from 'lodash';
 import { ChatContext, ACTIONS } from './context';
 import { UsersContext } from '../users-context/context';
+import { makeCall } from '/imports/ui/services/api';
 import ChatLogger from '/imports/ui/components/chat/chat-logger/ChatLogger';
 
 let usersData = {};
 let messageQueue = [];
+
+const CHAT_CONFIG = Meteor.settings.public.chat;
+const ITENS_PER_PAGE = CHAT_CONFIG.itemsPerPage;
+const TIME_BETWEEN_FETCHS = CHAT_CONFIG.timeBetweenFetchs;
+
+const getMessagesBeforeJoinCounter = async () => {
+  const counter = await makeCall('chatMessageBeforeJoinCounter');
+  return counter;
+};
+
+const startSyncMessagesbeforeJoin = async (dispatch) => {
+  const chatsMessagesCount = await getMessagesBeforeJoinCounter();
+  const pagesPerChat = chatsMessagesCount.map(chat => ({ ...chat, pages: Math.ceil(chat.count / ITENS_PER_PAGE), syncedPages: 0 }));
+
+  const syncRoutine = async (chatsToSync) => {
+    if (!chatsToSync.length) return;
+
+    const pagesToFetch = [...chatsToSync].sort((a, b) => a.pages - b.pages);
+    const chatWithLessPages = pagesToFetch[0];
+    chatWithLessPages.syncedPages += 1;
+    const messagesFromPage = await makeCall('fetchMessagePerPage', chatWithLessPages.chatId, chatWithLessPages.syncedPages);
+
+    if (messagesFromPage.length) {
+      dispatch({
+        type: ACTIONS.ADDED,
+        value: messagesFromPage,
+      });
+      dispatch({
+        type: ACTIONS.SYNC_STATUS,
+        value: {
+          chatId: chatWithLessPages.chatId,
+          percentage: Math.floor((chatWithLessPages.syncedPages / chatWithLessPages.pages) * 100),
+        },
+      });
+    }
+
+
+    await new Promise(r => setTimeout(r, TIME_BETWEEN_FETCHS));
+    syncRoutine(pagesToFetch.filter(chat => !(chat.syncedPages > chat.pages)));
+  };
+  syncRoutine(pagesPerChat);
+};
+
 const Adapter = () => {
   const usingChatContext = useContext(ChatContext);
   const { dispatch } = usingChatContext;
   const usingUsersContext = useContext(UsersContext);
   const { users } = usingUsersContext;
+  const [syncStarted, setSync] = useState(true);
   ChatLogger.trace('chatAdapter::body::users', users);
 
+  useEffect(() => {
+    const connectionStatus = Meteor.status();
+    if (connectionStatus.connected && !syncStarted) {
+      setSync(true);
+
+      startSyncMessagesbeforeJoin(dispatch);
+    }
+  }, [Meteor.status().connected, syncStarted]);
+
+
   useEffect(() => {
     usersData = users;
   }, [usingUsersContext]);
 
   useEffect(() => {
-    // TODO: listen to websocket message to avoid full list comparsion
+    if (!Meteor.status().connected) return;
+    setSync(false);
+    dispatch({
+      type: ACTIONS.CLEAR_ALL,
+    });
     const throttledDispatch = _.throttle(() => {
       const dispatchedMessageQueue = [...messageQueue];
       messageQueue = [];
@@ -32,9 +91,7 @@ const Adapter = () => {
       if (msg.data.indexOf('{"msg":"added","collection":"group-chat-msg"') != -1) {
         const parsedMsg = JSON.parse(msg.data);
         if (parsedMsg.msg === 'added') {
-          messageQueue.push({
-            msg: parsedMsg.fields,
-          });
+          messageQueue.push(parsedMsg.fields);
           throttledDispatch();
         }
       }
@@ -44,7 +101,7 @@ const Adapter = () => {
         });
       }
     });
-  }, []);
+  }, [Meteor.status().connected]);
 
   return null;
 };
diff --git a/bigbluebutton-html5/imports/ui/components/components-data/chat-context/context.jsx b/bigbluebutton-html5/imports/ui/components/components-data/chat-context/context.jsx
index 1f73539e4d..e72e79c4a1 100644
--- a/bigbluebutton-html5/imports/ui/components/components-data/chat-context/context.jsx
+++ b/bigbluebutton-html5/imports/ui/components/components-data/chat-context/context.jsx
@@ -22,13 +22,16 @@ export const ACTIONS = {
   REMOVED: 'removed',
   LAST_READ_MESSAGE_TIMESTAMP_CHANGED: 'last_read_message_timestamp_changed',
   INIT: 'initial_structure',
+  SYNC_STATUS: 'sync_status',
+  HAS_MESSAGE_TO_SYNC: 'has_message_to_sync',
+  CLEAR_ALL: 'clear_all',
 };
 
 const ROLE_MODERATOR = Meteor.settings.public.user.role_moderator;
 
 export const getGroupingTime = () => Meteor.settings.public.chat.grouping_messages_window;
 export const getGroupChatId = () => Meteor.settings.public.chat.public_group_id;
-export const getLoginTime = () => (Users.findOne({ userId: Auth.userID }) || {}).loginTime || 0;
+export const getLoginTime = () => (Users.findOne({ userId: Auth.userID }) || {}).authTokenValidatedTime || 0;
 
 const generateTimeWindow = (timestamp) => {
   const groupingTime = getGroupingTime();
@@ -40,12 +43,12 @@ const generateTimeWindow = (timestamp) => {
 
 export const ChatContext = createContext();
 
-const generateStateWithNewMessage = ({ msg, senderData }, state) => {
+const generateStateWithNewMessage = (msg, state) => {
   
   const timeWindow = generateTimeWindow(msg.timestamp);
   const userId = msg.sender.id;
   const keyName = userId + '-' + timeWindow;
-  const msgBuilder = ({msg, senderData}, chat) => {
+  const msgBuilder = (msg, chat) => {
     const msgTimewindow = generateTimeWindow(msg.timestamp);
     const key = msg.sender.id + '-' + msgTimewindow;
     const chatIndex = chat?.chatIndexes[key];
@@ -80,6 +83,7 @@ const generateStateWithNewMessage = ({ msg, senderData }, state) => {
         chatIndexes: {},
         preJoinMessages: {},
         posJoinMessages: {},
+        synced:true,
         unreadTimeWindows: new Set(),
         unreadCount: 0,
       };
@@ -87,6 +91,7 @@ const generateStateWithNewMessage = ({ msg, senderData }, state) => {
       state[msg.chatId] = {
         count: 0,
         lastSender: '',
+        synced:true,
         chatIndexes: {},
         messageGroups: {},
         unreadTimeWindows: new Set(),
@@ -106,7 +111,7 @@ const generateStateWithNewMessage = ({ msg, senderData }, state) => {
   
   if (!groupMessage || (groupMessage && groupMessage.sender.id !== stateMessages.lastSender.id)) {
 
-    const [tempGroupMessage, sender, newIndex] = msgBuilder({msg, senderData}, stateMessages);
+    const [tempGroupMessage, sender, newIndex] = msgBuilder(msg, stateMessages);
     stateMessages.lastSender = sender;
     stateMessages.chatIndexes[keyName] = newIndex;
     stateMessages.lastTimewindow = keyName + '-' + newIndex;
@@ -116,7 +121,8 @@ const generateStateWithNewMessage = ({ msg, senderData }, state) => {
     messageGroupsKeys.forEach(key => {
       messageGroups[key] = tempGroupMessage[key];
       const message = tempGroupMessage[key];
-      if (message.sender.id !== Auth.userID && !message.id.startsWith(SYSTEM_CHAT_TYPE)) {
+      const previousMessage = message.timestamp <= getLoginTime();
+      if (!previousMessage && message.sender.id !== Auth.userID && !message.id.startsWith(SYSTEM_CHAT_TYPE)) {
         stateMessages.unreadTimeWindows.add(key);
       }
     });
@@ -161,7 +167,7 @@ const reducer = (state, action) => {
       const currentClosedChats = Storage.getItem(CLOSED_CHAT_LIST_KEY) || [];
       const loginTime = getLoginTime();
       const newState = batchMsgs.reduce((acc, i)=> {
-        const message = i.msg;
+        const message = i;
         const chatId = message.chatId;
         if (
             chatId !== PUBLIC_GROUP_CHAT_KEY 
@@ -169,8 +175,7 @@ const reducer = (state, action) => {
             && currentClosedChats.includes(chatId) ){
           closedChatsToOpen.add(chatId)
         }
-
-        return generateStateWithNewMessage(i, acc);
+        return generateStateWithNewMessage(message, acc);
       }, state);
 
       if (closedChatsToOpen.size) {
@@ -260,6 +265,45 @@ const reducer = (state, action) => {
       }
       return state;
     }
+    case ACTIONS.SYNC_STATUS: {
+      ChatLogger.debug(ACTIONS.SYNC_STATUS);
+      const newState = { ...state };
+      newState[action.value.chatId].syncedPercent = action.value.percentage;
+      newState[action.value.chatId].syncing = action.value.percentage < 100 ? true : false;
+
+      return newState;
+    }
+    case ACTIONS.CLEAR_ALL: {
+      ChatLogger.debug(ACTIONS.CLEAR_ALL);
+      const newState = { ...state };
+      const chatIds = Object.keys(newState);
+      chatIds.forEach((chatId) => {
+        newState[chatId] = chatId === PUBLIC_GROUP_CHAT_KEY ? 
+        {
+          count: 0,
+          lastSender: '',
+          chatIndexes: {},
+          preJoinMessages: {},
+          posJoinMessages: {},
+          syncing: false,
+          syncedPercent: 0,
+          unreadTimeWindows: new Set(),
+          unreadCount: 0,
+        }
+        :  
+        {
+          count: 0,
+          lastSender: '',
+          chatIndexes: {},
+          messageGroups: {},
+          syncing: false,
+          syncedPercent: 0,
+          unreadTimeWindows: new Set(),
+          unreadCount: 0,
+        };
+      });
+      return newState;
+    }
     default: {
       throw new Error(`Unexpected action: ${JSON.stringify(action)}`);
     }
@@ -293,4 +337,4 @@ export const ContextConsumer = Component => props => (
 export default {
   ContextConsumer,
   ChatContextProvider,
-}
\ No newline at end of file
+}
diff --git a/bigbluebutton-html5/private/config/settings.yml b/bigbluebutton-html5/private/config/settings.yml
index d268233858..6d651d680c 100755
--- a/bigbluebutton-html5/private/config/settings.yml
+++ b/bigbluebutton-html5/private/config/settings.yml
@@ -302,6 +302,8 @@ public:
     time: 5000
   chat:
     enabled: true
+    itemsPerPage: 100
+    timeBetweenFetchs: 1000
     bufferChatInsertsMs: 0
     startClosed: false
     min_message_length: 1
diff --git a/bigbluebutton-html5/private/locales/en.json b/bigbluebutton-html5/private/locales/en.json
index 4e0dc1eb55..2ac4f36d93 100755
--- a/bigbluebutton-html5/private/locales/en.json
+++ b/bigbluebutton-html5/private/locales/en.json
@@ -1,6 +1,7 @@
 {
     "app.home.greeting": "Your presentation will begin shortly ...",
     "app.chat.submitLabel": "Send message",
+    "app.chat.loading": "loading: {0}%",
     "app.chat.errorMaxMessageLength": "The message is {0} characters(s) too long",
     "app.chat.disconnected": "You are disconnected, messages can't be sent",
     "app.chat.locked": "Chat is locked, messages can't be sent",
-- 
GitLab