From d8391f450ba7bdc68a72504b746525e400f48c5d Mon Sep 17 00:00:00 2001
From: Joao Siebel <joaos_desenv@imdt.com.br>
Date: Thu, 26 Jul 2018 11:56:26 -0300
Subject: [PATCH] use group chat messages in html5 client

---
 .../imports/api/group-chat-msg/index.js       |   9 +-
 .../server/methods/sendGroupChatMsg.js        |  18 ++-
 .../api/group-chat-msg/server/publishers.js   |  15 +--
 .../imports/api/group-chat/index.js           |   2 +-
 .../server/methods/createGroupChat.js         |  29 +++--
 .../api/group-chat/server/publishers.js       |  15 +--
 .../imports/startup/client/base.jsx           |   1 +
 .../imports/ui/components/chat/container.jsx  |   8 +-
 .../imports/ui/components/chat/service.js     | 108 ++++++++++++++++++
 .../ui/components/user-list/component.jsx     |   3 +
 .../ui/components/user-list/container.jsx     |   2 +
 .../ui/components/user-list/service.js        |  10 ++
 .../user-list/user-list-content/component.jsx |   2 +
 .../user-participants/component.jsx           |   6 +-
 bigbluebutton-html5/server/main.js            |   1 +
 15 files changed, 166 insertions(+), 63 deletions(-)

diff --git a/bigbluebutton-html5/imports/api/group-chat-msg/index.js b/bigbluebutton-html5/imports/api/group-chat-msg/index.js
index a88d49148d..fb37887407 100644
--- a/bigbluebutton-html5/imports/api/group-chat-msg/index.js
+++ b/bigbluebutton-html5/imports/api/group-chat-msg/index.js
@@ -1,14 +1,12 @@
 import { Meteor } from 'meteor/meteor';
 
-const GroupChat = new Mongo.Collection('group-chat-msg');
+const GroupChatMsg = new Mongo.Collection('group-chat-msg');
 
 if (Meteor.isServer) {
-  GroupChat._ensureIndex({
-    meetingId: 1, chatId: 1, access: 1, users: 1,
-  });
+  GroupChatMsg._ensureIndex({ meetingId: 1, chatId: 1 });
 }
 
