User information lookup feature in HTML5 client.

This commit is contained in:
Maxim Khlobystov 2019-04-05 18:32:21 -04:00
parent 2d5d23f75f
commit 3fa22c6908
31 changed files with 305 additions and 4 deletions

View File

@ -11,6 +11,7 @@ import clearPolls from '/imports/api/polls/server/modifiers/clearPolls';
import clearCaptions from '/imports/api/captions/server/modifiers/clearCaptions';
import clearPresentationPods from '/imports/api/presentation-pods/server/modifiers/clearPresentationPods';
import clearVoiceUsers from '/imports/api/voice-users/server/modifiers/clearVoiceUsers';
import clearUserInfo from '/imports/api/users-infos/server/modifiers/clearUserInfo';
export default function removeMeeting(meetingId) {
@ -25,6 +26,7 @@ export default function removeMeeting(meetingId) {
clearUsers(meetingId);
clearUsersSettings(meetingId);
clearVoiceUsers(meetingId);
clearUserInfo(meetingId);
return Logger.info(`Cleared Meetings with id ${meetingId}`);
});

View File

@ -0,0 +1,9 @@
import { Meteor } from 'meteor/meteor';
const UserInfos = new Mongo.Collection('users-infos');
if (Meteor.isServer) {
UserInfos._ensureIndex({ meetingId: 1, userId: 1 });
}
export default UserInfos;

View File

@ -0,0 +1,4 @@
import RedisPubSub from '/imports/startup/server/redis';
import handleUserInformation from './handlers/userInformation';
RedisPubSub.on('LookUpUserRespMsg', handleUserInformation);

View File

@ -0,0 +1,8 @@
import { check } from 'meteor/check';
import addUserInfo from '../modifiers/addUserInfo';
export default function handleUserInformation({ header, body }, meetingId) {
check(body, Object);
return addUserInfo(body.userInfo, header.userId, header.meetingId);
}

View File

@ -0,0 +1,3 @@
import './eventHandlers';
import './methods';
import './publishers';

View File

@ -0,0 +1,8 @@
import { Meteor } from 'meteor/meteor';
import requestUserInformation from './methods/requestUserInformation';
import removeUserInformation from './methods/removeUserInformation';
Meteor.methods({
requestUserInformation,
removeUserInformation,
});

View File

@ -0,0 +1,16 @@
import UserInfos from '/imports/api/users-infos';
import Logger from '/imports/startup/server/logger';
export default function removeUserInformation(credentials, meetingId, requesterUserId) {
const selector = {
meetingId,
requesterUserId,
};
const cb = (err, numChanged) => {
if (err) {
return Logger.error(`Removing user information from collection: ${err}`);
}
return Logger.info(`Removed user information: requester id=${requesterUserId} meeting=${meetingId}`);
};
return UserInfos.remove(selector, cb);
}

View File

@ -0,0 +1,22 @@
import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
import RedisPubSub from '/imports/startup/server/redis';
import Users from '/imports/api/users';
export default function getUserInformation(credentials, externalUserId) {
const REDIS_CONFIG = Meteor.settings.private.redis;
const CHANNEL = REDIS_CONFIG.channels.toThirdParty;
const EVENT_NAME = 'LookUpUserReqMsg';
const { meetingId, requesterUserId } = credentials;
check(meetingId, String);
check(requesterUserId, String);
check(externalUserId, String);
const payload = {
externalUserId,
};
return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload);
}

View File

@ -0,0 +1,18 @@
import UserInfos from '/imports/api/users-infos';
import Logger from '/imports/startup/server/logger';
export default function addUserInfo(userInfo, requesterUserId, meetingId) {
const info = {
meetingId,
requesterUserId,
userInfo,
};
const cb = (err, numChanged) => {
if (err) {
return Logger.error(`Adding user information to collection: ${err}`);
}
return Logger.info(`Added user information: requester id=${requesterUserId} meeting=${meetingId}`);
};
return UserInfos.insert(info, cb);
}

View File

@ -0,0 +1,8 @@
import UserInfos from '/imports/api/users-infos';
import Logger from '/imports/startup/server/logger';
export default function clearUsersInfo(meetingId) {
return UserInfos.remove({ meetingId }, () => {
Logger.info(`Cleared User Infos (${meetingId})`);
});
}

