diff --git a/src/actions/conversation.js b/src/actions/conversation.js index a60450d44..3d5b7ed8c 100644 --- a/src/actions/conversation.js +++ b/src/actions/conversation.js @@ -1,7 +1,4 @@ -import { - SET_CONVERSATION_DETAILS, - ADD_OR_UPDATE_USER_TYPING_IN_CONVERSATION, -} from '../constants/actions'; +import { SET_CONVERSATION_DETAILS } from '../constants/actions'; import axios from '../helpers/APIHelper'; import { getInboxAgents } from './inbox'; @@ -22,45 +19,6 @@ export const getConversationDetails = } catch {} }; -export const addUserTypingToConversation = - ({ conversation, user }) => - async (dispatch, getState) => { - const { id: conversationId } = conversation; - const { conversationTypingUsers } = await getState().conversation; - const records = conversationTypingUsers[conversationId] || []; - const hasUserRecordAlready = !!records.filter( - record => record.id === user.id && record.type === user.type, - ).length; - if (!hasUserRecordAlready) { - dispatch({ - type: ADD_OR_UPDATE_USER_TYPING_IN_CONVERSATION, - payload: { - conversationId, - users: [...records, user], - }, - }); - } - }; - -export const removeUserFromTypingConversation = - ({ conversation, user }) => - async (dispatch, getState) => { - const { id: conversationId } = conversation; - const { conversationTypingUsers } = await getState().conversation; - const records = conversationTypingUsers[conversationId] || []; - const updatedUsers = records.filter( - record => record.id !== user.id || record.type !== user.type, - ); - - dispatch({ - type: ADD_OR_UPDATE_USER_TYPING_IN_CONVERSATION, - payload: { - conversationId, - users: updatedUsers, - }, - }); - }; - export const toggleTypingStatus = ({ conversationId, typingStatus }) => async dispatch => { diff --git a/src/helpers/ActionCable.js b/src/helpers/ActionCable.js index f828b4c33..d68617259 100644 --- a/src/helpers/ActionCable.js +++ b/src/helpers/ActionCable.js @@ -8,12 +8,9 @@ import { } from 'reducer/conversationSlice'; import conversationActions from 'reducer/conversationSlice.action'; -import { - addUserTypingToConversation, - removeUserFromTypingConversation, -} from '../actions/conversation'; import { addOrUpdateActiveUsers } from '../actions/auth'; import { store } from '../store'; +import { addUserToTyping, destroyUserFromTyping } from 'reducer/conversationTypingSlice'; class ActionCableConnector extends BaseActionCableConnector { constructor(pubsubToken, webSocketUrl, accountId, userId) { @@ -84,13 +81,13 @@ class ActionCableConnector extends BaseActionCableConnector { }; onTypingOn = ({ conversation, user }) => { - //TODO: Move this to typingSlice const conversationId = conversation.id; this.clearTimer(conversationId); + store.dispatch( - addUserTypingToConversation({ - conversation, + addUserToTyping({ + conversationId, user, }), ); @@ -98,14 +95,13 @@ class ActionCableConnector extends BaseActionCableConnector { }; onTypingOff = ({ conversation, user }) => { - //TODO: Move this to typingSlice const conversationId = conversation.id; this.clearTimer(conversationId); store.dispatch( - removeUserFromTypingConversation({ - conversation, + destroyUserFromTyping({ + conversationId, user, }), ); diff --git a/src/reducer/conversationTypingSlice.js b/src/reducer/conversationTypingSlice.js new file mode 100644 index 000000000..da017ef11 --- /dev/null +++ b/src/reducer/conversationTypingSlice.js @@ -0,0 +1,36 @@ +import { createSlice, createEntityAdapter } from '@reduxjs/toolkit'; + +const conversationTypingAdapter = createEntityAdapter(); + +const typingSlice = createSlice({ + name: 'conversationTypingStatus', + initialState: conversationTypingAdapter.getInitialState({ + records: {}, + }), + reducers: { + addUserToTyping: (state, action) => { + const { conversationId, user } = action.payload; + const records = state.records[conversationId] || []; + const hasUserRecordAlready = !!records.filter( + record => record.id === user.id && record.type === user.type, + ).length; + + if (!hasUserRecordAlready) { + state.records[conversationId] = [...records, user]; + } + }, + destroyUserFromTyping: (state, action) => { + const { conversationId, user } = action.payload; + const records = state.entities[conversationId] || []; + const updatedRecords = records.filter( + record => record.id !== user.id || record.type !== user.type, + ); + state.records[conversationId] = updatedRecords; + }, + }, +}); +export const { addUserToTyping, destroyUserFromTyping } = typingSlice.actions; + +export const selectAllTypingUsers = state => state.conversationTypingStatus.records; + +export default typingSlice.reducer; diff --git a/src/reducer/index.js b/src/reducer/index.js index 1fbda66d2..c57131a27 100644 --- a/src/reducer/index.js +++ b/src/reducer/index.js @@ -8,6 +8,7 @@ import notification from './notification'; import agent from './agent'; import cannedResponseSlice from './cannedResponseSlice'; import conversationSlice from './conversationSlice'; +import conversationTypingSlice from './conversationTypingSlice'; export const rootReducer = combineReducers({ auth, @@ -18,6 +19,7 @@ export const rootReducer = combineReducers({ agent, cannedResponses: cannedResponseSlice, conversations: conversationSlice, + conversationTypingStatus: conversationTypingSlice, }); // export default (state, action) => diff --git a/src/reducer/tests/conversationTypingSlice.spec.js b/src/reducer/tests/conversationTypingSlice.spec.js new file mode 100644 index 000000000..907e39962 --- /dev/null +++ b/src/reducer/tests/conversationTypingSlice.spec.js @@ -0,0 +1,61 @@ +import conversationTypingSlice, { + selectAllTypingUsers, + addUserToTyping, + destroyUserFromTyping, +} from '../conversationTypingSlice'; + +describe('conversationTypingSlice', () => { + describe('reducers', () => { + const initialState = { entities: {}, ids: [], loading: false, records: {} }; + + it('Add typing users to store', () => { + expect( + conversationTypingSlice( + initialState, + addUserToTyping({ conversationId: 1, user: { id: 1, type: 'user' } }), + ), + ).toEqual({ + entities: {}, + ids: [], + loading: false, + records: { + 1: [{ id: 1, type: 'user' }], + }, + }); + }); + + it('Remove typing users from store', () => { + expect( + conversationTypingSlice( + { entities: {}, ids: [], loading: false, records: { 1: [{ id: 1, type: 'user' }] } }, + destroyUserFromTyping({ conversationId: 1, user: { id: 1, type: 'user' } }), + ), + ).toEqual({ + entities: {}, + ids: [], + loading: false, + records: { + 1: [], + }, + }); + }); + }); + + describe('selectors', () => { + it('selects all typing users', () => { + const state = { + conversationTypingStatus: { + entities: {}, + ids: [], + loading: false, + records: { + 1: [{ id: 1, type: 'user' }], + }, + }, + }; + expect(selectAllTypingUsers(state)).toEqual({ + 1: [{ id: 1, type: 'user' }], + }); + }); + }); +}); diff --git a/src/screens/ChatScreen/ChatScreen.js b/src/screens/ChatScreen/ChatScreen.js index 8bb958b67..330a64569 100644 --- a/src/screens/ChatScreen/ChatScreen.js +++ b/src/screens/ChatScreen/ChatScreen.js @@ -15,6 +15,7 @@ import { openURL } from 'helpers/UrlHelper'; import { markNotificationAsRead } from 'actions/notification'; import { getGroupedConversation, findUniqueMessages } from 'helpers'; import { actions as CannedResponseActions } from 'reducer/cannedResponseSlice'; +import { selectAllTypingUsers } from 'reducer/conversationTypingSlice'; import { clearConversation, selectors as conversationSelectors, @@ -38,7 +39,7 @@ const propTypes = { const ChatScreenComponent = ({ eva: { style }, navigation, route }) => { const dispatch = useDispatch(); - const conversationTypingUsers = useSelector(state => state.conversation.conversationTypingUsers); + const conversationTypingUsers = useSelector(selectAllTypingUsers); const isFetching = useSelector(selectMessagesLoading); const isAllMessagesFetched = useSelector(selectAllMessagesFetched); diff --git a/src/screens/Conversation/components/ConversationList/ConversationList.js b/src/screens/Conversation/components/ConversationList/ConversationList.js index 646f5ec5e..d91769ca3 100644 --- a/src/screens/Conversation/components/ConversationList/ConversationList.js +++ b/src/screens/Conversation/components/ConversationList/ConversationList.js @@ -16,6 +16,7 @@ import ConversationItem from '../ConversationItem/ConversationItem'; import ConversationEmptyMessage from '../ConversationEmptyMessage/ConversationEmptyMessage'; import i18n from 'i18n'; import createStyles from './ConversationList.style'; +import { selectAllTypingUsers } from 'reducer/conversationTypingSlice'; const propTypes = { assigneeType: PropTypes.string, @@ -46,8 +47,7 @@ const ConversationList = ({ const [refreshing, setRefreshing] = useState(false); const userId = useSelector(store => store.auth.user.id); const navigation = useNavigation(); - const conversationTypingUsers = useSelector(state => state.conversation.conversationTypingUsers); - + const conversationTypingUsers = useSelector(selectAllTypingUsers); const filters = { assigneeType, conversationStatus,