Add presenter upload capabilities

This commit is contained in:
Oswaldo Acauan 2017-05-03 13:36:16 -03:00
parent 3402fcb44f
commit 71fc15a803
13 changed files with 245 additions and 93 deletions

View File

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

View File

@ -0,0 +1,37 @@
import { isAllowedTo } from '/imports/startup/server/userPermissions';
import RedisPubSub from '/imports/startup/server/redis';
import { check } from 'meteor/check';
import Presentations from '/imports/api/presentations';
import Logger from '/imports/startup/server/logger';
export default function removePresentation(credentials, presentationId) {
const REDIS_CONFIG = Meteor.settings.redis;
const CHANNEL = REDIS_CONFIG.channels.toBBBApps.presentation;
const EVENT_NAME = 'remove_presentation';
if (!isAllowedTo('removePresentation', credentials)) {
throw new Meteor.Error('not-allowed', `You are not allowed to removePresentation`);
}
const { meetingId, requesterUserId, requesterToken } = credentials;
check(meetingId, String);
check(requesterUserId, String);
check(presentationId, String);
const currentPresentation = Presentations.findOne({
meetingId: meetingId,
'presentation.id': presentationId,
});
if (currentPresentation.name === 'default.pdf') {
throw new Meteor.Error('not-allowed', `You are not allowed to remove the default slide`);
}
let payload = {
meeting_id: currentPoll.meetingId,
presentation_id: presentationId,
};
return RedisPubSub.publish(CHANNEL, EVENT_NAME, payload);
}

View File

@ -0,0 +1,28 @@
import { isAllowedTo } from '/imports/startup/server/userPermissions';
import RedisPubSub from '/imports/startup/server/redis';
import { check } from 'meteor/check';
export default function sharePresentation(credentials, presentationId, shouldShare = true) {
const REDIS_CONFIG = Meteor.settings.redis;
const CHANNEL = REDIS_CONFIG.channels.toBBBApps.presentation;
const EVENT_NAME = 'share_presentation';
if (!isAllowedTo('sharePresentation', credentials)) {
throw new Meteor.Error('not-allowed', `You are not allowed to sharePresentation`);
}
const { meetingId, requesterUserId } = credentials;
check(meetingId, String);
check(requesterUserId, String);
check(presentationId, String);
check(shouldShare, Boolean);
let payload = {
meeting_id: currentPoll.meetingId,
presentation_id: presentationId,
share: shouldShare,
};
return RedisPubSub.publish(CHANNEL, EVENT_NAME, payload);
}

View File

@ -9,6 +9,9 @@ const presenter = {
subscribePoll: true,
subscribeAnswers: true,
// presentation
removePresentation: true,
sharePresentation: true,
};
// holds the values for whether the moderator user is allowed to perform an action (true)

View File

