Merge branch 'master' of https://github.com/bigbluebutton/bigbluebutton into upgrade-red5-nov-22-2016-snapshot

This commit is contained in:
Richard Alam 2016-11-22 15:59:55 +00:00
commit faa2e35307
66 changed files with 1066 additions and 753 deletions

View File

@ -0,0 +1,4 @@
import RedisPubSub from '/imports/startup/server/redis';
import handleCursorUpdate from './handlers/cursorUpdate';
RedisPubSub.on('presentation_cursor_updated_message', handleCursorUpdate);

View File

@ -0,0 +1,15 @@
import Logger from '/imports/startup/server/logger';
import { check } from 'meteor/check';
import updateCursor from '../modifiers/updateCursor';
export default function handleCursorUpdate({ payload }) {
const meetingId = payload.meeting_id;
const x = payload.x_percent;
const y = payload.y_percent;
check(meetingId, String);
check(x, Number);
check(y, Number);
return updateCursor(meetingId, x, y);
};

View File

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

View File

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

View File

@ -0,0 +1,10 @@
import Cursor from '/imports/api/cursor';
import Logger from '/imports/startup/server/logger';
export default function clearCursor(meetingId) {
if (meetingId) {
return Cursor.remove({ meetingId }, Logger.info(`Cleared Cursor (${meetingId})`));
}
return Cursor.remove({}, Logger.info('Cleared Cursor (all)'));
};

View File

@ -1,14 +0,0 @@
import Cursor from '/imports/api/cursor';
import { logger } from '/imports/startup/server/logger';
// called on server start and meeting end
export function clearCursorCollection() {
const meetingId = arguments[0];
if (meetingId != null) {
return Cursor.remove({
meetingId: meetingId,
}, () => logger.info(`cleared Cursor Collection (meetingId: ${meetingId})!`));
} else {
return Cursor.remove({}, () => logger.info('cleared Cursor Collection (all meetings)!'));
}
};

View File

@ -1,14 +0,0 @@
import { updateCursorLocation } from './updateCursorLocation';
import { eventEmitter } from '/imports/startup/server';
eventEmitter.on('presentation_cursor_updated_message', function (arg) {
const meetingId = arg.payload.meeting_id;
const cursor = {
x: arg.payload.x_percent,
y: arg.payload.y_percent,
};
// update the location of the cursor on the whiteboard
updateCursorLocation(meetingId, cursor);
return arg.callback();
});

View File

