diff --git a/bigbluebutton-html5/imports/api/meetings/server/modifiers/meetingHasEnded.js b/bigbluebutton-html5/imports/api/meetings/server/modifiers/meetingHasEnded.js index 4448dc1908b13e6b6023d0faedbaa3f1b5d27dd6..7651b0fd5b6cfdde951d668b40f88ccc6705de7c 100755 --- a/bigbluebutton-html5/imports/api/meetings/server/modifiers/meetingHasEnded.js +++ b/bigbluebutton-html5/imports/api/meetings/server/modifiers/meetingHasEnded.js @@ -28,6 +28,8 @@ import clearRecordMeeting from './clearRecordMeeting'; import clearVoiceCallStates from '/imports/api/voice-call-states/server/modifiers/clearVoiceCallStates'; import clearVideoStreams from '/imports/api/video-streams/server/modifiers/clearVideoStreams'; import clearAuthTokenValidation from '/imports/api/auth-token-validation/server/modifiers/clearAuthTokenValidation'; +import clearUsersPersistentData from '/imports/api/users-persistent-data/server/modifiers/clearUsersPersistentData'; + import clearWhiteboardMultiUser from '/imports/api/whiteboard-multi-user/server/modifiers/clearWhiteboardMultiUser'; import Metrics from '/imports/startup/server/metrics'; @@ -62,6 +64,7 @@ export default function meetingHasEnded(meetingId) { clearAuthTokenValidation(meetingId); clearWhiteboardMultiUser(meetingId); clearScreenshare(meetingId); + clearUsersPersistentData(meetingId); BannedUsers.delete(meetingId); Metrics.removeMeeting(meetingId); diff --git a/bigbluebutton-html5/imports/api/users-persistent-data/index.js b/bigbluebutton-html5/imports/api/users-persistent-data/index.js new file mode 100644 index 0000000000000000000000000000000000000000..ab5a6d5337560ae656d97ac8359e48b7f07268f4 --- /dev/null +++ b/bigbluebutton-html5/imports/api/users-persistent-data/index.js @@ -0,0 +1,9 @@ +import { Meteor } from 'meteor/meteor'; + +const UsersPersistentData = new Mongo.Collection('users-persistent-data'); + +if (Meteor.isServer) { + UsersPersistentData._ensureIndex({ meetingId: 1, userId: 1 }); +} + +export default UsersPersistentData; diff --git a/bigbluebutton-html5/imports/api/users-persistent-data/server/index.js b/bigbluebutton-html5/imports/api/users-persistent-data/server/index.js new file mode 100644 index 0000000000000000000000000000000000000000..f7744b2befe8082e50007a8b8ebb3926b7e8a88a --- /dev/null +++ b/bigbluebutton-html5/imports/api/users-persistent-data/server/index.js @@ -0,0 +1 @@ +import './publishers'; diff --git a/bigbluebutton-html5/imports/api/users-persistent-data/server/modifiers/addUserPersistentData.js b/bigbluebutton-html5/imports/api/users-persistent-data/server/modifiers/addUserPersistentData.js new file mode 100644 index 0000000000000000000000000000000000000000..224fdeda73fedc9b4c2d7bcbef6f51ff1a0fb804 --- /dev/null +++ b/bigbluebutton-html5/imports/api/users-persistent-data/server/modifiers/addUserPersistentData.js @@ -0,0 +1,76 @@ +import { check } from 'meteor/check'; +import UsersPersistentData from '/imports/api/users-persistent-data'; +import Logger from '/imports/startup/server/logger'; + +export default function addUserPersistentData(user) { + check(user, { + meetingId: String, + sortName: String, + color: String, + mobile: Boolean, + breakoutProps: Object, + inactivityCheck: Boolean, + responseDelay: Number, + loggedOut: Boolean, + intId: String, + extId: String, + name: String, + role: String, + guest: Boolean, + authed: Boolean, + guestStatus: String, + emoji: String, + presenter: Boolean, + locked: Boolean, + avatar: String, + clientType: String, + effectiveConnectionType: null, + }); + + + const { + intId, + extId, + meetingId, + name, + role, + token, + avatar, + guest, + color, + } = user; + + const userData = { + userId: intId, + extId, + meetingId, + name, + role, + token, + avatar, + guest, + color, + loggedOut: false, + }; + + const selector = { + userId: intId, + meetingId, + }; + + const modifier = { + $set: userData, + }; + + try { + const { insertedId } = UsersPersistentData.upsert(selector, modifier); + + if (insertedId) { + Logger.info(`Added user id=${intId} to user persistent Data: meeting=${meetingId}`); + } else { + Logger.info(`Upserted user id=${intId} to user persistent Data: meeting=${meetingId}`); + } + } catch (err) { + Logger.error(`Adding note to the collection: ${err}`); + } +} diff --git a/bigbluebutton-html5/imports/api/users-persistent-data/server/modifiers/clearUsersPersistentData.js b/bigbluebutton-html5/imports/api/users-persistent-data/server/modifiers/clearUsersPersistentData.js new file mode 100644 index 0000000000000000000000000000000000000000..4a5283c7e0dba7e6125942ba826dee23f602b7cf --- /dev/null +++ b/bigbluebutton-html5/imports/api/users-persistent-data/server/modifiers/clearUsersPersistentData.js @@ -0,0 +1,26 @@ +import Logger from '/imports/startup/server/logger'; +import UsersPersistentData from '/imports/api/users-persistent-data/index'; + +export default function clearUsersPersistentData(meetingId) { + if (meetingId) { + try { + const numberAffected = UsersPersistentData.remove({ meetingId }); + + if (numberAffected) { + Logger.info(`Cleared users persistent data (${meetingId})`); + } + } catch (err) { + Logger.error(`Error clearing users persistent data (${meetingId}). ${err}`); + } + } else { + try { + const numberAffected = UsersPersistentData.remove({}); + + if (numberAffected) { + Logger.info('Cleared users persistent data (all)'); + } + } catch (err) { + Logger.error(`Error clearing users persistent data (all). ${err}`); + } + } +} diff --git a/bigbluebutton-html5/imports/api/users-persistent-data/server/modifiers/setloggedOutStatus.js b/bigbluebutton-html5/imports/api/users-persistent-data/server/modifiers/setloggedOutStatus.js new file mode 100644 index 0000000000000000000000000000000000000000..7fb65739a9217a9431040e44366af389f4c63944 --- /dev/null +++ b/bigbluebutton-html5/imports/api/users-persistent-data/server/modifiers/setloggedOutStatus.js @@ -0,0 +1,26 @@ +import { check } from 'meteor/check'; +import UsersPersistentData from '/imports/api/users-persistent-data'; +import Logger from '/imports/startup/server/logger'; + +export default function setloggedOutStatus(userId, meetingId, status = true) { + check(userId, String); + check(meetingId, String); + check(status, Boolean); + + const selector = { + userId, + meetingId, + }; + + const modifier = { + $set: { + loggedOut: status, + }, + }; + + try { + UsersPersistentData.update(selector, modifier); + } catch (err) { + Logger.error(`Adding note to the collection: ${err}`); + } +} diff --git a/bigbluebutton-html5/imports/api/users-persistent-data/server/publishers.js b/bigbluebutton-html5/imports/api/users-persistent-data/server/publishers.js new file mode 100644 index 0000000000000000000000000000000000000000..3a05cb45059f6b5c102d2b7e963c78dab54f593b --- /dev/null +++ b/bigbluebutton-html5/imports/api/users-persistent-data/server/publishers.js @@ -0,0 +1,33 @@ +import UsersPersistentData from '/imports/api/users-persistent-data'; +import { Meteor } from 'meteor/meteor'; +import { extractCredentials } from '/imports/api/common/server/helpers'; +import { check } from 'meteor/check'; + +function usersPersistentData() { + if (!this.userId) { + return UsersPersistentData.find({ meetingId: '' }); + } + const { meetingId, requesterUserId } = extractCredentials(this.userId); + + check(meetingId, String); + check(requesterUserId, String); + + const selector = { + meetingId, + }; + + const options = { + fields: { + meetingId: false, + }, + }; + + return UsersPersistentData.find(selector, options); +} + +function publishUsersPersistentData(...args) { + const boundUsers = usersPersistentData.bind(this); + return boundUsers(...args); +} + +Meteor.publish('users-persistent-data', publishUsersPersistentData); diff --git a/bigbluebutton-html5/imports/api/users/server/modifiers/addUser.js b/bigbluebutton-html5/imports/api/users/server/modifiers/addUser.js index d40b5f3874199708295f8b7b20dd77743f5bb406..6229deeb9dfd3911b68353d00c7915a051ef6967 100755 --- a/bigbluebutton-html5/imports/api/users/server/modifiers/addUser.js +++ b/bigbluebutton-html5/imports/api/users/server/modifiers/addUser.js @@ -5,7 +5,7 @@ import Meetings from '/imports/api/meetings'; import VoiceUsers from '/imports/api/voice-users/'; import _ from 'lodash'; import SanitizeHTML from 'sanitize-html'; - +import addUserPsersistentData from '/imports/api/users-persistent-data/server/modifiers/addUserPersistentData'; import stringHash from 'string-hash'; import flat from 'flat'; @@ -58,26 +58,28 @@ export default function addUser(meetingId, userData) { from a list based on the userId */ const color = COLOR_LIST[stringHash(user.intId) % COLOR_LIST.length]; - const modifier = { - $set: Object.assign( - { - meetingId, - sortName: user.name.trim().toLowerCase(), - color, - mobile: false, - breakoutProps: { - isBreakoutUser: Meeting.meetingProp.isBreakout, - parentId: Meeting.breakoutProps.parentId, - }, - effectiveConnectionType: null, - inactivityCheck: false, - responseDelay: 0, - loggedOut: false, + const userInfos = Object.assign( + { + meetingId, + sortName: user.name.trim().toLowerCase(), + color, + mobile: false, + breakoutProps: { + isBreakoutUser: Meeting.meetingProp.isBreakout, + parentId: Meeting.breakoutProps.parentId, }, - flat(user), - ), - }; + effectiveConnectionType: null, + inactivityCheck: false, + responseDelay: 0, + loggedOut: false, + }, + flat(user), + ); + const modifier = { + $set: userInfos, + }; + addUserPsersistentData(userInfos); // Only add an empty VoiceUser if there isn't one already and if the user coming in isn't a // dial-in user. We want to avoid overwriting good data if (user.clientType !== 'dial-in-user' && !VoiceUsers.findOne({ meetingId, intId: userId })) { diff --git a/bigbluebutton-html5/imports/api/users/server/modifiers/removeUser.js b/bigbluebutton-html5/imports/api/users/server/modifiers/removeUser.js index 48e005873d09cb61a4126f9de45e1b1b48f0c3f3..37af85aec7a2243d8dbf4069e309be211d623b72 100755 --- a/bigbluebutton-html5/imports/api/users/server/modifiers/removeUser.js +++ b/bigbluebutton-html5/imports/api/users/server/modifiers/removeUser.js @@ -2,6 +2,7 @@ import { check } from 'meteor/check'; import Users from '/imports/api/users'; import VideoStreams from '/imports/api/video-streams'; import Logger from '/imports/startup/server/logger'; +import setloggedOutStatus from '/imports/api/users-persistent-data/server/modifiers/setloggedOutStatus'; import stopWatchingExternalVideoSystemCall from '/imports/api/external-videos/server/methods/stopWatchingExternalVideoSystemCall'; import clearUserInfoForRequester from '/imports/api/users-infos/server/modifiers/clearUserInfoForRequester'; import ClientConnections from '/imports/startup/server/ClientConnections'; @@ -32,6 +33,7 @@ export default function removeUser(meetingId, userId) { }; try { + setloggedOutStatus(userId, meetingId, true); VideoStreams.remove({ meetingId, userId }); const sessionUserId = `${meetingId}-${userId}`; diff --git a/bigbluebutton-html5/imports/ui/components/chat/time-window-list/time-window-chat-item/container.jsx b/bigbluebutton-html5/imports/ui/components/chat/time-window-list/time-window-chat-item/container.jsx index d22afc033160b971d784c63311cb1ab99a93e556..54496761446b59c432c41bda7be375c1d7abdcaf 100644 --- a/bigbluebutton-html5/imports/ui/components/chat/time-window-list/time-window-chat-item/container.jsx +++ b/bigbluebutton-html5/imports/ui/components/chat/time-window-list/time-window-chat-item/container.jsx @@ -50,7 +50,7 @@ export default function TimeWindowChatItemContainer(props) { ...{ color: user?.color, isModerator: user?.role === ROLE_MODERATOR, - isOnline: !!user, + isOnline: !user?.loggedOut, avatar: user?.avatar, name: user?.name, read: message.read, diff --git a/bigbluebutton-html5/imports/ui/components/components-data/users-context/adapter.jsx b/bigbluebutton-html5/imports/ui/components/components-data/users-context/adapter.jsx index 3d6d9ca29eace430bdc6b0232b2f71fb54fb66ba..f412c155f45f03df5bddd42beda67c7d86da98f7 100644 --- a/bigbluebutton-html5/imports/ui/components/components-data/users-context/adapter.jsx +++ b/bigbluebutton-html5/imports/ui/components/components-data/users-context/adapter.jsx @@ -1,5 +1,6 @@ import { useContext, useEffect } from 'react'; import Users from '/imports/api/users'; +import UsersPersistentData from '/imports/api/users-persistent-data'; import { UsersContext, ACTIONS } from './context'; import { ChatContext, ACTIONS as CHAT_ACTIONS } from '../chat-context/context'; import ChatLogger from '/imports/ui/components/chat/chat-logger/ChatLogger'; @@ -8,8 +9,30 @@ const Adapter = () => { const usingUsersContext = useContext(UsersContext); const { dispatch } = usingUsersContext; - const usingChatContext = useContext(ChatContext); - const { dispatch: chatDispatch } = usingChatContext; + useEffect(()=> { + const usersPersistentDataCursor = UsersPersistentData.find({}, { sort: { timestamp: 1 } }); + usersPersistentDataCursor.observe({ + added: (obj) => { + ChatLogger.debug("usersAdapter::observe::added_persistent_user", obj); + dispatch({ + type: ACTIONS.ADDED_USER_PERSISTENT_DATA, + value: { + user: obj, + }, + }); + }, + changed: (obj) => { + ChatLogger.debug("usersAdapter::observe::changed_persistent_user", obj); + dispatch({ + type: ACTIONS.CHANGED_USER_PERSISTENT_DATA, + value: { + user: obj, + }, + }); + }, + removed: (obj) => {}, + }); + }, []); useEffect(() => { const usersCursor = Users.find({}, { sort: { timestamp: 1 } }); @@ -31,14 +54,6 @@ const Adapter = () => { }, }); }, - removed: (obj) => { - dispatch({ - type: ACTIONS.REMOVED, - value: { - user: obj, - }, - }); - }, }); }, []); diff --git a/bigbluebutton-html5/imports/ui/components/components-data/users-context/context.jsx b/bigbluebutton-html5/imports/ui/components/components-data/users-context/context.jsx index bf107b4038acdf70e11031dbd1eb2cad8bd4f5e3..18585a8b2a75a8f0fb46b10783c57581e794baf3 100644 --- a/bigbluebutton-html5/imports/ui/components/components-data/users-context/context.jsx +++ b/bigbluebutton-html5/imports/ui/components/components-data/users-context/context.jsx @@ -9,6 +9,8 @@ export const ACTIONS = { ADDED: 'added', CHANGED: 'changed', REMOVED: 'removed', + ADDED_USER_PERSISTENT_DATA: 'added_user_persistent_data', + CHANGED_USER_PERSISTENT_DATA: 'changed_user_persistent_data', }; export const UsersContext = createContext(); @@ -47,6 +49,37 @@ const reducer = (state, action) => { return state; } + + //USER PERSISTENT DATA + case ACTIONS.ADDED_USER_PERSISTENT_DATA: { + const { user } = action.value; + if (state[user.userId]) { + return state; + } + + const newState = { + ...state, + [user.userId]: { + ...user, + }, + }; + return newState; + } + case ACTIONS.CHANGED_USER_PERSISTENT_DATA: { + const { user } = action.value; + const stateUser = state[user.userId]; + if (stateUser) { + const newState = { + ...state, + [user.userId]: { + ...stateUser, + ...user, + }, + }; + return newState; + } + return state; + } default: { throw new Error(`Unexpected action: ${JSON.stringify(action)}`); } diff --git a/bigbluebutton-html5/imports/ui/components/subscriptions/component.jsx b/bigbluebutton-html5/imports/ui/components/subscriptions/component.jsx index 32b4698e35c0bedd2a33fa74682e37edebd95fb4..72c73e42e492b5a4e4a12beffc48b1aa36af094d 100755 --- a/bigbluebutton-html5/imports/ui/components/subscriptions/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/subscriptions/component.jsx @@ -36,6 +36,8 @@ class Subscriptions extends Component { } } +let usersPersistentDataHandler = null; + export default withTracker(() => { const { credentials } = Auth; const { meetingId, requesterUserId } = credentials; @@ -105,6 +107,9 @@ export default withTracker(() => { const chatIds = chats.map(chat => chat.chatId); groupChatMessageHandler = Meteor.subscribe('group-chat-msg', chatIds, subscriptionErrorHandler); } + if (ready && !usersPersistentDataHandler) { + usersPersistentDataHandler = Meteor.subscribe('users-persistent-data'); + } return { subscriptionsReady: ready, diff --git a/bigbluebutton-html5/server/main.js b/bigbluebutton-html5/server/main.js index f80a2ed45c29ca366ea60ccf24fca8668f194d54..3ff24d74a20fa2605ef534adde9bffb0ed0d65c1 100755 --- a/bigbluebutton-html5/server/main.js +++ b/bigbluebutton-html5/server/main.js @@ -21,6 +21,7 @@ import '/imports/api/whiteboard-multi-user/server'; import '/imports/api/video-streams/server'; import '/imports/api/network-information/server'; import '/imports/api/users-infos/server'; +import '/imports/api/users-persistent-data/server'; import '/imports/api/connection-status/server'; import '/imports/api/note/server'; import '/imports/api/external-videos/server';