Merge branch 'develop' into multiple-choice-poll

This commit is contained in:
Anton Georgiev 2021-03-17 16:11:49 -04:00 committed by GitHub
commit b1bbc3fd52
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
41 changed files with 537 additions and 121 deletions

View File

@ -49,7 +49,6 @@ trait SystemConfiguration {
lazy val endMeetingWhenNoMoreAuthedUsers = Try(config.getBoolean("apps.endMeetingWhenNoMoreAuthedUsers")).getOrElse(false)
lazy val endMeetingWhenNoMoreAuthedUsersAfterMinutes = Try(config.getInt("apps.endMeetingWhenNoMoreAuthedUsersAfterMinutes")).getOrElse(2)
lazy val multiUserWhiteboardDefault = Try(config.getBoolean("whiteboard.multiUserDefault")).getOrElse(false)
// Redis server configuration
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 = {
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] = {
@ -184,7 +191,7 @@ class WhiteboardModel extends SystemConfiguration {
if (hasWhiteboard(wbId)) {
val wb = getWhiteboard(wbId)
if (wb.multiUser) {
if (wb.multiUser.contains(userId)) {
if (wb.annotationsMap.contains(userId)) {
val newWb = wb.copy(annotationsMap = wb.annotationsMap - userId)
saveWhiteboard(newWb)
@ -205,7 +212,7 @@ class WhiteboardModel extends SystemConfiguration {
var last: Option[AnnotationVO] = None
val wb = getWhiteboard(wbId)
if (wb.multiUser) {
if (wb.multiUser.contains(userId)) {
val usersAnnotations = getAnnotationsByUserId(wb, userId)
//not empty and head id equals annotation id
@ -234,13 +241,21 @@ class WhiteboardModel extends SystemConfiguration {
wb.copy(annotationsMap = newAnnotationsMap)
}
def modifyWhiteboardAccess(wbId: String, multiUser: Boolean) {
def modifyWhiteboardAccess(wbId: String, multiUser: Array[String]) {
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)
}
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

View File

@ -21,7 +21,7 @@ trait ClearWhiteboardPubMsgHdlr extends RightsManagementTrait {
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 reason = "No permission to clear the whiteboard."
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 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 envelope = BbbCoreEnvelope(GetWhiteboardAnnotationsRespMsg.NAME, routing)

View File

@ -21,7 +21,7 @@ trait ModifyWhiteboardAccessPubMsgHdlr extends RightsManagementTrait {
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 reason = "No permission to modify access to the whiteboard."
PermissionCheck.ejectUserForFailedPermission(meetingId, msg.header.userId, reason, bus.outGW, liveMeeting)

View File

@ -21,7 +21,7 @@ trait SendCursorPositionPubMsgHdlr extends RightsManagementTrait {
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 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

View File

@ -71,7 +71,7 @@ trait SendWhiteboardAnnotationPubMsgHdlr extends RightsManagementTrait {
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.PRESENTER_LEVEL, liveMeeting.users2x, msg.header.userId
)) {

View File

@ -21,7 +21,7 @@ trait UndoWhiteboardPubMsgHdlr extends RightsManagementTrait {
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 reason = "No permission to undo an annotation."
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.common2.msgs.AnnotationVO
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)
extends SendCursorPositionPubMsgHdlr
@ -56,18 +64,18 @@ class WhiteboardApp2x(implicit val context: ActorContext)
liveMeeting.wbModel.undoWhiteboard(whiteboardId, requesterId)
}
def getWhiteboardAccess(whiteboardId: String, liveMeeting: LiveMeeting): Boolean = {
def getWhiteboardAccess(whiteboardId: String, liveMeeting: LiveMeeting): Array[String] = {
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)
}
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
// allow delayed messages to be handled as clients may have been sending messages while the wb
// 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
chapterBreakLengthInMinutes = 0
}
whiteboard {
multiUserDefault = false
}

View File

@ -18,7 +18,7 @@ case class GetWhiteboardAnnotationsReqMsgBody(whiteboardId: String)
object ModifyWhiteboardAccessPubMsg { val NAME = "ModifyWhiteboardAccessPubMsg" }
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" }
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" }
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" }
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" }
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(whiteboardId, String);
check(multiUser, Boolean);
check(multiUser, Array);
clearAnnotations(meetingId, whiteboardId);

View File

@ -4,7 +4,7 @@ import modifyWhiteboardAccess from '../modifiers/modifyWhiteboardAccess';
export default function handleModifyWhiteboardAccess({ body }, meetingId) {
const { multiUser, whiteboardId } = body;
check(multiUser, Boolean);
check(multiUser, Array);
check(whiteboardId, 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 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({
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 { 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 CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
const EVENT_NAME = 'ModifyWhiteboardAccessPubMsg';
check(whiteboardId, String);
const { meetingId, requesterUserId } = extractCredentials(this.userId);
check(meetingId, String);
check(requesterUserId, String);
check(multiUser, Boolean);
check(whiteboardId, String);
const payload = {
multiUser,
multiUser: [],
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) {
check(meetingId, String);
check(whiteboardId, String);
check(multiUser, Boolean);
check(multiUser, Array);
const selector = {
meetingId,

View File

@ -123,8 +123,8 @@ const ChatContainer = (props) => {
const contextChat = usingChatContext?.chats[isPublicChat ? PUBLIC_GROUP_CHAT_KEY : chatID];
const lastTimeWindow = contextChat?.lastTimewindow;
const lastMsg = contextChat && (isPublicChat
? contextChat.preJoinMessages[lastTimeWindow] || contextChat.posJoinMessages[lastTimeWindow]
: contextChat.messageGroups[lastTimeWindow]);
? contextChat?.preJoinMessages[lastTimeWindow] || contextChat?.posJoinMessages[lastTimeWindow]
: contextChat?.messageGroups[lastTimeWindow]);
ChatLogger.debug('ChatContainer::render::chatData',contextChat);
applyPropsToState = () => {
ChatLogger.debug('ChatContainer::applyPropsToState::chatData',lastMsg, stateLastMsg, contextChat?.syncing);

View File

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

View File

@ -10,6 +10,7 @@ import Auth from '/imports/ui/services/auth';
import Meetings from '/imports/api/meetings';
import Users from '/imports/api/users';
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;
@ -73,7 +74,7 @@ export default withTracker(({ podId }) => {
slidePosition,
downloadPresentationUri: PresentationAreaService.downloadPresentationUri(podId),
userIsPresenter: PresentationAreaService.isPresenter(podId) && !layoutSwapped,
multiUser: PresentationAreaService.getMultiUserStatus(currentSlide && currentSlide.id)
multiUser: WhiteboardService.hasMultiUserAccess(currentSlide && currentSlide.id, Auth.userID)
&& !layoutSwapped,
presentationIsDownloadable,
mountPresentationArea: !!currentSlide,

View File

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

View File

@ -1,15 +1,9 @@
import WhiteboardMultiUser from '/imports/api/whiteboard-multi-user/';
import PresentationPods from '/imports/api/presentation-pods';
import Auth from '/imports/ui/services/auth';
import Cursor from '/imports/ui/components/cursor/service';
import WhiteboardService from '/imports/ui/components/whiteboard/service';
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) =>
Cursor.findOne(
{
@ -31,7 +25,7 @@ const getCurrentCursorIds = (podId, whiteboardId) => {
}
// 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
if (isMultiUser) {
@ -60,5 +54,4 @@ const getCurrentCursorIds = (podId, whiteboardId) => {
export default {
getCurrentCursorIds,
getMultiUserStatus,
};

View File

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

View File

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

View File

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

View File

@ -11,6 +11,7 @@ import _ from 'lodash';
import KEY_CODES from '/imports/utils/keyCodes';
import AudioService from '/imports/ui/components/audio/service';
import logger from '/imports/startup/client/logger';
import WhiteboardService from '/imports/ui/components/whiteboard/service';
const CHAT_CONFIG = Meteor.settings.public.chat;
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 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 aName = a.name.toLowerCase();
const bName = b.name.toLowerCase();
@ -125,6 +134,10 @@ const sortUsers = (a, b) => {
sort = sortUsersByPhoneUser(a, b);
}
if (sort === 0) {
sort = sortByWhiteboardAccess(a, b);
}
if (sort === 0) {
sort = sortUsersByName(a, b);
}
@ -189,6 +202,30 @@ const userFindSorting = {
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 = () => {
let users = Users
.find({
@ -206,7 +243,11 @@ const getUsers = () => {
}
}
return users.sort(sortUsers);
return addWhiteboardAccess(users).sort(sortUsers);
};
const getUserCount = () => {
return Users.find({ meetingId: Auth.meetingID }).count();
};
const hasBreakoutRoom = () => Breakouts.find({ parentMeetingId: Auth.meetingID },
@ -250,8 +291,8 @@ const getActiveChats = ({ groupChatsMessages, groupChats, users }) => {
const unreadTimewindows = contextChat.unreadTimeWindows;
for (const unreadTimeWindowId of unreadTimewindows) {
const timeWindow = (isPublicChat
? contextChat.preJoinMessages[unreadTimeWindowId] || contextChat.posJoinMessages[unreadTimeWindowId]
: contextChat.messageGroups[unreadTimeWindowId]);
? contextChat?.preJoinMessages[unreadTimeWindowId] || contextChat?.posJoinMessages[unreadTimeWindowId]
: contextChat?.messageGroups[unreadTimeWindowId]);
unreadMessagesCount += timeWindow.content.length;
}
}
@ -334,7 +375,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 amISubjectUser = isMe(subjectUser.userId);
const isSubjectUserModerator = subjectUser.role === ROLE_MODERATOR;
@ -386,6 +427,9 @@ const getAvailableActions = (amIModerator, isBreakoutRoom, subjectUser, subjectV
&& !isSubjectUserModerator
&& isMeetingLocked(Auth.meetingID);
const allowedToChangeWhiteboardAccess = amIPresenter
&& !amISubjectUser;
return {
allowedToChatPrivately,
allowedToMuteAudio,
@ -397,6 +441,7 @@ const getAvailableActions = (amIModerator, isBreakoutRoom, subjectUser, subjectV
allowedToDemote,
allowedToChangeStatus,
allowedToChangeUserLockStatus,
allowedToChangeWhiteboardAccess,
};
};
@ -652,4 +697,5 @@ export default {
isUserPresenter,
amIPresenter,
getUsersProp,
getUserCount,
};

View File

@ -18,7 +18,8 @@ import { Session } from 'meteor/session';
import { styles } from './styles';
import UserName from '../user-name/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({
presenter: {
@ -65,6 +66,14 @@ const messages = defineMessages({
id: 'app.userList.menu.makePresenter.label',
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: {
id: 'app.userList.menu.removeUser.label',
description: 'Forcefully remove this user from the meeting',
@ -237,8 +246,9 @@ class UserDropdown extends PureComponent {
} = this.props;
const { showNestedOptions } = this.state;
const amIPresenter = currentUser.presenter;
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 {
@ -252,6 +262,7 @@ class UserDropdown extends PureComponent {
allowedToDemote,
allowedToChangeStatus,
allowedToChangeUserLockStatus,
allowedToChangeWhiteboardAccess,
} = actionPermissions;
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) {
actions.push(this.makeDropdownItem(
'setPresenter',
@ -535,6 +557,7 @@ class UserDropdown extends PureComponent {
voice={voiceUser.isVoiceUser}
noVoice={!voiceUser.isVoiceUser}
color={user.color}
whiteboardAccess={user.whiteboardAccess}
emoji={user.emoji !== 'none'}
avatar={user.avatar}
>

View File

@ -31,6 +31,9 @@ const {
desktopPageSizes: DESKTOP_PAGE_SIZES,
mobilePageSizes: MOBILE_PAGE_SIZES,
} = Meteor.settings.public.kurento.pagination;
const PAGINATION_THRESHOLDS_CONF = Meteor.settings.public.kurento.paginationThresholds;
const PAGINATION_THRESHOLDS = PAGINATION_THRESHOLDS_CONF.thresholds.sort((t1, t2) => t1.users - t2.users);
const PAGINATION_THRESHOLDS_ENABLED = PAGINATION_THRESHOLDS_CONF.enabled;
const TOKEN = '_';
const ENABLE_PAGINATION_SESSION_VAR = 'enablePagination';
@ -64,6 +67,7 @@ class VideoService {
isConnected: false,
currentVideoPageIndex: 0,
numberOfPages: 0,
pageSize: 0,
});
this.userParameterProfile = null;
const BROWSER_RESULTS = browser();
@ -288,17 +292,66 @@ class VideoService {
return this.currentVideoPageIndex;
}
getMyPageSize () {
const myRole = this.getMyRole();
const pageSizes = !this.isMobile ? DESKTOP_PAGE_SIZES : MOBILE_PAGE_SIZES;
getPageSizeDictionary () {
// Dynamic page sizes are disabled. Fetch the stock page sizes.
if (!PAGINATION_THRESHOLDS_ENABLED || PAGINATION_THRESHOLDS.length <= 0) {
return !this.isMobile ? DESKTOP_PAGE_SIZES : MOBILE_PAGE_SIZES;
}
// Dynamic page sizes are enabled. Get the user count, isolate the
// matching threshold entry, return the val.
let targetThreshold;
const userCount = UserListService.getUserCount();
const processThreshold = (threshold = {
desktopPageSizes: DESKTOP_PAGE_SIZES,
mobilePageSizes: MOBILE_PAGE_SIZES
}) => {
// We don't demand that all page sizes should be set in pagination profiles.
// That saves us some space because don't necessarily need to scale mobile
// endpoints.
// If eg mobile isn't set, then return the default value.
if (!this.isMobile) {
return threshold.desktopPageSizes || DESKTOP_PAGE_SIZES;
} else {
return threshold.mobilePageSizes || MOBILE_PAGE_SIZES;
}
};
// Short-circuit: no threshold yet, return stock values (processThreshold has a default arg)
if (userCount < PAGINATION_THRESHOLDS[0].users) return processThreshold();
// Reverse search for the threshold where our participant count is directly equal or great
// The PAGINATION_THRESHOLDS config is sorted when imported.
for (let mapIndex = PAGINATION_THRESHOLDS.length - 1; mapIndex >= 0; --mapIndex) {
targetThreshold = PAGINATION_THRESHOLDS[mapIndex];
if (targetThreshold.users <= userCount) {
return processThreshold(targetThreshold);
}
}
}
setPageSize (size) {
if (this.pageSize !== size) {
this.pageSize = size;
}
return this.pageSize;
}
getMyPageSize () {
let size;
const myRole = this.getMyRole();
const pageSizes = this.getPageSizeDictionary();
switch (myRole) {
case ROLE_MODERATOR:
return pageSizes.moderator;
size = pageSizes.moderator;
break;
case ROLE_VIEWER:
default:
return pageSizes.viewer
size = pageSizes.viewer
}
return this.setPageSize(size);
}
getVideoPage (streams, pageSize) {

View File

@ -253,6 +253,7 @@ class VideoListItem extends Component {
}
{voiceUser.muted && !voiceUser.listenOnly ? <Icon className={styles.muted} iconName="unmute_filled" /> : null}
{voiceUser.listenOnly ? <Icon className={styles.voice} iconName="listen" /> : null}
{voiceUser.joined && !voiceUser.muted ? <Icon className={styles.voice} iconName="unmute" /> : null}
</div>
)
}

View File

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

View File

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

View File

@ -1,7 +1,8 @@
import Users from '/imports/api/users';
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 { Slides } from '/imports/api/slides';
import { makeCall } from '/imports/ui/services/api';
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 {
Annotations,
UnsentAnnotations,
sendAnnotation,
clearPreview,
getMultiUser,
getMultiUserSize,
getCurrentWhiteboardId,
isMultiUserActive,
hasMultiUserAccess,
changeWhiteboardAccess,
addGlobalAccess,
addIndividualAccess,
removeGlobalAccess,
removeIndividualAccess,
};

View File

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

View File

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

View File

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

View File

@ -244,3 +244,26 @@
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

@ -286,6 +286,41 @@ public:
mobilePageSizes:
moderator: 2
viewer: 2
paginationThresholds:
enabled: false
thresholds:
- users: 30
desktopPageSizes:
moderator: 25
viewer: 25
- users: 40
desktopPageSizes:
moderator: 20
viewer: 20
- users: 50
desktopPageSizes:
moderator: 16
viewer: 16
- users: 60
desktopPageSizes:
moderator: 14
viewer: 12
- users: 70
desktopPageSizes:
moderator: 12
viewer: 10
- users: 80
desktopPageSizes:
moderator: 10
viewer: 8
- users: 90
desktopPageSizes:
moderator: 8
viewer: 6
- users: 100
desktopPageSizes:
moderator: 6
viewer: 4
syncUsersWithConnectionManager:
enabled: false
syncInterval: 60000
@ -520,7 +555,6 @@ public:
- triangle
- rectangle
- pencil
- hand
clientLog:
server:
enabled: false

View File

@ -82,6 +82,8 @@
"app.userlist.menu.removeConfirmation.desc": "Prevent this user from rejoining the session.",
"app.userList.menu.muteUserAudio.label": "Mute 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.menu.promoteUser.label": "Promote to moderator",
"app.userList.menu.demoteUser.label": "Demote to viewer",