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