mirror of
https://github.com/darkwire/darkwire.io.git
synced 2025-07-18 02:44:01 +00:00
username and settings are now persistant
This commit is contained in:
parent
6d0b703102
commit
b5e654f833
@ -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",
|
||||
|
@ -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 });
|
||||
};
|
||||
|
@ -100,7 +100,9 @@ class Home extends Component {
|
||||
<Settings
|
||||
roomId={this.props.roomId}
|
||||
toggleSoundEnabled={this.props.toggleSoundEnabled}
|
||||
togglePersistenceEnabled={this.props.togglePersistenceEnabled}
|
||||
soundIsEnabled={this.props.soundIsEnabled}
|
||||
persistenceIsEnabled={this.props.persistenceIsEnabled}
|
||||
toggleNotificationEnabled={this.props.toggleNotificationEnabled}
|
||||
toggleNotificationAllowed={this.props.toggleNotificationAllowed}
|
||||
notificationIsEnabled={this.props.notificationIsEnabled}
|
||||
@ -151,7 +153,7 @@ class Home extends Component {
|
||||
|
||||
createUser() {
|
||||
return new Promise(async resolve => {
|
||||
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,
|
||||
|
@ -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);
|
||||
|
@ -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,
|
||||
|
@ -42,6 +42,21 @@ exports[`Settings component > should display 1`] = `
|
||||
Desktop Notification
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
class="form-check"
|
||||
>
|
||||
<label
|
||||
class="form-check-label"
|
||||
for="persistence-control"
|
||||
>
|
||||
<input
|
||||
class="form-check-input"
|
||||
id="persistence-control"
|
||||
type="checkbox"
|
||||
/>
|
||||
Persist configuration
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
<section>
|
||||
@ -233,6 +248,21 @@ exports[`Settings component > should display 2`] = `
|
||||
Desktop notifications have been denied
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
class="form-check"
|
||||
>
|
||||
<label
|
||||
class="form-check-label"
|
||||
for="persistence-control"
|
||||
>
|
||||
<input
|
||||
class="form-check-input"
|
||||
id="persistence-control"
|
||||
type="checkbox"
|
||||
/>
|
||||
Persist configuration
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
<section>
|
||||
|
@ -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 && <T path="desktopNotificationBlocked" />}
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-check">
|
||||
<label className="form-check-label" htmlFor="persistence-control">
|
||||
<input
|
||||
id="persistence-control"
|
||||
onChange={this.handlePersistenceToggle.bind(this)}
|
||||
className="form-check-input"
|
||||
type="checkbox"
|
||||
checked={this.props.persistenceIsEnabled}
|
||||
/>
|
||||
<T path="persistence" />
|
||||
</label>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
@ -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,
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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([
|
||||
{
|
||||
|
@ -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,
|
||||
|
@ -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' },
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -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 {
|
||||
|
@ -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',
|
||||
});
|
||||
});
|
||||
|
@ -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 (
|
||||
<Provider store={store}>
|
||||
<BrowserRouter>
|
||||
<div className="h-100">
|
||||
<Switch>
|
||||
<Route exact path="/" render={() => <Redirect to={`/${shortId.generate()}`} />} />
|
||||
<Route path="/:roomId" component={Home} />
|
||||
</Switch>
|
||||
</div>
|
||||
</BrowserRouter>
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
}
|
83
client/src/utils/persistence.js
Normal file
83
client/src/utils/persistence.js
Normal file
@ -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);
|
@ -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"
|
||||
|
Loading…
x
Reference in New Issue
Block a user