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 (
-
+
-
+
);
}
}
diff --git a/src/components/phase/PhaseVideo.js b/src/components/phase/PhaseVideo.js
index 89c6752b..1965ce30 100644
--- a/src/components/phase/PhaseVideo.js
+++ b/src/components/phase/PhaseVideo.js
@@ -15,6 +15,12 @@ const PhaseVideo = ({ url, asset, name, mimeType, folder }) => {
}
}
+ // assign quicktime videos to mp4
+ if (mimeType === 'video/quicktime') {
+ // eslint-disable-next-line no-param-reassign
+ mimeType = 'video/mp4';
+ }
+
return (