@ -1,17 +1,8 @@
import Cursor from '/imports/api/cursor';
import { logger } from '/imports/startup/server/logger';
import updateCursor from './updateCursor';
export function initializeCursor(meetingId) {
return Cursor.upsert({
meetingId: meetingId,
}, {
meetingId: meetingId,
x: 0,
y: 0,
}, (err, numChanged) => {
if (err) {
return logger.error(`err upserting cursor for ${meetingId}`);
}
export default function initializeCursor(meetingId) {
check(meetingId, String);
});
return updateCursor(meetingId, 0, 0);
};

View File

@ -0,0 +1,37 @@
import Logger from '/imports/startup/server/logger';
import Cursor from '/imports/api/cursor';
export default function updateCursor(meetingId, x = 0, y = 0) {
check(meetingId, String);
check(x, Number);
check(y, Number);
const selector = {
meetingId,
};
const modifier = {
$set: {
meetingId,
x,
y,
},
};
const cb = (err, numChanged) => {
if (err) {
return Logger.error(`Upserting cursor to collection: ${err}`);
}
const { insertedId } = numChanged;
if (insertedId) {
return Logger.info(`Initialized cursor meeting=${meetingId}`);
}
if (numChanged) {
return Logger.info(`Updated cursor meeting=${meetingId}`);
}
};
return Cursor.upsert(selector, modifier, cb);
};

View File

@ -1,19 +0,0 @@
import Cursor from '/imports/api/cursor';
import { logger } from '/imports/startup/server/logger';
export function updateCursorLocation(meetingId, cursorObject) {
return Cursor.upsert({
meetingId: meetingId,
}, {
$set: {
meetingId: meetingId,
x: cursorObject.x,
y: cursorObject.y,
},
}, (err, numChanged) => {
if (err != null) {
return logger.error(`_unsucc update of cursor for ${meetingId} err=${JSON.stringify(err)}`);
}
});
};

View File

@ -1,10 +0,0 @@
import Cursor from '/imports/api/cursor';
import { logger } from '/imports/startup/server/logger';
Meteor.publish('cursor', function (credentials) {
const { meetingId } = credentials;
logger.info(`publishing cursor for ${meetingId}`);
return Cursor.find({
meetingId: meetingId,
});
});

View File

@ -0,0 +1,22 @@
import Cursor from '/imports/api/cursor';
import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
import Logger from '/imports/startup/server/logger';
import { isAllowedTo } from '/imports/startup/server/userPermissions';
Meteor.publish('cursor', (credentials) => {
// TODO: Some publishers have ACL and others dont
// if (!isAllowedTo('@@@', credentials)) {
// this.error(new Meteor.Error(402, "The user was not authorized to subscribe for 'cursor'"));
// }
const { meetingId, requesterUserId, requesterToken } = credentials;
check(meetingId, String);
check(requesterUserId, String);
check(requesterToken, String);
Logger.info(`Publishing Cursor for ${meetingId} ${requesterUserId} ${requesterToken}`);
return Cursor.find({ meetingId });
});

View File

@ -1,7 +1,7 @@
import { check } from 'meteor/check';
import Meetings from '/imports/api/meetings';
import Logger from '/imports/startup/server/logger';
import { initializeCursor } from '/imports/api/cursor/server/modifiers/initializeCursor';
import initializeCursor from '/imports/api/cursor/server/modifiers/initializeCursor';
export default function addMeeting(meeting) {
const APP_CONFIG = Meteor.settings.public.app;

View File

@ -4,10 +4,10 @@ import removeMeeting from './removeMeeting';
import { clearUsersCollection } from '/imports/api/users/server/modifiers/clearUsersCollection';
import clearChats from '/imports/api/chat/server/modifiers/clearChats';
import { clearShapesCollection } from '/imports/api/shapes/server/modifiers/clearShapesCollection';
import clearShapes from '/imports/api/shapes/server/modifiers/clearShapes';
import clearSlides from '/imports/api/slides/server/modifiers/clearSlides';
import clearPolls from '/imports/api/polls/server/modifiers/clearPolls';
import { clearCursorCollection } from '/imports/api/cursor/server/modifiers/clearCursorCollection';
import clearCursor from '/imports/api/cursor/server/modifiers/clearCursor';
import { clearCaptionsCollection }
from '/imports/api/captions/server/modifiers/clearCaptionsCollection';
import clearPresentations from '/imports/api/presentations/server/modifiers/clearPresentations';
@ -16,10 +16,10 @@ export default function clearMeetings() {
return Meetings.remove({}, (err) => {
clearCaptionsCollection();
clearChats();
clearCursorCollection();
clearCursor();
clearPresentations();
clearPolls();
clearShapesCollection();
clearShapes();
clearSlides();
clearUsersCollection();

View File

@ -4,10 +4,10 @@ import Logger from '/imports/startup/server/logger';
import { clearUsersCollection } from '/imports/api/users/server/modifiers/clearUsersCollection';
import clearChats from '/imports/api/chat/server/modifiers/clearChats';
import { clearShapesCollection } from '/imports/api/shapes/server/modifiers/clearShapesCollection';
import clearShapes from '/imports/api/shapes/server/modifiers/clearShapes';
import clearSlides from '/imports/api/slides/server/modifiers/clearSlides';
import clearPolls from '/imports/api/polls/server/modifiers/clearPolls';
import { clearCursorCollection } from '/imports/api/cursor/server/modifiers/clearCursorCollection';
import clearCursor from '/imports/api/cursor/server/modifiers/clearCursor';
import { clearCaptionsCollection }
from '/imports/api/captions/server/modifiers/clearCaptionsCollection';
import clearPresentations from '/imports/api/presentations/server/modifiers/clearPresentations';
@ -27,10 +27,10 @@ export default function removeMeeting(meetingId) {
if (numChanged) {
clearCaptionsCollection(meetingId);
clearChats(meetingId);
clearCursorCollection(meetingId);
clearCursor(meetingId);
clearPresentations(meetingId);
clearPolls(meetingId);
clearShapesCollection(meetingId);
clearShapes(meetingId);
clearSlides(meetingId);
clearUsersCollection(meetingId);

View File

@ -21,7 +21,7 @@ function amIListenOnly() {
// Periodically check the status of the WebRTC call, when a call has been established attempt to
// hangup, retry if a call is in progress, send the leave voice conference message to BBB
function exitAudio(afterExitCall) {
function exitAudio(afterExitCall = () => {}) {
if (!MEDIA_CONFIG.useSIPAudio) {
vertoExitAudio();
return;
@ -36,7 +36,7 @@ function exitAudio(afterExitCall) {
triedHangup = false;
// function to initiate call
const checkToHangupCall = (function (context, afterExitCall) {
const checkToHangupCall = ((context, afterExitCall = () => {}) => {
// if an attempt to hang up the call is made when the current session is not yet finished,
// the request has no effect

View File

@ -1,21 +1,21 @@
import { publish } from '/imports/api/common/server/helpers';
import { isAllowedTo } from '/imports/startup/server/userPermissions';
import { appendMessageHeader } from '/imports/api/common/server/helpers';
import { logger } from '/imports/startup/server/logger';
import Meetings from '/imports/api/meetings';
export default function getStun (credentials) {
const REDIS_CONFIG = Meteor.settings.redis;
const { meetingId, requesterUserId } = credentials;
const eventName = 'send_stun_turn_info_request_message';
let message = {
payload: {
meeting_id: meetingId,
requester_id: requesterUserId,
},
};
message = appendMessageHeader(eventName, message);
return publish(REDIS_CONFIG.channels.fromBBBUsers, message);
};
import { publish } from '/imports/api/common/server/helpers';
import { isAllowedTo } from '/imports/startup/server/userPermissions';
import { appendMessageHeader } from '/imports/api/common/server/helpers';
import { logger } from '/imports/startup/server/logger';
import Meetings from '/imports/api/meetings';
export default function getStun(credentials) {
const REDIS_CONFIG = Meteor.settings.redis;
const { meetingId, requesterUserId } = credentials;
const eventName = 'send_stun_turn_info_request_message';
let message = {
payload: {
meeting_id: meetingId,
requester_id: requesterUserId,
},
};
message = appendMessageHeader(eventName, message);
return publish(REDIS_CONFIG.channels.fromBBBUsers, message);
};

View File

@ -26,11 +26,9 @@ export default function addPresentation(meetingId, presentation) {
const modifier = {
$set: {
meetingId,
presentation: {
id: presentation.id,
name: presentation.name,
current: presentation.current,
},
'presentation.id': presentation.id,
'presentation.name': presentation.name,
'presentation.current': presentation.current,
},
};

View File

@ -10,7 +10,7 @@ export default function removePresentation(meetingId, presentationId) {
const selector = {
meetingId,
presentationId,
'presentation.id': presentationId,
};
const cb = (err, numChanged) => {

View File

@ -0,0 +1,10 @@
import RedisPubSub from '/imports/startup/server/redis';
import handleWhiteboardGetReply from './handlers/whiteboardGetReply';
import handleWhiteboardSend from './handlers/whiteboardSend';
import handleWhiteboardCleared from './handlers/whiteboardCleared';
import handleWhiteboardUndo from './handlers/whiteboardUndo';
RedisPubSub.on('get_whiteboard_shapes_reply', handleWhiteboardGetReply);
RedisPubSub.on('send_whiteboard_shape_message', handleWhiteboardSend);
RedisPubSub.on('whiteboard_cleared_message', handleWhiteboardCleared);
RedisPubSub.on('undo_whiteboard_request', handleWhiteboardUndo);

View File

@ -0,0 +1,14 @@
import Logger from '/imports/startup/server/logger';
import { check } from 'meteor/check';
import clearShapesWhiteboard from '../modifiers/clearShapesWhiteboard';
export default function handleWhiteboardCleared({ payload }) {
const meetingId = payload.meeting_id;
const whiteboardId = payload.whiteboard_id;
check(meetingId, String);
check(whiteboardId, String);
return clearShapesWhiteboard(meetingId, whiteboardId);
};

View File

@ -0,0 +1,25 @@
import Logger from '/imports/startup/server/logger';
import { check } from 'meteor/check';
import { inReplyToHTML5Client } from '/imports/api/common/server/helpers';
import addShape from '../modifiers/addShape';
export default function handleWhiteboardGetReply({ payload }) {
if (!inReplyToHTML5Client({ payload })) {
return;
}
const meetingId = payload.meeting_id;
const shapes = payload.shapes;
check(meetingId, String);
check(shapes, Array);
let shapesAdded = [];
shapes.forEach(shape => {
let whiteboardId = shape.wb_id;
shapesAdded.push(addShape(meetingId, whiteboardId, shape));
});
return shapesAdded;
};

View File

@ -0,0 +1,18 @@
import Logger from '/imports/startup/server/logger';
import { check } from 'meteor/check';
import addShape from '../modifiers/addShape';
export default function handleWhiteboardSend({ payload }) {
const meetingId = payload.meeting_id;
const shape = payload.shape;
check(meetingId, String);
check(shape, Object);
const whiteboardId = shape.wb_id;
check(whiteboardId, String);
return addShape(meetingId, whiteboardId, shape);
};

View File

@ -0,0 +1,16 @@
import Logger from '/imports/startup/server/logger';
import { check } from 'meteor/check';
import removeShape from '../modifiers/removeShape';
export default function handleWhiteboardUndo({ payload }) {
const meetingId = payload.meeting_id;
const whiteboardId = payload.whiteboard_id;
const shapeId = payload.shape_id;
check(meetingId, String);
check(whiteboardId, String);
check(shapeId, String);
return removeShape(meetingId, whiteboardId, shapeId);
};

View File

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

View File

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

View File

@ -0,0 +1,85 @@
import { check } from 'meteor/check';
import Shapes from '/imports/api/shapes';
import Logger from '/imports/startup/server/logger';
const SHAPE_TYPE_TEXT = 'text';
const SHAPE_TYPE_POLL_RESULT = 'poll_result';
export default function addShape(meetingId, whiteboardId, shape) {
check(meetingId, String);
check(whiteboardId, String);
check(shape, Object);
const selector = {
meetingId,
'shape.id': shape.id,
};
let modifier = {
$set: {
meetingId,
whiteboardId,
'shape.id': shape.id,
'shape.wb_id': shape.wb_id,
'shape.shape_type': shape.shape_type,
'shape.status': shape.status,
'shape.shape.type': shape.shape.type,
'shape.shape.status': shape.shape.status,
},
};
const shapeType = shape.shape_type;
switch (shapeType) {
case SHAPE_TYPE_TEXT:
modifier.$set = Object.assign(modifier.$set, {
'shape.shape.textBoxHeight': shape.shape.textBoxHeight,
'shape.shape.fontColor': shape.shape.fontColor,
'shape.shape.dataPoints': shape.shape.dataPoints,
'shape.shape.x': shape.shape.x,
'shape.shape.textBoxWidth': shape.shape.textBoxWidth,
'shape.shape.whiteboardId': shape.shape.whiteboardId,
'shape.shape.fontSize': shape.shape.fontSize,
'shape.shape.id': shape.shape.id,
'shape.shape.y': shape.shape.y,
'shape.shape.calcedFontSize': shape.shape.calcedFontSize,
'shape.shape.text': shape.shape.text,
});
break;
case SHAPE_TYPE_POLL_RESULT:
shape.shape.result = JSON.parse(shape.shape.result);
default:
modifier.$set = Object.assign(modifier.$set, {
'shape.shape.points': shape.shape.points,
'shape.shape.whiteboardId': shape.shape.whiteboardId,
'shape.shape.id': shape.shape.id,
'shape.shape.square': shape.shape.square,
'shape.shape.transparency': shape.shape.transparency,
'shape.shape.thickness': shape.shape.thickness,
'shape.shape.color': shape.shape.color,
'shape.shape.result': shape.shape.result,
'shape.shape.num_respondents': shape.shape.num_respondents,
'shape.shape.num_responders': shape.shape.num_responders,
});
break;
}
const cb = (err, numChanged) => {
if (err) {
return Logger.error(`Adding shape to collection: ${err}`);
}
const { insertedId } = numChanged;
if (insertedId) {
return Logger.info(`Added shape id=${shape.id} whiteboard=${whiteboardId}`);
}
if (numChanged) {
return Logger.info(`Upserted shape id=${shape.id} whiteboard=${whiteboardId}`);
}
};
return Shapes.upsert(selector, modifier, cb);
};

View File

@ -1,106 +0,0 @@
import Shapes from '/imports/api/shapes';
import { logger } from '/imports/startup/server/logger';
export function addShapeToCollection(meetingId, whiteboardId, shapeObject) {
//A separate check for the polling shape before adding the object to the collection
//Meteor stringifies an array of JSONs (...shape.result) in this message
//Parsing the String and reassigning the value
if (shapeObject != null && shapeObject.shape_type === 'poll_result' &&
typeof shapeObject.shape.result === 'string') {
shapeObject.shape.result = JSON.parse(shapeObject.shape.result);
}
if (shapeObject != null && shapeObject.shape_type === 'text') {
logger.info(`we are dealing with a text shape and the event is:${shapeObject.status}`);
const entry = {
meetingId: meetingId,
whiteboardId: whiteboardId,
shape: {
wb_id: shapeObject.wb_id,
shape_type: shapeObject.shape_type,
status: shapeObject.status,
id: shapeObject.id,
shape: {
type: shapeObject.shape.type,
textBoxHeight: shapeObject.shape.textBoxHeight,
fontColor: shapeObject.shape.fontColor,
status: shapeObject.shape.status,
dataPoints: shapeObject.shape.dataPoints,
x: shapeObject.shape.x,
textBoxWidth: shapeObject.shape.textBoxWidth,
whiteboardId: shapeObject.shape.whiteboardId,
fontSize: shapeObject.shape.fontSize,
id: shapeObject.shape.id,
y: shapeObject.shape.y,
calcedFontSize: shapeObject.shape.calcedFontSize,
text: shapeObject.shape.text,
},
},
};
if (shapeObject.status === 'textCreated') {
Shapes.insert(entry);
return logger.info(`${shapeObject.status} adding an initial text shape to the collection`);
} else if (shapeObject.status === 'textEdited' || shapeObject.status === 'textPublished') {
//check if the shape with this id exists in the collection
//this check and 'else' block can be removed once issue #3170 is fixed
let _shape = Shapes.findOne({ 'shape.id': shapeObject.shape.id });
if (_shape != null) {
Shapes.update({
'shape.id': entry.shape.id,
}, {
$set: {
shape: entry.shape,
},
});
return logger.info(`${shapeObject.status} substituting the temp shapes with the newer one`);
} else {
Shapes.insert(entry);
}
}
// TODO: pencil messages currently don't send draw_end and are labeled all as DRAW_START
} else if (shapeObject != null && (shapeObject.status === 'DRAW_START' ||
shapeObject.status === 'DRAW_UPDATE' || shapeObject.status === 'DRAW_END')) {
shape = Shapes.findOne({
'shape.id': shapeObject.shape.id,
});
if (shape != null) {
Shapes.update({
'shape.id': shapeObject.shape.id,
}, {
$set: {
'shape.shape.points': shapeObject.shape.points,
},
});
} else {
const entry = {
meetingId: meetingId,
whiteboardId: whiteboardId,
shape: {
wb_id: shapeObject.wb_id,
shape_type: shapeObject.shape_type,
status: shapeObject.status,
id: shapeObject.id,
shape: {
type: shapeObject.shape.type,
status: shapeObject.shape.status,
points: shapeObject.shape.points,
whiteboardId: shapeObject.shape.whiteboardId,
id: shapeObject.shape.id,
square: shapeObject.shape.square,
transparency: shapeObject.shape.transparency,
thickness: shapeObject.shape.thickness,
color: shapeObject.shape.color,
result: shapeObject.shape.result,
num_respondents: shapeObject.shape.num_respondents,
num_responders: shapeObject.shape.num_responders,
},
},
};
Shapes.insert(entry);
}
}
};

View File

@ -0,0 +1,10 @@
import Shapes from '/imports/api/shapes';
import Logger from '/imports/startup/server/logger';
export default function clearShapes(meetingId) {
if (meetingId) {
return Shapes.remove({ meetingId, }, Logger.info(`Cleared Shapes (${meetingId})`));
}
return Shapes.remove({}, Logger.info('Cleared Shapes (all)'));
};

View File

@ -1,18 +0,0 @@
import Shapes from '/imports/api/shapes';
import { logger } from '/imports/startup/server/logger';
// called on server start and meeting end
export function clearShapesCollection() {
const meetingId = arguments[0];
if (meetingId != null) {
return Shapes.remove({
meetingId: meetingId,
}, () => {
logger.info(`cleared Shapes Collection (meetingId: ${meetingId}!`);
});
} else {
return Shapes.remove({}, () => {
logger.info('cleared Shapes Collection (all meetings)!');
});
}
};

View File

@ -0,0 +1,23 @@
import Shapes from '/imports/api/shapes';
import Logger from '/imports/startup/server/logger';
import { check } from 'meteor/check';
export default function clearShapesWhiteboard(meetingId, whiteboardId) {
check(meetingId, String);
check(whiteboardId, String);
const selector = {
meetingId,
whiteboardId,
};
const cb = (err) => {
if (err) {
return Logger.error(`Removing Shapes from collection: ${err}`);
}
return Logger.info(`Removed Shapes where whiteboard=${whiteboardId}`);
};
return Shapes.remove(selector, cb);
};

View File

@ -1,48 +0,0 @@
import { eventEmitter } from '/imports/startup/server';
import { inReplyToHTML5Client } from '/imports/api/common/server/helpers';
import { addShapeToCollection } from './addShapeToCollection';
import { removeAllShapesFromSlide } from './removeAllShapesFromSlide';
import { removeShapeFromSlide } from './removeShapeFromSlide';
eventEmitter.on('get_whiteboard_shapes_reply', function (arg) {
if (inReplyToHTML5Client(arg)) {
const meetingId = arg.payload.meeting_id;
const shapes = arg.payload.shapes;
const shapesLength = shapes.length;
for (let m = 0; m < shapesLength; m++) {
let shape = shapes[m];
let whiteboardId = shape.wb_id;
addShapeToCollection(meetingId, whiteboardId, shape);
}
}
return arg.callback();
});
eventEmitter.on('send_whiteboard_shape_message', function (arg) {
const payload = arg.payload;
const meetingId = payload.meeting_id;
const shape = payload.shape;
if (!!shape && !!shape.wb_id) {
const whiteboardId = shape.wb_id;
addShapeToCollection(meetingId, whiteboardId, shape);
}
return arg.callback();
});
eventEmitter.on('whiteboard_cleared_message', function (arg) {
const meetingId = arg.payload.meeting_id;
const whiteboardId = arg.payload.whiteboard_id;
removeAllShapesFromSlide(meetingId, whiteboardId);
return arg.callback();
});
eventEmitter.on('undo_whiteboard_request', function (arg) {
const meetingId = arg.payload.meeting_id;
const whiteboardId = arg.payload.whiteboard_id;
const shapeId = arg.payload.shape_id;
removeShapeFromSlide(meetingId, whiteboardId, shapeId);
return arg.callback();
});

View File

@ -1,18 +0,0 @@
import Shapes from '/imports/api/shapes';
import { logger } from '/imports/startup/server/logger';
export function removeAllShapesFromSlide(meetingId, whiteboardId) {
logger.info(`removeAllShapesFromSlide__${whiteboardId}`);
if ((meetingId != null) && (whiteboardId != null) && (Shapes.find({
meetingId: meetingId,
whiteboardId: whiteboardId,
}) != null)) {
return Shapes.remove({
meetingId: meetingId,
whiteboardId: whiteboardId,
}, () => {
logger.info('clearing all shapes from slide');
});
}
};

View File

@ -0,0 +1,27 @@
import { check } from 'meteor/check';
import Shapes from '/imports/api/shapes';
import Logger from '/imports/startup/server/logger';
export default function removeShape(meetingId, whiteboardId, shapeId) {
check(meetingId, String);
check(whiteboardId, String);
check(shapeId, String);
const selector = {
meetingId,
whiteboardId,
'shape.id': shapeId,
};
const cb = (err, numChanged) => {
if (err) {
return Logger.error(`Removing shape from collection: ${err}`);
}
if (numChanged) {
return Logger.info(`Removed shape id=${shapeId} whiteboard=${whiteboardId}`);
}
};
return Shapes.remove(selector, cb);
};

View File

@ -1,22 +0,0 @@
import Shapes from '/imports/api/shapes';
import { logger } from '/imports/startup/server/logger';
export function removeShapeFromSlide(meetingId, whiteboardId, shapeId) {
let shapeToRemove;
if (meetingId != null && whiteboardId != null && shapeId != null) {
shapeToRemove = Shapes.findOne({
meetingId: meetingId,
whiteboardId: whiteboardId,
'shape.id': shapeId,
});
if (shapeToRemove != null) {
Shapes.remove(shapeToRemove._id);
logger.info(`----removed shape[${shapeId}] from ${whiteboardId}`);
return logger.info(`remaining shapes on the slide: ${
Shapes.find({
meetingId: meetingId,
whiteboardId: whiteboardId,
}).count()}`);
}
}
};

View File

@ -1,8 +0,0 @@
import Shapes from '/imports/api/shapes';
Meteor.publish('shapes', function (credentials) {
const { meetingId } = credentials;
return Shapes.find({
meetingId: meetingId,
});
});

View File

@ -0,0 +1,22 @@
import Shapes from '/imports/api/shapes';
import { Meteor } from 'meteor/meteor';
import { check } from 'meteor/check';
import Logger from '/imports/startup/server/logger';
import { isAllowedTo } from '/imports/startup/server/userPermissions';
Meteor.publish('shapes', (credentials) => {
// TODO: Some publishers have ACL and others dont
// if (!isAllowedTo('@@@', credentials)) {
// this.error(new Meteor.Error(402, "The user was not authorized to subscribe for 'shapes'"));
// }
const { meetingId, requesterUserId, requesterToken } = credentials;
check(meetingId, String);
check(requesterUserId, String);
check(requesterToken, String);
Logger.info(`Publishing Shapes for ${meetingId} ${requesterUserId} ${requesterToken}`);
return Shapes.find({ meetingId });
});

View File

@ -3,8 +3,8 @@ import Logger from '/imports/startup/server/logger';
export default function clearSlides(meetingId) {
if (meetingId) {
return Slides.remove({ meetingId: meetingId }, Logger.info(`Cleared Slides (${meetingId})`));
} else {
return Slides.remove({}, Logger.info('Cleared Slides (all)'));
return Slides.remove({ meetingId }, Logger.info(`Cleared Slides (${meetingId})`));
}
return Slides.remove({}, Logger.info('Cleared Slides (all)'));
};

View File

@ -1,6 +1,7 @@
import Slides from '/imports/api/slides';
import Logger from '/imports/startup/server/logger';
import { check } from 'meteor/check';
import clearShapesWhiteboard from '/imports/api/shapes/server/modifiers/clearShapesWhiteboard';
export default function clearSlidesPresentation(meetingId, presentationId) {
check(meetingId, String);
@ -11,12 +12,18 @@ export default function clearSlidesPresentation(meetingId, presentationId) {
presentationId,
};
const cb = (err) => {
const whiteboardIds = Slides.find(selector).map(row => row.slide.id);
const cb = (err, numChanged) => {
if (err) {
return Logger.error(`Removing Slides from collection: ${err}`);
}
return Logger.info(`Removed Slides where presentationId=${presentationId}`);
if (numChanged) {
whiteboardIds.forEach(whiteboardId => clearShapesWhiteboard(meetingId, whiteboardId));
return Logger.info(`Removed Slides where presentationId=${presentationId}`);
}
};
return Slides.remove(selector, cb);

View File

@ -20,7 +20,7 @@ Meteor.methods({
}
};
if (isAllowedTo(action(), meetingId, requesterUserId, requesterToken)) {
if (isAllowedTo(action(), credentials)) {
let message = {
payload: {
user_id: toMuteUserId,

View File

@ -2,7 +2,7 @@ import Users from '/imports/api/users';
import { logger } from '/imports/startup/server/logger';
//update a voiceUser - a helper method
export function updateVoiceUser(meetingId, voiceUserObject, callback) {
export function updateVoiceUser(meetingId, voiceUserObject, callback = () => {}) {
let userObject;
userObject = Users.findOne({
userId: voiceUserObject.web_userid,

View File

@ -34,7 +34,7 @@ export function userJoined(meetingId, user, callback) {
if (userObject != null && userObject.authToken != null) {
getStun({
meetingId: meetingId,
requesterUserId: userId
requesterUserId: userId,
});
Users.update({
@ -131,7 +131,7 @@ export function userJoined(meetingId, user, callback) {
// logger.info "NOTE: got user_joined_message #{user.name} #{user.userid}"
getStun({
meetingId: meetingId,
requesterUserId: userId
requesterUserId: userId,
});
return Users.upsert({

View File

@ -2,7 +2,7 @@ import en from './en.json';
import ptBR from './pt-BR.json';
export default {
'en': en,
en: en,
'en-US': en,
'pt-BR': ptBR,
};

View File

@ -0,0 +1,48 @@
import React from 'react';
import { createContainer } from 'meteor/react-meteor-data';
import Button from '/imports/ui/components/button/component';
import Users from '/imports/api/users/index';
import Auth from '/imports/ui/services/auth/index';
import MuteAudioContainer from '../mute-button/container';
export default class JoinAudio extends React.Component {
renderLeaveButton() {
return (
<span>
<Button
onClick={this.props.close}
label={'Leave Audio'}
color={'danger'}
icon={'audio'}
size={'lg'}
circle={true}
/>
</span>
);
}
render() {
if (this.props.isInAudio) {
return (
<span>
<MuteAudioContainer/>
{this.renderLeaveButton()}
</span>
);
} else if (this.props.isInListenOnly) {
return this.renderLeaveButton();
}
return (
<Button
onClick={this.props.open}
label={'Join Audio'}
color={'primary'}
icon={'audio'}
size={'lg'}
circle={true}
/>
);
}
}

View File

@ -1,57 +1,26 @@
import React from 'react';
import { createContainer } from 'meteor/react-meteor-data';
import Button from '/imports/ui/components/button/component';
import Users from '/imports/api/users/index';
import Auth from '/imports/ui/services/auth/index';
class JoinAudioContainer extends React.Component {
handleClick() {
}
render() {
if (this.props.isInAudio) {
return (
<span>
<Button
onClick={this.handleClick}
label={'Mute'}
color={'primary'}
icon={'audio'}
size={'lg'}
circle={true}
/>
<Button
onClick={this.props.close}
label={'Leave Audio'}
color={'primary'}
icon={'audio'}
size={'lg'}
circle={true}
/>
</span>
)
}
else {
return (
<Button
onClick={this.props.open}
label={'Join Audio'}
color={'primary'}
icon={'audio'}
size={'lg'}
circle={true}
/>
)
}
}
}
export default createContainer((params) => {
const data = {
isInAudio: Users.findOne({userId: Auth.userID}).user.voiceUser.joined,
open: params.open,
close: params.close,
};
return data;
}, JoinAudioContainer);
import React from 'react';
import { createContainer } from 'meteor/react-meteor-data';
import Button from '/imports/ui/components/button/component';
import Users from '/imports/api/users/index';
import Auth from '/imports/ui/services/auth/index';
import JoinAudio from './component';
class JoinAudioContainer extends React.Component {
render() {
return (
<JoinAudio {...this.props} />
);
}
}
export default createContainer((params) => {
const user = Users.findOne({ userId: Auth.userID }).user;
return {
isInAudio: user.voiceUser.joined,
isInListenOnly: user.listenOnly,
open: params.open,
close: params.close,
};
}, JoinAudioContainer);

View File

@ -10,7 +10,7 @@ import Users from '/imports/api/users/index';
import JoinAudioContainer from './audio-menu/container';
import { exitAudio } from '/imports/api/phone';
const openJoinAudio = () => {showModal(<Audio />)};
const openJoinAudio = () => showModal(<Audio />);
export default class ActionsBar extends Component {
constructor(props) {
@ -29,7 +29,8 @@ export default class ActionsBar extends Component {
<div className={styles.center}>
<JoinAudioContainer
open={openJoinAudio.bind(this)}
close={exitAudio}
close={() => {exitAudio();}}
/>
<Button

View File

@ -0,0 +1,27 @@
import React from 'react';
import Button from '/imports/ui/components/button/component';
export default class MuteAudioComponent extends React.Component {
render() {
const { isMuted, muteUser, unmuteUser } = this.props;
let onClick = muteUser;
let label = 'Mute';
if (isMuted) {
onClick = unmuteUser;
label = 'Unmute';
}
return (
<Button
onClick={onClick}
label={label}
color={'primary'}
icon={'audio'}
size={'lg'}
circle={true}
/>
);
}
}

View File

@ -0,0 +1,28 @@
import React from 'react';
import {createContainer} from 'meteor/react-meteor-data';
import {callServer} from '/imports/ui/services/api';
import Button from '/imports/ui/components/button/component';
import Users from '/imports/api/users/index';
import Auth from '/imports/ui/services/auth/index';
import MuteAudioComponent from './component';
class MuteAudioContainer extends React.Component {
render() {
return (
<MuteAudioComponent
isMuted = {this.props.isMuted}
muteUser = {this.props.muteUser}
unmuteUser = {this.props.unmuteUser}
/>
);
}
}
export default createContainer((params) => {
const data = {
isMuted: Users.findOne({ userId: Auth.userID }).user.voiceUser.muted,
muteUser: () => callServer('muteUser', Auth.userID),
unmuteUser: () => callServer('unmuteUser', Auth.userID),
};
return data;
}, MuteAudioContainer);

View File

@ -1,87 +1,86 @@
import React from 'react';
import Button from '/imports/ui/components/button/component';
import { clearModal } from '/imports/ui/components/app/service';
import { joinMicrophone } from '/imports/api/phone';
import classNames from 'classnames';
import ReactDOM from 'react-dom';
import { callServer } from '/imports/ui/services/api';
import styles from '../styles.scss';
export default class AudioSettings extends React.Component {
constructor(props) {
super(props);
this.chooseAudio = this.chooseAudio.bind(this);
this.joinAudio = this.joinAudio.bind(this);
}
handleClick() {
}
chooseAudio() {
this.props.changeMenu(this.props.JOIN_AUDIO);
}
joinAudio() {
clearModal();
joinMicrophone();
}
render() {
return (
<div>
<div className={styles.center}>
<Button className={styles.backBtn}
label={'Back'}
icon={'left-arrow'}
size={'md'}
color={'primary'}
ghost={true}
onClick={this.chooseAudio}
/>
<div>
Choose your audio settings
</div>
</div>
<div className={styles.half}>
<label htmlFor='microphone'>Microphone source</label><br />
<select id='microphone' defaultValue='0'>
<option value='0' disabled>Default</option>
<option value='1' disabled>1</option>
<option value='2' disabled>2</option>
<option value='3' disabled>3</option>
</select><br />
<label htmlFor='speaker'>Speaker source</label><br />
<select id='speaker' defaultValue='0'>
<option value='0' disabled>Default</option>
<option value='1' disabled>1</option>
<option value='2' disabled>2</option>
<option value='3' disabled>3</option>
</select><br />
<Button className={styles.playSound}
label={'Play sound'}
icon={'audio'}
size={'md'}
color={'primary'}
ghost={true}
onClick={this.handleClick}
/><br />
</div>
<div className={styles.half}>
Please note, a dialog will appear in your browser,
requiring you to accept your microphone.
<br />
<img src='resources/images/allow-mic.png' alt='allow microphone image' width='100%'/>
<br />
<Button className={styles.enterBtn}
label={'Enter Session'}
size={'md'}
color={'primary'}
onClick={this.joinAudio}
/>
</div>
</div>
);
}
};
import React from 'react';
import Button from '/imports/ui/components/button/component';
import { clearModal } from '/imports/ui/components/app/service';
import { joinMicrophone } from '/imports/api/phone';
import classNames from 'classnames';
import ReactDOM from 'react-dom';
import { callServer } from '/imports/ui/services/api';
import styles from '../styles.scss';
export default class AudioSettings extends React.Component {
constructor(props) {
super(props);
this.chooseAudio = this.chooseAudio.bind(this);
this.joinAudio = this.joinAudio.bind(this);
}
handleClick() {
}
chooseAudio() {
this.props.changeMenu(this.props.JOIN_AUDIO);
}
joinAudio() {
clearModal();
joinMicrophone();
}
render() {
return (
<div>
<div className={styles.center}>
<Button className={styles.backBtn}
label={'Back'}
icon={'left-arrow'}
size={'md'}
color={'primary'}
ghost={true}
onClick={this.chooseAudio}
/>
<div>
Choose your audio settings
</div>
</div>
<div className={styles.half}>
<label htmlFor='microphone'>Microphone source</label><br />
<select id='microphone' defaultValue='0'>
<option value='0' disabled>Default</option>
<option value='1' disabled>1</option>
<option value='2' disabled>2</option>
<option value='3' disabled>3</option>
</select><br />
<label htmlFor='speaker'>Speaker source</label><br />
<select id='speaker' defaultValue='0'>
<option value='0' disabled>Default</option>
<option value='1' disabled>1</option>
<option value='2' disabled>2</option>
<option value='3' disabled>3</option>
</select><br />
<Button className={styles.playSound}
label={'Play sound'}
icon={'audio'}
size={'md'}
color={'primary'}
ghost={true}
onClick={this.handleClick}
/><br />
</div>
<div className={styles.half}>
Please note, a dialog will appear in your browser,
requiring you to accept your microphone.
<br />
<img src='resources/images/allow-mic.png' alt='allow microphone image' width='100%'/>
<br />
<Button className={styles.enterBtn}
label={'Enter Session'}
size={'md'}
color={'primary'}
onClick={this.joinAudio}
/>
</div>
</div>
);
}
};

View File

@ -1,65 +1,65 @@
import React from 'react';
import Icon from '/imports/ui/components/icon/component';
import Button from '/imports/ui/components/button/component';
import ModalBase from '../modal/base/component';
import { clearModal } from '/imports/ui/components/app/service';
import classNames from 'classnames';
import ReactDOM from 'react-dom';
import styles from './styles.scss';
import JoinAudio from './join-audio/component';
import ListenOnly from './listen-only/component';
import AudioSettings from './audio-settings/component';
export default class Audio extends React.Component {
constructor(props) {
super(props);
this.JOIN_AUDIO = 0;
this.AUDIO_SETTINGS = 1;
this.LISTEN_ONLY = 2;
this.submenus = [];
}
componentWillMount() {
/* activeSubmenu represents the submenu in the submenus array to be displayed to the user,
* initialized to 0
*/
this.setState({ activeSubmenu: 0 });
this.submenus.push({ componentName: JoinAudio, });
this.submenus.push({ componentName: AudioSettings, });
this.submenus.push({ componentName: ListenOnly, });
}
changeMenu(i) {
this.setState({ activeSubmenu: i });
}
createMenu() {
const curr = this.state.activeSubmenu === undefined ? 0 : this.state.activeSubmenu;
let props = {
changeMenu: this.changeMenu.bind(this),
JOIN_AUDIO: this.JOIN_AUDIO,
AUDIO_SETTINGS: this.AUDIO_SETTINGS,
LISTEN_ONLY: this.LISTEN_ONLY,
}
const Submenu = this.submenus[curr].componentName;
return <Submenu {...props}/>;
}
render() {
return (
<ModalBase
isOpen={true}
onHide={null}
import React from 'react';
import Icon from '/imports/ui/components/icon/component';
import Button from '/imports/ui/components/button/component';
import ModalBase from '../modal/base/component';
import { clearModal } from '/imports/ui/components/app/service';
import classNames from 'classnames';
import ReactDOM from 'react-dom';
import styles from './styles.scss';
import JoinAudio from './join-audio/component';
import ListenOnly from './listen-only/component';
import AudioSettings from './audio-settings/component';
export default class Audio extends React.Component {
constructor(props) {
super(props);
this.JOIN_AUDIO = 0;
this.AUDIO_SETTINGS = 1;
this.LISTEN_ONLY = 2;
this.submenus = [];
}
componentWillMount() {
/* activeSubmenu represents the submenu in the submenus array to be displayed to the user,
* initialized to 0
*/
this.setState({ activeSubmenu: 0 });
this.submenus.push({ componentName: JoinAudio, });
this.submenus.push({ componentName: AudioSettings, });
this.submenus.push({ componentName: ListenOnly, });
}
changeMenu(i) {
this.setState({ activeSubmenu: i });
}
createMenu() {
const curr = this.state.activeSubmenu === undefined ? 0 : this.state.activeSubmenu;
let props = {
changeMenu: this.changeMenu.bind(this),
JOIN_AUDIO: this.JOIN_AUDIO,
AUDIO_SETTINGS: this.AUDIO_SETTINGS,
LISTEN_ONLY: this.LISTEN_ONLY,
};
const Submenu = this.submenus[curr].componentName;
return <Submenu {...props}/>;
}
render() {
return (
<ModalBase
isOpen={true}
onHide={null}
onShow={null}
className={styles.inner}>
<div>
{this.createMenu()}
</div>
</ModalBase>
);
}
};
className={styles.inner}>
<div>
{this.createMenu()}
</div>
</ModalBase>
);
}
};

View File

@ -1,67 +1,65 @@
import React from 'react';
import Button from '/imports/ui/components/button/component';
import { clearModal } from '/imports/ui/components/app/service';
import classNames from 'classnames';
import ReactDOM from 'react-dom';
import styles from '../styles.scss';
export default class JoinAudio extends React.Component {
constructor(props) {
super(props);
this.handleClose = this.handleClose.bind(this);
this.openAudio = this.openAudio.bind(this);
this.openListen = this.openListen.bind(this);
}
handleClose() {
this.setState({ isOpen: false });
clearModal();
}
openAudio() {
this.props.changeMenu(this.props.AUDIO_SETTINGS);
}
openListen() {
this.props.changeMenu(this.props.LISTEN_ONLY);
}
render() {
return (
<div>
<div className={styles.center}>
<Button className={styles.closeBtn}
label={'Close'}
icon={'close'}
size={'lg'}
circle={true}
hideLabel={true}
onClick={this.handleClose}
/>
<div>
How would you like to join the audio?
</div>
</div>
<div className={styles.center}>
<Button className={styles.audioBtn}
label={'Audio'}
icon={'audio'}
circle={true}
size={'jumbo'}
onClick={this.openAudio}
/>
<Button className={styles.audioBtn}
label={'Listen Only'}
icon={'listen'}
circle={true}
size={'jumbo'}
onClick={this.openListen}
/>
</div>
</div>
);
}
};
import React from 'react';
import Button from '/imports/ui/components/button/component';
import { clearModal } from '/imports/ui/components/app/service';
import classNames from 'classnames';
import ReactDOM from 'react-dom';
import styles from '../styles.scss';
export default class JoinAudio extends React.Component {
constructor(props) {
super(props);
this.handleClose = this.handleClose.bind(this);
this.openAudio = this.openAudio.bind(this);
this.openListen = this.openListen.bind(this);
}
handleClose() {
this.setState({ isOpen: false });
clearModal();
}
openAudio() {
this.props.changeMenu(this.props.AUDIO_SETTINGS);
}
openListen() {
this.props.changeMenu(this.props.LISTEN_ONLY);
}
render() {
return (
<div>
<div className={styles.center}>
<Button className={styles.closeBtn}
label={'Close'}
icon={'close'}
size={'lg'}
circle={true}
hideLabel={true}
onClick={this.handleClose}
/>
<div>
How would you like to join the audio?
</div>
</div>
<div className={styles.center}>
<Button className={styles.audioBtn}
label={'Audio'}
icon={'audio'}
circle={true}
size={'jumbo'}
onClick={this.openAudio}
/>
<Button className={styles.audioBtn}
label={'Listen Only'}
icon={'listen'}
circle={true}
size={'jumbo'}
onClick={this.openListen}
/>
</div>
</div>
);
}
};

View File

@ -1,54 +1,53 @@
import React from 'react';
import Button from '/imports/ui/components/button/component';
import { clearModal } from '/imports/ui/components/app/service';
import { joinListenOnly } from '/imports/api/phone';
import classNames from 'classnames';
import ReactDOM from 'react-dom';
import styles from '../styles.scss';
export default class ListenOnly extends React.Component {
constructor(props) {
super(props);
this.chooseAudio = this.chooseAudio.bind(this);
}
chooseAudio() {
this.props.changeMenu(this.props.JOIN_AUDIO);
}
joinListen() {
joinListenOnly();
}
render() {
return (
<div>
<div className={styles.center}>
<Button className={styles.backBtn}
label={'Back'}
icon={'left-arrow'}
size={'md'}
color={'primary'}
ghost={true}
onClick={this.chooseAudio}
/>
<div>
Listen only message
</div>
</div>
<div>
Content goes here<br /><br />
Volume Slider Here
<Button className={styles.enterBtn}
label={'Enter Session'}
size={'md'}
color={'primary'}
onClick={this.joinListen}
/>
</div>
</div>
);
}
};
import React from 'react';
import Button from '/imports/ui/components/button/component';
import { clearModal } from '/imports/ui/components/app/service';
import { joinListenOnly } from '/imports/api/phone';
import classNames from 'classnames';
import ReactDOM from 'react-dom';
import styles from '../styles.scss';
export default class ListenOnly extends React.Component {
constructor(props) {
super(props);
this.chooseAudio = this.chooseAudio.bind(this);
}
chooseAudio() {
this.props.changeMenu(this.props.JOIN_AUDIO);
}
joinListen() {
joinListenOnly();
}
render() {
return (
<div>
<div className={styles.center}>
<Button className={styles.backBtn}
label={'Back'}
icon={'left-arrow'}
size={'md'}
color={'primary'}
ghost={true}
onClick={this.chooseAudio}
/>
<div>
Listen only message
</div>
</div>
<div>
Content goes here<br /><br />
Volume Slider Here
<Button className={styles.enterBtn}
label={'Enter Session'}
size={'md'}
color={'primary'}
onClick={this.joinListen}
/>
</div>
</div>
);
}
};

View File

@ -41,8 +41,9 @@ export default class MessageListItem extends Component {
</div>
<div className={styles.content}>
<div className={styles.meta}>
<div className={styles.name}>
<div className={user.isLogin ? styles.name : styles.logout}>
<span>{user.name}</span>
{!user.isLogin ? <span className={styles.offline}> (offline)</span> : null}
</div>
<time className={styles.time} dateTime={dateTime}>
<FormattedTime value={dateTime}/>

View File

@ -57,6 +57,26 @@
}
}
.logout {
display: flex;
min-width: 0;
font-weight: 600;
color: $color-gray-light;
text-transform: capitalize;
font-style: italic;
> span {
@extend %text-elipsis;
}
}
.offline {
font-weight: 100;
color: $color-gray-light;
text-transform: capitalize;
font-style: italic;
}
.time {
flex-shrink: 0;
flex-grow: 0;

View File

@ -37,6 +37,20 @@ const mapUser = (user) => ({
isListenOnly: user.listenOnly,
isSharingWebcam: user.webcam_stream.length,
isLocked: user.locked,
isLogin: true,
});
const logoutUser = (userID, userName) => ({
id: userID,
name: userName,
emoji: {
status: 'none',
},
isPresenter: false,
isModerator: false,
isCurrent: false,
isVoiceUser: false,
isLogin: false,
});
const mapMessage = (messagePayload) => {
@ -50,7 +64,7 @@ const mapMessage = (messagePayload) => {
};
if (message.chat_type !== SYSTEM_CHAT_TYPE) {
mappedMessage.sender = getUser(message.from_userid);
mappedMessage.sender = getUser(message.from_userid, message.from_username);
}
return mappedMessage;
@ -86,12 +100,12 @@ const reduceMessages = (previous, current, index, array) => {
}
};
const getUser = (userID) => {
const getUser = (userID, userName) => {
const user = Users.findOne({ userId: userID });
if (user) {
return mapUser(user.user);
} else {
return null;
return logoutUser(userID, userName);
}
};

View File

@ -1,62 +1,15 @@
import React from 'react';
import Modal from 'react-modal';
import Icon from '/imports/ui/components/icon/component';
import Button from '/imports/ui/components/button/component';
import BaseMenu from '../base/component';
import styles from '../styles.scss';
export default class UsersMenu extends BaseMenu {
constructor(props) {
super(props);
}
getContent() {
return (
<div className={styles.full} role='presentation'>
<div className={styles.row} role='presentation'>
<label><input className={styles.checkboxOffset} type='checkbox' tabIndex='7'
aria-labelledby='muteALlLabel' aria-describedby='muteAllDesc' />
Mute all except the presenter</label>
</div>
<div id='muteAllLabel' hidden>Mute all</div>
<div id='muteAllDesc' hidden>Mutes all participants except the presenter.</div>
<div className={styles.row} role='presentation'>
<label><input className={styles.checkboxOffset} type="checkbox" tabIndex='8'
aria-labelledby='lockAllLabel' aria-describedby='lockAllDesc' />
Lock all participants</label>
</div>
<div id='lockAllLabel' hidden>Lock all</div>
<div id='lockAllDesc' hidden>Toggles locked status for all participants.</div>
<div className={styles.indentedRow} role='presentation'>
<label><input className={styles.checkboxOffset} type='checkbox' tabIndex='9'
aria-labelledby='webcamLabel' aria-describedby='webcamDesc' />Webcam</label>
</div>
<div id='webcamLabel' hidden>Webcam lock</div>
<div id='webcamDesc' hidden>Disables the webcam for all locked participants.</div>
<div className={styles.indentedRow} role='presentation'>
<label><input className={styles.checkboxOffset} type='checkbox' tabIndex='10'
aria-labelledby='micLabel' aria-describedby='micDesc' />Microphone</label>
</div>
<div id='micLabel' hidden>Microphone lock</div>
<div id='micDesc' hidden>Disables the microphone for all locked participants.</div>
<div className={styles.indentedRow} role='presentation'>
<label><input className={styles.checkboxOffset} type='checkbox' tabIndex='11'
aria-labelledby='pubChatLabel' aria-describedby='pubChatDesc' />Public chat</label>
</div>
<div id='pubChatLabel' hidden>Public chat lock</div>
<div id='pubChatDesc' hidden>Disables public chat for all locked participants.</div>
<div className={styles.indentedRow} role='presentation'>
<label><input className={styles.checkboxOffset} type='checkbox' tabIndex='12'
aria-labelledby='privChatLabel' aria-describedby='privChatDesc' />Private chat</label>
</div>
<div id='privChatLabel' hidden>Private chat lock</div>
<div id='privChatDesc' hidden>Disables private chat for all locked participants.</div>
</div>
);
}
};
import React from 'react';
import BaseMenu from '../base/component';
import ParticipantsMenuContainer from './participants/container';
export default class UsersMenu extends BaseMenu {
constructor(props) {
super(props);
}
render() {
return (
<ParticipantsMenuContainer />
);
}
};

View File

@ -0,0 +1,115 @@
import React from 'react';
import Modal from 'react-modal';
import Icon from '/imports/ui/components/icon/component';
import Button from '/imports/ui/components/button/component';
import styles from '../../styles.scss';
import Service from './service';
const ROLE_MODERATOR = 'moderator';
const LOCK_OPTIONS = {
MUTE_ALL: {
label: 'Mute all except the presenter',
ariaLabelledBy: 'muteALlLabel',
ariaDescribedBy: 'muteAllDesc',
ariaLabel: 'Mute all',
ariaDesc: 'Mutes all participants except the presenter.',
tabIndex: 7,
},
LOCK_ALL: {
label: 'Lock all participants',
ariaLabelledBy: 'lockAllLabel',
ariaDescribedBy: 'lockAllDesc',
ariaLabel: 'Lock all',
ariaDesc: 'Toggles locked status for all participants.',
tabIndex: 8,
},
WEBCAM_LOCK: {
label: 'Webcam',
ariaLabelledBy: 'webcamLabel',
ariaDescribedBy: 'webcamDesc',
ariaLabel: 'Webcam lock',
ariaDesc: 'Disables the webcam for all locked participants.',
tabIndex: 9,
},
MIC_LOCK: {
label: 'Microphone',
ariaLabelledBy: 'micLabel',
ariaDescribedBy: 'micDesc',
ariaLabel: 'Microphone lock',
ariaDesc: 'Disables the microphone for all locked participants.',
tabIndex: 10,
},
PUBLIC_CHAT_LOCK: {
label: 'Public chat',
ariaLabelledBy: 'pubChatLabel',
ariaDescribedBy: 'pubChatDesc',
ariaLabel: 'Public chat lock',
ariaDesc: 'Disables public chat for all locked participants.',
tabIndex: 11,
},
PRIVATE_CHAT_LOCK: {
label: 'Private chat',
ariaLabelledBy: 'privChatLabel',
ariaDescribedBy: 'privChatDesc',
ariaLabel: 'Private chat lock',
ariaDesc: 'Disables private chat for all locked participants.',
tabIndex: 12,
},
};
export default class ParticipantsMenu extends React.Component {
constructor(props) {
super(props);
}
renderLockItem(lockOption) {
return (
<div className={styles.row} role='presentation' key={lockOption.label} >
<label>
<input
className={styles.checkboxOffset}
type="checkbox"
tabIndex={lockOption.tabIndex}
aria-labelledby={lockOption.ariaLabelledBy}
aria-describedby={lockOption.ariaDescribedBy} />
{lockOption.label}
</label>
<div id={lockOption.ariaLabelledBy} hidden>{lockOption.ariaLabel}</div>
<div id={lockOption.ariaDescribedBy} hidden>{lockOption.ariaDesc}</div>
</div>
);
}
renderLockOptions () {
const { isPresenter, role } = this.props;
let roleBasedOptions = [];
if (isPresenter || role === ROLE_MODERATOR) {
roleBasedOptions.push(
this.renderLockItem(LOCK_OPTIONS.LOCK_ALL),
this.renderLockItem(LOCK_OPTIONS.WEBCAM_LOCK),
this.renderLockItem(LOCK_OPTIONS.MIC_LOCK),
this.renderLockItem(LOCK_OPTIONS.PUBLIC_CHAT_LOCK),
this.renderLockItem(LOCK_OPTIONS.PRIVATE_CHAT_LOCK),
);
}
return _.compact([
this.renderLockItem(LOCK_OPTIONS.MUTE_ALL),
...roleBasedOptions,
]);
}
render () {
return (
<div>
{this.renderLockOptions()}
</div>
);
}
};

View File

@ -0,0 +1,24 @@
import React, { Component, PropTypes } from 'react';
import { createContainer } from 'meteor/react-meteor-data';
import ParticipantsMenu from './component';
import Service from './service';
class ParticipantsMenuContainer extends Component {
constructor(props) {
super(props);
}
render() {
return (
<ParticipantsMenu {...this.props}>
{this.props.children}
</ParticipantsMenu>
);
}
}
export default createContainer(() => {
let data = Service.checkUserRoles();
return data;
}, ParticipantsMenuContainer);

View File

@ -0,0 +1,17 @@
import Users from '/imports/api/users';
import AuthSingleton from '/imports/ui/services/auth/index.js';
checkUserRoles = () => {
const user = Users.findOne({
userId: AuthSingleton.getCredentials().requesterUserId,
}).user;
return {
isPresenter: user.presenter,
role: user.role,
};
};
export default {
checkUserRoles,
};

View File

@ -25,6 +25,19 @@ const stringToPastelColour = (str) => {
};
};
const whiteColour = (str) => {
let baseRed = 255;
let baseGreen = 255;
let baseBlue = 255;
return {
r: baseRed,
g: baseGreen,
b: baseBlue,
};
};
// https://www.w3.org/TR/WCAG20/#relativeluminancedef
// http://entropymine.com/imageworsener/srgbformula/
const relativeLuminance = (rgb) => {
@ -99,10 +112,15 @@ const addShadeIfNoContrast = (rgb) => {
return addShadeIfNoContrast(shadeColor(rgb, -25));
};
const getColor = (username) => {
let rgb = stringToPastelColour(username);
rgb = addShadeIfNoContrast(rgb);
return 'rgb(' + rgb.r + ',' + rgb.g + ',' + rgb.b + ')';
const getColor = (username, chkLogin) => {
if (chkLogin) {
let rgb = stringToPastelColour(username);
rgb = addShadeIfNoContrast(rgb);
return 'rgb(' + rgb.r + ',' + rgb.g + ',' + rgb.b + ')';
} else {
let rgb = whiteColour();
return 'rgb(' + rgb.r + ',' + rgb.g + ',' + rgb.b + ')';
}
};
export default getColor;

View File

@ -24,11 +24,12 @@ export default class UserAvatar extends Component {
} = this.props;
let avatarStyles = {
backgroundColor: getColor(user.name),
backgroundColor: getColor(user.name, user.isLogin),
};
return (
<div className={styles.userAvatar} style={avatarStyles}>
<div className={user.isLogin ? styles.userAvatar : styles.userLogout}
style={avatarStyles}>
<span>
{this.renderAvatarContent()}
</span>

View File

@ -29,6 +29,24 @@ $moderator-bg: $color-primary;
text-transform: capitalize;
}
.userLogout {
flex-basis: 2.2rem;
height: 2.2rem;
flex-shrink: 0;
line-height: 2.2rem;
justify-content: center;
position: relative;
display: flex;
flex-flow: column;
font-size: 1.1rem;
text-align: center;
border-radius: 50%;
border: 1px solid $color-gray-lighter;
color: $color-gray-light;
font-style: italic;
text-transform: capitalize;
}
.userStatus {
position: absolute;
background-color: $color-white;

View File

@ -28,6 +28,7 @@ const mapUser = user => ({
isListenOnly: user.listenOnly,
isSharingWebcam: user.webcam_stream.length,
isPhoneUser: user.phone_user,
isLogin: user ? true : false,
});
const mapOpenChats = chat => {

View File

@ -1,11 +1,7 @@
import '/imports/startup/server';
import '/imports/api/chat/server';
import '/imports/api/cursor/server/publications';
import '/imports/api/cursor/server/modifiers/clearCursorCollection';
import '/imports/api/cursor/server/modifiers/initializeCursor';
import '/imports/api/cursor/server/modifiers/updateCursorLocation';
import '/imports/api/cursor/server/modifiers/eventHandlers';
import '/imports/api/cursor/server';
import '/imports/api/deskshare/server/publications';
import '/imports/api/deskshare/server/modifiers/clearDeskshareCollection';
@ -21,12 +17,7 @@ import '/imports/api/polls/server';
import '/imports/api/presentations/server';
import '/imports/api/shapes/server/publications';
import '/imports/api/shapes/server/modifiers/addShapeToCollection';
import '/imports/api/shapes/server/modifiers/clearShapesCollection';
import '/imports/api/shapes/server/modifiers/removeAllShapesFromSlide';
import '/imports/api/shapes/server/modifiers/removeShapeFromSlide';
import '/imports/api/shapes/server/modifiers/eventHandlers';
import '/imports/api/shapes/server';
import '/imports/api/slides/server';

View File

@ -280,7 +280,7 @@ module BigBlueButton
return {} if !info[:format]
info[:video] = info[:streams].find do |stream|
stream[:codec_type] == 'audio'
stream[:codec_type] == 'video'
end
return {} if !info[:video]