diff --git a/components/User/UserProfile/UserGroups.js b/components/User/UserProfile/UserGroups.js
index 7dc0436e2..9b67853f1 100644
--- a/components/User/UserProfile/UserGroups.js
+++ b/components/User/UserProfile/UserGroups.js
@@ -38,16 +38,16 @@ class UserGroups extends React.Component {
handleClickOnEditGroup(e) {
e.preventDefault();
- console.log('handleClickOnEditGroup:', e.target.attributes.name.nodeValue);
+ // console.log('handleClickOnEditGroup:', e.target.attributes.name.value);
- const action = e.target.attributes.name.nodeValue; //eg. changeGroup_2
+ const action = e.target.attributes.name.value; //eg. changeGroup_2
const groupid = action.split('_')[1];
let group = this.props.groups.find((group) => {
return group._id.toString() === groupid;
});
- console.log('handleClickOnEditGroup: use group', group);
+ // console.log('handleClickOnEditGroup: use group', group);
this.context.executeAction(updateUsergroup, {group: group, offline: false});
@@ -58,9 +58,9 @@ class UserGroups extends React.Component {
handleClickOnRemoveGroup(e) {
e.preventDefault();
- console.log('handleClickOnRemoveGroup:', e.target.attributes.name.nodeValue);
+ console.log('handleClickOnRemoveGroup:', e.target.attributes.name.value);
- const action = e.target.attributes.name.nodeValue; //eg. changeGroup_2
+ const action = e.target.attributes.name.value; //eg. changeGroup_2
const groupid = action.split('_')[1];
this.context.executeAction(deleteUsergroup, {groupid: groupid});
@@ -68,9 +68,9 @@ class UserGroups extends React.Component {
handleClickOnLeaveGroup(e) {
e.preventDefault();
- console.log('handleClickOnLeaveGroup:', e.target.attributes.name.nodeValue);
+ console.log('handleClickOnLeaveGroup:', e.target.attributes.name.value);
- const action = e.target.attributes.name.nodeValue; //eg. changeGroup_2
+ const action = e.target.attributes.name.value; //eg. changeGroup_2
const groupid = action.split('_')[1];
this.context.executeAction(leaveUsergroup, {groupid: groupid});
diff --git a/components/User/UserProfile/UserProfile.js b/components/User/UserProfile/UserProfile.js
index 492db5a10..a4db83cc7 100644
--- a/components/User/UserProfile/UserProfile.js
+++ b/components/User/UserProfile/UserProfile.js
@@ -31,7 +31,8 @@ class UserProfile extends React.Component {
})
.then(() => {
},() => {//dismiss function
- if(this.props.IntlStore.currentLocale !== getIntlLanguage()) //user to reload page beacuse of cookie change
+ if(this.props.IntlStore.currentLocale !== getIntlLanguage() ||
+ (this.props.UserProfileStore.category === categories.categories[0] && this.props.UserProfileStore.categoryItem === categories.settings[0]) ) //user to reload page beacuse of cookie change or picture change
window.location.reload();
}).catch(swal.noop);
if (this.props.UserProfileStore.dimmer.userdeleted === true)
diff --git a/components/common/LanguageDropdown.js b/components/common/LanguageDropdown.js
index 4d540eea0..71aa37549 100644
--- a/components/common/LanguageDropdown.js
+++ b/components/common/LanguageDropdown.js
@@ -42,12 +42,16 @@ class LanguageDropdown extends React.Component {
});
let languageOptions =
+
English
German
+
+ Dutch
+
Greek
diff --git a/components/common/UploadMediaModal.js b/components/common/UploadMediaModal.js
new file mode 100644
index 000000000..dcfde42c8
--- /dev/null
+++ b/components/common/UploadMediaModal.js
@@ -0,0 +1,265 @@
+import React from 'react';
+import Dropzone from 'react-dropzone';
+import FocusTrap from 'focus-trap-react';
+import {Button, Icon, Image, Input, Modal, Divider, TextArea, Dropdown, Popup} from 'semantic-ui-react';
+import uploadMediaFiles from '../../actions/media/uploadMediaFiles';
+import { connectToStores, provideContext } from 'fluxible-addons-react';
+import {isEmpty} from '../../common';
+
+class UploadMediaModal extends React.Component {
+
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ openModal: false,
+ activeTrap: false,
+ active: true,
+ files: [],
+ license: false,
+ licenseValue: 'CC0',
+ copyrightHolder: '',
+ alt: '',
+ title: '',
+ isLoading: false
+ };
+
+ this.handleOpen = this.handleOpen.bind(this);
+ this.handleClose = this.handleClose.bind(this);
+ this.unmountTrap = this.unmountTrap.bind(this);
+ this.showLicense = this.showLicense.bind(this);
+ this.submitPressed = this.submitPressed.bind(this);
+ }
+
+ componentDidUpdate(prevProps, prevState) {
+ if(prevState.files !== this.state.files && !isEmpty(this.state.files)){
+ //TODO Bad approach to set focus, but setting it without timeout does not work
+ setTimeout(() => {
+ this.refs.UploadMediaModalSaveButton.focus();
+ }, 100);
+ }
+ }
+
+ handleChange(e) {
+ this.setState({ [e.target.name]: e.target.value });
+ }
+
+ handleOpen(){
+ $('#app').attr('aria-hidden','true');
+ this.setState({
+ modalOpen:true,
+ activeTrap:true,
+ isLoading: false
+ });
+ }
+
+ handleClose(){
+ $('#app').attr('aria-hidden','false');
+ this.setState({
+ modalOpen:false,
+ activeTrap: false,
+ files: [],
+ license: false,
+ licenseValue: 'CC0',
+ copyrightHolder: '',
+ alt: '',
+ title: '',
+ isLoading: false
+ });
+ }
+
+ unmountTrap(){
+ if(this.state.activeTrap){
+ this.setState({ activeTrap: false });
+ $('#app').attr('aria-hidden','false');
+ }
+ }
+
+ showLicense() {
+ this.setState({
+ license: true
+ });
+ }
+
+ onDrop(files) {
+ this.setState({
+ files
+ });
+ }
+
+ changeLicense(event, data) {
+ this.setState({
+ licenseValue: data.value
+ });
+ }
+
+ submitPressed(e) {
+ e.preventDefault();
+ let that = this;
+ if(this.state.copyrightHolder === undefined || this.state.copyrightHolder === ''){this.state.copyrightHolder = this.props.userFullName;}
+ console.log('copyrighthodler: ' + this.state.copyrightHolder);
+ let payload = {
+ type: this.state.files[0].type,
+ license: this.state.licenseValue,
+ copyrightHolder: this.state.copyrightHolder,
+ title: this.state.title || this.state.files[0].name,
+ text: this.state.alt,
+ filesize: this.state.files[0].size,
+ filename: this.state.files[0].name,
+ bytes: null
+ };
+ console.log(this.state, payload);
+
+ let reader = new FileReader();
+
+ reader.onloadend = function (evt) {
+ console.log('read total length from file: ', reader.result.length, evt.target.readyState);
+
+ if (evt.target.readyState === FileReader.DONE) {
+ payload.bytes = reader.result;
+ that.context.executeAction(uploadMediaFiles, payload);
+
+ that.setState({
+ isLoading: true
+ });
+ }
+ };
+
+ reader.onerror = (err) => {
+ swal({
+ title: 'Error',
+ text: 'Reading the selected file failed. Check you privileges and try again.',
+ type: 'error',
+ confirmButtonText: 'Close',
+ confirmButtonClass: 'negative ui button',
+ allowEscapeKey: false,
+ allowOutsideClick: false,
+ buttonsStyling: false
+ })
+ .then(() => {
+ return true;
+ });
+ };
+
+ reader.readAsDataURL(this.state.files[0]);
+
+ return false;
+ }
+
+ render() {
+ this.context.getUser().username;
+ let dropzone = '';
+ if(this.state.files.length < 1){
+ dropzone =
+
+
+ Drop a file directly from your filebrowser here to upload it.
Alternatively, click or anywhere around this text to select a file to upload.
+
+
;
+ } else { //TODO Implement a switch-case statement for other media files. Currently only works for images.
+ dropzone =
+
+
+
+
+
Not the right image? Click on the image to upload another one.
;
+ }
+ //let heading = 'Upload a media file';
+ let heading = 'Add image - upload image file from your computer';
+ let content =
+
+ {dropzone}
+
;
+ let saveHandler= this.showLicense;
+ let licenseBoxes = '';
+ let submitButtonText = 'Next';
+ let submitButtonIcon = 'arrow right';
+ if(this.state.license){
+ heading = 'License information';
+ //licenseBoxes = (this.state.licenseValue !== 'CC0') ?
: '';
+ licenseBoxes = (this.state.licenseValue !== 'CC0') ?
: '';
+ content =
;
+ saveHandler = (() => {$('#UploadFormSubmitButton').click();});
+ submitButtonText = 'Upload';
+ submitButtonIcon = 'upload';
+ }
+
+ const buttonColorBlack = {
+ color: 'black'
+ };
+
+ return (
+
+
+ Add Image
+
+ }
+ open={this.state.modalOpen}
+ onClose={this.handleClose}
+ size="small"
+ role="dialog"
+ id="UploadMediaModal"
+ aria-labelledby="UploadMediaModalHeader"
+ aria-describedby="UploadMediaModalDescription"
+ tabIndex="0">
+
+
+
+
+ {content}
+ {(this.state.isLoading === true) ? : ''}
+
+
+
+
+
+
+
+ );
+ }
+}
+
+UploadMediaModal.contextTypes = {
+ executeAction: React.PropTypes.func.isRequired,
+ getUser: React.PropTypes.func
+};
+
+export default UploadMediaModal;
diff --git a/components/common/UserPicture.js b/components/common/UserPicture.js
index 5cf038012..f10714ac7 100644
--- a/components/common/UserPicture.js
+++ b/components/common/UserPicture.js
@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { Component } from 'react';
import Identicons from 'identicons-react';
import classNames from 'classnames';
@@ -14,13 +14,10 @@ import classNames from 'classnames';
* avatar:
*/
-class UserPicture extends React.Component {
- componentDidMount() {
- }
-
- componentDidUpdate() {
- }
+//change by Ted for testing framework
+//class UserPicture extends Component {
+class UserPicture extends React.Component {
render() {
let classes = classNames({
'ui': true,
@@ -51,13 +48,9 @@ class UserPicture extends React.Component {
} else
picture =
![]({)
;
return (
-
{ this.props.link ?
picture : picture}
+
{ this.props.link ?
picture : picture}
);
}
}
-UserPicture.contextTypes = {
- executeAction: React.PropTypes.func.isRequired
-};
-
export default UserPicture;
diff --git a/components/webrtc/Chat.js b/components/webrtc/Chat.js
new file mode 100644
index 000000000..142b28ffd
--- /dev/null
+++ b/components/webrtc/Chat.js
@@ -0,0 +1,155 @@
+import React from 'react';
+import { Grid, Divider, Form, Button, Label, Popup, Message, Comment } from 'semantic-ui-react';
+
+class Chat extends React.Component {
+
+/*
+ Props:
+ isInitiator - var
+ height - var
+ sendRTCMessage - func
+ presenterID - var
+ myID - var
+ pcs - var
+*/
+
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ commentList: {},//{timestamp: {peer: username, message: text},timestamp: {peer: username, message: text}}
+ charCount: 0,
+ TextAreaContent: ''
+ };
+ this.textInputLength = 2000;
+ }
+
+ updateCharCount(e, {name, value}){
+ this.setState({charCount: value.length, [name]: value});
+ }
+
+ sendMessage(event) {
+ event.preventDefault();
+ if(this.state.TextAreaContent.length < 15){
+ swal({
+ titleText: 'Message too short',
+ text: 'The message you tried to send is too short. Please write more than 15 characters.',
+ type: 'warning',
+ confirmButtonColor: '#3085d6',
+ confirmButtonText: 'Okay',
+ allowOutsideClick: false
+ });
+ } else {
+ this.props.sendRTCMessage('message', this.state.TextAreaContent, this.props.presenterID);
+ this.addMessage({sender: this.props.myID, data: this.state.TextAreaContent}, true);
+ this.setState({charCount: 0, TextAreaContent: ''});
+ }
+ return false;
+ }
+
+ addMessage(data, fromMyself = false, peerID = null) {
+ let currentTime = new Date().getTime();
+ let newPost = {};
+ newPost[currentTime] = {};
+ if(!fromMyself)
+ newPost[currentTime].peer = this.props.pcs[peerID].username || Object.keys(this.props.pcs).indexOf(data.sender);
+ else
+ newPost[currentTime].peer = 'Me';
+ newPost[currentTime].message = data.data;
+ this.setState((prevState) => {
+ return {commentList: Object.assign({}, prevState.commentList, newPost)};
+ });
+ }
+
+ clearMessageList() {
+ this.setState({commentList: {}});
+ }
+
+ openMessageInModal(peer, message) {
+ swal({
+ titleText: 'Message from ' + peer,
+ html: '
' + escapeHTML(message) + '
',
+ confirmButtonColor: '#3085d6',
+ confirmButtonText: 'Close',
+ allowOutsideClick: false,
+ allowEscapeKey: false
+ });
+ function escapeHTML(text) {
+ return text.replace(/&/g,'&').replace(//g,'>');
+ }
+ }
+
+ render() {
+ let messages = [];
+ for(let i in this.state.commentList) {
+ let author = this.state.commentList[i].peer.toString();
+ let message = this.state.commentList[i].message;
+ messages.push(
+
+
+
+
+ {author}, {new Date(parseInt(i)).toLocaleTimeString('en-GB', { hour12: false, hour: 'numeric', minute: 'numeric'})}
+
+ {message}
+
+ {(this.props.isInitiator) ? (
+
+ Enlarge
+
+ ) : ('')}
+
+
+
+
+ }
+ content={this.props.isInitiator ? 'Answer this questions by speaking to your audience' : 'The presenter has recieved your message and may answer via voice'}
+ position='bottom right'
+ />);
+ }
+
+ return (
+
+ {(this.props.isInitiator) ? (
+
+
+ Questions from Audience:
+ {messages}
+
+
+
+
+
+ ) : (
+
+
+ Your Questions ({this.props.myName}):
+ {messages}
+
+
+
+
+
+
+ )}
+
+ );
+ }
+}
+
+Chat.contextTypes = {
+ executeAction: React.PropTypes.func.isRequired
+};
+
+export default Chat;
diff --git a/components/webrtc/SpeechRecognition.js b/components/webrtc/SpeechRecognition.js
new file mode 100644
index 000000000..83b38dd56
--- /dev/null
+++ b/components/webrtc/SpeechRecognition.js
@@ -0,0 +1,232 @@
+import React from 'react';
+import { Input, Button, Label } from 'semantic-ui-react';
+import ISO6391 from 'iso-639-1';
+
+class SpeechRecognition extends React.Component {
+
+/*
+ Props:
+ isInitiator - var
+ sendRTCMessage - func
+ showInviteModal - func
+ subtitle - var
+*/
+
+ constructor(props) {
+ super(props);
+
+ this.state={
+ speechRecognitionDisabled: false,
+ subtitle: ''
+ };
+
+ this.recognition = undefined;
+ }
+
+ getSubtitle() {
+ return this.state.subtitle;
+ }
+
+ componentDidUpdate() {
+ $('#input_subtitle').animate({
+ scrollLeft: $('#input_subtitle')[0].scrollLeft+1000
+ }, 1000);
+ }
+
+ componentWillReceiveProps(nextProps) {
+ if(this.state.subtitle !== nextProps.subtitle && nextProps.subtitle !== '')
+ this.setState({subtitle: nextProps.subtitle});
+ }
+
+ activateSpeechRecognition() {
+ console.log('Activating Speech Recognition...');
+ let that = this;
+ let final_transcript = '';
+
+ let first_char = /\S/;
+
+ function capitalize(s) {
+ return s.replace(first_char, (m) => {
+ return m.toUpperCase();
+ });
+ }
+
+ if (window.hasOwnProperty('webkitSpeechRecognition')) {
+ that.recognition = new webkitSpeechRecognition();
+ } else if (window.hasOwnProperty('SpeechRecognition')) {
+ that.recognition = new SpeechRecognition();
+ }
+
+ if (that.recognition) {
+ that.recognition.continuous = true;
+ that.recognition.interimResults = true;
+ that.recognition.lang = navigator.language || navigator.userLanguage;
+ that.recognition.maxAlternatives = 0;
+ that.recognition.start();
+
+ that.recognition.onresult = function (event) {
+
+ let interim_transcript = '';
+ if (typeof (event.results) == 'undefined') {
+ that.recognition.onend = null;
+ that.recognition.stop();
+ console.warn('error:', e);
+
+ swal({
+ titleText: 'Speech recognition disabled',
+ text: 'There was an error with the speech recognition API. This should be an edge case. You may restart it.',
+ type: 'info',
+ showCancelButton: true,
+ confirmButtonColor: '#3085d6',
+ cancelButtonColor: '#d33',
+ confirmButtonText: 'Enable again',
+ cancelButtonText: 'Keep it disabled',
+ allowOutsideClick: false,
+ allowEscapeKey: false,
+ }).then(() => {}, (dismiss) => {
+ if (dismiss === 'cancel') {
+ that.recognition.start();
+ }
+ }).then(() => {
+ });
+
+ return;
+ }
+ for (let i = event.resultIndex; i < event.results.length; ++i) {
+ if (event.results[i].isFinal) {
+ final_transcript += event.results[i][0].transcript;
+ } else {
+ interim_transcript += event.results[i][0].transcript;
+ }
+ }
+ final_transcript = capitalize(final_transcript);
+ // console.log('Final text: ', final_transcript);
+ // console.log('Interim text: ', interim_transcript);
+
+ let m = (final_transcript || interim_transcript);
+ let tosend = m.substr((m.length-300) > 0 ? m.length-300 : 0, 300);
+ that.props.sendRTCMessage('subtitle', tosend);
+ that.setState({subtitle: tosend});
+ };
+
+ that.recognition.onerror = function (e) {
+ if(e.type === 'error' && e.error !== 'no-speech'){
+ console.log('SpeechRecognition error: ', e);
+ that.disableSpeechRecognition(that);
+ swal({
+ titleText: 'Speech recognition disabled',
+ text: 'An error occured and we had to disable speech recognition. We are sorry about it, but speech recognition is a highly experimental feature. Your listeners will not recieve any transcript anymore.',
+ type: 'error',
+ confirmButtonColor: '#3085d6',
+ confirmButtonText: 'Okay',
+ allowOutsideClick: false,
+ allowEscapeKey: false
+ });
+ } else
+ that.recognition.stop();
+ };
+
+ that.recognition.onend = function (e) {
+ if(!that.state.speechRecognitionDisabled){
+ console.warn('Recognition ended itself - stupid thing! Restarting ....', e);
+ that.recognition.start();
+ } else {
+ //TODO in StatusObjekt packen
+ }
+ };
+
+ let tmp = {};
+ ISO6391.getAllCodes().forEach((code) => {
+ tmp[''+code] = ISO6391.getName(code);
+ });
+
+ swal({
+ titleText: 'Speech recognition enabled',
+ html: 'Speech recognition is an experimental feature. If enabled, your voice will be automatically transcribed and displayed at all peers as a transcript.
Please select the language in which you will talk or disable the feature.
',
+ type: 'info',
+ input: 'select',
+ inputValue: that.recognition.lang,
+ inputOptions: tmp,
+ showCancelButton: true,
+ confirmButtonColor: '#3085d6',
+ cancelButtonColor: '#d33',
+ confirmButtonText: 'Okay',
+ cancelButtonText: 'Disable',
+ allowOutsideClick: false,
+ allowEscapeKey: false,
+ preConfirm: function (lang) {
+ return new Promise((resolve, reject) => {
+ that.recognition.lang = lang;
+ resolve();
+ });
+ }
+ }).then(() => {}, (dismiss) => {
+ if (dismiss === 'cancel') {
+ that.disableSpeechRecognition(that);
+ console.log('Recognition disabled');
+ }
+ }).then(() => {
+ that.props.showInviteModal();
+ });
+
+ } else {
+ swal({
+ titleText: 'Speech recognition disabled',
+ text: 'Your browser isn\'t able to transcribe speech to text. Thus, your peers will not recieve a transcript. Google Chrome is currently the only browser that supports speech recognition.',
+ type: 'error',
+ confirmButtonColor: '#3085d6',
+ confirmButtonText: 'Okay',
+ allowOutsideClick: false,
+ allowEscapeKey: false
+ }).then(() => {
+ that.disableSpeechRecognition(that);
+ that.props.showInviteModal();
+ });
+ }
+ }
+
+ showStopSpeechRecognitionModal() {
+ swal({
+ titleText: 'Disable Speech Recognition',
+ text: 'You will deactivate speech recognition for this presentation. You will not be able to turn it back on.',
+ type: 'warning',
+ confirmButtonColor: '#3085d6',
+ confirmButtonText: 'Disable',
+ showCancelButton: true,
+ cancelButtonColor: '#d33',
+ allowOutsideClick: false,
+ allowEscapeKey: false
+ }).then(() => {
+ this.disableSpeechRecognition(this);
+ }, () => {});
+ }
+
+ disableSpeechRecognition(context) {
+ context.setState({speechRecognitionDisabled: true});
+ if(context.recognition)
+ context.recognition.stop();
+ context.setState((prevState) => {
+ return {subtitle: prevState.subtitle + '...Speechrecognition has been disabled'};
+ });
+ context.props.sendRTCMessage('subtitle', this.state.subtitle);
+ }
+
+ render() {
+ return (
+
+
+
+
+
+ {this.props.isInitiator ? (
+ );
+ }
+}
+
+SpeechRecognition.contextTypes = {
+ executeAction: React.PropTypes.func.isRequired
+};
+
+export default SpeechRecognition;
diff --git a/components/webrtc/presentationBroadcast.js b/components/webrtc/presentationBroadcast.js
new file mode 100644
index 000000000..0d1f5f87e
--- /dev/null
+++ b/components/webrtc/presentationBroadcast.js
@@ -0,0 +1,1056 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import { handleRoute, navigateAction} from 'fluxible-router';
+import { provideContext } from 'fluxible-addons-react';
+import { isEmpty } from '../../common';
+import { Grid, Button, Popup } from 'semantic-ui-react';
+import {Microservices} from '../../configs/microservices';
+import SpeechRecognition from './SpeechRecognition.js';
+import Chat from './Chat.js';
+import { QRCode } from 'react-qr-svg';
+
+class presentationBroadcast extends React.Component {
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ subtitle: '',//used for speech recognition results
+ roleText: '',
+ peerCountText: '',
+ paused: false,//user has manually paused slide transitions
+ showReopenModalButton: false,
+ myName: '',
+ };
+ this.isInitiator = false;
+ this.localStream = undefined;
+ this.myID = undefined;
+ this.presenterID = undefined;
+ this.pcs = {}; // {: {RTCConnection: RPC, dataChannel: dataChannel, username: username}, : {RTCConnection: RPC, dataChannel: dataChannel, username: username}}
+ this.pcConfig = {'iceServers': Microservices.webrtc.iceServers};
+ this.room = this.props.currentRoute.query.room + '';//NOTE Error handling implemented in first lines of componentDidMount
+ this.socket = undefined;
+ this.maxPeers = 100;
+
+ //******** SlideWiki specific variables ********
+ this.eventForwarding = true;
+ this.iframesrc = this.props.currentRoute.query.presentation + '';//NOTE Error handling implemented in first lines of componentDidMount
+ this.lastRemoteSlide = this.iframesrc + '';
+ this.currentSlide = this.iframesrc + '';
+ this.peerNumber = -1;//used for peernames, will be incremented on each new peer
+ }
+
+ componentDidUpdate(prevProps, prevState){
+ if(prevState.paused !== this.state.paused && this.state.paused === false)
+ this.changeSlide(this.lastRemoteSlide);
+ }
+
+ componentDidMount() {
+
+ let that = this;
+ if(isEmpty(that.iframesrc) || that.iframesrc === 'undefined' || isEmpty(that.room) || that.room === 'undefined'){
+ console.log('Navigating away because of missing paramenters in URL');
+ swal({
+ titleText: 'Something went terribly wrong',
+ text: 'It seems like your URL isn\'t correct. Please report this as a bug. You will now be redirected to the homepage.',
+ type: 'error',
+ confirmButtonColor: '#3085d6',
+ confirmButtonText: 'Okay',
+ allowOutsideClick: false,
+ allowEscapeKey: false
+ }).then(() => {that.context.executeAction(navigateAction, {'url': '/'});});
+ return;
+ }
+ //Remove menus as they shouldn't appear
+ $('.menu:first').remove();
+ $('.footer:first').remove();
+
+ that.socket = io(Microservices.webrtc.uri);
+
+ let deckID = that.iframesrc.toLowerCase().split('presentation')[1].split('/')[1];//TODO implement a better version to get the deckID
+ that.socket.emit('create or join', that.room, deckID);
+ console.log('Attempt to create or join room', that.room);
+
+ function setmyID() {
+ if (that.myID === undefined)
+ that.myID = that.socket.id;
+ return that.myID;
+ }
+
+ that.socket.on('created', (room, socketID) => { //only initiator recieves this
+ console.log('Created room ' + that.room);
+ that.isInitiator = true;
+ that.setState({
+ roleText: 'You are the presenter. Other people will hear your voice and reflect your presentation progress. ',
+ peerCountText: 'People currently listening: '
+ });
+ setmyID();
+ $('#slidewikiPresentation').on('load', activateIframeListeners);
+ requestStreams({
+ audio: true,
+ // video: {
+ // width: { min: 480, ideal: 720, max: 1920 },
+ // height: { min: 360, ideal: 540, max: 1080 },
+ // facingMode: "user"
+ // }
+ });
+ });
+
+ that.socket.on('join', (room, socketID) => { //whole room recieves this, except for the peer that tries to join
+ // a listener will join the room
+ console.log('Another peer made a request to join room ' + room);
+ if (that.isInitiator) {
+ // console.log('This peer is the initiator of room ' + that.room + '!');
+ let numberOfPeers = Object.keys(this.pcs).length;
+ console.log(numberOfPeers, this.maxPeers);
+ if (numberOfPeers >= this.maxPeers)
+ that.socket.emit('room is full', socketID);
+ else
+ that.socket.emit('ID of presenter', that.myID, socketID);
+ }
+ });
+
+ that.socket.on('joined', (room) => { //only recieved by peer that tries to join - a peer has joined the room
+ console.log('joined: ' + that.room);
+ setmyID();
+ that.setState({roleText: 'You are now listening to the presenter and your presentation will reflect his actions.'});
+ $('#slidewikiPresentation').on('load', activateIframeListeners);
+ gotStream('');//NOTE Skip requesting streams for the listeners, as they do not need them
+
+ that.forceUpdate();
+ swal.queue([{
+ title: 'You\'re about to join a live presentation',
+ html: 'Please keep in mind that this is an experimental feature and might not work for you. If you encounter any issues, please report them.
Nice to see you here! You will hear the presenters voice in a few moments and your presentation will reflect his progress. Just lean back and keep watching. In case you have any questions to the presenter, please use the "Send Question" functionality.
',
+ type: 'info',
+ confirmButtonColor: '#3085d6',
+ confirmButtonText: 'Okay',
+ allowOutsideClick: false,
+ allowEscapeKey: false,
+ onOpen: () => {
+ swal.showLoading();
+ },
+ preConfirm: () => {
+ return new Promise((resolve) => {
+ /*$('body>a#atlwdg-trigger').remove();*/
+ resolve();
+ });
+ }
+ }]);
+ });
+
+ that.socket.on('full', (room) => { //only recieved by peer that tries to join
+ console.log('Room ' + that.room + ' is full');
+ that.socket.close();
+ swal({
+ titleText: 'Room ' + room + ' is full',
+ text: 'Rooms have limited capacities for people. The room you tried to join is already full.',
+ type: 'warning',
+ confirmButtonColor: '#3085d6',
+ confirmButtonText: 'Okay',
+ allowOutsideClick: false,
+ allowEscapeKey: false
+ });
+ });
+
+ that.socket.on('ID of presenter', (id) => {
+ if(!that.isInitiator)
+ console.log('Received ID of presenter: ', id);
+ that.presenterID = id;
+ });
+
+ that.socket.on('room is full', () => {
+ console.log('Received room is full');
+ swal({
+ titleText: 'The Room is already full',
+ text: 'The maximium number of listeners is already reached. Please try again later.',
+ type: 'warning',
+ confirmButtonColor: '#3085d6',
+ confirmButtonText: 'Check',
+ allowOutsideClick: false,
+ allowEscapeKey: false
+ });
+ that.socket.close();
+ });
+
+ that.socket.on('log', (array) => {
+ setmyID();
+ });
+
+ that.socket.on('disconnect', () => {
+ console.info('Closed socket');
+ });
+
+ ////////////////////////////////////////////////
+
+ function sendMessage(cmd, data = undefined, receiver = undefined) {
+ // console.log('Sending message over socket: ', cmd, data, receiver);
+ that.socket.emit('message', { 'cmd': cmd, 'data': data, 'sender': that.myID, 'receiver': receiver });
+ }
+
+ function sendRTCMessage(cmd, data = undefined, receiver = undefined) {
+ let message = JSON.stringify({ 'cmd': cmd, 'data': data, sender: that.myID });
+ if (receiver) { //send to one peer only
+ // console.log('Sending message to peer: ', receiver);
+ try {
+ that.pcs[receiver].dataChannel.send(message);
+ } catch (e){
+ console.log('SendRTCMessage error: ', e);
+ }
+ } else { //broadcast from initiator
+ // console.log('Broadcasting message to peers');
+ for (let i in that.pcs) {
+ if (that.pcs[i].dataChannel)
+ try {
+ that.pcs[i].dataChannel.send(message);
+ } catch (e){
+ console.log('SendRTCMessage error: ', e);
+ }
+ }
+ }
+ }
+ that.sendRTCMessage = sendRTCMessage;
+
+ // This client receives a message
+ that.socket.on('message', (message) => {
+ if (message.sender === that.myID) { //Filter for messages from myself
+ if (message.cmd === 'peer wants to connect' && Object.keys(that.pcs).length === 0) { //peer triggers itself
+ start(that.presenterID);
+ }
+ } else if (message.receiver === that.myID) { //adressed to me
+ // console.log('Recieved message from peer: ', message);
+ if (message.cmd === 'peer wants to connect' && that.isInitiator) { //Everyone recieves this, except for the peer itself, as soon as a peer joins, only from peer
+ start(message.sender);
+ } else if (message.cmd === 'offer' || (message.cmd === 'answer' && that.isInitiator)) { //offer by initiator, answer by peer
+ that.pcs[message.sender].RTCconnection.setRemoteDescription(new RTCSessionDescription(message.data));
+ if (message.cmd === 'offer') // führt nur der peer aus
+ doAnswer(message.sender);
+ }
+ if (message.cmd === 'candidate') {
+ try { //Catch defective candidates
+ let candidate = new RTCIceCandidate({
+ sdpMLineIndex: message.data.label,
+ candidate: message.data.candidate
+ });
+ that.pcs[message.sender].RTCconnection.addIceCandidate(candidate).catch((e) => {
+ console.log('Error: was unable to add Ice candidate:', candidate, 'to sender', message.sender);
+ }); //Catch defective candidates, TODO add better exception handling
+ } catch (e) {
+ console.log('Error: building the candiate failed with', message);
+ }//TODO add better exception handling
+ }
+ }
+ });
+
+ //******** Media specific methods ********
+
+ function requestStreams(options) {
+ navigator.mediaDevices.getUserMedia(options)
+ .then(gotStream)
+ .catch((err) => {
+ switch (err.name) {
+ case 'NotAllowedError'://The user declined the use of the media device(s)
+ if(that.isInitiator)
+ requestStreamsErrorHandler('No access to microphone', 'Your browser reported that you refused to grant this application access to your microphone. The presention rooms feature is not usable without a microphone. Please grant us access to your microphone (see your URL bar) and click the Okay button. The room will be automatically recreated.', 'warning');
+ else
+ console.log('getUserMedia() error: ' + err.name);
+ break;
+ default:
+ console.log('getUserMedia() error: ' + err.name);
+ if(that.isInitiator)
+ requestStreamsErrorHandler('Device error', 'Your browser reported a problem accessing your microphone. You can\'t use the presention rooms feature without a microphone. Please try to fix your microphone (settings) and open up a new room. You will be redirected to the homepage.', 'error');
+ else
+ requestStreamsErrorHandler('Browser Error', 'Your browser reported a technical problem. You can\'t use the presention rooms feauter with this problem. Please try to fix it by updating your browser or resetting it and rejoin a room. You will be redirected to the homepage.', 'error');
+ }
+ });
+ }
+
+ function requestStreamsErrorHandler(title1, text1, type1) {
+ let dialog = {
+ title: title1,
+ html: text1,
+ type: type1,
+ confirmButtonColor: '#3085d6',
+ confirmButtonText: 'Okay',
+ allowOutsideClick: false,
+ allowEscapeKey: false,
+ preConfirm: () => {
+ return new Promise((resolve) => {
+ cleanup();
+ if(type1 === 'error')
+ that.context.executeAction(navigateAction, {'url': '/'});
+ else
+ location.reload();
+ resolve();
+ });
+ }
+ };
+
+ that.socket.close();
+ if(swal.isVisible())
+ swal.insertQueueStep(dialog);
+ else
+ swal(dialog);
+ }
+
+ function gotStream(stream) {
+ console.log('Adding local stream');
+ if (that.isInitiator) {
+ //$('#media').append('');
+ //let localVideo = document.querySelector('#localVideo');
+ //localVideo.srcObject = stream;
+ $('#media').remove();
+ swal({//NOTE implemented here because this dialog interrupted with error dialogs of requestStreams()
+ title: 'Room ' + that.room + ' successfully created!
',
+ html: 'Please keep in mind that this is an experimental feature and might not work for you. If you encounter any issues, please report them.
Other people are free to join the room. Rooms are currently limited to '+that.maxPeers+' people. See the counter at the bottom of the page for information about currently listening people.
',
+ type: 'info',
+ confirmButtonColor: '#3085d6',
+ confirmButtonText: 'Check',
+ allowOutsideClick: false,
+ allowEscapeKey: false
+ }).then(() => { that.refs.speechRecognition.activateSpeechRecognition(); /*$('body>a#atlwdg-trigger').remove();*/});
+ }
+ that.localStream = stream;
+
+ function sendASAP() {
+ if (that.presenterID) //wait for presenterID before sending the message
+ sendMessage('peer wants to connect', undefined, that.presenterID);
+ else
+ setTimeout(() => { sendASAP(); }, 10);
+ }
+
+ if (!that.isInitiator) {
+ sendASAP();
+ }
+ }
+
+ function start(peerID) {
+ if (typeof that.localStream !== 'undefined') {
+ console.log('creating RTCPeerConnnection for', (that.isInitiator) ? 'initiator' : 'peer');
+ createPeerConnection(peerID);
+ if (that.isInitiator){
+ that.localStream.getTracks().forEach((track) => that.pcs[peerID].RTCconnection.addTrack(track, that.localStream));
+ doCall(peerID);
+ }
+ }
+ }
+
+ window.onbeforeunload = function() {
+ hangup();
+ };
+
+ //******** WebRTC specific methods ********
+
+ function createPeerConnection(peerID) {
+ try {
+ that.pcs[peerID] = {};
+ that.pcs[peerID].username = '';
+ that.pcs[peerID].RTCconnection = new RTCPeerConnection(that.pcConfig);
+ that.pcs[peerID].RTCconnection.onicecandidate = handleIceCandidate.bind(that, peerID);
+ that.pcs[peerID].RTCconnection.ontrack = handleRemoteStreamAdded;
+ that.pcs[peerID].RTCconnection.onremovestream = handleRemoteStreamRemoved;
+ that.pcs[peerID].RTCconnection.oniceconnectionstatechange = handleICEConnectionStateChange.bind(that, peerID);
+ if (that.isInitiator) {
+ that.pcs[peerID].dataChannel = that.pcs[peerID].RTCconnection
+ .createDataChannel('messages', {
+ ordered: true
+ });
+ onDataChannelCreated(that.pcs[peerID].dataChannel, peerID);
+ } else
+ that.pcs[peerID].RTCconnection.ondatachannel = handleDataChannelEvent.bind(that, peerID);
+
+ console.log('Created RTCPeerConnnection');
+ if (that.isInitiator){
+ that.forceUpdate();
+ }
+ } catch (e) {
+ console.log('Failed to create PeerConnection, exception: ' + e.message);
+ console.log('Cannot create RTCPeerConnection object.');
+ connectionFailureHandler();
+ return;
+ }
+ }
+
+ function connectionFailureHandler() {
+ let dialog = {
+ title: 'An error occured',
+ html: 'We\'re sorry, but we can\'t connect you to the presenter. It seems like there is a problem with your connection or browser. Please update your browser, disable extensions or ask your network operator about it. We\'re using a peer to peer connection technique called WebRTC.',
+ type: 'error',
+ confirmButtonColor: '#3085d6',
+ confirmButtonText: 'Okay',
+ allowOutsideClick: false,
+ allowEscapeKey: false,
+ preConfirm: () => {
+ return new Promise((resolve) => {
+ cleanup();
+ that.context.executeAction(navigateAction, {'url': '/'});
+ resolve();
+ });
+ }
+ };
+ if(swal.isVisible){
+ swal.hideLoading();//NOTE is currently not working, contacted developer.
+ swal.insertQueueStep(dialog);
+ swal.clickConfirm();
+ } else
+ swal(dialog);
+ }
+
+ function handleICEConnectionStateChange(peerID, event) {
+ if(that.pcs[peerID] && that.pcs[peerID].RTCconnection){
+ switch(that.pcs[peerID].RTCconnection.iceConnectionState) {
+ case 'connected':
+ console.log('The connection has been successfully established');
+ if(!that.isInitiator){
+ try {
+ swal.hideLoading();
+ } catch (e) {
+ console.log('Error: swal was not defined', e);
+ }
+ }
+ break;
+ case 'disconnected':
+ console.log('The connection has been terminated');
+ break;
+ case 'failed':
+ console.warn('The connection has failed');
+ if(!that.isInitiator)
+ connectionFailureHandler();
+ else
+ stop(peerID);
+ break;
+ case 'closed':
+ console.log('The connection has been closed');
+ break;
+ }
+ }
+ }
+
+ function handleDataChannelEvent(peerID, event) { //called by peer
+ // console.log('ondatachannel:', event.channel);
+ that.pcs[peerID].dataChannel = event.channel;
+ that.pcs[peerID].dataChannel.onclose = handleRPCClose; //NOTE dirty workaround as browser are currently not implementing RPC.onconnectionstatechange
+ onDataChannelCreated(that.pcs[peerID].dataChannel, peerID);
+ }
+
+ function handleRPCClose() {
+ if (!that.isInitiator) {
+ swal({
+ titleText: 'The presenter closed the session',
+ text: 'This presentation has ended. Feel free to look at the deck as long as you want.',
+ type: 'warning',
+ confirmButtonColor: '#3085d6',
+ confirmButtonText: 'Check',
+ allowOutsideClick: false,
+ allowEscapeKey: false
+ });
+
+ that.setState({
+ roleText: 'This presentation has ended. Feel free to look at the deck as long as you want.',
+ peerCountText: ''
+ });
+ handleRemoteHangup(that.presenterID);
+ }
+ }
+
+ function onDataChannelCreated(channel, peerID) { //called by peer and by initiatior
+ console.log('Created data channel for ', peerID);
+ /*NOTE Browsers do currenty not support events that indicate whether ICE exchange has finished or not and the RPC connection has been fully established. Thus, I'm waiting for latest event onDataChannelCreated in order to close the that.socket after some time. This should be relativly safe.
+ */
+ if (!that.isInitiator && that.socket.disconnected === false) {
+ setTimeout(() => that.socket.close(), 10000); //close that.socket after 10 secs, TODO maybe come up with a better solution
+ }
+
+ channel.onopen = function() {
+ console.log('Data Channel opened');
+ if (that.isInitiator)
+ sendStatusObject(peerID);
+ else
+ that.sendUsername();
+ };
+
+ channel.onmessage = handleMessage.bind(that, channel, peerID);
+ }
+
+ function sendStatusObject(context, peerID) {
+ let tosend = {
+ slide: document.getElementById('slidewikiPresentation').contentWindow.location.href, // using href because currentSlide might be badly initialized
+ subtitle: that.refs.speechRecognition.getSubtitle()
+ };
+ sendRTCMessage('statusObject', tosend, peerID);
+ }
+
+ function handleIceCandidate(peerID, event) {
+ if (event && ((event.target && event.target.iceGatheringState !== 'complete') || event.candidate !== null)) {
+ sendMessage('candidate', {
+ type: 'candidate',
+ label: event.candidate.sdpMLineIndex,
+ id: event.candidate.sdpMid,
+ candidate: event.candidate.candidate
+ }, peerID);
+ } else {
+ console.log('End of candidates.');
+ }
+ }
+
+ function handleRemoteStreamAdded(event) {
+ if (that.isInitiator === false) {
+ $('#media').append('');
+ let remoteAudios = $('.remoteAudio');
+ remoteAudios[remoteAudios.length - 1].srcObject = event.streams[0];
+ }
+ }
+
+ function handleCreateOfferError(event) {
+ console.log('createOffer() error: ', event);//TODO add better error handling for this - maybe close the window if this is fatal
+ }
+
+ function doCall(peerID) { //calledy by initiatior
+ that.pcs[peerID].RTCconnection.createOffer(setLocalAndSendMessage.bind(that, peerID), handleCreateOfferError);
+ }
+
+ function doAnswer(peerID) {
+ that.pcs[peerID].RTCconnection.createAnswer()
+ .then(
+ setLocalAndSendMessage.bind(that, peerID),
+ onCreateSessionDescriptionError
+ );
+ }
+
+ function setLocalAndSendMessage(peerID, sessionDescription) {
+ // Set Opus as the preferred codec in SDP if Opus is present.
+ sessionDescription.sdp = preferOpus(sessionDescription.sdp);
+ that.pcs[peerID].RTCconnection.setLocalDescription(sessionDescription);
+ sendMessage(sessionDescription.type, sessionDescription, peerID);
+ }
+
+ function onCreateSessionDescriptionError(error) {//TODO add better error handling for this - maybe close the window if this is fatal
+ trace('Failed to create session description: ' + error.toString());
+ }
+
+ function handleRemoteStreamRemoved(event) {
+ console.log('Remote stream removed. Event: ', event);
+ }
+
+ function hangup() { //calledy by peer and by initiatior
+ console.log('Hanging up.');
+ if (that.isInitiator) {
+ stop(undefined, true);
+ } else {
+ sendRTCMessage('bye', that.myID, that.presenterID);
+ stop(that.presenterID);
+ }
+ //NOTE Don't need to close the socket, as the browser does this automatically if the window closes
+ }
+
+ function handleRemoteHangup(peerID) { //called by initiator
+ console.log('Terminating session for ', peerID);
+ stop(peerID);
+ }
+
+ function stop(peerID, presenter = false) {
+ try {
+ if (presenter) {
+ for (let i in that.pcs) {
+ that.pcs[i].dataChannel.close();
+ that.pcs[i].RTCconnection.close();
+ delete that.pcs[i];
+ }
+ } else {
+ that.pcs[peerID].dataChannel.close();
+ that.pcs[peerID].RTCconnection.close();
+ delete that.pcs[peerID];
+ }
+ } catch (e) {//TODO add better error handling
+ console.log('Error when deleting RTC connections', e);
+ } finally {
+ if (that.isInitiator){
+ that.forceUpdate();
+ }
+ }
+ }
+
+ function cleanup() {
+ try {
+ that.socket.close();
+ } catch (e) {}
+ try {
+ stop(undefined, true);
+ } catch (e) {}
+ }
+
+ function handleMessage(channel, peerID, event) {
+ // console.log(event.data);
+ if (event.data === undefined)
+ return;
+ let data = JSON.parse(event.data);
+ switch (data.cmd) {
+ case 'gotoslide':
+ if (!that.isInitiator)
+ changeSlide(data.data);
+ break;
+ case 'toggleblackscreen':
+ if (!that.isInitiator)
+ toggleBlackScreen();
+ break;
+ case 'message':
+ if (that.isInitiator) {
+ this.refs.chat.addMessage(data, false, peerID);
+ }
+ break;
+ case 'log':
+ console.log('Recieved log message from peer: ', data.data);
+ break;
+ case 'bye':
+ handleRemoteHangup(data.data);
+ break;
+ case 'subtitle':
+ this.setState({subtitle: data.data});
+ break;
+ case 'newUsername':
+ handleNewUsername(data.data, peerID);
+ break;
+ case 'username':
+ if(!that.isInitiator){
+ that.setState({myName: data.data});
+ }
+ break;
+ case 'completeTask':
+ showCompleteTaskModal();
+ break;
+ case 'taskCompleted':
+ checkUser(data.sender);
+ break;
+ case 'closeAndProceed':
+ closeModal();
+ break;
+ case 'statusObject':
+ if(!that.isInitiator){
+ this.setState({subtitle: data.data.subtitle});
+ changeSlide(data.data.slide);
+ }
+ break;
+ default:
+
+ }
+ }
+
+ //******** Media Codec specific methods (like Opus) ********
+
+ function preferOpus(sdp) { // Set Opus as the default audio codec if it's present.
+ let sdpLines = sdp.split('\r\n');
+ let mLineIndex;
+ // Search for m line.
+ for (let i = 0; i < sdpLines.length; i++) {
+ if (sdpLines[i].search('m=audio') !== -1) {
+ mLineIndex = i;
+ break;
+ }
+ }
+ if (mLineIndex === null) {
+ return sdp;
+ }
+
+ // If Opus is available, set it as the default in m line.
+ for (let i = 0; i < sdpLines.length; i++) {
+ if (sdpLines[i].search('opus/48000') !== -1) {
+ let opusPayload = extractSdp(sdpLines[i], /:(\d+) opus\/48000/i);
+ if (opusPayload) {
+ sdpLines[mLineIndex] = setDefaultCodec(sdpLines[mLineIndex],
+ opusPayload);
+ }
+ break;
+ }
+ }
+
+ // Remove CN in m line and sdp.
+ sdpLines = removeCN(sdpLines, mLineIndex);
+
+ sdp = sdpLines.join('\r\n');
+ return sdp;
+ }
+
+ function extractSdp(sdpLine, pattern) {
+ let result = sdpLine.match(pattern);
+ return result && result.length === 2 ? result[1] : null;
+ }
+
+ function setDefaultCodec(mLine, payload) { // Set the selected codec to the first in m line.
+ let elements = mLine.split(' ');
+ let newLine = [];
+ let index = 0;
+ for (let i = 0; i < elements.length; i++) {
+ if (index === 3) { // Format of media starts from the fourth.
+ newLine[index++] = payload; // Put target payload to the first.
+ }
+ if (elements[i] !== payload) {
+ newLine[index++] = elements[i];
+ }
+ }
+ return newLine.join(' ');
+ }
+
+ function removeCN(sdpLines, mLineIndex) { // Strip CN from sdp before CN constraints is ready.
+ let mLineElements = sdpLines[mLineIndex].split(' ');
+ // Scan from end for the convenience of removing an item.
+ for (let i = sdpLines.length - 1; i >= 0; i--) {
+ let payload = extractSdp(sdpLines[i], /a=rtpmap:(\d+) CN\/\d+/i);
+ if (payload) {
+ let cnPos = mLineElements.indexOf(payload);
+ if (cnPos !== -1) {
+ // Remove CN payload from m line.
+ mLineElements.splice(cnPos, 1);
+ }
+ // Remove CN line in sdp
+ sdpLines.splice(i, 1);
+ }
+ }
+
+ sdpLines[mLineIndex] = mLineElements.join(' ');
+ return sdpLines;
+ }
+
+ //******** SlideWiki specific methods ********
+
+ function toggleBlackScreen() {//TODO won't unpause the screen - I have no idea why...
+ let frame = document.getElementById('slidewikiPresentation').contentDocument;
+ let newEvent = new Event('keydown', {keyCode: 58});
+ newEvent.keyCode = 58;
+ newEvent.which = 58;
+ // frame.dispatchEvent(newEvent);
+ }
+
+ function activateIframeListeners() {
+ console.log('Adding iframe listeners');
+ let iframe = $('#slidewikiPresentation').contents();
+
+ document.addEventListener('keydown', (e) => {//NOTE used for arrow keys
+ let frame = document.getElementById('slidewikiPresentation').contentDocument;
+ let newEvent = new Event('keydown', {key: e.key, code: e.code, composed: true, charCode: e.charCode, keyCode: e.keyCode, which: e.which, bubbles: true, cancelable: true, which: e.keyCode});
+ newEvent.keyCode = e.keyCode;
+ newEvent.which = e.keyCode;
+ if(that.eventForwarding)
+ frame.dispatchEvent(newEvent);
+ });
+
+ if (that.isInitiator) {
+ iframe.on('slidechanged', () => {
+ that.currentSlide = document.getElementById('slidewikiPresentation').contentWindow.location.href;
+ sendRTCMessage('gotoslide', that.currentSlide);
+ });
+ iframe.on('paused', () => {
+ sendRTCMessage('toggleblackscreen');
+ });
+ iframe.on('resumed', () => {
+ sendRTCMessage('toggleblackscreen');
+ });
+ } else {
+ iframe.on('slidechanged', () => {
+ if (document.getElementById('slidewikiPresentation').contentWindow.location.href !== that.lastRemoteSlide) {
+ that.setState({paused: true});
+ }
+ });
+ let textArea = $('#messageToSend');
+ textArea.on('focus', () => {
+ that.eventForwarding = false;
+ });
+ textArea.on('focusout', () => {
+ that.eventForwarding = true;
+ });
+ }
+ }
+
+ function changeSlide(slideID) { // called by peers
+ that.lastRemoteSlide = slideID;
+ if (!that.state.paused) {
+ let doc = document.getElementById('slidewikiPresentation');
+ if(doc.contentDocument.readyState === 'complete'){
+ console.log('Changing to slide: ', slideID);
+ that.iframesrc = slideID;
+ doc.contentWindow.location.assign(slideID);
+ } else { //if readyState === 'loading' || readyState === 'interactive'
+ setTimeout(() => {
+ changeSlide(slideID);
+ }, 20);
+ }
+ }
+ }
+ that.changeSlide = changeSlide;
+
+
+ function handleNewUsername(username, peerID) {
+ if(isEmpty(username) || username === 'undefined')
+ that.pcs[peerID].username = 'Peer ' + nextPeerNumber();//TODO implement separate counter, as this will mess up numbers
+ else
+ that.pcs[peerID].username = username;
+ sendRTCMessage('username', that.pcs[peerID].username, peerID);
+ that.forceUpdate();
+ }
+
+ function nextPeerNumber() {
+ that.peerNumber += 1;
+ return that.peerNumber;
+ }
+
+ function showCompleteTaskModal() {
+ let tmp = that;
+ if(tmp === undefined)
+ tmp = this;
+ tmp.setState({showReopenModalButton: false});
+ swal({
+ titleText: 'Complete the given Task',
+ text: 'The presenter asked you to complete a task. As soon as you have completed the task, click on "Completed" and wait for the presenter to proceed.',
+ type: 'info',
+ confirmButtonColor: '#3085d6',
+ confirmButtonText: 'Completed',
+ showCancelButton: true,
+ cancelButtonText: 'Dismiss',
+ allowOutsideClick: false,
+ allowEscapeKey: false,
+ }).then(() => {
+ tmp.sendRTCMessage('taskCompleted',undefined, tmp.presenterID);
+ }).catch((e) => {
+ if(e === 'cancel'){
+ tmp.setState({showReopenModalButton: true});
+ }
+ });
+ tmp.forceUpdate();
+ }
+
+ that.showCompleteTaskModal = showCompleteTaskModal;
+
+ function checkUser(id) {
+ $('input#'+id).prop('checked', true);
+ let tmp = parseInt($('span#taskModalPeerCount').text());
+ $('span#taskModalPeerCount').text(tmp + 1);
+ }
+
+ function closeModal() {
+ that.setState({showReopenModalButton: false});
+ swal.closeModal();
+ }
+ }
+
+ sendUsername() {
+ if (this.context && this.context.getUser() && this.context.getUser().username)
+ this.sendRTCMessage('newUsername', this.context.getUser().username, this.presenterID);
+ else
+ this.sendRTCMessage('newUsername', 'undefined');
+ }
+
+ audienceCompleteTask (event) {
+ let nameArray = Object.keys(this.pcs).map((key) => {
+ return {'username': this.pcs[key].username ? this.pcs[key].username : 'Anonymous Rabbit', 'key': key};
+ }).sort((a,b) => a.username > b.username);
+ let contentArray = nameArray.map((tmp) => ' ' + tmp.username + '
');
+ let titleHTMLAddition = '';
+ let contentHTML = '';
+ let indexes = [0,Math.ceil(contentArray.length/3),Math.ceil(contentArray.length/3)*2,contentArray.length];
+ if(contentArray.length > 0){
+ titleHTMLAddition = ' 0/' + contentArray.length;
+ contentHTML = 'Detailed list of peers
'+
+ '
'+
+ '
'+contentArray.slice(indexes[0],indexes[1]).reduce((a,b) => a + b, '')+'
'+
+ '
'+contentArray.slice(indexes[1],indexes[2]).reduce((a,b) => a + b, '')+'
'+
+ '
'+contentArray.slice(indexes[2],indexes[3]).reduce((a,b) => a + b, '')+'
'+
+ '
';
+ } else {
+ contentHTML = 'There is currently no audience, please close this modal and reopen it as soon as some audience joined your room.
';
+ }
+ swal({
+ title: 'Audience Progress' + titleHTMLAddition,
+ html: contentHTML,
+ type: 'info',
+ confirmButtonColor: '#3085d6',
+ confirmButtonText: 'End Task',
+ allowOutsideClick: false,
+ allowEscapeKey: false,
+ onOpen: function () {
+ $('.ui.accordion').accordion();
+ }
+ }).then(() => {
+ this.sendRTCMessage('closeAndProceed');
+ });
+ this.sendRTCMessage('completeTask');
+ }
+
+ resumePlayback(){
+ this.setState({paused: false});
+ //NOTE SlideChange is triggerd by componentDidUpdate
+ }
+
+ copyURLToClipboard() {
+ let toCopy = document.createElement('input');
+ toCopy.style.position = 'fixed';
+ toCopy.style.top = 0;
+ toCopy.style.left = 0;
+ toCopy.style.width = '2em';
+ toCopy.style.height = '2em';
+ toCopy.style.padding = 0;
+ toCopy.style.border = 'none';
+ toCopy.style.outline = 'none';
+ toCopy.style.boxShadow = 'none';
+ toCopy.style.background = 'transparent';
+ toCopy.value = window.location.href;
+ document.body.appendChild(toCopy);
+ toCopy.value = window.location.href;
+ toCopy.select();
+
+ try {
+ let successful = document.execCommand('copy');
+ if(!successful)
+ throw 'Unable to copy';
+ else{
+ swal({
+ titleText: 'URL copied to clipboard',
+ type: 'success',
+ showConfirmButton: false,
+ allowOutsideClick: false,
+ timer: 1500
+ }).then(() => {}, () => {});
+ }
+ } catch (err) {
+ console.log('Oops, unable to copy');
+ swal({
+ titleText: 'Can\'t copy URL to clipboard',
+ text: 'Please select the URL in your browser and share it manually.',
+ type: 'error',
+ confirmButtonColor: '#3085d6',
+ confirmButtonText: 'Check',
+ allowOutsideClick: false
+ });
+ }
+ document.body.removeChild(toCopy);
+ }
+
+ showQRCode() {
+ swal({
+ titleText: 'Share this Room',
+ html: '',
+ confirmButtonColor: '#3085d6',
+ confirmButtonText: 'Close',
+ allowOutsideClick: false,
+ allowEscapeKey: false,
+ onOpen: () => {
+ ReactDOM.render(, document.getElementById('qr-code'));
+ }
+ });
+ }
+
+ showInviteModal() {
+ swal({
+ titleText: 'Invite other people',
+ html: 'Copy the following link and send it to other people in order to invite them to this room:
' + window.location.href + '
',
+ type: 'info',
+ confirmButtonColor: '#3085d6',
+ confirmButtonText: 'Copy to Clipboard',
+ showCancelButton: true,
+ cancelButtonColor: '#d33',
+ allowOutsideClick: false,
+ allowEscapeKey: false,
+ preConfirm: function() {
+ return new Promise((resolve, reject) => {
+ let toCopy = document.createElement('input');
+ toCopy.style.position = 'fixed';
+ toCopy.style.top = 0;
+ toCopy.style.left = 0;
+ toCopy.style.width = '2em';
+ toCopy.style.height = '2em';
+ toCopy.style.padding = 0;
+ toCopy.style.border = 'none';
+ toCopy.style.outline = 'none';
+ toCopy.style.boxShadow = 'none';
+ toCopy.style.background = 'transparent';
+ toCopy.value = window.location.href;
+ document.getElementById('clipboardtarget')
+ .appendChild(toCopy);
+ toCopy.value = window.location.href;
+ toCopy.select();
+
+ try {
+ let successful = document.execCommand('copy');
+ if (!successful)
+ throw 'Unable to copy';
+ resolve('Copied to clipboard');
+ } catch (err) {
+ console.log('Oops, unable to copy');
+ reject('Oops, unable to copy');
+ }
+ });
+ }
+ })
+ .then(() => {}, () => {});
+ }
+
+ render() {
+ let peernames = new Set(Object.keys(this.pcs).map((key) => {
+ let tmp = this.pcs[key].username === '' || this.pcs[key].username.startsWith('Peer');
+ return tmp ? 'Anonymous Rabbits' : this.pcs[key].username;
+ }));
+ peernames = Array.from(peernames).reduce((a,b) => a+', '+b, '').substring(1);
+
+ let height = typeof window !== 'undefined' ? window.innerHeight : 961;
+
+ return (
+
+
+
+
+
+
+
+
+ {(this.isInitiator) ? (
+
+ ) : ('')};
+
+
+
+
+
+ {this.isInitiator ? (
{this.state.roleText}{this.state.peerCountText}{Object.keys(this.pcs).length}}
+ content={peernames}
+ />
) : {this.state.roleText}
}
+
+
+
+
+
+
+ {/*{/*TODO open up the right functionality*/}*/}
+ {/*TODO open up the right functionality*/}
+ {this.isInitiator ? (
+
+
+
+ );
+ }
+}
+
+presentationBroadcast.contextTypes = {
+ executeAction: React.PropTypes.func.isRequired,
+ getUser: React.PropTypes.func
+};
+
+presentationBroadcast = handleRoute(presentationBroadcast);
+
+export default presentationBroadcast;
diff --git a/configs/microservices.sample.js b/configs/microservices.sample.js
index f875e7630..23d842050 100644
--- a/configs/microservices.sample.js
+++ b/configs/microservices.sample.js
@@ -68,6 +68,16 @@ export default {
'tag': {
uri : 'https://tagservice.experimental.slidewiki.org'
},
+ 'translation': {
+ uri: 'https://translationservice.experimental.slidewiki.org'
+ },
+ 'webrtc' : {
+ uri : 'https://signalingservice.experimental.slidewiki.org',
+ iceServers: [//Firefox complained that more than two STUN servers makes discovery slow
+ {'urls': 'stun:stun.l.google.com:19302'},
+ {'urls': 'stun:stun.schlund.de'},
+ ]
+ },
'questions': {
uri: 'https://questionservice.experimental.slidewiki.org'
}
diff --git a/configs/routes.js b/configs/routes.js
index 1a16a190d..f514aee11 100644
--- a/configs/routes.js
+++ b/configs/routes.js
@@ -33,6 +33,7 @@ import loadDiffview from '../actions/loadDiffview';
import checkReviewableUser from '../actions/userReview/checkReviewableUser';
import {navigateAction} from 'fluxible-router';
+import loadSupportedLanguages from '../actions/loadSupportedLanguages';
export default {
//-----------------------------------HomePage routes------------------------------
@@ -64,7 +65,7 @@ export default {
recentDecks: {
path: '/recent/:limit?/:offset?',
method: 'get',
- page: 'featuredDecks',
+ page: 'recentDecks',
title: 'Slidewiki -- recent decks',
handler: require('../components/Home/Recent'),
action: (context, payload, done) => {
@@ -86,6 +87,31 @@ export default {
}
},
+ featuredDecks: {
+ path: '/featured/:limit?/:offset?',
+ method: 'get',
+ page: 'featuredDecks',
+ title: 'Slidewiki -- featured decks',
+ handler: require('../components/Home/Featured'),
+ action: (context, payload, done) => {
+ async.series([
+ (callback) => {
+ context.dispatch('UPDATE_PAGE_TITLE', {
+ pageTitle: shortTitle + ' | Featured Decks'
+ });
+ callback();
+ },
+ (callback) => {
+ context.executeAction(loadFeatured, {params: {limit: 100, offset: 0}}, callback); //for now limit 100, can change this later to infinite scroll
+ }
+ ],
+ (err, result) => {
+ if(err) console.log(err);
+ done();
+ });
+ }
+ },
+
about: {
path: '/about',
method: 'get',
@@ -125,15 +151,15 @@ export default {
done();
}
},
- features: {
- path: '/features',
+ discover: {
+ path: '/discover',
method: 'get',
- page: 'features',
- title: 'SlideWiki -- Features',
+ page: 'discover',
+ title: 'SlideWiki -- Discover More',
handler: require('../components/Home/Features'),
action: (context, payload, done) => {
context.dispatch('UPDATE_PAGE_TITLE', {
- pageTitle: shortTitle + ' | Features'
+ pageTitle: shortTitle + ' | Discover More'
});
done();
}
@@ -151,6 +177,19 @@ export default {
done();
}
},
+ terms: {
+ path: '/terms',
+ method: 'get',
+ page: 'imprint',
+ title: 'SlideWiki -- Terms',
+ handler: require('../components/Home/Terms'),
+ action: (context, payload, done) => {
+ context.dispatch('UPDATE_PAGE_TITLE', {
+ pageTitle: shortTitle + ' | Terms'
+ });
+ done();
+ }
+ },
welcome: {
path: '/welcome',
method: 'get',
@@ -280,7 +319,22 @@ export default {
page: 'deck',
handler: require('../components/Deck/Deck'),
action: (context, payload, done) => {
- context.executeAction(loadDeck, payload, done);
+ async.series([
+ (callback) => {
+ context.executeAction(loadDeck, payload, callback);
+ },
+ (callback) => {
+ context.executeAction(loadPresentation, payload, callback);
+ },
+ (callback) => {
+ context.executeAction(loadTranslations, payload, callback);
+ },
+
+ ],
+ (err, result) => {
+ if(err) console.log(err);
+ done();
+ });
}
},
legacydeck: {
@@ -516,6 +570,12 @@ export default {
context.executeAction(loadDeckFamily, payload, done);
}
},
+ webrtc: {
+ path: '/presentationbroadcast',//Example: ...broadcast?room=foo&presentation=/Presentation/386-1/
+ method: 'get',
+ page: 'presentationBroadcast',
+ handler: require('../components/webrtc/presentationBroadcast')
+ },
/* This should be the last route in routes.js */
notfound: {
path: '*',
diff --git a/configs/version.js b/configs/version.js
deleted file mode 100644
index f3592c93b..000000000
--- a/configs/version.js
+++ /dev/null
@@ -1,5 +0,0 @@
-
-export default {
- branch: 'swik-134-presentation-mode',
- head: 'f344159'
-};
diff --git a/docker-compose.yml b/docker-compose.yml
index 085082924..e794f9bd7 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -18,6 +18,9 @@ slidewikiplatform:
- SERVICE_URL_TAG=https://tagservice.experimental.slidewiki.org
- SERVICE_URL_SEARCH=https://searchservice.experimental.slidewiki.org
- SERVICE_URL_PDF=https://pdfservice.experimental.slidewiki.org
+ - SERVICE_URL_TRANSLATION=https://translationservice.experimental.slidewiki.org
+ - SERVICE_URL_SIGNALING=https://signalingservice.experimental.slidewiki.org
+ - SERVICE_URL_QUESTION=https://questionservice.experimental.slidewiki.org
- SERVICE_VAR_IMPORT_HOST=importservice.experimental.slidewiki.org
- SERVICE_USER_APIKEY=2cbc621f86e97189239ee8c4c80b10b3a935b8a9f5db3def7b6a3ae7c4b75cb5
- SERVICE_USER_PRIVATE_RECAPTCHA_KEY=6LdNLyYTAAAAAFMC0J_zuVI1b9lXWZjPH6WLe-vJ
diff --git a/microservices.js.template b/microservices.js.template
index 241a82247..d337ceae2 100644
--- a/microservices.js.template
+++ b/microservices.js.template
@@ -35,7 +35,19 @@ export default {
},
'tag': {
uri : '${SERVICE_URL_TAG}'
+ },
+ 'translation': {
+ uri: '${SERVICE_URL_TRANSLATION}'
+ },
+ 'webrtc' : {
+ uri : '${SERVICE_URL_SIGNALING}',
+ iceServers: [//Firefox complained that more than two STUN servers makes discovery slow
+ {'urls': 'stun:stun.l.google.com:19302'},
+ {'urls': 'stun:stun.schlund.de'},
+ ]
+ },
+ 'questions': {
+ uri: '${SERVICE_URL_QUESTION}'
}
-
}
};
diff --git a/package.json b/package.json
index 03a837ed9..559ada8cc 100644
--- a/package.json
+++ b/package.json
@@ -17,7 +17,7 @@
},
"bugs": "https://github.com/slidewiki/slidewiki-platform/issues",
"engines": {
- "node": "7.3.0"
+ "node": ">=6.11.0"
},
"main": "start.js",
"scripts": {
@@ -28,14 +28,13 @@
"build:windows": "webpack --config ./webpack/prod.config.js && set NODE_ENV=production && set BABEL_ENV=production && npm start",
"build:nostart": "webpack --config ./webpack/prod.config.js",
"dev": "node ./webpack/dev-server.js",
- "dev:dashboard": "DASHBOARD=1 webpack-dashboard -- webpack-dev-server --config ./webpack/dev-server.js",
- "dev:windows:dashboard": "set HOST=127.0.0.1&& set PORT=3000&& set DASHBOARD=1&& webpack-dashboard -- webpack-dev-server --config ./webpack/dev-server.js",
"dev:windows": "set HOST=127.0.0.1&& set PORT=3000&& node ./webpack/dev-server.js",
"langs": "babel plugins/intl/translate.js | node",
"lint": "eslint --ignore-path .gitignore -c .eslintrc \"./**/*.js\"",
- "test": "mocha test/unit/ --recursive --compilers js:babel-register --reporter spec",
- "coverage": "istanbul cover _mocha --include-all-sources tests/unit/*.js",
- "coverall": "npm run coverage && #cat ./coverage/lcov.info | coveralls && rm -rf ./coverage",
+ "test:components": "mocha test/setup.js test/components/**/*.js",
+ "test:unit": "mocha test/setup.js test/unit/**/*.js",
+ "coverage": "istanbul cover _mocha -- --require babel-register test/setup.js test/components/*.js",
+ "coverall": "npm run coverage && cat ./coverage/lcov.info | coveralls && rm -rf ./coverage",
"countLOC": "sloc -f cli-table -k total,source,comment,empty -e node_modules\\|coverage\\|.git\\|bower_components\\|custom_modules ./",
"countLOC:details": "sloc -f cli-table -d -e node_modules\\|coverage\\|.git\\|bower_components\\|custom_modules ./",
"start:watch": "nodemon",
@@ -76,8 +75,9 @@
"css-loader": "^0.28.7",
"css-modules-require-hook": "^4.0.6",
"csurf": "^1.9.0",
- "debug": "^2.6.1",
- "diff": "3.2.0",
+ "crypt": "^0.0.2",
+ "debug": "^3.1.0",
+ "diff": "3.4.0",
"es5-shim": "^4.5.9",
"es6-shim": "^0.35.3",
"express": "^4.15.0",
@@ -106,15 +106,13 @@
"jquery-contextmenu": "^2.4.5",
"jquery-ui-dist": "^1.12.1",
"js-cookie": "2.1.3",
- "js-sha512": "^0.4.0",
+ "js-sha512": "^0.6.0",
"json3": "^3.3.2",
"locale": "^0.1.0",
"lodash": "^4.17.4",
"mathjax": "^2.7.0",
"md5": "^2.2.1",
"napa": "^2.3.0",
- "npm": "^4.2.0",
- "postcss": "^6.0.13",
"pre-commit": "^1.2.2",
"react": "^15.4.2",
"react-async-script": "~0.7.0",
@@ -124,34 +122,36 @@
"react-dnd": "^2.2.4",
"react-dnd-html5-backend": "^2.2.3",
"react-dom": "^15.4.2",
- "react-dropzone": "^3.11.0",
+ "react-dropzone": "^4.2.2",
"react-edit-inline": "^1.0.8",
"react-google-recaptcha": "^0.8.1",
- "react-hotkeys": "^0.9.0",
+ "react-hotkeys": "^0.10.0",
"react-image-cropper": "1.0.5",
"react-intl": "^2.2.3",
"react-intl-webpack-plugin": "0.0.3",
"react-list": "^0.8.6",
+ "react-qr-svg": "^2.1.0",
"react-resize-aware": "^1.0.11",
"react-responsive": "^1.2.6",
"react-share": "^1.16.0",
"request": "^2.80.0",
"request-promise": "^4.1.1",
"reveal": "0.0.4",
- "semantic-ui-react": "^0.74.2",
+ "semantic-ui-react": "^0.76.0",
"serialize-javascript": "^1.3.0",
"serve-favicon": "^2.4.1",
"smtp-connection": "^4.0.2",
+ "socket.io": "^2.0.3",
"striptags": "^2.1.1",
- "style-loader": "^0.18.2",
- "superagent": "^3.5.0",
+ "style-loader": "^0.19.0",
+ "superagent": "^3.8.1",
"superagent-csrf": "^1.0.0",
"superagent-csrf-middleware": "^0.3.0",
- "sweetalert2": "^6.4.2",
- "url-loader": "^0.5.8",
+ "sweetalert2": "^6.11.1",
+ "url-loader": "^0.6.2",
"uuid": "^3.0.1",
"virtual-dom": "2.1.1",
- "webpack": "^2.2.1",
+ "webpack": "^3.8.1",
"webpack-stats-plugin": "^0.1.5",
"webpack-visualizer-plugin": "^0.1.11",
"winston": "^2.3.1"
@@ -159,22 +159,25 @@
"devDependencies": {
"babel-eslint": "^7.1.1",
"chai": "^4.0.2",
- "coveralls": "^2.13.1",
+ "coveralls": "^3.0.0",
+ "enzyme": "3.1.0",
+ "enzyme-adapter-react-15": "1.0.2",
+ "css-loader": "^0.28.7",
"es6-promise": "^4.1.0",
"eslint": "^3.17.0",
"eslint-plugin-babel": "^4.1.1",
"eslint-plugin-react": "^6.10.0",
"husky": "^0.14.3",
"istanbul": "0.4.5",
- "jsdom": "^9.11.0",
+ "jsdom": "^9.12.0",
"json-loader": "^0.5.4",
"mocha": "^3.2.0",
"nodemon": "1.11.0",
"react-addons-test-utils": "^15.6.0",
"react-hot-loader": "1.3.1",
+ "react-test-renderer": "15.6.2",
"shelljs": "^0.7.8",
"sloc": "0.2.0",
- "webpack-dashboard": "^0.4.0",
"webpack-dev-server": "^2.4.1",
"webpack-vendor-chunk-plugin": "1.0.0"
}
diff --git a/plugins/googleAnalytics/ga.js b/plugins/googleAnalytics/ga.js
index 108e55c73..d0935e3dd 100644
--- a/plugins/googleAnalytics/ga.js
+++ b/plugins/googleAnalytics/ga.js
@@ -3,7 +3,7 @@ const googleAnalyticsID = 'UA-101110063-1';
export default `(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
- })(window,document,'script','//www.google-analytics.com/analytics.js','ga');
+ })(window,document,'script',('https:' == document.location.protocol ? 'https://ssl' : 'http://www') +'.google-analytics.com/analytics.js','ga');
ga('create', '${googleAnalyticsID}', 'auto');
ga('send', 'pageview');
diff --git a/server.js b/server.js
index 51fe9a66d..7b54acfb0 100644
--- a/server.js
+++ b/server.js
@@ -99,7 +99,9 @@ fetchrPlugin.registerService(require('./services/userProfile'));
fetchrPlugin.registerService(require('./services/suggester'));
fetchrPlugin.registerService(require('./services/logservice'));
fetchrPlugin.registerService(require('./services/like'));
+fetchrPlugin.registerService(require('./services/media'));
fetchrPlugin.registerService(require('./services/email'));
+fetchrPlugin.registerService(require('./services/media'));
fetchrPlugin.registerService(require('./services/userreview'));
diff --git a/server/handleServerRendering.js b/server/handleServerRendering.js
index c4579b2b1..9c27c4f95 100644
--- a/server/handleServerRendering.js
+++ b/server/handleServerRendering.js
@@ -5,6 +5,8 @@ import React from 'react';
import ReactDOM from 'react-dom/server';
import app from '../app';
import HTMLComponent from '../components/DefaultHTMLLayout';
+import PresentorComponent from '../components/PresentorHTMLLayout';
+import PresentationRoomsComponent from '../components/PresentationRoomsHTMLLayout';
import serialize from 'serialize-javascript';
import debugLib from 'debug';
@@ -39,7 +41,14 @@ let renderApp = function(req, res, context){
//todo: for future, we can choose to not include specific scripts in some predefined layouts
- const htmlElement = React.createElement(HTMLComponent, {
+ let layout = HTMLComponent;
+ if(req.url && req.url.slice(0,20).includes('/Presentation/')){//NOTE only test first few chars as presentaton rooms URL has "/Presentation/..." also in it
+ layout = PresentorComponent;
+ }
+ if(req.url && req.url.includes('/presentationbroadcast')){
+ layout = PresentationRoomsComponent;
+ }
+ const htmlElement = React.createElement(layout, {
//clientFile: env === 'production' ? 'main.min.js' : 'main.js',
clientFile: 'main.js',
addAssets: (env === 'production'),
diff --git a/services/activities.js b/services/activities.js
index dbafaed50..2bbc108d8 100644
--- a/services/activities.js
+++ b/services/activities.js
@@ -52,11 +52,15 @@ export default {
log.info({Id: req.reqId, Service: __filename.split('/').pop(), Resource: resource, Operation: 'read', Method: req.method});
let args = params.params? params.params : params;
+ let headers = {};
+ if (args.jwt) headers['----jwt----'] = args.jwt;
+
switch (resource) {
case 'activities.new':
rp.post({
uri: Microservices.activities.uri + '/activity/new',
- body: JSON.stringify(args.activity)
+ body: JSON.stringify(args.activity),
+ headers,
}).then((res) => {
callback(null, {activity: JSON.parse(res)});
}).catch((err) => {
@@ -67,7 +71,8 @@ export default {
case 'activities.newarray':
rp.post({
uri: Microservices.activities.uri + '/activities/new',
- body: JSON.stringify(args.activities)
+ body: JSON.stringify(args.activities),
+ headers,
}).then((res) => {
callback(null, {activities: JSON.parse(res)});
}).catch((err) => {
diff --git a/services/deck.js b/services/deck.js
index a2ffd57fb..470d8193c 100644
--- a/services/deck.js
+++ b/services/deck.js
@@ -236,6 +236,22 @@ export default {
body: toSend
}).then((deck) => callback(false, deck))
.catch((err) => callback(err));
+ } else if (resource === 'deck.translate'){
+
+ let toSend = {
+ language: params.language
+ };
+ rp({
+ method: 'PUT',
+ uri: Microservices.deck.uri + '/deck/' + params.deckId + '/translate',
+ json: true,
+ headers: {'----jwt----': params.jwt},
+ body: toSend
+ }).then((data) => {
+ //console.log('DECK:' + JSON.stringify(data.root_deck));
+ callback(false, data);
+ })
+ .catch((err) => callback(err));
}
},
update: (req, resource, params, body, config, callback) => {
diff --git a/services/like.js b/services/like.js
index e807ebcb0..04b7d9b90 100644
--- a/services/like.js
+++ b/services/like.js
@@ -70,6 +70,10 @@ export default {
//console.log("LIKE Deck id: " + targetDeckID + " User id: " + params.userid);
/*********connect to microservices*************/
//update backend store
+
+ let headers = {};
+ if (args.jwt) headers['----jwt----'] = args.jwt;
+
rp.post({
uri: Microservices.activities.uri + '/activity/new',
body:JSON.stringify({
@@ -78,7 +82,8 @@ export default {
content_id: String(targetDeckID),
content_kind: 'deck',
react_type: 'like'
- })
+ }),
+ headers,
}).then((res) => {
callback(null, {userid: String(params.userid), username: params.username, selector: args.selector});
}).catch((err) => {
diff --git a/services/media.js b/services/media.js
new file mode 100644
index 000000000..ef36c7bb5
--- /dev/null
+++ b/services/media.js
@@ -0,0 +1,73 @@
+import {Microservices} from '../configs/microservices';
+import rp from 'request-promise';
+import { isEmpty } from '../common.js';
+const log = require('../configs/log').log;
+import formdata from 'form-data';
+
+export default {
+ name: 'media',
+ create: (req, resource, params, config, emptyObject, callback) => {
+ if (resource === 'media.create') {
+ // It was hard to send a files data to the service.
+ // giving the file as parameter to here is not possible because it gets parsed before send via https
+ // FileReader does not create Array buffers (is just undefined)
+ // Not all outputs of FileReader are accepted by the API
+ // form-data could not be used because the API does not expect multiform
+
+ //NOTE available but currently not used params: copyrightHolderURL and copyrightAdditions
+ let holder = '';
+ //if(context.getUser() && context.getUser().username) holder = context.getUser().username + ', id=' + params.userID; else holder = params.userID;
+ if (params.license === 'CC0')
+ holder = '';
+ else if (params.copyrightHolder === '' && params.copyrightHolder === '')
+ holder = '©rightHolder=' + encodeURIComponent(params.userID); //NOTE prefer to use a real world name or the username at SlideWiki + it's ID
+ else
+ holder = '©rightHolder=' + encodeURIComponent(params.copyrightHolder); //NOTE prefer to use a real world name or the username at SlideWiki + it's ID
+
+ let url = Microservices.file.uri + '/v2/picture?' +
+ 'license=' + encodeURIComponent(params.license) +
+ holder +
+ '&title=' + encodeURIComponent(params.title) +
+ '&altText='+encodeURIComponent(params.text);
+ let headers = {
+ '----jwt----': params.jwt,
+ 'content-type': params.type
+ };
+ rp.post({
+ uri: url,
+ body: new Buffer(params.bytes.replace(/^data:image\/(png|jpg|jpeg);base64,/, ''), 'base64'),
+ headers: headers,
+ json: false
+ })
+ .then((res) => {
+ console.log('response from saving image:', res);
+ //callback(null, res);
+ callback(null, JSON.parse(res));
+ })
+ .catch((err) => {
+ console.log('Error while saving image', (err.response) ? {body: err.response.body, headers: err.response.request.headers} : err);
+ callback(err, null);
+ });
+ }
+ else if (resource === 'media.uploadProfilePicture') {
+ let url = Microservices.file.uri + '/profilepicture/' + params.username;
+ let headers = {
+ '----jwt----': params.jwt,
+ 'content-type': params.type
+ };
+ rp.put({
+ uri: url,
+ body: new Buffer(params.bytes.replace(/^data:image\/(png|jpg|jpeg);base64,/, ''), 'base64'),
+ headers: headers
+ })
+ .then((res) => {
+ // console.log('media: response from saving image:', res);
+ callback(null, Microservices.file.uri + JSON.parse(res).url);
+ })
+ .catch((err) => {
+ // console.log('media: Error while saving image', (err.response) ? {body: err.response.body, headers: err.response.request.headers} : err);
+ callback(err, null);
+ });
+ }
+ }
+};
diff --git a/services/presentation.js b/services/presentation.js
index ce214fe38..61c3b5d23 100644
--- a/services/presentation.js
+++ b/services/presentation.js
@@ -21,6 +21,7 @@ export default {
//let theme = get_sample_theme();
let isSubdeck = selector.id !== selector.subdeck;
let id = isSubdeck ? selector.subdeck : selector.id;
+ console.log( Microservices.deck.uri + '/deck/' + String(id) + '/slides');
rp.get({uri: Microservices.deck.uri + '/deck/' + String(id) + '/slides'}).then((res) => {
slideServiceRes = JSON.parse(res);
@@ -28,8 +29,17 @@ export default {
}).catch((err) => {
returnErr = true;
- callback(null, {content: slideServiceRes, theme: theme, selector: selector});
+ callback(null, {content: slideServiceRes, theme: undefined, selector: selector});
});
}//If presentation.content
+ else if(resource === 'presentation.live'){
+ rp.get({uri: Microservices.webrtc.uri + '/rooms/' + String(args.id)}).then((res) => {
+ // console.log('presentation.live returned', res);
+ callback(null, JSON.parse(res));
+ }).catch((err) => {
+ // console.log('Error:', err);
+ callback(err);
+ });
+ }
}
};
diff --git a/services/questions.js b/services/questions.js
index 393cfce29..015b2657f 100644
--- a/services/questions.js
+++ b/services/questions.js
@@ -20,24 +20,24 @@ export default {
uri: 'https://questionservice.experimental.slidewiki.org/questions',
//uri: Microservices.questions.uri + '/' + args.stype + '/' + args.sid + '/' + 'questions',
}).then((res) => {
- /* This is what we get from microservice */
- /*
- [{"related_object":"slide","related_object_id":"10678","question":"string","user_id":"17","difficulty":1,"choices":[{"choice":"string","is_correct":true,"explanation":"string"}],"id":10},
- {"related_object":"slide","related_object_id":"1141","question":"question 2","user_id":"17","difficulty":2,"choices":[{"choice":"string","is_correct":true,"explanation":"string"},
- {"choice":"string","is_correct":true,"explanation":"string"}],"id":11}]
- */
+ /* This is what we get from microservice */
+ /*
+ let q = [{'related_object':'slide','related_object_id':'10678','question':'string','user_id':'17','difficulty':1,'choices':[{'choice':'string','is_correct':true}],'explanation':'string explanation','id':10},
+ {'related_object':'slide','related_object_id':'1141','question':'question 2','user_id':'17','difficulty':2,'choices':[{'choice':'string1','is_correct':true},{'choice':'string2','is_correct':true},{'choice':'string3','is_correct':false}],'explanation':'string1 string2 explanation','id':11}];
+ */
let questions = JSON.parse(res)
- .map((item, index) => {
- return {
- id: item.id, title: item.question, difficulty: item.difficulty, relatedObject: item.related_object, relatedObjectId: item.related_object_id,
- answers: item.choices
- .map((ans, ansIndex) => {
- return {answer: ans.choice, correct: ans.is_correct, explanation: ans.explanation};
- }),
- userId: item.user_id,
- };
- }
- );
+ // let questions = q
+ .map((item, index) => {
+ return {
+ id: item.id, title: item.question, difficulty: item.difficulty, relatedObject: item.related_object, relatedObjectId: item.related_object_id,
+ answers: item.choices
+ .map((ans, ansIndex) => {
+ return {answer: ans.choice, correct: ans.is_correct};
+ }),
+ explanation: item.explanation,
+ userId: item.user_id,
+ };
+ });
callback(null, {questions: questions, totalLength: 2, selector: selector});
}).catch((err) => {
console.log('Questions get errored. Check via swagger for following object and id:', args.stype, args.sid);
diff --git a/services/translation.js b/services/translation.js
index 4f79904db..8cef4fbf3 100644
--- a/services/translation.js
+++ b/services/translation.js
@@ -1,3 +1,5 @@
+import {Microservices} from '../configs/microservices';
+import rp from 'request-promise';
const log = require('../configs/log').log;
export default {
@@ -8,31 +10,32 @@ export default {
log.info({Id: req.reqId, Service: __filename.split('/').pop(), Resource: resource, Operation: 'read', Method: req.method});
let args = params.params? params.params : params;
if(resource === 'translation.list'){
- /*********connect to microservices*************/
- //todo
- /*********received data from microservices*************/
- let translations = [];
-
- if(args.sid%2===0){
- translations=[
- {'lang': 'EN', 'id': 343},
- {'lang': 'DE', 'id': 32},
- {'lang': 'FR', 'id': 64}
- ];
- }
- else{
- translations = [
- {'lang': 'EN', 'id': 343},
- {'lang': 'ES', 'id': 56},
- {'lang': 'GR', 'id': 71},
- {'lang': 'FA', 'id': 81}
- ];
- }
-
-
- let currentLang = {'lang': 'EN', 'id': 343};
- callback(null, {translations: translations, currentLang: currentLang});
+ let deck_id = parseInt(args.sid.split('-')[0]);
+ let translations = [];
+ let currentLang = {};
+ rp({
+ method: 'GET',
+ json: true,
+ uri: Microservices.deck.uri + '/deck/' + deck_id + '/translations',
+ }).then((res) => {
+ callback(null, res);
+ }).catch((err) => {
+ callback(err, {translations: [], currentLang: currentLang});
+ });
+ }
+ if (resource === 'translation.supported'){
+ rp.get({uri: Microservices.translation.uri + '/supported'}).then((res) => {
+ callback(null, {supportedLangs: JSON.parse(res)});
+ }).catch((err) => {
+ callback(err, {supportedLangs: []});
+ });
+ // let supportedLangs = [];
+ // supportedLangs = [
+ // {'language':'Afrikan', 'code':'af'},
+ // {'language':'Russian', 'code':'ru'}
+ // ];
+ // callback(null, {supportedLangs: supportedLangs});
}
}
// other methods
diff --git a/services/userProfile.js b/services/userProfile.js
index fbf730fdc..832f6214d 100644
--- a/services/userProfile.js
+++ b/services/userProfile.js
@@ -1,8 +1,13 @@
import rp from 'request-promise';
import { isEmpty } from '../common.js';
import { Microservices } from '../configs/microservices';
+import cookieParser from 'cookie';
+
const log = require('../configs/log').log;
+const user_cookieName = 'user_json_storage';
+const secondsCookieShouldBeValid = 60*60*24*14 ; //2 weeks
+
export default {
name: 'userProfile',
@@ -149,10 +154,11 @@ export default {
method: 'GET',
uri: Microservices.user.uri + '/user/' + params.params.id + '/profile',
headers: { '----jwt----': params.params.jwt },
- json: true
+ resolveWithFullResponse: true,
})
- .then((body) => {
+ .then((response) => {
//console.log(body);
+ let body = JSON.parse(response.body);
let converted = {
id: body._id,
uname: body.username,
@@ -168,7 +174,19 @@ export default {
providers: body.providers || [],
groups: !isEmpty(body.groups) ? body.groups : []
};
- callback(null, converted);
+ callback(null, converted, {
+ headers: {
+ 'Set-Cookie': cookieParser.serialize(user_cookieName, JSON.stringify({
+ username: body.username,
+ userid: body._id,
+ jwt: response.headers['----jwt----'],
+ }), {
+ maxAge: secondsCookieShouldBeValid,
+ sameSite: true,
+ path: '/',
+ }),
+ }
+ });
})
.catch((err) => callback(err));
} else {
diff --git a/stores/ActivityFeedStore.js b/stores/ActivityFeedStore.js
index 62d408432..bdd0ad184 100644
--- a/stores/ActivityFeedStore.js
+++ b/stores/ActivityFeedStore.js
@@ -8,6 +8,7 @@ class ActivityFeedStore extends BaseStore {
this.activities = [];
this.selector = {};
this.hasMore = true;
+ this.presentations = [];
}
updateActivities(payload) {
this.activities = payload.activities;
@@ -116,6 +117,11 @@ class ActivityFeedStore extends BaseStore {
}
}
}
+ updatePresentations(payload) {
+ // console.log('ActivityFeedStore: updatePresentations', payload);
+ this.presentations = payload;
+ this.emitChange();
+ }
getState() {
return {
activities: this.activities,
@@ -123,6 +129,7 @@ class ActivityFeedStore extends BaseStore {
selector: this.selector,
hasMore: this.hasMore,
wasFetch: this.wasFetch,
+ presentations: this.presentations
};
}
dehydrate() {
@@ -133,6 +140,7 @@ class ActivityFeedStore extends BaseStore {
this.activityType = state.activityType;
this.selector = state.selector;
this.hasMore = state.hasMore;
+ this.presentations = state.presentations;
}
}
@@ -147,7 +155,8 @@ ActivityFeedStore.handlers = {
'ADD_ACTIVITY_SUCCESS': 'addActivity',
'ADD_ACTIVITIES_SUCCESS': 'addActivities',
'LIKE_ACTIVITY_SUCCESS': 'addLikeActivity',
- 'DISLIKE_ACTIVITY_SUCCESS': 'removeLikeActivity'
+ 'DISLIKE_ACTIVITY_SUCCESS': 'removeLikeActivity',
+ 'LOAD_PRESENTATIONS_SUCCESS': 'updatePresentations'
};
export default ActivityFeedStore;
diff --git a/stores/DeckPageStore.js b/stores/DeckPageStore.js
index 94f6fb8b3..af7713418 100644
--- a/stores/DeckPageStore.js
+++ b/stores/DeckPageStore.js
@@ -9,6 +9,7 @@ class DeckPageStore extends BaseStore {
this.componentsStatus = {
'NavigationPanel': {visible: 1, columnSize: 16},
'TreePanel': {visible: 1, columnSize: 3},
+ 'SlideEditPanel': {visible: 1, columnSize: 3},
'ActivityFeedPanel': {visible: 1, columnSize: 3},
'ContentPanel': {visible: 1, columnSize: 10},
'ContentModulesPanel': {visible: 1, columnSize: 10}};
@@ -20,7 +21,7 @@ class DeckPageStore extends BaseStore {
this.emitChange();
}
restoreAll() {
- this.componentsStatus = {'NavigationPanel': {visible: 1, columnSize: 16}, 'TreePanel': {visible: 1, columnSize: 3}, 'ActivityFeedPanel': {visible: 1, columnSize: 3}, 'ContentPanel': {visible: 1, columnSize: 10}, 'ContentModulesPanel': {visible: 1, columnSize: 10}};
+ this.componentsStatus = {'NavigationPanel': {visible: 1, columnSize: 16}, 'TreePanel': {visible: 1, columnSize: 3}, 'SlideEditPanel': {visible: 1, columnSize: 3}, 'ActivityFeedPanel': {visible: 1, columnSize: 3}, 'ContentPanel': {visible: 1, columnSize: 10}, 'ContentModulesPanel': {visible: 1, columnSize: 10}};
this.emitChange();
}
expandContentPanel() {
@@ -35,6 +36,30 @@ class DeckPageStore extends BaseStore {
}
this.emitChange();
}
+ showSlideEditPanel() {
+ //hide all others than Navigation and Content
+ for(let c in this.componentsStatus){
+ if(c=== 'NavigationPanel') {
+ //this.componentsStatus[c].visible=0;
+ this.componentsStatus[c].visible=1;
+ this.componentsStatus[c].columnSize=16;
+ }else if(c=== 'SlideEditPanel'){
+ this.componentsStatus[c].visible=1;
+ this.componentsStatus[c].columnSize=3;
+ }else if(c=== 'ContentPanel'){
+ this.componentsStatus[c].visible=1;
+ this.componentsStatus[c].columnSize=10;
+ }else if(c=== 'ActivityFeedPanel'){
+ this.componentsStatus[c].visible=0;
+ this.componentsStatus[c].visible=1;
+ this.componentsStatus[c].columnSize=3;
+ }else{
+ this.componentsStatus[c].visible=0;
+ }
+ }
+
+ this.emitChange();
+ }
hideLeftColumn() {
//hide all others than Navigation and Content
for(let c in this.componentsStatus){
@@ -71,7 +96,8 @@ DeckPageStore.handlers = {
'UPDATE_DECK_PAGE_CONTENT': 'updateContent',
'EXPAND_CONTENET_PANEL': 'expandContentPanel',
'HIDE_LEFT_COLUMN': 'hideLeftColumn',
- 'RESTORE_DECK_PAGE_LAYOUT': 'restoreAll'
+ 'RESTORE_DECK_PAGE_LAYOUT': 'restoreAll',
+ 'SHOW_SLIDE_EDIT_PANEL': 'showSlideEditPanel'
};
export default DeckPageStore;
diff --git a/stores/DeckTreeStore.js b/stores/DeckTreeStore.js
index f096645e1..58160a660 100644
--- a/stores/DeckTreeStore.js
+++ b/stores/DeckTreeStore.js
@@ -356,20 +356,24 @@ class DeckTreeStore extends BaseStore {
} catch (e) {
//there might be the case when the node for old selector does not exist anymore
}
- selectedNodeIndex = this.makeImmSelectorFromPath(newSelector.get('spath'));
- this.deckTree = this.deckTree.updateIn(selectedNodeIndex,(node) => node.update('selected', (val) => true));
- this.selector = newSelector;
+ try {
+ selectedNodeIndex = this.makeImmSelectorFromPath(newSelector.get('spath'));
+ this.deckTree = this.deckTree.updateIn(selectedNodeIndex,(node) => node.update('selected', (val) => true));
+ this.selector = newSelector;
- //unfocus old focused node
- this.deckTree = this.deckTree.updateIn(this.makeImmSelectorFromPath(this.focusedSelector.get('spath')),(node) => node.update('focused', (val) => false));
- this.focusedSelector = newSelector;
- //if the root deck was selected, focus it's first node
- if (!this.focusedSelector.get('spath')) {
- this.focusedSelector = this.makeSelectorFromNode(this.findNextNode(this.flatTree, this.focusedSelector));
+ //unfocus old focused node
+ this.deckTree = this.deckTree.updateIn(this.makeImmSelectorFromPath(this.focusedSelector.get('spath')),(node) => node.update('focused', (val) => false));
+ this.focusedSelector = newSelector;
+ //if the root deck was selected, focus it's first node
+ if (!this.focusedSelector.get('spath')) {
+ this.focusedSelector = this.makeSelectorFromNode(this.findNextNode(this.flatTree, this.focusedSelector));
+ }
+ //update the focused node in the tree
+ this.deckTree = this.deckTree.updateIn(this.makeImmSelectorFromPath(this.focusedSelector.get('spath')),(node) => node.update('focused', (val) => true));
+ this.updatePrevNextSelectors();
+ } catch (e) {
+ //todo: handle unexpected events here
}
- //update the focused node in the tree
- this.deckTree = this.deckTree.updateIn(this.makeImmSelectorFromPath(this.focusedSelector.get('spath')),(node) => node.update('focused', (val) => true));
- this.updatePrevNextSelectors();
}
deleteTreeNode(selector, silent) {
let selectorIm = Immutable.fromJS(selector);
diff --git a/stores/MediaStore.js b/stores/MediaStore.js
new file mode 100644
index 000000000..3edd2c06c
--- /dev/null
+++ b/stores/MediaStore.js
@@ -0,0 +1,79 @@
+import {BaseStore} from 'fluxible/addons';
+
+class MediaStore extends BaseStore {
+ constructor(dispatcher) {
+ super(dispatcher);
+ this.status = '';
+ this.filetype = '';
+ this.filename = '';
+ /*
+ file looks like:
+ {
+ type: 'image/png',
+ license: 'CC0',
+ title: 'title',
+ text: 'alternative text',
+ filesize: 12873218637,
+ filename: 'image.png',
+ bytes: 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD/4TRDRXhpZgAATU0AKgAAAAgACwEPAAIAAAAIAAAAkgEQAAIAA',
+ url: 'https://fileservice.experimental.slidewiki.org/picture/fds243jsdalkfdsfdsfdsf.png',
+ thumbnailUrl: 'https://fileservice.experimental.slidewiki.org/picture/fds243jsdalkfdsfdsfdsf_thumbnail.png'
+ }
+ */
+ this.file = {};
+ }
+ destructor()
+ {
+ this.status = '';
+ this.filetype = '';
+ this.filename = '';
+ this.file = {};
+ }
+ getState() {
+ return {
+ status: this.status,
+ filetype: this.filetype,
+ filename: this.filename,
+ file: this.file
+ };
+ }
+ dehydrate() {
+ return this.getState();
+ }
+ rehydrate(state) {
+ this.status = state.status;
+ this.filetype = state.filetype;
+ this.filename = state.filename;
+ this.file = state.file;
+ }
+
+ startUploading(data) {
+ console.log('start upload store notify');
+ this.status = 'uploading';
+ this.filetype = data.type;
+ this.filename = data.name;
+ this.file = {};
+ this.emitChange();
+ }
+
+ failureUpload(error) {
+ this.status = 'error';
+ this.emitChange();
+ }
+
+ successUpload(data) {
+ console.log('MediaStore: successUpload()', data);
+ this.status = 'success';
+ this.file = data;
+ this.emitChange();
+ }
+}
+
+MediaStore.storeName = 'MediaStore';
+MediaStore.handlers = {
+ 'START_UPLOADING_MEDIA_FILE': 'startUploading',
+ 'FAILURE_UPLOADING_MEDIA_FILE': 'failureUpload',
+ 'SUCCESS_UPLOADING_MEDIA_FILE': 'successUpload'
+};
+
+export default MediaStore;
diff --git a/stores/SendReportStore.js b/stores/SendReportStore.js
index df138d71d..398de5ac8 100644
--- a/stores/SendReportStore.js
+++ b/stores/SendReportStore.js
@@ -50,15 +50,22 @@ class SendReportStore extends BaseStore {
this.emitChange();
}
- openReportModal(payload){
+ openReportModal(){
this.openModal = true;
this.activeTrap = true;
this.emitChange();
}
- closeReportModal(payload){
+ closeReportModal(){
this.openModal = false;
this.activeTrap = false;
+
+ this.wrongFields = {
+ reason: false,
+ text: false,
+ name: false
+ };
+ this.error = null;
this.emitChange();
}
@@ -76,8 +83,7 @@ SendReportStore.handlers = {
'REPORT_SHOW_WRONG_FIELDS': 'showWrongFields',
'REPORT_MODAL_OPEN': 'openReportModal',
'REPORT_MODAL_CLOSE': 'closeReportModal'
- //'CREATION_FAILURE': 'creationFailure',
- //'CREATION_SUCCESS': 'creationSuccess',
+
};
diff --git a/stores/SlideViewStore.js b/stores/SlideViewStore.js
index af4ba11e2..73bbf95df 100644
--- a/stores/SlideViewStore.js
+++ b/stores/SlideViewStore.js
@@ -9,6 +9,11 @@ class SlideViewStore extends BaseStore {
this.content = '';
this.speakernotes = '';
this.tags = [];
+ this.loadingIndicator = '';
+ }
+ loading(payload){
+ this.loadingIndicator = payload.loadingIndicator;
+ this.emitChange();
}
updateContent(payload) {
if (payload.slide.revisions !== undefined)
@@ -19,6 +24,7 @@ class SlideViewStore extends BaseStore {
this.content = lastRevision.content;
this.speakernotes = lastRevision.speakernotes;
this.tags = lastRevision.tags? lastRevision.tags: [];
+ this.loadingIndicator = 'false';
this.emitChange();
}
else
@@ -26,6 +32,7 @@ class SlideViewStore extends BaseStore {
this.title = 'title not found';
this.content = 'content not found';
this.tags = [];
+ this.loadingIndicator = 'false';
this.emitChange();
}
}
@@ -36,6 +43,7 @@ class SlideViewStore extends BaseStore {
content: this.content,
tags: this.tags,
speakernotes: this.speakernotes,
+ loadingIndicator: this.loadingIndicator
};
}
dehydrate() {
@@ -47,12 +55,14 @@ class SlideViewStore extends BaseStore {
this.content = state.content;
this.tags = state.tags;
this.speakernotes = state.speakernotes;
+ this.loadingIndicator = state.loadingIndicator;
}
}
SlideViewStore.storeName = 'SlideViewStore';
SlideViewStore.handlers = {
- 'LOAD_SLIDE_CONTENT_SUCCESS': 'updateContent'
+ 'LOAD_SLIDE_CONTENT_SUCCESS': 'updateContent',
+ 'LOAD_SLIDE_CONTENT_LOAD': 'loading'
};
export default SlideViewStore;
diff --git a/stores/TranslationStore.js b/stores/TranslationStore.js
index c653bf39b..7b7f177be 100644
--- a/stores/TranslationStore.js
+++ b/stores/TranslationStore.js
@@ -5,31 +5,62 @@ class TranslationStore extends BaseStore {
super(dispatcher);
this.translations = [];
this.currentLang = {};
+ this.supportedLangs = [];
+ this.inProgress = false;
+ }
+ startTranslation(payload){
+ this.inProgress = true;
+ this.emitChange();
+ }
+ endTranslation(payload){
+ this.inProgress = false;
+ this.emitChange();
}
updateTranslations(payload) {
this.translations = payload.translations;
this.currentLang = payload.currentLang;
this.emitChange();
}
+ loadSupportedLangs(payload) {
+ this.supportedLangs = payload.supportedLangs;
+ this.emitChange();
+ }
getState() {
return {
translations: this.translations,
currentLang: this.currentLang,
+ supportedLangs: this.supportedLangs,
+ inProgress: this.inProgress
+ };
+ }
+ getSupportedLangs(){
+ return {
+ supportedLangs: this.supportedLangs
};
}
dehydrate() {
- return this.getState();
+ return {
+ translations: this.translations,
+ currentLang: this.currentLang,
+ supportedLangs: this.supportedLangs,
+ inProgress: this.inProgress
+ };
}
rehydrate(state) {
this.translations = state.translations;
this.currentLang = state.currentLang;
+ this.supportedLangs = state.supportedLangs;
+ this.inProgress = state.inProgress;
}
}
TranslationStore.storeName = 'TranslationStore';
TranslationStore.handlers = {
- 'LOAD_TRANSLATIONS_SUCCESS': 'updateTranslations'
+ 'LOAD_TRANSLATIONS_SUCCESS': 'updateTranslations',
+ 'LOAD_SUPPORTED_LANGS_SUCCESS': 'loadSupportedLangs',
+ 'START_TRANSLATION' : 'startTranslation',
+ 'END_TRANSLATION' : 'endTranslation'
};
export default TranslationStore;
diff --git a/stores/UserNotificationsStore.js b/stores/UserNotificationsStore.js
index 87d4f93da..678c68050 100644
--- a/stores/UserNotificationsStore.js
+++ b/stores/UserNotificationsStore.js
@@ -3,13 +3,15 @@ import {BaseStore} from 'fluxible/addons';
class UserNotificationsStore extends BaseStore {
constructor(dispatcher) {
super(dispatcher);
- this.notifications = undefined;
+ this.notifications = [];
this.newNotifications = [];
this.newNotificationsCount = 0;
this.subscriptions = [];
+ this.loading = true;
this.activityTypes = [
{type:'add', selected: true},
{type:'edit', selected: true},
+ {type:'move', selected: true},
{type:'comment', selected: true},
{type:'reply', selected: true},
{type:'download', selected: true},
@@ -25,10 +27,15 @@ class UserNotificationsStore extends BaseStore {
{type:'left', selected: true}
];
}
+ showLoading(payload){
+ this.loading = true;
+ this.emitChange();
+ }
loadNotifications(payload) {
this.notifications = payload.notifications;
this.newNotifications = payload.newNotifications;
this.subscriptions = payload.subscriptions;
+ this.loading = false;
this.markNewNotifications();
this.newNotificationsCount = this.newNotifications.length;
@@ -179,6 +186,7 @@ class UserNotificationsStore extends BaseStore {
notifications: this.notifications,
newNotifications: this.newNotifications,
newNotificationsCount: this.newNotificationsCount,
+ loading: this.loading,
subscriptions: this.subscriptions,
activityTypes: this.activityTypes
};
@@ -190,6 +198,7 @@ class UserNotificationsStore extends BaseStore {
this.notifications = state.notifications;
this.newNotifications = state.newNotifications;
this.newNotificationsCount = state.newNotificationsCount;
+ this.loading = state.loading;
this.subscriptions = state.subscriptions;
this.activityTypes = state.activityTypes;
}
@@ -202,7 +211,8 @@ UserNotificationsStore.handlers = {
'LOAD_NEW_USER_NOTIFICATIONS_COUNT_SUCCESS': 'loadNewNotificationsCount',
'UPDATE_NOTIFICATIONS_VISIBILITY': 'updateNotificationsVisibility',
'DELETE_USER_NOTIFICATION_SUCCESS': 'clearNotificationNewParameter',
- 'DELETE_ALL_USER_NOTIFICATIONS_SUCCESS': 'clearAllNotificationsNewParameter'
+ 'DELETE_ALL_USER_NOTIFICATIONS_SUCCESS': 'clearAllNotificationsNewParameter',
+ 'SHOW_NOTIFICATIONS_LOADING': 'showLoading'
};
export default UserNotificationsStore;
diff --git a/stores/UserProfileStore.js b/stores/UserProfileStore.js
index 4adfd366a..39c369865 100644
--- a/stores/UserProfileStore.js
+++ b/stores/UserProfileStore.js
@@ -307,7 +307,7 @@ class UserProfileStore extends BaseStore {
updateUsergroup(group) {
this.currentUsergroup = group;
- console.log('UserProfileStore: updateUsergroup', group);
+ // console.log('UserProfileStore: updateUsergroup', group);
this.saveUsergroupError = '';
this.deleteUsergroupError = '';
this.emitChange();
diff --git a/test/components/UserPicture.test.js b/test/components/UserPicture.test.js
new file mode 100644
index 000000000..8ac9bb4e9
--- /dev/null
+++ b/test/components/UserPicture.test.js
@@ -0,0 +1,43 @@
+import React from 'react';
+import {expect} from 'chai';
+import {shallow} from 'enzyme';
+
+import UserPicture from '../../components/common/UserPicture';
+
+describe('(Component) UserPicture', () => {
+
+ it('renders without exploding', () => {
+ const wrapper = shallow();
+
+ expect(wrapper).to.have.length(1);
+ });
+
+ it('renders a with
if picture prop IS NOT passed', () => {
+ const wrapper = shallow();
+
+ expect(wrapper.find('Identicons')).to.have.length(1);
+ expect(wrapper.find('img')).to.have.length(0);
+ });
+
+ it('renders an
if picture prop IS passed', () => {
+ let props = {
+ picture: 'https://www.gravatar.com/test'
+ };
+
+ const wrapper = shallow();
+
+ expect(wrapper.find('img')).to.have.length(1);
+ expect(wrapper.find('Identicons')).to.have.length(0);
+ });
+
+ it('wraps the picture inside an if link prop IS passed', () => {
+ let props = {
+ picture: 'https://www.gravatar.com/test',
+ link: 'https://a.link.com/'
+ };
+ const wrapper = shallow();
+
+ expect(wrapper.find('a')).to.have.length(1);
+ });
+
+});
diff --git a/test/mocha.opts b/test/mocha.opts
deleted file mode 100644
index ac2553a74..000000000
--- a/test/mocha.opts
+++ /dev/null
@@ -1 +0,0 @@
---require test/setup.js
diff --git a/test/setup.js b/test/setup.js
index f0eba5e5f..e72c3f084 100644
--- a/test/setup.js
+++ b/test/setup.js
@@ -1,10 +1,6 @@
-'use strict';
+require('babel-register')();
+// setup file
+var enzyme = require('enzyme');
+var Adapter = require('enzyme-adapter-react-15');
-let jsdom = require('jsdom');
-
-const DEFAULT_HTML = '';
-global.document = jsdom.jsdom(DEFAULT_HTML);
-
-global.window = document.defaultView;
-
-global.navigator = window.navigator;
+enzyme.configure({ adapter: new Adapter() });
diff --git a/test/unit/ContentModulesStore.js b/test/unit/ContentModulesStore.js
index ad81fd5b1..7b52ca728 100644
--- a/test/unit/ContentModulesStore.js
+++ b/test/unit/ContentModulesStore.js
@@ -2,7 +2,8 @@ import React from 'react';
import createMockComponentContext from 'fluxible/utils/createMockComponentContext';
import provideContext from 'fluxible-addons-react/provideContext';
import connectToStores from 'fluxible-addons-react/connectToStores';
-import TestUtils from 'react-addons-test-utils';
+//import TestUtils from 'react-addons-test-utils';
+import TestUtils from 'react-dom/test-utils';
import {expect} from 'chai';
import ContentModuleStore from '../../stores/ContentModulesStore';
diff --git a/test/unit/index.html b/test/unit/index.html
deleted file mode 100644
index 01814083a..000000000
--- a/test/unit/index.html
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
-
-
-
-
-
-
diff --git a/webpack/dev-server.js b/webpack/dev-server.js
index 43f208307..606f6f87b 100644
--- a/webpack/dev-server.js
+++ b/webpack/dev-server.js
@@ -3,7 +3,6 @@ let WebpackDevServer = require ('webpack-dev-server');
let webpack = require ('webpack');
let config = require ('./dev.config');
let shell = require ('shelljs');
-let DashboardPlugin = require('webpack-dashboard/plugin');
const host = process.env.HOST ? process.env.HOST : '0.0.0.0';
const mainPort = process.env.PORT ? parseInt(process.env.PORT) : 3000;
@@ -27,10 +26,6 @@ const options = {
};
const compiler = webpack(config);
-//enable webpack dashboard on-demand
-if(process.env.DASHBOARD){
- compiler.apply(new DashboardPlugin());
-}
new WebpackDevServer(compiler, options).listen(mainPort, host, () => {
shell.env.PORT = shell.env.PORT || mainPort;
shell.exec('"./node_modules/.bin/nodemon" start.js -e js,jsx', () => {});