diff --git a/packages/concordia-app/package.json b/packages/concordia-app/package.json index 9b842e9..6645ee2 100644 --- a/packages/concordia-app/package.json +++ b/packages/concordia-app/package.json @@ -26,7 +26,7 @@ "dependencies": { "@ezerous/breeze": "~0.4.0", "@ezerous/drizzle": "~0.4.1", - "@ezerous/eth-identity-provider": "^0.1.0", + "@ezerous/eth-identity-provider": "~0.1.2", "@reduxjs/toolkit": "~1.4.0", "@welldone-software/why-did-you-render": "^6.0.0-rc.1", "concordia-contracts": "~0.1.0", diff --git a/packages/concordia-app/public/locales/en/translation.json b/packages/concordia-app/public/locales/en/translation.json index 61d79a1..74f1e18 100644 --- a/packages/concordia-app/public/locales/en/translation.json +++ b/packages/concordia-app/public/locales/en/translation.json @@ -2,6 +2,15 @@ "board.header.no.topics.message": "There are no topics yet!", "board.sub.header.no.topics.guest": "Sign up and be the first to post.", "board.sub.header.no.topics.user": "Be the first to post.", + "clear.databases.modal.cancel.button": "Cancel, keep databases", + "clear.databases.modal.clear.button": "Yes, delete databases", + "clear.databases.modal.clearing.progress.message": "This might take a minute...", + "clear.databases.modal.clearing.progress.title": "Clearing all Concordia databases", + "clear.databases.modal.description.body.user": "Although this action is generally recoverable some of your topics and posts may be permanently lost.", + "clear.databases.modal.description.pre": "You are about to clear the Concordia databases stored locally in your browser.", + "clear.databases.modal.form.username.label.guest": "Please type concordia to confirm.", + "clear.databases.modal.form.username.label.user": "Please type your username to confirm.", + "clear.databases.modal.title": "Clear all Concordia databases. Are you sure?", "custom.loading.tab.pane.default.generic.message": "Magic in the background", "edit.information.modal.form.cancel.button": "Cancel", "edit.information.modal.form.error.invalid.profile.picture.url.message": "The profile picture URL provided is not valid.", @@ -17,6 +26,7 @@ "post.form.subject.field.placeholder": "Subject", "post.list.row.post.id": "#{{id}}", "profile.general.tab.address.row.title": "Account address:", + "profile.general.tab.clear.databases.button.title": "Clear databases", "profile.general.tab.edit.info.button.title": "Edit information", "profile.general.tab.location.row.not.set": "Not set", "profile.general.tab.location.row.title": "Location:", @@ -52,6 +62,7 @@ "register.form.sign.up.step.error.message.header": "Form contains errors", "register.form.sign.up.step.title": "Sign Up", "register.p.account.address": "Account address:", + "topbar.button.clear.databases": "Clear databases", "topbar.button.create.topic": "Create topic", "topbar.button.profile": "Profile", "topbar.button.register": "Sign Up", diff --git a/packages/concordia-app/src/components/ClearDatabasesModal/index.jsx b/packages/concordia-app/src/components/ClearDatabasesModal/index.jsx new file mode 100644 index 0000000..26e47fe --- /dev/null +++ b/packages/concordia-app/src/components/ClearDatabasesModal/index.jsx @@ -0,0 +1,151 @@ +import React, { + useCallback, useMemo, useState, + useEffect, +} from 'react'; +import { + Button, Form, Input, Modal, +} from 'semantic-ui-react'; +import PropTypes from 'prop-types'; +import { useTranslation } from 'react-i18next'; +import { useSelector } from 'react-redux'; +import purgeIndexedDBs from '../../utils/indexedDB/indexedDBUtils'; + +const ClearDatabasesModal = (props) => { + const { + open, onDatabasesCleared, onCancel, + } = props; + const [confirmationInput, setConfirmationInput] = useState(''); + const [userConfirmed, setUserConfirmed] = useState(false); + const [isClearing, setIsClearing] = useState(false); + const user = useSelector((state) => state.user); + const { t } = useTranslation(); + + useEffect(() => { + if (user.hasSignedUp && confirmationInput === user.username) { + setUserConfirmed(true); + } else if (!user.hasSignedUp && confirmationInput === 'concordia') { + setUserConfirmed(true); + } else { + setUserConfirmed(false); + } + }, [confirmationInput, user.hasSignedUp, user.username]); + + const handleSubmit = useCallback(() => { + setIsClearing(true); + + purgeIndexedDBs() + .then(() => { + onDatabasesCleared(); + }).catch((reason) => console.log(reason)); + }, [onDatabasesCleared]); + + const onCancelTry = useCallback(() => { + if (!isClearing) { + setConfirmationInput(''); + onCancel(); + } + }, [isClearing, onCancel]); + + const handleInputChange = (event, { value }) => { setConfirmationInput(value); }; + + const modalContent = useMemo(() => { + if (isClearing) { + return ( + <> +

+ {t('clear.databases.modal.clearing.progress.message')} +

+ + ); + } + + if (user.hasSignedUp) { + return ( + <> +

+ {t('clear.databases.modal.description.pre')} +

+

+ {t('clear.databases.modal.description.body.user')} +

+
+ + + + +
+ + ); + } + + return ( + <> +

+ {t('clear.databases.modal.description.pre')} +

+
+ + + + +
+ + ); + }, [confirmationInput, isClearing, t, user.hasSignedUp]); + + return useMemo(() => ( + + + {isClearing + ? t('clear.databases.modal.clearing.progress.title') + : t('clear.databases.modal.title')} + + + + {modalContent} + + + + {!isClearing && ( + + + + + )} + + ), [handleSubmit, isClearing, modalContent, onCancelTry, open, t, userConfirmed]); +}; + +ClearDatabasesModal.defaultProps = { + open: false, +}; + +ClearDatabasesModal.propTypes = { + open: PropTypes.bool, + onDatabasesCleared: PropTypes.func.isRequired, + onCancel: PropTypes.func.isRequired, +}; + +export default ClearDatabasesModal; diff --git a/packages/concordia-app/src/index.jsx b/packages/concordia-app/src/index.jsx index 4f21227..a29f530 100644 --- a/packages/concordia-app/src/index.jsx +++ b/packages/concordia-app/src/index.jsx @@ -1,3 +1,4 @@ +import './utils/indexedDB/patchIndexedDB'; import './utils/wdyr'; import React, { Suspense } from 'react'; import { render } from 'react-dom'; diff --git a/packages/concordia-app/src/layouts/MainLayout/MainLayoutMenu/index.jsx b/packages/concordia-app/src/layouts/MainLayout/MainLayoutMenu/index.jsx index 11524eb..3c51293 100644 --- a/packages/concordia-app/src/layouts/MainLayout/MainLayoutMenu/index.jsx +++ b/packages/concordia-app/src/layouts/MainLayout/MainLayoutMenu/index.jsx @@ -1,16 +1,32 @@ -import React from 'react'; +import React, { useState } from 'react'; import { Menu } from 'semantic-ui-react'; import { useTranslation } from 'react-i18next'; import { useHistory } from 'react-router'; import { useSelector } from 'react-redux'; import AppContext from '../../../components/AppContext'; import appLogo from '../../../assets/images/app_logo.png'; +import ClearDatabasesModal from '../../../components/ClearDatabasesModal'; const MainLayoutMenu = () => { const hasSignedUp = useSelector((state) => state.user.hasSignedUp); + const [isClearDatabasesOpen, setIsClearDatabasesOpen] = useState(false); const history = useHistory(); const { t } = useTranslation(); + const handleClearDatabasesClick = () => { + setIsClearDatabasesOpen(true); + }; + + const handleDatabasesCleared = () => { + setIsClearDatabasesOpen(false); + history.push('/home'); + window.location.reload(false); + }; + + const handleCancelDatabasesClear = () => { + setIsClearDatabasesOpen(false); + }; + return ( {() => ( @@ -23,6 +39,14 @@ const MainLayoutMenu = () => { > app_logo + + {t('topbar.button.clear.databases')} + {hasSignedUp && history.location.pathname === '/home' && ( { )} + + )} diff --git a/packages/concordia-app/src/options/breezeOptions.js b/packages/concordia-app/src/options/breezeOptions.js index 41984c0..3f3a15f 100644 --- a/packages/concordia-app/src/options/breezeOptions.js +++ b/packages/concordia-app/src/options/breezeOptions.js @@ -10,6 +10,7 @@ const REACT_APP_RENDEZVOUS_PORT = process.env.REACT_APP_RENDEZVOUS_PORT || REACT const breezeOptions = { ipfs: { + repo: 'concordia', config: { Addresses: { Swarm: [ diff --git a/packages/concordia-app/src/utils/indexedDB/indexedDBUtils.js b/packages/concordia-app/src/utils/indexedDB/indexedDBUtils.js new file mode 100644 index 0000000..bc4b3be --- /dev/null +++ b/packages/concordia-app/src/utils/indexedDB/indexedDBUtils.js @@ -0,0 +1,22 @@ +import { breeze } from '../../redux/store'; + +const purgeIndexedDBs = async () => { + const { ipfs, orbit } = breeze; + + if (orbit) await orbit.stop(); + if (ipfs) await ipfs.stop(); + + const databases = await indexedDB.databases(); + return Promise.all( + databases.map((db) => new Promise( + (resolve, reject) => { + const request = indexedDB.deleteDatabase(db.name); + request.onblocked = resolve; + request.onsuccess = resolve; + request.onerror = reject; + }, + )), + ); +}; + +export default purgeIndexedDBs; diff --git a/packages/concordia-app/src/utils/indexedDB/patchIndexedDB.js b/packages/concordia-app/src/utils/indexedDB/patchIndexedDB.js new file mode 100644 index 0000000..185ecda --- /dev/null +++ b/packages/concordia-app/src/utils/indexedDB/patchIndexedDB.js @@ -0,0 +1,46 @@ +/* Patches browsers that do not yet support indexedDB.databases() + (https://developer.mozilla.org/en-US/docs/Web/API/IDBFactory/databases) + See also https://gist.github.com/rmehner/b9a41d9f659c9b1c3340#gistcomment-3449418) */ +if (window.indexedDB && typeof window.indexedDB.databases === 'undefined') { + const LOCALSTORAGE_CACHE_KEY = 'indexedDBDatabases'; + + // Store a key value map of databases + const getFromStorage = () => JSON.parse(window.localStorage[LOCALSTORAGE_CACHE_KEY] || '{}'); + + // Write the database to local storage + const writeToStorage = (value) => { window.localStorage[LOCALSTORAGE_CACHE_KEY] = JSON.stringify(value); }; + + IDBFactory.prototype.databases = () => Promise.resolve( + Object.entries(getFromStorage()).reduce((acc, [name, version]) => { + acc.push({ name, version }); + return acc; + }, []), + ); + + // Intercept the existing open handler to write our DBs names + // and versions to localStorage + const { open } = IDBFactory.prototype; + + // eslint-disable-next-line func-names + IDBFactory.prototype.open = function (...args) { + const dbName = args[0]; + const version = args[1] || 1; + const existing = getFromStorage(); + writeToStorage({ ...existing, [dbName]: version }); + return open.apply(this, args); + }; + + // Intercept the existing deleteDatabase handler remove our + // dbNames from localStorage + const { deleteDatabase } = IDBFactory.prototype; + + // eslint-disable-next-line func-names + IDBFactory.prototype.deleteDatabase = function (...args) { + const dbName = args[0]; + const existing = getFromStorage(); + delete existing[dbName]; + writeToStorage(existing); + return deleteDatabase.apply(this, args); + }; + console.debug('IndexedDB patched successfully!'); +}