Switch to redux hooks

This commit is contained in:
Jeremie Pardou-Piquemal 2022-12-18 23:34:46 +01:00 committed by Jeremie Pardou
parent 644f790ad8
commit 0df6a5097c
10 changed files with 64 additions and 54 deletions

View File

@ -31,10 +31,6 @@ export const toggleSocketConnected = payload => async dispatch => {
dispatch({ type: 'TOGGLE_SOCKET_CONNECTED', payload }); dispatch({ type: 'TOGGLE_SOCKET_CONNECTED', payload });
}; };
export const createUser = payload => async dispatch => {
dispatch({ type: 'CREATE_USER', payload });
};
export const clearActivities = () => async dispatch => { export const clearActivities = () => async dispatch => {
dispatch({ type: 'CLEAR_ACTIVITIES' }); dispatch({ type: 'CLEAR_ACTIVITIES' });
}; };

View File

@ -40,7 +40,6 @@ describe('App actions', () => {
[actions.showNotice('test'), 'SHOW_NOTICE'], [actions.showNotice('test'), 'SHOW_NOTICE'],
[actions.toggleSoundEnabled('test'), 'TOGGLE_SOUND_ENABLED'], [actions.toggleSoundEnabled('test'), 'TOGGLE_SOUND_ENABLED'],
[actions.toggleSocketConnected('test'), 'TOGGLE_SOCKET_CONNECTED'], [actions.toggleSocketConnected('test'), 'TOGGLE_SOCKET_CONNECTED'],
[actions.createUser('test'), 'CREATE_USER'],
[actions.setLanguage('test'), 'CHANGE_LANGUAGE'], [actions.setLanguage('test'), 'CHANGE_LANGUAGE'],
]; ];

View File

@ -1,10 +1,18 @@
import { getSocket } from '@/utils/socket'; import { getSocket } from '@/utils/socket';
import { prepare as prepareMessage, process as processMessage } from '@/utils/message'; import { prepare as prepareMessage, process as processMessage } from '@/utils/message';
import { changeUsername } from '@/reducers/user';
export const sendEncryptedMessage = payload => async (dispatch, getState) => { export const sendEncryptedMessage = payload => async (dispatch, getState) => {
const state = getState(); const state = getState();
const msg = await prepareMessage(payload, state); const msg = await prepareMessage(payload, state);
switch(msg.original.type){
case "CHANGE_USERNAME":
dispatch(changeUsername(msg.original.payload));
dispatch({ type: `SEND_ENCRYPTED_MESSAGE_${msg.original.type}`, payload: msg.original.payload }); dispatch({ type: `SEND_ENCRYPTED_MESSAGE_${msg.original.type}`, payload: msg.original.payload });
break;
default:
dispatch({ type: `SEND_ENCRYPTED_MESSAGE_${msg.original.type}`, payload: msg.original.payload });
}
getSocket().emit('ENCRYPTED_MESSAGE', msg.toSend); getSocket().emit('ENCRYPTED_MESSAGE', msg.toSend);
}; };

View File

