From a8a2cea0f03cf69ce4580ceeeda332a18ea7aac3 Mon Sep 17 00:00:00 2001 From: Roy Meissner Date: Tue, 21 Nov 2017 16:41:49 +0100 Subject: [PATCH 001/285] First attempts towards audio recordings --- components/PresentationRoomsHTMLLayout.js | 1 + components/webrtc/presentationBroadcast.js | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/components/PresentationRoomsHTMLLayout.js b/components/PresentationRoomsHTMLLayout.js index 4f1e5bd68..f1bda5a61 100644 --- a/components/PresentationRoomsHTMLLayout.js +++ b/components/PresentationRoomsHTMLLayout.js @@ -44,6 +44,7 @@ class PresentationRoomsHTMLLayout extends React.Component { {/* Main app bundle */} + - + + - - + {/**/} diff --git a/components/webrtc/SessionRecorder.js b/components/webrtc/SessionRecorder.js index 961b5de5d..e30ae99ab 100644 --- a/components/webrtc/SessionRecorder.js +++ b/components/webrtc/SessionRecorder.js @@ -11,22 +11,22 @@ class SessionRecorder extends React.Component { constructor(props) { super(props); this.mediaRecorder = undefined; - this.blobKeys = []; + this.chunkKeys = []; this.state = { recordSession: true }; this.createAudioTrack = this.createAudioTrack.bind(this); - this.saveBlobToDisk = this.saveBlobToDisk.bind(this); + this.saveTrackToDisk = this.saveTrackToDisk.bind(this); } componentWillUpdate(nextProps, nextState) { if(this.state.recordSession !== nextState.recordSession){ - this.blobKeys = []; - window.localforage.clear(); try { this.mediaRecorder.stop(); } catch (e) {} + this.chunkKeys = []; + window.localforage.clear(); } } @@ -53,16 +53,12 @@ class SessionRecorder extends React.Component { recordStream(stream) { if(this.state.recordSession){ - // console.log('Initializing recorder'); - this.mediaRecorder = new MediaStreamRecorder(stream); - this.mediaRecorder.stream = stream; - // this.mediaRecorder.disableLogs = true; - this.mediaRecorder.mimeType = 'audio/ogg'; - this.mediaRecorder.ondataavailable = (blob) => { - console.log('New blob available'); + this.mediaRecorder = new MediaRecorder(stream); + this.mediaRecorder.ondataavailable = (chunk) => { + console.log('New chunk available', chunk); let now = new Date().getTime(); - this.blobKeys.push(now.toString()); - window.localforage.setItem(now.toString(), blob); //TODO implement catch for promise + this.chunkKeys.push({id: now.toString(), timecode: chunk.timecode}); + window.localforage.setItem(now.toString(), chunk.data); //TODO implement catch for promise }; console.log('starting recorder'); this.mediaRecorder.start(5000);//NOTE 5000 is the only option that works @@ -113,8 +109,8 @@ class SessionRecorder extends React.Component { this.mediaRecorder.stop(); this.recordSlideChange(); let timingBlob = new Blob([sessionStorage.getItem('slideTimings')], {type: 'application/json'}); - this.saveBlobToDisk(timingBlob, 'timings.json');//TODO last recording is just a time, but no slide url as this component triggers it and this.currentSlide is not available in this component - this.createAudioTrack(); + // this.saveBlobToDisk(timingBlob, 'timings.json');//TODO last recording is just a time, but no slide url as this component triggers it and this.currentSlide is not available in this component + setTimeout(this.createAudioTrack, 500); }).catch((e) => { if(e === 'cancel'){ this.mediaRecorder.resume(); @@ -123,16 +119,17 @@ class SessionRecorder extends React.Component { } createAudioTrack() { - let promises = this.blobKeys.map((key) => window.localforage.getItem(key)); + let promises = this.chunkKeys.map((obj) => window.localforage.getItem(obj.id)); Promise.all(promises).then((blobArray) => { - let safeBlobArray = (isEmpty(blobArray)) ? [] : blobArray; - console.log(safeBlobArray); - //let blob = new Blob(safeBlobArray, { 'type' : safeBlobArray[0].type });//NOTE only works on FF kinda correctly, chrome creates a correct file, but playback stops after 5s - window.ConcatenateBlobs( safeBlobArray, safeBlobArray[0].type, (concatenatedBlob) => this.saveBlobToDisk(concatenatedBlob, 'test.webm') ); + let safeChunkArray = (isEmpty(blobArray)) ? [] : blobArray;//TODO implement better version + console.log(safeChunkArray); + let track = new Blob(safeChunkArray, { 'type' : 'audio/ogg; codecs=opus' });//NOTE it is currently video/webm in chrome by default, but only a audio stream + console.log(track); + this.saveTrackToDisk(track, 'test.ogg'); }); } - saveBlobToDisk(file, fileName) { + saveTrackToDisk(file, fileName) { let hyperlink = document.createElement('a'); hyperlink.style.display = 'none'; hyperlink.href = URL.createObjectURL(file); diff --git a/components/webrtc/create_video.sh b/components/webrtc/create_video.sh index 30b92c725..f270c3ff6 100644 --- a/components/webrtc/create_video.sh +++ b/components/webrtc/create_video.sh @@ -13,4 +13,7 @@ # duration 2 # 4. -ffmpeg -f concat -safe 0 -i pics.txt -i audio.webm -vsync cfr -c:v libx265 -preset faster -c:a libvorbis output.mkv +#sizes are wrong by defaults +ffmpeg -i audio.ogg -c copy audio.ogg + +ffmpeg -f concat -safe 0 -i pics.txt -i audio.ogg -vsync cfr -c:v libx265 -preset faster -c:a libvorbis output.mkv From 17f74beb1362a5f6040e68ebea3acf1f58f43381 Mon Sep 17 00:00:00 2001 From: Roy Meissner Date: Mon, 5 Feb 2018 16:19:11 +0100 Subject: [PATCH 009/285] Experimented with codecs, added proper chrome+firefox support --- components/webrtc/SessionRecorder.js | 15 ++++++++++----- components/webrtc/create_video.sh | 11 ++++++----- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/components/webrtc/SessionRecorder.js b/components/webrtc/SessionRecorder.js index e30ae99ab..18be094d8 100644 --- a/components/webrtc/SessionRecorder.js +++ b/components/webrtc/SessionRecorder.js @@ -18,6 +18,7 @@ class SessionRecorder extends React.Component { this.createAudioTrack = this.createAudioTrack.bind(this); this.saveTrackToDisk = this.saveTrackToDisk.bind(this); + this.mime = ''; } componentWillUpdate(nextProps, nextState) { @@ -53,15 +54,17 @@ class SessionRecorder extends React.Component { recordStream(stream) { if(this.state.recordSession){ - this.mediaRecorder = new MediaRecorder(stream); + let webm = 'audio/webm\;codecs=opus', ogg = 'audio/ogg\;codecs=opus'; + this.mime = (MediaRecorder.isTypeSupported(ogg)) ? ogg : webm; + this.mediaRecorder = new MediaRecorder(stream, {mimeType : this.mime, audioBitsPerSecond: 64000});//64kbit/s opus this.mediaRecorder.ondataavailable = (chunk) => { - console.log('New chunk available', chunk); + // console.log('New chunk available', chunk); let now = new Date().getTime(); this.chunkKeys.push({id: now.toString(), timecode: chunk.timecode}); window.localforage.setItem(now.toString(), chunk.data); //TODO implement catch for promise }; console.log('starting recorder'); - this.mediaRecorder.start(5000);//NOTE 5000 is the only option that works + this.mediaRecorder.start(2000);//NOTE 5000 is the only option that works } } @@ -123,9 +126,11 @@ class SessionRecorder extends React.Component { Promise.all(promises).then((blobArray) => { let safeChunkArray = (isEmpty(blobArray)) ? [] : blobArray;//TODO implement better version console.log(safeChunkArray); - let track = new Blob(safeChunkArray, { 'type' : 'audio/ogg; codecs=opus' });//NOTE it is currently video/webm in chrome by default, but only a audio stream + let track = new Blob(safeChunkArray, { 'type' : this.mime }); console.log(track); - this.saveTrackToDisk(track, 'test.ogg'); + let name = 'test' + ((this.mime.contains('webm')) ? '.webm' : '.ogg'); + console.log(name); + this.saveTrackToDisk(track, name); }); } diff --git a/components/webrtc/create_video.sh b/components/webrtc/create_video.sh index f270c3ff6..8adabd4fc 100644 --- a/components/webrtc/create_video.sh +++ b/components/webrtc/create_video.sh @@ -1,7 +1,7 @@ #!/bin/bash # 1. download images of slides to folder, name like 01.png, 02.png, ... -# 2. calculate durations of each image +# 2. calculate durations of each image from the provided json file # 3. create a text file that contains : # file '/path/to/01.png' # duration 5 @@ -11,9 +11,10 @@ # duration 3 # file '/path/to/04.png' # duration 2 -# 4. +# 4 run: -#sizes are wrong by defaults -ffmpeg -i audio.ogg -c copy audio.ogg -ffmpeg -f concat -safe 0 -i pics.txt -i audio.ogg -vsync cfr -c:v libx265 -preset faster -c:a libvorbis output.mkv +# webm VP9/opus is supported by most modern browsers, fallback is h264/aac(MP4), that is supported on all browsers +ffmpeg -f concat -safe 0 -i pics.txt -i webm.webm -vsync cfr -c:v libx264 -tune stillimage -c:a aac -b:a 64k output.mp4 # fastest (4.1x)m audio quality might be worse than with opus +ffmpeg -f concat -safe 0 -i pics.txt -i webm.webm -vsync cfr -c:v libx265 -c:a libopus -application voip output.mkv # middle (0.8) same size as h264 +ffmpeg -f concat -safe 0 -i pics.txt -i webm.webm -vsync cfr -c:v libvpx-vp9 -c:a libopus -application voip output.webm # slowest (0.25x), biggest file From dfbbbcfa78d5f1028582494aef9fcb8a4ca05f64 Mon Sep 17 00:00:00 2001 From: Roy Meissner Date: Wed, 14 Feb 2018 16:58:02 +0100 Subject: [PATCH 010/285] Implemented upload to file-service and fixed a few video out of sync bugs --- components/webrtc/SessionRecorder.js | 47 +++++++++++++++++----- components/webrtc/create_video.sh | 20 --------- components/webrtc/presentationBroadcast.js | 21 +++++----- 3 files changed, 48 insertions(+), 40 deletions(-) delete mode 100644 components/webrtc/create_video.sh diff --git a/components/webrtc/SessionRecorder.js b/components/webrtc/SessionRecorder.js index 18be094d8..808c8e6a4 100644 --- a/components/webrtc/SessionRecorder.js +++ b/components/webrtc/SessionRecorder.js @@ -1,6 +1,7 @@ import React from 'react'; import { isEmpty } from '../../common'; import { Button, Icon } from 'semantic-ui-react'; +import { Microservices } from '../../configs/microservices'; class SessionRecorder extends React.Component { @@ -53,10 +54,14 @@ class SessionRecorder extends React.Component { } recordStream(stream) { + this.stream = stream; + } + + startRecording() { if(this.state.recordSession){ let webm = 'audio/webm\;codecs=opus', ogg = 'audio/ogg\;codecs=opus'; this.mime = (MediaRecorder.isTypeSupported(ogg)) ? ogg : webm; - this.mediaRecorder = new MediaRecorder(stream, {mimeType : this.mime, audioBitsPerSecond: 64000});//64kbit/s opus + this.mediaRecorder = new MediaRecorder(this.stream, {mimeType : this.mime, audioBitsPerSecond: 64000});//64kbit/s opus this.mediaRecorder.ondataavailable = (chunk) => { // console.log('New chunk available', chunk); let now = new Date().getTime(); @@ -74,12 +79,12 @@ class SessionRecorder extends React.Component { sessionStorage.setItem('deck', deckID); sessionStorage.setItem('origin', window.location.origin); sessionStorage.setItem('slideTimings', '');//clear it - this.recordSlideChange(url, true); + this.recordSlideChange(url); } } } - recordSlideChange(url = '', first = false) { + recordSlideChange(url = '') { if(this.state.recordSession){ // console.log('recording slide change', url, first); if(window.sessionStorage){ @@ -88,7 +93,7 @@ class SessionRecorder extends React.Component { prev = JSON.parse(prev); let now = new Date().getTime(); let newEl = {}; - newEl[now] = ((first) ? sessionStorage.getItem('origin') : '') + url; + newEl[now] = url; let toSave = Object.assign(prev, newEl); sessionStorage.setItem('slideTimings', JSON.stringify(toSave)); } @@ -111,9 +116,9 @@ class SessionRecorder extends React.Component { }).then(() => { this.mediaRecorder.stop(); this.recordSlideChange(); - let timingBlob = new Blob([sessionStorage.getItem('slideTimings')], {type: 'application/json'}); - // this.saveBlobToDisk(timingBlob, 'timings.json');//TODO last recording is just a time, but no slide url as this component triggers it and this.currentSlide is not available in this component - setTimeout(this.createAudioTrack, 500); + // let timingBlob = new Blob([sessionStorage.getItem('slideTimings')], {type: 'application/json'}); + // this.saveBlobToDisk(timingBlob, 'timings.json'); + setTimeout(this.createAudioTrack, 500);//NOTE Wait for recorder to write last chunk }).catch((e) => { if(e === 'cancel'){ this.mediaRecorder.resume(); @@ -128,9 +133,31 @@ class SessionRecorder extends React.Component { console.log(safeChunkArray); let track = new Blob(safeChunkArray, { 'type' : this.mime }); console.log(track); - let name = 'test' + ((this.mime.contains('webm')) ? '.webm' : '.ogg'); - console.log(name); - this.saveTrackToDisk(track, name); + let trackName = 'test' + ((this.mime.includes('webm')) ? '.webm' : '.ogg'); + // console.log(name); + // this.saveTrackToDisk(track, trackName); + this.uploadTrack(track, trackName, sessionStorage.getItem('slideTimings')); + }); + } + + uploadTrack(audioTrack, audioTrackName, slideTimings) { + let form = new FormData(); + form.append('slideTimings', slideTimings); + form.append('audioFile', audioTrack, audioTrackName); + + $.ajax({ + url: Microservices.file.uri + '/PRvideo?deckID=' + this.props.deckID +'&revision=' + (this.props.revision ? this.props.revision : 1), + data: form, + cache: false, + contentType: false, + processData: false, + method: 'POST', + success: ( data, textStatus, jqXHR ) => { + console.log(textStatus); + }, + error: ( jqXHR, textStatus, errorThrown) => { + console.log(textStatus, errorThrown, jqXHR); + } }); } diff --git a/components/webrtc/create_video.sh b/components/webrtc/create_video.sh deleted file mode 100644 index 8adabd4fc..000000000 --- a/components/webrtc/create_video.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/bin/bash - -# 1. download images of slides to folder, name like 01.png, 02.png, ... -# 2. calculate durations of each image from the provided json file -# 3. create a text file that contains : -# file '/path/to/01.png' -# duration 5 -# file '/path/to/02.png' -# duration 1 -# file '/path/to/03.png' -# duration 3 -# file '/path/to/04.png' -# duration 2 -# 4 run: - - -# webm VP9/opus is supported by most modern browsers, fallback is h264/aac(MP4), that is supported on all browsers -ffmpeg -f concat -safe 0 -i pics.txt -i webm.webm -vsync cfr -c:v libx264 -tune stillimage -c:a aac -b:a 64k output.mp4 # fastest (4.1x)m audio quality might be worse than with opus -ffmpeg -f concat -safe 0 -i pics.txt -i webm.webm -vsync cfr -c:v libx265 -c:a libopus -application voip output.mkv # middle (0.8) same size as h264 -ffmpeg -f concat -safe 0 -i pics.txt -i webm.webm -vsync cfr -c:v libvpx-vp9 -c:a libopus -application voip output.webm # slowest (0.25x), biggest file diff --git a/components/webrtc/presentationBroadcast.js b/components/webrtc/presentationBroadcast.js index f63dabb27..41768f4fa 100644 --- a/components/webrtc/presentationBroadcast.js +++ b/components/webrtc/presentationBroadcast.js @@ -48,13 +48,6 @@ class presentationBroadcast extends React.Component { componentDidMount() { - window.localforage.config({ - driver: [localforage.WEBSQL, localforage.INDEXEDDB], - name: 'SlideWikiRecorder', - size: 524288000 - }); - window.localforage.clear(); - 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'); @@ -87,13 +80,18 @@ class presentationBroadcast extends React.Component { that.socket.on('created', (room, socketID) => { //only initiator recieves this console.log('Created room ' + that.room); that.isInitiator = true; + window.localforage.config({ + driver: [localforage.WEBSQL, localforage.INDEXEDDB], + name: 'SlideWikiRecorder', + size: 524288000 + }); + window.localforage.clear(); 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); - that.refs.sessionRecorder.StartRecordSlideChanges(that.deckID, that.currentSlide); requestStreams({ audio: true, // video: { @@ -320,7 +318,8 @@ class presentationBroadcast extends React.Component { .then(() => { that.refs.speechRecognition.activateSpeechRecognition(); /*$('body>a#atlwdg-trigger').remove();*/}); } that.localStream = stream; - that.refs.sessionRecorder.recordStream(stream); + if(that.isInitiator) + that.refs.sessionRecorder.recordStream(stream); function sendASAP() { if (that.presenterID) //wait for presenterID before sending the message @@ -744,6 +743,8 @@ class presentationBroadcast extends React.Component { }); if (that.isInitiator) { + that.refs.sessionRecorder.StartRecordSlideChanges(that.deckID, document.getElementById('slidewikiPresentation').contentWindow.location.href); + that.refs.sessionRecorder.startRecording(); iframe.on('slidechanged', () => { that.currentSlide = document.getElementById('slidewikiPresentation').contentWindow.location.href; that.refs.sessionRecorder.recordSlideChange(that.currentSlide); @@ -1021,7 +1022,7 @@ class presentationBroadcast extends React.Component { {(this.isInitiator) ? (
- +
) : ('')}; From 639e907461129ce8bf21745e29e4b674ba8364ca Mon Sep 17 00:00:00 2001 From: Roy Meissner Date: Fri, 23 Feb 2018 10:14:44 +0100 Subject: [PATCH 011/285] Fixed merge error --- components/webrtc/presentationBroadcast.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/components/webrtc/presentationBroadcast.js b/components/webrtc/presentationBroadcast.js index a4db0e77a..e2e05c11d 100644 --- a/components/webrtc/presentationBroadcast.js +++ b/components/webrtc/presentationBroadcast.js @@ -1057,11 +1057,7 @@ class presentationBroadcast extends React.Component { + {(this.props.userid && (this.props.isMember || this.props.isCreator)) ? + + : ''} + + {(this.props.saveUsergroupIsLoading === true) ?
{this.context.intl.formatMessage(this.messages.loading)}
: ''} + +
+
+ +
+

{this.context.intl.formatMessage(this.messages.members)}

+
+
+ {userlist} +
+ + + + ); + } +} + +Details.contextTypes = { + executeAction: PropTypes.func.isRequired, + intl: React.PropTypes.object.isRequired +}; + +export default Details; diff --git a/components/UserGroups/Info.js b/components/UserGroups/Info.js new file mode 100644 index 000000000..738d64ad1 --- /dev/null +++ b/components/UserGroups/Info.js @@ -0,0 +1,47 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import UserPicture from '../common/UserPicture'; + +class Info extends React.Component { + + render() { + let content1 = ; + let content2 =

{ this.props.group.name }

+
+
+
+
+ Creator: {this.props.group.creator.displayName || this.props.group.creator.username} +
+ Members: {this.props.group.members.length + 1} +
+
+
+
+
; + + return ( +
+
+ {content1} + {content2} +
+
+ {content1} +
+
+ {content2} +
+
+
+
+
+ ); + } +} + +Info.contextTypes = { + executeAction: PropTypes.func.isRequired +}; + +export default Info; diff --git a/components/UserGroups/Menu.js b/components/UserGroups/Menu.js new file mode 100644 index 000000000..62d043e1f --- /dev/null +++ b/components/UserGroups/Menu.js @@ -0,0 +1,59 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import {NavLink} from 'fluxible-router'; +import { FormattedMessage, defineMessages } from 'react-intl'; + +class Menu extends React.Component { + constructor(props){ + super(props); + this.styles = {'backgroundColor': '#2185D0', 'color': 'white'}; + this.messages = this.getIntlMessages(); + } + getIntlMessages(){ + return defineMessages({ + members: { + id: 'GroupMenu.members', + defaultMessage: 'Members' + }, + sharedDecks: { + id: 'GroupMenu.sharedDecks', + defaultMessage: 'Shared Decks' + }, + collections: { + id: 'GroupMenu.collections', + defaultMessage: 'Playlists' + }, + }); + } + render() { + let memberMsg = this.context.intl.formatMessage(this.messages.members); + let sharedDecksMsg = this.context.intl.formatMessage(this.messages.sharedDecks); + let deckCollectionsMsg = this.context.intl.formatMessage(this.messages.collections); + + return ( +
+
+ +

{memberMsg}

+
+ +

+ + + {sharedDecksMsg}

+
+ +

{deckCollectionsMsg}

+
+
+
+ ); + } +} + +Menu.contextTypes = { + executeAction: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired +}; + +export default Menu; diff --git a/components/UserGroups/UserGroupPage.js b/components/UserGroups/UserGroupPage.js new file mode 100644 index 000000000..e81779503 --- /dev/null +++ b/components/UserGroups/UserGroupPage.js @@ -0,0 +1,114 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import { connectToStores } from 'fluxible-addons-react'; +import classNames from 'classnames/bind'; +import UserGroupsStore from '../../stores/UserGroupsStore'; +import UserProfileStore from '../../stores/UserProfileStore'; +import Info from './Info'; +import Menu from './Menu'; +import Details from './Details'; + + + +//import UserDecks from './UserDecks'; +//import UserCollections from '../../../DeckCollection/UserCollections'; + +class UserGroupPage extends React.Component { + constructor(props){ + super(props); + } + + showDecks(){ + let group = this.props.UserGroupsStore.currentUsergroup; + const isCreator = group.creator.userid === this.props.UserProfileStore.userid; + const isAdmin = group.members.find((m) => { + return m.userid === this.props.UserProfileStore.userid && (m.role && m.role[0] === 'admin'); + }); + return ''; + // return ; + } + + showCollections(){ + let group = this.props.UserGroupsStore.currentUsergroup; + const isCreator = group.creator.userid === this.props.UserProfileStore.userid; + const isAdmin = group.members.find((m) => { + return m.userid === this.props.UserProfileStore.userid && (m.role && m.role[0] === 'admin'); + }); + return ''; + // return ; + } + + showDetails(){ + let group = this.props.UserGroupsStore.currentUsergroup; + const isCreator = group.creator && group.creator.userid === this.props.UserProfileStore.userid; + const isAdmin = group.members && group.members.find((m) => { + return m.userid === this.props.UserProfileStore.userid && (m.role && m.role[0] === 'admin'); + }); + const isMember = group.members && group.members.find((m) => { + return m.userid === this.props.UserProfileStore.userid; + }); + return
; + } + + chooseView(){ + switch(this.props.UserGroupsStore.category){ + case 'settings': + return this.showDetails(); + case 'decks': + return this.showDecks(); + case 'playlists': + return this.showCollections(); + default: + return this.showDetails(); + } + } + + render() { + let profileClasses = classNames({ + 'tablet': true, + 'computer': true, + 'only': true, + 'sixteen': true, + 'wide': true, + 'column': true + }); + return ( +
+
+
+
+ +
+
+ +
+
+
+
+ {this.chooseView()} +
+
+
+ ); + } +} + +UserGroupPage.contextTypes = { + executeAction: PropTypes.func.isRequired +}; + +UserGroupPage = connectToStores(UserGroupPage, [UserGroupsStore, UserProfileStore], (context, props) => { + return { + UserGroupsStore: context.getStore(UserGroupsStore).getState(), + UserProfileStore: context.getStore(UserProfileStore).getState() + }; +}); + +export default UserGroupPage; diff --git a/configs/routes.js b/configs/routes.js index a8452fc28..fec6b5557 100644 --- a/configs/routes.js +++ b/configs/routes.js @@ -25,6 +25,7 @@ import notFoundError from '../actions/error/notFoundError'; import loadResetPassword from '../actions/loadResetPassword'; import async from 'async'; import { chooseAction } from '../actions/user/userprofile/chooseAction'; +import { chooseActionGroups } from '../actions/usergroups/chooseAction'; import loadFeatured from '../actions/loadFeatured'; import loadRecent from '../actions/loadRecent'; import loadLegacy from '../actions/loadLegacy'; @@ -287,6 +288,16 @@ export default { context.executeAction(chooseAction, payload, done); } }, + usergroup: { + path: '/usergroup/:id/:category?', + method: 'get', + page: 'usergroup', + title: 'SlideWiki -- User group', + handler: require('../components/UserGroups/UserGroupPage'), + action: (context, payload, done) => { + context.executeAction(chooseActionGroups, payload, done); + } + }, userprofilereview: { path: '/Sfn87Pfew9Af09aM', method: 'get', diff --git a/services/userProfile.js b/services/userProfile.js index c3ccf2c84..d5877d502 100644 --- a/services/userProfile.js +++ b/services/userProfile.js @@ -98,6 +98,8 @@ export default { userid: curr.userid, joined: curr.joined || '' }; + if (curr.role) + member.role = curr.role; prev.push(member); return prev; }, []); diff --git a/stores/UserGroupsStore.js b/stores/UserGroupsStore.js new file mode 100644 index 000000000..0206cd73d --- /dev/null +++ b/stores/UserGroupsStore.js @@ -0,0 +1,287 @@ +import { BaseStore } from 'fluxible/addons'; + +class UserGroupsStore extends BaseStore { + constructor(dispatcher) { + super(dispatcher); + this.category = undefined; + this.failures = { + emailNotAllowed: false, + wrongPassword: false + }; + this.dimmer = { + success: false, + failure: false + }; + this.userDecks = undefined; + this.userDecksMeta = {}; + this.nextUserDecksLoading = false; + this.nextUserDecksError = false; + this.errorMessage = ''; + this.currentUsergroup = { + creator: {}, + members: [] + }; + this.saveUsergroupError = ''; + this.saveUsergroupIsLoading = false; + this.deleteUsergroupError = ''; + this.usergroupsViewStatus = ''; + } + + destructor() { + this.category = undefined; + this.failures = { + emailNotAllowed: false, + wrongPassword: false + }; + this.dimmer = { + success: false, + failure: false + }; + this.userDecks = undefined; + this.userDecksMeta = {}; + this.nextUserDecksLoading = false; + this.nextUserDecksError = false; + this.errorMessage = ''; + this.currentUsergroup = { + creator: {}, + members: [] + }; + this.saveUsergroupError = ''; + this.saveUsergroupIsLoading = false; + this.deleteUsergroupError = ''; + this.usergroupsViewStatus = ''; + } + + getState() { + return { + category: this.category, + failures: this.failures, + dimmer: this.dimmer, + userDecks: this.userDecks, + userDecksMeta: this.userDecksMeta, + nextUserDecksLoading: this.nextUserDecksLoading, + nextUserDecksError: this.nextUserDecksError, + errorMessage: this.errorMessage, + currentUsergroup: this.currentUsergroup, + saveUsergroupError: this.saveUsergroupError, + saveUsergroupIsLoading: this.saveUsergroupIsLoading, + saveProfileIsLoading: this.saveProfileIsLoading, + deleteUsergroupError: this.deleteUsergroupError, + usergroupsViewStatus: this.usergroupsViewStatus + }; + } + + dehydrate() { + return this.getState(); + } + + rehydrate(state) { + this.category = state.category; + this.failures = state.failures; + this.userDecks = state.userDecks; + this.userDecksMeta = state.userDecksMeta; + this.nextUserDecksLoading = state.nextUserDecksLoading; + this.nextUserDecksError = state.nextUserDecksError; + this.dimmer = state.dimmer; + this.errorMessage = state.errorMessage; + this.currentUsergroup = state.currentUsergroup; + this.saveUsergroupError = state.saveUsergroupError; + this.saveUsergroupIsLoading = state.saveUsergroupIsLoading; + this.deleteUsergroupError = state.deleteUsergroupError; + this.usergroupsViewStatus = state.usergroupsViewStatus; + } + + changeTo(category) { + this.category = category; + this.emitChange(); + } + + updateUsergroup(group) { + this.currentUsergroup = group; + // console.log('UserGroupsStore: updateUsergroup', group); + this.saveUsergroupError = ''; + this.deleteUsergroupError = ''; + this.emitChange(); + } + + error(error) { + this.errorMessage = error.message; + this.emitChange(); + } + + saveUsergroupStart() { + this.saveUsergroupIsLoading = true; + this.emitChange(); + } + + saveUsergroupFailed(error) { + this.saveUsergroupIsLoading = false; + this.saveUsergroupError = error.message; + this.emitChange(); + } + + saveUsergroupSuccess() { + this.saveUsergroupIsLoading = false; + this.currentUsergroup = {}; + this.saveUsergroupError = ''; + this.emitChange(); + } + + updateUsergroupsStatus() { + this.usergroupsViewStatus = 'pending'; + this.emitChange(); + } + + deleteUsergroupFailed(error) { + this.deleteUsergroupError = { + action: 'delete', + message: error.message + }; + this.usergroupsViewStatus = ''; + this.emitChange(); + } + + deleteUsergroupSuccess(groupid) { + this.deleteUsergroupError = ''; + this.usergroupsViewStatus = ''; + this.emitChange(); + } + + + +/* + + // Old unchecked code + + + + + + successMessage() { + this.dimmer.success = true; + this.emitChange(); + this.dimmer.success = false; + } + + fillInUser(payload) { + if(this.username === payload.uname) + this.userpicture = payload.picture; + if(!payload.onlyPicture){ + Object.assign(this.user, payload); + this.category = payload.category; + } + this.emitChange(); + } + + fillInEditedUser(payload) { + Object.assign(this.user, payload); + if(this.username === payload.uname) + this.userpicture = payload.picture; + this.saveProfileIsLoading = false; + this.successMessage(); + } + + fillInUserDecks(payload) { + this.userDecks = payload.decks; + this.userDecksMeta = payload.metadata; + this.lastUser = this.user.uname; + this.emitChange(); + } + + actionFailed(payload) { + this.dimmer.failure = true; + this.saveProfileIsLoading = false; + this.emitChange(); + this.dimmer.failure = false; + } + + saveProfileStart() { + this.saveProfileIsLoading = true; + this.emitChange(); + } + + setUserDecksLoading(){ + this.userDecks = undefined; + // preserve sorting of sort dropdown during loading + this.userDecksMeta = { + sort: this.userDecksMeta.sort + }; + this.emitChange(); + } + + setNextUserDecksLoading(){ + this.nextUserDecksLoading = true; + this.emitChange(); + } + + fetchNextUserDecks(payload){ + this.userDecks = this.userDecks.concat(payload.decks); + this.userDecksMeta = payload.metadata; + this.nextUserDecksLoading = false; + this.nextUserDecksError = false; + this.emitChange(); + } + + fetchNextUserDecksFailed(){ + this.nextUserDecksError = true; + this.nextUserDecksLoading = false; + this.emitChange(); + this.nextUserDecksError = false; + } + + */ +} + +UserGroupsStore.storeName = 'UserGroupsStore'; +UserGroupsStore.handlers = { + 'USERGROUP_CATEGORY': 'changeTo', + 'UPDATE_USERGROUP': 'updateUsergroup', + 'USERGROUP_ERROR': 'error', + 'SAVE_USERGROUP_START': 'saveUsergroupStart', + 'SAVE_USERGROUP_FAILED': 'saveUsergroupFailed', + 'SAVE_USERGROUP_SUCCESS': 'saveUsergroupSuccess', + 'UPDATE_USERGROUPS_STATUS': 'updateUsergroupsStatus', + 'DELETE_USERGROUP_FAILED': 'deleteUsergroupFailed', + 'DELETE_USERGROUP_SUCCESS': 'deleteUsergroupSuccess', + 'LEAVE_USERGROUP_FAILED': 'deleteUsergroupFailed', + 'LEAVE_USERGROUP_SUCCESS': 'deleteUsergroupSuccess', + +/* + //old ones + + 'DELETE_USER_SUCCESS': 'userDeleted', + 'DELETE_USER_FAILURE': 'actionFailed', + 'NEW_USER_DATA': 'fillInUser', + 'NEW_EDITED_USER_DATA': 'fillInEditedUser', + 'NEW_USER_DECKS': 'fillInUserDecks', + 'NEW_USER_DECKS_LOADING': 'setUserDecksLoading', + + // loading more decks + 'FETCH_NEXT_USER_DECKS_LOADING': 'setNextUserDecksLoading', + 'FETCH_NEXT_USER_DECKS': 'fetchNextUserDecks', + 'FETCH_NEXT_USER_DECKS_FAILED': 'fetchNextUserDecksFailed', + + 'FETCH_USER_FAILED': 'actionFailed', + 'EDIT_USER_FAILED': 'actionFailed', + 'NEW_PASSWORD': 'successMessage', + 'EMAIL_NOT_ALLOWED': 'emailNotAllowed', + 'WRONG_PASSWORD': 'wrongPassword', + 'SIGNIN_SUCCESS': 'handleSignInSuccess', + 'SIGNIN_FAILURE': 'handleSignInError', + 'SOCIAL_SIGNIN_FAILURE': 'handleSocialSignInError', + 'USER_SIGNOUT': 'handleSignOut', + //social + 'SOCIAL_SIGNIN_SUCCESS': 'socialRegister', + 'REMOVE_PROVIDER_SUCCESS': 'removeProviderSuccess', + 'REMOVE_PROVIDER_FAILURE': 'removeProviderFailure', + 'ADD_PROVIDER_SUCCESS': 'addProviderSucess', + 'ADD_PROVIDER_FAILURE': 'addProviderFailure', + 'RESET_PROVIDER_STUFF': 'resetProviderStuff', + 'UPDATE_PROVIDER_ACTION': 'updateProviderAction', + 'USER_SIGNOUT': 'handleSignOut', + 'SAVE_USERPROFILE_START': 'saveProfileStart', + 'SHOW_DEACTIVATE_ACCOUNT_MODAL': 'showDeactivateModal', + 'HIDE_DEACTIVATE_ACCOUNT_MODAL': 'hideDeactivateModal'*/ +}; + +export default UserGroupsStore; diff --git a/stores/UserProfileStore.js b/stores/UserProfileStore.js index a0a60a276..f53ac5354 100644 --- a/stores/UserProfileStore.js +++ b/stores/UserProfileStore.js @@ -329,46 +329,11 @@ class UserProfileStore extends BaseStore { this.emitChange(); } - updateUsergroup(group) { - this.currentUsergroup = group; - // console.log('UserProfileStore: updateUsergroup', group); - this.saveUsergroupError = ''; - this.deleteUsergroupError = ''; - this.emitChange(); - } - - saveUsergroupFailed(error) { - this.saveUsergroupIsLoading = false; - this.saveUsergroupError = error.message; - this.emitChange(); - } - - saveUsergroupSuccess() { - this.saveUsergroupIsLoading = false; - this.currentUsergroup = {}; - this.saveUsergroupError = ''; - this.emitChange(); - } - - saveUsergroupStart() { - this.saveUsergroupIsLoading = true; - this.emitChange(); - } - saveProfileStart() { this.saveProfileIsLoading = true; this.emitChange(); } - deleteUsergroupFailed(error) { - this.deleteUsergroupError = { - action: 'delete', - message: error.message - }; - this.usergroupsViewStatus = ''; - this.emitChange(); - } - deleteUsergroupSuccess(groupid) { console.log('UserProfileStore deleteUsergroupSuccess: delete % from %', groupid, this.user.groups); //remove group from user @@ -461,18 +426,14 @@ UserProfileStore.handlers = { 'RESET_PROVIDER_STUFF': 'resetProviderStuff', 'UPDATE_PROVIDER_ACTION': 'updateProviderAction', 'USER_SIGNOUT': 'handleSignOut', - 'UPDATE_USERGROUP': 'updateUsergroup', - 'SAVE_USERGROUP_START': 'saveUsergroupStart', - 'SAVE_USERGROUP_FAILED': 'saveUsergroupFailed', - 'SAVE_USERGROUP_SUCCESS': 'saveUsergroupSuccess', - 'DELETE_USERGROUP_FAILED': 'deleteUsergroupFailed', + 'SHOW_DEACTIVATE_ACCOUNT_MODAL': 'showDeactivateModal', + 'HIDE_DEACTIVATE_ACCOUNT_MODAL': 'hideDeactivateModal', + 'DELETE_USERGROUP_SUCCESS': 'deleteUsergroupSuccess', 'UPDATE_USERGROUPS_STATUS': 'updateUsergroupsStatus', 'LEAVE_USERGROUP_FAILED': 'deleteUsergroupFailed', 'LEAVE_USERGROUP_SUCCESS': 'deleteUsergroupSuccess', - 'SAVE_USERPROFILE_START': 'saveProfileStart', - 'SHOW_DEACTIVATE_ACCOUNT_MODAL': 'showDeactivateModal', - 'HIDE_DEACTIVATE_ACCOUNT_MODAL': 'hideDeactivateModal' + 'SAVE_USERPROFILE_START': 'saveProfileStart' }; export default UserProfileStore; From 8dd0d2f491a5c51185d9ad1f275096286705e29a Mon Sep 17 00:00:00 2001 From: Kurt Junghanns Date: Wed, 8 Aug 2018 16:16:25 +0200 Subject: [PATCH 039/285] [SWIK-2400_usergroup_pages] Tried to dig into current error --- actions/usergroups/chooseAction.js | 7 ++++--- stores/UserGroupsStore.js | 3 ++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/actions/usergroups/chooseAction.js b/actions/usergroups/chooseAction.js index d0e638269..ea64578c5 100644 --- a/actions/usergroups/chooseAction.js +++ b/actions/usergroups/chooseAction.js @@ -38,22 +38,23 @@ export function chooseActionGroups(context, payload, done) { (callback) => { context.executeAction(fetchUser, {}, callback); }, + (callback) => { + context.dispatch('USERGROUP_CATEGORY', payload.params.category, callback); + }, (callback) => { switch (payload.params.category) { case categories.categories[0]: case undefined: - context.dispatch('USERGROUP_CATEGORY', payload.params.category); + context.executeAction(updateUsergroup, {group: {id: payload.params.id}}, callback); break; case categories.categories[1]: - context.dispatch('USERGROUP_CATEGORY', payload.params.category); let deckListType = payload.params.item === categories.decks[0] ? 'shared' : undefined; context.executeAction(fetchUserDecks, {deckListType, params: {username: payload.params.username}}, callback); break; case categories.categories[2]: - context.dispatch('USERGROUP_CATEGORY', payload.params.category); context.executeAction(loadUserCollections, {}, callback); break; default: diff --git a/stores/UserGroupsStore.js b/stores/UserGroupsStore.js index 0206cd73d..cea05d3a7 100644 --- a/stores/UserGroupsStore.js +++ b/stores/UserGroupsStore.js @@ -93,12 +93,13 @@ class UserGroupsStore extends BaseStore { changeTo(category) { this.category = category; + console.log('UserGroupsStore: changeTo', category); this.emitChange(); } updateUsergroup(group) { this.currentUsergroup = group; - // console.log('UserGroupsStore: updateUsergroup', group); + console.log('UserGroupsStore: updateUsergroup', group); this.saveUsergroupError = ''; this.deleteUsergroupError = ''; this.emitChange(); From 2f6c59b32ce6f79e2eb7fe8c3bcc63706bbab127 Mon Sep 17 00:00:00 2001 From: Serafeim Chatzopoulos Date: Wed, 8 Aug 2018 17:20:09 +0300 Subject: [PATCH 040/285] Create new playlist from playlists content module tab --- actions/collections/addNewCollection.js | 17 ++++++++++++++--- .../CollectionsPanel/CollectionsPanel.js | 6 ++++-- .../DeckCollection/Modals/NewCollectionModal.js | 3 ++- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/actions/collections/addNewCollection.js b/actions/collections/addNewCollection.js index 553b41ec6..028546ece 100644 --- a/actions/collections/addNewCollection.js +++ b/actions/collections/addNewCollection.js @@ -1,6 +1,7 @@ const log = require('../log/clog'); import serviceUnavailable from '../error/serviceUnavailable'; import UserProfileStore from '../../stores/UserProfileStore'; +import addDeckToCollection from './addDeckToCollection'; export default function addNewCollection(context, payload, done) { log.info(context); @@ -12,10 +13,20 @@ export default function addNewCollection(context, payload, done) { if (err) { log.error(context, {filepath: __filename}); context.dispatch('ADD_COLLECTION_FAILURE', err); + done(); } else { - context.dispatch('ADD_COLLECTION_SUCCESS', res); - } - done(); + // also add new collection to a deck + if (payload.deckId) { + context.executeAction(addDeckToCollection, { + deckId: payload.deckId, + collection: res, + collectionId: res._id, + }, done); + } else { + context.dispatch('ADD_COLLECTION_SUCCESS', res); + done(); + } + } }); } diff --git a/components/Deck/ContentModulesPanel/CollectionsPanel/CollectionsPanel.js b/components/Deck/ContentModulesPanel/CollectionsPanel/CollectionsPanel.js index 298301801..9c0e7003c 100644 --- a/components/Deck/ContentModulesPanel/CollectionsPanel/CollectionsPanel.js +++ b/components/Deck/ContentModulesPanel/CollectionsPanel/CollectionsPanel.js @@ -8,6 +8,7 @@ import CollectionsList from './CollectionsList'; import UserProfileStore from '../../../../stores/UserProfileStore'; import { Dropdown } from 'semantic-ui-react'; import addDeckToCollection from '../../../../actions/collections/addDeckToCollection'; +import NewCollectionModal from '../../../DeckCollection/Modals/NewCollectionModal'; class CollectionsPanel extends React.Component { @@ -97,7 +98,8 @@ class CollectionsPanel extends React.Component { const userId = this.props.UserProfileStore.userid; const selector = this.props.DeckCollectionStore.selector; - const groupIds = (this.props.UserProfileStore.user.groups || []).map( (group) => group.id); + const groups = this.props.UserProfileStore.user.groups; + const groupIds = (groups || []).map( (group) => group.id); // collections of the current deck const deckCollections = this.props.DeckCollectionStore.deckCollections; @@ -148,7 +150,7 @@ class CollectionsPanel extends React.Component {
-
+ this.setState({showNewCollectionModal: false})} userGroups={groups} loggedInUser={userId} deckId={selector.sid} /> ); } diff --git a/components/DeckCollection/Modals/NewCollectionModal.js b/components/DeckCollection/Modals/NewCollectionModal.js index 97837fb88..c39d2424c 100644 --- a/components/DeckCollection/Modals/NewCollectionModal.js +++ b/components/DeckCollection/Modals/NewCollectionModal.js @@ -73,7 +73,8 @@ class NewCollectionModal extends React.Component { this.context.executeAction(addNewCollection, { title: this.state.title, description: this.state.description, - userGroup: this.state.userGroup + userGroup: this.state.userGroup, + deckId: this.props.deckId }); this.handleClose(); From 9ecbaf61cfca9dfcb937c16fbf4c9e24fe103681 Mon Sep 17 00:00:00 2001 From: Kurt Junghanns Date: Wed, 8 Aug 2018 17:29:02 +0200 Subject: [PATCH 041/285] [SWIK-2400_usergroup_pages] Played around with everything in order to detect error cause --- actions/usergroups/chooseAction.js | 9 ++++++-- actions/usergroups/updateUsergroup.js | 2 +- components/UserGroups/Details.js | 32 ++++++++++++++++++--------- components/UserGroups/Info.js | 2 ++ 4 files changed, 32 insertions(+), 13 deletions(-) diff --git a/actions/usergroups/chooseAction.js b/actions/usergroups/chooseAction.js index ea64578c5..d93d1ecee 100644 --- a/actions/usergroups/chooseAction.js +++ b/actions/usergroups/chooseAction.js @@ -29,15 +29,20 @@ export function chooseActionGroups(context, payload, done) { default: title = shortTitle; }; - context.dispatch('UPDATE_PAGE_TITLE', {pageTitle: title}); console.log('choose action', payload.params.category, payload.params.id); async.series([ + (callback) => { + context.executeAction(updateUsergroup, {group: {_id: payload.params.id}}, callback); + }, (callback) => { context.executeAction(fetchUser, {}, callback); }, + (callback) => { + context.dispatch('UPDATE_PAGE_TITLE', {pageTitle: title}, callback); + }, (callback) => { context.dispatch('USERGROUP_CATEGORY', payload.params.category, callback); }, @@ -46,7 +51,7 @@ export function chooseActionGroups(context, payload, done) { case categories.categories[0]: case undefined: - context.executeAction(updateUsergroup, {group: {id: payload.params.id}}, callback); + callback() break; case categories.categories[1]: diff --git a/actions/usergroups/updateUsergroup.js b/actions/usergroups/updateUsergroup.js index 7ce7118fc..de6cc74b6 100644 --- a/actions/usergroups/updateUsergroup.js +++ b/actions/usergroups/updateUsergroup.js @@ -20,7 +20,7 @@ export default function updateUsergroup(context, payload, done) { } else { context.dispatch('UPDATE_USERGROUP', res[0]); - context.dispatch('UPDATE_PAGE_TITLE', {pageTitle: shortTitle + ' | Details of user group ' + res[0].name}); + //context.dispatch('UPDATE_PAGE_TITLE', {pageTitle: shortTitle + ' | Details of user group ' + res[0].name}); } done(); }); diff --git a/components/UserGroups/Details.js b/components/UserGroups/Details.js index 34f0b0cb0..8f0433eff 100644 --- a/components/UserGroups/Details.js +++ b/components/UserGroups/Details.js @@ -111,8 +111,12 @@ class Details extends React.Component { .catch(); return; } - this.refs.GroupName.value = this.props.currentUsergroup.name || ''; - this.refs.GroupDescription.value = this.props.currentUsergroup.description || ''; + try { + this.refs.GroupName.value = this.props.currentUsergroup.name || ''; + this.refs.GroupDescription.value = this.props.currentUsergroup.description || ''; + } catch (error) { + + } } componentDidMount() { @@ -157,14 +161,19 @@ class Details extends React.Component { } getGroup(members = undefined) { - let group = { - _id: this.props.currentUsergroup._id, - name: this.refs.GroupName.value, - description: this.refs.GroupDescription.value, - members: members, - timestamp: this.props.currentUsergroup.timestamp || '', - creator: this.props.currentUsergroup.creator || this.props.userid - }; + let group = {}; + try { + group = { + _id: this.props.currentUsergroup._id, + name: this.refs.GroupName.value, + description: this.refs.GroupDescription.value, + members: members, + timestamp: this.props.currentUsergroup.timestamp || '', + creator: this.props.currentUsergroup.creator || this.props.userid + }; + } catch (error) { + + } if (this.props.currentUsergroup._id) group.id = group._id; @@ -252,6 +261,9 @@ class Details extends React.Component { } render() { + if (!this.props.group || !this.props.group.creator) + return null; + let userlist = []; //add creator as default member userlist.push( diff --git a/components/UserGroups/Info.js b/components/UserGroups/Info.js index 738d64ad1..df74945ee 100644 --- a/components/UserGroups/Info.js +++ b/components/UserGroups/Info.js @@ -5,6 +5,8 @@ import UserPicture from '../common/UserPicture'; class Info extends React.Component { render() { + if (!this.props.group || !this.props.group.creator) + return null; let content1 = ; let content2 =

{ this.props.group.name }

From d40f48a8a6d62a7e8d33ef14c7de1c776466a0b7 Mon Sep 17 00:00:00 2001 From: Serafeim Chatzopoulos Date: Thu, 9 Aug 2018 13:07:55 +0300 Subject: [PATCH 042/285] Add playlists to user menu --- components/Header/Header.js | 5 ++++- components/Login/UserMenuDropdown.js | 9 +++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/components/Header/Header.js b/components/Header/Header.js index 5f83600f0..a41390096 100644 --- a/components/Header/Header.js +++ b/components/Header/Header.js @@ -58,7 +58,7 @@ class Header extends React.Component { ; let mobileLoginButton = - + ; let notification_locale = ''; @@ -68,6 +68,9 @@ class Header extends React.Component { + + + diff --git a/components/Login/UserMenuDropdown.js b/components/Login/UserMenuDropdown.js index 4342bf38f..53b8b1db0 100644 --- a/components/Login/UserMenuDropdown.js +++ b/components/Login/UserMenuDropdown.js @@ -71,6 +71,15 @@ class UserMenuDropdown extends React.Component { Decks + + + Playlists + Date: Thu, 9 Aug 2018 15:34:06 +0300 Subject: [PATCH 043/285] Add playlists count to playlists content module tab --- actions/collections/loadDeckCollections.js | 5 +++- actions/loadContentModules.js | 4 +++ .../CollectionsPanel/CollectionsPanel.js | 16 ++++++------ .../ContentModulesPanel.js | 2 +- services/deckgroups.js | 3 +++ stores/ContentModulesStore.js | 25 +++++++++++++++++-- 6 files changed, 43 insertions(+), 12 deletions(-) diff --git a/actions/collections/loadDeckCollections.js b/actions/collections/loadDeckCollections.js index 4a1263b5e..fb01a2129 100644 --- a/actions/collections/loadDeckCollections.js +++ b/actions/collections/loadDeckCollections.js @@ -4,9 +4,12 @@ import serviceUnavailable from '../error/serviceUnavailable'; // loads deck collections assigned to a deck export default function loadDeckCollections(context, payload, done) { log.info(context); + + let args = (payload.params) ? payload.params : payload; + args.countOnly = false; // then get deck collection options - context.service.read('deckgroups.forDeck', payload, {timeout: 20 * 1000}, (err, res) => { + context.service.read('deckgroups.forDeck', args, {timeout: 20 * 1000}, (err, res) => { if (err) { log.error(context, {filepath: __filename, message: err.message}); context.dispatch('LOAD_DECK_COLLECTIONS_FAILURE', err); diff --git a/actions/loadContentModules.js b/actions/loadContentModules.js index b92cd658f..66f3662dc 100644 --- a/actions/loadContentModules.js +++ b/actions/loadContentModules.js @@ -5,6 +5,7 @@ import loadDataSources from './datasource/loadDataSources'; import loadDataSourceCount from './datasource/loadDataSourceCount'; import loadQuestionsCount from './questions/loadQuestionsCount'; import loadCommentsCount from './contentdiscussion/loadCommentsCount'; +import loadPlaylistsCount from './collections/loadPlaylistsCount'; import deckContentTypeError from './error/deckContentTypeError'; import slideIdTypeError from './error/slideIdTypeError'; import { AllowedPattern } from './error/util/allowedPattern'; @@ -39,6 +40,9 @@ export default function loadContentModules(context, payload, done) { (callback) => { context.executeAction(loadCommentsCount, payload, callback); }, + (callback) => { + context.executeAction(loadPlaylistsCount, payload, callback); + } ]; if (payload.params.stype !== 'slide') { diff --git a/components/Deck/ContentModulesPanel/CollectionsPanel/CollectionsPanel.js b/components/Deck/ContentModulesPanel/CollectionsPanel/CollectionsPanel.js index 9c0e7003c..284464a26 100644 --- a/components/Deck/ContentModulesPanel/CollectionsPanel/CollectionsPanel.js +++ b/components/Deck/ContentModulesPanel/CollectionsPanel/CollectionsPanel.js @@ -29,11 +29,11 @@ class CollectionsPanel extends React.Component { }, createCollection: { id: 'CollectionsPanel.createCollection', - defaultMessage: 'Create new playlist' + defaultMessage: 'Add to new playlist' }, ariaCreateCollection: { id: 'CollectionsPanel.ariaCreateCollection', - defaultMessage: 'Create a new playlist' + defaultMessage: 'Add to new playlist' }, errorTitle: { id: 'CollectionsPanel.error.title', @@ -131,17 +131,17 @@ class CollectionsPanel extends React.Component {

{this.context.intl.formatMessage(this.messages.header)}

-
- -
{ (userId) &&
-
+
+
+ +
}
diff --git a/components/Deck/ContentModulesPanel/ContentModulesPanel.js b/components/Deck/ContentModulesPanel/ContentModulesPanel.js index 4560504e8..3044b7f13 100644 --- a/components/Deck/ContentModulesPanel/ContentModulesPanel.js +++ b/components/Deck/ContentModulesPanel/ContentModulesPanel.js @@ -157,7 +157,7 @@ class ContentModulesPanel extends React.Component { // hide playlists tab in slides let palylistsTab = (this.props.ContentModulesStore.selector.stype === 'deck') ? - Playlists{this.props.ContentModulesStore.moduleCount.questions} + Playlists{this.props.ContentModulesStore.moduleCount.playlists} : ''; pointingMenu = ( diff --git a/services/deckgroups.js b/services/deckgroups.js index ddcda3540..61c379b6c 100644 --- a/services/deckgroups.js +++ b/services/deckgroups.js @@ -49,6 +49,9 @@ export default { rp({ method: 'GET', uri: uri, + qs: { + countOnly: args.countOnly || undefined + }, json: true, }).then( (deckGroups) => callback(null, deckGroups)) .catch( (err) => callback(err)); diff --git a/stores/ContentModulesStore.js b/stores/ContentModulesStore.js index 93af372e0..d82ba1b8c 100644 --- a/stores/ContentModulesStore.js +++ b/stores/ContentModulesStore.js @@ -5,7 +5,7 @@ class ContentModulesStore extends BaseStore { constructor(dispatcher) { super(dispatcher); this.moduleType = 'questions'; - this.moduleCount = {'questions': 0, 'datasource': 0, 'comments': 0, 'tags': 0}; + this.moduleCount = {'questions': 0, 'datasource': 0, 'comments': 0, 'tags': 0, 'playlists': 0}; this.selector = {}; } updateContentModules(payload) { @@ -95,6 +95,23 @@ class ContentModulesStore extends BaseStore { this.selector = state.selector; this.moduleCount = state.moduleCount; } + loadPlaylistsCount(payload){ + this.moduleCount.playlists = payload; + this.emitChange(); + } + loadPlaylistsCountError(){ + // not critical to show an error + this.moduleCount.playlists = 0; + this.emitChange(); + } + increasePlaylistsCount(){ + this.moduleCount.playlists++; + this.emitChange(); + } + decreasePlaylistsCount(){ + this.moduleCount.playlists--; + this.emitChange(); + } } ContentModulesStore.storeName = 'ContentModulesStore'; @@ -114,7 +131,11 @@ ContentModulesStore.handlers = { 'LOAD_DATASOURCES_SUCCESS': 'updateDataSourcesSuccess', 'LOAD_AMOUNT_OF_TAGS_SUCCESS': 'updateTagsCount', 'ADD_QUESTION': 'addQuestionSuccess', - 'DELETE_QUESTION': 'deleteQuestionSuccess' + 'DELETE_QUESTION': 'deleteQuestionSuccess', + 'LOAD_PLAYLISTS_COUNT': 'loadPlaylistsCount', + 'LOAD_PLAYLISTS_COUNT_FAILURE': 'loadPlaylistsCountError', + 'ADD_DECK_TO_COLLECTION_SUCCESS': 'increasePlaylistsCount', + 'REMOVE_DECK_FROM_COLLECTION_SUCCESS': 'decreasePlaylistsCount', }; export default ContentModulesStore; From 6ac85491530a7d14266da0c5fbec33a3a96ce8b5 Mon Sep 17 00:00:00 2001 From: Serafeim Chatzopoulos Date: Thu, 9 Aug 2018 15:35:32 +0300 Subject: [PATCH 044/285] Fix load playlists count --- actions/collections/loadPlaylistsCount.js | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 actions/collections/loadPlaylistsCount.js diff --git a/actions/collections/loadPlaylistsCount.js b/actions/collections/loadPlaylistsCount.js new file mode 100644 index 000000000..f1cf3ae28 --- /dev/null +++ b/actions/collections/loadPlaylistsCount.js @@ -0,0 +1,22 @@ +import log from '../log/clog'; +import serviceUnavailable from '../error/serviceUnavailable'; + +// loads count of deck collections assigned to a deck +export default function loadPlaylistsCount(context, payload, done) { + log.info(context); + + let args = (payload.params) ? payload.params : payload; + args.countOnly = true; + + context.service.read('deckgroups.forDeck', args, {timeout: 20 * 1000}, (err, res) => { + if (err) { + log.error(context, {filepath: __filename, message: err.message}); + context.dispatch('LOAD_PLAYLISTS_COUNT_FAILURE', err); + } else { + context.dispatch('LOAD_PLAYLISTS_COUNT', res); + } + + done(); + }); + +} From be4733aade878b462288cb6d775541806103610e Mon Sep 17 00:00:00 2001 From: Stavros Maroulis Date: Sun, 12 Aug 2018 19:04:27 +0300 Subject: [PATCH 045/285] Add basic time series for user stats --- actions/stats/loadActivityStatsByCategory.js | 20 +++ actions/stats/loadActivityStatsByTime.js | 20 +++ actions/stats/loadUserStats.js | 26 +++ actions/stats/updateUserStatsPeriod.js | 23 +++ actions/user/userprofile/chooseAction.js | 3 +- app.js | 2 + .../PrivatePublicUserProfile.js | 3 +- .../PrivatePublicUserProfile/UserStats.js | 49 ++++++ components/User/UserProfile/UserProfile.js | 23 ++- configs/microservices.sample.js | 6 +- package.json | 2 + server.js | 1 + services/stats.js | 151 ++++++++++++++++++ stores/UserStatsStore.js | 56 +++++++ 14 files changed, 376 insertions(+), 9 deletions(-) create mode 100644 actions/stats/loadActivityStatsByCategory.js create mode 100644 actions/stats/loadActivityStatsByTime.js create mode 100644 actions/stats/loadUserStats.js create mode 100644 actions/stats/updateUserStatsPeriod.js create mode 100644 components/User/UserProfile/PrivatePublicUserProfile/UserStats.js create mode 100644 services/stats.js create mode 100644 stores/UserStatsStore.js diff --git a/actions/stats/loadActivityStatsByCategory.js b/actions/stats/loadActivityStatsByCategory.js new file mode 100644 index 000000000..c4157c3a2 --- /dev/null +++ b/actions/stats/loadActivityStatsByCategory.js @@ -0,0 +1,20 @@ +import UserProfileStore from '../../stores/UserProfileStore'; + +const log = require('../log/clog'); +import serviceUnavailable from '../error/serviceUnavailable'; +import fetchUser from '../user/userprofile/fetchUser'; +import loadCollectionDetails from '../collections/loadCollectionDetails'; + +export default function loadUserStats(context, payload, done) { + log.info(context); + + context.service.read('stats.userActivitiesByCategory', payload, {timeout: 20 * 1000}, (err, res) => { + if (err) { + log.error(context, {filepath: __filename}); + context.executeAction(serviceUnavailable, payload, done); + } else { + context.dispatch('LOAD_ACTIVITY_STATS_BY_CATEGORY', {activitiesByCategory: res}); + } + done(); + }); +} diff --git a/actions/stats/loadActivityStatsByTime.js b/actions/stats/loadActivityStatsByTime.js new file mode 100644 index 000000000..492c7bfaa --- /dev/null +++ b/actions/stats/loadActivityStatsByTime.js @@ -0,0 +1,20 @@ +import UserProfileStore from '../../stores/UserProfileStore'; + +const log = require('../log/clog'); +import serviceUnavailable from '../error/serviceUnavailable'; +import fetchUser from '../user/userprofile/fetchUser'; +import loadCollectionDetails from '../collections/loadCollectionDetails'; + +export default function loadUserStats(context, payload, done) { + log.info(context); + + context.service.read('stats.userActivitiesByTime', payload, {timeout: 20 * 1000}, (err, res) => { + if (err) { + log.error(context, {filepath: __filename}); + context.executeAction(serviceUnavailable, payload, done); + } else { + context.dispatch('LOAD_ACTIVITY_STATS_BY_TIME', {activitiesByTime: res}); + } + done(); + }); +} diff --git a/actions/stats/loadUserStats.js b/actions/stats/loadUserStats.js new file mode 100644 index 000000000..22e7148b2 --- /dev/null +++ b/actions/stats/loadUserStats.js @@ -0,0 +1,26 @@ +import async from 'async'; +const log = require('../log/clog'); +import serviceUnavailable from '../error/serviceUnavailable'; +import loadActivityStatsByTime from '../stats/loadActivityStatsByTime'; +import UserStatsStore from '../../stores/UserStatsStore'; +import UserProfileStore from '../../stores/UserProfileStore'; + + +export default function loadUserStats(context, payload, done) { + let datePeriod = context.getStore(UserStatsStore).datePeriod; + let username = context.getStore(UserProfileStore).username; + + log.info(context); + async.parallel([ + (callback) => { + context.executeAction(loadActivityStatsByTime, {datePeriod, username}, callback); + }, + ], (err, results) => { + if (err) { + log.error(context, {filepath: __filename}); + context.executeAction(serviceUnavailable, payload, done); + return; + } + done(); + }); +} diff --git a/actions/stats/updateUserStatsPeriod.js b/actions/stats/updateUserStatsPeriod.js new file mode 100644 index 000000000..377afb9c8 --- /dev/null +++ b/actions/stats/updateUserStatsPeriod.js @@ -0,0 +1,23 @@ +import async from 'async'; +const log = require('../log/clog'); +import serviceUnavailable from '../error/serviceUnavailable'; +import loadUserStats from '../stats/loadUserStats'; + + +export default function updateUserStatsPeriod(context, payload, done) { + log.info(context); + context.dispatch('UPDATE_USER_STATS_PERIOD', payload); + + async.parallel([ + (callback) => { + context.executeAction(loadUserStats, payload, callback); + }, + ], (err, results) => { + if (err) { + log.error(context, {filepath: __filename}); + context.executeAction(serviceUnavailable, payload, done); + return; + } + done(); + }); +} diff --git a/actions/user/userprofile/chooseAction.js b/actions/user/userprofile/chooseAction.js index c2b851560..66b3e4693 100644 --- a/actions/user/userprofile/chooseAction.js +++ b/actions/user/userprofile/chooseAction.js @@ -6,7 +6,7 @@ const log = require('../../log/clog'); import loadUserCollections from '../../collections/loadUserCollections'; import loadUserRecommendations from '../../recommendations/loadUserRecommendations'; import { shortTitle } from '../../../configs/general'; -import UserProfileStore from '../../../stores/UserProfileStore'; +import loadUserStats from '../../stats/loadUserStats'; export const categories = { //Do NOT alter the order of these items! Just add your items. Used in UserProfile and CategoryBox components categories: ['settings', 'groups', 'playlists', 'decks', 'recommendations', 'stats'], @@ -104,6 +104,7 @@ export function chooseAction(context, payload, done) { break; case categories.categories[5]: context.dispatch('USER_CATEGORY', {category: payload.params.category}); + context.executeAction(loadUserStats, {}, callback); break; default: context.executeAction(notFoundError, {}, callback); diff --git a/app.js b/app.js index 699238902..f2baaa4eb 100644 --- a/app.js +++ b/app.js @@ -50,6 +50,7 @@ import DeckCollectionStore from './stores/DeckCollectionStore'; import SSOStore from './stores/SSOStore'; import UserRecommendationsStore from './stores/UserRecommendationsStore'; import LoginModalStore from './stores/LoginModalStore'; +import UserStatsStore from './stores/UserStatsStore'; // create new fluxible instance & register all stores const app = new Fluxible({ @@ -103,6 +104,7 @@ const app = new Fluxible({ SSOStore, UserRecommendationsStore, LoginModalStore, + UserStatsStore ] }); diff --git a/components/User/UserProfile/PrivatePublicUserProfile/PrivatePublicUserProfile.js b/components/User/UserProfile/PrivatePublicUserProfile/PrivatePublicUserProfile.js index ecbf76d25..42e236c6d 100644 --- a/components/User/UserProfile/PrivatePublicUserProfile/PrivatePublicUserProfile.js +++ b/components/User/UserProfile/PrivatePublicUserProfile/PrivatePublicUserProfile.js @@ -7,6 +7,7 @@ import UserDecks from './UserDecks'; import UserCollections from '../../../DeckCollection/UserCollections'; import UserMenu from './UserMenu'; import UserRecommendations from '../UserRecommendations'; +import UserStats from './UserStats'; import classNames from 'classnames/bind'; import { fetchUserDecks } from '../../../../actions/user/userprofile/fetchUserDecks'; @@ -28,7 +29,7 @@ class PrivatePublicUserProfile extends React.Component { } showUserStats(){ - return (
); + return (); } chooseView(){ diff --git a/components/User/UserProfile/PrivatePublicUserProfile/UserStats.js b/components/User/UserProfile/PrivatePublicUserProfile/UserStats.js new file mode 100644 index 000000000..f5c0ffe58 --- /dev/null +++ b/components/User/UserProfile/PrivatePublicUserProfile/UserStats.js @@ -0,0 +1,49 @@ +import React from 'react'; +import {Line, LineChart, Tooltip, XAxis, YAxis} from 'recharts'; +import {Dropdown} from 'semantic-ui-react'; +import moment from 'moment'; +import updateUserStatsPeriod from '../../../../actions/stats/updateUserStatsPeriod'; +import {defineMessages, FormattedMessage} from 'react-intl'; +import PropTypes from 'prop-types'; + +class UserStats extends React.Component { + + handleDatePeriodChange(event, {value}) { + this.context.executeAction(updateUserStatsPeriod, { + datePeriod: value + }); + } + + render() { + const periodOptions = [{value: 'LAST_7_DAYS', text: 'Last 7 days'}, + {value: 'LAST_30_DAYS', text: 'Last 30 days'}, + {value: 'LAST_2_MONTHS', text: 'Last 2 months'}, + {value: 'LAST_6_MONTHS', text: 'Last 6 months'}, + {value: 'LAST_1_YEAR', text: 'Last 1 year'}, + {value: 'LAST_2_YEARS', text: 'Last 2 years'},]; + return (
+ + {this.props.userStats.activitiesByTime && this.props.userStats.activitiesByTime.length > 0 &&
+ + + moment(unixTime).format('Y-M-D')} /*domain={['dataMin', 'dataMax']} scale='time' interval='preserveStartEnd'*/ + /> + moment(unixTime).format('Y-M-D')}/> + + +
} +
+ ); + } +} + +UserStats.contextTypes = { + executeAction: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired +}; + +export default UserStats; diff --git a/components/User/UserProfile/UserProfile.js b/components/User/UserProfile/UserProfile.js index ea852df3a..51f459420 100644 --- a/components/User/UserProfile/UserProfile.js +++ b/components/User/UserProfile/UserProfile.js @@ -1,6 +1,6 @@ import PropTypes from 'prop-types'; import React from 'react'; -import { getIntlLanguage } from '../../../common.js'; +import {getIntlLanguage} from '../../../common.js'; import CategoryBox from './CategoryBox'; import ChangePicture from './ChangePicture'; import ChangePassword from './ChangePassword'; @@ -9,12 +9,13 @@ import ChangePersonalData from './ChangePersonalData'; import IntlStore from '../../../stores/IntlStore'; import UserGroups from './UserGroups'; import UserGroupEdit from './UserGroupEdit'; -import { connectToStores } from 'fluxible-addons-react'; +import {connectToStores} from 'fluxible-addons-react'; import UserProfileStore from '../../../stores/UserProfileStore'; +import UserStatsStore from '../../../stores/UserStatsStore'; import PrivatePublicUserProfile from './PrivatePublicUserProfile/PrivatePublicUserProfile'; import Integrations from './Integrations'; -import { FormattedMessage, defineMessages } from 'react-intl'; -import { categories } from '../../../actions/user/userprofile/chooseAction'; +import {defineMessages, FormattedMessage} from 'react-intl'; +import {categories} from '../../../actions/user/userprofile/chooseAction'; let MediaQuery = require ('react-responsive'); @@ -220,7 +221,16 @@ class UserProfile extends React.Component { } displayUserProfile() { - return (); + return (); } displayIntegrations() { @@ -256,9 +266,10 @@ UserProfile.contextTypes = { intl: PropTypes.object.isRequired }; -UserProfile = connectToStores(UserProfile, [UserProfileStore,IntlStore], (context, props) => { +UserProfile = connectToStores(UserProfile, [UserProfileStore, UserStatsStore, IntlStore], (context, props) => { return { UserProfileStore: context.getStore(UserProfileStore).getState(), + UserStatsStore: context.getStore(UserStatsStore).getState(), IntlStore: context.getStore(IntlStore).getState() }; }); diff --git a/configs/microservices.sample.js b/configs/microservices.sample.js index 279feef70..cb82d6a92 100644 --- a/configs/microservices.sample.js +++ b/configs/microservices.sample.js @@ -86,6 +86,10 @@ export default { }, 'recommendation': { uri: 'http://slidewiki.imp.bg.ac.rs' - } + }, + 'lrs': { + uri: 'https://learninglocker.experimental.slidewiki.org', + basicAuth :'MWEwNTkwMTg5M2Y4ZjIyZTY4ZThkMzhlYWE0NDZkZjAxZWUyNjdhODo2YjE5MzAxODhmZWM0OTg0ZjE1YzVhODI1Njg2NTY5NDk5YzRmODEz' + }, } }; diff --git a/package.json b/package.json index e0a25cd10..c753a8f8e 100644 --- a/package.json +++ b/package.json @@ -122,6 +122,7 @@ "mathjax": "^2.7.0", "md5": "^2.2.1", "mobile-detect": "^1.4.1", + "moment": "^2.22.2", "napa": "^2.3.0", "npm": "^5.5.1", "pre-commit": "^1.2.2", @@ -147,6 +148,7 @@ "react-responsive": "^1.2.6", "react-share": "^1.16.0", "react-tweet-embed": "^1.0.8", + "recharts": "^1.1.0", "request": "^2.80.0", "request-promise": "^4.1.1", "reveal": "0.0.4", diff --git a/server.js b/server.js index 3e5816d93..853d14b03 100644 --- a/server.js +++ b/server.js @@ -108,6 +108,7 @@ fetchrPlugin.registerService(require('./services/nlp')); fetchrPlugin.registerService(require('./services/deckgroups')); fetchrPlugin.registerService(require('./services/recommendations')); fetchrPlugin.registerService(require('./services/tags')); +fetchrPlugin.registerService(require('./services/stats')); // ************************** UI Internationalisation routines *************************************** diff --git a/services/stats.js b/services/stats.js new file mode 100644 index 000000000..f328d4300 --- /dev/null +++ b/services/stats.js @@ -0,0 +1,151 @@ +import {Microservices} from '../configs/microservices'; +import rp from 'request-promise'; +import moment from 'moment'; + +const fillMissingDates = (results) => { + let minDate = null; + let today = moment().utc().endOf('day'); + + results.forEach((result) => { + const resultDate = moment(result._id); + minDate = minDate ? moment.min(minDate, resultDate) : resultDate; + }); + + if (!minDate && !today) { + return results; + } + + const dateDiff = today.diff(minDate, 'days'); + let newResults = []; + for (let i = 0; i < dateDiff; i++) { + let date = minDate.clone().add(i, 'day'); + let found = results.find((result) => { + return date.isSame(result._id, 'day'); + }); + + newResults.push({date: date.valueOf(), value: found != null ? found.count : 0}); + } + return newResults; +}; + +const periodToDate = (datePeriod) => { + const today = moment().utc().endOf('day'); + + switch (datePeriod) { + case 'LAST_30_DAYS': + return today.subtract(30, 'days'); + case 'LAST_2_MONTHS': + return today.subtract(2, 'months'); + case 'LAST_6_MONTHS': + return today.subtract(6, 'months'); + case 'LAST_1_YEAR': + return today.subtract(1, 'years'); + case 'LAST_2_YEARS': + return today.subtract(2, 'years'); + case 'LAST_7_DAYS': + default: + return today.subtract(7, 'days'); + } +}; + +export default { + name: 'stats', + read: (req, resource, params, config, callback) => { + let args = params.params ? params.params : params; + let fromDate = periodToDate(args.datePeriod); + let username = args.username; + + if (resource === 'stats.userActivitiesByTime') { + let pipeline = [{ + '$match': { + 'timestamp': {'$gte': {'$dte': fromDate.toISOString()}}, + 'statement.actor.account.name': username + } + }, { + '$project': { + 'date': { + '$dateToString': { + 'format': '%Y-%m-%d', + 'date': '$timestamp' + } + } + } + }, { + '$group': { + '_id': '$date', + 'count': { + '$sum': 1 + } + } + }, { + '$sort': { + '_id': 1 + } + }]; + rp({ + method: 'GET', + uri: Microservices.lrs.uri + '/api/statements/aggregate', + qs: { + pipeline: JSON.stringify(pipeline), + }, + headers: {'Authorization': 'Basic ' + Microservices.lrs.basicAuth}, + json: true + }).then((response) => callback(null, fillMissingDates(response))) + .catch((err) => callback(err)); + } else if (resource === 'stats.userActivitiesByCategory') { + let pipeline = [ + { + '$match ': { + 'timestamp ': { + '$gte ': { + '$dte ': fromDate.toISOString() + } + } + } + }, + { + '$match ': { + 'statement.verb ': { + '$exists ': true + } + } + }, + { + '$project ': { + 'verb ': '$statement.verb.id ' + } + }, + { + '$group ': { + '_id ': '$verb ', + 'value ': { + '$sum ': 1 + } + } + }, { + '$project ': { + '_id ': false, + 'category ': '$_id ', + 'value ': true + } + }, { + '$sort ': { + 'count ': -1 + } + } + ]; + rp({ + method: 'GET', + uri: Microservices.lrs.uri + '/api/statements/aggregate', + qs: { + pipeline: JSON.stringify(pipeline), + }, + headers: {'Authorization': 'Basic ' + Microservices.lrs.basicAuth}, + json: true + }).then((response) => callback(null, response)) + .catch((err) => callback(err)); + + + } + } +}; diff --git a/stores/UserStatsStore.js b/stores/UserStatsStore.js new file mode 100644 index 000000000..a40033b96 --- /dev/null +++ b/stores/UserStatsStore.js @@ -0,0 +1,56 @@ +import {BaseStore} from 'fluxible/addons'; + +class UserStatsStore extends BaseStore { + constructor(dispatcher) { + super(dispatcher); + this.datePeriod = 'LAST_7_DAYS'; + this.activitiesByTime = []; + this.activitiesByCategory = []; + this.chartHeight = 450; + } + + updateActivityStatsByTime(payload) { + this.activitiesByTime = payload.activitiesByTime; + this.emitChange(); + } + + updateDatePeriod(payload) { + this.datePeriod = payload.datePeriod; + this.emitChange(); + } + + updateActivityStatsByCategory(payload) { + this.activitiesByCategory = payload.activitiesByCategory; + this.emitChange(); + } + + getState() { + return { + datePeriod: this.datePeriod, + activitiesByTime: this.activitiesByTime, + activitiesByCategory: this.activitiesByCategory, + chartHeight: this.chartHeight + }; + } + + dehydrate() { + return this.getState(); + } + + rehydrate(state) { + this.datePeriod = state.datePeriod; + this.activitiesByTime = state.activitiesByTime; + this.activitiesByCategory = state.activitiesByCategory; + this.chartHeight = state.chartHeight; + } +} + +UserStatsStore.storeName = 'UserStatsStore'; +UserStatsStore.handlers = { + 'UPDATE_USER_STATS_PERIOD': 'updateDatePeriod', + 'LOAD_ACTIVITY_STATS_BY_TIME': 'updateActivityStatsByTime', + 'LOAD_ACTIVITY_STATS_BY_CATEGORY': 'updateActivityStatsByCategory' + +}; + +export default UserStatsStore; From 06fb87d0e523cd41b0363919ea6328f65aac9e54 Mon Sep 17 00:00:00 2001 From: Serafeim Chatzopoulos Date: Mon, 13 Aug 2018 12:29:00 +0300 Subject: [PATCH 046/285] Add support for removing decks from playlist in playlist page --- .../collections/updateCollectionDeckOrder.js | 5 +-- .../CollectionPanel/CollectionDecksReorder.js | 13 +++++++- .../CollectionPanel/CollectionPanel.js | 32 +++++++++++++------ stores/DeckCollectionStore.js | 12 ++++++- 4 files changed, 49 insertions(+), 13 deletions(-) diff --git a/actions/collections/updateCollectionDeckOrder.js b/actions/collections/updateCollectionDeckOrder.js index 17de5fb47..286489a1e 100644 --- a/actions/collections/updateCollectionDeckOrder.js +++ b/actions/collections/updateCollectionDeckOrder.js @@ -9,6 +9,8 @@ export default function updateCollectionDeckOrder(context, payload, done) { // enrich payload with jwt payload.jwt = context.getStore(UserProfileStore).jwt; + context.dispatch('UPDATE_COLLECTION_DECK_ORDER_LOADING', true); + context.service.update('deckgroups.deckOrder', payload, {timeout: 20 * 1000}, (err, res) => { if (err) { log.error(context, {filepath: __filename}); @@ -22,8 +24,7 @@ export default function updateCollectionDeckOrder(context, payload, done) { }); } - - + context.dispatch('UPDATE_COLLECTION_DECK_ORDER_LOADING', false); done(); }); } diff --git a/components/DeckCollection/CollectionPanel/CollectionDecksReorder.js b/components/DeckCollection/CollectionPanel/CollectionDecksReorder.js index 0177d1d87..7b6943e18 100644 --- a/components/DeckCollection/CollectionPanel/CollectionDecksReorder.js +++ b/components/DeckCollection/CollectionPanel/CollectionDecksReorder.js @@ -17,6 +17,9 @@ class CollectionDecksReorder extends React.Component { handleMoveDown(index){ this.props.moveDown(index); } + handlRemove(index){ + this.props.remove(index); + } getIntlMessages(){ return defineMessages({ moveUp: { @@ -26,6 +29,10 @@ class CollectionDecksReorder extends React.Component { moveDown: { id: 'CollectionDecksReorder.movedown', defaultMessage: 'Move Down' + }, + remove: { + id: 'CollectionDecksReorder.remove', + defaultMessage: 'Remove' } }); } @@ -48,6 +55,7 @@ class CollectionDecksReorder extends React.Component {
+ { (index > 0) && - } + } +
diff --git a/components/DeckCollection/CollectionPanel/CollectionPanel.js b/components/DeckCollection/CollectionPanel/CollectionPanel.js index 9e57a4b80..146a3f77d 100644 --- a/components/DeckCollection/CollectionPanel/CollectionPanel.js +++ b/components/DeckCollection/CollectionPanel/CollectionPanel.js @@ -70,6 +70,11 @@ class CollectionPanel extends React.Component { newState.decksOrder[index + 1] = tmp; this.setState(newState); } + handleRemove(index){ + let newState = Object.assign({}, this.state); + newState.decksOrder.splice(index, 1); + this.setState(newState); + } showErrorPopup(text){ swal({ title: 'Error', @@ -105,10 +110,14 @@ class CollectionPanel extends React.Component { id: 'CollectionPanel.decks.title', defaultMessage: 'Decks in Playlist' }, - reorderDecks: { - id: 'CollectionPanel.decks.reorder', - defaultMessage: 'Reorder Decks' + editPlaylist: { + id: 'CollectionPanel.decks.edit', + defaultMessage: 'Edit' }, + editPlaylistHeader: { + id: 'CollectionPanel.decks.edit.header', + defaultMessage: 'Edit Playlist' + }, saveReorder: { id: 'CollectionPanel.save.reorder', defaultMessage: 'Save' @@ -156,10 +165,13 @@ class CollectionPanel extends React.Component { } let data = this.props.DeckCollectionStore.collectionDetails; - let content = (!this.state.editMode) - ? - : ; + let loadingDiv = (this.props.DeckCollectionStore.deckOrderLoading) ?
Loading
: ''; + + let content = (!this.state.editMode) + ? + : ; + // the user has edit rights in collection if he is the owner of the collection, or one of his user groups are assigned to the collection let hasEditRights = (this.props.UserProfileStore.userid && this.props.UserProfileStore.userid === data.user.id || this.props.UserProfileStore.user.groups && this.props.UserProfileStore.user.groups.map((group) => group._id).includes(data.userGroup)); @@ -183,12 +195,14 @@ class CollectionPanel extends React.Component {
+ {loadingDiv} {(data === undefined) ?
Loading
: ''}
-

{this.context.intl.formatMessage((!this.state.editMode) ? this.messages.decksInCollectionText : this.messages.reorderDecks)}

+

{this.context.intl.formatMessage((!this.state.editMode) ? this.messages.decksInCollectionText : this.messages.editPlaylistHeader)}

{ (!this.state.editMode && data.decks.length > 0 && hasEditRights) && - } { (this.state.editMode) && diff --git a/stores/DeckCollectionStore.js b/stores/DeckCollectionStore.js index c3c790115..7ca86b45d 100644 --- a/stores/DeckCollectionStore.js +++ b/stores/DeckCollectionStore.js @@ -14,6 +14,7 @@ class DeckCollectionStore extends BaseStore { this.updateCollectionMetadataError = false; this.updateCollectionDeckOrderError = false; this.loading = false; + this.deckOrderLoading = false; } destructor() { @@ -26,6 +27,7 @@ class DeckCollectionStore extends BaseStore { this.updateCollectionMetadataError = false; this.updateCollectionDeckOrderError = false; this.loading = false; + this.deckOrderLoading = false; } getState() { @@ -38,7 +40,8 @@ class DeckCollectionStore extends BaseStore { addCollectionError: this.addCollectionError, updateCollectionMetadataError: this.updateCollectionMetadataError, updateCollectionDeckOrderError: this.updateCollectionDeckOrderError, - loading: this.loading + loading: this.loading, + deckOrderLoading: this.deckOrderLoading, }; } @@ -56,6 +59,7 @@ class DeckCollectionStore extends BaseStore { this.updateCollectionMetadataError = state.updateCollectionMetadataError; this.updateCollectionDeckOrderError = state.updateCollectionDeckOrderError; this.loading = state.loading; + this.deckOrderLoading = state.deckOrderLoading; } updateCollections(payload){ @@ -168,6 +172,11 @@ class DeckCollectionStore extends BaseStore { this.emitChange(); } + updateCollectionDeckOrderLoading(payload){ + this.deckOrderLoading = payload; + this.emitChange(); + } + } DeckCollectionStore.storeName = 'DeckCollectionStore'; @@ -189,6 +198,7 @@ DeckCollectionStore.handlers = { 'UPDATE_COLLECTION_METADATA_ERROR': 'updateCollectionMetadataFailed', 'UPDATE_COLLECTION_DECK_ORDER_SUCCESS': 'updateCollectionDeckOrder', + 'UPDATE_COLLECTION_DECK_ORDER_LOADING': 'updateCollectionDeckOrderLoading', 'UPDATE_COLLECTION_DECK_ORDER_FAILURE': 'updateCollectionDeckOrderFailed', 'SET_COLLECTIONS_LOADING': 'startLoading', From aaf73617cd081c3cc8492a126066fe7153966de1 Mon Sep 17 00:00:00 2001 From: Serafeim Chatzopoulos Date: Tue, 14 Aug 2018 17:28:58 +0300 Subject: [PATCH 047/285] Add modal for adding decks to playlist --- .../CollectionPanel/CollectionPanel.js | 18 +- .../Modals/AddDecksModal/AddDecksModal.js | 154 ++++++++++++++++ .../Modals/AddDecksModal/DecksList.js | 169 ++++++++++++++++++ stores/DeckCollectionStore.js | 27 +++ 4 files changed, 366 insertions(+), 2 deletions(-) create mode 100644 components/DeckCollection/Modals/AddDecksModal/AddDecksModal.js create mode 100644 components/DeckCollection/Modals/AddDecksModal/DecksList.js diff --git a/components/DeckCollection/CollectionPanel/CollectionPanel.js b/components/DeckCollection/CollectionPanel/CollectionPanel.js index 146a3f77d..c500cbfa7 100644 --- a/components/DeckCollection/CollectionPanel/CollectionPanel.js +++ b/components/DeckCollection/CollectionPanel/CollectionPanel.js @@ -10,13 +10,14 @@ import CollectionDecksReorder from './CollectionDecksReorder'; import {Button, Icon} from 'semantic-ui-react'; import updateCollectionDeckOrder from '../../../actions/collections/updateCollectionDeckOrder'; import {FormattedMessage, defineMessages} from 'react-intl'; +import AddDecksModal from '../Modals/AddDecksModal/AddDecksModal'; class CollectionPanel extends React.Component { constructor(props){ super(props); this.state = { editMode: false, - decksOrder: this.props.DeckCollectionStore.collectionDetails.decks.slice() || [] + decksOrder: this.props.DeckCollectionStore.collectionDetails.decks.slice() || [], }; this.messages = this.getIntlMessages(); @@ -75,6 +76,18 @@ class CollectionPanel extends React.Component { newState.decksOrder.splice(index, 1); this.setState(newState); } + handleAdd(newDecks){ + let newState = Object.assign({}, this.state); + + // add decks that are not already included + newDecks.forEach( (newDeck) => { + let index = newState.decksOrder.findIndex( (d) => d.deckID === newDeck.deckID); + if (index < 0) { + newState.decksOrder.push(newDeck); + } + }); + this.setState(newState); + } showErrorPopup(text){ swal({ title: 'Error', @@ -141,7 +154,7 @@ class CollectionPanel extends React.Component { sortTitle: { id: 'CollectionPanel.sort.title', defaultMessage: 'Title' - } + }, }); } getSelectedSort(sortBy){ @@ -209,6 +222,7 @@ class CollectionPanel extends React.Component {
+
} { (!this.state.editMode) && diff --git a/components/DeckCollection/Modals/AddDecksModal/AddDecksModal.js b/components/DeckCollection/Modals/AddDecksModal/AddDecksModal.js new file mode 100644 index 000000000..454b3adf9 --- /dev/null +++ b/components/DeckCollection/Modals/AddDecksModal/AddDecksModal.js @@ -0,0 +1,154 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import {navigateAction} from 'fluxible-router'; +import {Button, Icon, Modal, Header, Form, Dropdown, Segment, TextArea, Menu} from 'semantic-ui-react'; +import FocusTrap from 'focus-trap-react'; +import {FormattedMessage, defineMessages} from 'react-intl'; + +class AddDecksModal extends React.Component { + + constructor(props) { + super(props); + this.state = { + activeItem: 'myDecksTab', + + // title: this.props.title || '', + // description: this.props.description || '', + // userGroup: this.props.userGroup || '', + // validationError: false + }; + + this.messages = this.getIntlMessages(); + } + handleMenuClick(e, { id }){ + this.setState({ activeItem: id }); + } + handleChange(fieldName, event) { + // let stateChange = {}; + // stateChange[fieldName] = event.target.value; + // this.setState(stateChange); + // } + // handleUserGroupChange(event, data){ + // this.setState({ + // userGroup: data.value + // }); + } + handleClose(){ + this.clearInputFields(); + this.props.handleClose(); + } + + clearInputFields(){ + this.setState({ + title: '', + description: '', + userGroup: '', + validationError: false + }); + } + validateForm(){ + // check if a non-empty title was given + return (this.state.title && this.state.title.trim() !== ''); + } + handleSave(event) { + // event.preventDefault(); + + // if(!this.validateForm()){ + // this.setState({ + // validationError: true + // }); + // return; + // } + // let title = this.context.intl.formatMessage(this.messages.newCollectionSuccessTitle); + // let text = this.context.intl.formatMessage(this.messages.newCollectionSuccessText); + + // swal({ + // title: title, + // text: text, + // type: 'success', + // timer: 2000, + // showCloseButton: false, + // showCancelButton: false, + // allowEscapeKey: false, + // showConfirmButton: false + // }) + // .then(() => {/* Confirmed */}, (reason) => {/* Canceled */}); + + // // this.context.executeAction(addNewCollection, { + // // title: this.state.title, + // // description: this.state.description, + // // userGroup: this.state.userGroup + // // }); + + this.handleClose(); + } + getIntlMessages(){ + return defineMessages({ + modalTitle: { + id: 'AddDecksToCollectionModal.title', + defaultMessage: 'Add decks to playlist' + }, + fromMyDecksTitle: { + id: 'AddDecksToCollectionModal.fromMyDecks', + defaultMessage: 'From My Decks' + }, + fromSlidewikiTitle: { + id: 'AddDecksToCollectionModal.fromSlidewiki', + defaultMessage: 'From Slidewiki' + }, + buttonAdd: { + id: 'AddDecksToCollectionModal.button.add', + defaultMessage: 'Add' + }, + buttonClose: { + id: 'AddDecksToCollectionModal.button.close', + defaultMessage: 'Close' + }, + + }); + } + render() { + + return ( + + + + + + -
-
- - -
- -
-
-
- -
- {(this.props.saveUsergroupIsLoading === true) ?
{this.context.intl.formatMessage(this.messages.loading)}
: ''} - -
-
- -
-

{this.context.intl.formatMessage(this.messages.members)}

-
-
- {userlist} -
-
-
-
- ); - } -} - -UserGroupEdit.contextTypes = { - executeAction: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired -}; - -export default UserGroupEdit; diff --git a/components/User/UserProfile/UserProfile.js b/components/User/UserProfile/UserProfile.js index 42d971fe0..08fbf9c0d 100644 --- a/components/User/UserProfile/UserProfile.js +++ b/components/User/UserProfile/UserProfile.js @@ -8,7 +8,6 @@ import DeactivateAccount from './DeactivateAccount'; import ChangePersonalData from './ChangePersonalData'; import IntlStore from '../../../stores/IntlStore'; import UserGroups from './UserGroups'; -import UserGroupEdit from './UserGroupEdit'; import { connectToStores } from 'fluxible-addons-react'; import UserProfileStore from '../../../stores/UserProfileStore'; import UserGroupsStore from '../../../stores/UserGroupsStore'; @@ -109,9 +108,6 @@ class UserProfile extends React.Component { case categories.groups[0]: return this.displayGroups(); break; - case categories.groups[1]: - return this.displayGroupedit(); - break; default: return this.notImplemented(); }}); @@ -234,10 +230,6 @@ class UserProfile extends React.Component { return (); } - displayGroupedit() { - return (); - } - notImplemented() { return (

{this.context.intl.formatMessage(this.messages.description)} -
- - -
+ { + (this.props.isAdmin || this.props.isCreator) ? +
+ + +
+ : '' + }
diff --git a/components/UserGroups/UserGroupPage.js b/components/UserGroups/UserGroupPage.js index 04a7a6e6a..9c6b32652 100644 --- a/components/UserGroups/UserGroupPage.js +++ b/components/UserGroups/UserGroupPage.js @@ -8,10 +8,8 @@ import Info from './Info'; import Menu from './Menu'; import Details from './Details'; - - //import UserDecks from './UserDecks'; -//import UserCollections from '../../../DeckCollection/UserCollections'; +import GroupCollections from '../DeckCollection/GroupCollections'; class UserGroupPage extends React.Component { constructor(props){ @@ -30,12 +28,14 @@ class UserGroupPage extends React.Component { showCollections(){ let group = this.props.UserGroupsStore.currentUsergroup; - const isCreator = group.creator.userid === this.props.UserProfileStore.userid; - const isAdmin = group.members.find((m) => { + const isCreator = group.creator && group.creator.userid === this.props.UserProfileStore.userid; + const isAdmin = group.members && group.members.find((m) => { return m.userid === this.props.UserProfileStore.userid && (m.role && m.role[0] === 'admin'); }); - return ''; - // return ; + + return ; } showDetails(){ diff --git a/services/deckgroups.js b/services/deckgroups.js index 61c379b6c..0b1e5ddc6 100644 --- a/services/deckgroups.js +++ b/services/deckgroups.js @@ -13,12 +13,11 @@ export default { let authToken = params.jwt; let args = params.params? params.params : params; - let usergroups = (params.usergroups || []).map( (usergroup) => { - return usergroup._id; - }).join('&usergroup='); - // suggest deck collections that this user can add decks to if(resource === 'deckgroups.forUser'){ + let usergroups = (params.usergroups || []).map( (usergroup) => { + return usergroup._id; + }).join('&usergroup='); // form request call let uri = `${Microservices.deck.uri}/groups?user=${args.userId}`; @@ -35,6 +34,19 @@ export default { }).then( (deckGroups) => callback(null, deckGroups)) .catch( (err) => callback(err)); + // get deck collections assigned to a user group + } else if(resource === 'deckgroups.forGroup'){ + // form request call + let uri = `${Microservices.deck.uri}/groups?usergroup=${args.groupid}&page=0&per_page=100`; + + // get deck collections that the user is either admin or a creator of a user group that is associated to this collection + rp({ + method: 'GET', + uri: uri, + json: true + }).then( (deckGroups) => callback(null, deckGroups)) + .catch( (err) => callback(err)); + // get deck collections assigned to a specified deck } else if (resource === 'deckgroups.forDeck'){ @@ -82,7 +94,7 @@ export default { json: true }).then( (user) => { return { - username: user.username, + username: user.username, displayName: user.displayName || user.username }; }); @@ -91,7 +103,7 @@ export default { group.decks = decks; group.user = { id: group.user, - username: user.username, + username: user.username, displayName: user.displayName }; @@ -165,7 +177,7 @@ export default { headers: {'----jwt----': args.jwt}, body: [ { - op: args.op, + op: args.op, deckId: args.deckId } ] diff --git a/stores/UserGroupsStore.js b/stores/UserGroupsStore.js index 08382daae..cc88f366b 100644 --- a/stores/UserGroupsStore.js +++ b/stores/UserGroupsStore.js @@ -103,7 +103,7 @@ class UserGroupsStore extends BaseStore { updateUsergroup(group) { this.currentUsergroup = group; - console.log('UserGroupsStore: updateUsergroup', group); + // console.log('UserGroupsStore: updateUsergroup', group); this.saveUsergroupError = ''; this.deleteUsergroupError = ''; this.emitChange(); From 5cb7a482d49c7b40cbae6a450f3525deff323f01 Mon Sep 17 00:00:00 2001 From: ali1k Date: Thu, 27 Sep 2018 14:24:04 +0200 Subject: [PATCH 180/285] adding a polyfil for IE --- client.js | 1 + package.json | 1 + 2 files changed, 2 insertions(+) diff --git a/client.js b/client.js index 54531b90b..f60fa5e01 100644 --- a/client.js +++ b/client.js @@ -7,6 +7,7 @@ import { IntlProvider } from 'react-intl'; import { loadLocale } from './configs/locales'; import es6Promise from 'es6-promise'; es6Promise.polyfill(); +import '@babel/polyfill'; const debugClient = debug('slidewiki-platform'); const dehydratedState = window.App; // Sent from the server diff --git a/package.json b/package.json index 0239851b7..d12ecfb9a 100644 --- a/package.json +++ b/package.json @@ -176,6 +176,7 @@ "winston": "^2.3.1" }, "devDependencies": { + "@babel/polyfill": "^7.0.0", "babel-eslint": "^7.1.1", "chai": "^4.0.2", "coveralls": "^3.0.0", From 73db1fd44114c21e670d3c943f43b4fc53e1ffdb Mon Sep 17 00:00:00 2001 From: ali1k Date: Thu, 27 Sep 2018 15:35:32 +0200 Subject: [PATCH 181/285] adding a separate view for IE --- client.js | 1 - components/Application.js | 2 +- .../Deck/Presentation/PresentationIE.js | 83 +++++++++++++++++++ configs/routes.js | 27 ++++++ package.json | 1 - server/handleServerRendering.js | 2 +- 6 files changed, 112 insertions(+), 4 deletions(-) create mode 100644 components/Deck/Presentation/PresentationIE.js diff --git a/client.js b/client.js index f60fa5e01..54531b90b 100644 --- a/client.js +++ b/client.js @@ -7,7 +7,6 @@ import { IntlProvider } from 'react-intl'; import { loadLocale } from './configs/locales'; import es6Promise from 'es6-promise'; es6Promise.polyfill(); -import '@babel/polyfill'; const debugClient = debug('slidewiki-platform'); const dehydratedState = window.App; // Sent from the server diff --git a/components/Application.js b/components/Application.js index fd07f40e1..69b74fc39 100644 --- a/components/Application.js +++ b/components/Application.js @@ -30,7 +30,7 @@ class Application extends React.Component { let cookieBanner = ''; let Handler = this.props.currentRoute.handler; let header = null , footer = null, content = null; - const noHF_pages = ['presentation', 'neo4jguide', 'webrtc', 'print'];//NOTE add the route name to the following array if you don't want header and footer rendered on the page + const noHF_pages = ['presentation', 'neo4jguide', 'webrtc', 'print', 'presentationIE'];//NOTE add the route name to the following array if you don't want header and footer rendered on the page if(!noHF_pages.includes(this.props.currentRoute.name)){ header =
; footer =