@ -5,6 +5,9 @@ import ActionsDropdown from './actions-dropdown/component';
import JoinAudioOptionsContainer from '../audio/audio-menu/container';
import MuteAudioContainer from './mute-button/container';
import { showModal } from '../app/service';
import PresentationUploderContainer from '../presentation/presentation-uploader/container'
export default class ActionsBar extends Component {
constructor(props) {
super(props);
@ -17,6 +20,7 @@ export default class ActionsBar extends Component {
<div className={styles.actionsbar}>
<div className={styles.left}>
<ActionsDropdown {...{isUserPresenter}}/>
<button onClick={()=>{showModal(<PresentationUploderContainer/>)}}>UPLOAD</button>
</div>
<div className={styles.center}>
<MuteAudioContainer />

View File

@ -1,23 +0,0 @@
import React, { Component, PropTypes } from 'react';
import { createContainer } from 'meteor/react-meteor-data';
import Service from './service';
import PresentationUploader from './component';
class PresentationUploaderContainer extends Component {
constructor(props) {
super(props);
}
render() {
return (
<PresentationUploader {...this.props}>
{this.props.children}
</PresentationUploader>
);
}
}
export default createContainer(() => ({
presentations: Service.getPresentations(),
}), PresentationUploaderContainer);

View File

@ -1,41 +0,0 @@
import Presentations from '/imports/api/presentations';
import Auth from '/imports/ui/services/auth';
import { callServer } from '/imports/ui/services/api';
const PRESENTATION_UPLOAD_ENDPOINT = '/bigbluebutton/presentation/upload';
const getPresentations = () =>
Presentations
.find()
.fetch()
.map(p => ({
_id: p._id,
id: p.presentation.id,
filename: p.presentation.name,
uploadedAt: new Date(),
isCurrent: p.presentation.current,
isUploaded: true,
isProcessed: true,
}));
const uploadPresentation = file => {
var data = new FormData();
data.append('file', file);
data.append('conference', Auth.meetingID);
data.append('room', Auth.meetingID);
return fetch(PRESENTATION_UPLOAD_ENDPOINT, {
method: 'POST',
body: data,
});
};
const updatePresentations = newState => {
};
export default {
getPresentations,
updatePresentations,
};

View File

@ -1,18 +1,14 @@
import React, { Component } from 'react';
import { defineMessages, injectIntl, FormattedDate } from 'react-intl';
import update from 'react-addons-update';
import Modal from '/imports/ui/components/modal/component';
import update from 'immutability-helper';
import ModalFullscreen from '/imports/ui/components/modal/component';
import Icon from '/imports/ui/components/icon/component';
import ButtonBase from '/imports/ui/components/button/base/component';
import Checkbox from '/imports/ui/components/checkbox/component';
import Dropzone from 'react-dropzone';
import styles from './styles.scss';
import cx from 'classnames';
import SUPPORTED_FILE_MIMES from '/imports/utils/supportedFileMimeTypes';
const FILE_SIZE_MIN = 0;
const FILE_SIZE_MAX = Infinity;
const intlMessages = defineMessages({
title: {
id: 'app.presentationUploder.title',
@ -56,6 +52,7 @@ class PresentationUploder extends Component {
this.state = {
presentations: props.presentations,
isProcessing: false,
};
this.handleConfirm = this.handleConfirm.bind(this);
@ -67,6 +64,13 @@ class PresentationUploder extends Component {
handleConfirm() {
const { presentations } = this.state;
this.setState({ isProcessing: true });
return this.props.handleSave(presentations)
.then(() => {
this.setState({ isProcessing: false });
});
}
handleDismiss() {
@ -75,6 +79,8 @@ class PresentationUploder extends Component {
handleFiledrop(files) {
let presentationsToUpload = files.map(file => ({
id: file.name,
file: file,
filename: file.name,
uploadedAt: new Date(),
isCurrent: false,
@ -126,24 +132,27 @@ class PresentationUploder extends Component {
render() {
const { intl } = this.props;
const { isProcessing } = this.state;
return (
<Modal
<ModalFullscreen
title={intl.formatMessage(intlMessages.title)}
confirm={{
callback: this.handleConfirm,
label: intl.formatMessage(intlMessages.confirmLabel),
description: intl.formatMessage(intlMessages.confirmDesc),
disabled: isProcessing,
}}
dismiss={{
callback: this.handleDismiss,
label: intl.formatMessage(intlMessages.dismissLabel),
description: intl.formatMessage(intlMessages.dismissDesc),
disabled: isProcessing,
}}>
<p>{intl.formatMessage(intlMessages.message)}</p>
{this.renderPresentationList()}
{this.renderDropzone()}
</Modal>
</ModalFullscreen>
);
}
@ -154,15 +163,19 @@ class PresentationUploder extends Component {
.sort((a, b) => a.uploadedAt.getTime() - b.uploadedAt.getTime());
return (
<table className={styles.table}>
<tbody>
{ presentationsSorted.map(item => this.renderPresentationItem(item))}
</tbody>
</table>
<div className={styles.fileList}>
<table className={styles.table}>
<tbody>
{ presentationsSorted.map(item => this.renderPresentationItem(item))}
</tbody>
</table>
</div>
);
}
renderPresentationItem(item) {
const { isProcessing } = this.state;
let itemClassName = {};
itemClassName[styles.tableItemNew] = !item.isUploaded && !item.isProcessed;
@ -171,11 +184,11 @@ class PresentationUploder extends Component {
return (
<tr
key={item._id}
key={item.id}
className={cx(itemClassName)}
>
<td className={styles.tableItemIcon}>
<Icon iconName={'undecided'}/>
<Icon iconName={'file'}/>
</td>
<th className={styles.tableItemName}>
<span>{item.filename}</span>
@ -198,13 +211,15 @@ class PresentationUploder extends Component {
}
</td>
<td className={styles.tableItemActions}>
<ButtonBase onClick={() => this.handleCurrentChange(item)}>
<Icon iconName={!item.isCurrent ? 'circle' : 'check'}/>
</ButtonBase>
<Checkbox
disabled={isProcessing}
checked={item.isCurrent}
onChange={() => this.handleCurrentChange(item)}
/>
<ButtonBase
disabled={item.isCurrent || item.filename === 'default.pdf'}
disabled={isProcessing || item.isCurrent || item.filename === 'default.pdf'}
onClick={() => this.handleRemove(item)}>
<Icon iconName={'circle-minus'}/>
<Icon iconName={'close'}/>
</ButtonBase>
</td>
</tr>
@ -212,18 +227,24 @@ class PresentationUploder extends Component {
}
renderDropzone() {
const { intl } = this.props;
const {
intl,
fileSizeMin,
fileSizeMax,
fileValidMimeTypes,
} = this.props;
return (
<Dropzone
className={styles.dropzone}
activeClassName={styles.dropzoneActive}
rejectClassName={styles.dropzoneReject}
accept={SUPPORTED_FILE_MIMES.join()}
minSize={FILE_SIZE_MIN}
maxSize={FILE_SIZE_MAX}
accept={fileValidMimeTypes.join()}
minSize={fileSizeMin}
maxSize={fileSizeMax}
disablePreview={true}
onDrop={this.handleFiledrop}
onDragStart={this.handleDragStart}
>
<Icon className={styles.dropzoneIcon} iconName={'undecided'}/>
<p className={styles.dropzoneMessage}>

View File

@ -0,0 +1,38 @@
import React, { Component, PropTypes } from 'react';
import { createContainer } from 'meteor/react-meteor-data';
import Service from './service';
import PresentationUploader from './component';
class PresentationUploaderContainer extends Component {
constructor(props) {
super(props);
}
render() {
return (
<PresentationUploader {...this.props}>
{this.props.children}
</PresentationUploader>
);
}
}
export default createContainer(() => {
const PRESENTATION_CONFIG = Meteor.settings.public.presentation;
const currentPresentations = Service.getPresentations();
return {
presentations: currentPresentations,
fileSizeMin: PRESENTATION_CONFIG.uploadSizeMin,
fileSizeMax: PRESENTATION_CONFIG.uploadSizeMax,
fileValidMimeTypes: PRESENTATION_CONFIG.uploadValidMimeTypes,
handleSave: (presentations) => {
return Service.persistPresentationChanges(
currentPresentations,
presentations,
PRESENTATION_CONFIG.uploadEndpoint
);
},
};
}, PresentationUploaderContainer);

View File

@ -0,0 +1,55 @@
import Presentations from '/imports/api/presentations';
import Auth from '/imports/ui/services/auth';
import { makeCall } from '/imports/ui/services/api';
const getPresentations = () =>
Presentations
.find()
.fetch()
.map(p => ({
_id: p._id,
id: p.presentation.id,
filename: p.presentation.name,
uploadedAt: new Date(),
isCurrent: p.presentation.current,
isUploaded: true,
isProcessed: true,
}));
const uploadPresentation = (file, meetingID, endpoint) => {
var data = new FormData();
data.append('Filename', file.filename);
data.append('presentation_name', file.filename);
data.append('fileUpload', file);
data.append('conference', meetingID);
data.append('room', meetingID);
/* TODO: Should we do the request on the html5 server instead of the client? */
return fetch(endpoint, {
method: 'POST',
body: data,
});
};
const removePresentation = (presentationID) => {
return makeCall('removePresentation');
};
const persistPresentationChanges = (oldState, newState, uploadEndpoint) => {
const presentationsToUpload = newState.filter(_ => !oldState.includes(_));
const presentationsToDelete = oldState.filter(_ => !newState.includes(_));
return new Promise((resolve, reject) =>
Promise.resolve().then(
Promise.all(presentationsToUpload.map(p =>
uploadPresentation(p.file, Auth.meetingID, uploadEndpoint)))
).then(
Promise.all(presentationsToDelete.map(p => removePresentation(p.filename)))
).then(() => makeCall('sharePresentation'))
);
};
export default {
getPresentations,
persistPresentationChanges,
};

View File

@ -1,4 +1,9 @@
@import "../../stylesheets/variables/_all";
@import "../../../stylesheets/variables/_all";
@import "../../../stylesheets/mixins/_scrollable";
.fileList {
@include scrollbox-vertical();
}
.table {
width: 100%;

8
bigbluebutton-html5/package.json Executable file → Normal file
View File

@ -15,16 +15,19 @@
"grunt-cli": "~1.2.0",
"hiredis": "^0.5.0",
"history": "~3.3.0",
"immutability-helper": "^2.2.0",
"lodash": "~4.17.4",
"meteor-node-stubs": "^0.2.3",
"node-sass": "~3.8.0",
"probe-image-size": "~2.1.1",
"react-addons-shallow-compare": "^15.4.2",
"react": "~15.4.2",
"react-addons-css-transition-group": "~15.4.2",
"react-addons-pure-render-mixin": "~15.4.2",
"react-addons-shallow-compare": "^15.4.2",
"react-autosize-textarea": "~0.3.2",
"react-color": "^2.11.1",
"react-dom": "~15.4.2",
"react-dropzone": "^3.13.1",
"react-intl": "~2.1.3",
"react-modal": "~1.7.1",
"react-router": "~3.0.2",
@ -32,8 +35,7 @@
"react-toggle": "^2.2.0",
"redis": "^2.6.2",
"winston": "^2.3.1",
"xml2js": "^0.4.17",
"lodash": "~4.17.4"
"xml2js": "^0.4.17"
},
"devDependencies": {
"autoprefixer": "^6.3.6",

View File

@ -0,0 +1,19 @@
presentation:
uploadEndpoint: '/bigbluebutton/presentation/upload'
uploadSizeMin: 0
uploadSizeMax: 50000000
uploadValidMimeTypes:
- 'application/vnd.ms-excel'
- 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
- 'application/msword'
- 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
- 'application/vnd.ms-powerpoint'
- 'application/vnd.openxmlformats-officedocument.presentationml.presentation'
- 'application/vnd.oasis.opendocument.text'
- 'application/rtf'
- 'text/plain'
- 'application/vnd.oasis.opendocument.spreadsheet'
- 'application/vnd.oasis.opendocument.presentation'
- 'application/pdf'
- 'image/jpeg'
- 'image/png'