Merge branch 'v2.0.x-release' of https://github.com/bigbluebutton/bigbluebutton into reopen-private-chat-bug
This commit is contained in:
commit
3937a01ffd
@ -15,26 +15,33 @@
|
||||
You should have received a copy of the GNU Lesser General Public License along
|
||||
with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
$(document).ready( function () {
|
||||
$(document).ready(function() {
|
||||
if (typeof jQuery !== 'undefined') {
|
||||
(function($) {
|
||||
$('[data-toggle="confirmation"]').confirmation({popout: true});
|
||||
$('#recordings').dataTable({
|
||||
columnDefs: [ {
|
||||
targets: 3,
|
||||
render: $.fn.dataTable.render.moment('X', 'LLL', locale)
|
||||
targets: 4,
|
||||
render: $.fn.dataTable.render.moment('X', 'lll', locale)
|
||||
} ],
|
||||
sPaginationType : "full_numbers",
|
||||
"columns": [
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
{ "width": "90px" },
|
||||
null,
|
||||
null,
|
||||
{ "width": "160px" }
|
||||
{ "width": "40px" },
|
||||
{ "width": "120px" }
|
||||
],
|
||||
"order": [[ 3, "desc" ]]
|
||||
"order": [[ 4, "desc" ]]
|
||||
});
|
||||
$(".glyphicon-eye-open").hover(function() {
|
||||
$(this).toggleClass('glyphicon-eye-open glyphicon-eye-close');
|
||||
});
|
||||
$(".glyphicon-eye-close").hover(function() {
|
||||
$(this).toggleClass('glyphicon-eye-close glyphicon-eye-open');
|
||||
});
|
||||
})(jQuery);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
14
bbb-lti/grails-app/assets/stylesheets/tool.css
Normal file
14
bbb-lti/grails-app/assets/stylesheets/tool.css
Normal file
@ -0,0 +1,14 @@
|
||||
.thumbnail {
|
||||
height: 51px;
|
||||
padding: 4px;
|
||||
width: 66px;
|
||||
float: left;
|
||||
}
|
||||
|
||||
.thumbnail:hover {
|
||||
display: inline-block;
|
||||
height: auto;
|
||||
position: absolute;
|
||||
width: auto;
|
||||
z-index: 99999;
|
||||
}
|
@ -309,9 +309,7 @@ class ToolController {
|
||||
recordings = response
|
||||
}
|
||||
// Sanitize recordings
|
||||
Iterator i = recordings.iterator();
|
||||
while (i.hasNext()) {
|
||||
def recording = i.next()
|
||||
for (recording in recordings) {
|
||||
// Calculate duration.
|
||||
long endTime = Long.parseLong((String)recording.get("endTime"))
|
||||
endTime -= (endTime % 1000)
|
||||
@ -326,10 +324,22 @@ class ToolController {
|
||||
// Add reportDate.
|
||||
recording.put("reportDate", reportDate)
|
||||
recording.put("unixDate", startTime / 1000)
|
||||
// Add sanitized thumbnails
|
||||
recording.put("thumbnails", sanitizeThumbnails(recording.playback.format))
|
||||
}
|
||||
return recordings
|
||||
}
|
||||
|
||||
private List<Object> sanitizeThumbnails(Object format) {
|
||||
if (format.preview == null || format.preview.images == null || format.preview.images.image == null) {
|
||||
return new ArrayList()
|
||||
}
|
||||
if (format.preview.images.image instanceof Map<?,?>) {
|
||||
return new ArrayList(format.preview.images.image)
|
||||
}
|
||||
return format.preview.images.image
|
||||
}
|
||||
|
||||
private String getCartridgeXML(){
|
||||
def lti_endpoint = ltiService.retrieveBasicLtiEndpoint() + '/' + grailsApplication.metadata['app.name']
|
||||
def launch_url = 'http://' + lti_endpoint + '/tool'
|
||||
|
@ -42,6 +42,6 @@ tool.view.preview=Preview
|
||||
tool.view.date=Date
|
||||
tool.view.duration=Duration
|
||||
tool.view.actions=Actions
|
||||
tool.view.dateFormat=E, MMM dd, yyyy HH:mm:ss Z
|
||||
tool.view.dateFormat=E, MM dd, yyyy HH:mm:ss Z
|
||||
|
||||
tool.error.general=Connection could not be established.
|
||||
|
@ -43,6 +43,6 @@ tool.view.preview=Vista preliminar
|
||||
tool.view.date=Fecha
|
||||
tool.view.duration=Duración
|
||||
tool.view.actions=Acciones
|
||||
tool.view.dateFormat=E, MMM dd, yyyy HH:mm:ss Z
|
||||
tool.view.dateFormat=E, MM dd, yyyy HH:mm:ss Z
|
||||
|
||||
tool.error.general=No pudo estableserce la conexión.
|
||||
|
@ -38,6 +38,6 @@ tool.view.preview=Apreçu
|
||||
tool.view.date=Date
|
||||
tool.view.duration=Durée
|
||||
tool.view.actions=Actions
|
||||
tool.view.dateFormat=E, MMM dd, yyyy HH:mm:ss Z
|
||||
tool.view.dateFormat=E, MM dd, yyyy HH:mm:ss Z
|
||||
|
||||
tool.error.general=Pas possible établir la connection.
|
||||
|
@ -4,6 +4,7 @@
|
||||
<link rel="shortcut icon" href="${assetPath(src: 'favicon.ico')}" type="image/x-icon">
|
||||
<asset:stylesheet src="bootstrap.css"/>
|
||||
<asset:stylesheet src="dataTables.bootstrap.min.css"/>
|
||||
<asset:stylesheet src="tool.css"/>
|
||||
<asset:javascript src="jquery.js"/>
|
||||
<asset:javascript src="jquery.dataTables.min.js"/>
|
||||
<asset:javascript src="dataTables.bootstrap.min.js"/>
|
||||
@ -44,16 +45,24 @@
|
||||
</td>
|
||||
<td class="cell c1" style="text-align:left;">${r.name}</td>
|
||||
<td class="cell c2" style="text-align:left;">${r.metadata.contextactivitydescription}</td>
|
||||
<td class="cell c3" style="text-align:left;">${r.metadata.contextactivitydescription}</td>
|
||||
<td class="cell c4" style="text-align:left;">${r.reportDate}</td>
|
||||
<td class="cell c3" style="text-align:left;">
|
||||
<g:if test="${r.published}">
|
||||
<div>
|
||||
<g:each in="${r.thumbnails}" var="thumbnail">
|
||||
<img src="${thumbnail.content}" class="thumbnail"></img>
|
||||
</g:each>
|
||||
</div>
|
||||
</g:if>
|
||||
</td>
|
||||
<td class="cell c4" style="text-align:left;">${r.unixDate}</td>
|
||||
<td class="cell c5" style="text-align:right;">${r.duration}</td>
|
||||
<g:if test="${ismoderator}">
|
||||
<td class="cell c6 lastcol" style="text-align:center;">
|
||||
<g:if test="${r.published == 'true'}">
|
||||
<a title="<g:message code="tool.view.recording.unpublish" />" class="btn btn-default btn-sm glyphicon glyphicon-eye-close" name="unpublish_recording" type="submit" value="${r.recordID}" href="${createLink(controller:'tool',action:'publish',id: '0')}?bbb_recording_published=${r.published}&bbb_recording_id=${r.recordID}"></a>
|
||||
<g:if test="${r.published}">
|
||||
<a title="<g:message code="tool.view.recording.unpublish" />" class="btn btn-default btn-sm glyphicon glyphicon-eye-open" name="unpublish_recording" type="submit" value="${r.recordID}" href="${createLink(controller:'tool',action:'publish',id: '0')}?bbb_recording_published=${r.published}&bbb_recording_id=${r.recordID}"></a>
|
||||
</g:if>
|
||||
<g:else>
|
||||
<a title="<g:message code="tool.view.recording.publish" />" class="btn btn-default btn-sm glyphicon glyphicon-eye-open" name="publish_recording" type="submit" value="${r.recordID}" href="${createLink(controller:'tool',action:'publish',id: '0')}?bbb_recording_published=${r.published}&bbb_recording_id=${r.recordID}"></a>
|
||||
<a title="<g:message code="tool.view.recording.publish" />" class="btn btn-default btn-sm glyphicon glyphicon-eye-close" name="publish_recording" type="submit" value="${r.recordID}" href="${createLink(controller:'tool',action:'publish',id: '0')}?bbb_recording_published=${r.published}&bbb_recording_id=${r.recordID}"></a>
|
||||
</g:else>
|
||||
<a title="<g:message code="tool.view.recording.delete" />" class="btn btn-danger btn-sm glyphicon glyphicon-trash" name="delete_recording" value="${r.recordID}"
|
||||
data-toggle="confirmation"
|
||||
|
@ -85,12 +85,12 @@
|
||||
<div >
|
||||
<h2>BigBlueButton HTML5 client test server</h2>
|
||||
<p> <a href="http://bigbluebutton.org/" target="_blank">BigBlueButton</a> is an open source web conferencing system for on-line learning. This is a public test server for the BigBlueButton <a href="http://docs.bigbluebutton.org/html/html5-overview.html">HTML5 client</a> currently under development.</p>
|
||||
<p> Our goal for the upcoming release of the HTML5 client is to implement all the <a href="https://youtu.be/oh0bEk3YSwI">viewer capabilities</a> of the Flash client. Students join online classes as a viewer. The HTML5 client will give remote students the ability to join from their Android mobile devices. Users using the Flash and HTML5 clients can join the same meeting (hence the two choices above). We built the HTML5 client using web real-time communication (WebRTC), <a href="https://facebook.github.io/react/">React</a>, and <a href="https://www.mongodb.com/">MongoDB</a>.</p>
|
||||
<p> The HTML5 works well with desktop and Android devices (phone and tablets) as they all support WebRTC. Apple does not (yet) support WebRTC in Safari for iOS devices, but don't worry -- we are working in parallel on app for iOS devices. What can this developer build of the HTML5 client do right now? Pretty much everything the Flash client can do for viewers except (a) view a desktop sharing stream from the presenter and (b) send/receive webcam streams. We're working on (a) and (b). For now, we are really happy to share with you our progress and get <a href="https://docs.google.com/forms/d/1gFz5JdN3vD6jxhlVskFYgtEKEcexdDnUzpkwUXwQ4OY/viewform?usp=send_for">your feedback</a> on what has been implemeted so far. Enjoy!</p>
|
||||
<p> Our goal for the upcoming release of the HTML5 client is to implement all the <a href="https://youtu.be/oh0bEk3YSwI">viewer capabilities</a> of the Flash client. Students join online classes as a viewer. The HTML5 client will give remote students the ability to join from their Android and Apple (iOS 11+) devices. Users using the Flash and HTML5 clients can join the same meeting (hence the two choices above). We built the HTML5 client using web real-time communication (WebRTC), <a href="https://facebook.github.io/react/">React</a>, and <a href="https://www.mongodb.com/">MongoDB</a>.</p>
|
||||
<p> What can this developer build of the HTML5 client do right now? Pretty much everything the Flash client can do for viewers except (a) view a desktop sharing stream from the presenter and (b) send/receive webcam streams. We're working on (a) and (b). For now, we are really happy to share with you our progress and get <a href="https://docs.google.com/forms/d/1gFz5JdN3vD6jxhlVskFYgtEKEcexdDnUzpkwUXwQ4OY/viewform?usp=send_for">your feedback</a> on what has been implemeted so far. Enjoy!</p>
|
||||
|
||||
<h4>For Developers</h4>
|
||||
<p> The BigBlueButton project is <a href="http://bigbluebutton.org/support">supported</a> by a community of developers that care about good design and a streamlined user experience. </p>
|
||||
<p>See <a href="/demo/demo1.jsp" target="_blank">API examples </a> for how to integrate BigBlueButton with your project.</p>
|
||||
<p>See <a href="http://docs.bigblubutton.org" target="_blank">Documentation</a> for more information on how you can integrate BigBlueButton with your project.</p>
|
||||
</div>
|
||||
<div class="span one"></div>
|
||||
|
||||
@ -98,6 +98,7 @@
|
||||
|
||||
|
||||
|
||||
|
||||
<hr class="featurette-divider">
|
||||
|
||||
<!-- BigBlueButton Features -->
|
||||
|
@ -7,4 +7,4 @@ import handleUserVoted from './handlers/userVoted';
|
||||
RedisPubSub.on('PollShowResultEvtMsg', handlePollPublished);
|
||||
RedisPubSub.on('PollStartedEvtMsg', handlePollStarted);
|
||||
RedisPubSub.on('PollStoppedEvtMsg', handlePollStopped);
|
||||
RedisPubSub.on('UserRespondedToPollEvtMsg', handleUserVoted);
|
||||
RedisPubSub.on('PollUpdatedEvtMsg', handleUserVoted);
|
||||
|
@ -3,11 +3,20 @@ import updateVotes from '../modifiers/updateVotes';
|
||||
|
||||
export default function userVoted({ body }, meetingId) {
|
||||
const { poll } = body;
|
||||
const { presenterId } = body;
|
||||
|
||||
check(meetingId, String);
|
||||
check(poll, Object);
|
||||
check(presenterId, String);
|
||||
check(poll, {
|
||||
id: String,
|
||||
answers: [
|
||||
{
|
||||
id: Number,
|
||||
key: String,
|
||||
numVotes: Number,
|
||||
},
|
||||
],
|
||||
numRespondents: Number,
|
||||
numResponders: Number,
|
||||
});
|
||||
|
||||
return updateVotes(poll, meetingId, presenterId);
|
||||
return updateVotes(poll, meetingId);
|
||||
}
|
||||
|
@ -10,6 +10,11 @@ export default function publishVote(credentials, id, pollAnswerId) { // TODO dis
|
||||
|
||||
const { meetingId, requesterUserId } = credentials;
|
||||
|
||||
/*
|
||||
We keep an array of people who were in the meeting at the time the poll
|
||||
was started. The poll is published to them only.
|
||||
Once they vote - their ID is removed and they cannot see the poll anymore
|
||||
*/
|
||||
const currentPoll = Polls.findOne({
|
||||
users: requesterUserId,
|
||||
meetingId,
|
||||
|
@ -3,19 +3,17 @@ import { check } from 'meteor/check';
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
import flat from 'flat';
|
||||
|
||||
export default function updateVotes(poll, meetingId, requesterId) {
|
||||
export default function updateVotes(poll, meetingId) {
|
||||
check(meetingId, String);
|
||||
check(requesterId, String);
|
||||
check(poll, Object);
|
||||
|
||||
const {
|
||||
id,
|
||||
answers,
|
||||
numResponders,
|
||||
numRespondents,
|
||||
} = poll;
|
||||
|
||||
const { numResponders } = poll;
|
||||
const { numRespondents } = poll;
|
||||
|
||||
check(id, String);
|
||||
check(answers, Array);
|
||||
|
||||
@ -24,15 +22,11 @@ export default function updateVotes(poll, meetingId, requesterId) {
|
||||
|
||||
const selector = {
|
||||
meetingId,
|
||||
requester: requesterId,
|
||||
id,
|
||||
};
|
||||
|
||||
const modifier = {
|
||||
$set: Object.assign(
|
||||
{ requester: requesterId },
|
||||
flat(poll, { safe: true }),
|
||||
),
|
||||
$set: flat(poll, { safe: true }),
|
||||
};
|
||||
|
||||
const cb = (err) => {
|
||||
|
@ -7,6 +7,7 @@ import handlePresentationConversionUpdate from './handlers/presentationConversio
|
||||
|
||||
RedisPubSub.on('SyncGetPresentationInfoRespMsg', handlePresentationInfoReply);
|
||||
RedisPubSub.on('PresentationPageGeneratedEvtMsg', handlePresentationConversionUpdate);
|
||||
RedisPubSub.on('PresentationPageCountErrorEvtMsg', handlePresentationConversionUpdate);
|
||||
RedisPubSub.on('PresentationConversionUpdateEvtMsg', handlePresentationConversionUpdate);
|
||||
RedisPubSub.on('PresentationConversionCompletedEvtMsg', handlePresentationAdded);
|
||||
RedisPubSub.on('RemovePresentationEvtMsg', handlePresentationRemove);
|
||||
|
@ -49,6 +49,7 @@ export default function handlePresentationConversionUpdate({ body }, meetingId)
|
||||
statusModifier.id = presentationId;
|
||||
statusModifier.name = presentationName;
|
||||
statusModifier['conversion.error'] = true;
|
||||
statusModifier['conversion.done'] = true;
|
||||
break;
|
||||
|
||||
case GENERATED_SLIDE_KEY:
|
||||
|
@ -1,5 +1,7 @@
|
||||
import { check } from 'meteor/check';
|
||||
|
||||
import Logger from '/imports/startup/server/logger';
|
||||
import Users from '/imports/api/users';
|
||||
import addVoiceUser from '../modifiers/addVoiceUser';
|
||||
|
||||
export default function handleJoinVoiceUser({ body }, meetingId) {
|
||||
@ -7,6 +9,74 @@ export default function handleJoinVoiceUser({ body }, meetingId) {
|
||||
voiceUser.joined = true;
|
||||
|
||||
check(meetingId, String);
|
||||
check(voiceUser, {
|
||||
voiceConf: String,
|
||||
intId: String,
|
||||
voiceUserId: String,
|
||||
callerName: String,
|
||||
callerNum: String,
|
||||
muted: Boolean,
|
||||
talking: Boolean,
|
||||
callingWith: String,
|
||||
listenOnly: Boolean,
|
||||
joined: Boolean,
|
||||
});
|
||||
|
||||
const {
|
||||
intId,
|
||||
callerName,
|
||||
} = voiceUser;
|
||||
|
||||
if (intId.toString().startsWith('v_')) {
|
||||
/* voice-only user - called into the conference */
|
||||
|
||||
const selector = {
|
||||
meetingId,
|
||||
userId: intId,
|
||||
};
|
||||
|
||||
const USER_CONFIG = Meteor.settings.public.user;
|
||||
const ROLE_VIEWER = USER_CONFIG.role_viewer;
|
||||
|
||||
const modifier = {
|
||||
$set: {
|
||||
meetingId,
|
||||
connectionStatus: 'online',
|
||||
roles: [ROLE_VIEWER.toLowerCase()],
|
||||
sortName: callerName.trim().toLowerCase(),
|
||||
color: '#ffffff', // TODO
|
||||
intId,
|
||||
extId: intId, // TODO
|
||||
name: callerName,
|
||||
role: ROLE_VIEWER.toLowerCase(),
|
||||
guest: false,
|
||||
authed: true,
|
||||
waitingForAcceptance: false,
|
||||
emoji: 'none',
|
||||
presenter: false,
|
||||
locked: false, // TODO
|
||||
avatar: '',
|
||||
},
|
||||
};
|
||||
|
||||
const cb = (err, numChanged) => {
|
||||
if (err) {
|
||||
return Logger.error(`Adding call-in user to VoiceUser collection: ${err}`);
|
||||
}
|
||||
|
||||
const { insertedId } = numChanged;
|
||||
if (insertedId) {
|
||||
return Logger.info(`Added a call-in user id=${intId} meeting=${meetingId}`);
|
||||
}
|
||||
|
||||
return Logger.info(`Upserted a call-in user id=${intId} meeting=${meetingId}`);
|
||||
};
|
||||
|
||||
Users.upsert(selector, modifier, cb);
|
||||
} else {
|
||||
|
||||
/* there is a corresponding web user in Users collection -- no need to add new one */
|
||||
}
|
||||
|
||||
return addVoiceUser(meetingId, voiceUser);
|
||||
}
|
||||
|
@ -1,11 +1,20 @@
|
||||
import { check } from 'meteor/check';
|
||||
|
||||
import removeVoiceUser from '../modifiers/removeVoiceUser';
|
||||
import removeVoiceUser from '/imports/api/voice-users/server/modifiers/removeVoiceUser';
|
||||
import removeUser from '/imports/api/users/server/modifiers/removeUser';
|
||||
|
||||
export default function handleVoiceUpdate({ body }, meetingId) {
|
||||
const voiceUser = body;
|
||||
|
||||
check(meetingId, String);
|
||||
check(voiceUser, {
|
||||
voiceConf: String,
|
||||
intId: String,
|
||||
voiceUserId: String,
|
||||
});
|
||||
|
||||
const { intId } = voiceUser;
|
||||
|
||||
removeUser(meetingId, intId);
|
||||
return removeVoiceUser(meetingId, voiceUser);
|
||||
}
|
||||
|
@ -2,10 +2,12 @@ import { Meteor } from 'meteor/meteor';
|
||||
import mapToAcl from '/imports/startup/mapToAcl';
|
||||
import listenOnlyToggle from './methods/listenOnlyToggle';
|
||||
import muteToggle from './methods/muteToggle';
|
||||
import ejectUserFromVoice from './methods/ejectUserFromVoice';
|
||||
|
||||
Meteor.methods(mapToAcl(['methods.listenOnlyToggle', 'methods.toggleSelfVoice', 'methods.toggleVoice',
|
||||
], {
|
||||
Meteor.methods(mapToAcl(['methods.listenOnlyToggle', 'methods.toggleSelfVoice',
|
||||
'methods.toggleVoice', 'methods.ejectUserFromVoice'], {
|
||||
listenOnlyToggle,
|
||||
toggleSelfVoice: (credentials) => { muteToggle(credentials, credentials.requesterUserId); },
|
||||
toggleVoice: muteToggle,
|
||||
ejectUserFromVoice,
|
||||
}));
|
||||
|
@ -0,0 +1,22 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import { check } from 'meteor/check';
|
||||
import RedisPubSub from '/imports/startup/server/redis';
|
||||
|
||||
export default function ejectUserFromVoice(credentials, userId) {
|
||||
const REDIS_CONFIG = Meteor.settings.redis;
|
||||
const CHANNEL = REDIS_CONFIG.channels.toAkkaApps;
|
||||
const EVENT_NAME = 'EjectUserFromVoiceCmdMsg';
|
||||
|
||||
const { requesterUserId, meetingId } = credentials;
|
||||
|
||||
check(meetingId, String);
|
||||
check(requesterUserId, String);
|
||||
check(userId, String);
|
||||
|
||||
const payload = {
|
||||
userId,
|
||||
ejectedBy: requesterUserId,
|
||||
};
|
||||
|
||||
return RedisPubSub.publishUserMessage(CHANNEL, EVENT_NAME, meetingId, requesterUserId, payload);
|
||||
}
|
@ -61,6 +61,8 @@ class ActionsDropdown extends Component {
|
||||
<Dropdown ref={(ref) => { this._dropdown = ref; }} >
|
||||
<DropdownTrigger tabIndex={0} >
|
||||
<Button
|
||||
hideLabel
|
||||
aria-label={intl.formatMessage(intlMessages.actionsLabel)}
|
||||
className={styles.button}
|
||||
label={intl.formatMessage(intlMessages.actionsLabel)}
|
||||
icon="plus"
|
||||
|
@ -42,17 +42,21 @@ const EmojiSelect = ({
|
||||
const statuses = Object.keys(options);
|
||||
const lastStatus = statuses.pop();
|
||||
|
||||
const statusLabel = statuses.indexOf(selected) === -1 ?
|
||||
intl.formatMessage(intlMessages.statusTriggerLabel)
|
||||
: intl.formatMessage({ id: `app.actionsBar.emojiMenu.${selected}Label` });
|
||||
|
||||
return (
|
||||
<Dropdown autoFocus>
|
||||
<DropdownTrigger tabIndex={0}>
|
||||
<Button
|
||||
className={styles.button}
|
||||
label={intl.formatMessage(intlMessages.statusTriggerLabel)}
|
||||
aria-label={intl.formatMessage(intlMessages.changeStatusLabel)}
|
||||
label={statusLabel}
|
||||
aria-label={statusLabel}
|
||||
aria-describedby="currentStatus"
|
||||
icon={options[selected !== lastStatus ? selected : statuses[1]]}
|
||||
ghost={false}
|
||||
hideLabel={false}
|
||||
hideLabel
|
||||
circle
|
||||
size="lg"
|
||||
color="primary"
|
||||
|
@ -1,9 +1,30 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { defineMessages, intlShape, injectIntl } from 'react-intl';
|
||||
import Button from '/imports/ui/components/button/component';
|
||||
import styles from './styles';
|
||||
import cx from 'classnames';
|
||||
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
joinAudio: {
|
||||
id: 'app.audio.joinAudio',
|
||||
description: 'Join audio button label',
|
||||
},
|
||||
leaveAudio: {
|
||||
id: 'app.audio.leaveAudio',
|
||||
description: 'Leave audio button label',
|
||||
},
|
||||
muteAudio: {
|
||||
id: 'app.actionsBar.muteLabel',
|
||||
description: 'Mute audio button label',
|
||||
},
|
||||
unmuteAudio: {
|
||||
id: 'app.actionsBar.unmuteLabel',
|
||||
description: 'Unmute audio button label',
|
||||
},
|
||||
});
|
||||
|
||||
const propTypes = {
|
||||
handleToggleMuteMicrophone: PropTypes.func.isRequired,
|
||||
handleJoinAudio: PropTypes.func.isRequired,
|
||||
@ -12,6 +33,7 @@ const propTypes = {
|
||||
unmute: PropTypes.bool.isRequired,
|
||||
mute: PropTypes.bool.isRequired,
|
||||
join: PropTypes.bool.isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
const AudioControls = ({
|
||||
@ -23,6 +45,7 @@ const AudioControls = ({
|
||||
disable,
|
||||
glow,
|
||||
join,
|
||||
intl,
|
||||
}) => (
|
||||
<span className={styles.container}>
|
||||
{mute ?
|
||||
@ -30,7 +53,9 @@ const AudioControls = ({
|
||||
className={glow ? cx(styles.button, styles.glow) : styles.button}
|
||||
onClick={handleToggleMuteMicrophone}
|
||||
disabled={disable}
|
||||
label={unmute ? 'Unmute' : 'Mute'}
|
||||
hideLabel
|
||||
label={unmute ? intl.formatMessage(intlMessages.unmuteAudio) : intl.formatMessage(intlMessages.muteAudio)}
|
||||
aria-label={unmute ? intl.formatMessage(intlMessages.unmuteAudio) : intl.formatMessage(intlMessages.muteAudio)}
|
||||
color={'primary'}
|
||||
icon={unmute ? 'mute' : 'unmute'}
|
||||
size={'lg'}
|
||||
@ -40,7 +65,9 @@ const AudioControls = ({
|
||||
className={styles.button}
|
||||
onClick={join ? handleLeaveAudio : handleJoinAudio}
|
||||
disabled={disable}
|
||||
label={join ? 'Leave Audio' : 'Join Audio'}
|
||||
hideLabel
|
||||
aria-label={join ? intl.formatMessage(intlMessages.leaveAudio) : intl.formatMessage(intlMessages.joinAudio)}
|
||||
label={join ? intl.formatMessage(intlMessages.leaveAudio) : intl.formatMessage(intlMessages.joinAudio)}
|
||||
color={join ? 'danger' : 'primary'}
|
||||
icon={join ? 'audio_off' : 'audio_on'}
|
||||
size={'lg'}
|
||||
@ -50,4 +77,4 @@ const AudioControls = ({
|
||||
|
||||
AudioControls.propTypes = propTypes;
|
||||
|
||||
export default AudioControls;
|
||||
export default injectIntl(AudioControls);
|
||||
|
@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import cx from 'classnames';
|
||||
import Tooltip from '/imports/ui/components/tooltip/component';
|
||||
import styles from './styles';
|
||||
import Icon from '../icon/component';
|
||||
import BaseButton from './base/component';
|
||||
@ -86,7 +87,6 @@ const defaultProps = {
|
||||
};
|
||||
|
||||
export default class Button extends BaseButton {
|
||||
|
||||
_getClassNames() {
|
||||
const {
|
||||
size,
|
||||
@ -113,8 +113,26 @@ export default class Button extends BaseButton {
|
||||
}
|
||||
|
||||
render() {
|
||||
const renderFuncName = this.props.circle ?
|
||||
'renderCircle' : 'renderDefault';
|
||||
const {
|
||||
circle,
|
||||
hideLabel,
|
||||
label,
|
||||
'aria-label' : ariaLabel
|
||||
} = this.props;
|
||||
|
||||
const renderFuncName = circle ? 'renderCircle' : 'renderDefault';
|
||||
|
||||
if (hideLabel) {
|
||||
const tooltipLabel = label ? label : ariaLabel;
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
title={tooltipLabel}
|
||||
>
|
||||
{this[renderFuncName]()}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
return this[renderFuncName]();
|
||||
}
|
||||
|
@ -43,7 +43,7 @@ $btn-jumbo-padding: $jumbo-padding-y $jumbo-padding-x;
|
||||
display: inline-block;
|
||||
border-radius: $border-size;
|
||||
font-weight: $btn-font-weight;
|
||||
line-height: 1.5;
|
||||
line-height: 1;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
vertical-align: middle;
|
||||
|
@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
|
||||
import { Link } from 'react-router';
|
||||
import { defineMessages, injectIntl } from 'react-intl';
|
||||
import injectWbResizeEvent from '/imports/ui/components/presentation/resize-wrapper/component';
|
||||
import Button from '/imports/ui/components/button/component';
|
||||
import styles from './styles';
|
||||
import MessageForm from './message-form/component';
|
||||
import MessageList from './message-list/component';
|
||||
@ -92,15 +93,21 @@ class Chat extends Component {
|
||||
</div>
|
||||
{
|
||||
chatID !== 'public' ?
|
||||
<Link
|
||||
to="/users"
|
||||
role="button"
|
||||
className={styles.closeIcon}
|
||||
<Link
|
||||
to="/users"
|
||||
role="button"
|
||||
tabIndex={-1}
|
||||
>
|
||||
<Button
|
||||
className={styles.closeBtn}
|
||||
icon="close"
|
||||
size="md"
|
||||
hideLabel
|
||||
onClick={() => actions.handleClosePrivateChat(chatID)}
|
||||
aria-label={intl.formatMessage(intlMessages.closeChatLabel, { 0: title })}
|
||||
>
|
||||
<Icon iconName="close" onClick={() => actions.handleClosePrivateChat(chatID)} />
|
||||
</Link> :
|
||||
<ChatDropdown />
|
||||
/>
|
||||
</Link> :
|
||||
<ChatDropdown />
|
||||
}
|
||||
</header>
|
||||
<MessageList
|
||||
|
@ -27,7 +27,11 @@ $padding: $md-padding-x;
|
||||
margin-right: -$padding;
|
||||
padding-right: $padding;
|
||||
padding-top: 0;
|
||||
padding-bottom: $padding;
|
||||
&:after {
|
||||
content: "";
|
||||
display: block;
|
||||
height: $padding;
|
||||
}
|
||||
}
|
||||
|
||||
.unreadButton {
|
||||
|
@ -1,6 +1,8 @@
|
||||
@import "/imports/ui/stylesheets/mixins/focus";
|
||||
@import "/imports/ui/stylesheets/variables/_all";
|
||||
|
||||
$icon-offset: -.4em;
|
||||
|
||||
.chat {
|
||||
background-color: #fff;
|
||||
padding: $md-padding-x;
|
||||
@ -36,7 +38,22 @@
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.closeIcon {
|
||||
.closeBtn {
|
||||
background-color: $color-white;
|
||||
flex: 0 0;
|
||||
margin-left: $sm-padding-x / 2;
|
||||
padding: 0 0.25rem !important;
|
||||
|
||||
i {
|
||||
font-size: 0.85em;
|
||||
color: $color-gray-dark !important;
|
||||
top: $icon-offset;
|
||||
}
|
||||
|
||||
&:focus,
|
||||
&:hover{
|
||||
background-color: $color-white !important;
|
||||
i{
|
||||
color: $color-gray;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,9 @@ import { defineMessages, injectIntl, intlShape } from 'react-intl';
|
||||
import Dropzone from 'react-dropzone';
|
||||
import update from 'immutability-helper';
|
||||
import cx from 'classnames';
|
||||
import _ from 'lodash';
|
||||
|
||||
import { notify } from '/imports/ui/services/notification';
|
||||
import ModalFullscreen from '/imports/ui/components/modal/fullscreen/component';
|
||||
import Icon from '/imports/ui/components/icon/component';
|
||||
import ButtonBase from '/imports/ui/components/button/base/component';
|
||||
@ -28,10 +30,12 @@ const propTypes = {
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
defaultFileName: 'default.pdf',
|
||||
};
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
current: {
|
||||
id: 'app.presentationUploder.currentBadge',
|
||||
},
|
||||
title: {
|
||||
id: 'app.presentationUploder.title',
|
||||
description: 'title of the modal',
|
||||
@ -68,6 +72,10 @@ const intlMessages = defineMessages({
|
||||
id: 'app.presentationUploder.fileToUpload',
|
||||
description: 'message used in the file selected for upload',
|
||||
},
|
||||
genericError: {
|
||||
id: 'app.presentationUploder.genericError',
|
||||
description: 'generic error while uploading/converting',
|
||||
},
|
||||
uploadProcess: {
|
||||
id: 'app.presentationUploder.upload.progress',
|
||||
description: 'message that indicates the percentage of the upload',
|
||||
@ -84,9 +92,12 @@ const intlMessages = defineMessages({
|
||||
id: 'app.presentationUploder.conversion.genericConversionStatus',
|
||||
description: 'indicates that file is being converted',
|
||||
},
|
||||
TIMEOUT: {
|
||||
id: 'app.presentationUploder.conversion.timeout',
|
||||
},
|
||||
GENERATING_THUMBNAIL: {
|
||||
id: 'app.presentationUploder.conversion.generatingThumbnail',
|
||||
description: 's that it is generating thumbnails',
|
||||
description: 'indicatess that it is generating thumbnails',
|
||||
},
|
||||
GENERATING_SVGIMAGES: {
|
||||
id: 'app.presentationUploder.conversion.generatingSvg',
|
||||
@ -96,14 +107,21 @@ const intlMessages = defineMessages({
|
||||
id: 'app.presentationUploder.conversion.generatedSlides',
|
||||
description: 'warns that were slides generated',
|
||||
},
|
||||
PAGE_COUNT_EXCEEDED: {
|
||||
id: 'app.presentationUploder.conversion.pageCountExceeded',
|
||||
description: 'warns the user that the conversion failed because of the page count',
|
||||
},
|
||||
});
|
||||
|
||||
class PresentationUploader extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
const currentPres = props.presentations.find(p => p.isCurrent);
|
||||
|
||||
this.state = {
|
||||
presentations: props.presentations,
|
||||
oldCurrentId: currentPres ? currentPres.id : -1,
|
||||
preventClosing: false,
|
||||
disableActions: false,
|
||||
};
|
||||
@ -118,27 +136,19 @@ class PresentationUploader extends Component {
|
||||
this.deepMergeUpdateFileKey = this.deepMergeUpdateFileKey.bind(this);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const nextPresentations = nextProps.presentations;
|
||||
|
||||
// Update only the conversion state when receiving new props
|
||||
nextPresentations.forEach((file) => {
|
||||
this.updateFileKey(file.filename, 'id', file.id);
|
||||
this.deepMergeUpdateFileKey(file.id, 'conversion', file.conversion);
|
||||
});
|
||||
}
|
||||
|
||||
updateFileKey(id, key, value, operation = '$set') {
|
||||
this.setState(({ presentations }) => {
|
||||
// Compare id and filename since non-uploaded files dont have a real id
|
||||
const fileIndex = presentations.findIndex(f => f.id === id || f.filename === id);
|
||||
const fileIndex = presentations.findIndex(f => f.id === id);
|
||||
|
||||
return fileIndex === -1 ? false : {
|
||||
presentations: update(presentations, {
|
||||
[fileIndex]: { $apply: file =>
|
||||
update(file, { [key]: {
|
||||
[operation]: value,
|
||||
} }),
|
||||
[fileIndex]: {
|
||||
$apply: file =>
|
||||
update(file, {
|
||||
[key]: {
|
||||
[operation]: value,
|
||||
},
|
||||
}),
|
||||
},
|
||||
}),
|
||||
};
|
||||
@ -150,26 +160,54 @@ class PresentationUploader extends Component {
|
||||
this.updateFileKey(id, key, applyValue, '$apply');
|
||||
}
|
||||
|
||||
isDefault(presentation) {
|
||||
const { defaultFileName } = this.props;
|
||||
return presentation.filename === defaultFileName
|
||||
&& !presentation.id.includes(defaultFileName);
|
||||
}
|
||||
|
||||
handleConfirm() {
|
||||
const { presentations } = this.state;
|
||||
const presentationsToSave = this.state.presentations
|
||||
.filter(p => !p.upload.error && !p.conversion.error);
|
||||
|
||||
this.setState({
|
||||
disableActions: true,
|
||||
preventClosing: true,
|
||||
presentations: presentationsToSave,
|
||||
});
|
||||
|
||||
return this.props.handleSave(presentations)
|
||||
return this.props.handleSave(presentationsToSave)
|
||||
.then(() => {
|
||||
this.setState({
|
||||
disableActions: false,
|
||||
preventClosing: false,
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
const hasError = this.state.presentations.some(p => p.upload.error || p.conversion.error);
|
||||
if (!hasError) {
|
||||
this.setState({
|
||||
disableActions: false,
|
||||
preventClosing: false,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// if theres error we dont want to close the modal
|
||||
this.setState({
|
||||
disableActions: false,
|
||||
preventClosing: true,
|
||||
}, () => {
|
||||
// if the selected current has error we revert back to the old one
|
||||
const newCurrent = this.state.presentations.find(p => p.isCurrent);
|
||||
if (newCurrent.upload.error || newCurrent.conversion.error) {
|
||||
this.handleCurrentChange(this.state.oldCurrentId);
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
notify(this.props.intl.formatMessage(intlMessages.genericError), 'error');
|
||||
|
||||
console.error(error);
|
||||
|
||||
this.setState({
|
||||
disableActions: false,
|
||||
preventClosing: true,
|
||||
error,
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -184,52 +222,62 @@ class PresentationUploader extends Component {
|
||||
}
|
||||
|
||||
handleFiledrop(files) {
|
||||
const presentationsToUpload = files.map(file => ({
|
||||
file,
|
||||
id: file.name,
|
||||
filename: file.name,
|
||||
isCurrent: false,
|
||||
conversion: { done: false, error: false },
|
||||
upload: { done: false, error: false, progress: 0 },
|
||||
onProgress: (event) => {
|
||||
if (!event.lengthComputable) {
|
||||
this.deepMergeUpdateFileKey(file.name, 'upload', {
|
||||
progress: 100,
|
||||
done: true,
|
||||
const presentationsToUpload = files.map((file) => {
|
||||
const id = _.uniqueId(file.name);
|
||||
|
||||
return {
|
||||
file,
|
||||
id,
|
||||
filename: file.name,
|
||||
isCurrent: false,
|
||||
conversion: { done: false, error: false },
|
||||
upload: { done: false, error: false, progress: 0 },
|
||||
onProgress: (event) => {
|
||||
if (!event.lengthComputable) {
|
||||
this.deepMergeUpdateFileKey(id, 'upload', {
|
||||
progress: 100,
|
||||
done: true,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.deepMergeUpdateFileKey(id, 'upload', {
|
||||
progress: (event.loaded / event.total) * 100,
|
||||
done: event.loaded === event.total,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.deepMergeUpdateFileKey(file.name, 'upload', {
|
||||
progress: (event.loaded / event.total) * 100,
|
||||
done: event.loaded === event.total,
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
this.deepMergeUpdateFileKey(file.name, 'upload', { error });
|
||||
},
|
||||
}));
|
||||
},
|
||||
onConversion: (conversion) => {
|
||||
this.deepMergeUpdateFileKey(id, 'conversion', conversion);
|
||||
},
|
||||
onUpload: (upload) => {
|
||||
this.deepMergeUpdateFileKey(id, 'upload', upload);
|
||||
},
|
||||
onDone: (newId) => {
|
||||
this.updateFileKey(id, 'id', newId);
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
this.setState(({ presentations }) => ({
|
||||
presentations: presentations.concat(presentationsToUpload),
|
||||
}));
|
||||
}
|
||||
|
||||
handleCurrentChange(item) {
|
||||
handleCurrentChange(id) {
|
||||
const { presentations, disableActions } = this.state;
|
||||
if (disableActions) return;
|
||||
|
||||
const currentIndex = presentations.findIndex(p => p.isCurrent);
|
||||
const newCurrentIndex = presentations.indexOf(item);
|
||||
const newCurrentIndex = presentations.findIndex(p => p.id === id);
|
||||
|
||||
const commands = {};
|
||||
|
||||
// we can end up without a current presentation
|
||||
if (currentIndex !== -1) {
|
||||
commands[currentIndex] = {
|
||||
$apply: (_) => {
|
||||
const p = _;
|
||||
$apply: (presentation) => {
|
||||
const p = presentation;
|
||||
p.isCurrent = false;
|
||||
return p;
|
||||
},
|
||||
@ -237,8 +285,8 @@ class PresentationUploader extends Component {
|
||||
}
|
||||
|
||||
commands[newCurrentIndex] = {
|
||||
$apply: (_) => {
|
||||
const p = _;
|
||||
$apply: (presentation) => {
|
||||
const p = presentation;
|
||||
p.isCurrent = true;
|
||||
return p;
|
||||
},
|
||||
@ -256,14 +304,6 @@ class PresentationUploader extends Component {
|
||||
if (disableActions) return;
|
||||
|
||||
const toRemoveIndex = presentations.indexOf(item);
|
||||
const toRemove = presentations[toRemoveIndex];
|
||||
|
||||
|
||||
if (toRemove.isCurrent) {
|
||||
const defaultPresentation =
|
||||
presentations.find(_ => _.filename === this.props.defaultFileName);
|
||||
this.handleCurrentChange(defaultPresentation);
|
||||
}
|
||||
|
||||
this.setState({
|
||||
presentations: update(presentations, {
|
||||
@ -276,7 +316,17 @@ class PresentationUploader extends Component {
|
||||
const { presentations } = this.state;
|
||||
|
||||
const presentationsSorted = presentations
|
||||
.sort((a, b) => b.filename === this.props.defaultFileName);
|
||||
.sort((a, b) => {
|
||||
// Sort by ID first so files with the same name have the same order
|
||||
if (a.id > b.id) {
|
||||
return 1;
|
||||
}
|
||||
if (a.id < b.id) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
})
|
||||
.sort((a, b) => this.isDefault(b));
|
||||
|
||||
return (
|
||||
<div className={styles.fileList}>
|
||||
@ -303,12 +353,12 @@ class PresentationUploader extends Component {
|
||||
}
|
||||
|
||||
if (item.upload.done && item.upload.error) {
|
||||
const errorMessage = intlMessages[item.upload.error.code] || intlMessages.genericError;
|
||||
const errorMessage = intlMessages[item.upload.status] || intlMessages.genericError;
|
||||
return intl.formatMessage(errorMessage);
|
||||
}
|
||||
|
||||
if (!item.conversion.done && item.conversion.error) {
|
||||
const errorMessage = intlMessages[status] || intlMessages.genericError;
|
||||
if (item.conversion.done && item.conversion.error) {
|
||||
const errorMessage = intlMessages[item.conversion.status] || intlMessages.genericError;
|
||||
return intl.formatMessage(errorMessage);
|
||||
}
|
||||
|
||||
@ -329,19 +379,23 @@ class PresentationUploader extends Component {
|
||||
}
|
||||
|
||||
renderPresentationItem(item) {
|
||||
const { disableActions } = this.state;
|
||||
const { disableActions, oldCurrentId } = this.state;
|
||||
|
||||
const isProcessing = (!item.conversion.done && item.upload.done)
|
||||
|| (!item.upload.done && item.upload.progress > 0);
|
||||
const itemClassName = {};
|
||||
const isActualCurrent = item.id === oldCurrentId;
|
||||
const isUploading = !item.upload.done && item.upload.progress > 0;
|
||||
const isConverting = !item.conversion.done && item.upload.done;
|
||||
const hasError = item.conversion.error || item.upload.error;
|
||||
const isProcessing = (isUploading || isConverting) && !hasError;
|
||||
|
||||
itemClassName[styles.tableItemNew] = item.id === item.filename;
|
||||
itemClassName[styles.tableItemUploading] = !item.upload.done;
|
||||
itemClassName[styles.tableItemProcessing] = !item.conversion.done && item.upload.done;
|
||||
itemClassName[styles.tableItemError] = item.conversion.error || item.upload.error;
|
||||
itemClassName[styles.tableItemAnimated] = isProcessing;
|
||||
const itemClassName = {
|
||||
[styles.tableItemNew]: item.id.indexOf(item.filename) !== -1,
|
||||
[styles.tableItemUploading]: isUploading,
|
||||
[styles.tableItemConverting]: isConverting,
|
||||
[styles.tableItemError]: hasError,
|
||||
[styles.tableItemAnimated]: isProcessing,
|
||||
};
|
||||
|
||||
const hideRemove = isProcessing || item.filename === this.props.defaultFileName;
|
||||
const hideRemove = this.isDefault(item);
|
||||
|
||||
return (
|
||||
<tr
|
||||
@ -349,33 +403,44 @@ class PresentationUploader extends Component {
|
||||
className={cx(itemClassName)}
|
||||
>
|
||||
<td className={styles.tableItemIcon}>
|
||||
<Icon iconName={'file'} />
|
||||
<Icon iconName="file" />
|
||||
</td>
|
||||
<th className={styles.tableItemName}>
|
||||
{
|
||||
isActualCurrent ?
|
||||
<th className={styles.tableItemCurrent}>
|
||||
<span className={styles.currentLabel}>
|
||||
{this.props.intl.formatMessage(intlMessages.current)}
|
||||
</span>
|
||||
</th>
|
||||
: null
|
||||
}
|
||||
<th className={styles.tableItemName} colSpan={!isActualCurrent ? 2 : 0}>
|
||||
<span>{item.filename}</span>
|
||||
</th>
|
||||
<td className={styles.tableItemStatus}>
|
||||
<td className={styles.tableItemStatus} colSpan={hasError ? 2 : 0}>
|
||||
{this.renderPresentationItemStatus(item)}
|
||||
</td>
|
||||
<td className={styles.tableItemActions}>
|
||||
<Checkbox
|
||||
disabled={disableActions}
|
||||
ariaLabel={'Set as current presentation'}
|
||||
className={styles.itemAction}
|
||||
checked={item.isCurrent}
|
||||
onChange={() => this.handleCurrentChange(item)}
|
||||
/>
|
||||
{ hideRemove ? null : (
|
||||
<ButtonBase
|
||||
{ hasError ? null : (
|
||||
<td className={styles.tableItemActions}>
|
||||
<Checkbox
|
||||
disabled={disableActions}
|
||||
className={cx(styles.itemAction, styles.itemActionRemove)}
|
||||
label={'Remove presentation'}
|
||||
onClick={() => this.handleRemove(item)}
|
||||
>
|
||||
<Icon iconName={'delete'} />
|
||||
</ButtonBase>
|
||||
)}
|
||||
</td>
|
||||
ariaLabel="Set as current presentation"
|
||||
className={styles.itemAction}
|
||||
checked={item.isCurrent}
|
||||
onChange={() => this.handleCurrentChange(item.id)}
|
||||
/>
|
||||
{ hideRemove ? null : (
|
||||
<ButtonBase
|
||||
disabled={disableActions}
|
||||
className={cx(styles.itemAction, styles.itemActionRemove)}
|
||||
label="Remove presentation"
|
||||
onClick={() => this.handleRemove(item)}
|
||||
>
|
||||
<Icon iconName="delete" />
|
||||
</ButtonBase>
|
||||
)}
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
@ -404,7 +469,7 @@ class PresentationUploader extends Component {
|
||||
disablePreview
|
||||
onDrop={this.handleFiledrop}
|
||||
>
|
||||
<Icon className={styles.dropzoneIcon} iconName={'upload'} />
|
||||
<Icon className={styles.dropzoneIcon} iconName="upload" />
|
||||
<p className={styles.dropzoneMessage}>
|
||||
{intl.formatMessage(intlMessages.dropzoneLabel)}
|
||||
<span className={styles.dropzoneLink}>
|
||||
|
@ -1,6 +1,7 @@
|
||||
import Presentations from '/imports/api/presentations';
|
||||
import Auth from '/imports/ui/services/auth';
|
||||
import { makeCall } from '/imports/ui/services/api';
|
||||
import _ from 'lodash';
|
||||
|
||||
const CONVERSION_TIMEOUT = 300000;
|
||||
|
||||
@ -29,7 +30,9 @@ const futch = (url, opts = {}, onProgress) => new Promise((res, rej) => {
|
||||
|
||||
const getPresentations = () =>
|
||||
Presentations
|
||||
.find()
|
||||
.find({
|
||||
'conversion.error': false,
|
||||
})
|
||||
.fetch()
|
||||
.map(presentation => ({
|
||||
id: presentation.id,
|
||||
@ -39,36 +42,40 @@ const getPresentations = () =>
|
||||
conversion: presentation.conversion || { done: true, error: false },
|
||||
}));
|
||||
|
||||
const observePresentationConversion = (meetingId, filename) => new Promise((resolve, reject) => {
|
||||
const conversionTimeout = setTimeout(() => {
|
||||
reject({
|
||||
filename,
|
||||
message: 'Conversion timeout.',
|
||||
});
|
||||
}, CONVERSION_TIMEOUT);
|
||||
const observePresentationConversion = (meetingId, filename, onConversion) =>
|
||||
new Promise((resolve) => {
|
||||
const conversionTimeout = setTimeout(() => {
|
||||
onConversion({
|
||||
done: true,
|
||||
error: true,
|
||||
status: 'TIMEOUT',
|
||||
});
|
||||
}, CONVERSION_TIMEOUT);
|
||||
|
||||
const didValidate = (doc) => {
|
||||
clearTimeout(conversionTimeout);
|
||||
resolve(doc);
|
||||
};
|
||||
const didValidate = (doc) => {
|
||||
clearTimeout(conversionTimeout);
|
||||
resolve(doc);
|
||||
};
|
||||
|
||||
Tracker.autorun((c) => {
|
||||
/* FIXME: With two presentations with the same name this will not work as expected */
|
||||
const query = Presentations.find({ meetingId });
|
||||
Tracker.autorun((c) => {
|
||||
const query = Presentations.find({ meetingId });
|
||||
|
||||
query.observe({
|
||||
changed: (newDoc) => {
|
||||
if (newDoc.name !== filename) return;
|
||||
if (newDoc.conversion.done) {
|
||||
c.stop();
|
||||
didValidate(newDoc);
|
||||
}
|
||||
},
|
||||
query.observe({
|
||||
changed: (newDoc) => {
|
||||
if (newDoc.name !== filename) return;
|
||||
|
||||
onConversion(newDoc.conversion);
|
||||
|
||||
if (newDoc.conversion.done) {
|
||||
c.stop();
|
||||
didValidate(newDoc);
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const uploadAndConvertPresentation = (file, meetingID, endpoint, onError, onProgress) => {
|
||||
const uploadAndConvertPresentation = (file, meetingID, endpoint, onUpload, onProgress, onConversion) => {
|
||||
const data = new FormData();
|
||||
data.append('presentation_name', file.name);
|
||||
data.append('Filename', file.name);
|
||||
@ -84,19 +91,17 @@ const uploadAndConvertPresentation = (file, meetingID, endpoint, onError, onProg
|
||||
};
|
||||
|
||||
return futch(endpoint, opts, onProgress)
|
||||
.then(() => observePresentationConversion(meetingID, file.name))
|
||||
.then(() => observePresentationConversion(meetingID, file.name, onConversion))
|
||||
// Trap the error so we can have parallel upload
|
||||
.catch((error) => {
|
||||
onError(error);
|
||||
return observePresentationConversion(meetingID, file.name);
|
||||
onUpload({ error: true, done: true, status: error.code });
|
||||
return Promise.resolve();
|
||||
});
|
||||
};
|
||||
|
||||
const uploadAndConvertPresentations = (presentationsToUpload, meetingID, uploadEndpoint) =>
|
||||
Promise.all(
|
||||
presentationsToUpload.map(p =>
|
||||
uploadAndConvertPresentation(p.file, meetingID, uploadEndpoint, p.onError, p.onProgress)),
|
||||
);
|
||||
Promise.all(presentationsToUpload.map(p =>
|
||||
uploadAndConvertPresentation(p.file, meetingID, uploadEndpoint, p.onUpload, p.onProgress, p.onConversion)));
|
||||
|
||||
const setPresentation = presentationID => makeCall('setPresentation', presentationID);
|
||||
|
||||
@ -106,27 +111,42 @@ const removePresentations = presentationsToRemove =>
|
||||
Promise.all(presentationsToRemove.map(p => removePresentation(p.id)));
|
||||
|
||||
const persistPresentationChanges = (oldState, newState, uploadEndpoint) => {
|
||||
const presentationsToUpload = newState.filter(_ => !oldState.includes(_));
|
||||
const presentationsToRemove = oldState.filter(_ => !newState.includes(_));
|
||||
const currentPresentation = newState.find(_ => _.isCurrent);
|
||||
const presentationsToUpload = newState.filter(p => !p.upload.done);
|
||||
const presentationsToRemove = oldState.filter(p => !_.find(newState, ['id', p.id]));
|
||||
|
||||
return new Promise((resolve, reject) =>
|
||||
uploadAndConvertPresentations(presentationsToUpload, Auth.meetingID, uploadEndpoint)
|
||||
.then((presentations) => {
|
||||
if (!presentations.length && !currentPresentation) return Promise.resolve();
|
||||
let currentPresentation = newState.find(p => p.isCurrent);
|
||||
|
||||
// If its a newly uploaded presentation we need to get its id from promise result
|
||||
const currentPresentationId =
|
||||
currentPresentation.id !== currentPresentation.filename ?
|
||||
currentPresentation.id :
|
||||
presentations[presentationsToUpload.findIndex(_ => _ === currentPresentation)].id;
|
||||
return uploadAndConvertPresentations(presentationsToUpload, Auth.meetingID, uploadEndpoint)
|
||||
.then((presentations) => {
|
||||
if (!presentations.length && !currentPresentation) return Promise.resolve();
|
||||
|
||||
return setPresentation(currentPresentationId);
|
||||
})
|
||||
.then(removePresentations.bind(null, presentationsToRemove))
|
||||
.then(resolve)
|
||||
.catch(reject),
|
||||
);
|
||||
// Update the presentation with their new ids
|
||||
presentations.forEach((p, i) => {
|
||||
if (p === undefined) return;
|
||||
presentationsToUpload[i].onDone(p.id);
|
||||
});
|
||||
|
||||
return Promise.resolve(presentations);
|
||||
})
|
||||
.then((presentations) => {
|
||||
if (currentPresentation === undefined) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// If its a newly uploaded presentation we need to get it from promise result
|
||||
if (!currentPresentation.conversion.done) {
|
||||
const currentIndex = presentationsToUpload.findIndex(p => p === currentPresentation);
|
||||
currentPresentation = presentations[currentIndex];
|
||||
}
|
||||
|
||||
// skip setting as current if error happened
|
||||
if (currentPresentation.conversion.error) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return setPresentation(currentPresentation.id);
|
||||
})
|
||||
.then(removePresentations.bind(null, presentationsToRemove));
|
||||
};
|
||||
|
||||
export default {
|
||||
|
@ -57,12 +57,14 @@ $item-height: 1rem;
|
||||
|
||||
.tableItemIcon,
|
||||
.tableItemActions,
|
||||
.tableItemStatus {
|
||||
.tableItemStatus,
|
||||
.tableItemCurrent {
|
||||
width: 1%;
|
||||
}
|
||||
|
||||
.tableItemActions {
|
||||
min-width: 68px; // size of the 2 icons (check/trash)
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.tableItemIcon > i {
|
||||
@ -75,7 +77,7 @@ $item-height: 1rem;
|
||||
position: relative;
|
||||
|
||||
&:before {
|
||||
content: ' ';
|
||||
content: "\00a0";
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
@ -84,10 +86,13 @@ $item-height: 1rem;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 0 $sm-padding-x;
|
||||
}
|
||||
}
|
||||
|
||||
.tableItemCurrent {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.tableItemStatus {
|
||||
text-align: right;
|
||||
}
|
||||
@ -100,7 +105,7 @@ $item-height: 1rem;
|
||||
background-color: transparentize($color-primary, .75);
|
||||
}
|
||||
|
||||
.tableItemProcessing {
|
||||
.tableItemConverting {
|
||||
background-color: transparentize($color-success, .75);
|
||||
}
|
||||
|
||||
@ -183,3 +188,18 @@ $item-height: 1rem;
|
||||
font-size: 80%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.currentLabel {
|
||||
display: inline;
|
||||
padding: .25em .5em;
|
||||
font-size: 75%;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
color: $color-white;
|
||||
background: $color-primary;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
vertical-align: baseline;
|
||||
border-radius: .25em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
@ -195,8 +195,7 @@ class ApplicationMenu extends BaseMenu {
|
||||
{ availableLocales.map((locale, index) =>
|
||||
(<option key={index} value={locale.locale}>
|
||||
{locale.name}
|
||||
</option>),
|
||||
) }
|
||||
</option>)) }
|
||||
</select>
|
||||
: null }
|
||||
</label>
|
||||
|
@ -0,0 +1,86 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Tippy from 'tippy.js';
|
||||
import _ from 'lodash';
|
||||
import cx from 'classnames';
|
||||
import { ESCAPE } from '/imports/utils/keyCodes';
|
||||
|
||||
const propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
position: PropTypes.oneOf(['bottom']),
|
||||
children: PropTypes.element.isRequired,
|
||||
className: PropTypes.string,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
position: 'bottom',
|
||||
className: null,
|
||||
};
|
||||
|
||||
class Tooltip extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.tippySelectorId = _.uniqueId('tippy-');
|
||||
this.onShow = this.onShow.bind(this);
|
||||
this.onHide = this.onHide.bind(this);
|
||||
this.handleEscapeHide = this.handleEscapeHide.bind(this);
|
||||
this.delay = [250, 100];
|
||||
this.dynamicTitle = true;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const {
|
||||
position,
|
||||
} = this.props;
|
||||
|
||||
const options = {
|
||||
position,
|
||||
dynamicTitle: this.dynamicTitle,
|
||||
delay: this.delay,
|
||||
onShow: this.onShow,
|
||||
onHide: this.onHide,
|
||||
};
|
||||
|
||||
this.tooltip = Tippy(`#${this.tippySelectorId}`, options);
|
||||
}
|
||||
|
||||
onShow() {
|
||||
document.addEventListener('keyup', this.handleEscapeHide);
|
||||
}
|
||||
|
||||
onHide() {
|
||||
document.removeEventListener('keyup', this.handleEscapeHide);
|
||||
}
|
||||
|
||||
handleEscapeHide(e) {
|
||||
if (e.keyCode !== ESCAPE) return;
|
||||
|
||||
this.tooltip.tooltips[0].hide();
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
children,
|
||||
className,
|
||||
title,
|
||||
...restProps
|
||||
} = this.props;
|
||||
|
||||
const WrappedComponent = React.Children.only(children);
|
||||
|
||||
const WrappedComponentBound = React.cloneElement(WrappedComponent, {
|
||||
...restProps,
|
||||
title,
|
||||
id: this.tippySelectorId,
|
||||
className: cx(children.props.className, className),
|
||||
});
|
||||
|
||||
return WrappedComponentBound;
|
||||
}
|
||||
}
|
||||
|
||||
export default Tooltip;
|
||||
|
||||
Tooltip.defaultProps = defaultProps;
|
||||
Tooltip.propTypes = propTypes;
|
@ -5,7 +5,6 @@ import { withRouter } from 'react-router';
|
||||
|
||||
import injectWbResizeEvent from '/imports/ui/components/presentation/resize-wrapper/component';
|
||||
import styles from './styles';
|
||||
import UserListHeader from './user-list-header/component';
|
||||
import UserContent from './user-list-content/component';
|
||||
|
||||
const propTypes = {
|
||||
@ -27,6 +26,7 @@ const propTypes = {
|
||||
kickUser: PropTypes.func.isRequired,
|
||||
toggleVoice: PropTypes.func.isRequired,
|
||||
changeRole: PropTypes.func.isRequired,
|
||||
roving: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
@ -54,10 +54,6 @@ class UserList extends Component {
|
||||
render() {
|
||||
return (
|
||||
<div className={styles.userList}>
|
||||
{/* <UserListHeader
|
||||
intl={this.props.intl}
|
||||
compact={this.state.compact}
|
||||
/> */}
|
||||
{<UserContent
|
||||
intl={this.props.intl}
|
||||
openChats={this.props.openChats}
|
||||
@ -75,6 +71,7 @@ class UserList extends Component {
|
||||
normalizeEmojiName={this.props.normalizeEmojiName}
|
||||
isMeetingLocked={this.props.isMeetingLocked}
|
||||
isPublicChat={this.props.isPublicChat}
|
||||
roving={this.props.roving}
|
||||
/>}
|
||||
</div>
|
||||
);
|
||||
|
@ -1,10 +1,36 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { createContainer } from 'meteor/react-meteor-data';
|
||||
import { meetingIsBreakout } from '/imports/ui/components/app/service';
|
||||
import Meetings from '/imports/api/meetings';
|
||||
import Service from './service';
|
||||
import UserList from './component';
|
||||
|
||||
const propTypes = {
|
||||
openChats: PropTypes.arrayOf(String).isRequired,
|
||||
openChat: PropTypes.string.isRequired,
|
||||
users: PropTypes.arrayOf(Object).isRequired,
|
||||
currentUser: PropTypes.shape({}).isRequired,
|
||||
meeting: PropTypes.shape({}).isRequired,
|
||||
isBreakoutRoom: PropTypes.bool.isRequired,
|
||||
getAvailableActions: PropTypes.func.isRequired,
|
||||
normalizeEmojiName: PropTypes.func.isRequired,
|
||||
isMeetingLocked: PropTypes.func.isRequired,
|
||||
isPublicChat: PropTypes.func.isRequired,
|
||||
setEmojiStatus: PropTypes.func.isRequired,
|
||||
assignPresenter: PropTypes.func.isRequired,
|
||||
kickUser: PropTypes.func.isRequired,
|
||||
toggleVoice: PropTypes.func.isRequired,
|
||||
changeRole: PropTypes.func.isRequired,
|
||||
roving: PropTypes.func.isRequired,
|
||||
userActions: PropTypes.func.isRequired,
|
||||
children: PropTypes.Object,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
children: {},
|
||||
};
|
||||
|
||||
const UserListContainer = (props) => {
|
||||
const {
|
||||
users,
|
||||
@ -24,7 +50,8 @@ const UserListContainer = (props) => {
|
||||
kickUser,
|
||||
toggleVoice,
|
||||
changeRole,
|
||||
} = props;
|
||||
roving,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<UserList
|
||||
@ -44,12 +71,16 @@ const UserListContainer = (props) => {
|
||||
normalizeEmojiName={normalizeEmojiName}
|
||||
isMeetingLocked={isMeetingLocked}
|
||||
isPublicChat={isPublicChat}
|
||||
roving={roving}
|
||||
>
|
||||
{children}
|
||||
</UserList>
|
||||
);
|
||||
};
|
||||
|
||||
UserListContainer.propTypes = propTypes;
|
||||
UserListContainer.defaultProps = defaultProps;
|
||||
|
||||
export default createContainer(({ params }) => ({
|
||||
users: Service.getUsers(),
|
||||
meeting: Meetings.findOne({}),
|
||||
@ -67,4 +98,5 @@ export default createContainer(({ params }) => ({
|
||||
kickUser: Service.kickUser,
|
||||
toggleVoice: Service.toggleVoice,
|
||||
changeRole: Service.changeRole,
|
||||
roving: Service.roving,
|
||||
}), UserListContainer);
|
||||
|
@ -8,6 +8,7 @@ import mapUser from '/imports/ui/services/user/mapUser';
|
||||
import { EMOJI_STATUSES } from '/imports/utils/statuses';
|
||||
import { makeCall } from '/imports/ui/services/api';
|
||||
import _ from 'lodash';
|
||||
import KEY_CODES from '/imports/utils/keyCodes';
|
||||
|
||||
const APP_CONFIG = Meteor.settings.public.app;
|
||||
const ALLOW_MODERATOR_TO_UNMUTE_AUDIO = APP_CONFIG.allowModeratorToUnmuteAudio;
|
||||
@ -41,18 +42,21 @@ const sortUsersByName = (a, b) => {
|
||||
};
|
||||
|
||||
const sortUsersByEmoji = (a, b) => {
|
||||
const emojiA = a in EMOJI_STATUSES ? EMOJI_STATUSES[a] : a;
|
||||
const emojiB = b in EMOJI_STATUSES ? EMOJI_STATUSES[b] : b;
|
||||
const { status: statusA } = a.emoji;
|
||||
const { status: statusB } = b.emoji;
|
||||
|
||||
if (emojiA && emojiB) {
|
||||
const emojiA = statusA in EMOJI_STATUSES ? EMOJI_STATUSES[statusA] : statusA;
|
||||
const emojiB = statusB in EMOJI_STATUSES ? EMOJI_STATUSES[statusB] : statusB;
|
||||
|
||||
if (emojiA && emojiB && (emojiA !== EMOJI_STATUSES.none && emojiB !== EMOJI_STATUSES.none)) {
|
||||
if (a.emoji.changedAt < b.emoji.changedAt) {
|
||||
return -1;
|
||||
} else if (a.emoji.changedAt > b.emoji.changedAt) {
|
||||
return 1;
|
||||
}
|
||||
} else if (emojiA) {
|
||||
} else if (emojiA && emojiA !== EMOJI_STATUSES.none) {
|
||||
return -1;
|
||||
} else if (emojiB) {
|
||||
} else if (emojiB && emojiB !== EMOJI_STATUSES.none) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
@ -72,7 +76,7 @@ const sortUsersByModerator = (a, b) => {
|
||||
|
||||
const sortUsersByPhoneUser = (a, b) => {
|
||||
if (!a.isPhoneUser && !b.isPhoneUser) {
|
||||
return sortUsersByName(a, b);
|
||||
return 0;
|
||||
} else if (!a.isPhoneUser) {
|
||||
return -1;
|
||||
} else if (!b.isPhoneUser) {
|
||||
@ -211,27 +215,46 @@ const getOpenChats = (chatID) => {
|
||||
.sort(sortChats);
|
||||
};
|
||||
|
||||
const isVoiceOnlyUser = userId => userId.toString().startsWith('v_');
|
||||
|
||||
const getAvailableActions = (currentUser, user, router, isBreakoutRoom) => {
|
||||
const isDialInUser = isVoiceOnlyUser(user.id) || user.isPhoneUser;
|
||||
|
||||
const hasAuthority = currentUser.isModerator || user.isCurrent;
|
||||
const allowedToChatPrivately = !user.isCurrent;
|
||||
|
||||
const allowedToChatPrivately = !user.isCurrent && !isDialInUser;
|
||||
|
||||
const allowedToMuteAudio = hasAuthority
|
||||
&& user.isVoiceUser
|
||||
&& !user.isMuted
|
||||
&& !user.isListenOnly;
|
||||
|
||||
const allowedToUnmuteAudio = hasAuthority
|
||||
&& user.isVoiceUser
|
||||
&& !user.isListenOnly
|
||||
&& user.isMuted
|
||||
&& (ALLOW_MODERATOR_TO_UNMUTE_AUDIO || user.isCurrent);
|
||||
const allowedToResetStatus = hasAuthority && user.emoji.status !== EMOJI_STATUSES.none;
|
||||
|
||||
const allowedToResetStatus = hasAuthority
|
||||
&& user.emoji.status !== EMOJI_STATUSES.none
|
||||
&& !isDialInUser;
|
||||
|
||||
// if currentUser is a moderator, allow kicking other users
|
||||
const allowedToKick = currentUser.isModerator && !user.isCurrent && !isBreakoutRoom;
|
||||
|
||||
const allowedToSetPresenter = currentUser.isModerator && !user.isPresenter;
|
||||
const allowedToSetPresenter = currentUser.isModerator
|
||||
&& !user.isPresenter
|
||||
&& !isDialInUser;
|
||||
|
||||
const allowedToPromote = currentUser.isModerator && !user.isCurrent && !user.isModerator;
|
||||
const allowedToDemote = currentUser.isModerator && !user.isCurrent && user.isModerator;
|
||||
const allowedToPromote = currentUser.isModerator
|
||||
&& !user.isCurrent
|
||||
&& !user.isModerator
|
||||
&& !isDialInUser;
|
||||
|
||||
const allowedToDemote = currentUser.isModerator
|
||||
&& !user.isCurrent
|
||||
&& user.isModerator
|
||||
&& !isDialInUser;
|
||||
|
||||
return {
|
||||
allowedToChatPrivately,
|
||||
@ -278,12 +301,54 @@ const setEmojiStatus = (userId) => { makeCall('setEmojiStatus', userId, 'none');
|
||||
|
||||
const assignPresenter = (userId) => { makeCall('assignPresenter', userId); };
|
||||
|
||||
const kickUser = (userId) => { makeCall('kickUser', userId); };
|
||||
const kickUser = (userId) => {
|
||||
if (isVoiceOnlyUser(userId)) {
|
||||
makeCall('ejectUserFromVoice', userId);
|
||||
} else {
|
||||
makeCall('kickUser', userId);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleVoice = (userId) => { makeCall('toggleVoice', userId); };
|
||||
|
||||
const changeRole = (userId, role) => { makeCall('changeRole', userId, role); };
|
||||
|
||||
const roving = (event, itemCount, changeState) => {
|
||||
if (this.selectedIndex === undefined) {
|
||||
this.selectedIndex = -1;
|
||||
}
|
||||
|
||||
if ([KEY_CODES.ESCAPE, KEY_CODES.TAB].includes(event.keyCode)) {
|
||||
document.activeElement.blur();
|
||||
this.selectedIndex = -1;
|
||||
changeState(this.selectedIndex);
|
||||
}
|
||||
|
||||
if (event.keyCode === KEY_CODES.ARROW_DOWN) {
|
||||
this.selectedIndex += 1;
|
||||
|
||||
if (this.selectedIndex === itemCount) {
|
||||
this.selectedIndex = 0;
|
||||
}
|
||||
|
||||
changeState(this.selectedIndex);
|
||||
}
|
||||
|
||||
if (event.keyCode === KEY_CODES.ARROW_UP) {
|
||||
this.selectedIndex -= 1;
|
||||
|
||||
if (this.selectedIndex < 0) {
|
||||
this.selectedIndex = itemCount - 1;
|
||||
}
|
||||
|
||||
changeState(this.selectedIndex);
|
||||
}
|
||||
|
||||
if ([KEY_CODES.ARROW_RIGHT, KEY_CODES.SPACE].includes(event.keyCode)) {
|
||||
document.activeElement.firstChild.click();
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
setEmojiStatus,
|
||||
assignPresenter,
|
||||
@ -297,4 +362,5 @@ export default {
|
||||
normalizeEmojiName,
|
||||
isMeetingLocked,
|
||||
isPublicChat,
|
||||
roving,
|
||||
};
|
||||
|
@ -1,6 +1,5 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import KEY_CODES from '/imports/utils/keyCodes';
|
||||
import styles from './styles';
|
||||
import UserParticipants from './user-participants/component';
|
||||
import UserMessages from './user-messages/component';
|
||||
@ -24,6 +23,7 @@ const propTypes = {
|
||||
kickUser: PropTypes.func.isRequired,
|
||||
toggleVoice: PropTypes.func.isRequired,
|
||||
changeRole: PropTypes.func.isRequired,
|
||||
roving: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
@ -35,82 +35,6 @@ const defaultProps = {
|
||||
};
|
||||
|
||||
class UserContent extends Component {
|
||||
|
||||
static focusElement(active, element) {
|
||||
const modifiedActive = active;
|
||||
const modifiedElement = element;
|
||||
if (!modifiedActive.getAttribute('role') === 'tabpanel') {
|
||||
modifiedActive.tabIndex = -1;
|
||||
}
|
||||
modifiedElement.tabIndex = 0;
|
||||
modifiedElement.focus();
|
||||
}
|
||||
|
||||
static removeFocusFromChildren(children, numberOfItems) {
|
||||
const modifiedChildren = children;
|
||||
for (let i = 0; i < numberOfItems; i += 1) {
|
||||
modifiedChildren.childNodes[i].tabIndex = -1;
|
||||
}
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.rovingIndex = this.rovingIndex.bind(this);
|
||||
this.focusList = this.focusList.bind(this);
|
||||
this.focusedItemIndex = -1;
|
||||
}
|
||||
|
||||
focusList(list) {
|
||||
const focusList = list;
|
||||
document.activeElement.tabIndex = -1;
|
||||
this.focusedItemIndex = -1;
|
||||
focusList.tabIndex = 0;
|
||||
focusList.focus();
|
||||
}
|
||||
|
||||
|
||||
rovingIndex(event, list, items, numberOfItems) {
|
||||
const active = document.activeElement;
|
||||
const changedItems = items;
|
||||
|
||||
if (event.keyCode === KEY_CODES.TAB) {
|
||||
if (this.focusedItemIndex !== -1) {
|
||||
this.focusedItemIndex = 0;
|
||||
UserContent.removeFocusFromChildren(changedItems, numberOfItems);
|
||||
}
|
||||
}
|
||||
|
||||
if (event.keyCode === KEY_CODES.ESCAPE
|
||||
|| this.focusedItemIndex < 0
|
||||
|| this.focusedItemIndex > numberOfItems) {
|
||||
this.focusList(list);
|
||||
}
|
||||
|
||||
if ([KEY_CODES.ARROW_RIGHT, KEY_CODES.ARROW_SPACE].includes(event.keyCode)) {
|
||||
active.firstChild.click();
|
||||
}
|
||||
|
||||
if (event.keyCode === KEY_CODES.ARROW_DOWN) {
|
||||
this.focusedItemIndex += 1;
|
||||
|
||||
if (this.focusedItemIndex === numberOfItems) {
|
||||
this.focusedItemIndex = 0;
|
||||
}
|
||||
UserContent.focusElement(active, changedItems.childNodes[this.focusedItemIndex]);
|
||||
}
|
||||
|
||||
if (event.keyCode === KEY_CODES.ARROW_UP) {
|
||||
this.focusedItemIndex -= 1;
|
||||
|
||||
if (this.focusedItemIndex < 0) {
|
||||
this.focusedItemIndex = numberOfItems - 1;
|
||||
}
|
||||
|
||||
UserContent.focusElement(active, changedItems.childNodes[this.focusedItemIndex]);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className={styles.content}>
|
||||
@ -119,7 +43,7 @@ class UserContent extends Component {
|
||||
openChats={this.props.openChats}
|
||||
compact={this.props.compact}
|
||||
intl={this.props.intl}
|
||||
rovingIndex={this.rovingIndex}
|
||||
roving={this.props.roving}
|
||||
/>
|
||||
<UserParticipants
|
||||
users={this.props.users}
|
||||
@ -135,8 +59,8 @@ class UserContent extends Component {
|
||||
changeRole={this.props.changeRole}
|
||||
getAvailableActions={this.props.getAvailableActions}
|
||||
normalizeEmojiName={this.props.normalizeEmojiName}
|
||||
rovingIndex={this.rovingIndex}
|
||||
isMeetingLocked={this.props.isMeetingLocked}
|
||||
roving={this.props.roving}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -10,14 +10,18 @@
|
||||
}
|
||||
|
||||
.scrollableList {
|
||||
pointer-events: none;
|
||||
@include elementFocus($list-item-bg-hover);
|
||||
@include scrollbox-vertical($user-list-bg);
|
||||
|
||||
&:active {
|
||||
box-shadow: none;
|
||||
border-radius: none;
|
||||
}
|
||||
}
|
||||
|
||||
.list {
|
||||
margin-left: $md-padding-y;
|
||||
pointer-events: all;
|
||||
margin-bottom: 1px;
|
||||
}
|
||||
|
||||
.smallTitle {
|
||||
|
@ -13,8 +13,8 @@ const propTypes = {
|
||||
intl: PropTypes.shape({
|
||||
formatMessage: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
rovingIndex: PropTypes.func.isRequired,
|
||||
isPublicChat: PropTypes.func.isRequired,
|
||||
roving: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
@ -32,80 +32,102 @@ const listTransition = {
|
||||
};
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
usersTitle: {
|
||||
id: 'app.userList.usersTitle',
|
||||
description: 'Title for the Header',
|
||||
},
|
||||
messagesTitle: {
|
||||
id: 'app.userList.messagesTitle',
|
||||
description: 'Title for the messages list',
|
||||
},
|
||||
participantsTitle: {
|
||||
id: 'app.userList.participantsTitle',
|
||||
description: 'Title for the Users list',
|
||||
},
|
||||
toggleCompactView: {
|
||||
id: 'app.userList.toggleCompactView.label',
|
||||
description: 'Toggle user list view mode',
|
||||
},
|
||||
ChatLabel: {
|
||||
id: 'app.userList.menu.chat.label',
|
||||
description: 'Save the changes and close the settings menu',
|
||||
},
|
||||
ClearStatusLabel: {
|
||||
id: 'app.userList.menu.clearStatus.label',
|
||||
description: 'Clear the emoji status of this user',
|
||||
},
|
||||
MakePresenterLabel: {
|
||||
id: 'app.userList.menu.makePresenter.label',
|
||||
description: 'Set this user to be the presenter in this meeting',
|
||||
},
|
||||
KickUserLabel: {
|
||||
id: 'app.userList.menu.kickUser.label',
|
||||
description: 'Forcefully remove this user from the meeting',
|
||||
},
|
||||
MuteUserAudioLabel: {
|
||||
id: 'app.userList.menu.muteUserAudio.label',
|
||||
description: 'Forcefully mute this user',
|
||||
},
|
||||
UnmuteUserAudioLabel: {
|
||||
id: 'app.userList.menu.unmuteUserAudio.label',
|
||||
description: 'Forcefully unmute this user',
|
||||
},
|
||||
PromoteUserLabel: {
|
||||
id: 'app.userList.menu.promoteUser.label',
|
||||
description: 'Forcefully promote this viewer to a moderator',
|
||||
},
|
||||
DemoteUserLabel: {
|
||||
id: 'app.userList.menu.demoteUser.label',
|
||||
description: 'Forcefully demote this moderator to a viewer',
|
||||
},
|
||||
});
|
||||
|
||||
class UserMessages extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.state = {
|
||||
index: -1,
|
||||
};
|
||||
|
||||
this.openChatRefs = [];
|
||||
this.selectedIndex = -1;
|
||||
|
||||
this.focusOpenChatItem = this.focusOpenChatItem.bind(this);
|
||||
this.changeState = this.changeState.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (!this.props.compact) {
|
||||
this._msgsList.addEventListener(
|
||||
'keydown',
|
||||
event => this.props.rovingIndex(
|
||||
event => this.props.roving(
|
||||
event,
|
||||
this._msgsList,
|
||||
this._msgItems,
|
||||
this.props.openChats.length,
|
||||
this.changeState,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (this.state.index === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.state.index !== prevState.index) {
|
||||
this.focusOpenChatItem(this.state.index);
|
||||
}
|
||||
}
|
||||
|
||||
getOpenChats() {
|
||||
const {
|
||||
openChats,
|
||||
openChat,
|
||||
intl,
|
||||
compact,
|
||||
isPublicChat,
|
||||
} = this.props;
|
||||
|
||||
let index = -1;
|
||||
|
||||
return openChats.map(chat => (
|
||||
<CSSTransition
|
||||
classNames={listTransition}
|
||||
appear
|
||||
enter
|
||||
exit={false}
|
||||
timeout={0}
|
||||
component="div"
|
||||
className={cx(styles.chatsList)}
|
||||
key={chat.id}
|
||||
>
|
||||
<div ref={(node) => { this.openChatRefs[index += 1] = node; }}>
|
||||
<ChatListItem
|
||||
isPublicChat={isPublicChat}
|
||||
compact={compact}
|
||||
openChat={openChat}
|
||||
chat={chat}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
</div>
|
||||
</CSSTransition>
|
||||
));
|
||||
}
|
||||
|
||||
changeState(newIndex) {
|
||||
this.setState({ index: newIndex });
|
||||
}
|
||||
|
||||
focusOpenChatItem(index) {
|
||||
if (!this.openChatRefs[index]) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.openChatRefs[index].firstChild.focus();
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
intl,
|
||||
compact,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div className={styles.messages}>
|
||||
{
|
||||
@ -120,28 +142,9 @@ class UserMessages extends Component {
|
||||
className={styles.scrollableList}
|
||||
ref={(ref) => { this._msgsList = ref; }}
|
||||
>
|
||||
<div ref={(ref) => { this._msgItems = ref; }} className={styles.list}>
|
||||
<TransitionGroup>
|
||||
{openChats.map(chat => (
|
||||
<CSSTransition
|
||||
classNames={listTransition}
|
||||
appear
|
||||
enter
|
||||
exit={false}
|
||||
timeout={0}
|
||||
component="div"
|
||||
className={cx(styles.chatsList)}
|
||||
key={chat.id}
|
||||
>
|
||||
<ChatListItem
|
||||
isPublicChat={isPublicChat}
|
||||
compact={compact}
|
||||
openChat={openChat}
|
||||
chat={chat}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
</CSSTransition>
|
||||
))}
|
||||
<div className={styles.list}>
|
||||
<TransitionGroup ref={(ref) => { this._msgItems = ref; }} >
|
||||
{ this.getOpenChats() }
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -23,7 +23,7 @@ const propTypes = {
|
||||
getAvailableActions: PropTypes.func.isRequired,
|
||||
normalizeEmojiName: PropTypes.func.isRequired,
|
||||
isMeetingLocked: PropTypes.func.isRequired,
|
||||
rovingIndex: PropTypes.func.isRequired,
|
||||
roving: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
@ -48,18 +48,6 @@ const intlMessages = defineMessages({
|
||||
id: 'app.userList.usersTitle',
|
||||
description: 'Title for the Header',
|
||||
},
|
||||
messagesTitle: {
|
||||
id: 'app.userList.messagesTitle',
|
||||
description: 'Title for the messages list',
|
||||
},
|
||||
participantsTitle: {
|
||||
id: 'app.userList.participantsTitle',
|
||||
description: 'Title for the Users list',
|
||||
},
|
||||
toggleCompactView: {
|
||||
id: 'app.userList.toggleCompactView.label',
|
||||
description: 'Toggle user list view mode',
|
||||
},
|
||||
ChatLabel: {
|
||||
id: 'app.userList.menu.chat.label',
|
||||
description: 'Save the changes and close the settings menu',
|
||||
@ -98,88 +86,157 @@ class UserParticipants extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.state = {
|
||||
index: -1,
|
||||
};
|
||||
|
||||
this.userRefs = [];
|
||||
this.selectedIndex = -1;
|
||||
|
||||
this.getScrollContainerRef = this.getScrollContainerRef.bind(this);
|
||||
this.focusUserItem = this.focusUserItem.bind(this);
|
||||
this.changeState = this.changeState.bind(this);
|
||||
this.getUsers = this.getUsers.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (!this.props.compact) {
|
||||
this.refScrollContainer.addEventListener(
|
||||
'keydown',
|
||||
event => this.props.rovingIndex(
|
||||
event => this.props.roving(
|
||||
event,
|
||||
this.refScrollContainer,
|
||||
this.refScrollItems,
|
||||
this.props.users.length,
|
||||
this.changeState,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (this.state.index === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.state.index !== prevState.index) {
|
||||
this.focusUserItem(this.state.index);
|
||||
}
|
||||
}
|
||||
|
||||
getScrollContainerRef() {
|
||||
return this.refScrollContainer;
|
||||
}
|
||||
|
||||
render() {
|
||||
getUsers() {
|
||||
const {
|
||||
users,
|
||||
currentUser,
|
||||
compact,
|
||||
isBreakoutRoom,
|
||||
intl,
|
||||
currentUser,
|
||||
meeting,
|
||||
getAvailableActions,
|
||||
normalizeEmojiName,
|
||||
isMeetingLocked,
|
||||
compact,
|
||||
setEmojiStatus,
|
||||
users,
|
||||
intl,
|
||||
changeRole,
|
||||
assignPresenter,
|
||||
setEmojiStatus,
|
||||
kickUser,
|
||||
toggleVoice,
|
||||
changeRole,
|
||||
} = this.props;
|
||||
|
||||
const userActions =
|
||||
{
|
||||
openChat: {
|
||||
label: () => intl.formatMessage(intlMessages.ChatLabel),
|
||||
handler: (router, user) => router.push(`/users/chat/${user.id}`),
|
||||
icon: 'chat',
|
||||
},
|
||||
clearStatus: {
|
||||
label: () => intl.formatMessage(intlMessages.ClearStatusLabel),
|
||||
handler: user => setEmojiStatus(user.id, 'none'),
|
||||
icon: 'clear_status',
|
||||
},
|
||||
setPresenter: {
|
||||
label: () => intl.formatMessage(intlMessages.MakePresenterLabel),
|
||||
handler: user => assignPresenter(user.id),
|
||||
icon: 'presentation',
|
||||
},
|
||||
kick: {
|
||||
label: user => intl.formatMessage(intlMessages.KickUserLabel, { 0: user.name }),
|
||||
handler: user => kickUser(user.id),
|
||||
icon: 'circle_close',
|
||||
},
|
||||
mute: {
|
||||
label: () => intl.formatMessage(intlMessages.MuteUserAudioLabel),
|
||||
handler: user => toggleVoice(user.id),
|
||||
icon: 'audio_off',
|
||||
},
|
||||
unmute: {
|
||||
label: () => intl.formatMessage(intlMessages.UnmuteUserAudioLabel),
|
||||
handler: user => toggleVoice(user.id),
|
||||
icon: 'audio_on',
|
||||
},
|
||||
promote: {
|
||||
label: user => intl.formatMessage(intlMessages.PromoteUserLabel, { 0: user.name }),
|
||||
handler: user => changeRole(user.id, 'MODERATOR'),
|
||||
icon: 'promote',
|
||||
},
|
||||
demote: {
|
||||
label: user => intl.formatMessage(intlMessages.DemoteUserLabel, { 0: user.name }),
|
||||
handler: user => changeRole(user.id, 'VIEWER'),
|
||||
icon: 'user',
|
||||
},
|
||||
};
|
||||
{
|
||||
openChat: {
|
||||
label: () => intl.formatMessage(intlMessages.ChatLabel),
|
||||
handler: (router, user) => router.push(`/users/chat/${user.id}`),
|
||||
icon: 'chat',
|
||||
},
|
||||
clearStatus: {
|
||||
label: () => intl.formatMessage(intlMessages.ClearStatusLabel),
|
||||
handler: user => setEmojiStatus(user.id, 'none'),
|
||||
icon: 'clear_status',
|
||||
},
|
||||
setPresenter: {
|
||||
label: () => intl.formatMessage(intlMessages.MakePresenterLabel),
|
||||
handler: user => assignPresenter(user.id),
|
||||
icon: 'presentation',
|
||||
},
|
||||
kick: {
|
||||
label: user => intl.formatMessage(intlMessages.KickUserLabel, { 0: user.name }),
|
||||
handler: user => kickUser(user.id),
|
||||
icon: 'circle_close',
|
||||
},
|
||||
mute: {
|
||||
label: () => intl.formatMessage(intlMessages.MuteUserAudioLabel),
|
||||
handler: user => toggleVoice(user.id),
|
||||
icon: 'audio_off',
|
||||
},
|
||||
unmute: {
|
||||
label: () => intl.formatMessage(intlMessages.UnmuteUserAudioLabel),
|
||||
handler: user => toggleVoice(user.id),
|
||||
icon: 'audio_on',
|
||||
},
|
||||
promote: {
|
||||
label: user => intl.formatMessage(intlMessages.PromoteUserLabel, { 0: user.name }),
|
||||
handler: user => changeRole(user.id, 'MODERATOR'),
|
||||
icon: 'promote',
|
||||
},
|
||||
demote: {
|
||||
label: user => intl.formatMessage(intlMessages.DemoteUserLabel, { 0: user.name }),
|
||||
handler: user => changeRole(user.id, 'VIEWER'),
|
||||
icon: 'user',
|
||||
},
|
||||
};
|
||||
|
||||
let index = -1;
|
||||
|
||||
return users.map(user => (
|
||||
<CSSTransition
|
||||
classNames={listTransition}
|
||||
appear
|
||||
enter
|
||||
exit
|
||||
timeout={0}
|
||||
component="div"
|
||||
className={cx(styles.participantsList)}
|
||||
key={user.id}
|
||||
>
|
||||
<div ref={(node) => { this.userRefs[index += 1] = node; }}>
|
||||
<UserListItem
|
||||
compact={compact}
|
||||
isBreakoutRoom={isBreakoutRoom}
|
||||
user={user}
|
||||
currentUser={currentUser}
|
||||
userActions={userActions}
|
||||
meeting={meeting}
|
||||
getAvailableActions={getAvailableActions}
|
||||
normalizeEmojiName={normalizeEmojiName}
|
||||
isMeetingLocked={isMeetingLocked}
|
||||
getScrollContainerRef={this.getScrollContainerRef}
|
||||
/>
|
||||
</div>
|
||||
</CSSTransition>
|
||||
));
|
||||
}
|
||||
|
||||
focusUserItem(index) {
|
||||
if (!this.userRefs[index]) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.userRefs[index].firstChild.focus();
|
||||
}
|
||||
|
||||
changeState(newIndex) {
|
||||
this.setState({ index: newIndex });
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
users,
|
||||
intl,
|
||||
compact,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div className={styles.participants}>
|
||||
@ -196,33 +253,9 @@ class UserParticipants extends Component {
|
||||
tabIndex={0}
|
||||
ref={(ref) => { this.refScrollContainer = ref; }}
|
||||
>
|
||||
<div ref={(ref) => { this.refScrollItems = ref; }} className={styles.list}>
|
||||
<TransitionGroup>
|
||||
{ users.map(user => (
|
||||
<CSSTransition
|
||||
classNames={listTransition}
|
||||
appear
|
||||
enter
|
||||
exit
|
||||
timeout={0}
|
||||
component="div"
|
||||
className={cx(styles.participantsList)}
|
||||
key={user.id}
|
||||
>
|
||||
<UserListItem
|
||||
compact={compact}
|
||||
isBreakoutRoom={isBreakoutRoom}
|
||||
user={user}
|
||||
currentUser={currentUser}
|
||||
userActions={userActions}
|
||||
meeting={meeting}
|
||||
getAvailableActions={getAvailableActions}
|
||||
normalizeEmojiName={normalizeEmojiName}
|
||||
isMeetingLocked={isMeetingLocked}
|
||||
getScrollContainerRef={this.getScrollContainerRef}
|
||||
/>
|
||||
</CSSTransition>
|
||||
))}
|
||||
<div className={styles.list}>
|
||||
<TransitionGroup ref={(ref) => { this.refScrollItems = ref; }}>
|
||||
{ this.getUsers() }
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -25,7 +25,7 @@ const propTypes = {
|
||||
}).isRequired,
|
||||
userActions: PropTypes.shape({}).isRequired,
|
||||
router: PropTypes.shape({}).isRequired,
|
||||
isBreakoutRoom: PropTypes.bool.isRequired,
|
||||
isBreakoutRoom: PropTypes.bool,
|
||||
getAvailableActions: PropTypes.func.isRequired,
|
||||
meeting: PropTypes.shape({}).isRequired,
|
||||
isMeetingLocked: PropTypes.func.isRequired,
|
||||
@ -34,12 +34,10 @@ const propTypes = {
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
shouldShowActions: false,
|
||||
isBreakoutRoom: false,
|
||||
};
|
||||
|
||||
class UserListItem extends Component {
|
||||
|
||||
static createAction(action, ...options) {
|
||||
return (
|
||||
<UserAction
|
||||
|
@ -57,7 +57,6 @@ const propTypes = {
|
||||
|
||||
|
||||
class UserListContent extends Component {
|
||||
|
||||
/**
|
||||
* Return true if the content fit on the screen, false otherwise.
|
||||
*
|
||||
@ -140,8 +139,10 @@ class UserListContent extends Component {
|
||||
};
|
||||
|
||||
const isDropdownVisible =
|
||||
UserListContent.checkIfDropdownIsVisible(dropdownContent.offsetTop,
|
||||
dropdownContent.offsetHeight);
|
||||
UserListContent.checkIfDropdownIsVisible(
|
||||
dropdownContent.offsetTop,
|
||||
dropdownContent.offsetHeight,
|
||||
);
|
||||
|
||||
if (!isDropdownVisible) {
|
||||
const offsetPageTop =
|
||||
@ -201,13 +202,15 @@ class UserListContent extends Component {
|
||||
? intl.formatMessage(messages.presenter)
|
||||
: '';
|
||||
|
||||
const userAriaLabel = intl.formatMessage(messages.userAriaLabel,
|
||||
const userAriaLabel = intl.formatMessage(
|
||||
messages.userAriaLabel,
|
||||
{
|
||||
0: user.name,
|
||||
1: presenter,
|
||||
2: you,
|
||||
3: user.emoji.status,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
const contents = (
|
||||
<div
|
||||
|
@ -1,43 +0,0 @@
|
||||
import React from 'react';
|
||||
import { defineMessages } from 'react-intl';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './styles';
|
||||
|
||||
const intlMessages = defineMessages({
|
||||
participantsTitle: {
|
||||
id: 'app.userList.participantsTitle',
|
||||
description: 'Title for the Users list',
|
||||
},
|
||||
});
|
||||
|
||||
const propTypes = {
|
||||
compact: PropTypes.bool,
|
||||
intl: PropTypes.shape({ formatMessage: PropTypes.func.isRequired }).isRequired,
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
compact: false,
|
||||
};
|
||||
|
||||
const UserListHeader = props => (
|
||||
<div className={styles.header}>
|
||||
{
|
||||
!props.compact ?
|
||||
<div className={styles.headerTitle} role="banner">
|
||||
{props.intl.formatMessage(intlMessages.participantsTitle)}
|
||||
</div> : null
|
||||
}
|
||||
{/* <Button
|
||||
label={intl.formatMessage(intlMessages.toggleCompactView)}
|
||||
hideLabel
|
||||
icon={!this.state.compact ? 'left_arrow' : 'right_arrow'}
|
||||
className={styles.btnToggle}
|
||||
onClick={this.handleToggleCompactView}
|
||||
/> */}
|
||||
</div>
|
||||
);
|
||||
|
||||
UserListHeader.propTypes = propTypes;
|
||||
UserListHeader.defaultProps = defaultProps;
|
||||
|
||||
export default UserListHeader;
|
@ -1,18 +0,0 @@
|
||||
@import "/imports/ui/components/user-list/styles.scss";
|
||||
|
||||
.header {
|
||||
@extend %flex-column;
|
||||
justify-content: left;
|
||||
flex-grow: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding: 0 $md-padding-x;
|
||||
margin: $md-padding-x 0;
|
||||
}
|
||||
|
||||
.headerTitle {
|
||||
flex: 0;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: $color-heading;
|
||||
}
|
@ -80,7 +80,7 @@ export default class ToolbarSubmenu extends Component {
|
||||
{objectsToRender ? objectsToRender.map(obj =>
|
||||
(
|
||||
<ToolbarSubmenuItem
|
||||
label={label}
|
||||
label={obj.value}
|
||||
icon={!customIcon ? obj.icon : null}
|
||||
customIcon={customIcon ? ToolbarSubmenu.getCustomIcon(type, obj) : null}
|
||||
onItemClick={this.onItemClick}
|
||||
|
@ -32,7 +32,7 @@ class Settings {
|
||||
});
|
||||
|
||||
// Sets default locale to browser locale
|
||||
defaultValues.application.locale = navigator.languages[0] ||
|
||||
defaultValues.application.locale = navigator.languages ? navigator.languages[0] : false ||
|
||||
navigator.language ||
|
||||
defaultValues.application.locale;
|
||||
|
||||
|
@ -4,13 +4,13 @@ $border-size-large: 3px;
|
||||
$border-radius: .2rem;
|
||||
|
||||
$sm-padding-x: .75rem;
|
||||
$sm-padding-y: .25rem;
|
||||
$sm-padding-y: .3rem;
|
||||
|
||||
$md-padding-x: 1rem;
|
||||
$md-padding-y: .375rem;
|
||||
$md-padding-y: .45rem;
|
||||
|
||||
$lg-padding-x: 1.25rem;
|
||||
$lg-padding-y: .5rem;
|
||||
$lg-padding-y: 0.6rem;
|
||||
|
||||
$jumbo-padding-x: 3.025rem;
|
||||
$jumbo-padding-y: 1.25rem;
|
||||
$jumbo-padding-y: 1.5rem;
|
||||
|
47
bigbluebutton-html5/package-lock.json
generated
47
bigbluebutton-html5/package-lock.json
generated
@ -215,10 +215,6 @@
|
||||
"integrity": "sha1-lfE2KbEsOlGl0hWr3OKqnzL4B3M=",
|
||||
"dev": true
|
||||
},
|
||||
"attr-accept": {
|
||||
"version": "https://registry.npmjs.org/attr-accept/-/attr-accept-1.1.0.tgz",
|
||||
"integrity": "sha1-tc01In8WOTWo8d4Q7T66FpQfa+Y="
|
||||
},
|
||||
"autoprefixer": {
|
||||
"version": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-7.1.6.tgz",
|
||||
"integrity": "sha1-+5MwOfdK90qD5xIlznjZ/Vi6hNc=",
|
||||
@ -1282,7 +1278,7 @@
|
||||
},
|
||||
"fibers": {
|
||||
"version": "https://registry.npmjs.org/fibers/-/fibers-2.0.0.tgz",
|
||||
"integrity": "sha1-8m0Krx+ZmV++HLPzQO+sCL2p3Es=",
|
||||
"integrity": "sha512-sLxo4rZVk7xLgAjb/6zEzHJfSALx6u6coN1z61XCOF7i6CyTdJawF4+RdpjCSeS8AP66eR2InScbYAz9RAVOgA==",
|
||||
"dev": true
|
||||
},
|
||||
"figures": {
|
||||
@ -1427,7 +1423,7 @@
|
||||
},
|
||||
"glob": {
|
||||
"version": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
|
||||
"integrity": "sha1-wZyd+aAocC1nhhI4SmVSQExjbRU=",
|
||||
"integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==",
|
||||
"requires": {
|
||||
"fs.realpath": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
"inflight": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||
@ -1593,7 +1589,7 @@
|
||||
},
|
||||
"humanize-duration": {
|
||||
"version": "https://registry.npmjs.org/humanize-duration/-/humanize-duration-3.10.1.tgz",
|
||||
"integrity": "sha1-ZbVQwKoJUVbst8NA20TuC99xr0s=",
|
||||
"integrity": "sha512-FHD+u5OKj8TSsSdMHJxSCC78N5Rt4ecil6sWvI+xPbUKhxvHmkKo/V8imbR1m2dXueZYLIl7PcSYX9i/oEiOIA==",
|
||||
"dev": true
|
||||
},
|
||||
"husky": {
|
||||
@ -1629,7 +1625,7 @@
|
||||
},
|
||||
"immutability-helper": {
|
||||
"version": "https://registry.npmjs.org/immutability-helper/-/immutability-helper-2.4.0.tgz",
|
||||
"integrity": "sha1-ANQh4pV8F/DweBR18F/9g35zRY0=",
|
||||
"integrity": "sha512-rW/L/56ZMo9NStMK85kFrUFFGy4NeJbCdhfrDHIZrFfxYtuwuxD+dT3mWMcdmrNO61hllc60AeGglCRhfZ1dZw==",
|
||||
"requires": {
|
||||
"invariant": "https://registry.npmjs.org/invariant/-/invariant-2.2.2.tgz"
|
||||
}
|
||||
@ -3503,6 +3499,12 @@
|
||||
"integrity": "sha1-KYuJ34uTsCIdv0Ia0rGx6iP8Z3c=",
|
||||
"dev": true
|
||||
},
|
||||
"popper.js": {
|
||||
"version": "1.12.9",
|
||||
"resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.12.9.tgz",
|
||||
"integrity": "sha1-DfvC3/lsRRuzMu3Pz6r1ZtMx1bM=",
|
||||
"dev": true
|
||||
},
|
||||
"postcss": {
|
||||
"version": "https://registry.npmjs.org/postcss/-/postcss-6.0.13.tgz",
|
||||
"integrity": "sha1-ueyrTuAMids+yTEUW9lZC78/El8=",
|
||||
@ -3745,14 +3747,6 @@
|
||||
"prop-types": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.0.tgz"
|
||||
}
|
||||
},
|
||||
"react-dropzone": {
|
||||
"version": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-4.2.1.tgz",
|
||||
"integrity": "sha1-aV6AvQsGXxGB5p8tD20dXMcmZMk=",
|
||||
"requires": {
|
||||
"attr-accept": "https://registry.npmjs.org/attr-accept/-/attr-accept-1.1.0.tgz",
|
||||
"prop-types": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.0.tgz"
|
||||
}
|
||||
},
|
||||
"react-intl": {
|
||||
"version": "https://registry.npmjs.org/react-intl/-/react-intl-2.4.0.tgz",
|
||||
"integrity": "sha1-ZsFNyd+ac7L7v71gIXJugKYT6xU=",
|
||||
@ -3784,20 +3778,13 @@
|
||||
"warning": "https://registry.npmjs.org/warning/-/warning-3.0.0.tgz"
|
||||
}
|
||||
},
|
||||
"react-tabs": {
|
||||
"version": "https://registry.npmjs.org/react-tabs/-/react-tabs-2.1.0.tgz",
|
||||
"integrity": "sha1-uhhKUZ4KCAPPeQoesZvE/bpf0Oo=",
|
||||
"react-tippy": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/react-tippy/-/react-tippy-1.2.2.tgz",
|
||||
"integrity": "sha512-xqmymAhKub1JGtLJ+HncUauBpwJjHAp6EkKBLeGtuhneaGQ3GnRp5aEd/YRNc4NmIb6o1lbf/Z6R9G3/VjnjYA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"classnames": "https://registry.npmjs.org/classnames/-/classnames-2.2.5.tgz",
|
||||
"prop-types": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.0.tgz"
|
||||
}
|
||||
},
|
||||
"react-toastify": {
|
||||
"version": "https://registry.npmjs.org/react-toastify/-/react-toastify-2.1.6.tgz",
|
||||
"integrity": "sha1-Gkh/rSekjZ6u9FaDXpVevnmxp5A=",
|
||||
"requires": {
|
||||
"prop-types": "https://registry.npmjs.org/prop-types/-/prop-types-15.6.0.tgz",
|
||||
"react-transition-group": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.2.1.tgz"
|
||||
"popper.js": "1.12.9"
|
||||
}
|
||||
},
|
||||
"react-toggle": {
|
||||
@ -4396,7 +4383,7 @@
|
||||
},
|
||||
"tiny-emitter": {
|
||||
"version": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.0.2.tgz",
|
||||
"integrity": "sha1-gtJ0aKylrejl/R5tIrV91D69+3w="
|
||||
"integrity": "sha512-2NM0auVBGft5tee/OxP4PI3d8WItkDM+fPnaRAVo6xTDI2knbz9eC5ArWGqtGlYqiH3RU5yMpdyTTO7MguC4ow=="
|
||||
},
|
||||
"tinycolor2": {
|
||||
"version": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.1.tgz",
|
||||
|
@ -41,11 +41,12 @@
|
||||
"react-modal": "~3.0.4",
|
||||
"react-router": "~3.0.2",
|
||||
"react-tabs": "~2.1.0",
|
||||
"react-toggle": "~4.0.2",
|
||||
"react-toastify": "~2.1.2",
|
||||
"react-toggle": "~4.0.2",
|
||||
"react-transition-group": "~2.2.1",
|
||||
"redis": "~2.8.0",
|
||||
"string-hash": "~1.1.3",
|
||||
"tippy.js": "^2.0.2",
|
||||
"winston": "~2.4.0",
|
||||
"xml2js": "~0.4.19"
|
||||
},
|
||||
|
@ -31,6 +31,7 @@ acl:
|
||||
- 'toggleVoice'
|
||||
- 'clearPublicChatHistory'
|
||||
- 'changeRole'
|
||||
- 'ejectUserFromVoice'
|
||||
presenter:
|
||||
methods:
|
||||
- 'assignPresenter'
|
||||
|
@ -12,3 +12,4 @@ redis:
|
||||
- 'from-akka-apps-wb-redis-channel'
|
||||
ignored:
|
||||
- 'CheckAlivePongSysMsg'
|
||||
- 'DoLatencyTracerMsg'
|
||||
|
@ -62,13 +62,17 @@
|
||||
"app.presentationUploder.dropzoneLabel": "Drag files here to upload",
|
||||
"app.presentationUploder.browseFilesLabel": "or browse for files",
|
||||
"app.presentationUploder.fileToUpload": "To be uploaded...",
|
||||
"app.presentationUploder.currentBadge": "Current",
|
||||
"app.presentationUploder.genericError": "Ops, something went wrong",
|
||||
"app.presentationUploder.upload.progress": "Uploading ({progress}%)",
|
||||
"app.presentationUploder.upload.413": "File is too large.",
|
||||
"app.presentationUploder.upload.413": "File is too large",
|
||||
"app.presentationUploder.conversion.conversionProcessingSlides": "Processing page {current} of {total}",
|
||||
"app.presentationUploder.conversion.genericConversionStatus": "Converting file...",
|
||||
"app.presentationUploder.conversion.generatingThumbnail": "Generating thumbnails...",
|
||||
"app.presentationUploder.conversion.generatedSlides": "Slides generated...",
|
||||
"app.presentationUploder.conversion.generatingSvg": "Generating SVG images...",
|
||||
"presentationUploder.conversion.generatedSlides": "Slides generated...",
|
||||
"app.presentationUploder.conversion.pageCountExceeded": "Ops, the page count exceeded the limit",
|
||||
"app.presentationUploder.conversion.timeout": "Ops, the conversion is taking too long",
|
||||
"app.polling.pollingTitle": "Polling Options",
|
||||
"app.failedMessage": "Apologies, trouble connecting to the server.",
|
||||
"app.connectingMessage": "Connecting...",
|
||||
|
Loading…
Reference in New Issue
Block a user