View File

@ -0,0 +1,8 @@
import UserInfos from '/imports/api/users-infos';
import Logger from '/imports/startup/server/logger';
export default function clearUsersInfoForRequester(meetingId, requesterUserId) {
return UserInfos.remove({ meetingId }, () => {
Logger.info(`Cleared User Infos requested by user=${requesterUserId}`);
});
}

View File

@ -0,0 +1,22 @@
import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
import UserInfos from '/imports/api/users-infos';
import Logger from '/imports/startup/server/logger';
function userInfos(credentials) {
const { meetingId, requesterUserId } = credentials;
check(meetingId, String);
check(requesterUserId, String);
Logger.debug(`Publishing user infos requested by user=${requesterUserId}`);
return UserInfos.find({ meetingId, requesterUserId });
}
function publish(...args) {
const boundUserInfos = userInfos.bind(this);
return boundUserInfos(...args);
}
Meteor.publish('users-infos', publish);

View File

@ -2,6 +2,7 @@ import { check } from 'meteor/check';
import Users from '/imports/api/users';
import Logger from '/imports/startup/server/logger';
import stopWatchingExternalVideo from '/imports/api/external-videos/server/methods/stopWatchingExternalVideo';
import clearUserInfoForRequester from '/imports/api/users-infos/server/modifiers/clearUserInfoForRequester';
const clearAllSessions = (sessionUserId) => {
const serverSessions = Meteor.server.sessions;
@ -46,6 +47,8 @@ export default function removeUser(meetingId, userId) {
const sessionUserId = `${meetingId}-${userId}`;
clearAllSessions(sessionUserId);
clearUserInfoForRequester(meetingId, userId);
return Logger.info(`Removed user id=${userId} meeting=${meetingId}`);
};

View File

@ -1,6 +1,7 @@
import { check } from 'meteor/check';
import Logger from '/imports/startup/server/logger';
import Users from '/imports/api/users';
import clearUserInfoForRequester from '/imports/api/users-infos/server/modifiers/clearUserInfoForRequester';
export default function userEjected(meetingId, userId) {
check(meetingId, String);
@ -23,6 +24,7 @@ export default function userEjected(meetingId, userId) {
}
if (numChanged) {
clearUserInfoForRequester(meetingId, userId);
return Logger.info(`Ejected user id=${userId} meeting=${meetingId}`);
}

View File

@ -10,6 +10,7 @@ import Settings from '/imports/ui/services/settings';
import AudioManager from '/imports/ui/services/audio-manager';
import logger from '/imports/startup/client/logger';
import Users from '/imports/api/users';
import UserInfos from '/imports/api/users-infos';
import Annotations from '/imports/api/annotations';
import AnnotationsLocal from '/imports/ui/components/whiteboard/service';
import GroupChat from '/imports/api/group-chat';
@ -159,7 +160,7 @@ Base.defaultProps = defaultProps;
const SUBSCRIPTIONS_NAME = [
'users', 'meetings', 'polls', 'presentations',
'slides', 'captions', 'voiceUsers', 'whiteboard-multi-user', 'screenshare',
'group-chat', 'presentation-pods', 'users-settings', 'guestUser',
'group-chat', 'presentation-pods', 'users-settings', 'guestUser', 'users-infos',
];
const BaseContainer = withTracker(() => {
@ -168,6 +169,7 @@ const BaseContainer = withTracker(() => {
const { meetingId, requesterUserId } = credentials;
let breakoutRoomSubscriptionHandler;
let userSubscriptionHandler;
let userInfoSubscriptionHandler;
const subscriptionErrorHandler = {
onError: (error) => {
@ -204,8 +206,12 @@ const BaseContainer = withTracker(() => {
// override meteor subscription to verify if is moderator
userSubscriptionHandler = Meteor.subscribe('users', credentials, mappedUser.isModerator, subscriptionErrorHandler);
breakoutRoomSubscriptionHandler = Meteor.subscribe('breakouts', credentials, mappedUser.isModerator, subscriptionErrorHandler);
userInfoSubscriptionhandler = Meteor.subscribe('users-infos', credentials, subscriptionErrorHandler);
}
const UserInfo = UserInfos.find({ meetingId, requesterUserId }).fetch();
const annotationsHandler = Meteor.subscribe('annotations', credentials, {
onReady: () => {
AnnotationsLocal.remove({});
@ -230,10 +236,12 @@ const BaseContainer = withTracker(() => {
annotationsHandler,
groupChatMessageHandler,
userSubscriptionHandler,
userInfoSubscriptionHandler,
breakoutRoomSubscriptionHandler,
animations,
meetingExist: !!Meetings.find({ meetingId }).count(),
User,
UserInfo,
meteorIsConnected: Meteor.status().connected,
};
})(Base);

View File

@ -8,11 +8,11 @@ import Logger from './logger';
// Fake meetingId used for messages that have no meetingId
const NO_MEETING_ID = '_';
const makeEnvelope = (channel, eventName, header, body) => {
const makeEnvelope = (channel, eventName, header, body, routing) => {
const envelope = {
envelope: {
name: eventName,
routing: {
routing: routing || {
sender: 'bbb-apps-akka',
// sender: 'html5-server', // TODO
},
@ -227,7 +227,7 @@ class RedisPubSub {
userId,
};
const envelope = makeEnvelope(channel, eventName, header, payload);
const envelope = makeEnvelope(channel, eventName, header, payload, { meetingId, userId });
return this.pub.publish(channel, envelope, RedisPubSub.handlePublishError);
}

View File

@ -8,6 +8,7 @@ import PanelManager from '/imports/ui/components/panel-manager/component';
import PollingContainer from '/imports/ui/components/polling/container';
import logger from '/imports/startup/client/logger';
import ActivityCheckContainer from '/imports/ui/components/activity-check/container';
import UserInfoContainer from '/imports/ui/components/user-info/container';
import ToastContainer from '../toast/container';
import ModalContainer from '../modal/container';
import NotificationsBarContainer from '../notifications-bar/container';
@ -206,6 +207,17 @@ class App extends Component {
/>) : null);
}
renderUserInformation() {
const { UserInfo, User } = this.props;
return (UserInfo.length > 0 ? (
<UserInfoContainer
UserInfo={UserInfo}
requesterUserId={User.userId}
meetingId={User.meetingId}
/>) : null);
}
render() {
const {
customStyle, customStyleUrl, openPanel,
@ -214,6 +226,7 @@ class App extends Component {
return (
<main className={styles.main}>
{this.renderActivityCheck()}
{this.renderUserInformation()}
<NotificationsBarContainer />
<section className={styles.wrapper}>
<div className={openPanel ? styles.content : styles.noPanelContent}>

View File

@ -0,0 +1,75 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { defineMessages, intlShape } from 'react-intl';
import Button from '/imports/ui/components/button/component';
import Modal from '/imports/ui/components/modal/simple/component';
import { makeCall } from '/imports/ui/services/api';
import { styles } from './styles';
const propTypes = {
intl: intlShape.isRequired,
};
const intlMessages = defineMessages({
title: {
id: 'app.user-info.title',
description: 'User info title label',
},
});
class UserInfo extends Component {
constructor(props) {
super(props);
this.handleCloseUserInfo = this.handleCloseUserInfo.bind(this);
}
handleCloseUserInfo() {
const { mountModal, requesterUserId, meetingId } = this.props;
makeCall('removeUserInformation', meetingId, requesterUserId);
}
renderUserInfo(UserInfo) {
const userInfoList = UserInfo.map((user, index, array) => {
const infoList = user.userInfo.map((info) => {
const key = Object.keys(info)[0];
return (
<tr key={key}>
<td className={styles.keyCell}>{key}</td>
<td className={styles.valueCell}>{info[key]}</td>
</tr>
);
});
if (array.length > 1) {
infoList.unshift(<tr key={infoList.length}>
<th className={styles.titleCell}>{`User ${index + 1}`}</th>
</tr>);
}
return infoList;
});
return (
<table className={styles.userInfoTable}>
<tbody>
{userInfoList}
</tbody>
</table>
);
}
render() {
const { intl, UserInfo } = this.props;
return (
<Modal
title={intl.formatMessage(intlMessages.title)}
onRequestClose={this.handleCloseUserInfo}
>
{this.renderUserInfo(UserInfo)}
</Modal>
);
}
}
UserInfo.propTypes = propTypes;
export default UserInfo;

View File

@ -0,0 +1,7 @@
import React from 'react';
import { injectIntl } from 'react-intl';
import UserInfo from './component';
const UserInfoContainer = props => <UserInfo {...props} />;
export default injectIntl(UserInfoContainer);

View File

@ -0,0 +1,24 @@
@import "/imports/ui/stylesheets/variables/_all";
.keyCell, .valueCell, .userInfoTable {
border: var(--border-size) solid var(--color-gray-lighter);
}
.userInfoTable {
border-collapse: collapse;
border: none;
width: 90%;
margin: auto;
table-layout: fixed;
td {
word-wrap: break-word;
}
}
.keyCell, .valueCell {
padding: var(--md-padding-x);
}

View File

@ -32,6 +32,7 @@ const propTypes = {
getGroupChatPrivate: PropTypes.func.isRequired,
showBranding: PropTypes.bool.isRequired,
toggleUserLock: PropTypes.func.isRequired,
requestUserInformation: PropTypes.func.isRequired,
};
const defaultProps = {
@ -69,6 +70,7 @@ class UserList extends PureComponent {
getUsersId,
hasPrivateChatBetweenUsers,
toggleUserLock,
requestUserInformation,
} = this.props;
return (
@ -106,6 +108,7 @@ class UserList extends PureComponent {
getUsersId,
hasPrivateChatBetweenUsers,
toggleUserLock,
requestUserInformation,
}
}
/>}

View File

@ -25,6 +25,7 @@ const propTypes = {
roving: PropTypes.func.isRequired,
getGroupChatPrivate: PropTypes.func.isRequired,
toggleUserLock: PropTypes.func.isRequired,
requestUserInformation: PropTypes.func.isRequired,
};
const UserListContainer = props => <UserList {...props} />;
@ -58,4 +59,5 @@ export default withTracker(({ chatID, compact }) => ({
showBranding: getFromUserSettings('displayBrandingArea', Meteor.settings.public.app.branding.displayBrandingArea),
hasPrivateChatBetweenUsers: Service.hasPrivateChatBetweenUsers,
toggleUserLock: Service.toggleUserLock,
requestUserInformation: Service.requestUserInformation,
}))(UserListContainer);

View File

@ -461,6 +461,10 @@ const toggleUserLock = (userId, lockStatus) => {
makeCall('toggleUserLock', userId, lockStatus);
};
const requestUserInformation = (userId) => {
makeCall('requestUserInformation', userId);
};
export default {
setEmojiStatus,
assignPresenter,
@ -487,4 +491,5 @@ export default {
getEmoji: () => Users.findOne({ userId: Auth.userID }).emoji,
hasPrivateChatBetweenUsers,
toggleUserLock,
requestUserInformation,
};

View File

@ -34,6 +34,7 @@ const propTypes = {
pollIsOpen: PropTypes.bool.isRequired,
forcePollOpen: PropTypes.bool.isRequired,
toggleUserLock: PropTypes.func.isRequired,
requestUserInformation: PropTypes.func.isRequired,
};
const defaultProps = {
@ -72,6 +73,7 @@ class UserContent extends PureComponent {
hasPrivateChatBetweenUsers,
toggleUserLock,
pendingUsers,
requestUserInformation,
} = this.props;
return (
@ -136,6 +138,7 @@ class UserContent extends PureComponent {
getUsersId,
hasPrivateChatBetweenUsers,
toggleUserLock,
requestUserInformation,
}}
/>
</div>

View File

@ -32,6 +32,7 @@ const propTypes = {
isMeetingLocked: PropTypes.func.isRequired,
roving: PropTypes.func.isRequired,
toggleUserLock: PropTypes.func.isRequired,
requestUserInformation: PropTypes.func.isRequired,
};
const defaultProps = {
@ -128,6 +129,7 @@ class UserParticipants extends Component {
users,
hasPrivateChatBetweenUsers,
toggleUserLock,
requestUserInformation,
} = this.props;
let index = -1;
@ -164,6 +166,7 @@ class UserParticipants extends Component {
getGroupChatPrivate,
hasPrivateChatBetweenUsers,
toggleUserLock,
requestUserInformation,
}}
userId={u}
getScrollContainerRef={this.getScrollContainerRef}

View File

@ -49,6 +49,7 @@ class UserListItem extends PureComponent {
toggleVoice,
hasPrivateChatBetweenUsers,
toggleUserLock,
requestUserInformation,
} = this.props;
const { meetingId, lockSettingsProp } = meeting;
@ -78,6 +79,7 @@ class UserListItem extends PureComponent {
user,
hasPrivateChatBetweenUsers,
toggleUserLock,
requestUserInformation,
}}
/>
);

View File

@ -89,6 +89,10 @@ const messages = defineMessages({
id: 'app.userList.menu.lockUser.label',
description: 'Lock a unlocked user',
},
DirectoryLookupLabel: {
id: 'app.userList.menu.directoryLookup.label',
description: 'Directory lookup',
},
});
const propTypes = {
@ -201,6 +205,7 @@ class UserDropdown extends PureComponent {
lockSettingsProp,
hasPrivateChatBetweenUsers,
toggleUserLock,
requestUserInformation,
} = this.props;
const { showNestedOptions } = this.state;
@ -230,6 +235,8 @@ class UserDropdown extends PureComponent {
|| hasPrivateChatBetweenUsers(currentUser, user)
|| user.isModerator);
const allowUserLookup = Meteor.settings.public.app.allowUserLookup;
if (showNestedOptions) {
if (allowedToChangeStatus) {
actions.push(this.makeDropdownItem(
@ -349,6 +356,15 @@ class UserDropdown extends PureComponent {
));
}
if (allowUserLookup) {
actions.push(this.makeDropdownItem(
'directoryLookup',
intl.formatMessage(messages.DirectoryLookupLabel),
() => this.onActionsHide(requestUserInformation(user.externalUserId)),
'user',
));
}
return actions;
}

View File

@ -31,6 +31,7 @@ const mapUser = (user) => {
isOnline: user.connectionStatus === 'online',
clientType: user.clientType,
loginTime: user.loginTime,
externalUserId: user.extId,
};
mappedUser.isLocked = user.locked && !(mappedUser.isPresenter || mappedUser.isModerator);

View File

@ -17,6 +17,7 @@ public:
lockOnJoin: true
basename: "/html5client"
askForFeedbackOnLogout: false
allowUserLookup: false
defaultSettings:
application:
animations: true
@ -265,9 +266,11 @@ private:
debug: false
channels:
toAkkaApps: to-akka-apps-redis-channel
toThirdParty: to-third-party-redis-channel
subscribeTo:
- to-html5-redis-channel
- from-akka-apps-*
- from-third-party-redis-channel
async:
- from-akka-apps-wb-redis-channel
ignored:

View File

@ -49,6 +49,7 @@
"app.userList.menu.demoteUser.label": "Demote to viewer",
"app.userList.menu.unlockUser.label": "Unlock {0}",
"app.userList.menu.lockUser.label": "Lock {0}",
"app.userList.menu.directoryLookup.label": "Directory Lookup",
"app.userList.userOptions.manageUsersLabel": "Manage users",
"app.userList.userOptions.muteAllLabel": "Mute all users",
"app.userList.userOptions.muteAllDesc": "Mutes all users in the meeting",
@ -392,6 +393,7 @@
"app.userList.guest.pendingUsers": "{0} Pending Users",
"app.userList.guest.pendingGuestUsers": "{0} Pending Guest Users",
"app.userList.guest.pendingGuestAlert": "Has joined the session and is waiting for your approval.",
"app.user-info.title": "Directory Lookup",
"app.toast.breakoutRoomEnded": "The breakout room ended. Please rejoin in the audio.",
"app.toast.chat.public": "New Public Chat message",
"app.toast.chat.private": "New Private Chat message",

View File

@ -19,6 +19,7 @@ import '/imports/api/users-settings/server';
import '/imports/api/voice-users/server';
import '/imports/api/whiteboard-multi-user/server';
import '/imports/api/video/server';
import '/imports/api/users-infos/server';
import '/imports/api/external-videos/server';
import '/imports/api/guest-users/server';