diff --git a/CHANGELOG.md b/CHANGELOG.md index 91c0b458..638dc02e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,45 @@ 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.11.3](https://github.com/graasp/graasp-desktop/compare/v0.11.2...v0.11.3) (2019-07-22) + +### Bug Fixes + +- use channel specific to each app instance to get resources ([85fa4f4](https://github.com/graasp/graasp-desktop/commit/85fa4f4)), closes [#136](https://github.com/graasp/graasp-desktop/issues/136) + +## [0.11.2](https://github.com/graasp/graasp-desktop/compare/v0.11.1...v0.11.2) (2019-07-21) + +### Bug Fixes + +- only receive messages from intended app ([7b3cfbc](https://github.com/graasp/graasp-desktop/commit/7b3cfbc)), closes [#136](https://github.com/graasp/graasp-desktop/issues/136) + +## [0.11.1](https://github.com/graasp/graasp-desktop/compare/v0.11.0...v0.11.1) (2019-07-18) + +### Bug Fixes + +- only respond to app that sent message ([19f9a3d](https://github.com/graasp/graasp-desktop/commit/19f9a3d)), closes [#136](https://github.com/graasp/graasp-desktop/issues/136) + +# [0.11.0](https://github.com/graasp/graasp-desktop/compare/v0.10.1...v0.11.0) (2019-07-17) + +### Bug Fixes + +- show nearby spaces below header ([4225cce](https://github.com/graasp/graasp-desktop/commit/4225cce)), closes [#138](https://github.com/graasp/graasp-desktop/issues/138) + +### Features + +- activation / deactivation of location sharing ([455e554](https://github.com/graasp/graasp-desktop/commit/455e554)), closes [#107](https://github.com/graasp/graasp-desktop/issues/107) +- make apps resizable ([f8bd12c](https://github.com/graasp/graasp-desktop/commit/f8bd12c)), closes [#139](https://github.com/graasp/graasp-desktop/issues/139) + +## [0.10.1](https://github.com/graasp/graasp-desktop/compare/v0.10.0...v0.10.1) (2019-07-16) + +### Bug Fixes + +- automatically write env.json with contents from env ([128b276](https://github.com/graasp/graasp-desktop/commit/128b276)), closes [#116](https://github.com/graasp/graasp-desktop/issues/116) +- fix order of commands in dockerfile ([e1176fc](https://github.com/graasp/graasp-desktop/commit/e1176fc)), closes [#123](https://github.com/graasp/graasp-desktop/issues/123) +- setup before testing ([93c360c](https://github.com/graasp/graasp-desktop/commit/93c360c)), closes [#122](https://github.com/graasp/graasp-desktop/issues/122) +- show quicktime videos using mp4 ([1c59c04](https://github.com/graasp/graasp-desktop/commit/1c59c04)), closes [#133](https://github.com/graasp/graasp-desktop/issues/133) +- support editing video and images locally ([4f0e960](https://github.com/graasp/graasp-desktop/commit/4f0e960)), closes [#121](https://github.com/graasp/graasp-desktop/issues/121) + # [0.10.0](https://github.com/graasp/graasp-desktop/compare/v0.9.0...v0.10.0) (2019-07-02) ### Bug Fixes diff --git a/package.json b/package.json index a3706618..3dc0f622 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "graasp-desktop", - "version": "0.10.0", + "version": "0.11.3", "description": "Desktop application for the Graasp ecosystem.", "private": true, "author": "React EPFL", @@ -9,7 +9,7 @@ "Hassan Abdul Ghaffar" ], "license": "MIT", - "homepage": "./", + "homepage": "https://graasp.eu/", "main": "public/electron.js", "keywords": [ "Graasp Desktop", @@ -20,6 +20,9 @@ "type": "git", "url": "https://github.com/graasp/graasp-desktop" }, + "bugs": { + "url": "https://github.com/graasp/graasp-desktop/issues" + }, "scripts": { "dev": "yarn react-scripts start", "build": "env-cmd -f ./.env react-scripts build", @@ -52,10 +55,12 @@ "@material-ui/icons": "4.2.0", "@sentry/browser": "5.1.1", "@sentry/electron": "0.17.1", + "about-window": "1.13.1", "archiver": "3.0.0", "bson-objectid": "1.2.5", "cheerio": "1.0.0-rc.3", "classnames": "2.2.6", + "clsx": "1.0.4", "connected-react-router": "6.4.0", "download": "7.1.0", "electron-devtools-installer": "2.2.4", @@ -70,7 +75,7 @@ "i18next": "15.1.0", "immutable": "4.0.0-rc.12", "is-online": "8.2.0", - "lodash": "4.17.11", + "lodash": "4.17.13", "lowdb": "1.0.0", "md5": "2.2.1", "mime-types": "2.1.24", @@ -88,6 +93,7 @@ "react-loading": "2.0.3", "react-redux": "7.0.3", "react-redux-toastr": "7.4.9", + "react-resizable": "1.8.0", "react-router": "5.0.0", "react-router-dom": "5.0.0", "redux": "4.0.1", diff --git a/public/app/assets/icon.png b/public/app/assets/icon.png new file mode 100644 index 00000000..5d928d79 Binary files /dev/null and b/public/app/assets/icon.png differ diff --git a/public/app/config/channels.js b/public/app/config/channels.js index b76c6463..2328f9d6 100644 --- a/public/app/config/channels.js +++ b/public/app/config/channels.js @@ -23,6 +23,8 @@ module.exports = { SET_LANGUAGE_CHANNEL: 'user:lang:set', SET_DEVELOPER_MODE_CHANNEL: 'user:developer-mode:set', GET_DEVELOPER_MODE_CHANNEL: 'user:developer-mode:get', + SET_GEOLOCATION_ENABLED_CHANNEL: 'user:geolocation-enabled:set', + GET_GEOLOCATION_ENABLED_CHANNEL: 'user:geolocation-enabled:get', GET_APP_INSTANCE_RESOURCES_CHANNEL: 'app-instance-resources:get', POST_APP_INSTANCE_RESOURCE_CHANNEL: 'app-instance-resource:post', PATCH_APP_INSTANCE_RESOURCE_CHANNEL: 'app-instance-resource:patch', diff --git a/public/app/config/config.js b/public/app/config/config.js index 35266a12..37dd92ef 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 process = require('process'); // types that we support downloading const DOWNLOADABLE_MIME_TYPES = [ @@ -26,18 +27,27 @@ const DOWNLOADABLE_MIME_TYPES = [ 'application/pdf', ]; +// resolve path for windows '\' +const escapeEscapeCharacter = str => { + return process.platform === 'win32' ? str.replace(/\\/g, '\\\\') : str; +}; + // categories const RESOURCE = 'Resource'; const APPLICATION = 'Application'; -const VAR_FOLDER = `${app.getPath('userData')}/var`; +const VAR_FOLDER = `${escapeEscapeCharacter(app.getPath('userData'))}/var`; const DATABASE_PATH = `${VAR_FOLDER}/db.json`; +const ICON_PATH = 'app/assets/icon.png'; +const PRODUCT_NAME = 'Graasp'; const TMP_FOLDER = 'tmp'; const DEFAULT_LANG = 'en'; const DEFAULT_DEVELOPER_MODE = false; +const DEFAULT_GEOLOCATION_ENABLED = false; module.exports = { DEFAULT_DEVELOPER_MODE, + DEFAULT_GEOLOCATION_ENABLED, DOWNLOADABLE_MIME_TYPES, TMP_FOLDER, RESOURCE, @@ -45,4 +55,7 @@ module.exports = { DATABASE_PATH, VAR_FOLDER, DEFAULT_LANG, + ICON_PATH, + PRODUCT_NAME, + escapeEscapeCharacter, }; diff --git a/public/app/config/messages.js b/public/app/config/messages.js index 14e9b4d4..b1e6a757 100644 --- a/public/app/config/messages.js +++ b/public/app/config/messages.js @@ -34,6 +34,10 @@ const ERROR_GETTING_DEVELOPER_MODE = 'There was an error getting the developer mode'; const ERROR_SETTING_DEVELOPER_MODE = 'There was an error setting the developer mode'; +const ERROR_GETTING_GEOLOCATION_ENABLED = + 'There was an error getting the geolocation enabled'; +const ERROR_SETTING_GEOLOCATION_ENABLED = + 'There was an error setting the geolocation enabled'; const ERROR_GETTING_DATABASE = 'There was an error getting the database.'; const ERROR_SETTING_DATABASE = 'There was an error updating the database.'; const SUCCESS_SYNCING_MESSAGE = 'Space was successfully synced'; @@ -42,6 +46,8 @@ const ERROR_SYNCING_MESSAGE = 'There was an error syncing the space.'; module.exports = { ERROR_GETTING_DEVELOPER_MODE, ERROR_SETTING_DEVELOPER_MODE, + ERROR_GETTING_GEOLOCATION_ENABLED, + ERROR_SETTING_GEOLOCATION_ENABLED, ERROR_GETTING_LANGUAGE, ERROR_SETTING_LANGUAGE, ERROR_DOWNLOADING_MESSAGE, diff --git a/public/app/listeners/exportSpace.js b/public/app/listeners/exportSpace.js new file mode 100644 index 00000000..116a2ae6 --- /dev/null +++ b/public/app/listeners/exportSpace.js @@ -0,0 +1,76 @@ +// eslint-disable-next-line import/no-extraneous-dependencies +const { app } = require('electron'); +const archiver = require('archiver'); +const fs = require('fs'); +const logger = require('../../app/logger'); + +const { VAR_FOLDER } = require('../config/config'); +const { ERROR_GENERAL } = require('../config/errors'); +const { SPACES_COLLECTION } = require('../db'); +const { EXPORTED_SPACE_CHANNEL } = require('../config/channels'); + +// use promisified fs +const fsPromises = fs.promises; + +const exportSpace = (mainWindow, db) => async (event, { archivePath, id }) => { + try { + // get space from local database + const space = db + .get(SPACES_COLLECTION) + .find({ id }) + .value(); + + // abort if space does not exist + if (!space) { + mainWindow.webContents.send(EXPORTED_SPACE_CHANNEL, ERROR_GENERAL); + } else { + // stringify space + const spaceString = JSON.stringify(space); + const spaceDirectory = `${VAR_FOLDER}/${id}`; + const spacePath = `${spaceDirectory}/${id}.json`; + + // create manifest + const manifest = { + id, + version: app.getVersion(), + createdAt: new Date().toISOString(), + }; + const manifestString = JSON.stringify(manifest); + const manifestPath = `${spaceDirectory}/manifest.json`; + + // write space and manifest to json file inside space folder + await fsPromises.writeFile(spacePath, spaceString); + await fsPromises.writeFile(manifestPath, manifestString); + + // prepare output file for zip + const output = fs.createWriteStream(archivePath); + output.on('close', () => { + mainWindow.webContents.send(EXPORTED_SPACE_CHANNEL); + }); + output.on('end', () => { + mainWindow.webContents.send(EXPORTED_SPACE_CHANNEL, ERROR_GENERAL); + }); + + // archive space folder into zip + const archive = archiver('zip', { + zlib: { level: 9 }, + }); + archive.on('warning', err => { + if (err.code === 'ENOENT') { + logger.error(err); + } + }); + archive.on('error', () => { + mainWindow.webContents.send(EXPORTED_SPACE_CHANNEL, ERROR_GENERAL); + }); + archive.pipe(output); + archive.directory(spaceDirectory, false); + archive.finalize(); + } + } catch (err) { + logger.error(err); + mainWindow.webContents.send(EXPORTED_SPACE_CHANNEL, ERROR_GENERAL); + } +}; + +module.exports = exportSpace; diff --git a/public/app/listeners/getGeolocationEnabled.js b/public/app/listeners/getGeolocationEnabled.js new file mode 100644 index 00000000..a6f66382 --- /dev/null +++ b/public/app/listeners/getGeolocationEnabled.js @@ -0,0 +1,20 @@ +const { DEFAULT_GEOLOCATION_ENABLED } = require('../config/config'); +const { GET_GEOLOCATION_ENABLED_CHANNEL } = require('../config/channels'); +const { ERROR_GENERAL } = require('../config/errors'); +const logger = require('../logger'); + +const getGeolocationEnabled = (mainWindow, db) => async () => { + try { + const geolocationEnabled = + db.get('user.geolocationEnabled').value() || DEFAULT_GEOLOCATION_ENABLED; + mainWindow.webContents.send( + GET_GEOLOCATION_ENABLED_CHANNEL, + geolocationEnabled + ); + } catch (e) { + logger.error(e); + mainWindow.webContents.send(GET_GEOLOCATION_ENABLED_CHANNEL, ERROR_GENERAL); + } +}; + +module.exports = getGeolocationEnabled; diff --git a/public/app/listeners/index.js b/public/app/listeners/index.js index 8ad392ff..f9bd7fca 100644 --- a/public/app/listeners/index.js +++ b/public/app/listeners/index.js @@ -5,6 +5,10 @@ const getSpaces = require('./getSpaces'); const deleteSpace = require('./deleteSpace'); const showSyncSpacePrompt = require('./showSyncSpacePrompt'); const syncSpace = require('./syncSpace'); +const exportSpace = require('./exportSpace'); +const showLoadSpacePrompt = require('./showLoadSpacePrompt'); +const showExportSpacePrompt = require('./showExportSpacePrompt'); +const showDeleteSpacePrompt = require('./showDeleteSpacePrompt'); module.exports = { loadSpace, @@ -14,4 +18,8 @@ module.exports = { showSyncSpacePrompt, syncSpace, deleteSpace, + exportSpace, + showLoadSpacePrompt, + showExportSpacePrompt, + showDeleteSpacePrompt, }; diff --git a/public/app/listeners/setGeolocationEnabled.js b/public/app/listeners/setGeolocationEnabled.js new file mode 100644 index 00000000..92411ef1 --- /dev/null +++ b/public/app/listeners/setGeolocationEnabled.js @@ -0,0 +1,21 @@ +const { SET_GEOLOCATION_ENABLED_CHANNEL } = require('../config/channels'); +const { ERROR_GENERAL } = require('../config/errors'); +const logger = require('../logger'); + +const setGeolocationEnabled = (mainWindow, db) => async ( + event, + geolocationEnabled +) => { + try { + db.set('user.geolocationEnabled', geolocationEnabled).write(); + mainWindow.webContents.send( + SET_GEOLOCATION_ENABLED_CHANNEL, + geolocationEnabled + ); + } catch (e) { + logger.error(e); + mainWindow.webContents.send(SET_GEOLOCATION_ENABLED_CHANNEL, ERROR_GENERAL); + } +}; + +module.exports = setGeolocationEnabled; diff --git a/public/app/listeners/showDeleteSpacePrompt.js b/public/app/listeners/showDeleteSpacePrompt.js new file mode 100644 index 00000000..ca69ee6d --- /dev/null +++ b/public/app/listeners/showDeleteSpacePrompt.js @@ -0,0 +1,18 @@ +// eslint-disable-next-line import/no-extraneous-dependencies +const { dialog } = require('electron'); +const { RESPOND_DELETE_SPACE_PROMPT_CHANNEL } = require('../config/channels'); + +const showDeleteSpacePrompt = mainWindow => () => { + const options = { + type: 'warning', + buttons: ['Cancel', 'Delete'], + defaultId: 0, + cancelId: 0, + message: 'Are you sure you want to delete this space?', + }; + dialog.showMessageBox(null, options, respond => { + mainWindow.webContents.send(RESPOND_DELETE_SPACE_PROMPT_CHANNEL, respond); + }); +}; + +module.exports = showDeleteSpacePrompt; diff --git a/public/app/listeners/showExportSpacePrompt.js b/public/app/listeners/showExportSpacePrompt.js new file mode 100644 index 00000000..92741a7c --- /dev/null +++ b/public/app/listeners/showExportSpacePrompt.js @@ -0,0 +1,15 @@ +// eslint-disable-next-line import/no-extraneous-dependencies +const { dialog } = require('electron'); +const { RESPOND_EXPORT_SPACE_PROMPT_CHANNEL } = require('../config/channels'); + +const showExportSpacePrompt = mainWindow => (event, spaceTitle) => { + const options = { + title: 'Save As', + defaultPath: `${spaceTitle}.zip`, + }; + dialog.showSaveDialog(null, options, filePath => { + mainWindow.webContents.send(RESPOND_EXPORT_SPACE_PROMPT_CHANNEL, filePath); + }); +}; + +module.exports = showExportSpacePrompt; diff --git a/public/app/listeners/showLoadSpacePrompt.js b/public/app/listeners/showLoadSpacePrompt.js new file mode 100644 index 00000000..68ac471d --- /dev/null +++ b/public/app/listeners/showLoadSpacePrompt.js @@ -0,0 +1,11 @@ +// eslint-disable-next-line import/no-extraneous-dependencies +const { dialog } = require('electron'); +const { RESPOND_LOAD_SPACE_PROMPT_CHANNEL } = require('../config/channels'); + +const showLoadSpacePrompt = mainWindow => (event, options) => { + dialog.showOpenDialog(null, options, filePaths => { + mainWindow.webContents.send(RESPOND_LOAD_SPACE_PROMPT_CHANNEL, filePaths); + }); +}; + +module.exports = showLoadSpacePrompt; diff --git a/public/electron.js b/public/electron.js index 07766936..1abbbc31 100644 --- a/public/electron.js +++ b/public/electron.js @@ -4,43 +4,36 @@ const { shell, ipcMain, Menu, - dialog, // eslint-disable-next-line import/no-extraneous-dependencies } = require('electron'); const path = require('path'); const isDev = require('electron-is-dev'); -const fs = require('fs'); -const archiver = require('archiver'); const ObjectId = require('bson-objectid'); const { autoUpdater } = require('electron-updater'); const Sentry = require('@sentry/electron'); const ua = require('universal-analytics'); const { machineIdSync } = require('node-machine-id'); +const openAboutWindow = require('about-window').default; const logger = require('./app/logger'); -const { - ensureDatabaseExists, - bootstrapDatabase, - SPACES_COLLECTION, -} = require('./app/db'); +const { ensureDatabaseExists, bootstrapDatabase } = require('./app/db'); const { VAR_FOLDER, DATABASE_PATH, + ICON_PATH, + PRODUCT_NAME, DEFAULT_LANG, DEFAULT_DEVELOPER_MODE, + escapeEscapeCharacter, } = require('./app/config/config'); const { LOAD_SPACE_CHANNEL, EXPORT_SPACE_CHANNEL, - EXPORTED_SPACE_CHANNEL, DELETE_SPACE_CHANNEL, GET_SPACE_CHANNEL, GET_SPACES_CHANNEL, - RESPOND_EXPORT_SPACE_PROMPT_CHANNEL, - RESPOND_DELETE_SPACE_PROMPT_CHANNEL, SHOW_DELETE_SPACE_PROMPT_CHANNEL, SHOW_EXPORT_SPACE_PROMPT_CHANNEL, SHOW_LOAD_SPACE_PROMPT_CHANNEL, - RESPOND_LOAD_SPACE_PROMPT_CHANNEL, SAVE_SPACE_CHANNEL, GET_USER_FOLDER_CHANNEL, GET_LANGUAGE_CHANNEL, @@ -51,6 +44,8 @@ const { GET_APP_INSTANCE_CHANNEL, GET_DEVELOPER_MODE_CHANNEL, SET_DEVELOPER_MODE_CHANNEL, + GET_GEOLOCATION_ENABLED_CHANNEL, + SET_GEOLOCATION_ENABLED_CHANNEL, GET_DATABASE_CHANNEL, SET_DATABASE_CHANNEL, SHOW_SYNC_SPACE_PROMPT_CHANNEL, @@ -66,6 +61,10 @@ const { syncSpace, getSpace, deleteSpace, + exportSpace, + showLoadSpacePrompt, + showExportSpacePrompt, + showDeleteSpacePrompt, } = require('./app/listeners'); // add keys to process @@ -73,9 +72,6 @@ Object.keys(env).forEach(key => { process.env[key] = env[key]; }); -// use promisified fs -const fsPromises = fs.promises; - let mainWindow; // set up sentry @@ -140,29 +136,63 @@ const createWindow = () => { }); }; -// const handleLoad = () => { -// logger.info('load'); -// }; +const macAppMenu = [ + { + label: app.getName(), + submenu: [ + { role: 'about' }, + { type: 'separator' }, + { role: 'services' }, + { type: 'separator' }, + { role: 'hide' }, + { role: 'hideothers' }, + { role: 'unhide' }, + { type: 'separator' }, + { role: 'quit' }, + ], + }, +]; +const standardAppMenu = []; +const macFileSubmenu = [{ role: 'close' }]; +const standardFileSubmenu = [{ + label: 'About', + click: () => { + openAboutWindow({ + // asset for icon is in the public/assets folder + base_path: escapeEscapeCharacter(app.getAppPath()), + icon_path: path.join(__dirname, ICON_PATH), + copyright: 'Copyright © 2019 React', + product_name: PRODUCT_NAME, + use_version_info: false, + adjust_window_size: true, + win_options: { + parent: mainWindow, + resizable: false, + minimizable: false, + maximizable: false, + movable: true, + frame: true, + }, + // automatically show info from package.json + package_json_dir: path.join(__dirname, '../'), + bug_link_text: 'Report a Bug/Issue', + }); + }, +}, + { role: 'quit' }, +]; + +const learnMoreLink = 'https://github.com/react-epfl/graasp-desktop/blob/master/README.md'; +const fileIssueLink = 'https://github.com/react-epfl/graasp-desktop/issues'; const generateMenu = () => { + const isMac = process.platform === 'darwin'; const template = [ + ...(isMac ? macAppMenu : standardAppMenu), { label: 'File', submenu: [ - // { - // label: 'Load Space', - // click() { - // handleLoad(); - // }, - // }, - { - label: 'About', - role: 'about', - }, - { - label: 'Quit', - role: 'quit', - }, + ...(isMac ? macFileSubmenu : standardFileSubmenu), ], }, { type: 'separator' }, @@ -175,9 +205,7 @@ const generateMenu = () => { { role: 'cut' }, { role: 'copy' }, { role: 'paste' }, - { role: 'pasteandmatchstyle' }, - { role: 'delete' }, - { role: 'selectall' }, + { role: 'selectAll' }, ], }, { @@ -188,7 +216,6 @@ const generateMenu = () => { { role: 'toggledevtools' }, { type: 'separator' }, { role: 'resetzoom' }, - { role: 'resetzoom' }, { role: 'zoomin' }, { role: 'zoomout' }, { type: 'separator' }, @@ -197,7 +224,18 @@ const generateMenu = () => { }, { role: 'window', - submenu: [{ role: 'minimize' }, { role: 'close' }], + submenu: [ + { role: 'minimize' }, + { role: 'zoom' }, + ...(isMac + ? [ + { type: 'separator' }, + { role: 'front' }, + { type: 'separator' }, + { role: 'window' }, + ] + : [{ role: 'close' }]), + ], }, { role: 'help', @@ -205,18 +243,14 @@ const generateMenu = () => { { click() { // eslint-disable-next-line - require('electron').shell.openExternal( - 'https://github.com/react-epfl/graasp-desktop/blob/master/README.md' - ); + require('electron').shell.openExternal(learnMoreLink); }, label: 'Learn More', }, { click() { // eslint-disable-next-line - require('electron').shell.openExternal( - 'https://github.com/react-epfl/graasp-desktop/issues' - ); + require('electron').shell.openExternal(fileIssueLink); }, label: 'File Issue on GitHub', }, @@ -224,7 +258,8 @@ const generateMenu = () => { }, ]; - Menu.setApplicationMenu(Menu.buildFromTemplate(template)); + Menu.setApplicationMenu(null); + mainWindow.setMenu(Menu.buildFromTemplate(template)); }; app.on('ready', async () => { @@ -258,101 +293,16 @@ app.on('ready', async () => { ipcMain.on(LOAD_SPACE_CHANNEL, loadSpace(mainWindow, db)); // called when exporting a space - ipcMain.on(EXPORT_SPACE_CHANNEL, async (event, { archivePath, id }) => { - try { - // get space from local database - const space = db - .get(SPACES_COLLECTION) - .find({ id }) - .value(); - - // abort if space does not exist - if (!space) { - mainWindow.webContents.send(EXPORTED_SPACE_CHANNEL, ERROR_GENERAL); - } else { - // stringify space - const spaceString = JSON.stringify(space); - const spaceDirectory = `${VAR_FOLDER}/${id}`; - const spacePath = `${spaceDirectory}/${id}.json`; - - // create manifest - const manifest = { - id, - version: app.getVersion(), - createdAt: new Date().toISOString(), - }; - const manifestString = JSON.stringify(manifest); - const manifestPath = `${spaceDirectory}/manifest.json`; - - // write space and manifest to json file inside space folder - await fsPromises.writeFile(spacePath, spaceString); - await fsPromises.writeFile(manifestPath, manifestString); - - // prepare output file for zip - const output = fs.createWriteStream(archivePath); - output.on('close', () => { - mainWindow.webContents.send(EXPORTED_SPACE_CHANNEL); - }); - output.on('end', () => { - mainWindow.webContents.send(EXPORTED_SPACE_CHANNEL, ERROR_GENERAL); - }); - - // archive space folder into zip - const archive = archiver('zip', { - zlib: { level: 9 }, - }); - archive.on('warning', err => { - if (err.code === 'ENOENT') { - logger.error(err); - } - }); - archive.on('error', () => { - mainWindow.webContents.send(EXPORTED_SPACE_CHANNEL, ERROR_GENERAL); - }); - archive.pipe(output); - archive.directory(spaceDirectory, false); - archive.finalize(); - } - } catch (err) { - logger.error(err); - mainWindow.webContents.send(EXPORTED_SPACE_CHANNEL, ERROR_GENERAL); - } - }); + ipcMain.on(EXPORT_SPACE_CHANNEL, exportSpace(mainWindow, db)); // prompt when loading a space - ipcMain.on(SHOW_LOAD_SPACE_PROMPT_CHANNEL, (event, options) => { - dialog.showOpenDialog(null, options, filePaths => { - mainWindow.webContents.send(RESPOND_LOAD_SPACE_PROMPT_CHANNEL, filePaths); - }); - }); + ipcMain.on(SHOW_LOAD_SPACE_PROMPT_CHANNEL, showLoadSpacePrompt(mainWindow)); // prompt when exporting a space - ipcMain.on(SHOW_EXPORT_SPACE_PROMPT_CHANNEL, (event, spaceTitle) => { - const options = { - title: 'Save As', - defaultPath: `${spaceTitle}.zip`, - }; - dialog.showSaveDialog(null, options, filePath => { - mainWindow.webContents.send( - RESPOND_EXPORT_SPACE_PROMPT_CHANNEL, - filePath - ); - }); - }); + ipcMain.on(SHOW_EXPORT_SPACE_PROMPT_CHANNEL, showExportSpacePrompt(mainWindow)); // prompt when deleting a space - ipcMain.on(SHOW_DELETE_SPACE_PROMPT_CHANNEL, () => { - const options = { - type: 'warning', - buttons: ['Cancel', 'Delete'], - defaultId: 0, - cancelId: 0, - message: 'Are you sure you want to delete this space?', - }; - dialog.showMessageBox(null, options, respond => { - mainWindow.webContents.send(RESPOND_DELETE_SPACE_PROMPT_CHANNEL, respond); - }); - }); + ipcMain.on(SHOW_DELETE_SPACE_PROMPT_CHANNEL, showDeleteSpacePrompt(mainWindow)); // called when getting user folder ipcMain.on(GET_USER_FOLDER_CHANNEL, () => { @@ -409,11 +359,23 @@ app.on('ready', async () => { } }); + // called when getting geolocation enabled + ipcMain.on( + GET_GEOLOCATION_ENABLED_CHANNEL, + getGeolocationEnabled(mainWindow, db) + ); + + // called when setting geolocation enabled + ipcMain.on( + SET_GEOLOCATION_ENABLED_CHANNEL, + setGeolocationEnabled(mainWindow, db) + ); + // called when getting AppInstanceResources ipcMain.on(GET_APP_INSTANCE_RESOURCES_CHANNEL, (event, data = {}) => { const defaultResponse = []; + const { userId, appInstanceId, spaceId, subSpaceId, type } = data; try { - const { userId, appInstanceId, spaceId, subSpaceId, type } = data; const appInstanceResourcesHandle = db .get('spaces') .find({ id: spaceId }) @@ -438,12 +400,24 @@ app.on('ready', async () => { const appInstanceResources = appInstanceResourcesHandle.value(); const response = appInstanceResources || defaultResponse; - mainWindow.webContents.send(GET_APP_INSTANCE_RESOURCES_CHANNEL, response); + + // response is sent back to channel specific for this app instance + mainWindow.webContents.send( + `${GET_APP_INSTANCE_RESOURCES_CHANNEL}_${appInstanceId}`, + { + appInstanceId, + payload: response, + } + ); } catch (e) { console.error(e); + // error is sent back to channel specific for this app instance mainWindow.webContents.send( - GET_APP_INSTANCE_RESOURCES_CHANNEL, - defaultResponse + `${GET_APP_INSTANCE_RESOURCES_CHANNEL}_${appInstanceId}`, + { + appInstanceId, + payload: defaultResponse, + } ); } }); diff --git a/src/App.js b/src/App.js index ab5b27cf..1ea1c9c7 100644 --- a/src/App.js +++ b/src/App.js @@ -26,6 +26,7 @@ import { getUserFolder, getLanguage, getDeveloperMode, + getGeolocationEnabled, } from './actions/user'; import { DEFAULT_LANGUAGE } from './config/constants'; import './App.css'; @@ -36,7 +37,7 @@ const theme = createMuiTheme({ }, palette: { primary: { light: '#5050d2', main: '#5050d2', dark: '#5050d2' }, - secondary: { light: '#00b904', main: '#00b904', dark: '#00b904' }, + secondary: { light: '#eeeeee', main: '#eeeeee', dark: '#eeeeee' }, }, }); @@ -48,10 +49,12 @@ export class App extends Component { dispatchGetUserFolder: PropTypes.func.isRequired, dispatchGetLanguage: PropTypes.func.isRequired, dispatchGetDeveloperMode: PropTypes.func.isRequired, + dispatchGetGeolocationEnabled: PropTypes.func.isRequired, lang: PropTypes.string, i18n: PropTypes.shape({ changeLanguage: PropTypes.func.isRequired, }).isRequired, + geolocationEnabled: PropTypes.bool.isRequired, }; static defaultProps = { @@ -64,25 +67,34 @@ export class App extends Component { dispatchGetUserFolder, dispatchGetLanguage, dispatchGetDeveloperMode, + dispatchGetGeolocationEnabled, } = this.props; dispatchGetLanguage(); dispatchGetDeveloperMode(); dispatchGetUserFolder(); + dispatchGetGeolocationEnabled(); } componentDidMount() { - const { dispatchGetGeolocation } = this.props; - dispatchGetGeolocation(); this.updateWindowDimensions(); window.addEventListener('resize', this.updateWindowDimensions); } - componentDidUpdate({ lang: prevLang }) { - const { lang, i18n } = this.props; + componentDidUpdate({ + lang: prevLang, + geolocationEnabled: prevGeolocationEnabled, + dispatchGetGeolocation, + }) { + const { lang, i18n, geolocationEnabled } = this.props; if (lang !== prevLang) { i18n.changeLanguage(lang); } + + // fetch geolocation only if enabled + if (geolocationEnabled && geolocationEnabled !== prevGeolocationEnabled) { + dispatchGetGeolocation(); + } } componentWillUnmount() { @@ -124,6 +136,7 @@ export class App extends Component { const mapStateToProps = ({ User }) => ({ lang: User.getIn(['current', 'lang']), + geolocationEnabled: User.getIn(['current', 'geolocationEnabled']), }); const mapDispatchToProps = { @@ -131,6 +144,7 @@ const mapDispatchToProps = { dispatchGetUserFolder: getUserFolder, dispatchGetLanguage: getLanguage, dispatchGetDeveloperMode: getDeveloperMode, + dispatchGetGeolocationEnabled: getGeolocationEnabled, }; const ConnectedApp = connect( diff --git a/src/App.test.js b/src/App.test.js index c44fc9ed..29858b0f 100644 --- a/src/App.test.js +++ b/src/App.test.js @@ -13,6 +13,8 @@ describe('', () => { dispatchGetUserFolder: jest.fn(), dispatchGetLanguage: jest.fn(), dispatchGetDeveloperMode: jest.fn(), + dispatchGetGeolocationEnabled: jest.fn(), + geolocationEnabled: false, }; const component = shallow(); it('renders correctly', () => { diff --git a/src/Home.js b/src/Home.js index 21f0df84..18d60e69 100644 --- a/src/Home.js +++ b/src/Home.js @@ -125,7 +125,7 @@ class Home extends Component { const mapStateToProps = ({ Space }) => ({ spaces: Space.get('saved'), - activity: Space.get('current').get('activity'), + activity: Boolean(Space.getIn(['current', 'activity']).size), }); const mapDispatchToProps = { diff --git a/src/actions/appInstance.js b/src/actions/appInstance.js index c2fcf794..6265b763 100644 --- a/src/actions/appInstance.js +++ b/src/actions/appInstance.js @@ -28,6 +28,7 @@ const getAppInstance = async ( if (appInstance) { callback({ + appInstanceId: id, type: GET_APP_INSTANCE_SUCCEEDED, payload: appInstance, }); @@ -42,6 +43,7 @@ const getAppInstance = async ( GET_APP_INSTANCE_CHANNEL, async (event, response) => { callback({ + appInstanceId: id, type: GET_APP_INSTANCE_SUCCEEDED, payload: response, }); diff --git a/src/actions/appInstanceResource.js b/src/actions/appInstanceResource.js index 24472644..17fa5483 100644 --- a/src/actions/appInstanceResource.js +++ b/src/actions/appInstanceResource.js @@ -14,6 +14,7 @@ const getAppInstanceResources = async ( callback ) => { try { + // send a message to the generic channel window.ipcRenderer.send(GET_APP_INSTANCE_RESOURCES_CHANNEL, { userId, appInstanceId, @@ -22,12 +23,16 @@ const getAppInstanceResources = async ( type, }); + // set a listener to a channel specific for this app instance window.ipcRenderer.once( - GET_APP_INSTANCE_RESOURCES_CHANNEL, + `${GET_APP_INSTANCE_RESOURCES_CHANNEL}_${appInstanceId}`, async (event, response) => { + const { payload, appInstanceId: responseAppInstanceId } = response; callback({ + payload, + // have to include the appInstanceId to avoid broadcasting + appInstanceId: responseAppInstanceId, type: GET_APP_INSTANCE_RESOURCES_SUCCEEDED, - payload: response, }); } ); @@ -55,6 +60,8 @@ const postAppInstanceResource = async ( POST_APP_INSTANCE_RESOURCE_CHANNEL, async (event, response) => { callback({ + // have to include the appInstanceId to avoid broadcasting + appInstanceId, type: POST_APP_INSTANCE_RESOURCE_SUCCEEDED, payload: response, }); @@ -82,6 +89,7 @@ const patchAppInstanceResource = async ( PATCH_APP_INSTANCE_RESOURCE_CHANNEL, async (event, response) => { callback({ + appInstanceId, type: PATCH_APP_INSTANCE_RESOURCE_SUCCEEDED, payload: response, }); diff --git a/src/actions/space.js b/src/actions/space.js index 6c4e2bdc..38d86718 100644 --- a/src/actions/space.js +++ b/src/actions/space.js @@ -272,6 +272,9 @@ const deleteSpace = ({ id }) => dispatch => { if (response === ERROR_GENERAL) { toastr.error(ERROR_MESSAGE_HEADER, ERROR_DELETING_MESSAGE); } else { + // update saved spaces in state + dispatch(getSpaces()); + toastr.success(SUCCESS_MESSAGE_HEADER, SUCCESS_DELETING_MESSAGE); dispatch({ type: DELETE_SPACE_SUCCESS, @@ -307,6 +310,9 @@ const syncSpace = async ({ id }) => async dispatch => { if (res === ERROR_GENERAL) { toastr.error(ERROR_MESSAGE_HEADER, ERROR_SYNCING_MESSAGE); } else { + // update saved spaces in state + dispatch(getSpaces()); + toastr.success(SUCCESS_MESSAGE_HEADER, SUCCESS_SYNCING_MESSAGE); dispatch({ type: SYNC_SPACE_SUCCEEDED, diff --git a/src/actions/user.js b/src/actions/user.js index 6673d52a..f4b8f273 100644 --- a/src/actions/user.js +++ b/src/actions/user.js @@ -12,6 +12,10 @@ import { FLAG_SETTING_DEVELOPER_MODE, GET_DEVELOPER_MODE_SUCCEEDED, SET_DEVELOPER_MODE_SUCCEEDED, + FLAG_GETTING_GEOLOCATION_ENABLED, + FLAG_SETTING_GEOLOCATION_ENABLED, + GET_GEOLOCATION_ENABLED_SUCCEEDED, + SET_GEOLOCATION_ENABLED_SUCCEEDED, } from '../types'; import { ERROR_GETTING_GEOLOCATION, @@ -21,6 +25,8 @@ import { ERROR_SETTING_LANGUAGE, ERROR_SETTING_DEVELOPER_MODE, ERROR_GETTING_DEVELOPER_MODE, + ERROR_SETTING_GEOLOCATION_ENABLED, + ERROR_GETTING_GEOLOCATION_ENABLED, } from '../config/messages'; import { GET_USER_FOLDER_CHANNEL, @@ -28,6 +34,8 @@ import { SET_LANGUAGE_CHANNEL, GET_DEVELOPER_MODE_CHANNEL, SET_DEVELOPER_MODE_CHANNEL, + GET_GEOLOCATION_ENABLED_CHANNEL, + SET_GEOLOCATION_ENABLED_CHANNEL, } from '../config/channels'; import { createFlag } from './common'; import { ERROR_GENERAL } from '../config/errors'; @@ -37,6 +45,12 @@ const flagGettingLanguage = createFlag(FLAG_GETTING_LANGUAGE); const flagSettingLanguage = createFlag(FLAG_SETTING_LANGUAGE); const flagGettingDeveloperMode = createFlag(FLAG_GETTING_DEVELOPER_MODE); const flagSettingDeveloperMode = createFlag(FLAG_SETTING_DEVELOPER_MODE); +const flagGettingGeolocationEnabled = createFlag( + FLAG_GETTING_GEOLOCATION_ENABLED +); +const flagSettingGeolocationEnabled = createFlag( + FLAG_SETTING_GEOLOCATION_ENABLED +); const getGeolocation = async () => async dispatch => { // only fetch location if online @@ -175,6 +189,57 @@ const setDeveloperMode = async developerMode => dispatch => { } }; +const getGeolocationEnabled = async () => dispatch => { + try { + dispatch(flagGettingGeolocationEnabled(true)); + window.ipcRenderer.send(GET_GEOLOCATION_ENABLED_CHANNEL); + window.ipcRenderer.once( + GET_GEOLOCATION_ENABLED_CHANNEL, + (event, geolocationEnabled) => { + if (geolocationEnabled === ERROR_GENERAL) { + toastr.error(ERROR_MESSAGE_HEADER, ERROR_GETTING_GEOLOCATION_ENABLED); + } else { + dispatch({ + type: GET_GEOLOCATION_ENABLED_SUCCEEDED, + payload: geolocationEnabled, + }); + } + dispatch(flagGettingGeolocationEnabled(false)); + } + ); + } catch (e) { + console.error(e); + toastr.error(ERROR_MESSAGE_HEADER, ERROR_GETTING_GEOLOCATION_ENABLED); + } +}; + +const setGeolocationEnabled = async geolocationEnabled => dispatch => { + try { + dispatch(flagSettingGeolocationEnabled(true)); + window.ipcRenderer.send( + SET_GEOLOCATION_ENABLED_CHANNEL, + geolocationEnabled + ); + window.ipcRenderer.once( + SET_GEOLOCATION_ENABLED_CHANNEL, + (event, enabled) => { + if (enabled === ERROR_GENERAL) { + toastr.error(ERROR_MESSAGE_HEADER, ERROR_SETTING_GEOLOCATION_ENABLED); + } else { + dispatch({ + type: SET_GEOLOCATION_ENABLED_SUCCEEDED, + payload: enabled, + }); + } + dispatch(flagSettingGeolocationEnabled(false)); + } + ); + } catch (e) { + console.error(e); + toastr.error(ERROR_MESSAGE_HEADER, ERROR_SETTING_GEOLOCATION_ENABLED); + } +}; + export { getUserFolder, getGeolocation, @@ -182,4 +247,6 @@ export { setLanguage, getDeveloperMode, setDeveloperMode, + getGeolocationEnabled, + setGeolocationEnabled, }; diff --git a/src/components/LoadSpace.js b/src/components/LoadSpace.js index d1e068b4..24117779 100644 --- a/src/components/LoadSpace.js +++ b/src/components/LoadSpace.js @@ -189,7 +189,7 @@ const mapDispatchToProps = { }; const mapStateToProps = ({ Space }) => ({ - activity: Space.get('current').get('activity'), + activity: Boolean(Space.getIn(['current', 'activity']).size), }); const ConnectedComponent = connect( diff --git a/src/components/Settings.js b/src/components/Settings.js index 40dc5635..7f196edb 100644 --- a/src/components/Settings.js +++ b/src/components/Settings.js @@ -19,6 +19,7 @@ import Styles from '../Styles'; import MainMenu from './common/MainMenu'; import LanguageSelect from './common/LanguageSelect'; import DeveloperSwitch from './common/DeveloperSwitch'; +import GeolocationControl from './common/GeolocationControl'; class Settings extends Component { state = { @@ -102,6 +103,7 @@ class Settings extends Component { + diff --git a/src/components/SpacesNearby.js b/src/components/SpacesNearby.js index fb57c241..d8a22acb 100644 --- a/src/components/SpacesNearby.js +++ b/src/components/SpacesNearby.js @@ -19,6 +19,8 @@ import MainMenu from './common/MainMenu'; import { getSpacesNearby } from '../actions'; import SpaceGrid from './space/SpaceGrid'; import Loader from './common/Loader'; +import GeolocationControl from './common/GeolocationControl'; +import { CONTROL_TYPES } from '../config/constants'; class SpacesNearby extends Component { state = { @@ -35,6 +37,7 @@ class SpacesNearby extends Component { geolocation: PropTypes.instanceOf(Map), spaces: PropTypes.instanceOf(Set).isRequired, activity: PropTypes.bool, + geolocationEnabled: PropTypes.bool.isRequired, }; static defaultProps = { @@ -76,7 +79,7 @@ class SpacesNearby extends Component { }; render() { - const { classes, theme, spaces, activity } = this.props; + const { classes, theme, spaces, activity, geolocationEnabled } = this.props; const { open } = this.state; if (activity) { @@ -93,6 +96,14 @@ class SpacesNearby extends Component { ); } + const geolocationContent = geolocationEnabled ? ( + + ) : ( +
+ +
+ ); + return (
@@ -140,7 +151,7 @@ class SpacesNearby extends Component { })} >
- + {geolocationContent}
); @@ -150,7 +161,8 @@ class SpacesNearby extends Component { const mapStateToProps = ({ User, Space }) => ({ geolocation: User.getIn(['current', 'geolocation']), spaces: Space.getIn(['nearby', 'content']), - activity: Space.getIn(['nearby', 'activity']), + activity: Boolean(Space.getIn(['nearby', 'activity']).size), + geolocationEnabled: User.getIn(['current', 'geolocationEnabled']), }); const mapDispatchToProps = { @@ -161,6 +173,7 @@ const ConnectedComponent = connect( mapStateToProps, mapDispatchToProps )(SpacesNearby); + const StyledComponent = withStyles(Styles, { withTheme: true })( ConnectedComponent ); diff --git a/src/components/VisitSpace.js b/src/components/VisitSpace.js index 6df94377..2067e74a 100644 --- a/src/components/VisitSpace.js +++ b/src/components/VisitSpace.js @@ -144,7 +144,7 @@ class VisitSpace extends Component {
@@ -182,7 +182,7 @@ class VisitSpace extends Component { } const mapStateToProps = ({ Space }) => ({ - activity: Space.get('current').get('activity'), + activity: Boolean(Space.getIn(['current', 'activity']).size), }); const ConnectedComponent = connect(mapStateToProps)(VisitSpace); diff --git a/src/components/common/GeolocationControl.js b/src/components/common/GeolocationControl.js new file mode 100644 index 00000000..7158e0d4 --- /dev/null +++ b/src/components/common/GeolocationControl.js @@ -0,0 +1,126 @@ +import React, { Component } from 'react'; +import FormControl from '@material-ui/core/FormControl'; +import { withStyles } from '@material-ui/core/styles'; +import { withTranslation } from 'react-i18next'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; +import Switch from '@material-ui/core/Switch'; +import Button from '@material-ui/core/Button'; +import FormControlLabel from '@material-ui/core/FormControlLabel'; +import { getGeolocationEnabled, setGeolocationEnabled } from '../../actions'; +import Loader from './Loader'; +import { CONTROL_TYPES } from '../../config/constants'; + +const styles = theme => ({ + formControl: { + margin: theme.spacing(), + minWidth: 120, + }, +}); + +class GeolocationControl extends Component { + static propTypes = { + geolocationEnabled: PropTypes.bool.isRequired, + activity: PropTypes.bool.isRequired, + t: PropTypes.func.isRequired, + dispatchGetGeolocationEnabled: PropTypes.func.isRequired, + dispatchSetGeolocationEnabled: PropTypes.func.isRequired, + classes: PropTypes.shape({ + formControl: PropTypes.string.isRequired, + }).isRequired, + controlType: PropTypes.oneOf(Object.keys(CONTROL_TYPES)), + }; + + static defaultProps = { + controlType: CONTROL_TYPES.SWITCH, + }; + + constructor(props) { + super(props); + const { dispatchGetGeolocationEnabled } = this.props; + dispatchGetGeolocationEnabled(); + } + + handleClick = async () => { + const { dispatchSetGeolocationEnabled } = this.props; + dispatchSetGeolocationEnabled(true); + }; + + handleChange = async ({ target }) => { + const { dispatchSetGeolocationEnabled } = this.props; + const { checked } = target; + dispatchSetGeolocationEnabled(checked); + }; + + render() { + const { + classes, + t, + geolocationEnabled, + activity, + controlType = CONTROL_TYPES.SWITCH, + } = this.props; + + if (activity) { + return ; + } + + const switchControl = ( + + ); + + return ( + + {(() => { + switch (controlType) { + case CONTROL_TYPES.BUTTON: + return ( + + ); + case CONTROL_TYPES.SWITCH: + default: + return ( + + ); + } + })()} + + ); + } +} + +const mapStateToProps = ({ User }) => ({ + geolocationEnabled: User.getIn(['current', 'geolocationEnabled']), + activity: Boolean(User.getIn(['current', 'activity']).size), +}); + +const mapDispatchToProps = { + dispatchGetGeolocationEnabled: getGeolocationEnabled, + dispatchSetGeolocationEnabled: setGeolocationEnabled, +}; + +const ConnectedComponent = connect( + mapStateToProps, + mapDispatchToProps +)(GeolocationControl); + +const StyledComponent = withStyles(styles)(ConnectedComponent); + +const TranslatedComponent = withTranslation()(StyledComponent); + +export default TranslatedComponent; diff --git a/src/components/common/MediaCard.js b/src/components/common/MediaCard.js index 5d04d540..f9e5acd0 100644 --- a/src/components/common/MediaCard.js +++ b/src/components/common/MediaCard.js @@ -1,47 +1,107 @@ import React from 'react'; import PropTypes from 'prop-types'; +import clsx from 'clsx'; import { withStyles } from '@material-ui/core/styles'; import Card from '@material-ui/core/Card'; +import CardActionArea from '@material-ui/core/CardActionArea'; import CardActions from '@material-ui/core/CardActions'; import CardContent from '@material-ui/core/CardContent'; import CardMedia from '@material-ui/core/CardMedia'; import Typography from '@material-ui/core/Typography'; +import Collapse from '@material-ui/core/Collapse'; +import IconButton from '@material-ui/core/IconButton'; +import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; +import DeleteButton from '../space/DeleteButton'; +import ExportButton from '../space/ExportButton'; +import SyncButton from '../space/SyncButton'; +import { MIN_CARD_WIDTH } from '../../config/constants'; const styles = theme => ({ card: { - maxWidth: 345, - minWidth: 300, + width: '100%', + minWidth: MIN_CARD_WIDTH, + margin: 'auto', + marginBottom: 15, }, + cardDescription: { margin: 0, paddingTop: 0, paddingBottom: 0 }, media: { height: 300, }, leftIcon: { marginRight: theme.spacing(), }, + expand: { + transform: 'rotate(0deg)', + marginLeft: 'auto', + transition: theme.transitions.create('transform', { + duration: theme.transitions.duration.shortest, + }), + }, + expandOpen: { + transform: 'rotate(180deg)', + }, }); const MediaCard = props => { - const { classes, name, image, text, button } = props; + const { classes, image, text, viewLink, space } = props; + const { id, name } = space; + const [expanded, setExpanded] = React.useState(false); + const handleExpandClick = () => { + setExpanded(!expanded); + }; + return ( - - - - {name} - - - - {button} + + + + + + {name} + + + + + + + + + + + + + + + + {text && ( + + + + )} + ); }; MediaCard.propTypes = { classes: PropTypes.shape({ media: PropTypes.string.isRequired }).isRequired, - name: PropTypes.string.isRequired, + space: PropTypes.shape({ + id: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + }).isRequired, image: PropTypes.string.isRequired, text: PropTypes.string, - button: PropTypes.node.isRequired, + viewLink: PropTypes.func.isRequired, }; MediaCard.defaultProps = { diff --git a/src/components/phase/Phase.js b/src/components/phase/Phase.js index a1c0f000..d35f8024 100644 --- a/src/components/phase/Phase.js +++ b/src/components/phase/Phase.js @@ -10,6 +10,7 @@ import PhaseItems from './PhaseItems'; const styles = { containerStyle: { flex: 1, + paddingBottom: '2rem', }, }; diff --git a/src/components/phase/PhaseApp.css b/src/components/phase/PhaseApp.css index b75a263f..eaaa4c61 100644 --- a/src/components/phase/PhaseApp.css +++ b/src/components/phase/PhaseApp.css @@ -3,7 +3,3 @@ height: 100%; border: 0; } - -.AppDiv { - height: 600px; -} diff --git a/src/components/phase/PhaseApp.js b/src/components/phase/PhaseApp.js index 1db0a8bd..bc284bc2 100644 --- a/src/components/phase/PhaseApp.js +++ b/src/components/phase/PhaseApp.js @@ -2,12 +2,14 @@ import React, { Component } from 'react'; import Qs from 'qs'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; +import { ResizableBox } from 'react-resizable'; import './PhaseApp.css'; import { GET_APP_INSTANCE_RESOURCES, PATCH_APP_INSTANCE_RESOURCE, GET_APP_INSTANCE, POST_APP_INSTANCE_RESOURCE, + APP_INSTANCE_RESOURCE_TYPES, } from '../../types'; import { getAppInstanceResources, @@ -54,32 +56,52 @@ class PhaseApp extends Component { } postMessage = data => { - const message = JSON.stringify(data); - - if (this.iframe.contentWindow.postMessage) { - this.iframe.contentWindow.postMessage(message, '*'); - } else { - console.error('unable to find postMessage'); + // get component app instance id + const { appInstance } = this.props; + const { id: componentAppInstanceId } = appInstance || {}; + // get app instance id in message + const { appInstanceId: messageAppInstanceId } = data; + + // only post message to intended app instance + if (componentAppInstanceId === messageAppInstanceId) { + const message = JSON.stringify(data); + + if (this.iframe.contentWindow.postMessage) { + this.iframe.contentWindow.postMessage(message, '*'); + } else { + console.error('unable to find postMessage'); + } } }; handleReceiveMessage = event => { try { - const { dispatchGetAppInstance } = this.props; + const { dispatchGetAppInstance, appInstance } = this.props; + + // get app instance id in message + const { id: componentAppInstanceId } = appInstance || {}; const { type, payload } = JSON.parse(event.data); + let { id: messageAppInstanceId } = payload; + if (APP_INSTANCE_RESOURCE_TYPES.includes(type)) { + ({ appInstanceId: messageAppInstanceId } = payload); + } - switch (type) { - case GET_APP_INSTANCE_RESOURCES: - return getAppInstanceResources(payload, this.postMessage); - case POST_APP_INSTANCE_RESOURCE: - return postAppInstanceResource(payload, this.postMessage); - case PATCH_APP_INSTANCE_RESOURCE: - return patchAppInstanceResource(payload, this.postMessage); - case GET_APP_INSTANCE: - return dispatchGetAppInstance(payload, this.postMessage); - default: - return false; + // only receive message from intended app instance + if (componentAppInstanceId === messageAppInstanceId) { + switch (type) { + case GET_APP_INSTANCE_RESOURCES: + return getAppInstanceResources(payload, this.postMessage); + case POST_APP_INSTANCE_RESOURCE: + return postAppInstanceResource(payload, this.postMessage); + case PATCH_APP_INSTANCE_RESOURCE: + return patchAppInstanceResource(payload, this.postMessage); + case GET_APP_INSTANCE: + return dispatchGetAppInstance(payload, this.postMessage); + default: + return false; + } } + return false; } catch (e) { console.error(e); return false; @@ -139,7 +161,12 @@ class PhaseApp extends Component { const queryString = Qs.stringify(params); return ( -
+