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';