From 71e0ca9d922f02acf2b7b9291209e1dbb03cb24f Mon Sep 17 00:00:00 2001 From: Tainan Felipe <tainanfelipe214@gmail.com> Date: Tue, 2 Mar 2021 11:35:26 -0300 Subject: [PATCH] Add sync and reconnection handler --- .../api/group-chat-msg/server/methods.js | 4 ++ .../methods/chatMessageBeforeJoinCounter.js | 30 ++++++++ .../server/methods/fetchMessagePerPage.js | 22 ++++++ .../api/group-chat-msg/server/publishers.js | 4 +- .../imports/ui/components/chat/container.jsx | 25 ++++++- .../components-data/chat-context/adapter.jsx | 69 +++++++++++++++++-- .../components-data/chat-context/context.jsx | 57 +++++++++++++-- .../private/config/settings.yml | 2 + bigbluebutton-html5/private/locales/en.json | 1 + 9 files changed, 196 insertions(+), 18 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 40288796db..ede79c838d 100644 --- a/bigbluebutton-html5/imports/api/group-chat-msg/server/publishers.js +++ b/bigbluebutton-html5/imports/api/group-chat-msg/server/publishers.js @@ -22,14 +22,12 @@ function groupChatMsg(chatsIds) { const User = Users.findOne({ userId }); const selector = { - // change loginTime to lasJoinTime when available - timestamp: { $gte: User.loginTime }, + timestamp: { $gte: User.authTokenValidatedTime }, $or: [ { meetingId, chatId: { $eq: PUBLIC_GROUP_CHAT_ID } }, { chatId: { $in: chatsIds } }, ], }; - console.log('Users\n\n', selector, GroupChatMsg.find(selector).fetch()); return GroupChatMsg.find(selector); } diff --git a/bigbluebutton-html5/imports/ui/components/chat/container.jsx b/bigbluebutton-html5/imports/ui/components/chat/container.jsx index 229f1c98f0..8ba901ea19 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,6 +44,10 @@ 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; @@ -121,7 +126,23 @@ const ChatContainer = (props) => { applyPropsToState = () => { if (!_.isEqualWith(lastMsg, stateLastMsg) || previousChatId !== chatID) { 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) { 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..7778401cb5 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; @@ -161,7 +166,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 +174,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 +264,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)}`); } diff --git a/bigbluebutton-html5/private/config/settings.yml b/bigbluebutton-html5/private/config/settings.yml index 85e8e1b4c8..96483b541b 100755 --- a/bigbluebutton-html5/private/config/settings.yml +++ b/bigbluebutton-html5/private/config/settings.yml @@ -298,6 +298,8 @@ public: lines: 2 time: 5000 chat: + itemsPerPage: 100 + timeBetweenFetchs: 1000 enabled: true bufferChatInsertsMs: 0 startClosed: false diff --git a/bigbluebutton-html5/private/locales/en.json b/bigbluebutton-html5/private/locales/en.json index 6f5a08db03..8a1dc013fb 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