Skip to content

Commit 4e9c7f4

Browse files
authored
feat: agent/contact availability status (#218)
* Publish/subscribe in action cable * Add availability status for user * Add user id in get user details * Add availability status colors * Add lodash filter * Add availability status in conversation list * Some style fixes * Add availability change screen * Add more redux actions * Fix conversation duplicate issue * Add notification settings api call on app start * Add translations * Add add/remove item from array helper * Add availability and preference constants * Add notification preference screen * Add preference and availability in settings screen * Move get notification settings api call to settings screen * Complete update availability status feature * Add translations for availability status types * Fix prop type warnings * Code beautification * Remove scroll view in conversation list * Update empty conversation image * Fix rendering attachemnt item in chat screen * Fix scroll to button issue * Remove last_seen from message read api * Update locale texts
1 parent a88e6b4 commit 4e9c7f4

31 files changed

+864
-73
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"axios": "^0.19.2",
3333
"hermes-engine": "0.4.2-rc1",
3434
"i18n-js": "^3.7.1",
35+
"lodash.filter": "^4.6.0",
3536
"lodash.groupby": "^4.6.0",
3637
"md5": "^2.2.1",
3738
"moment": "^2.27.0",

src/actions/auth.js

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import axios from '../helpers/APIHelper';
1+
import APIHelper from '../helpers/APIHelper';
2+
import axios from 'axios';
23
import * as Sentry from '@sentry/react-native';
34
import {
45
LOGIN,
@@ -12,14 +13,22 @@ import {
1213
RESET_AUTH,
1314
SET_LOCALE,
1415
SET_ACCOUNT,
16+
UPDATE_USER,
17+
UPDATE_ACTIVITY_STATUS,
18+
UPDATE_ACTIVITY_STATUS_SUCCESS,
19+
UPDATE_ACTIVITY_STATUS_ERROR,
1520
} from '../constants/actions';
1621
import { showToast } from '../helpers/ToastHelper';
1722
import I18n from '../i18n';
23+
import { getHeaders } from '../helpers/AuthHelper';
24+
import { getBaseUrl } from '../helpers/UrlHelper';
25+
26+
import { API_URL } from '../constants/url';
1827

1928
export const doLogin = ({ email, password }) => async (dispatch) => {
2029
try {
2130
dispatch({ type: LOGIN });
22-
const response = await axios.post('auth/sign_in', { email, password });
31+
const response = await APIHelper.post('auth/sign_in', { email, password });
2332
const { data } = response.data;
2433
const { name: username, id, account_id } = data;
2534
// Check user has any account
@@ -42,7 +51,7 @@ export const doLogin = ({ email, password }) => async (dispatch) => {
4251
export const onResetPassword = ({ email }) => async (dispatch) => {
4352
try {
4453
dispatch({ type: RESET_PASSWORD });
45-
const response = await axios.post('auth/password', { email });
54+
const response = await APIHelper.post('auth/password', { email });
4655
const { data } = response;
4756
showToast(data);
4857

@@ -54,7 +63,7 @@ export const onResetPassword = ({ email }) => async (dispatch) => {
5463

5564
export const getAccountDetails = () => async (dispatch) => {
5665
try {
57-
const result = await axios.get('');
66+
const result = await APIHelper.get('');
5867

5968
const {
6069
data: { locale },
@@ -75,3 +84,41 @@ export const onLogOut = () => async (dispatch) => {
7584
export const setAccount = ({ accountId }) => async (dispatch) => {
7685
dispatch({ type: SET_ACCOUNT, payload: accountId });
7786
};
87+
// Add/Update availability status of agents
88+
export const addOrUpdateActiveUsers = ({ users }) => async (dispatch, getState) => {
89+
const { user: loggedUser } = await getState().auth;
90+
Object.keys(users).forEach((user) => {
91+
if (parseInt(user) === loggedUser.id) {
92+
loggedUser.availability_status = users[user];
93+
dispatch({
94+
type: UPDATE_USER,
95+
payload: loggedUser,
96+
});
97+
}
98+
});
99+
};
100+
101+
export const updateAvailabilityStatus = ({ availability }) => async (dispatch) => {
102+
dispatch({ type: UPDATE_ACTIVITY_STATUS });
103+
try {
104+
const headers = await getHeaders();
105+
const baseUrl = await getBaseUrl();
106+
107+
await axios.put(
108+
`${baseUrl}${API_URL}profile`,
109+
{
110+
availability,
111+
},
112+
{
113+
headers: headers,
114+
},
115+
);
116+
117+
dispatch({
118+
type: UPDATE_ACTIVITY_STATUS_SUCCESS,
119+
payload: availability,
120+
});
121+
} catch (error) {
122+
dispatch({ type: UPDATE_ACTIVITY_STATUS_ERROR, payload: error });
123+
}
124+
};

src/actions/conversation.js

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
const lodashFilter = require('lodash.filter');
2+
13
import {
24
GET_CONVERSATION,
35
GET_CONVERSATION_ERROR,
@@ -27,6 +29,7 @@ import {
2729
ADD_MESSAGE,
2830
SET_ASSIGNEE_TYPE,
2931
SET_CONVERSATION_META,
32+
UPDATE_SINGLE_CONVERSATION,
3033
} from '../constants/actions';
3134

3235
import axios from '../helpers/APIHelper';
@@ -157,12 +160,11 @@ export const addOrUpdateConversation = ({ conversation }) => async (dispatch, ge
157160
// Check conversation is already exists or not
158161
const [conversationExists] = payload.filter((c) => c.id === conversation.id);
159162
let updatedConversations = payload;
160-
if (conversationExists) {
161-
updatedConversations = payload.filter((c) => c.id !== conversation.id);
162-
} else {
163+
164+
if (!conversationExists) {
163165
updatedConversations.unshift(conversation);
166+
dispatch({ type: UPDATE_CONVERSATION, payload: updatedConversations });
164167
}
165-
dispatch({ type: UPDATE_CONVERSATION, payload: updatedConversations });
166168
}
167169

168170
dispatch(getAllNotifications({ pageNo: 1 }));
@@ -301,16 +303,14 @@ export const markMessagesAsRead = ({ conversationId }) => async (dispatch, getSt
301303
dispatch({ type: MARK_MESSAGES_AS_READ });
302304
try {
303305
const apiUrl = `conversations/${conversationId}/update_last_seen`;
304-
const agent_last_seen_at = new Date().getTime();
305-
const response = await axios.post(apiUrl, {
306-
agent_last_seen_at,
307-
});
308306

307+
const response = await axios.post(apiUrl);
309308
dispatch({
310309
type: MARK_MESSAGES_AS_READ_SUCCESS,
311310
payload: response.data,
312311
});
313312

313+
const agent_last_seen_at = new Date().getTime() / 1000;
314314
const updatedConversations = payload.map((item) =>
315315
item.id === conversationId ? { ...item, agent_last_seen_at } : item,
316316
);
@@ -426,3 +426,19 @@ export const toggleConversationStatus = ({ conversationId }) => async (dispatch,
426426
dispatch(getConversationDetails({ conversationId }));
427427
} catch (error) {}
428428
};
429+
430+
export const addOrUpdateActiveContacts = ({ contacts }) => async (dispatch, getState) => {
431+
const { data } = await getState().conversation;
432+
Object.keys(contacts).forEach((contact) => {
433+
let conversations = lodashFilter(data.payload, { meta: { sender: { id: parseInt(contact) } } });
434+
conversations.forEach((item) => {
435+
const updatedConversation = item;
436+
updatedConversation.meta.sender.availability = contacts[contact];
437+
updatedConversation.meta.sender.availability_status = contacts[contact];
438+
dispatch({
439+
type: UPDATE_SINGLE_CONVERSATION,
440+
payload: updatedConversation,
441+
});
442+
});
443+
});
444+
};

src/actions/settings.js

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
11
import axios from 'axios';
22

3+
import APIHelper from '../helpers/APIHelper';
4+
35
import {
46
SET_URL,
57
SET_URL_ERROR,
68
SET_URL_SUCCESS,
79
RESET_SETTINGS,
810
SET_LOCALE,
11+
GET_NOTIFICATION_SETTINGS,
12+
GET_NOTIFICATION_SETTINGS_ERROR,
13+
GET_NOTIFICATION_SETTINGS_SUCCESS,
14+
UPDATE_NOTIFICATION_SETTINGS,
15+
UPDATE_NOTIFICATION_SETTINGS_SUCCESS,
16+
UPDATE_NOTIFICATION_SETTINGS_ERROR,
917
} from '../constants/actions';
1018

1119
import * as RootNavigation from '../helpers/NavigationHelper';
@@ -35,6 +43,34 @@ export const setInstallationUrl = ({ url }) => async (dispatch) => {
3543
}
3644
};
3745

46+
export const getNotificationSettings = () => async (dispatch) => {
47+
dispatch({ type: GET_NOTIFICATION_SETTINGS });
48+
try {
49+
const response = await APIHelper.get('notification_settings');
50+
const { data } = response;
51+
dispatch({
52+
type: GET_NOTIFICATION_SETTINGS_SUCCESS,
53+
payload: data,
54+
});
55+
} catch (error) {
56+
dispatch({ type: GET_NOTIFICATION_SETTINGS_ERROR, payload: error });
57+
}
58+
};
59+
60+
export const updateNotificationSettings = (preferences) => async (dispatch) => {
61+
dispatch({ type: UPDATE_NOTIFICATION_SETTINGS });
62+
try {
63+
const response = await APIHelper.patch('notification_settings', preferences);
64+
const { data } = response;
65+
dispatch({
66+
type: UPDATE_NOTIFICATION_SETTINGS_SUCCESS,
67+
payload: data,
68+
});
69+
} catch (error) {
70+
dispatch({ type: UPDATE_NOTIFICATION_SETTINGS_ERROR, payload: error });
71+
}
72+
};
73+
3874
export const resetSettings = () => async (dispatch) => {
3975
dispatch({ type: RESET_SETTINGS });
4076
};
48.9 KB
Loading

src/components/AvailabilityItem.js

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import React from 'react';
2+
import { TouchableOpacity, View } from 'react-native';
3+
import PropTypes from 'prop-types';
4+
import { Radio, withStyles } from '@ui-kitten/components';
5+
6+
import CustomText from './Text';
7+
8+
const styles = (theme) => ({
9+
itemView: {
10+
flexDirection: 'row',
11+
alignItems: 'center',
12+
justifyContent: 'space-between',
13+
marginBottom: 8,
14+
marginTop: 8,
15+
},
16+
iconView: {
17+
flex: 1,
18+
},
19+
icon: {
20+
width: 16,
21+
height: 16,
22+
},
23+
textView: {
24+
flex: 8,
25+
},
26+
text: {
27+
color: theme['text-hint-color'],
28+
fontWeight: theme['font-semi-bold'],
29+
fontSize: theme['font-size-small'],
30+
textAlign: 'left',
31+
textTransform: 'capitalize',
32+
},
33+
radioView: {
34+
flex: 1,
35+
alignItems: 'flex-end',
36+
},
37+
});
38+
39+
const propTypes = {
40+
eva: PropTypes.shape({
41+
style: PropTypes.object,
42+
}).isRequired,
43+
title: PropTypes.string,
44+
item: PropTypes.string,
45+
onCheckedChange: PropTypes.func,
46+
isChecked: PropTypes.bool,
47+
};
48+
49+
const AvailabilityItemComponent = ({ title, item, onCheckedChange, isChecked, eva: { style } }) => (
50+
<TouchableOpacity style={style.itemView} onPress={() => onCheckedChange({ item })}>
51+
<View style={style.textView}>
52+
<CustomText style={style.text}>{title}</CustomText>
53+
</View>
54+
55+
<View style={style.radioView}>
56+
<Radio checked={isChecked} onChange={() => onCheckedChange({ item })} />
57+
</View>
58+
</TouchableOpacity>
59+
);
60+
61+
AvailabilityItemComponent.propTypes = propTypes;
62+
63+
const AvailabilityItem = withStyles(AvailabilityItemComponent, styles);
64+
export default AvailabilityItem;

src/components/ChatAttachmentItem.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,12 @@ const propTypes = {
129129
}).isRequired,
130130
type: PropTypes.string,
131131
showAttachment: PropTypes.func,
132-
attachment: PropTypes.shape({}),
132+
attachment: PropTypes.arrayOf(
133+
PropTypes.shape({
134+
file_type: PropTypes.string,
135+
data_url: PropTypes.string,
136+
}),
137+
),
133138
};
134139

135140
const FileIcon = (style) => {

src/components/ConversationItem.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ const propTypes = {
2626
sender: PropTypes.shape({
2727
name: PropTypes.string,
2828
thumbnail: PropTypes.string,
29+
availability_status: PropTypes.string,
2930
}),
3031
channel: PropTypes.string,
3132
}),
@@ -44,7 +45,7 @@ const ConversationItemComponent = ({
4445

4546
const {
4647
meta: {
47-
sender: { name, thumbnail },
48+
sender: { name, thumbnail, availability_status: availabilityStatus },
4849
channel,
4950
},
5051
messages,
@@ -63,6 +64,8 @@ const ConversationItemComponent = ({
6364
conversationId: id,
6465
});
6566

67+
const isActive = availabilityStatus === 'online' ? true : false;
68+
6669
return (
6770
<TouchableOpacity
6871
activeOpacity={0.5}
@@ -75,6 +78,8 @@ const ConversationItemComponent = ({
7578
userName={name}
7679
defaultBGColor={theme['color-primary-default']}
7780
channel={channel}
81+
isActive={isActive}
82+
availabilityStatus={availabilityStatus}
7883
/>
7984
</View>
8085
<View>
@@ -157,7 +162,6 @@ const styles = (theme) => ({
157162
conversationUserNotActive: {
158163
textTransform: 'capitalize',
159164
fontSize: theme['font-size-small'],
160-
fontWeight: theme['font-medium'],
161165
paddingTop: 4,
162166
color: theme['text-basic-color'],
163167
},

src/components/ImageLoader.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ const ImageLoader = ({ style }) => {
1313
};
1414

1515
const propTypes = {
16-
style: PropTypes.arrayOf(PropTypes.shape({})),
16+
style: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.shape({})), PropTypes.shape({})]),
1717
};
1818

1919
const styles = () => ({});

0 commit comments

Comments
 (0)