@ -4,6 +4,8 @@ import PropTypes from 'prop-types';
import { nanoid } from 'nanoid'; import { nanoid } from 'nanoid';
import { X, AlertCircle } from 'react-feather'; import { X, AlertCircle } from 'react-feather';
import classNames from 'classnames'; import classNames from 'classnames';
import { useSelector, useDispatch } from 'react-redux';
import { createUser } from '@/reducers/user';
import Crypto from '@/utils/crypto'; import Crypto from '@/utils/crypto';
import { connect as connectSocket } from '@/utils/socket'; import { connect as connectSocket } from '@/utils/socket';
@ -290,15 +292,18 @@ const Home = ({
); );
}; };
export const WithUser = ({ createUser, username, ...rest }) => { export const WithUser = ({ ...rest }) => {
const [loaded, setLoaded] = React.useState(false); const [loaded, setLoaded] = React.useState(false);
const loading = React.useRef(false); const loading = React.useRef(false);
const user = useSelector(state => state.user);
const dispatch = useDispatch();
React.useEffect(() => { React.useEffect(() => {
let mounted = true; let mounted = true;
const createUserLocal = async () => { const createUserLocal = async () => {
const localUsername = username || nanoid(); const localUsername = user.username || nanoid();
const encryptDecryptKeys = await crypto.createEncryptDecryptKeys(); const encryptDecryptKeys = await crypto.createEncryptDecryptKeys();
const exportedEncryptDecryptPrivateKey = await crypto.exportKey(encryptDecryptKeys.privateKey); const exportedEncryptDecryptPrivateKey = await crypto.exportKey(encryptDecryptKeys.privateKey);
@ -309,11 +314,14 @@ export const WithUser = ({ createUser, username, ...rest }) => {
return; return;
} }
createUser({ const payload = {
username: localUsername, username: localUsername,
publicKey: exportedEncryptDecryptPublicKey, publicKey: exportedEncryptDecryptPublicKey,
privateKey: exportedEncryptDecryptPrivateKey, privateKey: exportedEncryptDecryptPrivateKey,
}); };
dispatch(createUser(payload));
dispatch({ type: 'CREATE_USER', payload });
loading.current = false; loading.current = false;
setLoaded(true); setLoaded(true);
@ -328,12 +336,13 @@ export const WithUser = ({ createUser, username, ...rest }) => {
loading.current = false; loading.current = false;
mounted = false; mounted = false;
}; };
}, [createUser, loaded, username]); }, [dispatch, loaded, user.username]);
if (!loaded) { if (!loaded) {
return null; return null;
} }
return <Home username={username} {...rest} />;
return <Home username={user.username} publicKey={user.publicKey} userId={user.id} {...rest} />;
}; };
WithUser.defaultProps = { WithUser.defaultProps = {
@ -343,10 +352,7 @@ WithUser.defaultProps = {
WithUser.propTypes = { WithUser.propTypes = {
receiveEncryptedMessage: PropTypes.func.isRequired, receiveEncryptedMessage: PropTypes.func.isRequired,
receiveUnencryptedMessage: PropTypes.func.isRequired, receiveUnencryptedMessage: PropTypes.func.isRequired,
createUser: PropTypes.func.isRequired,
activities: PropTypes.array.isRequired, activities: PropTypes.array.isRequired,
username: PropTypes.string.isRequired,
publicKey: PropTypes.object.isRequired,
members: PropTypes.array.isRequired, members: PropTypes.array.isRequired,
socketId: PropTypes.string.isRequired, socketId: PropTypes.string.isRequired,
roomId: PropTypes.string.isRequired, roomId: PropTypes.string.isRequired,
@ -355,7 +361,6 @@ WithUser.propTypes = {
openModal: PropTypes.func.isRequired, openModal: PropTypes.func.isRequired,
closeModal: PropTypes.func.isRequired, closeModal: PropTypes.func.isRequired,
iAmOwner: PropTypes.bool.isRequired, iAmOwner: PropTypes.bool.isRequired,
userId: PropTypes.string.isRequired,
toggleWindowFocus: PropTypes.func.isRequired, toggleWindowFocus: PropTypes.func.isRequired,
soundIsEnabled: PropTypes.bool.isRequired, soundIsEnabled: PropTypes.bool.isRequired,
persistenceIsEnabled: PropTypes.bool.isRequired, persistenceIsEnabled: PropTypes.bool.isRequired,

View File

@ -43,7 +43,6 @@ const WithNewMessageNotification = ({
const { username, type, text, fileName, locked, newUsername, currentUsername, action } = currentLastMessage; const { username, type, text, fileName, locked, newUsername, currentUsername, action } = currentLastMessage;
if (currentLastMessage !== lastMessage && !windowIsFocused) { if (currentLastMessage !== lastMessage && !windowIsFocused) {
setLastMessage(currentLastMessage);
if (notificationIsAllowed && notificationIsEnabled) { if (notificationIsAllowed && notificationIsEnabled) {
// Generate the proper notification according to the message type // Generate the proper notification according to the message type
switch (type) { switch (type) {
@ -79,6 +78,8 @@ const WithNewMessageNotification = ({
if (soundIsEnabled) beep.play(); if (soundIsEnabled) beep.play();
} }
setLastMessage(currentLastMessage);
if (unreadMessageCount !== lastUnreadMessageCount) { if (unreadMessageCount !== lastUnreadMessageCount) {
setLastUnreadMessageCount(unreadMessageCount); setLastUnreadMessageCount(unreadMessageCount);
Tinycon.setBubble(unreadMessageCount); Tinycon.setBubble(unreadMessageCount);

View File

@ -3,7 +3,6 @@ import { useLoaderData } from 'react-router-dom';
import { import {
receiveEncryptedMessage, receiveEncryptedMessage,
createUser,
openModal, openModal,
closeModal, closeModal,
toggleWindowFocus, toggleWindowFocus,
@ -26,8 +25,6 @@ const mapStateToProps = state => {
return { return {
activities: state.activities.items, activities: state.activities.items,
userId: state.user.id,
username: state.user.username,
publicKey: state.user.publicKey, publicKey: state.user.publicKey,
privateKey: state.user.privateKey, privateKey: state.user.privateKey,
members: state.room.members.filter(m => m.username && m.publicKey), members: state.room.members.filter(m => m.username && m.publicKey),
@ -48,7 +45,6 @@ const mapStateToProps = state => {
const mapDispatchToProps = { const mapDispatchToProps = {
receiveEncryptedMessage, receiveEncryptedMessage,
createUser,
openModal, openModal,
closeModal, closeModal,
toggleWindowFocus, toggleWindowFocus,

View File

@ -206,10 +206,15 @@ describe('Connected Home component', () => {
</Provider>, </Provider>,
); );
expect(store.getState().app.unreadMessageCount).toBe(1); expect(store.getState().app.unreadMessageCount).toBe(1);
expect(notify).toHaveBeenCalledTimes(1);
expect(notify).toHaveBeenLastCalledWith('sender said:', 'new message'); expect(notify).toHaveBeenLastCalledWith('sender said:', 'new message');
expect(beep.play).toHaveBeenCalledTimes(1);
expect(beep.play).toHaveBeenLastCalledWith(); expect(beep.play).toHaveBeenLastCalledWith();
expect(Tinycon.setBubble).toHaveBeenLastCalledWith(1); expect(Tinycon.setBubble).toHaveBeenLastCalledWith(1);
notify.mockClear();
beep.play.mockClear();
// Test with sound and notification disabled // Test with sound and notification disabled
await act(() => toggleNotificationEnabled(false)(store.dispatch)); await act(() => toggleNotificationEnabled(false)(store.dispatch));
await act(() => toggleSoundEnabled(false)(store.dispatch)); await act(() => toggleSoundEnabled(false)(store.dispatch));
@ -227,8 +232,8 @@ describe('Connected Home component', () => {
); );
expect(store.getState().app.unreadMessageCount).toBe(2); expect(store.getState().app.unreadMessageCount).toBe(2);
expect(notify).toHaveBeenCalledTimes(2); expect(notify).toHaveBeenCalledTimes(0);
expect(beep.play).toHaveBeenCalledTimes(2); expect(beep.play).toHaveBeenCalledTimes(0);
expect(Tinycon.setBubble).toHaveBeenLastCalledWith(2); expect(Tinycon.setBubble).toHaveBeenLastCalledWith(2);
}); });
}); });

View File

@ -1,27 +1,28 @@
const initialState = { import { createSlice } from '@reduxjs/toolkit'
export const userSlice = createSlice({
name: 'user',
initialState: {
privateKey: {}, privateKey: {},
publicKey: {}, publicKey: {},
username: '', username: '',
id: '', id: '',
}; },
reducers: {
const user = (receivedState, action) => { createUser: (state, action) => {
const state = { ...initialState, ...receivedState }; const { privateKey, publicKey, username } = action.payload;
state.privateKey = privateKey;
switch (action.type) { state.publicKey = publicKey;
case 'CREATE_USER': state.username = username;
return { state.id = publicKey.n;
...action.payload, },
id: action.payload.publicKey.n, changeUsername: (state, action) => {
}; const { newUsername } = action.payload;
case 'SEND_ENCRYPTED_MESSAGE_CHANGE_USERNAME': state.username = newUsername;
return {
...state,
username: action.payload.newUsername,
};
default:
return state;
} }
}; },
})
export default user; export const { createUser,changeUsername } = userSlice.actions
export default userSlice.reducer

View File

@ -1,6 +1,6 @@
import { describe, it, expect, vi } from 'vitest'; import { describe, it, expect, vi } from 'vitest';
import reducer from './user'; import reducer, { createUser, changeUsername} from './user';
vi.mock('@/i18n', () => { vi.mock('@/i18n', () => {
return { return {
@ -15,7 +15,8 @@ describe('User reducer', () => {
it('should handle CREATE_USER', () => { it('should handle CREATE_USER', () => {
const payload = { publicKey: { n: 'alicekey' }, username: 'alice' }; const payload = { publicKey: { n: 'alicekey' }, username: 'alice' };
expect(reducer({}, { type: 'CREATE_USER', payload })).toEqual({
expect(reducer({},createUser(payload) )).toEqual({
id: 'alicekey', id: 'alicekey',
publicKey: { n: 'alicekey' }, publicKey: { n: 'alicekey' },
username: 'alice', username: 'alice',
@ -24,10 +25,8 @@ describe('User reducer', () => {
it('should handle SEND_ENCRYPTED_MESSAGE_CHANGE_USERNAME', () => { it('should handle SEND_ENCRYPTED_MESSAGE_CHANGE_USERNAME', () => {
const payload = { newUsername: 'alice' }; const payload = { newUsername: 'alice' };
expect(reducer({ username: 'polux' }, { type: 'SEND_ENCRYPTED_MESSAGE_CHANGE_USERNAME', payload })).toEqual({
id: '', expect(reducer({ username: 'polux' }, changeUsername(payload))).toEqual({
privateKey: {},
publicKey: {},
username: 'alice', username: 'alice',
}); });
}); });

View File

@ -86,7 +86,7 @@ Then run it. Example:
$ docker run --init --name darkwire.io --rm -p 3001:3001 darkwire.io $ docker run --init --name darkwire.io --rm -p 3001:3001 darkwire.io
``` ```
You are able to use any of the enviroment variables available in `server/.env.dist` and `client/.env.dist`. The defaults are available in [Dockerfile](Dockerfile) You are able to use any of the environment variables available in `server/.env.dist` and `client/.env.dist`. The defaults are available in [Dockerfile](Dockerfile)
### Security ### Security