-export default GroupChat;
+export default GroupChatMsg;
 
 export const CHAT_ACCESS = {
   PUBLIC: 'PUBLIC_ACCESS',
@@ -17,3 +15,4 @@ export const CHAT_ACCESS = {
 
 export const CHAT_ACCESS_PUBLIC = CHAT_ACCESS.PUBLIC;
 export const CHAT_ACCESS_PRIVATE = CHAT_ACCESS.PRIVATE;
+export const GROUP_MESSAGE_PUBLIC_ID = 'MAIN-PUBLIC-GROUP-CHAT';
diff --git a/bigbluebutton-html5/imports/api/group-chat-msg/server/methods/sendGroupChatMsg.js b/bigbluebutton-html5/imports/api/group-chat-msg/server/methods/sendGroupChatMsg.js
index 4bf8896b9d..2b387daba7 100644
--- a/bigbluebutton-html5/imports/api/group-chat-msg/server/methods/sendGroupChatMsg.js
+++ b/bigbluebutton-html5/imports/api/group-chat-msg/server/methods/sendGroupChatMsg.js
@@ -29,6 +29,7 @@ const parseMessage = (message) => {
 export default function sendGroupChatMsg(credentials, chatId, message) {
   const REDIS_CONFIG = Meteor.settings.private.redis;
   const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
+  const EVENT_NAME = 'SendGroupChatMessageMsg';
 
   const { meetingId, requesterUserId, requesterToken } = credentials;
 
@@ -37,19 +38,14 @@ export default function sendGroupChatMsg(credentials, chatId, message) {
   check(requesterToken, String);
   check(message, Object);
 
-  const eventName = 'SendGroupChatMessageMsg';
+  const parsedMessage = parseMessage(message.message);
+
+  message.message = parsedMessage
 
-  const parsedMessage = parseMessage(message);
   const payload = {
-    chatId,
-    // correlationId: `${Date.now()}`,
-    sender: {
-      id: requesterUserId,
-      name: '',
-    },
-    // color: '1',
-    message: parsedMessage,
+    msg: message,
+    chatId: chatId
   };
 
-  return RedisPubSub.publishUserMessage(CHANNEL, eventName, meetingId, requesterUserId, payload);
+  return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload);
 }
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 d9f6af4f38..8bfb688a99 100644
--- a/bigbluebutton-html5/imports/api/group-chat-msg/server/publishers.js
+++ b/bigbluebutton-html5/imports/api/group-chat-msg/server/publishers.js
@@ -1,11 +1,10 @@
+import GroupChatMsg, { CHAT_ACCESS_PUBLIC, GROUP_MESSAGE_PUBLIC_ID } from '/imports/api/group-chat-msg';
 import { Meteor } from 'meteor/meteor';
 import { check } from 'meteor/check';
 
 import Logger from '/imports/startup/server/logger';
 import mapToAcl from '/imports/startup/mapToAcl';
 
-import { GroupChat, CHAT_ACCESS_PUBLIC } from '/imports/api/group-chat-msg';
-
 function groupChatMsg(credentials) {
   const { meetingId, requesterUserId, requesterToken } = credentials;
 
@@ -15,17 +14,7 @@ function groupChatMsg(credentials) {
 
   Logger.info(`Publishing group-chat-msg for ${meetingId} ${requesterUserId} ${requesterToken}`);
 
-  return GroupChat.find({
-    $or: [
-      {
-        access: CHAT_ACCESS_PUBLIC,
-        meetingId,
-      }, {
-        users: { $in: [requesterUserId] },
-        meetingId,
-      },
-    ],
-  });
+  return GroupChatMsg.find({ meetingId });
 }
 
 function publish(...args) {
diff --git a/bigbluebutton-html5/imports/api/group-chat/index.js b/bigbluebutton-html5/imports/api/group-chat/index.js
index 6d4f5c43dd..f26082d0a5 100644
--- a/bigbluebutton-html5/imports/api/group-chat/index.js
+++ b/bigbluebutton-html5/imports/api/group-chat/index.js
@@ -10,7 +10,7 @@ if (Meteor.isServer) {
 
 export default GroupChat;
 
-export const CHAT_ACCESS = {
+const CHAT_ACCESS = {
   PUBLIC: 'PUBLIC_ACCESS',
   PRIVATE: 'PRIVATE_ACCESS',
 };
diff --git a/bigbluebutton-html5/imports/api/group-chat/server/methods/createGroupChat.js b/bigbluebutton-html5/imports/api/group-chat/server/methods/createGroupChat.js
index a9b6ea40e4..ccefeb7690 100644
--- a/bigbluebutton-html5/imports/api/group-chat/server/methods/createGroupChat.js
+++ b/bigbluebutton-html5/imports/api/group-chat/server/methods/createGroupChat.js
@@ -1,31 +1,28 @@
 import { Meteor } from 'meteor/meteor';
 import { check } from 'meteor/check';
 import RedisPubSub from '/imports/startup/server/redis';
+import { CHAT_ACCESS_PUBLIC, CHAT_ACCESS_PRIVATE } from '/imports/api/group-chat'
 
-export default function createGroupChat(credentials) {
+
+export default function createGroupChat(credentials, receiver) {
   const REDIS_CONFIG = Meteor.settings.private.redis;
   const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
+  const EVENT_NAME = 'CreateGroupChatReqMsg';
+
   const { meetingId, requesterUserId, requesterToken } = credentials;
 
   check(meetingId, String);
   check(requesterUserId, String);
   check(requesterToken, String);
+  check(receiver, Object);
 
-  const eventName = 'CreateGroupChatReqMsg';
-
-  const payload = {
-    // TODO: Implement this together with #4988
-    // correlationId: String,
-    // name: String,
-    // access: String,
-    // users: Vector[String],
-    // msg: Vector[{
-    //   correlationId: String,
-    //   sender: GroupChatUser,
-    //   color: String,
-    //   message: String
-    // }],
+  let payload = {
+    correlationId: `${requesterUserId}-${Date.now()}`,
+    msg: [],
+    users: [receiver.id],
+    access: CHAT_ACCESS_PRIVATE,
+    name: receiver.name
   };
 
-  return RedisPubSub.publishUserMessage(CHANNEL, eventName, meetingId, requesterUserId, payload);
+  return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload);
 }
diff --git a/bigbluebutton-html5/imports/api/group-chat/server/publishers.js b/bigbluebutton-html5/imports/api/group-chat/server/publishers.js
index 39657bea68..1c3daaad2a 100644
--- a/bigbluebutton-html5/imports/api/group-chat/server/publishers.js
+++ b/bigbluebutton-html5/imports/api/group-chat/server/publishers.js
@@ -1,11 +1,10 @@
+import GroupChat, { CHAT_ACCESS_PUBLIC } from '/imports/api/group-chat';
 import { Meteor } from 'meteor/meteor';
 import { check } from 'meteor/check';
 
 import Logger from '/imports/startup/server/logger';
 import mapToAcl from '/imports/startup/mapToAcl';
 
-import { GroupChat, CHAT_ACCESS_PUBLIC } from '/imports/api/group-chat';
-
 function groupChat(credentials) {
   const { meetingId, requesterUserId, requesterToken } = credentials;
 
@@ -15,17 +14,7 @@ function groupChat(credentials) {
 
   Logger.info(`Publishing group-chat for ${meetingId} ${requesterUserId} ${requesterToken}`);
 
-  return GroupChat.find({
-    $or: [
-      {
-        access: CHAT_ACCESS_PUBLIC,
-        meetingId,
-      }, {
-        users: { $in: [requesterUserId] },
-        meetingId,
-      },
-    ],
-  });
+  return GroupChat.find({ meetingId });
 }
 
 function publish(...args) {
diff --git a/bigbluebutton-html5/imports/startup/client/base.jsx b/bigbluebutton-html5/imports/startup/client/base.jsx
index 46d5e1b9e2..4a2f4d4416 100755
--- a/bigbluebutton-html5/imports/startup/client/base.jsx
+++ b/bigbluebutton-html5/imports/startup/client/base.jsx
@@ -100,6 +100,7 @@ Base.defaultProps = defaultProps;
 const SUBSCRIPTIONS_NAME = [
   'users', 'chat', 'meetings', 'polls', 'presentations',
   'slides', 'captions', 'breakouts', 'voiceUsers', 'whiteboard-multi-user', 'screenshare',
+  'group-chat', 'group-chat-msg',
 ];
 
 const BaseContainer = withRouter(withTracker(({ params, router }) => {
diff --git a/bigbluebutton-html5/imports/ui/components/chat/container.jsx b/bigbluebutton-html5/imports/ui/components/chat/container.jsx
index 37d2e99eec..38b19cb92f 100644
--- a/bigbluebutton-html5/imports/ui/components/chat/container.jsx
+++ b/bigbluebutton-html5/imports/ui/components/chat/container.jsx
@@ -52,9 +52,11 @@ export default injectIntl(withTracker(({ params, intl }) => {
   let systemMessageIntl = {};
 
   if (chatID === PUBLIC_CHAT_KEY) {
-    messages = ChatService.reduceAndMapMessages((ChatService.getPublicMessages()));
+    messages = ChatService.reduceAndMapGroupMessages(ChatService.getPublicGroupMessages());
   } else {
-    messages = ChatService.getPrivateMessages(chatID);
+    // messages = ChatService.getPrivateMessages(chatID);
+    messages = ChatService.getPrivateGroupMessages(chatID);
+
     const user = ChatService.getUser(chatID);
     chatName = user.name;
     systemMessageIntl = { 0: user.name };
@@ -115,7 +117,7 @@ export default injectIntl(withTracker(({ params, intl }) => {
 
       handleSendMessage: (message) => {
         ChatService.updateScrollPosition(chatID, null);
-        return ChatService.sendMessage(chatID, message);
+        return ChatService.sendGroupMessage(chatID, message);
       },
 
       handleScrollUpdate: position => ChatService.updateScrollPosition(chatID, position),
diff --git a/bigbluebutton-html5/imports/ui/components/chat/service.js b/bigbluebutton-html5/imports/ui/components/chat/service.js
index 085e0c2eec..859ae4d5be 100755
--- a/bigbluebutton-html5/imports/ui/components/chat/service.js
+++ b/bigbluebutton-html5/imports/ui/components/chat/service.js
@@ -1,6 +1,8 @@
 import Chats from '/imports/api/chat';
 import Users from '/imports/api/users';
 import Meetings from '/imports/api/meetings';
+import GroupChatMsg, { GROUP_MESSAGE_PUBLIC_ID, CHAT_ACCESS_PRIVATE } from '/imports/api/group-chat-msg';
+import GroupChat from '/imports/api/group-chat';
 import Auth from '/imports/ui/services/auth';
 import UnreadMessages from '/imports/ui/services/unread-messages';
 import Storage from '/imports/ui/services/storage/session';
@@ -48,6 +50,21 @@ const mapMessage = (message) => {
   return mappedMessage;
 };
 
+const mapGroupMessage = (message) => {
+  const mappedMessage = {
+    id: message._id,
+    content: message.content,
+    time: message.timestamp, // + message.from_tz_offset,
+    sender: null,
+  };
+
+  if (message.sender !== SYSTEM_CHAT_TYPE) {
+    mappedMessage.sender = getUser(message.sender);
+  }
+
+  return mappedMessage;
+};
+
 const reduceMessages = (previous, current) => {
   const lastMessage = previous[previous.length - 1];
   const currentMessage = current;
@@ -78,6 +95,42 @@ const reduceMessages = (previous, current) => {
 const reduceAndMapMessages = messages =>
   (messages.reduce(reduceMessages, []).map(mapMessage));
 
+const reduceAndMapGroupMessages = messages => (messages.reduce(reduceGroupMessages, []).map(mapGroupMessage));
+
+const reduceGroupMessages = (previous, current) => {
+  const lastMessage = previous[previous.length - 1];
+  const currentMessage = current;
+    currentMessage.content = [{
+    id: current.id,
+    text: current.message,
+    time: current.timestamp,
+  }];
+    if (!lastMessage || !currentMessage.chatId === GROUP_MESSAGE_PUBLIC_ID) {
+    return previous.concat(currentMessage);
+  }
+  // Check if the last message is from the same user and time discrepancy
+  // between the two messages exceeds window and then group current message
+  // with the last one
+  const timeOfLastMessage = lastMessage.content[lastMessage.content.length - 1].time;
+  if (lastMessage.sender === currentMessage.sender
+    && (currentMessage.timestamp - timeOfLastMessage) <= GROUPING_MESSAGES_WINDOW) {
+    lastMessage.content.push(currentMessage.content.pop());
+    return previous;
+  }
+
+  return previous.concat(currentMessage);
+};
+
+const getPublicGroupMessages = () => {
+  const publicGroupMessages = GroupChatMsg.find({
+    chatId: GROUP_MESSAGE_PUBLIC_ID
+  }, {
+    sort: ['timestamp']
+  }).fetch();
+
+  return publicGroupMessages;
+};
+
 const getPublicMessages = () => {
   const publicMessages = Chats.find({
     type: { $in: [PUBLIC_CHAT_TYPE, SYSTEM_CHAT_TYPE] },
@@ -101,6 +154,28 @@ const getPrivateMessages = (userID) => {
   return reduceAndMapMessages(messages);
 };
 
+const getPrivateGroupMessages = (chatID) => {
+  const sender = getUser(Auth.userID);
+
+  const privateChat = GroupChat.findOne({ users: { $all: [chatID, sender.id] } });
+
+  let messages = [];
+
+  if (privateChat) {
+    const {
+      chatId
+    } = privateChat;
+
+    messages = GroupChatMsg.find({
+        chatId: chatId
+    }, {
+      sort: ['timestamp']
+    }).fetch();
+  }
+
+  return reduceAndMapGroupMessages(messages, []);
+};
+
 const isChatLocked = (receiverID) => {
   const isPublic = receiverID === PUBLIC_CHAT_ID;
 
@@ -132,6 +207,35 @@ const lastReadMessageTime = (receiverID) => {
   return UnreadMessages.get(chatType);
 };
 
+const sendGroupMessage = (chatID, message) => {
+
+  const isPublicChat = chatID === 'public';
+
+  let chatId = GROUP_MESSAGE_PUBLIC_ID;
+
+  const sender = getUser(Auth.userID);
+
+  if (!isPublicChat) {
+    let privateChat = GroupChat.findOne({ users: { $all: [chatID, sender.id] } });
+
+    if (privateChat) {
+      chatId = privateChat.chatId;
+    }
+  }
+
+  const payload = {
+    color: "0",
+    correlationId: `${sender.id}-${Date.now()}`,
+    sender: {
+      id: sender.id,
+      name: sender.name
+    },
+    message: message
+  };
+
+  return makeCall('sendGroupChatMsg', chatId, payload);
+};
+
 const sendMessage = (receiverID, message) => {
   const isPublic = receiverID === PUBLIC_CHAT_ID;
 
@@ -247,8 +351,11 @@ const getNotified = (chat) => {
 
 export default {
   reduceAndMapMessages,
+  reduceAndMapGroupMessages,
+  getPublicGroupMessages,
   getPublicMessages,
   getPrivateMessages,
+  getPrivateGroupMessages,
   getUser,
   getScrollPosition,
   hasUnreadMessages,
@@ -256,6 +363,7 @@ export default {
   isChatLocked,
   updateScrollPosition,
   updateUnreadMessage,
+  sendGroupMessage,
   sendMessage,
   closePrivateChat,
   removeFromClosedChatsSession,
diff --git a/bigbluebutton-html5/imports/ui/components/user-list/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/component.jsx
index 27c04e3ac7..2bcd6de254 100755
--- a/bigbluebutton-html5/imports/ui/components/user-list/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/user-list/component.jsx
@@ -27,6 +27,7 @@ const propTypes = {
   toggleVoice: PropTypes.func.isRequired,
   changeRole: PropTypes.func.isRequired,
   roving: PropTypes.func.isRequired,
+  getGroupChatPrivate: PropTypes.func.isRequired,
 };
 const SHOW_BRANDING = Meteor.settings.public.app.branding.displayBrandingArea;
 const defaultProps = {
@@ -62,6 +63,7 @@ class UserList extends Component {
       isPublicChat,
       roving,
       CustomLogoUrl,
+      getGroupChatPrivate,
     } = this.props;
 
     return (
@@ -91,6 +93,7 @@ class UserList extends Component {
           isMeetingLocked,
           isPublicChat,
           roving,
+          getGroupChatPrivate,
         }
       }
         />}
diff --git a/bigbluebutton-html5/imports/ui/components/user-list/container.jsx b/bigbluebutton-html5/imports/ui/components/user-list/container.jsx
index f30d12964c..a21bd8419c 100755
--- a/bigbluebutton-html5/imports/ui/components/user-list/container.jsx
+++ b/bigbluebutton-html5/imports/ui/components/user-list/container.jsx
@@ -22,6 +22,7 @@ const propTypes = {
   toggleVoice: PropTypes.func.isRequired,
   changeRole: PropTypes.func.isRequired,
   roving: PropTypes.func.isRequired,
+  getGroupChatPrivate: PropTypes.func.isRequired,
 };
 
 const UserListContainer = props => <UserList {...props} />;
@@ -46,4 +47,5 @@ export default withTracker(({ chatID, compact }) => ({
   roving: Service.roving,
   CustomLogoUrl: Service.getCustomLogoUrl(),
   compact,
+  getGroupChatPrivate: Service.getGroupChatPrivate,
 }))(UserListContainer);
diff --git a/bigbluebutton-html5/imports/ui/components/user-list/service.js b/bigbluebutton-html5/imports/ui/components/user-list/service.js
index 11290c48f1..3981d91207 100755
--- a/bigbluebutton-html5/imports/ui/components/user-list/service.js
+++ b/bigbluebutton-html5/imports/ui/components/user-list/service.js
@@ -1,5 +1,6 @@
 import Users from '/imports/api/users';
 import Chat from '/imports/api/chat';
+import GroupChat from '/imports/api/group-chat';
 import Meetings from '/imports/api/meetings';
 import Auth from '/imports/ui/services/auth';
 import UnreadMessages from '/imports/ui/services/unread-messages';
@@ -393,6 +394,14 @@ const roving = (event, itemCount, changeState) => {
   }
 };
 
+const getGroupChatPrivate = (sender, receiver) => {
+  let privateChat = GroupChat.findOne({ users: { $all: [receiver.id, sender.id] } });
+
+  if (!privateChat) {
+    makeCall("createGroupChat", receiver);
+  }
+};
+
 export default {
   setEmojiStatus,
   assignPresenter,
@@ -409,4 +418,5 @@ export default {
   roving,
   setCustomLogoUrl,
   getCustomLogoUrl,
+  getGroupChatPrivate,
 };
diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/component.jsx
index 74025e823c..227f2159ba 100644
--- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/component.jsx
@@ -24,6 +24,7 @@ const propTypes = {
   toggleVoice: PropTypes.func.isRequired,
   changeRole: PropTypes.func.isRequired,
   roving: PropTypes.func.isRequired,
+  getGroupChatPrivate: PropTypes.func.isRequired,
 };
 
 const defaultProps = {
@@ -65,6 +66,7 @@ class UserContent extends Component {
           normalizeEmojiName={this.props.normalizeEmojiName}
           isMeetingLocked={this.props.isMeetingLocked}
           roving={this.props.roving}
+          getGroupChatPrivate={this.props.getGroupChatPrivate}
         />
       </div>
     );
diff --git a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/component.jsx b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/component.jsx
index df0745e206..e77065cc4a 100755
--- a/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/component.jsx
+++ b/bigbluebutton-html5/imports/ui/components/user-list/user-list-content/user-participants/component.jsx
@@ -142,13 +142,17 @@ class UserParticipants extends Component {
       setEmojiStatus,
       removeUser,
       toggleVoice,
+      getGroupChatPrivate,
     } = this.props;
 
     const userActions =
     {
       openChat: {
         label: () => intl.formatMessage(intlMessages.ChatLabel),
-        handler: (router, user) => router.push(`/users/chat/${user.id}`),
+        handler: (router, user) => {
+          getGroupChatPrivate(currentUser, user)
+          router.push(`/users/chat/${user.id}`)
+        },
         icon: 'chat',
       },
       clearStatus: {
diff --git a/bigbluebutton-html5/server/main.js b/bigbluebutton-html5/server/main.js
index 2a41da487e..d9900dbedf 100644
--- a/bigbluebutton-html5/server/main.js
+++ b/bigbluebutton-html5/server/main.js
@@ -12,6 +12,7 @@ import '/imports/api/slides/server';
 import '/imports/api/breakouts/server';
 import '/imports/api/chat/server';
 import '/imports/api/group-chat/server';
+import '/imports/api/group-chat-msg/server';
 import '/imports/api/screenshare/server';
 import '/imports/api/voice-users/server';
 import '/imports/api/whiteboard-multi-user/server';
-- 
GitLab