diff --git a/CHANGELOG.md b/CHANGELOG.md index 154ea715..b532385f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,49 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +### [0.15.6](https://github.com/graasp/graasp-desktop/compare/v0.15.5...v0.15.6) (2020-09-30) + +### Features + +- add signin and signout actions, refactor dashboard ([442092b](https://github.com/graasp/graasp-desktop/commit/442092bae81ceb67fa8b1add4cc3ec6da6c8cce5)) +- add switch for action enabled and action access ([2e20609](https://github.com/graasp/graasp-desktop/commit/2e20609f1dcb01d76c836ce1822e64a7ce886563)) +- add teacher theme ([d95398b](https://github.com/graasp/graasp-desktop/commit/d95398b982c88427231d2ed4fccec289d96909e4)), closes [#283](https://github.com/graasp/graasp-desktop/issues/283) +- add user filter in dashboard ([8d81829](https://github.com/graasp/graasp-desktop/commit/8d81829eb57fdcdb4edd6f031ab4814ec726ad6d)), closes [#284](https://github.com/graasp/graasp-desktop/issues/284) +- add welcome screen when there si nothing to display in Home ([414cec5](https://github.com/graasp/graasp-desktop/commit/414cec5c0f27d2183a647a1f406e833f9542177b)), closes [#273](https://github.com/graasp/graasp-desktop/issues/273) +- close app from mainmenu ([8503ee5](https://github.com/graasp/graasp-desktop/commit/8503ee583467965f56f0b36ed969dd9f81a087cc)), closes [#282](https://github.com/graasp/graasp-desktop/issues/282) +- delete appInstanceResource and related file ([2d5b4e4](https://github.com/graasp/graasp-desktop/commit/2d5b4e4555b15556ca9b670a81840afd97a6f063)) +- display username in drawer ([a03ae5e](https://github.com/graasp/graasp-desktop/commit/a03ae5eceb67ba2d9666871080014a1c3807f017)), closes [#240](https://github.com/graasp/graasp-desktop/issues/240) +- keep language when creating new user ([8845d5c](https://github.com/graasp/graasp-desktop/commit/8845d5c5804d7e8527ed708c25509bd9757df5b7)) +- save file locally when receiving postFile message ([d308f36](https://github.com/graasp/graasp-desktop/commit/d308f3635ae818a875e22d9657002a19fd1908fe)) + +### Bug Fixes + +- **sync:** add default protocol for remote space image ([3219d99](https://github.com/graasp/graasp-desktop/commit/3219d99a050411b5922ec63d2e750f613e6af506)) +- fix saving login action twice on signin ([24a1155](https://github.com/graasp/graasp-desktop/commit/24a1155e262814b40ac7829e480dc89df47acbf6)) +- **spaces nearby:** get correctly geolocation from user ([0a63a6a](https://github.com/graasp/graasp-desktop/commit/0a63a6a98c0a1d062575ff15ea0e93cb24f41ec4)) +- define user in SignInScreen ([2c7aff3](https://github.com/graasp/graasp-desktop/commit/2c7aff36b329483a649c7861f438d2602185fbcf)) +- display connexion status toastr only on change ([8a725c3](https://github.com/graasp/graasp-desktop/commit/8a725c38d12427946631383f704d19966ef8e516)), closes [#254](https://github.com/graasp/graasp-desktop/issues/254) +- do not add classroom when name already exists ([0c6c83e](https://github.com/graasp/graasp-desktop/commit/0c6c83eace335506301ac6c9eae5d7befb68be45)), closes [#300](https://github.com/graasp/graasp-desktop/issues/300) +- does not save action and resource when space is not saved ([0b5bb5d](https://github.com/graasp/graasp-desktop/commit/0b5bb5de37470d977e317aa5138926843472b638)), closes [#296](https://github.com/graasp/graasp-desktop/issues/296) +- fix database typo in user filter, fr translation ([83b90d6](https://github.com/graasp/graasp-desktop/commit/83b90d6185d43c64abf0d2a40f459c7b3a1184d4)) +- fix minor test issues ([d023709](https://github.com/graasp/graasp-desktop/commit/d023709660cf79548d26b596c68e63512a116495)) +- handle empty items in phases on space sync ([b82a02b](https://github.com/graasp/graasp-desktop/commit/b82a02bd41c0f3fc6c3b5ca4a21cdacd5b2b4f6e)) +- remove space in classroom instead of in saved spaces ([0434c50](https://github.com/graasp/graasp-desktop/commit/0434c503c42be6ebf4b4a4456e9fe64d1dc1c33b)) +- translations for sign in as guest ([76eaa8d](https://github.com/graasp/graasp-desktop/commit/76eaa8d6dad8316b0e01de58ba4cde4ff90290a1)) +- use logger in postFile, refactor postFile ([d9145b6](https://github.com/graasp/graasp-desktop/commit/d9145b66623a75043387c2e42fe2711ec1faf775)) + +### Build System + +- **deps-dev:** update version and release deps ([b677150](https://github.com/graasp/graasp-desktop/commit/b67715015f359d92e78fbd47dbe6624332e06e56)) + +### Tests + +- update test to work with student as default user mode ([ef975ce](https://github.com/graasp/graasp-desktop/commit/ef975ce26dc5aa1393c0d489cf126656479ab2f4)) + +### Documentation + +- add login and user mode docs ([40d6f35](https://github.com/graasp/graasp-desktop/commit/40d6f35555ebf66cd2f35ff1a06d0b1aaf48b2a6)) + ### [0.15.5](https://github.com/graasp/graasp-desktop/compare/v0.15.4...v0.15.5) (2020-06-05) ### Features diff --git a/docs/authentication/authenticaton-proposals.md b/docs/authentication/authenticaton-proposals.md new file mode 100644 index 00000000..25357796 --- /dev/null +++ b/docs/authentication/authenticaton-proposals.md @@ -0,0 +1,26 @@ +# User Authentication + +Since Graasp Desktop v.0.15.4, a user authentication is available in order for multiple users to use the application. + +![Login Screen](./img/loginScreen.png) + +The authentication process is triggered right at the launch of the application. It asks for a username before allowing access to the application. Each new username will create a corresponding user account on the fly. If a username already exists in the database, the user will sign in with the corresponding account and be able to access previously stored data. + +One can use the application anonymously without restriction, but any saved data will not be retrievable after the user closes the session. + +In order to automatically authenticate whenever the application is launched, the +current user session is stored in a dedicated part of the local storage and is used to sign +in. When the user signs out, the cached session is cleared and the application redirects +to the login screen. + +## Alternative solution: Space-level Authentication (online) + +Graasp offers a practical method for students to access spaces. From a link, before accessing a space, students create personal credentials, resulting in a Light account. This account allows to save data for this student for this space only. + +If online data from Light users are required in Graasp Desktop, it is necessary to implement a authentication procedure triggered each time a space is visited. + +However, as of today's requirements, Graasp Desktop is used in poorly connected countries. Trying to fetch data online is thus not necessary. Additionally, authenticating at a space-level is rather tedious and tends to be repetitive in a context where the user only consumes and visits spaces. Security should also be handled with high precautions. + +The following schema depicts a proposed space-level login. Blue steps run while online, whereas grey steps are performed in an offline environment. If online, the application connects to and fetch the online account user data. If offline, the application skips the online procedure and immediately access the space. + +![Space-level Authentication Flowchart](./img/spaceLevelAuthentication.png) diff --git a/docs/authentication/img/loginScreen.png b/docs/authentication/img/loginScreen.png new file mode 100644 index 00000000..9e3a4f39 Binary files /dev/null and b/docs/authentication/img/loginScreen.png differ diff --git a/docs/authentication/img/spaceLevelAuthentication.png b/docs/authentication/img/spaceLevelAuthentication.png new file mode 100644 index 00000000..4db339ef Binary files /dev/null and b/docs/authentication/img/spaceLevelAuthentication.png differ diff --git a/docs/authentication/userMode.md b/docs/authentication/userMode.md new file mode 100644 index 00000000..dae0d8b0 --- /dev/null +++ b/docs/authentication/userMode.md @@ -0,0 +1,62 @@ +# User Modes + +Though different user accounts can be created to access the application and save personal data, spaces are shared among all the users. To handle these spaces (avoid unexpected action on other users), we divide users into different categories/modes/stakeholders, leading to different permissions. + +## Stakeholders in Graasp Desktop + +### Teachers + +Teachers need to manage spaces (as they can online) and their students. Assuming that +spaces are exclusively created and edited on the main online platform, Graasp Desktop +is primarily a common pedagogical course support to provide the spaces to their classes +in an offline context. +Additionally, teachers use Graasp Desktop to gather and browse their students’ data in +order to gain insight of their progression throughout the courses, as well as to +eventually correct their submissions. + +### Students + +Students use Graasp Desktop to learn in offline environments, either during classes or +remotely at home. They visit spaces created by their teacher, and might also visit other +interesting spaces if they have a stable internet connection. Additionally, students +eventually save resources and leave activity traces while visiting spaces. Such data +results in interesting analytics of their learning sessions, that students can display to +evaluate their learning progression. Finally, students share their resources with their +friends or teachers in order to be evaluated. + +### Developers + +Even though they are not directly related to the pedagogical purpose of the +application, developers are necessary in order to maintain and improve the +functionalities of the Graasp Desktop application. For instance, they need full access to +the database and to all the application’s features, as well as particular development +tools to implement new features. + +### Researchers + +Graasp Desktop is an educational support to collect meaningful data from learners, +and researchers can use it to conduct their studies. They require specific applications +to track the users, as well as a mechanism to manage consent from users to have legal +access to their data. +While Developers and Researchers also have their importance in the development of +Graasp Desktop and could have their own modes, as part of this thesis, we only focus +on teacher and student differences in order to develop a teacher mode. + +| Action | Student | Teacher | +| :-------------------------: | :------: | :------------------------------: | +| Visit a Space | yes | yes | +| Export a Space | yes | yes | +| Save a Space | no | yes | +| Add a Space | no | yes | +| Delete a Space | no | yes | +| Sync a Space | no | yes | +| Load a Space | no | yes | +| Load Space's data | no/yes\* | yes | +| Classrooms Functionalities | no | yes | +| Displayed data in Dashboard | own data | own and other users' shared data | + +
*Student*'s and *Teacher*'s permissions comparison
+ +## Implementation + +Since Graasp Desktop 0.15.4, the _Student_ and _Teacher_ modes are available. By default, a user is a _Student_. To become a _Teacher_, the setting **Student Mode** should be disabled in the settings. _Developer_ and _Research_ are not yet available. diff --git a/package.json b/package.json index 825d4016..642031f7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "graasp-desktop", - "version": "0.15.5", + "version": "0.15.6", "description": "Desktop application for the Graasp ecosystem.", "private": true, "author": "React EPFL", @@ -54,7 +54,7 @@ "dist:win:docker": "./scripts/buildWindowsWithDocker.sh", "dist:all": "run-s dist:posix dist:win", "release:manual": "run-s version dist:all", - "release": "git add CHANGELOG.md && standard-version -a && env-cmd -f ./.env electron-builder -ml && env-cmd -f ./.env electron-builder -w --x64 --ia32", + "release": "git fetch --tags && git add CHANGELOG.md && standard-version -a && env-cmd -f ./.env electron-builder -ml && env-cmd -f ./.env electron-builder -w --x64 --ia32", "hooks:uninstall": "node node_modules/husky/husky.js uninstall", "hooks:install": "node node_modules/husky/husky.js install", "postinstall": "electron-builder install-app-deps", @@ -69,60 +69,61 @@ "mocha": "mkdirp test/tmp && concurrently \"env-cmd -f ./.env.test react-scripts start\" \"wait-on http://localhost:3000 && mocha --require @babel/register \"test/**/*.test.js\"\"" }, "dependencies": { - "@material-ui/core": "4.9.4", + "@material-ui/core": "4.11.0", "@material-ui/icons": "4.9.1", - "@sentry/browser": "5.13.0", - "@sentry/electron": "1.2.1", - "about-window": "1.13.2", + "@sentry/browser": "5.19.2", + "@sentry/electron": "1.4.0", + "about-window": "1.13.4", "archiver": "3.1.1", - "bson-objectid": "1.3.0", + "bson-objectid": "1.3.1", "chai": "4.2.0", "cheerio": "1.0.0-rc.3", "classnames": "2.2.6", - "clsx": "1.1.0", - "connected-react-router": "6.7.0", + "clsx": "1.1.1", + "connected-react-router": "6.8.0", "download": "7.1.0", "electron-devtools-installer": "2.2.4", - "electron-is-dev": "1.1.0", - "electron-log": "4.0.7", + "electron-is-dev": "1.2.0", + "electron-log": "4.2.2", "electron-publisher-s3": "20.17.2", - "electron-updater": "4.2.2", + "electron-updater": "4.3.1", "enzyme": "3.11.0", "enzyme-adapter-react-16": "1.15.2", "extract-zip": "1.6.7", + "fs-extra": "9.0.1", "history": "4.10.1", - "i18next": "19.3.2", + "i18next": "19.6.2", "immutable": "4.0.0-rc.12", - "is-online": "8.2.1", + "is-online": "8.4.0", "katex": "0.11.1", - "lodash": "4.17.15", + "lodash": "4.17.19", "lowdb": "1.0.0", "md5": "2.2.1", - "mime-types": "2.1.26", - "mkdirp": "1.0.3", + "mime-types": "2.1.27", + "mkdirp": "1.0.4", "mocha": "7.1.0", "node-machine-id": "1.1.12", "prop-types": "15.7.2", - "qs": "6.9.1", - "re-resizable": "6.2.0", - "react": "16.13.0", + "qs": "6.9.4", + "re-resizable": "6.5.4", + "react": "16.13.1", "react-countup": "4.3.3", "react-detect-offline": "2.4.0", - "react-dev-utils": "10.2.0", - "react-diff-viewer": "3.0.2", - "react-dom": "16.13.0", - "react-i18next": "11.3.3", - "react-immutable-proptypes": "2.1.0", + "react-dev-utils": "10.2.1", + "react-diff-viewer": "3.1.1", + "react-dom": "16.13.1", + "react-i18next": "11.7.0", + "react-immutable-proptypes": "2.2.0", "react-joyride": "2.2.1", "react-json-view": "1.19.1", "react-loading": "2.0.3", - "react-quill": "1.3.3", + "react-quill": "1.3.5", "react-redux": "7.2.0", - "react-redux-toastr": "7.6.4", - "react-router": "5.1.2", - "react-router-dom": "5.1.2", + "react-redux-toastr": "7.6.5", + "react-router": "5.2.0", + "react-router-dom": "5.2.0", "react-select": "3.1.0", - "react-split-pane": "0.1.89", + "react-split-pane": "0.1.91", "recharts": "1.8.5", "redux": "4.0.5", "redux-devtools-extension": "2.13.8", @@ -130,31 +131,31 @@ "redux-thunk": "2.3.0", "request-promise": "4.2.5", "rimraf": "3.0.2", - "universal-analytics": "0.4.20" + "universal-analytics": "0.4.23" }, "devDependencies": { - "@babel/cli": "7.8.4", - "@babel/core": "7.9.0", - "@babel/plugin-transform-runtime": "7.9.0", - "@babel/preset-env": "7.9.0", - "@babel/register": "7.9.0", + "@babel/cli": "7.10.5", + "@babel/core": "7.10.5", + "@babel/plugin-transform-runtime": "7.10.5", + "@babel/preset-env": "7.10.4", + "@babel/register": "7.10.5", "@commitlint/cli": "9.1.1", "@commitlint/config-conventional": "9.1.1", "codacy-coverage": "3.4.0", - "concurrently": "5.1.0", - "cross-env": "7.0.0", + "concurrently": "5.2.0", + "cross-env": "7.0.2", "electron": "8.2.4", - "electron-builder": "22.3.2", + "electron-builder": "22.7.0", "env-cmd": "10.1.0", - "enzyme-to-json": "3.4.4", - "eslint-config-airbnb": "18.0.1", - "eslint-config-prettier": "6.10.0", + "enzyme-to-json": "3.5.0", + "eslint-config-airbnb": "18.2.0", + "eslint-config-prettier": "6.11.0", "eslint-plugin-mocha": "6.3.0", "husky": "4.2.5", "npm-run-all": "4.1.5", "prettier": "1.19.1", "pretty-quick": "2.0.1", - "react-scripts": "3.4.0", + "react-scripts": "3.4.1", "redux-mock-store": "1.5.4", "spectron": "10.0.1", "standard-version": "8.0.2", diff --git a/public/app/config/channels.js b/public/app/config/channels.js index 3ff1321b..38f4613e 100644 --- a/public/app/config/channels.js +++ b/public/app/config/channels.js @@ -71,5 +71,7 @@ module.exports = { GET_SPACE_IN_CLASSROOM_CHANNEL: 'classroom:space:get', LOAD_SPACE_IN_CLASSROOM_CHANNEL: 'classroom:space:load', GET_SPACE_TO_LOAD_IN_CLASSROOM_CHANNEL: 'classroom:space:load:get-space', - TOUR_COMPLETED_CHANNEL: 'tour:complete', + POST_FILE_CHANNEL: 'file:post', + DELETE_FILE_CHANNEL: 'file:delete', + COMPLETE_TOUR_CHANNEL: 'tour:complete', }; diff --git a/public/app/config/config.js b/public/app/config/config.js index 2858468f..6b2e5b39 100644 --- a/public/app/config/config.js +++ b/public/app/config/config.js @@ -1,5 +1,6 @@ // eslint-disable-next-line import/no-extraneous-dependencies const { app } = require('electron'); +const ObjectId = require('bson-objectid'); const isWindows = require('../utils/isWindows'); const tours = { @@ -93,6 +94,12 @@ const ACTIONS_VERBS = { LOGOUT: 'logout', }; +const buildFilePath = ({ userId, spaceId, name }) => { + // add generated id to handle duplicate files + const generatedId = ObjectId().str; + return `${VAR_FOLDER}/${spaceId}/files/${userId}/${generatedId}_${name}`; +}; + module.exports = { DEFAULT_LOGGING_LEVEL, DEFAULT_PROTOCOL, @@ -119,4 +126,5 @@ module.exports = { VISIBILITIES, DEFAULT_FORMAT, ACTIONS_VERBS, + buildFilePath, }; diff --git a/public/app/config/messages.js b/public/app/config/messages.js index 4c4350e1..f4b2779d 100644 --- a/public/app/config/messages.js +++ b/public/app/config/messages.js @@ -102,6 +102,8 @@ const ERROR_GETTING_SPACE_IN_CLASSROOM_MESSAGE = const ERROR_SETTING_ACTION_ACCESSIBILITY = 'There was an error setting the action accessibility'; const ERROR_SETTING_ACTIONS_AS_ENABLED = 'There was an error enabling actions'; +const ERROR_POSTING_FILE_MESSAGE = 'There was an error uploading the file'; +const ERROR_DELETING_FILE_MESSAGE = 'There was an error deleting the file'; module.exports = { ERROR_GETTING_DEVELOPER_MODE, @@ -172,4 +174,6 @@ module.exports = { ERROR_INVALID_USERNAME_MESSAGE, ERROR_SETTING_ACTION_ACCESSIBILITY, ERROR_SETTING_ACTIONS_AS_ENABLED, + ERROR_POSTING_FILE_MESSAGE, + ERROR_DELETING_FILE_MESSAGE, }; diff --git a/public/app/listeners/completeTour.js b/public/app/listeners/completeTour.js index c930c5df..37f238dc 100644 --- a/public/app/listeners/completeTour.js +++ b/public/app/listeners/completeTour.js @@ -1,4 +1,4 @@ -const { TOUR_COMPLETED_CHANNEL } = require('../config/channels'); +const { COMPLETE_TOUR_CHANNEL } = require('../config/channels'); const { ERROR_GENERAL } = require('../config/errors'); const logger = require('../logger'); @@ -8,10 +8,10 @@ const completeTour = (mainWindow, db) => async (event, { tourName }) => { db.get('user') .set(`tour.${tourName}`, true) .write(); - mainWindow.webContents.send(TOUR_COMPLETED_CHANNEL); + mainWindow.webContents.send(COMPLETE_TOUR_CHANNEL); } catch (e) { logger.error(e); - mainWindow.webContents.send(TOUR_COMPLETED_CHANNEL, ERROR_GENERAL); + mainWindow.webContents.send(COMPLETE_TOUR_CHANNEL, ERROR_GENERAL); } }; diff --git a/public/app/listeners/deleteAppInstanceResource.js b/public/app/listeners/deleteAppInstanceResource.js new file mode 100644 index 00000000..7f8c22d8 --- /dev/null +++ b/public/app/listeners/deleteAppInstanceResource.js @@ -0,0 +1,19 @@ +const { DELETE_APP_INSTANCE_RESOURCE_CHANNEL } = require('../config/channels'); +const { APP_INSTANCE_RESOURCES_COLLECTION } = require('../db'); + +const deleteAppInstanceResource = (mainWindow, db) => (event, payload = {}) => { + try { + const { appInstanceId: appInstance, id } = payload; + + db.get(APP_INSTANCE_RESOURCES_COLLECTION) + .remove({ appInstance, id }) + .write(); + + mainWindow.webContents.send(DELETE_APP_INSTANCE_RESOURCE_CHANNEL, payload); + } catch (e) { + console.error(e); + mainWindow.webContents.send(DELETE_APP_INSTANCE_RESOURCE_CHANNEL, null); + } +}; + +module.exports = deleteAppInstanceResource; diff --git a/public/app/listeners/deleteFile.js b/public/app/listeners/deleteFile.js new file mode 100644 index 00000000..1e378be1 --- /dev/null +++ b/public/app/listeners/deleteFile.js @@ -0,0 +1,27 @@ +const fs = require('fs'); +const logger = require('../logger'); +const { DELETE_FILE_CHANNEL } = require('../config/channels'); +const { ERROR_GENERAL } = require('../config/errors'); + +const deleteFile = mainWindow => (event, payload = {}) => { + try { + const { + data: { uri }, + } = payload; + + if (uri) { + const filepath = uri.substr('file://'.length); + fs.unlinkSync(filepath); + logger.debug(`${uri} was deleted`); + } else { + logger.error('no uri specified'); + } + + mainWindow.webContents.send(DELETE_FILE_CHANNEL, payload); + } catch (e) { + console.error(e); + mainWindow.webContents.send(DELETE_FILE_CHANNEL, ERROR_GENERAL); + } +}; + +module.exports = deleteFile; diff --git a/public/app/listeners/index.js b/public/app/listeners/index.js index 2c071944..206cbd6b 100644 --- a/public/app/listeners/index.js +++ b/public/app/listeners/index.js @@ -31,6 +31,7 @@ const signOut = require('./signOut'); const isAuthenticated = require('./isAuthenticated'); const getAppInstanceResources = require('./getAppInstanceResources'); const postAppInstanceResource = require('./postAppInstanceResource'); +const deleteAppInstanceResource = require('./deleteAppInstanceResource'); const patchAppInstanceResource = require('./patchAppInstanceResource'); const getAppInstance = require('./getAppInstance'); const setSpaceAsFavorite = require('./setSpaceAsFavorite'); @@ -51,6 +52,8 @@ const setActionAccessibility = require('./setActionAccessibility'); const setActionsAsEnabled = require('./setActionsAsEnabled'); const windowAllClosed = require('./windowAllClosed'); const completeTour = require('./completeTour'); +const postFile = require('./postFile'); +const deleteFile = require('./deleteFile'); module.exports = { loadSpace, @@ -83,6 +86,7 @@ module.exports = { isAuthenticated, getAppInstanceResources, postAppInstanceResource, + deleteAppInstanceResource, patchAppInstanceResource, getAppInstance, getUserMode, @@ -105,4 +109,6 @@ module.exports = { setActionsAsEnabled, windowAllClosed, completeTour, + postFile, + deleteFile, }; diff --git a/public/app/listeners/postFile.js b/public/app/listeners/postFile.js new file mode 100644 index 00000000..86f30d9c --- /dev/null +++ b/public/app/listeners/postFile.js @@ -0,0 +1,31 @@ +const fse = require('fs-extra'); +const path = require('path'); +const { POST_FILE_CHANNEL } = require('../config/channels'); +const { buildFilePath } = require('../config/config'); +const logger = require('../logger'); +const { ERROR_GENERAL } = require('../config/errors'); + +const postFile = mainWindow => (event, payload = {}) => { + const { userId, spaceId, data } = payload; + // download file given path + const { path: filepath, name } = data; + const savePath = buildFilePath({ userId, spaceId, name }); + const dirPath = path.dirname(savePath); + try { + fse.ensureDirSync(dirPath); + fse.copySync(filepath, path.resolve(savePath)); + logger.debug(`the file ${name} was uploaded`); + + // update data + const newData = { name, uri: `file://${savePath}` }; + const newPayload = { ...payload, data: newData }; + + // send back the resource + mainWindow.webContents.send(POST_FILE_CHANNEL, newPayload); + } catch (e) { + logger.error(e); + mainWindow.webContents.send(POST_FILE_CHANNEL, ERROR_GENERAL); + } +}; + +module.exports = postFile; diff --git a/public/app/listeners/showClearUserInputPrompt.js b/public/app/listeners/showClearUserInputPrompt.js index 7fef4905..c7fe9873 100644 --- a/public/app/listeners/showClearUserInputPrompt.js +++ b/public/app/listeners/showClearUserInputPrompt.js @@ -5,15 +5,17 @@ const { } = require('../config/channels'); const logger = require('../logger'); -const showClearUserInputPrompt = mainWindow => () => { +const showClearUserInputPrompt = mainWindow => ( + event, + { message, buttons } +) => { logger.debug('showing clear user input prompt'); const options = { type: 'warning', - buttons: ['Cancel', 'Clear'], + buttons, defaultId: 0, cancelId: 0, - message: - 'Are you sure you want to clear all of the user input in this space?', + message, }; dialog.showMessageBox(mainWindow, options).then(({ response }) => { mainWindow.webContents.send( diff --git a/public/app/listeners/showDeleteSpacePrompt.js b/public/app/listeners/showDeleteSpacePrompt.js index ef7cb27b..cdbca3f8 100644 --- a/public/app/listeners/showDeleteSpacePrompt.js +++ b/public/app/listeners/showDeleteSpacePrompt.js @@ -3,15 +3,15 @@ const { dialog } = require('electron'); const { RESPOND_DELETE_SPACE_PROMPT_CHANNEL } = require('../config/channels'); const logger = require('../logger'); -const showDeleteSpacePrompt = mainWindow => () => { +const showDeleteSpacePrompt = mainWindow => (event, { message, buttons }) => { logger.debug('showing delete space prompt'); const options = { type: 'warning', - buttons: ['Cancel', 'Delete'], + buttons, defaultId: 0, cancelId: 0, - message: 'Are you sure you want to delete this space?', + message, }; dialog.showMessageBox(mainWindow, options).then(({ response }) => { diff --git a/public/electron.js b/public/electron.js index af1c347c..4007e836 100644 --- a/public/electron.js +++ b/public/electron.js @@ -37,6 +37,7 @@ const { SET_LANGUAGE_CHANNEL, GET_APP_INSTANCE_RESOURCES_CHANNEL, POST_APP_INSTANCE_RESOURCE_CHANNEL, + DELETE_APP_INSTANCE_RESOURCE_CHANNEL, PATCH_APP_INSTANCE_RESOURCE_CHANNEL, GET_APP_INSTANCE_CHANNEL, GET_DEVELOPER_MODE_CHANNEL, @@ -74,7 +75,9 @@ const { LOAD_SPACE_IN_CLASSROOM_CHANNEL, SET_ACTION_ACCESSIBILITY_CHANNEL, SET_ACTIONS_AS_ENABLED_CHANNEL, - TOUR_COMPLETED_CHANNEL, + COMPLETE_TOUR_CHANNEL, + POST_FILE_CHANNEL, + DELETE_FILE_CHANNEL, } = require('./app/config/channels'); const env = require('./env.json'); const { @@ -104,6 +107,7 @@ const { getAppInstanceResources, postAppInstanceResource, patchAppInstanceResource, + deleteAppInstanceResource, getAppInstance, setSyncMode, getSyncMode, @@ -129,6 +133,8 @@ const { setActionsAsEnabled, windowAllClosed, completeTour, + postFile, + deleteFile, } = require('./app/listeners'); const isMac = require('./app/utils/isMac'); @@ -490,6 +496,13 @@ app.on('ready', async () => { // called when creating an action ipcMain.on(POST_ACTION_CHANNEL, postAction(mainWindow, db)); + + // called when creating a file + ipcMain.on(POST_FILE_CHANNEL, postFile(mainWindow, db)); + + // called when creating a file + ipcMain.on(DELETE_FILE_CHANNEL, deleteFile(mainWindow, db)); + // called when logging in a user ipcMain.on(SIGN_IN_CHANNEL, signIn(mainWindow, db)); @@ -517,6 +530,12 @@ app.on('ready', async () => { patchAppInstanceResource(mainWindow, db) ); + // called when deleting an AppInstanceResource + ipcMain.on( + DELETE_APP_INSTANCE_RESOURCE_CHANNEL, + deleteAppInstanceResource(mainWindow, db) + ); + // called when getting an AppInstance ipcMain.on(GET_APP_INSTANCE_CHANNEL, getAppInstance(mainWindow, db)); @@ -604,7 +623,7 @@ app.on('ready', async () => { ipcMain.on(SYNC_SPACE_CHANNEL, syncSpace(mainWindow, db)); // called when a tour is closed or completed - ipcMain.on(TOUR_COMPLETED_CHANNEL, completeTour(mainWindow, db)); + ipcMain.on(COMPLETE_TOUR_CHANNEL, completeTour(mainWindow, db)); }); app.on('window-all-closed', () => windowAllClosed(mainWindow)); diff --git a/src/App.js b/src/App.js index aeecc261..a7859c9e 100644 --- a/src/App.js +++ b/src/App.js @@ -85,6 +85,7 @@ export class App extends Component { }).isRequired, connexionStatus: PropTypes.bool.isRequired, userMode: PropTypes.oneOf(USER_MODES).isRequired, + t: PropTypes.func.isRequired, }; static defaultProps = { @@ -144,16 +145,20 @@ export class App extends Component { }; triggerConnectionToastr = () => { - const { classes, connexionStatus } = this.props; + const { classes, connexionStatus, t } = this.props; if (connexionStatus) { - toastr.light(CONNECTION_MESSAGE_HEADER, CONNECTION_ONLINE_MESSAGE, { + toastr.light(t(CONNECTION_MESSAGE_HEADER), t(CONNECTION_ONLINE_MESSAGE), { icon: , }); } else { - toastr.light(CONNECTION_MESSAGE_HEADER, CONNECTION_OFFLINE_MESSAGE, { - icon: , - }); + toastr.light( + t(CONNECTION_MESSAGE_HEADER), + t(CONNECTION_OFFLINE_MESSAGE), + { + icon: , + } + ); } }; diff --git a/src/actions/appInstanceResource.js b/src/actions/appInstanceResource.js index 17fa5483..ecdfb5e6 100644 --- a/src/actions/appInstanceResource.js +++ b/src/actions/appInstanceResource.js @@ -2,11 +2,13 @@ import { GET_APP_INSTANCE_RESOURCES_CHANNEL, PATCH_APP_INSTANCE_RESOURCE_CHANNEL, POST_APP_INSTANCE_RESOURCE_CHANNEL, + DELETE_APP_INSTANCE_RESOURCE_CHANNEL, } from '../config/channels'; import { GET_APP_INSTANCE_RESOURCES_SUCCEEDED, PATCH_APP_INSTANCE_RESOURCE_SUCCEEDED, POST_APP_INSTANCE_RESOURCE_SUCCEEDED, + DELETE_APP_INSTANCE_RESOURCE_SUCCEEDED, } from '../types'; const getAppInstanceResources = async ( @@ -100,8 +102,38 @@ const patchAppInstanceResource = async ( } }; +const deleteAppInstanceResource = async ( + { id, data, appInstanceId, spaceId, subSpaceId, type } = {}, + callback +) => { + try { + window.ipcRenderer.send(DELETE_APP_INSTANCE_RESOURCE_CHANNEL, { + id, + data, + appInstanceId, + spaceId, + subSpaceId, + type, + }); + + window.ipcRenderer.once( + DELETE_APP_INSTANCE_RESOURCE_CHANNEL, + async (event, response) => { + callback({ + appInstanceId, + type: DELETE_APP_INSTANCE_RESOURCE_SUCCEEDED, + payload: response, + }); + } + ); + } catch (err) { + console.error(err); + } +}; + export { getAppInstanceResources, postAppInstanceResource, patchAppInstanceResource, + deleteAppInstanceResource, }; diff --git a/src/actions/authentication.js b/src/actions/authentication.js index 2b296dfb..c25b03d1 100644 --- a/src/actions/authentication.js +++ b/src/actions/authentication.js @@ -1,4 +1,5 @@ import { toastr } from 'react-redux-toastr'; +import i18n from '../config/i18n'; import { FLAG_SIGNING_IN, FLAG_SIGNING_OUT, @@ -33,7 +34,7 @@ const signIn = async ({ username, lang, anonymous }) => async dispatch => { window.ipcRenderer.send(SIGN_IN_CHANNEL, { username, lang, anonymous }); window.ipcRenderer.once(SIGN_IN_CHANNEL, async (event, user) => { if (user === ERROR_GENERAL) { - toastr.error(ERROR_MESSAGE_HEADER, ERROR_SIGNING_IN); + toastr.error(i18n.t(ERROR_MESSAGE_HEADER), i18n.t(ERROR_SIGNING_IN)); } else { dispatch({ type: SIGN_IN_SUCCEEDED, @@ -44,7 +45,7 @@ const signIn = async ({ username, lang, anonymous }) => async dispatch => { dispatch(flagSigningIn(false)); }); } catch (e) { - toastr.error(ERROR_MESSAGE_HEADER, ERROR_SIGNING_IN); + toastr.error(i18n.t(ERROR_MESSAGE_HEADER), i18n.t(ERROR_SIGNING_IN)); dispatch(flagSigningIn(false)); } }; @@ -55,7 +56,7 @@ const signOut = user => dispatch => { window.ipcRenderer.send(SIGN_OUT_CHANNEL); window.ipcRenderer.once(SIGN_OUT_CHANNEL, (event, response) => { if (response === ERROR_GENERAL) { - toastr.error(ERROR_MESSAGE_HEADER, ERROR_SIGNING_OUT); + toastr.error(i18n.t(ERROR_MESSAGE_HEADER), i18n.t(ERROR_SIGNING_OUT)); } else { dispatch({ type: SIGN_OUT_SUCCEEDED, @@ -78,7 +79,7 @@ const signOut = user => dispatch => { }); } catch (e) { console.error(e); - toastr.error(ERROR_MESSAGE_HEADER, ERROR_SIGNING_OUT); + toastr.error(i18n.t(ERROR_MESSAGE_HEADER), i18n.t(ERROR_SIGNING_OUT)); dispatch(flagSigningOut(false)); } }; @@ -91,7 +92,10 @@ const isAuthenticated = async () => dispatch => { IS_AUTHENTICATED_CHANNEL, (event, authenticated) => { if (authenticated === ERROR_GENERAL) { - toastr.error(ERROR_MESSAGE_HEADER, ERROR_GETTING_AUTHENTICATED); + toastr.error( + i18n.t(ERROR_MESSAGE_HEADER), + i18n.t(ERROR_GETTING_AUTHENTICATED) + ); } else { dispatch({ type: IS_AUTHENTICATED_SUCCEEDED, @@ -103,7 +107,10 @@ const isAuthenticated = async () => dispatch => { ); } catch (e) { console.error(e); - toastr.error(ERROR_MESSAGE_HEADER, ERROR_GETTING_AUTHENTICATED); + toastr.error( + i18n.t(ERROR_MESSAGE_HEADER), + i18n.t(ERROR_GETTING_AUTHENTICATED) + ); dispatch(flagGettingAuthenticated(false)); } }; diff --git a/src/actions/classroom.js b/src/actions/classroom.js index 7661a50c..f37e301b 100644 --- a/src/actions/classroom.js +++ b/src/actions/classroom.js @@ -1,4 +1,5 @@ import { toastr } from 'react-redux-toastr'; +import i18n from '../config/i18n'; import { ADD_CLASSROOM_SUCCEEDED, GET_CLASSROOMS_SUCCEEDED, @@ -91,7 +92,10 @@ export const getClassrooms = () => dispatch => { window.ipcRenderer.once(GET_CLASSROOMS_CHANNEL, (event, classrooms) => { // dispatch that the getter has succeeded if (classrooms === ERROR_ACCESS_DENIED_CLASSROOM) { - toastr.error(ERROR_MESSAGE_HEADER, ERROR_ACCESS_DENIED_CLASSROOM_MESSAGE); + toastr.error( + i18n.t(ERROR_MESSAGE_HEADER), + i18n.t(ERROR_ACCESS_DENIED_CLASSROOM_MESSAGE) + ); } else { dispatch({ type: GET_CLASSROOMS_SUCCEEDED, @@ -112,18 +116,24 @@ export const getClassroom = async payload => async dispatch => { window.ipcRenderer.once(GET_CLASSROOM_CHANNEL, async (event, response) => { // if there is no response, show error if (!response) { - toastr.error(ERROR_MESSAGE_HEADER, ERROR_GETTING_CLASSROOM_MESSAGE); + toastr.error( + i18n.t(ERROR_MESSAGE_HEADER), + i18n.t(ERROR_GETTING_CLASSROOM_MESSAGE) + ); } switch (response) { case ERROR_ACCESS_DENIED_CLASSROOM: toastr.error( - ERROR_MESSAGE_HEADER, - ERROR_ACCESS_DENIED_CLASSROOM_MESSAGE + i18n.t(ERROR_MESSAGE_HEADER), + i18n.t(ERROR_ACCESS_DENIED_CLASSROOM_MESSAGE) ); break; case ERROR_GENERAL: - toastr.error(ERROR_MESSAGE_HEADER, ERROR_GETTING_CLASSROOM_MESSAGE); + toastr.error( + i18n.t(ERROR_MESSAGE_HEADER), + i18n.t(ERROR_GETTING_CLASSROOM_MESSAGE) + ); break; // todo: check that it is actually a classroom before dispatching success default: @@ -135,7 +145,10 @@ export const getClassroom = async payload => async dispatch => { return dispatch(flagGettingClassroom(false)); }); } catch (err) { - toastr.error(ERROR_MESSAGE_HEADER, ERROR_GETTING_CLASSROOM_MESSAGE); + toastr.error( + i18n.t(ERROR_MESSAGE_HEADER), + i18n.t(ERROR_GETTING_CLASSROOM_MESSAGE) + ); } }; @@ -150,24 +163,30 @@ export const addClassroom = payload => dispatch => { window.ipcRenderer.once(ADD_CLASSROOM_CHANNEL, async (event, response) => { // if there is no response, show error if (!response) { - toastr.error(ERROR_MESSAGE_HEADER, ERROR_ADDING_CLASSROOM_MESSAGE); + toastr.error( + i18n.t(ERROR_MESSAGE_HEADER), + i18n.t(ERROR_ADDING_CLASSROOM_MESSAGE) + ); } switch (response) { case ERROR_DUPLICATE_CLASSROOM_NAME: toastr.error( - ERROR_MESSAGE_HEADER, - ERROR_DUPLICATE_CLASSROOM_NAME_MESSAGE + i18n.t(ERROR_MESSAGE_HEADER), + i18n.t(ERROR_DUPLICATE_CLASSROOM_NAME_MESSAGE) ); break; case ERROR_GENERAL: - toastr.error(ERROR_MESSAGE_HEADER, ERROR_ADDING_CLASSROOM_MESSAGE); + toastr.error( + i18n.t(ERROR_MESSAGE_HEADER), + i18n.t(ERROR_ADDING_CLASSROOM_MESSAGE) + ); break; // todo: check that it is actually a classroom before dispatching success default: toastr.success( - SUCCESS_MESSAGE_HEADER, - SUCCESS_ADDING_CLASSROOM_MESSAGE + i18n.t(SUCCESS_MESSAGE_HEADER), + i18n.t(SUCCESS_ADDING_CLASSROOM_MESSAGE) ); dispatch({ type: ADD_CLASSROOM_SUCCEEDED, @@ -177,7 +196,10 @@ export const addClassroom = payload => dispatch => { return dispatch(flagAddingClassroom(false)); }); } catch (err) { - toastr.error(ERROR_MESSAGE_HEADER, ERROR_ADDING_CLASSROOM_MESSAGE); + toastr.error( + i18n.t(ERROR_MESSAGE_HEADER), + i18n.t(ERROR_ADDING_CLASSROOM_MESSAGE) + ); dispatch(flagAddingClassroom(false)); } }; @@ -196,14 +218,17 @@ export const deleteClassroom = ({ id, name }) => dispatch => { ); window.ipcRenderer.once(DELETE_CLASSROOM_CHANNEL, (event, response) => { if (response === ERROR_GENERAL) { - toastr.error(ERROR_MESSAGE_HEADER, ERROR_DELETING_CLASSROOM_MESSAGE); + toastr.error( + i18n.t(ERROR_MESSAGE_HEADER), + i18n.t(ERROR_DELETING_CLASSROOM_MESSAGE) + ); } else { // update saved classrooms in state dispatch(getClassrooms()); toastr.success( - SUCCESS_MESSAGE_HEADER, - SUCCESS_DELETING_CLASSROOM_MESSAGE + i18n.t(SUCCESS_MESSAGE_HEADER), + i18n.t(SUCCESS_DELETING_CLASSROOM_MESSAGE) ); } @@ -216,13 +241,19 @@ export const editClassroom = payload => dispatch => { window.ipcRenderer.send(EDIT_CLASSROOM_CHANNEL, payload); window.ipcRenderer.once(EDIT_CLASSROOM_CHANNEL, (event, response) => { if (response === ERROR_GENERAL) { - toastr.error(ERROR_MESSAGE_HEADER, ERROR_EDITING_CLASSROOM_MESSAGE); + toastr.error( + i18n.t(ERROR_MESSAGE_HEADER), + i18n.t(ERROR_EDITING_CLASSROOM_MESSAGE) + ); } else { // update saved classrooms and current classroom in state dispatch(getClassroom(payload)); dispatch(getClassrooms()); - toastr.success(SUCCESS_MESSAGE_HEADER, SUCCESS_EDITING_CLASSROOM_MESSAGE); + toastr.success( + i18n.t(SUCCESS_MESSAGE_HEADER), + i18n.t(SUCCESS_EDITING_CLASSROOM_MESSAGE) + ); } dispatch(flagEditingClassroom(false)); @@ -237,15 +268,15 @@ export const addUserInClassroom = payload => dispatch => { switch (response) { case ERROR_DUPLICATE_USERNAME_IN_CLASSROOM: toastr.error( - ERROR_MESSAGE_HEADER, - ERROR_DUPLICATE_USERNAME_IN_CLASSROOM_MESSAGE + i18n.t(ERROR_MESSAGE_HEADER), + i18n.t(ERROR_DUPLICATE_USERNAME_IN_CLASSROOM_MESSAGE) ); break; case ERROR_GENERAL: toastr.error( - ERROR_MESSAGE_HEADER, - ERROR_ADDING_USER_IN_CLASSROOM_MESSAGE + i18n.t(ERROR_MESSAGE_HEADER), + i18n.t(ERROR_ADDING_USER_IN_CLASSROOM_MESSAGE) ); break; default: @@ -269,7 +300,10 @@ export const deleteUsersInClassroom = payload => dispatch => { RESPOND_DELETE_USERS_IN_CLASSROOM_PROMPT_CHANNEL, (event, response) => { if (response === ERROR_NO_USER_TO_DELETE) { - toastr.error(ERROR_MESSAGE_HEADER, ERROR_NO_USER_TO_DELETE_MESSAGE); + toastr.error( + i18n.t(ERROR_MESSAGE_HEADER), + i18n.t(ERROR_NO_USER_TO_DELETE_MESSAGE) + ); } // accept deletion if (response === 1) { @@ -281,13 +315,16 @@ export const deleteUsersInClassroom = payload => dispatch => { window.ipcRenderer.once(DELETE_USERS_IN_CLASSROOM_CHANNEL, (e, response) => { switch (response) { case ERROR_NO_USER_TO_DELETE: { - toastr.error(ERROR_MESSAGE_HEADER, ERROR_NO_USER_TO_DELETE_MESSAGE); + toastr.error( + i18n.t(ERROR_MESSAGE_HEADER), + i18n.t(ERROR_NO_USER_TO_DELETE_MESSAGE) + ); break; } case ERROR_GENERAL: { toastr.error( - ERROR_MESSAGE_HEADER, - ERROR_DELETING_USER_IN_CLASSROOM_MESSAGE + i18n.t(ERROR_MESSAGE_HEADER), + i18n.t(ERROR_DELETING_USER_IN_CLASSROOM_MESSAGE) ); break; } @@ -301,8 +338,8 @@ export const deleteUsersInClassroom = payload => dispatch => { ); toastr.success( - SUCCESS_MESSAGE_HEADER, - SUCCESS_DELETING_USERS_IN_CLASSROOM_MESSAGE + i18n.t(SUCCESS_MESSAGE_HEADER), + i18n.t(SUCCESS_DELETING_USERS_IN_CLASSROOM_MESSAGE) ); } } @@ -317,8 +354,8 @@ export const editUserInClassroom = payload => dispatch => { window.ipcRenderer.once(EDIT_USER_IN_CLASSROOM_CHANNEL, (event, response) => { if (response === ERROR_GENERAL) { toastr.error( - ERROR_MESSAGE_HEADER, - ERROR_EDITING_USER_IN_CLASSROOM_MESSAGE + i18n.t(ERROR_MESSAGE_HEADER), + i18n.t(ERROR_EDITING_USER_IN_CLASSROOM_MESSAGE) ); } else { // update saved classrooms in state @@ -327,8 +364,8 @@ export const editUserInClassroom = payload => dispatch => { ); toastr.success( - SUCCESS_MESSAGE_HEADER, - SUCCESS_EDITING_USER_IN_CLASSROOM_MESSAGE + i18n.t(SUCCESS_MESSAGE_HEADER), + i18n.t(SUCCESS_EDITING_USER_IN_CLASSROOM_MESSAGE) ); } @@ -347,8 +384,8 @@ export const createGetSpaceInClassroom = ( window.ipcRenderer.once(GET_SPACE_IN_CLASSROOM_CHANNEL, (event, response) => { if (response === ERROR_GENERAL) { toastr.error( - ERROR_MESSAGE_HEADER, - ERROR_GETTING_SPACE_IN_CLASSROOM_MESSAGE + i18n.t(ERROR_MESSAGE_HEADER), + i18n.t(ERROR_GETTING_SPACE_IN_CLASSROOM_MESSAGE) ); } else { dispatch({ @@ -401,22 +438,31 @@ export const loadSpaceInClassroom = payload => dispatch => { (event, response) => { switch (response) { case ERROR_GENERAL: - toastr.error(ERROR_MESSAGE_HEADER, ERROR_LOADING_MESSAGE); + toastr.error( + i18n.t(ERROR_MESSAGE_HEADER), + i18n.t(ERROR_LOADING_MESSAGE) + ); break; case ERROR_DUPLICATE_USERNAME_IN_CLASSROOM: toastr.error( - ERROR_MESSAGE_HEADER, - ERROR_DUPLICATE_USERNAME_IN_CLASSROOM_MESSAGE + i18n.t(ERROR_MESSAGE_HEADER), + i18n.t(ERROR_DUPLICATE_USERNAME_IN_CLASSROOM_MESSAGE) ); break; case ERROR_INVALID_USERNAME: - toastr.error(ERROR_MESSAGE_HEADER, ERROR_INVALID_USERNAME_MESSAGE); + toastr.error( + i18n.t(ERROR_MESSAGE_HEADER), + i18n.t(ERROR_INVALID_USERNAME_MESSAGE) + ); break; default: dispatch({ type: LOAD_SPACE_IN_CLASSROOM_SUCCEEDED, }); - toastr.success(SUCCESS_MESSAGE_HEADER, SUCCESS_SPACE_LOADED_MESSAGE); + toastr.success( + i18n.t(SUCCESS_MESSAGE_HEADER), + i18n.t(SUCCESS_SPACE_LOADED_MESSAGE) + ); } dispatch(flagLoadingSpaceInClassroom(false)); } diff --git a/src/actions/developer.js b/src/actions/developer.js index f3a2d98c..8ce04d54 100644 --- a/src/actions/developer.js +++ b/src/actions/developer.js @@ -1,4 +1,5 @@ import { toastr } from 'react-redux-toastr'; +import i18n from '../config/i18n'; import { createFlag } from './common'; import { FLAG_GETTING_DATABASE, @@ -23,7 +24,10 @@ const getDatabase = async () => dispatch => { window.ipcRenderer.send(GET_DATABASE_CHANNEL); window.ipcRenderer.once(GET_DATABASE_CHANNEL, (event, db) => { if (db === ERROR_GENERAL) { - toastr.error(ERROR_MESSAGE_HEADER, ERROR_GETTING_DATABASE); + toastr.error( + i18n.t(ERROR_MESSAGE_HEADER), + i18n.t(ERROR_GETTING_DATABASE) + ); } else { dispatch({ type: GET_DATABASE_SUCCEEDED, @@ -34,7 +38,7 @@ const getDatabase = async () => dispatch => { }); } catch (err) { console.error(err); - toastr.error(ERROR_MESSAGE_HEADER, ERROR_GETTING_DATABASE); + toastr.error(i18n.t(ERROR_MESSAGE_HEADER), i18n.t(ERROR_GETTING_DATABASE)); } }; @@ -44,7 +48,10 @@ const setDatabase = async database => dispatch => { window.ipcRenderer.send(SET_DATABASE_CHANNEL, database); window.ipcRenderer.once(SET_DATABASE_CHANNEL, (event, db) => { if (db === ERROR_GENERAL) { - toastr.error(ERROR_MESSAGE_HEADER, ERROR_GETTING_DATABASE); + toastr.error( + i18n.t(ERROR_MESSAGE_HEADER), + i18n.t(ERROR_GETTING_DATABASE) + ); } else { dispatch({ type: SET_DATABASE_SUCCEEDED, @@ -55,7 +62,7 @@ const setDatabase = async database => dispatch => { }); } catch (err) { console.error(err); - toastr.error(ERROR_MESSAGE_HEADER, ERROR_SETTING_DATABASE); + toastr.error(i18n.t(ERROR_MESSAGE_HEADER), i18n.t(ERROR_SETTING_DATABASE)); } }; diff --git a/src/actions/exportSpace.js b/src/actions/exportSpace.js index 4c0a0618..7f6872cd 100644 --- a/src/actions/exportSpace.js +++ b/src/actions/exportSpace.js @@ -1,4 +1,5 @@ import { toastr } from 'react-redux-toastr'; +import i18n from '../config/i18n'; import { FLAG_EXPORTING_SPACE, EXPORT_SPACE_SUCCESS, @@ -46,9 +47,15 @@ export const exportSpace = (id, spaceName, userId, selection) => dispatch => { ); window.ipcRenderer.once(EXPORTED_SPACE_CHANNEL, (event, response) => { if (response === ERROR_GENERAL) { - toastr.error(ERROR_MESSAGE_HEADER, ERROR_EXPORTING_MESSAGE); + toastr.error( + i18n.t(ERROR_MESSAGE_HEADER), + i18n.t(ERROR_EXPORTING_MESSAGE) + ); } else { - toastr.success(SUCCESS_MESSAGE_HEADER, SUCCESS_EXPORTING_MESSAGE); + toastr.success( + i18n.t(SUCCESS_MESSAGE_HEADER), + i18n.t(SUCCESS_EXPORTING_MESSAGE) + ); dispatch({ type: EXPORT_SPACE_SUCCESS, }); diff --git a/src/actions/file.js b/src/actions/file.js new file mode 100644 index 00000000..cea040c4 --- /dev/null +++ b/src/actions/file.js @@ -0,0 +1,77 @@ +import { POST_FILE_CHANNEL, DELETE_FILE_CHANNEL } from '../config/channels'; +import { + POST_FILE_SUCCEEDED, + POST_FILE_FAILED, + DELETE_FILE_FAILED, + DELETE_FILE_SUCCEEDED, +} from '../types'; +import { ERROR_GENERAL } from '../config/errors'; +import { + ERROR_POSTING_FILE_MESSAGE, + ERROR_DELETING_FILE_MESSAGE, +} from '../config/messages'; + +export const postFile = async ( + { userId, appInstanceId, spaceId, subSpaceId, format, data, type } = {}, + callback +) => { + try { + window.ipcRenderer.send(POST_FILE_CHANNEL, { + userId, + appInstanceId, + spaceId, + subSpaceId, + format, + type, + data, + }); + + window.ipcRenderer.once(POST_FILE_CHANNEL, async (event, response) => { + if (response === ERROR_GENERAL) { + callback({ + appInstanceId, + type: POST_FILE_FAILED, + payload: ERROR_POSTING_FILE_MESSAGE, + }); + } else { + callback({ + // have to include the appInstanceId to avoid broadcasting + appInstanceId, + type: POST_FILE_SUCCEEDED, + payload: response, + }); + } + }); + } catch (err) { + callback({ + appInstanceId, + type: POST_FILE_FAILED, + payload: ERROR_POSTING_FILE_MESSAGE, + }); + } +}; + +export const deleteFile = async (payload = {}, callback) => { + try { + window.ipcRenderer.send(DELETE_FILE_CHANNEL, payload); + const { appInstanceId } = payload; + window.ipcRenderer.once(DELETE_FILE_CHANNEL, async (event, response) => { + if (response === ERROR_GENERAL) { + callback({ + appInstanceId, + type: DELETE_FILE_FAILED, + payload: ERROR_DELETING_FILE_MESSAGE, + }); + } else { + callback({ + // have to include the appInstanceId to avoid broadcasting + appInstanceId, + type: DELETE_FILE_SUCCEEDED, + payload: response, + }); + } + }); + } catch (err) { + console.error(err); + } +}; diff --git a/src/actions/index.js b/src/actions/index.js index f91254a0..000ab200 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -12,3 +12,4 @@ export * from './loadSpace'; export * from './exportSpace'; export * from './classroom'; export * from './tour'; +export * from './file'; diff --git a/src/actions/loadSpace.js b/src/actions/loadSpace.js index 43393544..acdaa256 100644 --- a/src/actions/loadSpace.js +++ b/src/actions/loadSpace.js @@ -1,4 +1,5 @@ import { toastr } from 'react-redux-toastr'; +import i18n from '../config/i18n'; import { FLAG_LOADING_SPACE, FLAG_EXTRACTING_FILE_TO_LOAD_SPACE, @@ -42,13 +43,19 @@ export const loadSpace = payload => dispatch => { window.ipcRenderer.once(LOADED_SPACE_CHANNEL, (event, response) => { switch (response) { case ERROR_GENERAL: - toastr.error(ERROR_MESSAGE_HEADER, ERROR_LOADING_MESSAGE); + toastr.error( + i18n.t(ERROR_MESSAGE_HEADER), + i18n.t(ERROR_LOADING_MESSAGE) + ); break; default: dispatch({ type: LOAD_SPACE_SUCCEEDED, }); - toastr.success(SUCCESS_MESSAGE_HEADER, SUCCESS_SPACE_LOADED_MESSAGE); + toastr.success( + i18n.t(SUCCESS_MESSAGE_HEADER), + i18n.t(SUCCESS_SPACE_LOADED_MESSAGE) + ); setSpaceAsRecent({ spaceId: response.spaceId, recent: true })(dispatch); } dispatch(flagLoadingSpace(false)); @@ -77,19 +84,28 @@ export const createExtractFile = ( async (event, response) => { switch (response) { case ERROR_ZIP_CORRUPTED: - toastr.error(ERROR_MESSAGE_HEADER, ERROR_ZIP_CORRUPTED_MESSAGE); + toastr.error( + i18n.t(ERROR_MESSAGE_HEADER), + i18n.t(ERROR_ZIP_CORRUPTED_MESSAGE) + ); break; case ERROR_JSON_CORRUPTED: - toastr.error(ERROR_MESSAGE_HEADER, ERROR_JSON_CORRUPTED_MESSAGE); + toastr.error( + i18n.t(ERROR_MESSAGE_HEADER), + i18n.t(ERROR_JSON_CORRUPTED_MESSAGE) + ); break; case ERROR_SPACE_ALREADY_AVAILABLE: toastr.error( - ERROR_MESSAGE_HEADER, - ERROR_SPACE_ALREADY_AVAILABLE_MESSAGE + i18n.t(ERROR_MESSAGE_HEADER), + i18n.t(ERROR_SPACE_ALREADY_AVAILABLE_MESSAGE) ); break; case ERROR_GENERAL: - toastr.error(ERROR_MESSAGE_HEADER, ERROR_LOADING_MESSAGE); + toastr.error( + i18n.t(ERROR_MESSAGE_HEADER), + i18n.t(ERROR_LOADING_MESSAGE) + ); break; default: // wait for saved space @@ -120,7 +136,10 @@ export const createClearLoadSpace = (payload, type, flagType) => dispatch => { window.ipcRenderer.once(CLEAR_LOAD_SPACE_CHANNEL, (event, response) => { switch (response) { case ERROR_GENERAL: - toastr.error(ERROR_MESSAGE_HEADER, ERROR_LOADING_MESSAGE); + toastr.error( + i18n.t(ERROR_MESSAGE_HEADER), + i18n.t(ERROR_LOADING_MESSAGE) + ); break; default: dispatch({ diff --git a/src/actions/space.js b/src/actions/space.js index c52e6a55..5c005540 100644 --- a/src/actions/space.js +++ b/src/actions/space.js @@ -1,4 +1,5 @@ import { toastr } from 'react-redux-toastr'; +import i18n from '../config/i18n'; import { GET_SPACES, FLAG_GETTING_SPACE, @@ -47,6 +48,8 @@ import { SUCCESS_SAVING_MESSAGE, ERROR_CLEARING_USER_INPUT_MESSAGE, SUCCESS_CLEARING_USER_INPUT_MESSAGE, + PROMPT_DELETE_SPACE_MESSAGE, + PROMPT_CLEAR_USER_INPUT_MESSAGE, } from '../config/messages'; import { createFlag, isErrorResponse } from './common'; import { @@ -110,7 +113,10 @@ const createGetLocalSpace = async ( }); } catch (err) { if (showError) { - toastr.error(ERROR_MESSAGE_HEADER, ERROR_GETTING_SPACE_MESSAGE); + toastr.error( + i18n.t(ERROR_MESSAGE_HEADER), + i18n.t(ERROR_GETTING_SPACE_MESSAGE) + ); } } finally { dispatch(flagGettingSpace(false)); @@ -159,7 +165,10 @@ const createGetRemoteSpace = async ( payload: remoteSpace, }); } catch (err) { - toastr.error(ERROR_MESSAGE_HEADER, ERROR_GETTING_SPACE_MESSAGE); + toastr.error( + i18n.t(ERROR_MESSAGE_HEADER), + i18n.t(ERROR_GETTING_SPACE_MESSAGE) + ); } finally { dispatch(flagGettingSpace(false)); } @@ -194,24 +203,33 @@ const saveSpace = async ({ space }) => async dispatch => { // if there is no response, show error if (!response) { dispatch(flagSavingSpace(false)); - return toastr.error(ERROR_MESSAGE_HEADER, ERROR_SAVING_SPACE_MESSAGE); + return toastr.error( + i18n.t(ERROR_MESSAGE_HEADER), + i18n.t(ERROR_SAVING_SPACE_MESSAGE) + ); } switch (response) { case ERROR_SPACE_ALREADY_AVAILABLE: toastr.error( - ERROR_MESSAGE_HEADER, - ERROR_SPACE_ALREADY_AVAILABLE_MESSAGE + i18n.t(ERROR_MESSAGE_HEADER), + i18n.t(ERROR_SPACE_ALREADY_AVAILABLE_MESSAGE) ); break; case ERROR_DOWNLOADING_FILE: - toastr.error(ERROR_MESSAGE_HEADER, ERROR_DOWNLOADING_MESSAGE); + toastr.error( + i18n.t(ERROR_MESSAGE_HEADER), + i18n.t(ERROR_DOWNLOADING_MESSAGE) + ); break; // todo: check that it is actually a space before dispatching success default: - toastr.success(SUCCESS_MESSAGE_HEADER, SUCCESS_SAVING_MESSAGE); + toastr.success( + i18n.t(SUCCESS_MESSAGE_HEADER), + i18n.t(SUCCESS_SAVING_MESSAGE) + ); dispatch({ type: SAVE_SPACE_SUCCEEDED, payload: response, @@ -220,7 +238,10 @@ const saveSpace = async ({ space }) => async dispatch => { return dispatch(flagSavingSpace(false)); }); } catch (err) { - toastr.error(ERROR_MESSAGE_HEADER, ERROR_SAVING_SPACE_MESSAGE); + toastr.error( + i18n.t(ERROR_MESSAGE_HEADER), + i18n.t(ERROR_SAVING_SPACE_MESSAGE) + ); dispatch(flagSavingSpace(false)); } }; @@ -234,7 +255,11 @@ const clearSpace = () => dispatch => { const deleteSpace = ({ id }) => dispatch => { // show confirmation prompt - window.ipcRenderer.send(SHOW_DELETE_SPACE_PROMPT_CHANNEL); + const buttons = [i18n.t('Cancel'), i18n.t('Delete')]; + window.ipcRenderer.send(SHOW_DELETE_SPACE_PROMPT_CHANNEL, { + message: i18n.t(PROMPT_DELETE_SPACE_MESSAGE), + buttons, + }); window.ipcRenderer.once( RESPOND_DELETE_SPACE_PROMPT_CHANNEL, (event, response) => { @@ -246,12 +271,18 @@ const deleteSpace = ({ id }) => dispatch => { ); window.ipcRenderer.once(DELETED_SPACE_CHANNEL, (event, response) => { if (response === ERROR_GENERAL) { - toastr.error(ERROR_MESSAGE_HEADER, ERROR_DELETING_MESSAGE); + toastr.error( + i18n.t(ERROR_MESSAGE_HEADER), + i18n.t(ERROR_DELETING_MESSAGE) + ); } else { // update saved spaces in state dispatch(getSpaces()); - toastr.success(SUCCESS_MESSAGE_HEADER, SUCCESS_DELETING_MESSAGE); + toastr.success( + i18n.t(SUCCESS_MESSAGE_HEADER), + i18n.t(SUCCESS_DELETING_MESSAGE) + ); dispatch({ type: DELETE_SPACE_SUCCESS, payload: true, @@ -271,7 +302,11 @@ const deleteSpace = ({ id }) => dispatch => { const clearUserInput = async ({ spaceId, userId }) => async dispatch => { try { // show confirmation prompt - window.ipcRenderer.send(SHOW_CLEAR_USER_INPUT_PROMPT_CHANNEL); + const buttons = [i18n.t('Cancel'), i18n.t('Clear')]; + window.ipcRenderer.send(SHOW_CLEAR_USER_INPUT_PROMPT_CHANNEL, { + message: i18n.t(PROMPT_CLEAR_USER_INPUT_MESSAGE), + buttons, + }); // listen for response from prompt window.ipcRenderer.once( @@ -290,11 +325,14 @@ const clearUserInput = async ({ spaceId, userId }) => async dispatch => { // listen for response from backend window.ipcRenderer.once(CLEARED_USER_INPUT_CHANNEL, (event, response) => { if (response === ERROR_GENERAL) { - toastr.error(ERROR_MESSAGE_HEADER, ERROR_CLEARING_USER_INPUT_MESSAGE); + toastr.error( + i18n.t(ERROR_MESSAGE_HEADER), + i18n.t(ERROR_CLEARING_USER_INPUT_MESSAGE) + ); } else { toastr.success( - SUCCESS_MESSAGE_HEADER, - SUCCESS_CLEARING_USER_INPUT_MESSAGE + i18n.t(SUCCESS_MESSAGE_HEADER), + i18n.t(SUCCESS_CLEARING_USER_INPUT_MESSAGE) ); dispatch({ type: GET_SPACE_SUCCEEDED, @@ -338,7 +376,10 @@ const getSpacesNearby = async ({ payload: spaces, }); } catch (err) { - toastr.error(ERROR_MESSAGE_HEADER, ERROR_GETTING_SPACES_NEARBY); + toastr.error( + i18n.t(ERROR_MESSAGE_HEADER), + i18n.t(ERROR_GETTING_SPACES_NEARBY) + ); } finally { dispatch(flagGettingSpacesNearby(false)); } diff --git a/src/actions/syncSpace.js b/src/actions/syncSpace.js index 2d78e014..bcc84890 100644 --- a/src/actions/syncSpace.js +++ b/src/actions/syncSpace.js @@ -1,4 +1,5 @@ import { toastr } from 'react-redux-toastr'; +import i18n from '../config/i18n'; import { createGetLocalSpace, createGetRemoteSpace, getSpaces } from './space'; import { GET_SYNC_REMOTE_SPACE_SUCCEEDED, @@ -39,12 +40,18 @@ export const syncSpace = async ({ id }) => async dispatch => { // this runs once the space has been synced window.ipcRenderer.once(SYNCED_SPACE_CHANNEL, (event, res) => { if (res === ERROR_GENERAL) { - toastr.error(ERROR_MESSAGE_HEADER, ERROR_SYNCING_MESSAGE); + toastr.error( + i18n.t(ERROR_MESSAGE_HEADER), + i18n.t(ERROR_SYNCING_MESSAGE) + ); } else { // update saved spaces in state dispatch(getSpaces()); - toastr.success(SUCCESS_MESSAGE_HEADER, SUCCESS_SYNCING_MESSAGE); + toastr.success( + i18n.t(SUCCESS_MESSAGE_HEADER), + i18n.t(SUCCESS_SYNCING_MESSAGE) + ); dispatch({ type: SYNC_SPACE_SUCCEEDED, payload: res, @@ -54,7 +61,7 @@ export const syncSpace = async ({ id }) => async dispatch => { }); } catch (err) { dispatch(flagSyncingSpace(false)); - toastr.error(ERROR_MESSAGE_HEADER, ERROR_SYNCING_MESSAGE); + toastr.error(i18n.t(ERROR_MESSAGE_HEADER), i18n.t(ERROR_SYNCING_MESSAGE)); } }; diff --git a/src/actions/tour.js b/src/actions/tour.js index 927dc264..1a86e0ef 100644 --- a/src/actions/tour.js +++ b/src/actions/tour.js @@ -1,18 +1,25 @@ -import { TOUR_COMPLETED_CHANNEL } from '../config/channels'; +import { COMPLETE_TOUR_CHANNEL } from '../config/channels'; import { ERROR_GENERAL } from '../config/errors'; import { INITIALIZE_TOUR, NAVIGATE_STOP_TOUR, - NEXT_OR_PREV_TOUR, + NEXT_TOUR_STEP, + PREV_TOUR_STEP, RESET_TOUR, RESTART_TOUR, START_TOUR, STOP_TOUR, } from '../types/tour'; -const nextStepTour = payload => dispatch => +const goToNextStep = payload => dispatch => dispatch({ - type: NEXT_OR_PREV_TOUR, + type: NEXT_TOUR_STEP, + payload, + }); + +const goToPrevStep = payload => dispatch => + dispatch({ + type: PREV_TOUR_STEP, payload, }); @@ -53,8 +60,8 @@ const completeTour = async tourName => dispatch => { dispatch({ type: RESET_TOUR, }); - window.ipcRenderer.send(TOUR_COMPLETED_CHANNEL, { tourName }); - window.ipcRenderer.once(TOUR_COMPLETED_CHANNEL, async (event, error) => { + window.ipcRenderer.send(COMPLETE_TOUR_CHANNEL, { tourName }); + window.ipcRenderer.once(COMPLETE_TOUR_CHANNEL, async (event, error) => { if (error === ERROR_GENERAL) { console.error(error); } @@ -66,7 +73,8 @@ const completeTour = async tourName => dispatch => { export { stopTour, - nextStepTour, + goToNextStep, + goToPrevStep, restartTour, startTour, navigateStopTour, diff --git a/src/actions/user.js b/src/actions/user.js index a236eac7..a2e97a5e 100644 --- a/src/actions/user.js +++ b/src/actions/user.js @@ -1,4 +1,5 @@ import { toastr } from 'react-redux-toastr'; +import i18n from '../config/i18n'; import { getCurrentPosition } from '../utils/geolocation'; import { GET_GEOLOCATION_SUCCEEDED, @@ -117,7 +118,10 @@ const getGeolocation = async () => async dispatch => { }); } catch (e) { console.error(e); - toastr.error(ERROR_MESSAGE_HEADER, ERROR_GETTING_GEOLOCATION); + toastr.error( + i18n.t(ERROR_MESSAGE_HEADER), + i18n.t(ERROR_GETTING_GEOLOCATION) + ); } } }; @@ -128,7 +132,10 @@ const getUserFolder = async () => dispatch => { window.ipcRenderer.send(GET_USER_FOLDER_CHANNEL); window.ipcRenderer.once(GET_USER_FOLDER_CHANNEL, (event, folder) => { if (folder === ERROR_GENERAL) { - toastr.error(ERROR_MESSAGE_HEADER, ERROR_GETTING_USER_FOLDER); + toastr.error( + i18n.t(ERROR_MESSAGE_HEADER), + i18n.t(ERROR_GETTING_USER_FOLDER) + ); } else { dispatch({ type: GET_USER_FOLDER_SUCCEEDED, @@ -139,7 +146,10 @@ const getUserFolder = async () => dispatch => { }); } catch (e) { console.error(e); - toastr.error(ERROR_MESSAGE_HEADER, ERROR_GETTING_USER_FOLDER); + toastr.error( + i18n.t(ERROR_MESSAGE_HEADER), + i18n.t(ERROR_GETTING_USER_FOLDER) + ); dispatch(flagGettingUserFolder(false)); } }; @@ -150,7 +160,10 @@ const getLanguage = async () => dispatch => { window.ipcRenderer.send(GET_LANGUAGE_CHANNEL); window.ipcRenderer.once(GET_LANGUAGE_CHANNEL, (event, lang) => { if (lang === ERROR_GENERAL) { - toastr.error(ERROR_MESSAGE_HEADER, ERROR_GETTING_LANGUAGE); + toastr.error( + i18n.t(ERROR_MESSAGE_HEADER), + i18n.t(ERROR_GETTING_LANGUAGE) + ); } else { dispatch({ type: GET_LANGUAGE_SUCCEEDED, @@ -161,7 +174,7 @@ const getLanguage = async () => dispatch => { }); } catch (e) { console.error(e); - toastr.error(ERROR_MESSAGE_HEADER, ERROR_GETTING_LANGUAGE); + toastr.error(i18n.t(ERROR_MESSAGE_HEADER), i18n.t(ERROR_GETTING_LANGUAGE)); } }; @@ -171,7 +184,10 @@ const setLanguage = async ({ lang }) => dispatch => { window.ipcRenderer.send(SET_LANGUAGE_CHANNEL, lang); window.ipcRenderer.once(SET_LANGUAGE_CHANNEL, (event, language) => { if (language === ERROR_GENERAL) { - toastr.error(ERROR_MESSAGE_HEADER, ERROR_SETTING_LANGUAGE); + toastr.error( + i18n.t(ERROR_MESSAGE_HEADER), + i18n.t(ERROR_SETTING_LANGUAGE) + ); } else { dispatch({ type: SET_LANGUAGE_SUCCEEDED, @@ -182,7 +198,7 @@ const setLanguage = async ({ lang }) => dispatch => { }); } catch (e) { console.error(e); - toastr.error(ERROR_MESSAGE_HEADER, ERROR_SETTING_LANGUAGE); + toastr.error(i18n.t(ERROR_MESSAGE_HEADER), i18n.t(ERROR_SETTING_LANGUAGE)); } }; @@ -194,7 +210,10 @@ const getDeveloperMode = async () => dispatch => { GET_DEVELOPER_MODE_CHANNEL, (event, developerMode) => { if (developerMode === ERROR_GENERAL) { - toastr.error(ERROR_MESSAGE_HEADER, ERROR_GETTING_DEVELOPER_MODE); + toastr.error( + i18n.t(ERROR_MESSAGE_HEADER), + i18n.t(ERROR_GETTING_DEVELOPER_MODE) + ); } else { dispatch({ type: GET_DEVELOPER_MODE_SUCCEEDED, @@ -206,7 +225,10 @@ const getDeveloperMode = async () => dispatch => { ); } catch (e) { console.error(e); - toastr.error(ERROR_MESSAGE_HEADER, ERROR_GETTING_DEVELOPER_MODE); + toastr.error( + i18n.t(ERROR_MESSAGE_HEADER), + i18n.t(ERROR_GETTING_DEVELOPER_MODE) + ); } }; @@ -216,7 +238,10 @@ const setDeveloperMode = async developerMode => dispatch => { window.ipcRenderer.send(SET_DEVELOPER_MODE_CHANNEL, developerMode); window.ipcRenderer.once(SET_DEVELOPER_MODE_CHANNEL, (event, mode) => { if (mode === ERROR_GENERAL) { - toastr.error(ERROR_MESSAGE_HEADER, ERROR_SETTING_DEVELOPER_MODE); + toastr.error( + i18n.t(ERROR_MESSAGE_HEADER), + i18n.t(ERROR_SETTING_DEVELOPER_MODE) + ); } else { dispatch({ type: SET_DEVELOPER_MODE_SUCCEEDED, @@ -227,7 +252,10 @@ const setDeveloperMode = async developerMode => dispatch => { }); } catch (e) { console.error(e); - toastr.error(ERROR_MESSAGE_HEADER, ERROR_SETTING_DEVELOPER_MODE); + toastr.error( + i18n.t(ERROR_MESSAGE_HEADER), + i18n.t(ERROR_SETTING_DEVELOPER_MODE) + ); } }; @@ -239,7 +267,10 @@ const getGeolocationEnabled = async () => dispatch => { GET_GEOLOCATION_ENABLED_CHANNEL, (event, geolocationEnabled) => { if (geolocationEnabled === ERROR_GENERAL) { - toastr.error(ERROR_MESSAGE_HEADER, ERROR_GETTING_GEOLOCATION_ENABLED); + toastr.error( + i18n.t(ERROR_MESSAGE_HEADER), + i18n.t(ERROR_GETTING_GEOLOCATION_ENABLED) + ); } else { dispatch({ type: GET_GEOLOCATION_ENABLED_SUCCEEDED, @@ -251,7 +282,10 @@ const getGeolocationEnabled = async () => dispatch => { ); } catch (e) { console.error(e); - toastr.error(ERROR_MESSAGE_HEADER, ERROR_GETTING_GEOLOCATION_ENABLED); + toastr.error( + i18n.t(ERROR_MESSAGE_HEADER), + i18n.t(ERROR_GETTING_GEOLOCATION_ENABLED) + ); } }; @@ -266,7 +300,10 @@ const setGeolocationEnabled = async geolocationEnabled => dispatch => { SET_GEOLOCATION_ENABLED_CHANNEL, (event, enabled) => { if (enabled === ERROR_GENERAL) { - toastr.error(ERROR_MESSAGE_HEADER, ERROR_SETTING_GEOLOCATION_ENABLED); + toastr.error( + i18n.t(ERROR_MESSAGE_HEADER), + i18n.t(ERROR_SETTING_GEOLOCATION_ENABLED) + ); } else { dispatch({ type: SET_GEOLOCATION_ENABLED_SUCCEEDED, @@ -278,7 +315,10 @@ const setGeolocationEnabled = async geolocationEnabled => dispatch => { ); } catch (e) { console.error(e); - toastr.error(ERROR_MESSAGE_HEADER, ERROR_SETTING_GEOLOCATION_ENABLED); + toastr.error( + i18n.t(ERROR_MESSAGE_HEADER), + i18n.t(ERROR_SETTING_GEOLOCATION_ENABLED) + ); } }; @@ -288,7 +328,10 @@ const getSyncMode = async () => dispatch => { window.ipcRenderer.send(GET_SYNC_MODE_CHANNEL); window.ipcRenderer.once(GET_SYNC_MODE_CHANNEL, (event, mode) => { if (mode === ERROR_GENERAL) { - toastr.error(ERROR_MESSAGE_HEADER, ERROR_GETTING_SYNC_MODE); + toastr.error( + i18n.t(ERROR_MESSAGE_HEADER), + i18n.t(ERROR_GETTING_SYNC_MODE) + ); } else { dispatch({ type: GET_SYNC_MODE_SUCCEEDED, @@ -299,7 +342,7 @@ const getSyncMode = async () => dispatch => { }); } catch (e) { console.error(e); - toastr.error(ERROR_MESSAGE_HEADER, ERROR_GETTING_SYNC_MODE); + toastr.error(i18n.t(ERROR_MESSAGE_HEADER), i18n.t(ERROR_GETTING_SYNC_MODE)); } }; @@ -309,7 +352,10 @@ const setSyncMode = async syncMode => dispatch => { window.ipcRenderer.send(SET_SYNC_MODE_CHANNEL, syncMode); window.ipcRenderer.once(SET_SYNC_MODE_CHANNEL, (event, mode) => { if (mode === ERROR_GENERAL) { - toastr.error(ERROR_MESSAGE_HEADER, ERROR_SETTING_SYNC_MODE); + toastr.error( + i18n.t(ERROR_MESSAGE_HEADER), + i18n.t(ERROR_SETTING_SYNC_MODE) + ); } else { dispatch({ type: SET_SYNC_MODE_SUCCEEDED, @@ -320,7 +366,7 @@ const setSyncMode = async syncMode => dispatch => { }); } catch (e) { console.error(e); - toastr.error(ERROR_MESSAGE_HEADER, ERROR_SETTING_SYNC_MODE); + toastr.error(i18n.t(ERROR_MESSAGE_HEADER), i18n.t(ERROR_SETTING_SYNC_MODE)); } }; @@ -330,7 +376,10 @@ const getUserMode = async () => dispatch => { window.ipcRenderer.send(GET_USER_MODE_CHANNEL); window.ipcRenderer.once(GET_USER_MODE_CHANNEL, (event, userMode) => { if (userMode === ERROR_GENERAL) { - toastr.error(ERROR_MESSAGE_HEADER, ERROR_GETTING_USER_MODE); + toastr.error( + i18n.t(ERROR_MESSAGE_HEADER), + i18n.t(ERROR_GETTING_USER_MODE) + ); } else { dispatch({ type: GET_USER_MODE_SUCCEEDED, @@ -341,7 +390,7 @@ const getUserMode = async () => dispatch => { }); } catch (e) { console.error(e); - toastr.error(ERROR_MESSAGE_HEADER, ERROR_GETTING_USER_MODE); + toastr.error(i18n.t(ERROR_MESSAGE_HEADER), i18n.t(ERROR_GETTING_USER_MODE)); } }; @@ -351,7 +400,10 @@ const setUserMode = async userMode => dispatch => { window.ipcRenderer.send(SET_USER_MODE_CHANNEL, userMode); window.ipcRenderer.once(SET_USER_MODE_CHANNEL, (event, mode) => { if (mode === ERROR_GENERAL) { - toastr.error(ERROR_MESSAGE_HEADER, ERROR_SETTING_USER_MODE); + toastr.error( + i18n.t(ERROR_MESSAGE_HEADER), + i18n.t(ERROR_SETTING_USER_MODE) + ); } else { dispatch({ type: SET_USER_MODE_SUCCEEDED, @@ -362,7 +414,7 @@ const setUserMode = async userMode => dispatch => { }); } catch (e) { console.error(e); - toastr.error(ERROR_MESSAGE_HEADER, ERROR_SETTING_USER_MODE); + toastr.error(i18n.t(ERROR_MESSAGE_HEADER), i18n.t(ERROR_SETTING_USER_MODE)); } }; @@ -374,7 +426,10 @@ const setSpaceAsFavorite = payload => dispatch => { SET_SPACE_AS_FAVORITE_CHANNEL, (event, response) => { if (response === ERROR_GENERAL) { - toastr.error(ERROR_MESSAGE_HEADER, ERROR_SETTING_SPACE_AS_FAVORITE); + toastr.error( + i18n.t(ERROR_MESSAGE_HEADER), + i18n.t(ERROR_SETTING_SPACE_AS_FAVORITE) + ); } else { dispatch({ type: SET_SPACE_AS_FAVORITE_SUCCEEDED, @@ -386,7 +441,10 @@ const setSpaceAsFavorite = payload => dispatch => { ); } catch (e) { console.error(e); - toastr.error(ERROR_MESSAGE_HEADER, ERROR_SETTING_SPACE_AS_FAVORITE); + toastr.error( + i18n.t(ERROR_MESSAGE_HEADER), + i18n.t(ERROR_SETTING_SPACE_AS_FAVORITE) + ); } }; @@ -396,7 +454,10 @@ const setSpaceAsRecent = payload => dispatch => { window.ipcRenderer.send(SET_SPACE_AS_RECENT_CHANNEL, payload); window.ipcRenderer.once(SET_SPACE_AS_RECENT_CHANNEL, (event, response) => { if (response === ERROR_GENERAL) { - toastr.error(ERROR_MESSAGE_HEADER, ERROR_SETTING_SPACE_AS_RECENT); + toastr.error( + i18n.t(ERROR_MESSAGE_HEADER), + i18n.t(ERROR_SETTING_SPACE_AS_RECENT) + ); } else { dispatch({ type: SET_SPACE_AS_RECENT_SPACES_SUCCEEDED, @@ -407,7 +468,10 @@ const setSpaceAsRecent = payload => dispatch => { }); } catch (e) { console.error(e); - toastr.error(ERROR_MESSAGE_HEADER, ERROR_SETTING_SPACE_AS_RECENT); + toastr.error( + i18n.t(ERROR_MESSAGE_HEADER), + i18n.t(ERROR_SETTING_SPACE_AS_RECENT) + ); } }; @@ -420,8 +484,8 @@ const setActionAccessibility = payload => dispatch => { (event, response) => { if (response === ERROR_GENERAL) { toastr.error( - ERROR_MESSAGE_HEADER, - ERROR_SETTING_ACTION_ACCESSIBILITY + i18n.t(ERROR_MESSAGE_HEADER), + i18n.t(ERROR_SETTING_ACTION_ACCESSIBILITY) ); } else { dispatch({ @@ -434,7 +498,10 @@ const setActionAccessibility = payload => dispatch => { ); } catch (e) { console.error(e); - toastr.error(ERROR_MESSAGE_HEADER, ERROR_SETTING_ACTION_ACCESSIBILITY); + toastr.error( + i18n.t(ERROR_MESSAGE_HEADER), + i18n.t(ERROR_SETTING_ACTION_ACCESSIBILITY) + ); } }; @@ -446,7 +513,10 @@ const setActionsAsEnabled = payload => dispatch => { SET_ACTIONS_AS_ENABLED_CHANNEL, (event, response) => { if (response === ERROR_GENERAL) { - toastr.error(ERROR_MESSAGE_HEADER, ERROR_SETTING_ACTIONS_AS_ENABLED); + toastr.error( + i18n.t(ERROR_MESSAGE_HEADER), + i18n.t(ERROR_SETTING_ACTIONS_AS_ENABLED) + ); } else { dispatch({ type: SET_ACTIONS_AS_ENABLED_SUCCEEDED, @@ -458,7 +528,10 @@ const setActionsAsEnabled = payload => dispatch => { ); } catch (e) { console.error(e); - toastr.error(ERROR_MESSAGE_HEADER, ERROR_SETTING_ACTIONS_AS_ENABLED); + toastr.error( + i18n.t(ERROR_MESSAGE_HEADER), + i18n.t(ERROR_SETTING_ACTIONS_AS_ENABLED) + ); } }; diff --git a/src/components/SpacesNearby.js b/src/components/SpacesNearby.js index 38ed7d8c..d3cf1ed7 100644 --- a/src/components/SpacesNearby.js +++ b/src/components/SpacesNearby.js @@ -68,7 +68,7 @@ class SpacesNearby extends Component { searchQuery: prevSearchQuery, }) { const { geolocation, spaces, searchQuery } = this.props; - if (!geolocation.equals(prevGeolocation)) { + if (!geolocation?.equals(prevGeolocation)) { this.getSpacesNearby(); } if (!spaces.equals(prevSpaces) || searchQuery !== prevSearchQuery) { @@ -78,7 +78,7 @@ class SpacesNearby extends Component { getSpacesNearby = () => { const { dispatchGetSpacesNearby, geolocation } = this.props; - if (!geolocation.isEmpty()) { + if (geolocation && !geolocation.isEmpty()) { const { coords: { latitude, longitude }, } = geolocation.toJS(); @@ -130,7 +130,7 @@ class SpacesNearby extends Component { } const mapStateToProps = ({ authentication, Space }) => ({ - geolocation: authentication.getIn(['user', 'settings', 'geolocation']), + geolocation: authentication.getIn(['user', 'geolocation']), spaces: Space.getIn(['nearby', 'content']), activity: Boolean(Space.getIn(['nearby', 'activity']).size), geolocationEnabled: authentication.getIn([ diff --git a/src/components/VisitSpace.js b/src/components/VisitSpace.js index 7bf29c14..6a8c0be4 100644 --- a/src/components/VisitSpace.js +++ b/src/components/VisitSpace.js @@ -27,7 +27,7 @@ import { VISIT_BUTTON_ID, VISIT_INPUT_ID, VISIT_MAIN_ID, - VISIT_SPACE_INPUT, + VISIT_SPACE_INPUT_CLASS, } from '../config/selectors'; class VisitSpace extends Component { @@ -71,14 +71,14 @@ class VisitSpace extends Component { }; handleClick = () => { - const { history } = this.props; + const { history, t } = this.props; const { spaceId } = this.state; const id = extractSpaceId(spaceId) || spaceId; if (!window.navigator.onLine) { - return toastr.error(ERROR_MESSAGE_HEADER, OFFLINE_ERROR_MESSAGE); + return toastr.error(t(ERROR_MESSAGE_HEADER), t(OFFLINE_ERROR_MESSAGE)); } if (!isValidSpaceId(id)) { - return toastr.error(ERROR_MESSAGE_HEADER, INVALID_SPACE_ID_OR_URL); + return toastr.error(t(ERROR_MESSAGE_HEADER), t(INVALID_SPACE_ID_OR_URL)); } if (id && id !== '') { const { replace } = history; @@ -119,7 +119,7 @@ class VisitSpace extends Component { dispatchInitializeTour({ tour: tours.VISIT_SPACE_TOUR, steps: VISIT_SPACE_TOUR_STEPS, }), - 750 + TOUR_DELAY_750 ); } @@ -112,7 +119,7 @@ export class Tour extends Component { tour: tours.SETTINGS_TOUR, steps: SETTINGS_TOUR_STEPS, }), - 750 + TOUR_DELAY_750 ); } } @@ -130,6 +137,7 @@ export class Tour extends Component { dispatchNavigateAndStopTour, dispatchTourCompleted, currentTour, + t, } = this.props; const callback = async data => { const { action, index, type, status } = data; @@ -150,11 +158,14 @@ export class Tour extends Component { case 1: push(VISIT_PATH); dispatchNavigateAndStopTour(newStepIndex); - await waitForElement({ selector: `.${VISIT_SPACE_INPUT}` }); - dispatchStartTour(); + await waitForElement({ + selector: `.${VISIT_SPACE_INPUT_CLASS}`, + }); + document.getElementById(DRAWER_BUTTON_ID).click(); + setTimeout(() => dispatchStartTour(), TOUR_DELAY_500); break; case 2: - replace('/space/owozgj'); + replace(EXAMPLE_VISIT_SPACE_LINK); dispatchNavigateAndStopTour(newStepIndex); await waitForElement({ selector: `.${SPACE_PREVIEW_ICON_CLASS}`, @@ -207,7 +218,7 @@ export class Tour extends Component { }, options: { primaryColor: THEME_COLORS[USER_MODES.TEACHER], - zIndex: 10000, + zIndex: TOUR_Z_INDEX, }, }} floaterProps={{ @@ -215,8 +226,8 @@ export class Tour extends Component { }} callback={callback} locale={{ - last: 'End tour', - skip: 'Close tour', + last: t('End tour'), + skip: t('Close tour'), }} showSkipButton hideBackButton @@ -241,7 +252,7 @@ const mapStateToProps = ({ tour, authentication }) => ({ }); const mapDispatchToProps = { - dispatchNextStep: nextStepTour, + dispatchNextStep: goToNextStep, dispatchStartTour: startTour, dispatchNavigateAndStopTour: navigateStopTour, dispatchTourCompleted: completeTour, diff --git a/src/components/common/WelcomeContent.js b/src/components/common/WelcomeContent.js index 6ecbcad7..6b4f8ffe 100644 --- a/src/components/common/WelcomeContent.js +++ b/src/components/common/WelcomeContent.js @@ -15,7 +15,7 @@ import { VISIT_PATH, LOAD_SPACE_PATH, } from '../../config/paths'; -import { VISIT_SPACE_BUTTON } from '../../config/selectors'; +import { VISIT_SPACE_BUTTON_CLASS } from '../../config/selectors'; const styles = theme => ({ ...Styles(theme), @@ -116,7 +116,10 @@ class WelcomeContent extends Component { // class wrappedButton is necessary to match the layout when encapsulated with Online tag }