diff --git a/public/app/config/channels.js b/public/app/config/channels.js
index 8fc683c0..b76c6463 100644
--- a/public/app/config/channels.js
+++ b/public/app/config/channels.js
@@ -14,9 +14,9 @@ module.exports = {
EXPORTED_SPACE_CHANNEL: 'space:exported',
SHOW_LOAD_SPACE_PROMPT_CHANNEL: 'prompt:space:load:show',
SHOW_EXPORT_SPACE_PROMPT_CHANNEL: 'prompt:space:export:show',
- SHOW_DELETE_SPACE_PROMPT_CHANNEL: 'prompt:space:delete:show',
RESPOND_LOAD_SPACE_PROMPT_CHANNEL: 'prompt:space:load:response',
RESPOND_EXPORT_SPACE_PROMPT_CHANNEL: 'prompt:space:export:respond',
+ SHOW_DELETE_SPACE_PROMPT_CHANNEL: 'prompt:space:delete:show',
RESPOND_DELETE_SPACE_PROMPT_CHANNEL: 'prompt:space:delete:respond',
GET_USER_FOLDER_CHANNEL: 'user:folder:get',
GET_LANGUAGE_CHANNEL: 'user:lang:get',
@@ -31,4 +31,8 @@ module.exports = {
PATCH_APP_INSTANCE_CHANNEL: 'app-instance:patch',
GET_DATABASE_CHANNEL: 'developer:database:get',
SET_DATABASE_CHANNEL: 'developer:database:set',
+ SHOW_SYNC_SPACE_PROMPT_CHANNEL: 'prompt:space:sync:show',
+ RESPOND_SYNC_SPACE_PROMPT_CHANNEL: 'prompt:space:sync:respond',
+ SYNC_SPACE_CHANNEL: 'space:sync',
+ SYNCED_SPACE_CHANNEL: 'space:synced',
};
diff --git a/public/app/config/config.js b/public/app/config/config.js
index 4f31ac96..35266a12 100644
--- a/public/app/config/config.js
+++ b/public/app/config/config.js
@@ -32,14 +32,14 @@ const APPLICATION = 'Application';
const VAR_FOLDER = `${app.getPath('userData')}/var`;
const DATABASE_PATH = `${VAR_FOLDER}/db.json`;
-const TEMPORARY_EXTRACT_FOLDER = 'tmp';
+const TMP_FOLDER = 'tmp';
const DEFAULT_LANG = 'en';
const DEFAULT_DEVELOPER_MODE = false;
module.exports = {
DEFAULT_DEVELOPER_MODE,
DOWNLOADABLE_MIME_TYPES,
- TEMPORARY_EXTRACT_FOLDER,
+ TMP_FOLDER,
RESOURCE,
APPLICATION,
DATABASE_PATH,
diff --git a/public/app/config/messages.js b/public/app/config/messages.js
index fc4287b3..14e9b4d4 100644
--- a/public/app/config/messages.js
+++ b/public/app/config/messages.js
@@ -36,6 +36,8 @@ const ERROR_SETTING_DEVELOPER_MODE =
'There was an error setting the developer mode';
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';
+const ERROR_SYNCING_MESSAGE = 'There was an error syncing the space.';
module.exports = {
ERROR_GETTING_DEVELOPER_MODE,
@@ -65,4 +67,6 @@ module.exports = {
INVALID_SPACE_ID,
ERROR_GETTING_DATABASE,
ERROR_SETTING_DATABASE,
+ SUCCESS_SYNCING_MESSAGE,
+ ERROR_SYNCING_MESSAGE,
};
diff --git a/public/app/download.js b/public/app/download.js
index edf67d1d..54d3cc45 100644
--- a/public/app/download.js
+++ b/public/app/download.js
@@ -1,6 +1,15 @@
const request = require('request-promise');
const cheerio = require('cheerio');
+const download = require('download');
const providers = require('./config/providers');
+const logger = require('./logger');
+const mapping = require('./config/mapping');
+const {
+ getExtension,
+ isDownloadable,
+ generateHash,
+ isFileAvailable,
+} = require('./utilities');
const getDownloadUrl = async ({ url, lang }) => {
let proxied = false;
@@ -22,6 +31,104 @@ const getDownloadUrl = async ({ url, lang }) => {
return false;
};
+const downloadSpaceResources = async ({ lang, space, absoluteSpacePath }) => {
+ // make a working copy of the space to save
+ const spaceToSave = { ...space };
+
+ const { phases, image, id } = spaceToSave;
+ const relativeSpacePath = id;
+
+ // if there is a background/thumbnail image, save it
+ if (image) {
+ const { thumbnailUrl, backgroundUrl } = image;
+ const assets = [
+ { url: thumbnailUrl, key: 'thumbnailAsset' },
+ { url: backgroundUrl, key: 'backgroundAsset' },
+ ];
+
+ // eslint-disable-next-line no-restricted-syntax
+ for (const asset of assets) {
+ let { url } = asset;
+ const { key } = asset;
+ if (url) {
+ // default to https
+ if (url.startsWith('//')) {
+ url = `https:${url}`;
+ }
+ const ext = getExtension({ url });
+ const hash = generateHash({ url });
+ const imageFileName = `${hash}.${ext}`;
+ const relativeImagePath = `${relativeSpacePath}/${imageFileName}`;
+ const absoluteImagePath = `${absoluteSpacePath}/${imageFileName}`;
+ // eslint-disable-next-line no-await-in-loop
+ const imageAvailable = await isFileAvailable(absoluteImagePath);
+ if (!imageAvailable) {
+ logger.debug(`downloading ${url}`);
+ // eslint-disable-next-line no-await-in-loop
+ await download(url, absoluteSpacePath, {
+ filename: imageFileName,
+ });
+ logger.debug(
+ `downloaded ${url} to ${absoluteSpacePath}/${imageFileName}`
+ );
+ }
+ spaceToSave.image[key] = relativeImagePath;
+ }
+ }
+ }
+ // eslint-disable-next-line no-restricted-syntax
+ for (const phase of phases) {
+ const { items = [] } = phase;
+ for (let i = 0; i < items.length; i += 1) {
+ const resource = items[i];
+ if (resource && isDownloadable(resource)) {
+ let { url } = resource;
+
+ // check mappings for files
+ if (mapping[url]) {
+ url = mapping[url];
+ }
+
+ // download from proxy url if available
+ // eslint-disable-next-line no-await-in-loop
+ const downloadUrl = await getDownloadUrl({ url, lang });
+ if (downloadUrl) {
+ url = downloadUrl;
+ }
+
+ // default to https
+ if (url.startsWith('//')) {
+ url = `https:${url}`;
+ }
+
+ // generate hash and get extension to save file
+ const hash = generateHash(resource);
+ const ext = getExtension(resource);
+ const fileName = `${hash}.${ext}`;
+ const relativeFilePath = `${relativeSpacePath}/${fileName}`;
+ const absoluteFilePath = `${absoluteSpacePath}/${fileName}`;
+ phase.items[i].hash = hash;
+
+ // eslint-disable-next-line no-await-in-loop
+ const fileAvailable = await isFileAvailable(absoluteFilePath);
+
+ // if the file is available, point this resource to its path
+ if (!fileAvailable) {
+ logger.debug(`downloading ${url}`);
+ // eslint-disable-next-line no-await-in-loop
+ await download(url, absoluteSpacePath, {
+ filename: fileName,
+ });
+ logger.debug(`downloaded ${url} to ${absoluteSpacePath}/${fileName}`);
+ }
+ phase.items[i].asset = relativeFilePath;
+ }
+ }
+ }
+ return spaceToSave;
+};
+
module.exports = {
getDownloadUrl,
+ downloadSpaceResources,
};
diff --git a/public/app/listeners/index.js b/public/app/listeners/index.js
index 2be4981b..16a9f11d 100644
--- a/public/app/listeners/index.js
+++ b/public/app/listeners/index.js
@@ -1,9 +1,13 @@
const loadSpace = require('./loadSpace');
const saveSpace = require('./saveSpace');
const getSpace = require('./getSpace');
+const showSyncSpacePrompt = require('./showSyncSpacePrompt');
+const syncSpace = require('./syncSpace');
module.exports = {
loadSpace,
saveSpace,
getSpace,
+ showSyncSpacePrompt,
+ syncSpace,
};
diff --git a/public/app/listeners/loadSpace.js b/public/app/listeners/loadSpace.js
index 524a7387..6d99f87f 100644
--- a/public/app/listeners/loadSpace.js
+++ b/public/app/listeners/loadSpace.js
@@ -1,7 +1,7 @@
const extract = require('extract-zip');
const rimraf = require('rimraf');
const fs = require('fs');
-const { VAR_FOLDER, TEMPORARY_EXTRACT_FOLDER } = require('../config/config');
+const { VAR_FOLDER, TMP_FOLDER } = require('../config/config');
const { LOADED_SPACE_CHANNEL } = require('../config/channels');
const {
ERROR_SPACE_ALREADY_AVAILABLE,
@@ -16,7 +16,7 @@ const { SPACES_COLLECTION } = require('../db');
const fsPromises = fs.promises;
const loadSpace = (mainWindow, db) => async (event, { fileLocation }) => {
- const extractPath = `${VAR_FOLDER}/${TEMPORARY_EXTRACT_FOLDER}`;
+ const extractPath = `${VAR_FOLDER}/${TMP_FOLDER}`;
try {
extract(fileLocation, { dir: extractPath }, async extractError => {
if (extractError) {
diff --git a/public/app/listeners/saveSpace.js b/public/app/listeners/saveSpace.js
index ccb350da..7f732070 100644
--- a/public/app/listeners/saveSpace.js
+++ b/public/app/listeners/saveSpace.js
@@ -1,27 +1,18 @@
const isOnline = require('is-online');
-const download = require('download');
-const { VAR_FOLDER, DEFAULT_LANG } = require('../config/config');
-const { getDownloadUrl } = require('../download');
const { SAVE_SPACE_CHANNEL } = require('../config/channels');
const {
ERROR_SPACE_ALREADY_AVAILABLE,
ERROR_DOWNLOADING_FILE,
} = require('../config/errors');
const logger = require('../logger');
-const mapping = require('../config/mapping');
-const {
- isFileAvailable,
- getExtension,
- isDownloadable,
- generateHash,
- createSpaceDirectory,
-} = require('../utilities');
+const { DEFAULT_LANG } = require('../config/config');
+const { createSpaceDirectory } = require('../utilities');
+const { downloadSpaceResources } = require('../download');
const { SPACES_COLLECTION } = require('../db');
const saveSpace = (mainWindow, db) => async (event, { space }) => {
logger.debug('saving space');
- // make a working copy of the space to save
- const spaceToSave = { ...space };
+
try {
// get handle to spaces collection
const spaces = db.get(SPACES_COLLECTION);
@@ -45,109 +36,19 @@ const saveSpace = (mainWindow, db) => async (event, { space }) => {
}
// create directory where resources will be stored
- createSpaceDirectory({ id });
-
- const { phases, image } = spaceToSave;
-
- const spacePath = id;
+ const absoluteSpacePath = createSpaceDirectory({ id });
// use language defined in space otherwise fall back on
// user language, otherwise fall back on the global default
const userLang = db.get('user.lang').value();
const lang = language || userLang || DEFAULT_LANG;
- // todo: follow new format
- // if there is a background/thumbnail image, save it
- if (image) {
- const { thumbnailUrl, backgroundUrl } = image;
- const assets = [
- { url: thumbnailUrl, key: 'thumbnailAsset' },
- { url: backgroundUrl, key: 'backgroundAsset' },
- ];
-
- // eslint-disable-next-line no-restricted-syntax
- for (const asset of assets) {
- let { url } = asset;
- const { key } = asset;
- if (url) {
- // default to https
- if (url.startsWith('//')) {
- url = `https:${url}`;
- }
- const ext = getExtension({ url });
- const hash = generateHash({ url });
- const imageFileName = `${hash}.${ext}`;
- const imagePath = `${spacePath}/${imageFileName}`;
- const absoluteSpacePath = `${VAR_FOLDER}/${spacePath}`;
- const absoluteImagePath = `${VAR_FOLDER}/${imagePath}`;
- // eslint-disable-next-line no-await-in-loop
- const imageAvailable = await isFileAvailable(absoluteImagePath);
- if (!imageAvailable) {
- logger.debug(`downloading ${url}`);
- // eslint-disable-next-line no-await-in-loop
- await download(url, absoluteSpacePath, {
- filename: imageFileName,
- });
- logger.debug(
- `downloaded ${url} to ${absoluteSpacePath}/${imageFileName}`
- );
- }
- spaceToSave.image[key] = imagePath;
- }
- }
- }
- // eslint-disable-next-line no-restricted-syntax
- for (const phase of phases) {
- const { items = [] } = phase;
- for (let i = 0; i < items.length; i += 1) {
- const resource = items[i];
- if (resource && isDownloadable(resource)) {
- let { url } = resource;
-
- // check mappings for files
- if (mapping[url]) {
- url = mapping[url];
- }
-
- // download from proxy url if available
- // eslint-disable-next-line no-await-in-loop
- const downloadUrl = await getDownloadUrl({ url, lang });
- if (downloadUrl) {
- url = downloadUrl;
- }
-
- // default to https
- if (url.startsWith('//')) {
- url = `https:${url}`;
- }
+ const spaceToSave = await downloadSpaceResources({
+ lang,
+ space,
+ absoluteSpacePath,
+ });
- // generate hash and get extension to save file
- const hash = generateHash(resource);
- const ext = getExtension(resource);
- const fileName = `${hash}.${ext}`;
- const filePath = `${spacePath}/${fileName}`;
- const absoluteSpacePath = `${VAR_FOLDER}/${spacePath}`;
- const absoluteFilePath = `${VAR_FOLDER}/${filePath}`;
- phase.items[i].hash = hash;
-
- // eslint-disable-next-line no-await-in-loop
- const fileAvailable = await isFileAvailable(absoluteFilePath);
-
- // if the file is available, point this resource to its path
- if (!fileAvailable) {
- logger.debug(`downloading ${url}`);
- // eslint-disable-next-line no-await-in-loop
- await download(url, absoluteSpacePath, {
- filename: fileName,
- });
- logger.debug(
- `downloaded ${url} to ${absoluteSpacePath}/${fileName}`
- );
- }
- phase.items[i].asset = filePath;
- }
- }
- }
// mark space as saved
spaceToSave.saved = true;
spaces.push(spaceToSave).write();
diff --git a/public/app/listeners/showSyncSpacePrompt.js b/public/app/listeners/showSyncSpacePrompt.js
new file mode 100644
index 00000000..51142834
--- /dev/null
+++ b/public/app/listeners/showSyncSpacePrompt.js
@@ -0,0 +1,21 @@
+const {
+ dialog,
+ // eslint-disable-next-line import/no-extraneous-dependencies
+} = require('electron');
+const { RESPOND_SYNC_SPACE_PROMPT_CHANNEL } = require('../config/channels');
+
+const showSyncSpacePrompt = mainWindow => () => {
+ const options = {
+ type: 'warning',
+ buttons: ['Cancel', 'Sync'],
+ defaultId: 0,
+ cancelId: 0,
+ message:
+ 'Are you sure you want to sync this space? All user input will be deleted.',
+ };
+ dialog.showMessageBox(null, options, respond => {
+ mainWindow.webContents.send(RESPOND_SYNC_SPACE_PROMPT_CHANNEL, respond);
+ });
+};
+
+module.exports = showSyncSpacePrompt;
diff --git a/public/app/listeners/syncSpace.js b/public/app/listeners/syncSpace.js
new file mode 100644
index 00000000..8a81c5df
--- /dev/null
+++ b/public/app/listeners/syncSpace.js
@@ -0,0 +1,64 @@
+const fs = require('fs');
+const rimraf = require('rimraf');
+const logger = require('../logger');
+const { createSpaceDirectory } = require('../utilities');
+const { SPACES_COLLECTION } = require('../db');
+const {
+ SYNCED_SPACE_CHANNEL,
+ SYNC_SPACE_CHANNEL,
+} = require('../config/channels');
+const { DEFAULT_LANG, VAR_FOLDER } = require('../config/config');
+const { ERROR_GENERAL } = require('../config/errors');
+const { downloadSpaceResources } = require('../download');
+
+// use promisified fs
+const fsPromises = fs.promises;
+
+const syncSpace = (mainWindow, db) => async (event, { remoteSpace }) => {
+ logger.debug('syncing space');
+
+ try {
+ const { id } = remoteSpace;
+ // get handle to spaces collection
+ const spaces = db.get(SPACES_COLLECTION);
+ const { language } = remoteSpace;
+ const localSpace = spaces.find({ id }).value();
+
+ // fail if local space is no longer available
+ if (!localSpace) {
+ return mainWindow.webContents.send(SYNC_SPACE_CHANNEL, ERROR_GENERAL);
+ }
+
+ const absoluteSpacePath = `${VAR_FOLDER}/${id}`;
+ const tmpPath = createSpaceDirectory({ id, tmp: true });
+
+ // use language defined in space otherwise fall back on
+ // user language, otherwise fall back on the global default
+ const userLang = db.get('user.lang').value();
+ const lang = language || userLang || DEFAULT_LANG;
+
+ const spaceToSave = await downloadSpaceResources({
+ lang,
+ space: remoteSpace,
+ absoluteSpacePath: tmpPath,
+ });
+
+ // delete previous space contents
+ spaces.remove({ id }).write();
+ rimraf.sync(absoluteSpacePath);
+
+ // rename the temporary path as the final one
+ await fsPromises.rename(tmpPath, absoluteSpacePath);
+
+ // mark space as saved
+ spaceToSave.saved = true;
+ spaces.push(spaceToSave).write();
+
+ return mainWindow.webContents.send(SYNCED_SPACE_CHANNEL, spaceToSave);
+ } catch (err) {
+ logger.error(err);
+ return mainWindow.webContents.send(SYNCED_SPACE_CHANNEL, ERROR_GENERAL);
+ }
+};
+
+module.exports = syncSpace;
diff --git a/public/app/utilities.js b/public/app/utilities.js
index 02cf68e4..213ecd8e 100644
--- a/public/app/utilities.js
+++ b/public/app/utilities.js
@@ -8,6 +8,7 @@ const {
APPLICATION,
RESOURCE,
VAR_FOLDER,
+ TMP_FOLDER,
} = require('./config/config');
const isFileAvailable = filePath =>
@@ -52,11 +53,15 @@ const generateHash = resource => {
};
// create space directory
-const createSpaceDirectory = async ({ id }) => {
+const createSpaceDirectory = ({ id, tmp }) => {
try {
- mkdirp.sync(`${VAR_FOLDER}/${id}`);
+ const rootPath = tmp ? `${VAR_FOLDER}/${TMP_FOLDER}` : VAR_FOLDER;
+ const p = `${rootPath}/${id}`;
+ mkdirp.sync(p);
+ return p;
} catch (err) {
logger.error(err);
+ return false;
}
};
diff --git a/public/electron.js b/public/electron.js
index 3508c83c..4c2498b8 100644
--- a/public/electron.js
+++ b/public/electron.js
@@ -55,10 +55,18 @@ const {
SET_DEVELOPER_MODE_CHANNEL,
GET_DATABASE_CHANNEL,
SET_DATABASE_CHANNEL,
+ SHOW_SYNC_SPACE_PROMPT_CHANNEL,
+ SYNC_SPACE_CHANNEL,
} = require('./app/config/channels');
const { ERROR_GENERAL } = require('./app/config/errors');
const env = require('./env.json');
-const { loadSpace, saveSpace, getSpace } = require('./app/listeners');
+const {
+ loadSpace,
+ saveSpace,
+ showSyncSpacePrompt,
+ syncSpace,
+ getSpace,
+} = require('./app/listeners');
// add keys to process
Object.keys(env).forEach(key => {
@@ -592,6 +600,12 @@ app.on('ready', async () => {
mainWindow.webContents.send(SET_DATABASE_CHANNEL, null);
}
});
+
+ // prompt when syncing a space
+ ipcMain.on(SHOW_SYNC_SPACE_PROMPT_CHANNEL, showSyncSpacePrompt(mainWindow));
+
+ // called when syncing a space
+ ipcMain.on(SYNC_SPACE_CHANNEL, syncSpace(mainWindow, db));
});
app.on('window-all-closed', () => {
diff --git a/src/actions/space.js b/src/actions/space.js
index 037f49eb..6c4e2bdc 100644
--- a/src/actions/space.js
+++ b/src/actions/space.js
@@ -13,6 +13,8 @@ import {
SAVE_SPACE_SUCCEEDED,
FLAG_GETTING_SPACES_NEARBY,
GET_SPACES_NEARBY_SUCCEEDED,
+ FLAG_SYNCING_SPACE,
+ SYNC_SPACE_SUCCEEDED,
} from '../types';
import {
ERROR_ZIP_CORRUPTED,
@@ -31,11 +33,15 @@ import {
GET_SPACES_CHANNEL,
LOAD_SPACE_CHANNEL,
LOADED_SPACE_CHANNEL,
- RESPOND_DELETE_SPACE_PROMPT_CHANNEL,
- RESPOND_EXPORT_SPACE_PROMPT_CHANNEL,
SHOW_DELETE_SPACE_PROMPT_CHANNEL,
+ RESPOND_DELETE_SPACE_PROMPT_CHANNEL,
SHOW_EXPORT_SPACE_PROMPT_CHANNEL,
+ RESPOND_EXPORT_SPACE_PROMPT_CHANNEL,
SAVE_SPACE_CHANNEL,
+ SHOW_SYNC_SPACE_PROMPT_CHANNEL,
+ RESPOND_SYNC_SPACE_PROMPT_CHANNEL,
+ SYNC_SPACE_CHANNEL,
+ SYNCED_SPACE_CHANNEL,
} from '../config/channels';
import {
// ERROR_DOWNLOADING_MESSAGE,
@@ -55,6 +61,8 @@ import {
SUCCESS_MESSAGE_HEADER,
SUCCESS_SAVING_MESSAGE,
SUCCESS_SPACE_LOADED_MESSAGE,
+ SUCCESS_SYNCING_MESSAGE,
+ ERROR_SYNCING_MESSAGE,
} from '../config/messages';
import { createFlag, isErrorResponse } from './common';
import {
@@ -71,6 +79,7 @@ const flagDeletingSpace = createFlag(FLAG_DELETING_SPACE);
const flagExportingSpace = createFlag(FLAG_EXPORTING_SPACE);
const flagSavingSpace = createFlag(FLAG_SAVING_SPACE);
const flagGettingSpacesNearby = createFlag(FLAG_GETTING_SPACES_NEARBY);
+const flagSyncingSpace = createFlag(FLAG_SYNCING_SPACE);
/**
* helper function to wrap a listener to the get space channel around a promise
@@ -273,6 +282,45 @@ const deleteSpace = ({ id }) => dispatch => {
});
};
+const syncSpace = async ({ id }) => async dispatch => {
+ try {
+ const url = generateGetSpaceEndpoint(id);
+ const response = await fetch(url, DEFAULT_GET_REQUEST);
+ // throws if it is an error
+ await isErrorResponse(response);
+ const remoteSpace = await response.json();
+
+ // show confirmation prompt
+ window.ipcRenderer.send(SHOW_SYNC_SPACE_PROMPT_CHANNEL);
+
+ // this runs when the user has responded to the sync dialog
+ window.ipcRenderer.once(RESPOND_SYNC_SPACE_PROMPT_CHANNEL, (event, res) => {
+ // only sync the space if the response is positive
+ if (res === 1) {
+ dispatch(flagSyncingSpace(true));
+ window.ipcRenderer.send(SYNC_SPACE_CHANNEL, { remoteSpace });
+ }
+ });
+
+ // this runs once the space has been synced
+ window.ipcRenderer.once(SYNCED_SPACE_CHANNEL, (event, res) => {
+ if (res === ERROR_GENERAL) {
+ toastr.error(ERROR_MESSAGE_HEADER, ERROR_SYNCING_MESSAGE);
+ } else {
+ toastr.success(SUCCESS_MESSAGE_HEADER, SUCCESS_SYNCING_MESSAGE);
+ dispatch({
+ type: SYNC_SPACE_SUCCEEDED,
+ payload: res,
+ });
+ }
+ dispatch(flagSyncingSpace(false));
+ });
+ } catch (err) {
+ dispatch(flagSyncingSpace(false));
+ toastr.error(ERROR_MESSAGE_HEADER, ERROR_SYNCING_MESSAGE);
+ }
+};
+
const loadSpace = ({ fileLocation }) => dispatch => {
dispatch(flagLoadingSpace(true));
window.ipcRenderer.send(LOAD_SPACE_CHANNEL, { fileLocation });
@@ -347,4 +395,5 @@ export {
getSpace,
saveSpace,
getSpacesNearby,
+ syncSpace,
};
diff --git a/src/components/space/SpaceHeader.js b/src/components/space/SpaceHeader.js
index 29d57381..eedfafda 100644
--- a/src/components/space/SpaceHeader.js
+++ b/src/components/space/SpaceHeader.js
@@ -9,13 +9,20 @@ import DeleteIcon from '@material-ui/icons/Delete';
import SaveIcon from '@material-ui/icons/Save';
import WarningIcon from '@material-ui/icons/Warning';
import WifiIcon from '@material-ui/icons/Wifi';
+import SyncIcon from '@material-ui/icons/Sync';
import Toolbar from '@material-ui/core/Toolbar/Toolbar';
+import { Online } from 'react-detect-offline';
import { withTranslation } from 'react-i18next';
import IconButton from '@material-ui/core/IconButton/IconButton';
import Tooltip from '@material-ui/core/Tooltip';
import { withStyles } from '@material-ui/core';
import Styles from '../../Styles';
-import { deleteSpace, exportSpace, saveSpace } from '../../actions/space';
+import {
+ deleteSpace,
+ exportSpace,
+ saveSpace,
+ syncSpace,
+} from '../../actions/space';
class SpaceHeader extends Component {
static propTypes = {
@@ -30,6 +37,7 @@ class SpaceHeader extends Component {
dispatchExportSpace: PropTypes.func.isRequired,
dispatchDeleteSpace: PropTypes.func.isRequired,
dispatchSaveSpace: PropTypes.func.isRequired,
+ dispatchSyncSpace: PropTypes.func.isRequired,
t: PropTypes.func.isRequired,
};
@@ -52,6 +60,14 @@ class SpaceHeader extends Component {
dispatchSaveSpace({ space });
};
+ handleSync = () => {
+ const {
+ space: { id },
+ dispatchSyncSpace,
+ } = this.props;
+ dispatchSyncSpace({ id });
+ };
+
renderSaveButton() {
const { space, classes, t } = this.props;
const { saved, offlineSupport } = space;
@@ -137,6 +153,30 @@ class SpaceHeader extends Component {
return null;
}
+ renderSyncButton() {
+ const { space, classes, t } = this.props;
+ const { saved } = space;
+ if (saved) {
+ return (
+
+
+
+
+
+
+
+ );
+ }
+ return null;
+ }
+
render() {
const {
openDrawer,
@@ -165,6 +205,7 @@ class SpaceHeader extends Component {
{name}
+ {this.renderSyncButton()}
{this.renderPreviewIcon()}
{this.renderDeleteButton()}
{this.renderExportButton()}
@@ -186,6 +227,7 @@ const mapDispatchToProps = {
dispatchExportSpace: exportSpace,
dispatchDeleteSpace: deleteSpace,
dispatchSaveSpace: saveSpace,
+ dispatchSyncSpace: syncSpace,
};
const ConnectedComponent = connect(
diff --git a/src/config/channels.js b/src/config/channels.js
index 8fc683c0..b76c6463 100644
--- a/src/config/channels.js
+++ b/src/config/channels.js
@@ -14,9 +14,9 @@ module.exports = {
EXPORTED_SPACE_CHANNEL: 'space:exported',
SHOW_LOAD_SPACE_PROMPT_CHANNEL: 'prompt:space:load:show',
SHOW_EXPORT_SPACE_PROMPT_CHANNEL: 'prompt:space:export:show',
- SHOW_DELETE_SPACE_PROMPT_CHANNEL: 'prompt:space:delete:show',
RESPOND_LOAD_SPACE_PROMPT_CHANNEL: 'prompt:space:load:response',
RESPOND_EXPORT_SPACE_PROMPT_CHANNEL: 'prompt:space:export:respond',
+ SHOW_DELETE_SPACE_PROMPT_CHANNEL: 'prompt:space:delete:show',
RESPOND_DELETE_SPACE_PROMPT_CHANNEL: 'prompt:space:delete:respond',
GET_USER_FOLDER_CHANNEL: 'user:folder:get',
GET_LANGUAGE_CHANNEL: 'user:lang:get',
@@ -31,4 +31,8 @@ module.exports = {
PATCH_APP_INSTANCE_CHANNEL: 'app-instance:patch',
GET_DATABASE_CHANNEL: 'developer:database:get',
SET_DATABASE_CHANNEL: 'developer:database:set',
+ SHOW_SYNC_SPACE_PROMPT_CHANNEL: 'prompt:space:sync:show',
+ RESPOND_SYNC_SPACE_PROMPT_CHANNEL: 'prompt:space:sync:respond',
+ SYNC_SPACE_CHANNEL: 'space:sync',
+ SYNCED_SPACE_CHANNEL: 'space:synced',
};
diff --git a/src/config/messages.js b/src/config/messages.js
index fc4287b3..14e9b4d4 100644
--- a/src/config/messages.js
+++ b/src/config/messages.js
@@ -36,6 +36,8 @@ const ERROR_SETTING_DEVELOPER_MODE =
'There was an error setting the developer mode';
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';
+const ERROR_SYNCING_MESSAGE = 'There was an error syncing the space.';
module.exports = {
ERROR_GETTING_DEVELOPER_MODE,
@@ -65,4 +67,6 @@ module.exports = {
INVALID_SPACE_ID,
ERROR_GETTING_DATABASE,
ERROR_SETTING_DATABASE,
+ SUCCESS_SYNCING_MESSAGE,
+ ERROR_SYNCING_MESSAGE,
};
diff --git a/src/reducers/SpaceReducer.js b/src/reducers/SpaceReducer.js
index 4e751485..d9ddb4db 100644
--- a/src/reducers/SpaceReducer.js
+++ b/src/reducers/SpaceReducer.js
@@ -1,6 +1,5 @@
import { Set, Map } from 'immutable';
import {
- GET_SPACE,
TOGGLE_SPACE_MENU,
GET_SPACES,
CLEAR_SPACE,
@@ -15,6 +14,8 @@ import {
FLAG_SAVING_SPACE,
FLAG_GETTING_SPACES_NEARBY,
GET_SPACES_NEARBY_SUCCEEDED,
+ FLAG_SYNCING_SPACE,
+ SYNC_SPACE_SUCCEEDED,
} from '../types';
const INITIAL_STATE = Map({
@@ -42,28 +43,22 @@ export default (state = INITIAL_STATE, { type, payload }) => {
.setIn(['current', 'content'], Map())
.setIn(['current', 'deleted'], false);
case FLAG_SAVING_SPACE:
- return state.setIn(['current', 'activity'], payload);
case FLAG_GETTING_SPACE:
- return state.setIn(['current', 'activity'], payload);
case FLAG_GETTING_SPACES:
- return state.setIn(['current', 'activity'], payload);
case FLAG_LOADING_SPACE:
- return state.setIn(['current', 'activity'], payload);
case FLAG_EXPORTING_SPACE:
- return state.setIn(['current', 'activity'], payload);
case FLAG_DELETING_SPACE:
+ case FLAG_SYNCING_SPACE:
return state.setIn(['current', 'activity'], payload);
case DELETE_SPACE_SUCCESS:
return state.setIn(['current', 'deleted'], payload);
case GET_SPACES:
return state.setIn(['saved'], Set(payload));
- case GET_SPACE:
- return state.setIn(['current', 'content'], Map(payload));
case TOGGLE_SPACE_MENU:
return state.setIn(['current', 'menu', 'open'], payload);
case GET_SPACE_SUCCEEDED:
- return state.setIn(['current', 'content'], Map(payload));
case SAVE_SPACE_SUCCEEDED:
+ case SYNC_SPACE_SUCCEEDED:
return state.setIn(['current', 'content'], Map(payload));
case FLAG_GETTING_SPACES_NEARBY:
return state.setIn(['nearby', 'activity'], payload);
diff --git a/src/types/space.js b/src/types/space.js
index 30394c56..b09cf9f5 100644
--- a/src/types/space.js
+++ b/src/types/space.js
@@ -1,4 +1,3 @@
-export const GET_SPACE = 'GET_SPACE';
export const GET_SPACES = 'GET_SPACES';
export const FLAG_GETTING_SPACE = 'FLAG_GETTING_SPACE';
export const FLAG_LOADING_SPACE = 'FLAG_LOADING_SPACE';
@@ -9,8 +8,9 @@ export const TOGGLE_SPACE_MENU = 'TOGGLE_SPACE_MENU';
export const CLEAR_SPACE = 'CLEAR_SPACE';
export const GET_SPACE_SUCCEEDED = 'GET_SPACE_SUCCEEDED';
export const DELETE_SPACE_SUCCESS = 'DELETE_SPACE_SUCCESS';
-export const SAVE_SPACE = 'SAVE_SPACE';
export const FLAG_SAVING_SPACE = 'FLAG_SAVING_SPACE';
export const SAVE_SPACE_SUCCEEDED = 'SAVE_SPACE_SUCCEEDED';
export const FLAG_GETTING_SPACES_NEARBY = 'FLAG_GETTING_SPACES_NEARBY';
export const GET_SPACES_NEARBY_SUCCEEDED = 'GET_SPACES_NEARBY_SUCCEEDED';
+export const FLAG_SYNCING_SPACE = 'FLAG_SYNCING_SPACE';
+export const SYNC_SPACE_SUCCEEDED = 'SYNC_SPACE_SUCCEEDED';