Skip to content

Commit

Permalink
feat: agent/contact availability status (#218)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
muhsin-k authored Aug 8, 2020
1 parent a88e6b4 commit 4e9c7f4
Show file tree
Hide file tree
Showing 31 changed files with 864 additions and 73 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"axios": "^0.19.2",
"hermes-engine": "0.4.2-rc1",
"i18n-js": "^3.7.1",
"lodash.filter": "^4.6.0",
"lodash.groupby": "^4.6.0",
"md5": "^2.2.1",
"moment": "^2.27.0",
Expand Down
55 changes: 51 additions & 4 deletions src/actions/auth.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import axios from '../helpers/APIHelper';
import APIHelper from '../helpers/APIHelper';
import axios from 'axios';
import * as Sentry from '@sentry/react-native';
import {
LOGIN,
Expand All @@ -12,14 +13,22 @@ import {
RESET_AUTH,
SET_LOCALE,
SET_ACCOUNT,
UPDATE_USER,
UPDATE_ACTIVITY_STATUS,
UPDATE_ACTIVITY_STATUS_SUCCESS,
UPDATE_ACTIVITY_STATUS_ERROR,
} from '../constants/actions';
import { showToast } from '../helpers/ToastHelper';
import I18n from '../i18n';
import { getHeaders } from '../helpers/AuthHelper';
import { getBaseUrl } from '../helpers/UrlHelper';

import { API_URL } from '../constants/url';

export const doLogin = ({ email, password }) => async (dispatch) => {
try {
dispatch({ type: LOGIN });
const response = await axios.post('auth/sign_in', { email, password });
const response = await APIHelper.post('auth/sign_in', { email, password });
const { data } = response.data;
const { name: username, id, account_id } = data;
// Check user has any account
Expand All @@ -42,7 +51,7 @@ export const doLogin = ({ email, password }) => async (dispatch) => {
export const onResetPassword = ({ email }) => async (dispatch) => {
try {
dispatch({ type: RESET_PASSWORD });
const response = await axios.post('auth/password', { email });
const response = await APIHelper.post('auth/password', { email });
const { data } = response;
showToast(data);

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

export const getAccountDetails = () => async (dispatch) => {
try {
const result = await axios.get('');
const result = await APIHelper.get('');

const {
data: { locale },
Expand All @@ -75,3 +84,41 @@ export const onLogOut = () => async (dispatch) => {
export const setAccount = ({ accountId }) => async (dispatch) => {
dispatch({ type: SET_ACCOUNT, payload: accountId });
};
// Add/Update availability status of agents
export const addOrUpdateActiveUsers = ({ users }) => async (dispatch, getState) => {
const { user: loggedUser } = await getState().auth;
Object.keys(users).forEach((user) => {
if (parseInt(user) === loggedUser.id) {
loggedUser.availability_status = users[user];
dispatch({
type: UPDATE_USER,
payload: loggedUser,
});
}
});
};

export const updateAvailabilityStatus = ({ availability }) => async (dispatch) => {
dispatch({ type: UPDATE_ACTIVITY_STATUS });
try {
const headers = await getHeaders();
const baseUrl = await getBaseUrl();

await axios.put(
`${baseUrl}${API_URL}profile`,
{
availability,
},
{
headers: headers,
},
);

dispatch({
type: UPDATE_ACTIVITY_STATUS_SUCCESS,
payload: availability,
});
} catch (error) {
dispatch({ type: UPDATE_ACTIVITY_STATUS_ERROR, payload: error });
}
};
32 changes: 24 additions & 8 deletions src/actions/conversation.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
const lodashFilter = require('lodash.filter');

import {
GET_CONVERSATION,
GET_CONVERSATION_ERROR,
Expand Down Expand Up @@ -27,6 +29,7 @@ import {
ADD_MESSAGE,
SET_ASSIGNEE_TYPE,
SET_CONVERSATION_META,
UPDATE_SINGLE_CONVERSATION,
} from '../constants/actions';

import axios from '../helpers/APIHelper';
Expand Down Expand Up @@ -157,12 +160,11 @@ export const addOrUpdateConversation = ({ conversation }) => async (dispatch, ge
// Check conversation is already exists or not
const [conversationExists] = payload.filter((c) => c.id === conversation.id);
let updatedConversations = payload;
if (conversationExists) {
updatedConversations = payload.filter((c) => c.id !== conversation.id);
} else {

if (!conversationExists) {
updatedConversations.unshift(conversation);
dispatch({ type: UPDATE_CONVERSATION, payload: updatedConversations });
}
dispatch({ type: UPDATE_CONVERSATION, payload: updatedConversations });
}

dispatch(getAllNotifications({ pageNo: 1 }));
Expand Down Expand Up @@ -301,16 +303,14 @@ export const markMessagesAsRead = ({ conversationId }) => async (dispatch, getSt
dispatch({ type: MARK_MESSAGES_AS_READ });
try {
const apiUrl = `conversations/${conversationId}/update_last_seen`;
const agent_last_seen_at = new Date().getTime();
const response = await axios.post(apiUrl, {
agent_last_seen_at,
});

const response = await axios.post(apiUrl);
dispatch({
type: MARK_MESSAGES_AS_READ_SUCCESS,
payload: response.data,
});

const agent_last_seen_at = new Date().getTime() / 1000;
const updatedConversations = payload.map((item) =>
item.id === conversationId ? { ...item, agent_last_seen_at } : item,
);
Expand Down Expand Up @@ -426,3 +426,19 @@ export const toggleConversationStatus = ({ conversationId }) => async (dispatch,
dispatch(getConversationDetails({ conversationId }));
} catch (error) {}
};

export const addOrUpdateActiveContacts = ({ contacts }) => async (dispatch, getState) => {
const { data } = await getState().conversation;
Object.keys(contacts).forEach((contact) => {
let conversations = lodashFilter(data.payload, { meta: { sender: { id: parseInt(contact) } } });
conversations.forEach((item) => {
const updatedConversation = item;
updatedConversation.meta.sender.availability = contacts[contact];
updatedConversation.meta.sender.availability_status = contacts[contact];
dispatch({
type: UPDATE_SINGLE_CONVERSATION,
payload: updatedConversation,
});
});
});
};
36 changes: 36 additions & 0 deletions src/actions/settings.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
import axios from 'axios';

import APIHelper from '../helpers/APIHelper';

import {
SET_URL,
SET_URL_ERROR,
SET_URL_SUCCESS,
RESET_SETTINGS,
SET_LOCALE,
GET_NOTIFICATION_SETTINGS,
GET_NOTIFICATION_SETTINGS_ERROR,
GET_NOTIFICATION_SETTINGS_SUCCESS,
UPDATE_NOTIFICATION_SETTINGS,
UPDATE_NOTIFICATION_SETTINGS_SUCCESS,
UPDATE_NOTIFICATION_SETTINGS_ERROR,
} from '../constants/actions';

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

export const getNotificationSettings = () => async (dispatch) => {
dispatch({ type: GET_NOTIFICATION_SETTINGS });
try {
const response = await APIHelper.get('notification_settings');
const { data } = response;
dispatch({
type: GET_NOTIFICATION_SETTINGS_SUCCESS,
payload: data,
});
} catch (error) {
dispatch({ type: GET_NOTIFICATION_SETTINGS_ERROR, payload: error });
}
};

export const updateNotificationSettings = (preferences) => async (dispatch) => {
dispatch({ type: UPDATE_NOTIFICATION_SETTINGS });
try {
const response = await APIHelper.patch('notification_settings', preferences);
const { data } = response;
dispatch({
type: UPDATE_NOTIFICATION_SETTINGS_SUCCESS,
payload: data,
});
} catch (error) {
dispatch({ type: UPDATE_NOTIFICATION_SETTINGS_ERROR, payload: error });
}
};

export const resetSettings = () => async (dispatch) => {
dispatch({ type: RESET_SETTINGS });
};
Binary file modified src/assets/images/emptyConversations.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
64 changes: 64 additions & 0 deletions src/components/AvailabilityItem.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import React from 'react';
import { TouchableOpacity, View } from 'react-native';
import PropTypes from 'prop-types';
import { Radio, withStyles } from '@ui-kitten/components';

import CustomText from './Text';

const styles = (theme) => ({
itemView: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
marginBottom: 8,
marginTop: 8,
},
iconView: {
flex: 1,
},
icon: {
width: 16,
height: 16,
},
textView: {
flex: 8,
},
text: {
color: theme['text-hint-color'],
fontWeight: theme['font-semi-bold'],
fontSize: theme['font-size-small'],
textAlign: 'left',
textTransform: 'capitalize',
},
radioView: {
flex: 1,
alignItems: 'flex-end',
},
});

const propTypes = {
eva: PropTypes.shape({
style: PropTypes.object,
}).isRequired,
title: PropTypes.string,
item: PropTypes.string,
onCheckedChange: PropTypes.func,
isChecked: PropTypes.bool,
};

const AvailabilityItemComponent = ({ title, item, onCheckedChange, isChecked, eva: { style } }) => (
<TouchableOpacity style={style.itemView} onPress={() => onCheckedChange({ item })}>
<View style={style.textView}>
<CustomText style={style.text}>{title}</CustomText>
</View>

<View style={style.radioView}>
<Radio checked={isChecked} onChange={() => onCheckedChange({ item })} />
</View>
</TouchableOpacity>
);

AvailabilityItemComponent.propTypes = propTypes;

const AvailabilityItem = withStyles(AvailabilityItemComponent, styles);
export default AvailabilityItem;
7 changes: 6 additions & 1 deletion src/components/ChatAttachmentItem.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,12 @@ const propTypes = {
}).isRequired,
type: PropTypes.string,
showAttachment: PropTypes.func,
attachment: PropTypes.shape({}),
attachment: PropTypes.arrayOf(
PropTypes.shape({
file_type: PropTypes.string,
data_url: PropTypes.string,
}),
),
};

const FileIcon = (style) => {
Expand Down
8 changes: 6 additions & 2 deletions src/components/ConversationItem.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const propTypes = {
sender: PropTypes.shape({
name: PropTypes.string,
thumbnail: PropTypes.string,
availability_status: PropTypes.string,
}),
channel: PropTypes.string,
}),
Expand All @@ -44,7 +45,7 @@ const ConversationItemComponent = ({

const {
meta: {
sender: { name, thumbnail },
sender: { name, thumbnail, availability_status: availabilityStatus },
channel,
},
messages,
Expand All @@ -63,6 +64,8 @@ const ConversationItemComponent = ({
conversationId: id,
});

const isActive = availabilityStatus === 'online' ? true : false;

return (
<TouchableOpacity
activeOpacity={0.5}
Expand All @@ -75,6 +78,8 @@ const ConversationItemComponent = ({
userName={name}
defaultBGColor={theme['color-primary-default']}
channel={channel}
isActive={isActive}
availabilityStatus={availabilityStatus}
/>
</View>
<View>
Expand Down Expand Up @@ -157,7 +162,6 @@ const styles = (theme) => ({
conversationUserNotActive: {
textTransform: 'capitalize',
fontSize: theme['font-size-small'],
fontWeight: theme['font-medium'],
paddingTop: 4,
color: theme['text-basic-color'],
},
Expand Down
2 changes: 1 addition & 1 deletion src/components/ImageLoader.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const ImageLoader = ({ style }) => {
};

const propTypes = {
style: PropTypes.arrayOf(PropTypes.shape({})),
style: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.shape({})), PropTypes.shape({})]),
};

const styles = () => ({});
Expand Down
Loading

0 comments on commit 4e9c7f4

Please sign in to comment.