Add presenter upload capabilities
This commit is contained in:
parent
3402fcb44f
commit
71fc15a803
4
bigbluebutton-html5/imports/api/presentations/server/methods.js
Normal file → Executable file
4
bigbluebutton-html5/imports/api/presentations/server/methods.js
Normal file → Executable file
@ -1,4 +1,8 @@
|
||||
import { Meteor } from 'meteor/meteor';
|
||||
import removePresentation from './methods/removePresentation';
|
||||
import sharePresentation from './methods/sharePresentation';
|
||||
|
||||
Meteor.methods({
|
||||
removePresentation,
|
||||
sharePresentation,
|
||||
});
|
||||
|
@ -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);
|
||||
}
|
@ -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);
|
||||
}
|
@ -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)
|
||||
|
@ -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 />
|
||||
|
@ -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);
|
@ -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,
|
||||
};
|
@ -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}>
|
@ -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);
|
@ -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,
|
||||
};
|
@ -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
8
bigbluebutton-html5/package.json
Executable file → Normal 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",
|
||||
|
19
bigbluebutton-html5/private/config/public/presentation.yaml
Executable file
19
bigbluebutton-html5/private/config/public/presentation.yaml
Executable 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'
|
Loading…
Reference in New Issue
Block a user