From b5e654f83309e02730c715ed0369c58e23493349 Mon Sep 17 00:00:00 2001 From: Jeremie Pardou-Piquemal <571533+jrmi@users.noreply.github.com> Date: Sat, 15 Aug 2020 16:22:12 +0200 Subject: [PATCH] username and settings are now persistant --- client/package.json | 1 - client/src/actions/app.js | 4 + client/src/components/Home/Home.jsx | 8 +- .../Home/WithNewMessageNotification.jsx | 1 - client/src/components/Home/index.jsx | 3 + .../__snapshots__/Settings.test.jsx.snap | 30 +++++++ client/src/components/Settings/index.jsx | 19 ++++- client/src/i18n/en.json | 1 + client/src/i18n/fr.json | 1 + client/src/main.tsx | 4 +- client/src/reducers/app.js | 16 ++-- client/src/reducers/app.test.js | 57 ++++++++----- client/src/reducers/user.js | 4 +- client/src/reducers/user.test.js | 3 + client/src/root.jsx | 37 --------- client/src/utils/persistence.js | 83 +++++++++++++++++++ client/yarn.lock | 5 -- 17 files changed, 201 insertions(+), 76 deletions(-) delete mode 100644 client/src/root.jsx create mode 100644 client/src/utils/persistence.js diff --git a/client/package.json b/client/package.json index c289df5..26f2c7d 100644 --- a/client/package.json +++ b/client/package.json @@ -17,7 +17,6 @@ "classnames": "^2.3.2", "clipboard": "^2.0.11", "jquery": "3", - "js-cookie": "^3.0.1", "moment": "^2.29.4", "nanoid": "^4.0.0", "randomcolor": "^0.6.2", diff --git a/client/src/actions/app.js b/client/src/actions/app.js index a8a5e1d..3aac089 100644 --- a/client/src/actions/app.js +++ b/client/src/actions/app.js @@ -15,6 +15,10 @@ export const toggleSoundEnabled = payload => async dispatch => { dispatch({ type: 'TOGGLE_SOUND_ENABLED', payload }); }; +export const togglePersistenceEnabled = payload => async dispatch => { + dispatch({ type: 'TOGGLE_PERSISTENCE_ENABLED', payload }); +}; + export const toggleNotificationEnabled = payload => async dispatch => { dispatch({ type: 'TOGGLE_NOTIFICATION_ENABLED', payload }); }; diff --git a/client/src/components/Home/Home.jsx b/client/src/components/Home/Home.jsx index 32505ff..9a84fb3 100644 --- a/client/src/components/Home/Home.jsx +++ b/client/src/components/Home/Home.jsx @@ -100,7 +100,9 @@ class Home extends Component { { - const username = nanoid(); + const username = this.props.username || nanoid(); const encryptDecryptKeys = await crypto.createEncryptDecryptKeys(); const exportedEncryptDecryptPrivateKey = await crypto.exportKey(encryptDecryptKeys.privateKey); @@ -238,7 +240,7 @@ Home.propTypes = { username: PropTypes.string.isRequired, publicKey: PropTypes.object.isRequired, members: PropTypes.array.isRequired, - socketId: PropTypes.object.isRequired, + socketId: PropTypes.string.isRequired, roomId: PropTypes.string.isRequired, roomLocked: PropTypes.bool.isRequired, modalComponent: PropTypes.string, @@ -249,7 +251,9 @@ Home.propTypes = { toggleWindowFocus: PropTypes.func.isRequired, faviconCount: PropTypes.number.isRequired, soundIsEnabled: PropTypes.bool.isRequired, + persistenceIsEnabled: PropTypes.bool.isRequired, toggleSoundEnabled: PropTypes.func.isRequired, + togglePersistenceEnabled: PropTypes.func.isRequired, notificationIsEnabled: PropTypes.bool.isRequired, notificationIsAllowed: PropTypes.bool, toggleNotificationEnabled: PropTypes.func.isRequired, diff --git a/client/src/components/Home/WithNewMessageNotification.jsx b/client/src/components/Home/WithNewMessageNotification.jsx index 316ac2f..9383a49 100644 --- a/client/src/components/Home/WithNewMessageNotification.jsx +++ b/client/src/components/Home/WithNewMessageNotification.jsx @@ -95,7 +95,6 @@ const WithNewMessageNotification = WrappedComponent => { switch (Notification.permission) { case 'granted': this.props.toggleNotificationAllowed(true); - this.props.toggleNotificationEnabled(true); break; case 'denied': this.props.toggleNotificationAllowed(false); diff --git a/client/src/components/Home/index.jsx b/client/src/components/Home/index.jsx index 18ec84b..01d84ea 100644 --- a/client/src/components/Home/index.jsx +++ b/client/src/components/Home/index.jsx @@ -8,6 +8,7 @@ import { closeModal, toggleWindowFocus, toggleSoundEnabled, + togglePersistenceEnabled, toggleNotificationEnabled, toggleNotificationAllowed, toggleSocketConnected, @@ -36,6 +37,7 @@ const mapStateToProps = state => { iAmOwner: Boolean(me && me.isOwner), faviconCount: state.app.unreadMessageCount, soundIsEnabled: state.app.soundIsEnabled, + persistenceIsEnabled: state.app.persistenceIsEnabled, notificationIsEnabled: state.app.notificationIsEnabled, notificationIsAllowed: state.app.notificationIsAllowed, socketConnected: state.app.socketConnected, @@ -51,6 +53,7 @@ const mapDispatchToProps = { closeModal, toggleWindowFocus, toggleSoundEnabled, + togglePersistenceEnabled, toggleNotificationEnabled, toggleNotificationAllowed, toggleSocketConnected, diff --git a/client/src/components/Settings/__snapshots__/Settings.test.jsx.snap b/client/src/components/Settings/__snapshots__/Settings.test.jsx.snap index fd4c33b..9fa8bf1 100644 --- a/client/src/components/Settings/__snapshots__/Settings.test.jsx.snap +++ b/client/src/components/Settings/__snapshots__/Settings.test.jsx.snap @@ -42,6 +42,21 @@ exports[`Settings component > should display 1`] = ` Desktop Notification +
+ +
@@ -233,6 +248,21 @@ exports[`Settings component > should display 2`] = ` Desktop notifications have been denied +
+ +
diff --git a/client/src/components/Settings/index.jsx b/client/src/components/Settings/index.jsx index cb07bbd..37891e0 100644 --- a/client/src/components/Settings/index.jsx +++ b/client/src/components/Settings/index.jsx @@ -1,6 +1,5 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; -import Cookie from 'js-cookie'; import RoomLink from '@/components/RoomLink'; import T from '@/components/T'; @@ -12,6 +11,10 @@ class Settings extends Component { this.props.toggleSoundEnabled(!this.props.soundIsEnabled); } + handlePersistenceToggle() { + this.props.togglePersistenceEnabled(!this.props.persistenceIsEnabled); + } + handleNotificationToggle() { Notification.requestPermission().then(permission => { if (permission === 'granted') { @@ -26,7 +29,6 @@ class Settings extends Component { handleLanguageChange(evt) { const language = evt.target.value; - Cookie.set('language', language); this.props.setLanguage(language); } @@ -68,6 +70,18 @@ class Settings extends Component { {this.props.notificationIsAllowed === false && } +
+ +
@@ -170,6 +184,7 @@ class Settings extends Component { Settings.propTypes = { soundIsEnabled: PropTypes.bool.isRequired, + persistenceIsEnabled: PropTypes.bool.isRequired, toggleSoundEnabled: PropTypes.func.isRequired, notificationIsEnabled: PropTypes.bool.isRequired, notificationIsAllowed: PropTypes.bool, diff --git a/client/src/i18n/en.json b/client/src/i18n/en.json index b7d66e8..7f644d3 100644 --- a/client/src/i18n/en.json +++ b/client/src/i18n/en.json @@ -32,6 +32,7 @@ "lists all commands" ], "sound": "Sound", + "persistence": "Persist configuration", "newMessageNotification": "New message notification", "desktopNotification": "Desktop Notification", "desktopNotificationBlocked": "Desktop notifications have been denied", diff --git a/client/src/i18n/fr.json b/client/src/i18n/fr.json index 4f7a282..abd1152 100644 --- a/client/src/i18n/fr.json +++ b/client/src/i18n/fr.json @@ -32,6 +32,7 @@ "lister toutes les commandes" ], "sound": "Son", + "persistence": "Mémoriser la configuration", "newMessageNotification": "Notification lors d'un nouveau message", "desktopNotification": "Notification Système", "desktopNotificationBlocked": "Les notifications systèmes ont été refusée", diff --git a/client/src/main.tsx b/client/src/main.tsx index 65da1e5..04b3806 100644 --- a/client/src/main.tsx +++ b/client/src/main.tsx @@ -12,8 +12,10 @@ import 'bootstrap/dist/js/bootstrap.bundle.min.js'; import configureStore from '@/store/'; import Home from '@/components/Home/'; import { hasTouchSupport } from '@/utils/dom'; +import { loadPersistedState, persistState } from '@/utils/persistence'; -const store = configureStore(); +const store = configureStore(loadPersistedState()); +store.subscribe(() => persistState(store)); const router = createBrowserRouter([ { diff --git a/client/src/reducers/app.js b/client/src/reducers/app.js index 405c9de..fd9f098 100644 --- a/client/src/reducers/app.js +++ b/client/src/reducers/app.js @@ -1,8 +1,6 @@ -import Cookie from 'js-cookie'; - import { getTranslations } from '@/i18n'; -const language = Cookie.get('language') || navigator.language || 'en'; +const language = navigator.language || 'en'; const initialState = { modalComponent: null, @@ -10,14 +8,17 @@ const initialState = { windowIsFocused: true, unreadMessageCount: 0, soundIsEnabled: true, - notificationIsEnabled: false, + persistenceIsEnabled: false, + notificationIsEnabled: true, notificationIsAllowed: null, socketConnected: false, language, translations: getTranslations(language), }; -const app = (state = initialState, action) => { +const app = (receivedState, action) => { + const state = { ...initialState, ...receivedState }; + switch (action.type) { case 'OPEN_MODAL': return { @@ -50,6 +51,11 @@ const app = (state = initialState, action) => { ...state, soundIsEnabled: action.payload, }; + case 'TOGGLE_PERSISTENCE_ENABLED': + return { + ...state, + persistenceIsEnabled: action.payload, + }; case 'TOGGLE_NOTIFICATION_ENABLED': return { ...state, diff --git a/client/src/reducers/app.test.js b/client/src/reducers/app.test.js index 659ff4f..5a111f4 100644 --- a/client/src/reducers/app.test.js +++ b/client/src/reducers/app.test.js @@ -16,9 +16,10 @@ describe('App reducer', () => { modalComponent: null, scrolledToBottom: true, socketConnected: false, - notificationIsEnabled: false, + notificationIsEnabled: true, notificationIsAllowed: null, soundIsEnabled: true, + persistenceIsEnabled: false, translations: { path: 'test' }, unreadMessageCount: 0, windowIsFocused: true, @@ -26,51 +27,65 @@ describe('App reducer', () => { }); it('should handle OPEN_MODAL', () => { - expect(reducer({}, { type: 'OPEN_MODAL', payload: 'test' })).toEqual({ modalComponent: 'test' }); + expect(reducer({}, { type: 'OPEN_MODAL', payload: 'test' })).toEqual( + expect.objectContaining({ modalComponent: 'test' }), + ); }); it('should handle CLOSE_MODAL', () => { - expect(reducer({}, { type: 'CLOSE_MODAL' })).toEqual({ modalComponent: null }); + expect(reducer({}, { type: 'CLOSE_MODAL' })).toEqual(expect.objectContaining({ modalComponent: null })); }); it('should handle SET_SCROLLED_TO_BOTTOM', () => { - expect(reducer({}, { type: 'SET_SCROLLED_TO_BOTTOM', payload: true })).toEqual({ scrolledToBottom: true }); - expect(reducer({}, { type: 'SET_SCROLLED_TO_BOTTOM', payload: false })).toEqual({ scrolledToBottom: false }); + expect(reducer({}, { type: 'SET_SCROLLED_TO_BOTTOM', payload: true })).toEqual( + expect.objectContaining({ scrolledToBottom: true }), + ); + expect(reducer({}, { type: 'SET_SCROLLED_TO_BOTTOM', payload: false })).toEqual( + expect.objectContaining({ scrolledToBottom: false }), + ); }); it('should handle TOGGLE_WINDOW_FOCUS', () => { - expect(reducer({ unreadMessageCount: 10 }, { type: 'TOGGLE_WINDOW_FOCUS', payload: true })).toEqual({ - windowIsFocused: true, - unreadMessageCount: 0, - }); + expect(reducer({ unreadMessageCount: 10 }, { type: 'TOGGLE_WINDOW_FOCUS', payload: true })).toEqual( + expect.objectContaining({ + windowIsFocused: true, + unreadMessageCount: 0, + }), + ); }); it('should handle RECEIVE_ENCRYPTED_MESSAGE_TEXT_MESSAGE', () => { expect( reducer({ unreadMessageCount: 10, windowIsFocused: false }, { type: 'RECEIVE_ENCRYPTED_MESSAGE_TEXT_MESSAGE' }), - ).toEqual({ unreadMessageCount: 11, windowIsFocused: false }); + ).toEqual(expect.objectContaining({ unreadMessageCount: 11, windowIsFocused: false })); expect( reducer({ unreadMessageCount: 10, windowIsFocused: true }, { type: 'RECEIVE_ENCRYPTED_MESSAGE_TEXT_MESSAGE' }), - ).toEqual({ unreadMessageCount: 0, windowIsFocused: true }); + ).toEqual(expect.objectContaining({ unreadMessageCount: 0, windowIsFocused: true })); }); it('should handle TOGGLE_SOUND_ENABLED', () => { - expect(reducer({}, { type: 'TOGGLE_SOUND_ENABLED', payload: true })).toEqual({ - soundIsEnabled: true, - }); + expect(reducer({}, { type: 'TOGGLE_SOUND_ENABLED', payload: true })).toEqual( + expect.objectContaining({ + soundIsEnabled: true, + }), + ); }); it('should handle TOGGLE_SOCKET_CONNECTED', () => { - expect(reducer({}, { type: 'TOGGLE_SOCKET_CONNECTED', payload: true })).toEqual({ - socketConnected: true, - }); + expect(reducer({}, { type: 'TOGGLE_SOCKET_CONNECTED', payload: true })).toEqual( + expect.objectContaining({ + socketConnected: true, + }), + ); }); it('should handle CHANGE_LANGUAGE', () => { getTranslations.mockReturnValueOnce({ path: 'new lang' }); - expect(reducer({}, { type: 'CHANGE_LANGUAGE', payload: 'fr' })).toEqual({ - language: 'fr', - translations: { path: 'new lang' }, - }); + expect(reducer({}, { type: 'CHANGE_LANGUAGE', payload: 'fr' })).toEqual( + expect.objectContaining({ + language: 'fr', + translations: { path: 'new lang' }, + }), + ); }); }); diff --git a/client/src/reducers/user.js b/client/src/reducers/user.js index 4296a45..0c20766 100644 --- a/client/src/reducers/user.js +++ b/client/src/reducers/user.js @@ -5,7 +5,9 @@ const initialState = { id: '', }; -const user = (state = initialState, action) => { +const user = (receivedState, action) => { + const state = { ...initialState, ...receivedState }; + switch (action.type) { case 'CREATE_USER': return { diff --git a/client/src/reducers/user.test.js b/client/src/reducers/user.test.js index a9d9655..5cb07a8 100644 --- a/client/src/reducers/user.test.js +++ b/client/src/reducers/user.test.js @@ -25,6 +25,9 @@ describe('User reducer', () => { it('should handle SEND_ENCRYPTED_MESSAGE_CHANGE_USERNAME', () => { const payload = { newUsername: 'alice' }; expect(reducer({ username: 'polux' }, { type: 'SEND_ENCRYPTED_MESSAGE_CHANGE_USERNAME', payload })).toEqual({ + id: '', + privateKey: {}, + publicKey: {}, username: 'alice', }); }); diff --git a/client/src/root.jsx b/client/src/root.jsx deleted file mode 100644 index b277f23..0000000 --- a/client/src/root.jsx +++ /dev/null @@ -1,37 +0,0 @@ -import 'bootstrap/dist/css/bootstrap.min.css'; -import 'react-simple-dropdown/styles/Dropdown.css'; -import 'stylesheets/app.sass'; -import 'bootstrap/dist/js/bootstrap.bundle.min.js'; -import React, { Component } from 'react'; -import { Redirect } from 'react-router'; -import { Provider } from 'react-redux'; -import configureStore from './store'; -import { BrowserRouter, Route, Switch } from 'react-router-dom'; -import shortId from 'shortid'; -import Home from 'components/Home'; -import { hasTouchSupport } from './utils/dom'; - -const store = configureStore(); - -export default class Root extends Component { - componentWillMount() { - if (hasTouchSupport) { - document.body.classList.add('touch'); - } - } - - render() { - return ( - - -
- - } /> - - -
-
-
- ); - } -} diff --git a/client/src/utils/persistence.js b/client/src/utils/persistence.js new file mode 100644 index 0000000..71eacc1 --- /dev/null +++ b/client/src/utils/persistence.js @@ -0,0 +1,83 @@ +/** + * Handle localStorage persistence + */ + +import { debounce } from 'lodash'; +import { getTranslations } from '@/i18n'; + +const getSettings = () => { + try { + return JSON.parse(localStorage.getItem('settings')) || {}; + } catch (e) { + return {}; + } +}; + +const setSettings = settings => { + localStorage.setItem('settings', JSON.stringify(settings)); +}; + +export const loadPersistedState = () => { + const state = {}; + + const stored = getSettings(); + + if (stored.persistenceIsEnabled !== true) { + return; + } + + state.app = {}; + if (stored.language) { + state.app.language = stored.language; + state.app.translations = getTranslations(stored.language); + } + state.user = {}; + if (stored.username) { + state.user.username = stored.username; + } + state.app.soundIsEnabled = stored.soundIsEnabled !== false; + state.app.persistenceIsEnabled = stored.persistenceIsEnabled === true; + state.app.notificationIsEnabled = stored.notificationIsEnabled !== false; + return state; +}; + +let prevState; + +export const persistState = debounce(async store => { + const state = store.getState(); + + // We need prev state to compare + if (prevState) { + const { + user: { username }, + app: { notificationIsEnabled, soundIsEnabled, persistenceIsEnabled, language }, + } = state; + + + if (!persistenceIsEnabled) { + setSettings({ persistenceIsEnabled: false }); + return; + } + + const stored = getSettings(); + + if (prevState.user.notificationIsEnabled !== notificationIsEnabled) { + stored.notificationIsEnabled = notificationIsEnabled; + } + if (prevState.app.soundIsEnabled !== soundIsEnabled) { + stored.soundIsEnabled = soundIsEnabled; + } + if (prevState.app.persistenceIsEnabled !== persistenceIsEnabled) { + stored.persistenceIsEnabled = persistenceIsEnabled; + } + if (prevState.user.username !== username && username) { + stored.username = username; + } + if (prevState.app.language !== language && language) { + stored.language = language; + } + + setSettings(stored); + } + prevState = JSON.parse(JSON.stringify(state)); +}, 1000); diff --git a/client/yarn.lock b/client/yarn.lock index b2d1b9e..a65b427 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -2185,11 +2185,6 @@ jquery@3: resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.6.1.tgz#fab0408f8b45fc19f956205773b62b292c147a16" integrity sha512-opJeO4nCucVnsjiXOE+/PcCgYw9Gwpvs/a6B1LL/lQhwWwpbVEVYDZ1FokFr8PRc7ghYlrFPuyHuiiDNTQxmcw== -js-cookie@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-3.0.1.tgz#9e39b4c6c2f56563708d7d31f6f5f21873a92414" - integrity sha512-+0rgsUXZu4ncpPxRL+lNEptWMOWl9etvPHc/koSRp6MPwpRYAhmk0dUG00J4bxVV3r9uUzfo24wW0knS07SKSw== - js-sdsl@^4.1.4: version "4.2.0" resolved "https://registry.yarnpkg.com/js-sdsl/-/js-sdsl-4.2.0.tgz#278e98b7bea589b8baaf048c20aeb19eb7ad09d0"