Merge pull request #11669 from pedrobmarin/2.3-per-user-wb-v2

[Refactor] Whiteboard individual access
This commit is contained in:
Anton Georgiev 2021-03-17 09:04:36 -04:00 committed by GitHub
commit ba9665a2c8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 434 additions and 112 deletions

View File

@ -49,7 +49,6 @@ trait SystemConfiguration {
lazy val endMeetingWhenNoMoreAuthedUsers = Try(config.getBoolean("apps.endMeetingWhenNoMoreAuthedUsers")).getOrElse(false) lazy val endMeetingWhenNoMoreAuthedUsers = Try(config.getBoolean("apps.endMeetingWhenNoMoreAuthedUsers")).getOrElse(false)
lazy val endMeetingWhenNoMoreAuthedUsersAfterMinutes = Try(config.getInt("apps.endMeetingWhenNoMoreAuthedUsersAfterMinutes")).getOrElse(2) lazy val endMeetingWhenNoMoreAuthedUsersAfterMinutes = Try(config.getInt("apps.endMeetingWhenNoMoreAuthedUsersAfterMinutes")).getOrElse(2)
lazy val multiUserWhiteboardDefault = Try(config.getBoolean("whiteboard.multiUserDefault")).getOrElse(false)
// Redis server configuration // Redis server configuration
lazy val redisHost = Try(config.getString("redis.host")).getOrElse("127.0.0.1") lazy val redisHost = Try(config.getString("redis.host")).getOrElse("127.0.0.1")

View File

@ -25,7 +25,14 @@ class WhiteboardModel extends SystemConfiguration {
} }
private def createWhiteboard(wbId: String): Whiteboard = { private def createWhiteboard(wbId: String): Whiteboard = {
new Whiteboard(wbId, multiUserWhiteboardDefault, System.currentTimeMillis(), 0, new HashMap[String, List[AnnotationVO]]()) new Whiteboard(
wbId,
Array.empty[String],
Array.empty[String],
System.currentTimeMillis(),
0,
new HashMap[String, List[AnnotationVO]]()
)
} }
private def getAnnotationsByUserId(wb: Whiteboard, id: String): List[AnnotationVO] = { private def getAnnotationsByUserId(wb: Whiteboard, id: String): List[AnnotationVO] = {
@ -184,7 +191,7 @@ class WhiteboardModel extends SystemConfiguration {
if (hasWhiteboard(wbId)) { if (hasWhiteboard(wbId)) {
val wb = getWhiteboard(wbId) val wb = getWhiteboard(wbId)
if (wb.multiUser) { if (wb.multiUser.contains(userId)) {
if (wb.annotationsMap.contains(userId)) { if (wb.annotationsMap.contains(userId)) {
val newWb = wb.copy(annotationsMap = wb.annotationsMap - userId) val newWb = wb.copy(annotationsMap = wb.annotationsMap - userId)
saveWhiteboard(newWb) saveWhiteboard(newWb)
@ -205,7 +212,7 @@ class WhiteboardModel extends SystemConfiguration {
var last: Option[AnnotationVO] = None var last: Option[AnnotationVO] = None
val wb = getWhiteboard(wbId) val wb = getWhiteboard(wbId)
if (wb.multiUser) { if (wb.multiUser.contains(userId)) {
val usersAnnotations = getAnnotationsByUserId(wb, userId) val usersAnnotations = getAnnotationsByUserId(wb, userId)
//not empty and head id equals annotation id //not empty and head id equals annotation id
@ -234,13 +241,21 @@ class WhiteboardModel extends SystemConfiguration {
wb.copy(annotationsMap = newAnnotationsMap) wb.copy(annotationsMap = newAnnotationsMap)
} }
def modifyWhiteboardAccess(wbId: String, multiUser: Boolean) { def modifyWhiteboardAccess(wbId: String, multiUser: Array[String]) {
val wb = getWhiteboard(wbId) val wb = getWhiteboard(wbId)
val newWb = wb.copy(multiUser = multiUser, changedModeOn = System.currentTimeMillis()) val newWb = wb.copy(multiUser = multiUser, oldMultiUser = wb.multiUser, changedModeOn = System.currentTimeMillis())
saveWhiteboard(newWb) saveWhiteboard(newWb)
} }
def getWhiteboardAccess(wbId: String): Boolean = getWhiteboard(wbId).multiUser def getWhiteboardAccess(wbId: String): Array[String] = getWhiteboard(wbId).multiUser
def hasWhiteboardAccess(wbId: String, userId: String): Boolean = {
val wb = getWhiteboard(wbId)
wb.multiUser.contains(userId) || {
val lastChange = System.currentTimeMillis() - wb.changedModeOn
wb.oldMultiUser.contains(userId) && lastChange < 5000
}
}
def getChangedModeOn(wbId: String): Long = getWhiteboard(wbId).changedModeOn def getChangedModeOn(wbId: String): Long = getWhiteboard(wbId).changedModeOn

View File

@ -21,7 +21,7 @@ trait ClearWhiteboardPubMsgHdlr extends RightsManagementTrait {
bus.outGW.send(msgEvent) bus.outGW.send(msgEvent)
} }
if (filterWhiteboardMessage(msg.body.whiteboardId, liveMeeting) && permissionFailed(PermissionCheck.GUEST_LEVEL, PermissionCheck.PRESENTER_LEVEL, liveMeeting.users2x, msg.header.userId)) { if (filterWhiteboardMessage(msg.body.whiteboardId, msg.header.userId, liveMeeting) && permissionFailed(PermissionCheck.GUEST_LEVEL, PermissionCheck.PRESENTER_LEVEL, liveMeeting.users2x, msg.header.userId)) {
val meetingId = liveMeeting.props.meetingProp.intId val meetingId = liveMeeting.props.meetingProp.intId
val reason = "No permission to clear the whiteboard." val reason = "No permission to clear the whiteboard."
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, bus.outGW, liveMeeting) PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, bus.outGW, liveMeeting)

View File

@ -9,7 +9,7 @@ trait GetWhiteboardAnnotationsReqMsgHdlr {
def handle(msg: GetWhiteboardAnnotationsReqMsg, liveMeeting: LiveMeeting, bus: MessageBus): Unit = { def handle(msg: GetWhiteboardAnnotationsReqMsg, liveMeeting: LiveMeeting, bus: MessageBus): Unit = {
def broadcastEvent(msg: GetWhiteboardAnnotationsReqMsg, history: Array[AnnotationVO], multiUser: Boolean): Unit = { def broadcastEvent(msg: GetWhiteboardAnnotationsReqMsg, history: Array[AnnotationVO], multiUser: Array[String]): Unit = {
val routing = Routing.addMsgToHtml5InstanceIdRouting(liveMeeting.props.meetingProp.intId, liveMeeting.props.systemProps.html5InstanceId.toString) val routing = Routing.addMsgToHtml5InstanceIdRouting(liveMeeting.props.meetingProp.intId, liveMeeting.props.systemProps.html5InstanceId.toString)
val envelope = BbbCoreEnvelope(GetWhiteboardAnnotationsRespMsg.NAME, routing) val envelope = BbbCoreEnvelope(GetWhiteboardAnnotationsRespMsg.NAME, routing)

View File

@ -21,7 +21,7 @@ trait ModifyWhiteboardAccessPubMsgHdlr extends RightsManagementTrait {
bus.outGW.send(msgEvent) bus.outGW.send(msgEvent)
} }
if (filterWhiteboardMessage(msg.body.whiteboardId, liveMeeting) && permissionFailed(PermissionCheck.GUEST_LEVEL, PermissionCheck.PRESENTER_LEVEL, liveMeeting.users2x, msg.header.userId)) { if (filterWhiteboardMessage(msg.body.whiteboardId, msg.header.userId, liveMeeting) && permissionFailed(PermissionCheck.GUEST_LEVEL, PermissionCheck.PRESENTER_LEVEL, liveMeeting.users2x, msg.header.userId)) {
val meetingId = liveMeeting.props.meetingProp.intId val meetingId = liveMeeting.props.meetingProp.intId
val reason = "No permission to modify access to the whiteboard." val reason = "No permission to modify access to the whiteboard."
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, bus.outGW, liveMeeting) PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, bus.outGW, liveMeeting)

View File

@ -21,7 +21,7 @@ trait SendCursorPositionPubMsgHdlr extends RightsManagementTrait {
bus.outGW.send(msgEvent) bus.outGW.send(msgEvent)
} }
if (filterWhiteboardMessage(msg.body.whiteboardId, liveMeeting) && permissionFailed(PermissionCheck.GUEST_LEVEL, PermissionCheck.PRESENTER_LEVEL, liveMeeting.users2x, msg.header.userId)) { if (filterWhiteboardMessage(msg.body.whiteboardId, msg.header.userId, liveMeeting) && permissionFailed(PermissionCheck.GUEST_LEVEL, PermissionCheck.PRESENTER_LEVEL, liveMeeting.users2x, msg.header.userId)) {
val meetingId = liveMeeting.props.meetingProp.intId val meetingId = liveMeeting.props.meetingProp.intId
val reason = "No permission to send your cursor position." val reason = "No permission to send your cursor position."
// Just drop messages as these might be delayed messages from multi-user whiteboard. Don't want to // Just drop messages as these might be delayed messages from multi-user whiteboard. Don't want to

View File

@ -71,7 +71,7 @@ trait SendWhiteboardAnnotationPubMsgHdlr extends RightsManagementTrait {
WhiteboardKeyUtil.DRAW_UPDATE_STATUS == annotation.status) WhiteboardKeyUtil.DRAW_UPDATE_STATUS == annotation.status)
} }
if (!excludedWbMsg(msg.body.annotation) && filterWhiteboardMessage(msg.body.annotation.wbId, liveMeeting) && permissionFailed( if (!excludedWbMsg(msg.body.annotation) && filterWhiteboardMessage(msg.body.annotation.wbId, msg.header.userId, liveMeeting) && permissionFailed(
PermissionCheck.GUEST_LEVEL, PermissionCheck.GUEST_LEVEL,
PermissionCheck.PRESENTER_LEVEL, liveMeeting.users2x, msg.header.userId PermissionCheck.PRESENTER_LEVEL, liveMeeting.users2x, msg.header.userId
)) { )) {

View File

@ -21,7 +21,7 @@ trait UndoWhiteboardPubMsgHdlr extends RightsManagementTrait {
bus.outGW.send(msgEvent) bus.outGW.send(msgEvent)
} }
if (filterWhiteboardMessage(msg.body.whiteboardId, liveMeeting) && permissionFailed(PermissionCheck.GUEST_LEVEL, PermissionCheck.PRESENTER_LEVEL, liveMeeting.users2x, msg.header.userId)) { if (filterWhiteboardMessage(msg.body.whiteboardId, msg.header.userId, liveMeeting) && permissionFailed(PermissionCheck.GUEST_LEVEL, PermissionCheck.PRESENTER_LEVEL, liveMeeting.users2x, msg.header.userId)) {
val meetingId = liveMeeting.props.meetingProp.intId val meetingId = liveMeeting.props.meetingProp.intId
val reason = "No permission to undo an annotation." val reason = "No permission to undo an annotation."
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, bus.outGW, liveMeeting) PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, bus.outGW, liveMeeting)

View File

@ -5,8 +5,16 @@ import akka.event.Logging
import org.bigbluebutton.core.running.LiveMeeting import org.bigbluebutton.core.running.LiveMeeting
import org.bigbluebutton.common2.msgs.AnnotationVO import org.bigbluebutton.common2.msgs.AnnotationVO
import org.bigbluebutton.core.apps.WhiteboardKeyUtil import org.bigbluebutton.core.apps.WhiteboardKeyUtil
import scala.collection.immutable.{ Map, List }
case class Whiteboard(id: String, multiUser: Boolean, changedModeOn: Long, annotationCount: Int, annotationsMap: scala.collection.immutable.Map[String, scala.collection.immutable.List[AnnotationVO]]) case class Whiteboard(
id: String,
multiUser: Array[String],
oldMultiUser: Array[String],
changedModeOn: Long,
annotationCount: Int,
annotationsMap: Map[String, List[AnnotationVO]]
)
class WhiteboardApp2x(implicit val context: ActorContext) class WhiteboardApp2x(implicit val context: ActorContext)
extends SendCursorPositionPubMsgHdlr extends SendCursorPositionPubMsgHdlr
@ -56,18 +64,18 @@ class WhiteboardApp2x(implicit val context: ActorContext)
liveMeeting.wbModel.undoWhiteboard(whiteboardId, requesterId) liveMeeting.wbModel.undoWhiteboard(whiteboardId, requesterId)
} }
def getWhiteboardAccess(whiteboardId: String, liveMeeting: LiveMeeting): Boolean = { def getWhiteboardAccess(whiteboardId: String, liveMeeting: LiveMeeting): Array[String] = {
liveMeeting.wbModel.getWhiteboardAccess(whiteboardId) liveMeeting.wbModel.getWhiteboardAccess(whiteboardId)
} }
def modifyWhiteboardAccess(whiteboardId: String, multiUser: Boolean, liveMeeting: LiveMeeting) { def modifyWhiteboardAccess(whiteboardId: String, multiUser: Array[String], liveMeeting: LiveMeeting) {
liveMeeting.wbModel.modifyWhiteboardAccess(whiteboardId, multiUser) liveMeeting.wbModel.modifyWhiteboardAccess(whiteboardId, multiUser)
} }
def filterWhiteboardMessage(whiteboardId: String, liveMeeting: LiveMeeting): Boolean = { def filterWhiteboardMessage(whiteboardId: String, userId: String, liveMeeting: LiveMeeting): Boolean = {
// Need to check if the wb mode change from multi-user to single-user. Give 5sec allowance to // Need to check if the wb mode change from multi-user to single-user. Give 5sec allowance to
// allow delayed messages to be handled as clients may have been sending messages while the wb // allow delayed messages to be handled as clients may have been sending messages while the wb
// mode was changed. (ralam nov 22, 2017) // mode was changed. (ralam nov 22, 2017)
if (!liveMeeting.wbModel.getWhiteboardAccess(whiteboardId) && liveMeeting.wbModel.getChangedModeOn(whiteboardId) > 5000) true else false !liveMeeting.wbModel.hasWhiteboardAccess(whiteboardId, userId)
} }
} }

View File

@ -95,8 +95,3 @@ recording {
# set zero to disable chapter break # set zero to disable chapter break
chapterBreakLengthInMinutes = 0 chapterBreakLengthInMinutes = 0
} }
whiteboard {
multiUserDefault = false
}

View File

@ -18,7 +18,7 @@ case class GetWhiteboardAnnotationsReqMsgBody(whiteboardId: String)
object ModifyWhiteboardAccessPubMsg { val NAME = "ModifyWhiteboardAccessPubMsg" } object ModifyWhiteboardAccessPubMsg { val NAME = "ModifyWhiteboardAccessPubMsg" }
case class ModifyWhiteboardAccessPubMsg(header: BbbClientMsgHeader, body: ModifyWhiteboardAccessPubMsgBody) extends StandardMsg case class ModifyWhiteboardAccessPubMsg(header: BbbClientMsgHeader, body: ModifyWhiteboardAccessPubMsgBody) extends StandardMsg
case class ModifyWhiteboardAccessPubMsgBody(whiteboardId: String, multiUser: Boolean) case class ModifyWhiteboardAccessPubMsgBody(whiteboardId: String, multiUser: Array[String])
object SendCursorPositionPubMsg { val NAME = "SendCursorPositionPubMsg" } object SendCursorPositionPubMsg { val NAME = "SendCursorPositionPubMsg" }
case class SendCursorPositionPubMsg(header: BbbClientMsgHeader, body: SendCursorPositionPubMsgBody) extends StandardMsg case class SendCursorPositionPubMsg(header: BbbClientMsgHeader, body: SendCursorPositionPubMsgBody) extends StandardMsg
@ -48,11 +48,11 @@ case class ClearWhiteboardEvtMsgBody(whiteboardId: String, userId: String, fullC
object GetWhiteboardAnnotationsRespMsg { val NAME = "GetWhiteboardAnnotationsRespMsg" } object GetWhiteboardAnnotationsRespMsg { val NAME = "GetWhiteboardAnnotationsRespMsg" }
case class GetWhiteboardAnnotationsRespMsg(header: BbbClientMsgHeader, body: GetWhiteboardAnnotationsRespMsgBody) extends BbbCoreMsg case class GetWhiteboardAnnotationsRespMsg(header: BbbClientMsgHeader, body: GetWhiteboardAnnotationsRespMsgBody) extends BbbCoreMsg
case class GetWhiteboardAnnotationsRespMsgBody(whiteboardId: String, annotations: Array[AnnotationVO], multiUser: Boolean) case class GetWhiteboardAnnotationsRespMsgBody(whiteboardId: String, annotations: Array[AnnotationVO], multiUser: Array[String])
object ModifyWhiteboardAccessEvtMsg { val NAME = "ModifyWhiteboardAccessEvtMsg" } object ModifyWhiteboardAccessEvtMsg { val NAME = "ModifyWhiteboardAccessEvtMsg" }
case class ModifyWhiteboardAccessEvtMsg(header: BbbClientMsgHeader, body: ModifyWhiteboardAccessEvtMsgBody) extends BbbCoreMsg case class ModifyWhiteboardAccessEvtMsg(header: BbbClientMsgHeader, body: ModifyWhiteboardAccessEvtMsgBody) extends BbbCoreMsg
case class ModifyWhiteboardAccessEvtMsgBody(whiteboardId: String, multiUser: Boolean) case class ModifyWhiteboardAccessEvtMsgBody(whiteboardId: String, multiUser: Array[String])
object SendCursorPositionEvtMsg { val NAME = "SendCursorPositionEvtMsg" } object SendCursorPositionEvtMsg { val NAME = "SendCursorPositionEvtMsg" }
case class SendCursorPositionEvtMsg(header: BbbClientMsgHeader, body: SendCursorPositionEvtMsgBody) extends BbbCoreMsg case class SendCursorPositionEvtMsg(header: BbbClientMsgHeader, body: SendCursorPositionEvtMsgBody) extends BbbCoreMsg

View File

@ -15,7 +15,7 @@ export default function handleWhiteboardAnnotations({ header, body }, meetingId)
check(annotations, Array); check(annotations, Array);
check(whiteboardId, String); check(whiteboardId, String);
check(multiUser, Boolean); check(multiUser, Array);
clearAnnotations(meetingId, whiteboardId); clearAnnotations(meetingId, whiteboardId);

View File

@ -4,7 +4,7 @@ import modifyWhiteboardAccess from '../modifiers/modifyWhiteboardAccess';
export default function handleModifyWhiteboardAccess({ body }, meetingId) { export default function handleModifyWhiteboardAccess({ body }, meetingId) {
const { multiUser, whiteboardId } = body; const { multiUser, whiteboardId } = body;
check(multiUser, Boolean); check(multiUser, Array);
check(whiteboardId, String); check(whiteboardId, String);
check(meetingId, String); check(meetingId, String);

View File

@ -0,0 +1,31 @@
import Users from '/imports/api/users';
import WhiteboardMultiUser from '/imports/api/whiteboard-multi-user/';
const getMultiUser = (meetingId, whiteboardId) => {
const data = WhiteboardMultiUser.findOne(
{
meetingId,
whiteboardId,
}, { fields: { multiUser: 1 } },
);
if (!data || !data.multiUser || !Array.isArray(data.multiUser)) return [];
return data.multiUser;
};
const getUsers = (meetingId) => {
const data = Users.find(
{ meetingId },
{ fields: { userId: 1 } },
).fetch();
if (!data) return [];
return data.map(user => user.userId);
};
export {
getMultiUser,
getUsers,
};

View File

@ -1,6 +1,12 @@
import { Meteor } from 'meteor/meteor'; import { Meteor } from 'meteor/meteor';
import changeWhiteboardAccess from './methods/changeWhiteboardAccess'; import addGlobalAccess from './methods/addGlobalAccess';
import addIndividualAccess from './methods/addIndividualAccess';
import removeGlobalAccess from './methods/removeGlobalAccess';
import removeIndividualAccess from './methods/removeIndividualAccess';
Meteor.methods({ Meteor.methods({
changeWhiteboardAccess, addGlobalAccess,
addIndividualAccess,
removeGlobalAccess,
removeIndividualAccess,
}); });

View File

@ -0,0 +1,27 @@
import RedisPubSub from '/imports/startup/server/redis';
import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
import { getUsers } from '/imports/api/whiteboard-multi-user/server/helpers';
import { extractCredentials } from '/imports/api/common/server/helpers';
export default function addGlobalAccess(whiteboardId) {
const REDIS_CONFIG = Meteor.settings.private.redis;
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
const EVENT_NAME = 'ModifyWhiteboardAccessPubMsg';
check(whiteboardId, String);
const { meetingId, requesterUserId } = extractCredentials(this.userId);
check(meetingId, String);
check(requesterUserId, String);
const multiUser = getUsers(meetingId);
const payload = {
multiUser,
whiteboardId,
};
return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload);
}

View File

@ -0,0 +1,32 @@
import RedisPubSub from '/imports/startup/server/redis';
import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
import { getMultiUser } from '/imports/api/whiteboard-multi-user/server/helpers';
import { extractCredentials } from '/imports/api/common/server/helpers';
export default function addIndividualAccess(whiteboardId, userId) {
const REDIS_CONFIG = Meteor.settings.private.redis;
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
const EVENT_NAME = 'ModifyWhiteboardAccessPubMsg';
check(whiteboardId, String);
check(userId, String);
const { meetingId, requesterUserId } = extractCredentials(this.userId);
check(meetingId, String);
check(requesterUserId, String);
const multiUser = getMultiUser(meetingId, whiteboardId);
if (!multiUser.includes(userId)) {
multiUser.push(userId);
const payload = {
multiUser,
whiteboardId,
};
return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload);
}
}

View File

@ -3,20 +3,20 @@ import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check'; import { check } from 'meteor/check';
import { extractCredentials } from '/imports/api/common/server/helpers'; import { extractCredentials } from '/imports/api/common/server/helpers';
export default function changeWhiteboardAccess(multiUser, whiteboardId) { export default function removeGlobalAccess(whiteboardId) {
const REDIS_CONFIG = Meteor.settings.private.redis; const REDIS_CONFIG = Meteor.settings.private.redis;
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps; const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
const EVENT_NAME = 'ModifyWhiteboardAccessPubMsg'; const EVENT_NAME = 'ModifyWhiteboardAccessPubMsg';
check(whiteboardId, String);
const { meetingId, requesterUserId } = extractCredentials(this.userId); const { meetingId, requesterUserId } = extractCredentials(this.userId);
check(meetingId, String); check(meetingId, String);
check(requesterUserId, String); check(requesterUserId, String);
check(multiUser, Boolean);
check(whiteboardId, String);
const payload = { const payload = {
multiUser, multiUser: [],
whiteboardId, whiteboardId,
}; };

View File

@ -0,0 +1,30 @@
import RedisPubSub from '/imports/startup/server/redis';
import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
import { getMultiUser } from '/imports/api/whiteboard-multi-user/server/helpers';
import { extractCredentials } from '/imports/api/common/server/helpers';
export default function removeIndividualAccess(whiteboardId, userId) {
const REDIS_CONFIG = Meteor.settings.private.redis;
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
const EVENT_NAME = 'ModifyWhiteboardAccessPubMsg';
check(whiteboardId, String);
check(userId, String);
const { meetingId, requesterUserId } = extractCredentials(this.userId);
check(meetingId, String);
check(requesterUserId, String);
const multiUser = getMultiUser(meetingId, whiteboardId);
if (multiUser.includes(userId)) {
const payload = {
multiUser: multiUser.filter(id => id !== userId),
whiteboardId,
};
return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload);
}
}

View File

@ -5,7 +5,7 @@ import WhiteboardMultiUser from '/imports/api/whiteboard-multi-user/';
export default function modifyWhiteboardAccess(meetingId, whiteboardId, multiUser) { export default function modifyWhiteboardAccess(meetingId, whiteboardId, multiUser) {
check(meetingId, String); check(meetingId, String);
check(whiteboardId, String); check(whiteboardId, String);
check(multiUser, Boolean); check(multiUser, Array);
const selector = { const selector = {
meetingId, meetingId,

View File

@ -663,12 +663,9 @@ class PresentationArea extends PureComponent {
currentSlide, currentSlide,
podId, podId,
} = this.props; } = this.props;
const { zoom, fitToWidth, isFullscreen } = this.state; const { zoom, fitToWidth, isFullscreen } = this.state;
if (!currentSlide) { if (!currentSlide) return null;
return null;
}
return ( return (
<PresentationToolbarContainer <PresentationToolbarContainer

View File

@ -10,6 +10,7 @@ import Auth from '/imports/ui/services/auth';
import Meetings from '/imports/api/meetings'; import Meetings from '/imports/api/meetings';
import Users from '/imports/api/users'; import Users from '/imports/api/users';
import getFromUserSettings from '/imports/ui/services/users-settings'; import getFromUserSettings from '/imports/ui/services/users-settings';
import WhiteboardService from '/imports/ui/components/whiteboard/service';
const ROLE_VIEWER = Meteor.settings.public.user.role_viewer; const ROLE_VIEWER = Meteor.settings.public.user.role_viewer;
@ -73,7 +74,7 @@ export default withTracker(({ podId }) => {
slidePosition, slidePosition,
downloadPresentationUri: PresentationAreaService.downloadPresentationUri(podId), downloadPresentationUri: PresentationAreaService.downloadPresentationUri(podId),
userIsPresenter: PresentationAreaService.isPresenter(podId) && !layoutSwapped, userIsPresenter: PresentationAreaService.isPresenter(podId) && !layoutSwapped,
multiUser: PresentationAreaService.getMultiUserStatus(currentSlide && currentSlide.id) multiUser: WhiteboardService.hasMultiUserAccess(currentSlide && currentSlide.id, Auth.userID)
&& !layoutSwapped, && !layoutSwapped,
presentationIsDownloadable, presentationIsDownloadable,
mountPresentationArea: !!currentSlide, mountPresentationArea: !!currentSlide,

View File

@ -16,7 +16,7 @@ import PropTypes from 'prop-types';
import { withTracker } from 'meteor/react-meteor-data'; import { withTracker } from 'meteor/react-meteor-data';
import CursorWrapperService from './service'; import CursorWrapperService from './service';
import CursorContainer from '../container'; import CursorContainer from '../container';
import WhiteboardService from '/imports/ui/components/whiteboard/service';
const CursorWrapperContainer = ({ presenterCursorId, multiUserCursorIds, ...rest }) => ( const CursorWrapperContainer = ({ presenterCursorId, multiUserCursorIds, ...rest }) => (
<g> <g>
@ -47,7 +47,7 @@ export default withTracker((params) => {
const cursorIds = CursorWrapperService.getCurrentCursorIds(podId, whiteboardId); const cursorIds = CursorWrapperService.getCurrentCursorIds(podId, whiteboardId);
const { presenterCursorId, multiUserCursorIds } = cursorIds; const { presenterCursorId, multiUserCursorIds } = cursorIds;
const isMultiUser = CursorWrapperService.getMultiUserStatus(whiteboardId); const isMultiUser = WhiteboardService.isMultiUserActive(whiteboardId);
return { return {
presenterCursorId, presenterCursorId,

View File

@ -1,15 +1,9 @@
import WhiteboardMultiUser from '/imports/api/whiteboard-multi-user/';
import PresentationPods from '/imports/api/presentation-pods'; import PresentationPods from '/imports/api/presentation-pods';
import Auth from '/imports/ui/services/auth'; import Auth from '/imports/ui/services/auth';
import Cursor from '/imports/ui/components/cursor/service'; import Cursor from '/imports/ui/components/cursor/service';
import WhiteboardService from '/imports/ui/components/whiteboard/service';
import Users from '/imports/api/users'; import Users from '/imports/api/users';
const getMultiUserStatus = (whiteboardId) => {
const data = WhiteboardMultiUser.findOne({ meetingId: Auth.meetingID, whiteboardId });
return data ? data.multiUser : false;
};
const getPresenterCursorId = (whiteboardId, userId) => const getPresenterCursorId = (whiteboardId, userId) =>
Cursor.findOne( Cursor.findOne(
{ {
@ -31,7 +25,7 @@ const getCurrentCursorIds = (podId, whiteboardId) => {
} }
// checking whether multiUser mode is on or off // checking whether multiUser mode is on or off
const isMultiUser = getMultiUserStatus(whiteboardId); const isMultiUser = WhiteboardService.isMultiUserActive(whiteboardId);
// it's a multi-user mode - fetching all the cursors except the presenter's // it's a multi-user mode - fetching all the cursors except the presenter's
if (isMultiUser) { if (isMultiUser) {
@ -60,5 +54,4 @@ const getCurrentCursorIds = (podId, whiteboardId) => {
export default { export default {
getCurrentCursorIds, getCurrentCursorIds,
getMultiUserStatus,
}; };

View File

@ -1,4 +1,3 @@
import WhiteboardMultiUser from '/imports/api/whiteboard-multi-user/';
import PresentationPods from '/imports/api/presentation-pods'; import PresentationPods from '/imports/api/presentation-pods';
import Presentations from '/imports/api/presentations'; import Presentations from '/imports/api/presentations';
import { Slides, SlidePositions } from '/imports/api/slides'; import { Slides, SlidePositions } from '/imports/api/slides';
@ -185,21 +184,12 @@ const isPresenter = (podId) => {
return pod.currentPresenterId === Auth.userID; return pod.currentPresenterId === Auth.userID;
}; };
const getMultiUserStatus = (whiteboardId) => {
const data = WhiteboardMultiUser.findOne({
meetingId: Auth.meetingID,
whiteboardId,
});
return data ? data.multiUser : false;
};
export default { export default {
getCurrentSlide, getCurrentSlide,
getSlidePosition, getSlidePosition,
isPresenter, isPresenter,
isPresentationDownloadable, isPresentationDownloadable,
downloadPresentationUri, downloadPresentationUri,
getMultiUserStatus,
currentSlidHasContent, currentSlidHasContent,
parseCurrentSlideContent, parseCurrentSlideContent,
getCurrentPresentation, getCurrentPresentation,

View File

@ -46,6 +46,7 @@ const UserAvatar = ({
avatar, avatar,
noVoice, noVoice,
className, className,
whiteboardAccess,
}) => ( }) => (
<div <div
@ -54,6 +55,7 @@ const UserAvatar = ({
className={cx(styles.avatar, { className={cx(styles.avatar, {
[styles.moderator]: moderator, [styles.moderator]: moderator,
[styles.presenter]: presenter, [styles.presenter]: presenter,
[styles.whiteboardAccess]: whiteboardAccess && !presenter,
[styles.muted]: muted, [styles.muted]: muted,
[styles.listenOnly]: listenOnly, [styles.listenOnly]: listenOnly,
[styles.voice]: voice, [styles.voice]: voice,

View File

@ -117,6 +117,15 @@
@include presenterIndicator(); @include presenterIndicator();
} }
.whiteboardAccess {
&:before {
content: "\00a0\e925\00a0";
padding: var(--md-padding-y);
border-radius: 50% !important;
}
@include presenterIndicator();
}
.voice { .voice {
&:after { &:after {
content: "\00a0\e931\00a0"; content: "\00a0\e931\00a0";

View File

@ -11,6 +11,7 @@ import _ from 'lodash';
import KEY_CODES from '/imports/utils/keyCodes'; import KEY_CODES from '/imports/utils/keyCodes';
import AudioService from '/imports/ui/components/audio/service'; import AudioService from '/imports/ui/components/audio/service';
import logger from '/imports/startup/client/logger'; import logger from '/imports/startup/client/logger';
import WhiteboardService from '/imports/ui/components/whiteboard/service';
const CHAT_CONFIG = Meteor.settings.public.chat; const CHAT_CONFIG = Meteor.settings.public.chat;
const PUBLIC_GROUP_CHAT_ID = CHAT_CONFIG.public_group_id; const PUBLIC_GROUP_CHAT_ID = CHAT_CONFIG.public_group_id;
@ -43,6 +44,14 @@ export const setModeratorOnlyMessage = msg => Storage.setItem('ModeratorOnlyMess
const getCustomLogoUrl = () => Storage.getItem(CUSTOM_LOGO_URL_KEY); const getCustomLogoUrl = () => Storage.getItem(CUSTOM_LOGO_URL_KEY);
const sortByWhiteboardAccess = (a, b) => {
const _a = a.whiteboardAccess;
const _b = b.whiteboardAccess;
if (!_b && _a) return -1;
if (!_a && _b) return 1;
return 0;
};
const sortUsersByName = (a, b) => { const sortUsersByName = (a, b) => {
const aName = a.name.toLowerCase(); const aName = a.name.toLowerCase();
const bName = b.name.toLowerCase(); const bName = b.name.toLowerCase();
@ -125,6 +134,10 @@ const sortUsers = (a, b) => {
sort = sortUsersByPhoneUser(a, b); sort = sortUsersByPhoneUser(a, b);
} }
if (sort === 0) {
sort = sortByWhiteboardAccess(a, b);
}
if (sort === 0) { if (sort === 0) {
sort = sortUsersByName(a, b); sort = sortUsersByName(a, b);
} }
@ -189,6 +202,30 @@ const userFindSorting = {
userId: 1, userId: 1,
}; };
const addWhiteboardAccess = (users) => {
const whiteboardId = WhiteboardService.getCurrentWhiteboardId();
if (whiteboardId) {
const multiUserWhiteboard = WhiteboardService.getMultiUser(whiteboardId);
return users.map(user => {
const whiteboardAccess = multiUserWhiteboard.includes(user.userId);
return {
...user,
whiteboardAccess,
};
});
}
return users.map(user => {
const whiteboardAccess = false;
return {
...user,
whiteboardAccess,
};
});
};
const getUsers = () => { const getUsers = () => {
let users = Users let users = Users
.find({ .find({
@ -206,7 +243,7 @@ const getUsers = () => {
} }
} }
return users.sort(sortUsers); return addWhiteboardAccess(users).sort(sortUsers);
}; };
const hasBreakoutRoom = () => Breakouts.find({ parentMeetingId: Auth.meetingID }, const hasBreakoutRoom = () => Breakouts.find({ parentMeetingId: Auth.meetingID },
@ -334,7 +371,7 @@ const curatedVoiceUser = (intId) => {
}; };
}; };
const getAvailableActions = (amIModerator, isBreakoutRoom, subjectUser, subjectVoiceUser, usersProp) => { const getAvailableActions = (amIModerator, isBreakoutRoom, subjectUser, subjectVoiceUser, usersProp, amIPresenter) => {
const isDialInUser = isVoiceOnlyUser(subjectUser.userId) || subjectUser.phone_user; const isDialInUser = isVoiceOnlyUser(subjectUser.userId) || subjectUser.phone_user;
const amISubjectUser = isMe(subjectUser.userId); const amISubjectUser = isMe(subjectUser.userId);
const isSubjectUserModerator = subjectUser.role === ROLE_MODERATOR; const isSubjectUserModerator = subjectUser.role === ROLE_MODERATOR;
@ -386,6 +423,9 @@ const getAvailableActions = (amIModerator, isBreakoutRoom, subjectUser, subjectV
&& !isSubjectUserModerator && !isSubjectUserModerator
&& isMeetingLocked(Auth.meetingID); && isMeetingLocked(Auth.meetingID);
const allowedToChangeWhiteboardAccess = amIPresenter
&& !amISubjectUser;
return { return {
allowedToChatPrivately, allowedToChatPrivately,
allowedToMuteAudio, allowedToMuteAudio,
@ -397,6 +437,7 @@ const getAvailableActions = (amIModerator, isBreakoutRoom, subjectUser, subjectV
allowedToDemote, allowedToDemote,
allowedToChangeStatus, allowedToChangeStatus,
allowedToChangeUserLockStatus, allowedToChangeUserLockStatus,
allowedToChangeWhiteboardAccess,
}; };
}; };

View File

@ -18,7 +18,8 @@ import { Session } from 'meteor/session';
import { styles } from './styles'; import { styles } from './styles';
import UserName from '../user-name/component'; import UserName from '../user-name/component';
import UserIcons from '../user-icons/component'; import UserIcons from '../user-icons/component';
import Service from '../../../../service'; import Service from '/imports/ui/components/user-list/service';
import WhiteboardService from '/imports/ui/components/whiteboard/service';
const messages = defineMessages({ const messages = defineMessages({
presenter: { presenter: {
@ -65,6 +66,14 @@ const messages = defineMessages({
id: 'app.userList.menu.makePresenter.label', id: 'app.userList.menu.makePresenter.label',
description: 'label to make another user presenter', description: 'label to make another user presenter',
}, },
giveWhiteboardAccess: {
id: 'app.userList.menu.giveWhiteboardAccess.label',
description: 'label to give user whiteboard access',
},
removeWhiteboardAccess: {
id: 'app.userList.menu.removeWhiteboardAccess.label',
description: 'label to remove user whiteboard access',
},
RemoveUserLabel: { RemoveUserLabel: {
id: 'app.userList.menu.removeUser.label', id: 'app.userList.menu.removeUser.label',
description: 'Forcefully remove this user from the meeting', description: 'Forcefully remove this user from the meeting',
@ -237,8 +246,9 @@ class UserDropdown extends PureComponent {
} = this.props; } = this.props;
const { showNestedOptions } = this.state; const { showNestedOptions } = this.state;
const amIPresenter = currentUser.presenter;
const amIModerator = currentUser.role === ROLE_MODERATOR; const amIModerator = currentUser.role === ROLE_MODERATOR;
const actionPermissions = getAvailableActions(amIModerator, meetingIsBreakout, user, voiceUser, usersProp); const actionPermissions = getAvailableActions(amIModerator, meetingIsBreakout, user, voiceUser, usersProp, amIPresenter);
const actions = []; const actions = [];
const { const {
@ -252,6 +262,7 @@ class UserDropdown extends PureComponent {
allowedToDemote, allowedToDemote,
allowedToChangeStatus, allowedToChangeStatus,
allowedToChangeUserLockStatus, allowedToChangeUserLockStatus,
allowedToChangeWhiteboardAccess,
} = actionPermissions; } = actionPermissions;
const { disablePrivateChat } = lockSettingsProps; const { disablePrivateChat } = lockSettingsProps;
@ -357,6 +368,17 @@ class UserDropdown extends PureComponent {
)); ));
} }
if (allowedToChangeWhiteboardAccess && !user.presenter && isMeteorConnected) {
const label = user.whiteboardAccess ? intl.formatMessage(messages.removeWhiteboardAccess) : intl.formatMessage(messages.giveWhiteboardAccess);
actions.push(this.makeDropdownItem(
'changeWhiteboardAccess',
label,
() => WhiteboardService.changeWhiteboardAccess(user.userId, !user.whiteboardAccess),
'pen_tool',
));
}
if (allowedToSetPresenter && isMeteorConnected) { if (allowedToSetPresenter && isMeteorConnected) {
actions.push(this.makeDropdownItem( actions.push(this.makeDropdownItem(
'setPresenter', 'setPresenter',
@ -535,6 +557,7 @@ class UserDropdown extends PureComponent {
voice={voiceUser.isVoiceUser} voice={voiceUser.isVoiceUser}
noVoice={!voiceUser.isVoiceUser} noVoice={!voiceUser.isVoiceUser}
color={user.color} color={user.color}
whiteboardAccess={user.whiteboardAccess}
emoji={user.emoji !== 'none'} emoji={user.emoji !== 'none'}
avatar={user.avatar} avatar={user.avatar}
> >

View File

@ -2,6 +2,7 @@ import React from 'react';
import { withTracker } from 'meteor/react-meteor-data'; import { withTracker } from 'meteor/react-meteor-data';
import TextShapeService from './service'; import TextShapeService from './service';
import TextDrawComponent from './component'; import TextDrawComponent from './component';
import WhiteboardService from '/imports/ui/components/whiteboard/service';
const TextDrawContainer = props => ( const TextDrawContainer = props => (
<TextDrawComponent {...props} /> <TextDrawComponent {...props} />
@ -10,7 +11,7 @@ const TextDrawContainer = props => (
export default withTracker((params) => { export default withTracker((params) => {
const { whiteboardId } = params; const { whiteboardId } = params;
const isPresenter = TextShapeService.isPresenter(); const isPresenter = TextShapeService.isPresenter();
const isMultiUser = TextShapeService.getMultiUserStatus(whiteboardId); const isMultiUser = WhiteboardService.isMultiUserActive(whiteboardId);
const activeTextShapeId = TextShapeService.activeTextShapeId(); const activeTextShapeId = TextShapeService.activeTextShapeId();
let isActive = false; let isActive = false;

View File

@ -1,7 +1,6 @@
import Storage from '/imports/ui/services/storage/session'; import Storage from '/imports/ui/services/storage/session';
import Users from '/imports/api/users'; import Users from '/imports/api/users';
import Auth from '/imports/ui/services/auth'; import Auth from '/imports/ui/services/auth';
import WhiteboardMultiUser from '/imports/api/whiteboard-multi-user/';
const DRAW_SETTINGS = 'drawSettings'; const DRAW_SETTINGS = 'drawSettings';
@ -26,11 +25,6 @@ const isPresenter = () => {
return currentUser ? currentUser.presenter : false; return currentUser ? currentUser.presenter : false;
}; };
const getMultiUserStatus = (whiteboardId) => {
const data = WhiteboardMultiUser.findOne({ meetingId: Auth.meetingID, whiteboardId });
return data ? data.multiUser : false;
};
const activeTextShapeId = () => { const activeTextShapeId = () => {
const drawSettings = Storage.getItem(DRAW_SETTINGS); const drawSettings = Storage.getItem(DRAW_SETTINGS);
return drawSettings ? drawSettings.textShape.textShapeActiveId : ''; return drawSettings ? drawSettings.textShape.textShapeActiveId : '';
@ -41,5 +35,4 @@ export default {
activeTextShapeId, activeTextShapeId,
isPresenter, isPresenter,
resetTextShapeActiveId, resetTextShapeActiveId,
getMultiUserStatus,
}; };

View File

@ -1,7 +1,8 @@
import Users from '/imports/api/users'; import Users from '/imports/api/users';
import Auth from '/imports/ui/services/auth'; import Auth from '/imports/ui/services/auth';
import WhiteboardMultiUser from '/imports/api/whiteboard-multi-user/'; import WhiteboardMultiUser from '/imports/api/whiteboard-multi-user';
import addAnnotationQuery from '/imports/api/annotations/addAnnotation'; import addAnnotationQuery from '/imports/api/annotations/addAnnotation';
import { Slides } from '/imports/api/slides';
import { makeCall } from '/imports/ui/services/api'; import { makeCall } from '/imports/ui/services/api';
import logger from '/imports/startup/client/logger'; import logger from '/imports/startup/client/logger';
@ -184,9 +185,104 @@ Users.find({ userId: Auth.userID }, { fields: { presenter: 1 } }).observeChanges
}, },
}); });
const getMultiUser = (whiteboardId) => {
const data = WhiteboardMultiUser.findOne(
{
meetingId: Auth.meetingID,
whiteboardId,
}, { fields: { multiUser: 1 } },
);
if (!data || !data.multiUser || !Array.isArray(data.multiUser)) return [];
return data.multiUser;
};
const getMultiUserSize = (whiteboardId) => {
const multiUser = getMultiUser(whiteboardId);
if (multiUser.length === 0) return 0;
// Individual whiteboard access is controlled by an array of userIds.
// When an user leaves the meeting or the presenter role moves from an
// user to another we applying a filter at the whiteboard collection.
// Ideally this should change to something more cohese but this would
// require extra changes at multiple backend modules.
const multiUserSize = Users.find(
{
meetingId: Auth.meetingID,
userId: { $in: multiUser },
presenter: false,
}, { fields: { userId: 1 } },
).fetch();
return multiUserSize.length;
};
const getCurrentWhiteboardId = () => {
const currentSlide = Slides.findOne({
podId: 'DEFAULT_PRESENTATION_POD',
meetingId: Auth.meetingID,
current: true,
}, { fields: { id: 1 } },
);
return currentSlide && currentSlide.id;
}
const isMultiUserActive = (whiteboardId) => {
const multiUser = getMultiUser(whiteboardId);
return multiUser.length !== 0;
};
const hasMultiUserAccess = (whiteboardId, userId) => {
const multiUser = getMultiUser(whiteboardId);
return multiUser.includes(userId);
};
const changeWhiteboardAccess = (userId, access) => {
const whiteboardId = getCurrentWhiteboardId();
if (!whiteboardId) return;
if (access) {
addIndividualAccess(whiteboardId, userId);
} else {
removeIndividualAccess(whiteboardId, userId);
}
};
const addGlobalAccess = (whiteboardId) => {
makeCall('addGlobalAccess', whiteboardId);
};
const addIndividualAccess = (whiteboardId, userId) => {
makeCall('addIndividualAccess', whiteboardId, userId);
};
const removeGlobalAccess = (whiteboardId) => {
makeCall('removeGlobalAccess', whiteboardId);
};
const removeIndividualAccess = (whiteboardId, userId) => {
makeCall('removeIndividualAccess', whiteboardId, userId);
};
export { export {
Annotations, Annotations,
UnsentAnnotations, UnsentAnnotations,
sendAnnotation, sendAnnotation,
clearPreview, clearPreview,
getMultiUser,
getMultiUserSize,
getCurrentWhiteboardId,
isMultiUserActive,
hasMultiUserAccess,
changeWhiteboardAccess,
addGlobalAccess,
addIndividualAccess,
removeGlobalAccess,
removeIndividualAccess,
}; };

View File

@ -71,14 +71,18 @@ class WhiteboardToolbar extends Component {
constructor(props) { constructor(props) {
super(props); super(props);
const { annotations, multiUser, isPresenter } = this.props; const {
annotations,
multiUserSize,
isPresenter,
} = this.props;
let annotationSelected = { let annotationSelected = {
icon: 'hand', icon: 'hand',
value: 'hand', value: 'hand',
}; };
if (multiUser && !isPresenter) { if (multiUserSize !== 0 && !isPresenter) {
annotationSelected = { annotationSelected = {
icon: 'pen_tool', icon: 'pen_tool',
value: 'pencil', value: 'pencil',
@ -132,7 +136,12 @@ class WhiteboardToolbar extends Component {
} }
componentDidMount() { componentDidMount() {
const { actions, multiUser, isPresenter } = this.props; const {
actions,
multiUserSize,
isPresenter,
} = this.props;
const drawSettings = actions.getCurrentDrawSettings(); const drawSettings = actions.getCurrentDrawSettings();
const { const {
annotationSelected, thicknessSelected, colorSelected, fontSizeSelected, annotationSelected, thicknessSelected, colorSelected, fontSizeSelected,
@ -144,7 +153,7 @@ class WhiteboardToolbar extends Component {
// if there are saved drawSettings in the session storage // if there are saved drawSettings in the session storage
// - retrieve them and update toolbar values // - retrieve them and update toolbar values
if (drawSettings) { if (drawSettings) {
if (multiUser && !isPresenter) { if (multiUserSize !== 0 && !isPresenter) {
drawSettings.whiteboardAnnotationTool = 'pencil'; drawSettings.whiteboardAnnotationTool = 'pencil';
this.handleAnnotationChange({ icon: 'pen_tool', value: 'pencil' }); this.handleAnnotationChange({ icon: 'pen_tool', value: 'pencil' });
} }
@ -357,12 +366,16 @@ class WhiteboardToolbar extends Component {
handleSwitchWhiteboardMode() { handleSwitchWhiteboardMode() {
const { const {
multiUser, multiUserSize,
whiteboardId, whiteboardId,
actions, actions,
} = this.props; } = this.props;
actions.changeWhiteboardMode(!multiUser, whiteboardId); if (multiUserSize !== 0) {
actions.removeWhiteboardGlobalAccess(whiteboardId);
} else {
actions.addWhiteboardGlobalAccess(whiteboardId);
}
} }
// changes a current selected annotation both in the state and in the session // changes a current selected annotation both in the state and in the session
@ -758,19 +771,26 @@ class WhiteboardToolbar extends Component {
} }
renderMultiUserItem() { renderMultiUserItem() {
const { intl, multiUser, isMeteorConnected } = this.props; const {
intl,
isMeteorConnected,
multiUserSize,
} = this.props;
return ( return (
<ToolbarMenuItem <span className={styles.multiUserToolItem}>
disabled={!isMeteorConnected} {multiUserSize > 0 && <span className={styles.multiUserTool}>{multiUserSize}</span>}
label={multiUser <ToolbarMenuItem
? intl.formatMessage(intlMessages.toolbarMultiUserOff) disabled={!isMeteorConnected}
: intl.formatMessage(intlMessages.toolbarMultiUserOn) label={multiUserSize > 0
} ? intl.formatMessage(intlMessages.toolbarMultiUserOff)
icon={multiUser ? 'multi_whiteboard' : 'whiteboard'} : intl.formatMessage(intlMessages.toolbarMultiUserOn)
onItemClick={this.handleSwitchWhiteboardMode} }
className={styles.toolbarButton} icon={multiUserSize > 0 ? 'multi_whiteboard' : 'whiteboard'}
/> onItemClick={this.handleSwitchWhiteboardMode}
className={styles.toolbarButton}
/>
</span>
); );
} }
@ -800,9 +820,6 @@ WhiteboardToolbar.defaultProps = {
}; };
WhiteboardToolbar.propTypes = { WhiteboardToolbar.propTypes = {
// defines a current mode of the whiteboard, multi/single user
multiUser: PropTypes.bool.isRequired,
// defines whether a current user is a presenter or not // defines whether a current user is a presenter or not
isPresenter: PropTypes.bool.isRequired, isPresenter: PropTypes.bool.isRequired,

View File

@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import { withTracker } from 'meteor/react-meteor-data'; import { withTracker } from 'meteor/react-meteor-data';
import WhiteboardService from '/imports/ui/components/whiteboard/service';
import WhiteboardToolbarService from './service'; import WhiteboardToolbarService from './service';
import WhiteboardToolbar from './component'; import WhiteboardToolbar from './component';
@ -9,11 +10,13 @@ const WhiteboardToolbarContainer = props => (
export default withTracker((params) => { export default withTracker((params) => {
const { whiteboardId } = params; const { whiteboardId } = params;
const data = { const data = {
actions: { actions: {
undoAnnotation: WhiteboardToolbarService.undoAnnotation, undoAnnotation: WhiteboardToolbarService.undoAnnotation,
clearWhiteboard: WhiteboardToolbarService.clearWhiteboard, clearWhiteboard: WhiteboardToolbarService.clearWhiteboard,
changeWhiteboardMode: WhiteboardToolbarService.changeWhiteboardMode, addWhiteboardGlobalAccess: WhiteboardService.addGlobalAccess,
removeWhiteboardGlobalAccess: WhiteboardService.removeGlobalAccess,
setInitialWhiteboardToolbarValues: WhiteboardToolbarService.setInitialWhiteboardToolbarValues, setInitialWhiteboardToolbarValues: WhiteboardToolbarService.setInitialWhiteboardToolbarValues,
getCurrentDrawSettings: WhiteboardToolbarService.getCurrentDrawSettings, getCurrentDrawSettings: WhiteboardToolbarService.getCurrentDrawSettings,
setFontSize: WhiteboardToolbarService.setFontSize, setFontSize: WhiteboardToolbarService.setFontSize,
@ -23,10 +26,10 @@ export default withTracker((params) => {
setTextShapeObject: WhiteboardToolbarService.setTextShapeObject, setTextShapeObject: WhiteboardToolbarService.setTextShapeObject,
}, },
textShapeActiveId: WhiteboardToolbarService.getTextShapeActiveId(), textShapeActiveId: WhiteboardToolbarService.getTextShapeActiveId(),
multiUser: WhiteboardToolbarService.getMultiUserStatus(whiteboardId),
isPresenter: WhiteboardToolbarService.isPresenter(), isPresenter: WhiteboardToolbarService.isPresenter(),
annotations: WhiteboardToolbarService.filterAnnotationList(), annotations: WhiteboardToolbarService.filterAnnotationList(),
isMeteorConnected: Meteor.status().connected, isMeteorConnected: Meteor.status().connected,
multiUserSize: WhiteboardService.getMultiUserSize(whiteboardId),
}; };
return data; return data;

View File

@ -2,7 +2,6 @@ import { makeCall } from '/imports/ui/services/api';
import Storage from '/imports/ui/services/storage/session'; import Storage from '/imports/ui/services/storage/session';
import Users from '/imports/api/users'; import Users from '/imports/api/users';
import Auth from '/imports/ui/services/auth'; import Auth from '/imports/ui/services/auth';
import WhiteboardMultiUser from '/imports/api/whiteboard-multi-user/';
import getFromUserSettings from '/imports/ui/services/users-settings'; import getFromUserSettings from '/imports/ui/services/users-settings';
const DRAW_SETTINGS = 'drawSettings'; const DRAW_SETTINGS = 'drawSettings';
@ -24,10 +23,6 @@ const clearWhiteboard = (whiteboardId) => {
makeCall('clearWhiteboard', whiteboardId); makeCall('clearWhiteboard', whiteboardId);
}; };
const changeWhiteboardMode = (multiUser, whiteboardId) => {
makeCall('changeWhiteboardAccess', multiUser, whiteboardId);
};
const setInitialWhiteboardToolbarValues = (tool, thickness, color, fontSize, textShape) => { const setInitialWhiteboardToolbarValues = (tool, thickness, color, fontSize, textShape) => {
const _drawSettings = Storage.getItem(DRAW_SETTINGS); const _drawSettings = Storage.getItem(DRAW_SETTINGS);
if (!_drawSettings) { if (!_drawSettings) {
@ -59,11 +54,6 @@ const getTextShapeActiveId = () => {
return drawSettings ? drawSettings.textShape.textShapeActiveId : ''; return drawSettings ? drawSettings.textShape.textShapeActiveId : '';
}; };
const getMultiUserStatus = (whiteboardId) => {
const data = WhiteboardMultiUser.findOne({ meetingId: Auth.meetingID, whiteboardId });
return data ? data.multiUser : false;
};
const isPresenter = () => { const isPresenter = () => {
const currentUser = Users.findOne({ userId: Auth.userID }, { fields: { presenter: 1 } }); const currentUser = Users.findOne({ userId: Auth.userID }, { fields: { presenter: 1 } });
return currentUser ? currentUser.presenter : false; return currentUser ? currentUser.presenter : false;
@ -71,10 +61,11 @@ const isPresenter = () => {
const filterAnnotationList = () => { const filterAnnotationList = () => {
const multiUserPenOnly = getFromUserSettings('bbb_multi_user_pen_only', WHITEBOARD_TOOLBAR.multiUserPenOnly); const multiUserPenOnly = getFromUserSettings('bbb_multi_user_pen_only', WHITEBOARD_TOOLBAR.multiUserPenOnly);
const amIPresenter = isPresenter();
let filteredAnnotationList = WHITEBOARD_TOOLBAR.tools; let filteredAnnotationList = WHITEBOARD_TOOLBAR.tools;
if (!isPresenter() && multiUserPenOnly) { if (!amIPresenter && multiUserPenOnly) {
filteredAnnotationList = [{ filteredAnnotationList = [{
icon: 'pen_tool', icon: 'pen_tool',
value: 'pencil', value: 'pencil',
@ -82,13 +73,13 @@ const filterAnnotationList = () => {
} }
const presenterTools = getFromUserSettings('bbb_presenter_tools', WHITEBOARD_TOOLBAR.presenterTools); const presenterTools = getFromUserSettings('bbb_presenter_tools', WHITEBOARD_TOOLBAR.presenterTools);
if (isPresenter() && Array.isArray(presenterTools)) { if (amIPresenter && Array.isArray(presenterTools)) {
filteredAnnotationList = WHITEBOARD_TOOLBAR.tools.filter(el => filteredAnnotationList = WHITEBOARD_TOOLBAR.tools.filter(el =>
presenterTools.includes(el.value)); presenterTools.includes(el.value));
} }
const multiUserTools = getFromUserSettings('bbb_multi_user_tools', WHITEBOARD_TOOLBAR.multiUserTools); const multiUserTools = getFromUserSettings('bbb_multi_user_tools', WHITEBOARD_TOOLBAR.multiUserTools);
if (!isPresenter() && !multiUserPenOnly && Array.isArray(multiUserTools)) { if (!amIPresenter && !multiUserPenOnly && Array.isArray(multiUserTools)) {
filteredAnnotationList = WHITEBOARD_TOOLBAR.tools.filter(el => filteredAnnotationList = WHITEBOARD_TOOLBAR.tools.filter(el =>
multiUserTools.includes(el.value)); multiUserTools.includes(el.value));
} }
@ -99,7 +90,6 @@ const filterAnnotationList = () => {
export default { export default {
undoAnnotation, undoAnnotation,
clearWhiteboard, clearWhiteboard,
changeWhiteboardMode,
setInitialWhiteboardToolbarValues, setInitialWhiteboardToolbarValues,
getCurrentDrawSettings, getCurrentDrawSettings,
setFontSize, setFontSize,
@ -108,7 +98,6 @@ export default {
setColor, setColor,
setTextShapeObject, setTextShapeObject,
getTextShapeActiveId, getTextShapeActiveId,
getMultiUserStatus,
isPresenter, isPresenter,
filterAnnotationList, filterAnnotationList,
}; };

View File

@ -244,3 +244,26 @@
color: var(--toolbar-list-color); color: var(--toolbar-list-color);
} }
.multiUserTool {
background-color: var(--color-danger);
border-radius: 50%;
width: var(--lg-padding-x);
height: var(--lg-padding-x);
position: absolute;
z-index: 2;
right: 0px;
color: var(--color-white);
display: flex;
justify-content: center;
align-items: center;
box-shadow: 1px 1px var(--border-size-large) var(--color-gray-dark);
font-size: var(--sm-padding-x);
}
.multiUserToolItem {
.toolbarButton {
border-top-right-radius: 0 !important;
border-top-left-radius: 0 !important;
}
}

View File

@ -520,7 +520,6 @@ public:
- triangle - triangle
- rectangle - rectangle
- pencil - pencil
- hand
clientLog: clientLog:
server: server:
enabled: false enabled: false

View File

@ -82,6 +82,8 @@
"app.userlist.menu.removeConfirmation.desc": "Prevent this user from rejoining the session.", "app.userlist.menu.removeConfirmation.desc": "Prevent this user from rejoining the session.",
"app.userList.menu.muteUserAudio.label": "Mute user", "app.userList.menu.muteUserAudio.label": "Mute user",
"app.userList.menu.unmuteUserAudio.label": "Unmute user", "app.userList.menu.unmuteUserAudio.label": "Unmute user",
"app.userList.menu.giveWhiteboardAccess.label" : "Give whiteboard access",
"app.userList.menu.removeWhiteboardAccess.label": "Remove whiteboard access",
"app.userList.userAriaLabel": "{0} {1} {2} Status {3}", "app.userList.userAriaLabel": "{0} {1} {2} Status {3}",
"app.userList.menu.promoteUser.label": "Promote to moderator", "app.userList.menu.promoteUser.label": "Promote to moderator",
"app.userList.menu.demoteUser.label": "Demote to viewer", "app.userList.menu.demoteUser.label": "Demote to viewer",