[NEW] Omnichannel inquiry queue (#2352)
* [WIP] Omnichannel queue * Request inquiry when login * Show take inquiry queued room * Queue List as a Screen * Poc using unread badge * Prevent navigation to empty list * Remove chat from queue when taked * Fix header status on omnichannel preview room * Fix room actions view to preview queued chat * Use isOmnichannelPreview and dont show actions when is preview * Filter queue chats taken by other people * Fix room info to omnichannel preview room * Handle show Queue * Reset inquiry store when change server * Improve queue logic * Disable swipe on RoomItem when is a Queue Item * Add unreadBadge style * Move unread badge to presentation folder * Cleanup inquiry reducers * Move take saga to rocketchat function * Remove comments * Add relevant comments * Subscribe to public stream if is livechat manager or doesnt have departments * Add pt-br and improve queue empty message * Fix take when dont have view-livechat-manager permission * Add missing events * Create selector for inquiry queue * Minor fixes Co-authored-by: Diego Mello <diegolmello@gmail.com>
This commit is contained in:
parent
34824e0765
commit
ac708dd32b
|
@ -33,6 +33,7 @@ export const ROOMS = createRequestTypes('ROOMS', [
|
||||||
'CLOSE_SEARCH_HEADER'
|
'CLOSE_SEARCH_HEADER'
|
||||||
]);
|
]);
|
||||||
export const ROOM = createRequestTypes('ROOM', ['SUBSCRIBE', 'UNSUBSCRIBE', 'LEAVE', 'DELETE', 'REMOVED', 'CLOSE', 'FORWARD', 'USER_TYPING']);
|
export const ROOM = createRequestTypes('ROOM', ['SUBSCRIBE', 'UNSUBSCRIBE', 'LEAVE', 'DELETE', 'REMOVED', 'CLOSE', 'FORWARD', 'USER_TYPING']);
|
||||||
|
export const INQUIRY = createRequestTypes('INQUIRY', [...defaultTypes, 'SET_ENABLED', 'RESET', 'QUEUE_ADD', 'QUEUE_UPDATE', 'QUEUE_REMOVE']);
|
||||||
export const APP = createRequestTypes('APP', ['START', 'READY', 'INIT', 'INIT_LOCAL_SETTINGS', 'SET_MASTER_DETAIL']);
|
export const APP = createRequestTypes('APP', ['START', 'READY', 'INIT', 'INIT_LOCAL_SETTINGS', 'SET_MASTER_DETAIL']);
|
||||||
export const MESSAGES = createRequestTypes('MESSAGES', ['REPLY_BROADCAST']);
|
export const MESSAGES = createRequestTypes('MESSAGES', ['REPLY_BROADCAST']);
|
||||||
export const CREATE_CHANNEL = createRequestTypes('CREATE_CHANNEL', [...defaultTypes]);
|
export const CREATE_CHANNEL = createRequestTypes('CREATE_CHANNEL', [...defaultTypes]);
|
||||||
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
import * as types from './actionsTypes';
|
||||||
|
|
||||||
|
export function inquirySetEnabled(enabled) {
|
||||||
|
return {
|
||||||
|
type: types.INQUIRY.SET_ENABLED,
|
||||||
|
enabled
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function inquiryReset() {
|
||||||
|
return {
|
||||||
|
type: types.INQUIRY.RESET
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function inquiryQueueAdd(inquiry) {
|
||||||
|
return {
|
||||||
|
type: types.INQUIRY.QUEUE_ADD,
|
||||||
|
inquiry
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function inquiryQueueUpdate(inquiry) {
|
||||||
|
return {
|
||||||
|
type: types.INQUIRY.QUEUE_UPDATE,
|
||||||
|
inquiry
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function inquiryQueueRemove(inquiryId) {
|
||||||
|
return {
|
||||||
|
type: types.INQUIRY.QUEUE_REMOVE,
|
||||||
|
inquiryId
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function inquiryRequest() {
|
||||||
|
return {
|
||||||
|
type: types.INQUIRY.REQUEST
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function inquirySuccess(inquiries) {
|
||||||
|
return {
|
||||||
|
type: types.INQUIRY.SUCCESS,
|
||||||
|
inquiries
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function inquiryFailure(error) {
|
||||||
|
return {
|
||||||
|
type: types.INQUIRY.FAILURE,
|
||||||
|
error
|
||||||
|
};
|
||||||
|
}
|
|
@ -483,6 +483,7 @@ export default {
|
||||||
Tags: 'Tags',
|
Tags: 'Tags',
|
||||||
Take_a_photo: 'Take a photo',
|
Take_a_photo: 'Take a photo',
|
||||||
Take_a_video: 'Take a video',
|
Take_a_video: 'Take a video',
|
||||||
|
Take_it: 'Take it!',
|
||||||
tap_to_change_status: 'tap to change status',
|
tap_to_change_status: 'tap to change status',
|
||||||
Tap_to_view_servers_list: 'Tap to view servers list',
|
Tap_to_view_servers_list: 'Tap to view servers list',
|
||||||
Terms_of_Service: ' Terms of Service ',
|
Terms_of_Service: ' Terms of Service ',
|
||||||
|
@ -621,5 +622,7 @@ export default {
|
||||||
Passcode_app_locked_title: 'App locked',
|
Passcode_app_locked_title: 'App locked',
|
||||||
Passcode_app_locked_subtitle: 'Try again in {{timeLeft}} seconds',
|
Passcode_app_locked_subtitle: 'Try again in {{timeLeft}} seconds',
|
||||||
After_seconds_set_by_admin: 'After {{seconds}} seconds (set by admin)',
|
After_seconds_set_by_admin: 'After {{seconds}} seconds (set by admin)',
|
||||||
Dont_activate: 'Don\'t activate now'
|
Dont_activate: 'Don\'t activate now',
|
||||||
|
Queued_chats: 'Queued chats',
|
||||||
|
Queue_is_empty: 'Queue is empty'
|
||||||
};
|
};
|
||||||
|
|
|
@ -551,5 +551,7 @@ export default {
|
||||||
Passcode_app_locked_title: 'Aplicativo bloqueado',
|
Passcode_app_locked_title: 'Aplicativo bloqueado',
|
||||||
Passcode_app_locked_subtitle: 'Tente novamente em {{timeLeft}} segundos',
|
Passcode_app_locked_subtitle: 'Tente novamente em {{timeLeft}} segundos',
|
||||||
After_seconds_set_by_admin: 'Após {{seconds}} segundos (Configurado pelo adm)',
|
After_seconds_set_by_admin: 'Após {{seconds}} segundos (Configurado pelo adm)',
|
||||||
Dont_activate: 'Não ativar agora'
|
Dont_activate: 'Não ativar agora',
|
||||||
|
Queued_chats: 'Bate-papos na fila',
|
||||||
|
Queue_is_empty: 'A fila está vazia'
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,95 @@
|
||||||
|
import log from '../../../utils/log';
|
||||||
|
import store from '../../createStore';
|
||||||
|
import RocketChat from '../../rocketchat';
|
||||||
|
import {
|
||||||
|
inquiryRequest,
|
||||||
|
inquiryQueueAdd,
|
||||||
|
inquiryQueueUpdate,
|
||||||
|
inquiryQueueRemove
|
||||||
|
} from '../../../actions/inquiry';
|
||||||
|
|
||||||
|
const removeListener = listener => listener.stop();
|
||||||
|
|
||||||
|
let connectedListener;
|
||||||
|
let disconnectedListener;
|
||||||
|
let queueListener;
|
||||||
|
|
||||||
|
const streamTopic = 'stream-livechat-inquiry-queue-observer';
|
||||||
|
|
||||||
|
export default function subscribeInquiry() {
|
||||||
|
const handleConnection = () => {
|
||||||
|
store.dispatch(inquiryRequest());
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleQueueMessageReceived = (ddpMessage) => {
|
||||||
|
const [{ type, ...sub }] = ddpMessage.fields.args;
|
||||||
|
|
||||||
|
// added can be ignored, since it is handled by 'changed' event
|
||||||
|
if (/added/.test(type)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the sub isn't on the queue anymore
|
||||||
|
if (sub.status !== 'queued') {
|
||||||
|
// remove it from the queue
|
||||||
|
store.dispatch(inquiryQueueRemove(sub._id));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { queued } = store.getState().inquiry;
|
||||||
|
// check if this sub is on the current queue
|
||||||
|
const idx = queued.findIndex(item => item._id === sub._id);
|
||||||
|
if (idx >= 0) {
|
||||||
|
// if it is on the queue let's update
|
||||||
|
store.dispatch(inquiryQueueUpdate(sub));
|
||||||
|
} else {
|
||||||
|
// if it is not on the queue let's add
|
||||||
|
store.dispatch(inquiryQueueAdd(sub));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const stop = () => {
|
||||||
|
if (connectedListener) {
|
||||||
|
connectedListener.then(removeListener);
|
||||||
|
connectedListener = false;
|
||||||
|
}
|
||||||
|
if (disconnectedListener) {
|
||||||
|
disconnectedListener.then(removeListener);
|
||||||
|
disconnectedListener = false;
|
||||||
|
}
|
||||||
|
if (queueListener) {
|
||||||
|
queueListener.then(removeListener);
|
||||||
|
queueListener = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
connectedListener = this.sdk.onStreamData('connected', handleConnection);
|
||||||
|
disconnectedListener = this.sdk.onStreamData('close', handleConnection);
|
||||||
|
queueListener = this.sdk.onStreamData(streamTopic, handleQueueMessageReceived);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { user } = store.getState().login;
|
||||||
|
RocketChat.getAgentDepartments(user.id).then((result) => {
|
||||||
|
if (result.success) {
|
||||||
|
const { departments } = result;
|
||||||
|
|
||||||
|
if (!departments.length || RocketChat.hasRole('livechat-manager')) {
|
||||||
|
this.sdk.subscribe(streamTopic, 'public').catch(e => console.log(e));
|
||||||
|
}
|
||||||
|
|
||||||
|
const departmentIds = departments.map(({ departmentId }) => departmentId);
|
||||||
|
departmentIds.forEach((departmentId) => {
|
||||||
|
// subscribe to all departments of the agent
|
||||||
|
this.sdk.subscribe(streamTopic, `department/${ departmentId }`).catch(e => console.log(e));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
stop: () => stop()
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
log(e);
|
||||||
|
return Promise.reject();
|
||||||
|
}
|
||||||
|
}
|
|
@ -20,6 +20,7 @@ import {
|
||||||
} from '../actions/share';
|
} from '../actions/share';
|
||||||
|
|
||||||
import subscribeRooms from './methods/subscriptions/rooms';
|
import subscribeRooms from './methods/subscriptions/rooms';
|
||||||
|
import subscribeInquiry from './methods/subscriptions/inquiry';
|
||||||
import getUsersPresence, { getUserPresence, subscribeUsersPresence } from './methods/getUsersPresence';
|
import getUsersPresence, { getUserPresence, subscribeUsersPresence } from './methods/getUsersPresence';
|
||||||
|
|
||||||
import protectedFunction from './methods/helpers/protectedFunction';
|
import protectedFunction from './methods/helpers/protectedFunction';
|
||||||
|
@ -72,6 +73,15 @@ const RocketChat = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
async subscribeInquiry() {
|
||||||
|
if (!this.inquirySub) {
|
||||||
|
try {
|
||||||
|
this.inquirySub = await subscribeInquiry.call(this);
|
||||||
|
} catch (e) {
|
||||||
|
log(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
canOpenRoom,
|
canOpenRoom,
|
||||||
createChannel({
|
createChannel({
|
||||||
name, users, type, readOnly, broadcast
|
name, users, type, readOnly, broadcast
|
||||||
|
@ -203,6 +213,11 @@ const RocketChat = {
|
||||||
this.roomsSub = null;
|
this.roomsSub = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.inquirySub) {
|
||||||
|
this.inquirySub.stop();
|
||||||
|
this.inquirySub = null;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.sdk) {
|
if (this.sdk) {
|
||||||
this.sdk.disconnect();
|
this.sdk.disconnect();
|
||||||
this.sdk = null;
|
this.sdk = null;
|
||||||
|
@ -816,7 +831,7 @@ const RocketChat = {
|
||||||
},
|
},
|
||||||
getAgentDepartments(uid) {
|
getAgentDepartments(uid) {
|
||||||
// RC 2.4.0
|
// RC 2.4.0
|
||||||
return this.sdk.get(`livechat/agents/${ uid }/departments`);
|
return this.sdk.get(`livechat/agents/${ uid }/departments?enabledDepartmentsOnly=true`);
|
||||||
},
|
},
|
||||||
getCustomFields() {
|
getCustomFields() {
|
||||||
// RC 2.2.0
|
// RC 2.2.0
|
||||||
|
@ -826,6 +841,16 @@ const RocketChat = {
|
||||||
// RC 0.26.0
|
// RC 0.26.0
|
||||||
return this.methodCallWrapper('livechat:changeLivechatStatus');
|
return this.methodCallWrapper('livechat:changeLivechatStatus');
|
||||||
},
|
},
|
||||||
|
getInquiriesQueued() {
|
||||||
|
// RC 2.4.0
|
||||||
|
return this.sdk.get('livechat/inquiries.queued');
|
||||||
|
},
|
||||||
|
takeInquiry(inquiryId) {
|
||||||
|
// this inquiry is added to the db by the subscriptions stream
|
||||||
|
// and will be removed by the queue stream
|
||||||
|
// RC 2.4.0
|
||||||
|
return this.methodCallWrapper('livechat:takeInquiry', inquiryId);
|
||||||
|
},
|
||||||
|
|
||||||
getUidDirectMessage(room) {
|
getUidDirectMessage(room) {
|
||||||
const { id: userId } = reduxStore.getState().login.user;
|
const { id: userId } = reduxStore.getState().login.user;
|
||||||
|
@ -963,6 +988,14 @@ const RocketChat = {
|
||||||
// RC 0.47.0
|
// RC 0.47.0
|
||||||
return this.sdk.get('chat.getMessage', { msgId });
|
return this.sdk.get('chat.getMessage', { msgId });
|
||||||
},
|
},
|
||||||
|
hasRole(role) {
|
||||||
|
const shareUser = reduxStore.getState().share.user;
|
||||||
|
const loginUser = reduxStore.getState().login.user;
|
||||||
|
// get user roles on the server from redux
|
||||||
|
const userRoles = (shareUser?.roles || loginUser?.roles) || [];
|
||||||
|
|
||||||
|
return userRoles.indexOf(r => r === role) > -1;
|
||||||
|
},
|
||||||
async hasPermission(permissions, rid) {
|
async hasPermission(permissions, rid) {
|
||||||
const db = database.active;
|
const db = database.active;
|
||||||
const subsCollection = db.collections.get('subscriptions');
|
const subsCollection = db.collections.get('subscriptions');
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { View } from 'react-native';
|
||||||
|
|
||||||
import styles from './styles';
|
import styles from './styles';
|
||||||
import Wrapper from './Wrapper';
|
import Wrapper from './Wrapper';
|
||||||
import UnreadBadge from './UnreadBadge';
|
import UnreadBadge from '../UnreadBadge';
|
||||||
import TypeIcon from './TypeIcon';
|
import TypeIcon from './TypeIcon';
|
||||||
import LastMessage from './LastMessage';
|
import LastMessage from './LastMessage';
|
||||||
import Title from './Title';
|
import Title from './Title';
|
||||||
|
@ -41,6 +41,7 @@ const RoomItem = ({
|
||||||
groupMentions,
|
groupMentions,
|
||||||
roomUpdatedAt,
|
roomUpdatedAt,
|
||||||
testID,
|
testID,
|
||||||
|
swipeEnabled,
|
||||||
onPress,
|
onPress,
|
||||||
toggleFav,
|
toggleFav,
|
||||||
toggleRead,
|
toggleRead,
|
||||||
|
@ -59,6 +60,7 @@ const RoomItem = ({
|
||||||
type={type}
|
type={type}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
isFocused={isFocused}
|
isFocused={isFocused}
|
||||||
|
swipeEnabled={swipeEnabled}
|
||||||
>
|
>
|
||||||
<Wrapper
|
<Wrapper
|
||||||
accessibilityLabel={accessibilityLabel}
|
accessibilityLabel={accessibilityLabel}
|
||||||
|
@ -172,6 +174,7 @@ RoomItem.propTypes = {
|
||||||
userMentions: PropTypes.number,
|
userMentions: PropTypes.number,
|
||||||
groupMentions: PropTypes.number,
|
groupMentions: PropTypes.number,
|
||||||
roomUpdatedAt: PropTypes.instanceOf(Date),
|
roomUpdatedAt: PropTypes.instanceOf(Date),
|
||||||
|
swipeEnabled: PropTypes.bool,
|
||||||
toggleFav: PropTypes.func,
|
toggleFav: PropTypes.func,
|
||||||
toggleRead: PropTypes.func,
|
toggleRead: PropTypes.func,
|
||||||
onPress: PropTypes.func,
|
onPress: PropTypes.func,
|
||||||
|
@ -180,7 +183,8 @@ RoomItem.propTypes = {
|
||||||
|
|
||||||
RoomItem.defaultProps = {
|
RoomItem.defaultProps = {
|
||||||
avatarSize: 48,
|
avatarSize: 48,
|
||||||
status: 'offline'
|
status: 'offline',
|
||||||
|
swipeEnabled: true
|
||||||
};
|
};
|
||||||
|
|
||||||
export default RoomItem;
|
export default RoomItem;
|
||||||
|
|
|
@ -26,7 +26,8 @@ class Touchable extends React.Component {
|
||||||
hideChannel: PropTypes.func,
|
hideChannel: PropTypes.func,
|
||||||
children: PropTypes.element,
|
children: PropTypes.element,
|
||||||
theme: PropTypes.string,
|
theme: PropTypes.string,
|
||||||
isFocused: PropTypes.bool
|
isFocused: PropTypes.bool,
|
||||||
|
swipeEnabled: PropTypes.bool
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
@ -168,7 +169,7 @@ class Touchable extends React.Component {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
testID, isRead, width, favorite, children, theme, isFocused
|
testID, isRead, width, favorite, children, theme, isFocused, swipeEnabled
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -177,6 +178,7 @@ class Touchable extends React.Component {
|
||||||
minDeltaX={20}
|
minDeltaX={20}
|
||||||
onGestureEvent={this._onGestureEvent}
|
onGestureEvent={this._onGestureEvent}
|
||||||
onHandlerStateChange={this._onHandlerStateChange}
|
onHandlerStateChange={this._onHandlerStateChange}
|
||||||
|
enabled={swipeEnabled}
|
||||||
>
|
>
|
||||||
<Animated.View>
|
<Animated.View>
|
||||||
<LeftActions
|
<LeftActions
|
||||||
|
|
|
@ -45,7 +45,8 @@ const RoomItemContainer = React.memo(({
|
||||||
getRoomTitle,
|
getRoomTitle,
|
||||||
getRoomAvatar,
|
getRoomAvatar,
|
||||||
getIsGroupChat,
|
getIsGroupChat,
|
||||||
getIsRead
|
getIsRead,
|
||||||
|
swipeEnabled
|
||||||
}) => {
|
}) => {
|
||||||
const [, setForceUpdate] = useState(1);
|
const [, setForceUpdate] = useState(1);
|
||||||
|
|
||||||
|
@ -125,6 +126,7 @@ const RoomItemContainer = React.memo(({
|
||||||
useRealName={useRealName}
|
useRealName={useRealName}
|
||||||
unread={item.unread}
|
unread={item.unread}
|
||||||
groupMentions={item.groupMentions}
|
groupMentions={item.groupMentions}
|
||||||
|
swipeEnabled={swipeEnabled}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}, arePropsEqual);
|
}, arePropsEqual);
|
||||||
|
@ -153,7 +155,8 @@ RoomItemContainer.propTypes = {
|
||||||
getRoomTitle: PropTypes.func,
|
getRoomTitle: PropTypes.func,
|
||||||
getRoomAvatar: PropTypes.func,
|
getRoomAvatar: PropTypes.func,
|
||||||
getIsGroupChat: PropTypes.func,
|
getIsGroupChat: PropTypes.func,
|
||||||
getIsRead: PropTypes.func
|
getIsRead: PropTypes.func,
|
||||||
|
swipeEnabled: PropTypes.bool
|
||||||
};
|
};
|
||||||
|
|
||||||
RoomItemContainer.defaultProps = {
|
RoomItemContainer.defaultProps = {
|
||||||
|
@ -163,7 +166,8 @@ RoomItemContainer.defaultProps = {
|
||||||
getRoomTitle: () => 'title',
|
getRoomTitle: () => 'title',
|
||||||
getRoomAvatar: () => '',
|
getRoomAvatar: () => '',
|
||||||
getIsGroupChat: () => false,
|
getIsGroupChat: () => false,
|
||||||
getIsRead: () => false
|
getIsRead: () => false,
|
||||||
|
swipeEnabled: true
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = (state, ownProps) => {
|
const mapStateToProps = (state, ownProps) => {
|
||||||
|
|
|
@ -51,23 +51,6 @@ export default StyleSheet.create({
|
||||||
updateAlert: {
|
updateAlert: {
|
||||||
...sharedStyles.textSemibold
|
...sharedStyles.textSemibold
|
||||||
},
|
},
|
||||||
unreadNumberContainer: {
|
|
||||||
minWidth: 21,
|
|
||||||
height: 21,
|
|
||||||
paddingVertical: 3,
|
|
||||||
paddingHorizontal: 5,
|
|
||||||
borderRadius: 10.5,
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
marginLeft: 10
|
|
||||||
},
|
|
||||||
unreadText: {
|
|
||||||
overflow: 'hidden',
|
|
||||||
fontSize: 13,
|
|
||||||
...sharedStyles.textMedium,
|
|
||||||
letterSpacing: 0.56,
|
|
||||||
textAlign: 'center'
|
|
||||||
},
|
|
||||||
status: {
|
status: {
|
||||||
marginLeft: 4,
|
marginLeft: 4,
|
||||||
marginRight: 7,
|
marginRight: 7,
|
||||||
|
|
|
@ -1,12 +1,32 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { View, Text } from 'react-native';
|
import { View, Text, StyleSheet } from 'react-native';
|
||||||
|
|
||||||
import styles from './styles';
|
import sharedStyles from '../views/Styles';
|
||||||
import { themes } from '../../constants/colors';
|
import { themes } from '../constants/colors';
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
unreadNumberContainer: {
|
||||||
|
minWidth: 21,
|
||||||
|
height: 21,
|
||||||
|
paddingVertical: 3,
|
||||||
|
paddingHorizontal: 5,
|
||||||
|
borderRadius: 10.5,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
marginLeft: 10
|
||||||
|
},
|
||||||
|
unreadText: {
|
||||||
|
overflow: 'hidden',
|
||||||
|
fontSize: 13,
|
||||||
|
...sharedStyles.textMedium,
|
||||||
|
letterSpacing: 0.56,
|
||||||
|
textAlign: 'center'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const UnreadBadge = React.memo(({
|
const UnreadBadge = React.memo(({
|
||||||
theme, unread, userMentions, groupMentions
|
theme, unread, userMentions, groupMentions, style
|
||||||
}) => {
|
}) => {
|
||||||
if (!unread || unread <= 0) {
|
if (!unread || unread <= 0) {
|
||||||
return;
|
return;
|
||||||
|
@ -27,7 +47,8 @@ const UnreadBadge = React.memo(({
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
styles.unreadNumberContainer,
|
styles.unreadNumberContainer,
|
||||||
{ backgroundColor }
|
{ backgroundColor },
|
||||||
|
style
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Text
|
<Text
|
||||||
|
@ -35,7 +56,7 @@ const UnreadBadge = React.memo(({
|
||||||
styles.unreadText,
|
styles.unreadText,
|
||||||
{ color }
|
{ color }
|
||||||
]}
|
]}
|
||||||
>{ unread }
|
>{unread}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
@ -45,7 +66,8 @@ UnreadBadge.propTypes = {
|
||||||
theme: PropTypes.string,
|
theme: PropTypes.string,
|
||||||
unread: PropTypes.number,
|
unread: PropTypes.number,
|
||||||
userMentions: PropTypes.number,
|
userMentions: PropTypes.number,
|
||||||
groupMentions: PropTypes.number
|
groupMentions: PropTypes.number,
|
||||||
|
style: PropTypes.object
|
||||||
};
|
};
|
||||||
|
|
||||||
export default UnreadBadge;
|
export default UnreadBadge;
|
|
@ -16,6 +16,7 @@ import activeUsers from './activeUsers';
|
||||||
import usersTyping from './usersTyping';
|
import usersTyping from './usersTyping';
|
||||||
import inviteLinks from './inviteLinks';
|
import inviteLinks from './inviteLinks';
|
||||||
import createDiscussion from './createDiscussion';
|
import createDiscussion from './createDiscussion';
|
||||||
|
import inquiry from './inquiry';
|
||||||
|
|
||||||
export default combineReducers({
|
export default combineReducers({
|
||||||
settings,
|
settings,
|
||||||
|
@ -34,5 +35,6 @@ export default combineReducers({
|
||||||
activeUsers,
|
activeUsers,
|
||||||
usersTyping,
|
usersTyping,
|
||||||
inviteLinks,
|
inviteLinks,
|
||||||
createDiscussion
|
createDiscussion,
|
||||||
|
inquiry
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,51 @@
|
||||||
|
import { INQUIRY } from '../actions/actionsTypes';
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
enabled: false,
|
||||||
|
queued: [],
|
||||||
|
error: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function inquiry(state = initialState, action) {
|
||||||
|
switch (action.type) {
|
||||||
|
case INQUIRY.SUCCESS:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
queued: action.inquiries
|
||||||
|
};
|
||||||
|
case INQUIRY.FAILURE:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
error: action.error
|
||||||
|
};
|
||||||
|
case INQUIRY.SET_ENABLED:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
enabled: action.enabled
|
||||||
|
};
|
||||||
|
case INQUIRY.QUEUE_ADD:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
queued: [...state.queued, action.inquiry]
|
||||||
|
};
|
||||||
|
case INQUIRY.QUEUE_UPDATE:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
queued: state.queued.map((item) => {
|
||||||
|
if (item._id === action.inquiry._id) {
|
||||||
|
return action.inquiry;
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
})
|
||||||
|
};
|
||||||
|
case INQUIRY.QUEUE_REMOVE:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
queued: state.queued.filter(({ _id }) => _id !== action.inquiryId)
|
||||||
|
};
|
||||||
|
case INQUIRY.RESET:
|
||||||
|
return initialState;
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,6 +10,7 @@ import state from './state';
|
||||||
import deepLinking from './deepLinking';
|
import deepLinking from './deepLinking';
|
||||||
import inviteLinks from './inviteLinks';
|
import inviteLinks from './inviteLinks';
|
||||||
import createDiscussion from './createDiscussion';
|
import createDiscussion from './createDiscussion';
|
||||||
|
import inquiry from './inquiry';
|
||||||
|
|
||||||
const root = function* root() {
|
const root = function* root() {
|
||||||
yield all([
|
yield all([
|
||||||
|
@ -23,7 +24,8 @@ const root = function* root() {
|
||||||
state(),
|
state(),
|
||||||
deepLinking(),
|
deepLinking(),
|
||||||
inviteLinks(),
|
inviteLinks(),
|
||||||
createDiscussion()
|
createDiscussion(),
|
||||||
|
inquiry()
|
||||||
]);
|
]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,38 @@
|
||||||
|
import { put, takeLatest, select } from 'redux-saga/effects';
|
||||||
|
|
||||||
|
import * as types from '../actions/actionsTypes';
|
||||||
|
import RocketChat from '../lib/rocketchat';
|
||||||
|
import { inquirySuccess, inquiryFailure, inquirySetEnabled } from '../actions/inquiry';
|
||||||
|
|
||||||
|
const handleRequest = function* handleRequest() {
|
||||||
|
try {
|
||||||
|
const routingConfig = yield RocketChat.getRoutingConfig();
|
||||||
|
const statusLivechat = yield select(state => state.login.user.statusLivechat);
|
||||||
|
// if routingConfig showQueue is enabled and omnichannel is enabled
|
||||||
|
const showQueue = routingConfig.showQueue && statusLivechat === 'available';
|
||||||
|
|
||||||
|
if (showQueue) {
|
||||||
|
// get all the current chats on the queue
|
||||||
|
const result = yield RocketChat.getInquiriesQueued();
|
||||||
|
if (result.success) {
|
||||||
|
const { inquiries } = result;
|
||||||
|
|
||||||
|
// subscribe to inquiry queue changes
|
||||||
|
RocketChat.subscribeInquiry();
|
||||||
|
|
||||||
|
// put request result on redux state
|
||||||
|
yield put(inquirySuccess(inquiries));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// set enabled to know if we should show the queue button
|
||||||
|
yield put(inquirySetEnabled(showQueue));
|
||||||
|
} catch (e) {
|
||||||
|
yield put(inquiryFailure(e));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const root = function* root() {
|
||||||
|
yield takeLatest(types.INQUIRY.REQUEST, handleRequest);
|
||||||
|
};
|
||||||
|
export default root;
|
|
@ -15,6 +15,7 @@ import {
|
||||||
loginFailure, loginSuccess, setUser, logout
|
loginFailure, loginSuccess, setUser, logout
|
||||||
} from '../actions/login';
|
} from '../actions/login';
|
||||||
import { roomsRequest } from '../actions/rooms';
|
import { roomsRequest } from '../actions/rooms';
|
||||||
|
import { inquiryRequest } from '../actions/inquiry';
|
||||||
import { toMomentLocale } from '../utils/moment';
|
import { toMomentLocale } from '../utils/moment';
|
||||||
import RocketChat from '../lib/rocketchat';
|
import RocketChat from '../lib/rocketchat';
|
||||||
import log, { logEvent, events } from '../utils/log';
|
import log, { logEvent, events } from '../utils/log';
|
||||||
|
@ -93,6 +94,7 @@ const handleLoginSuccess = function* handleLoginSuccess({ user }) {
|
||||||
|
|
||||||
const server = yield select(getServer);
|
const server = yield select(getServer);
|
||||||
yield put(roomsRequest());
|
yield put(roomsRequest());
|
||||||
|
yield put(inquiryRequest());
|
||||||
yield fork(fetchPermissions);
|
yield fork(fetchPermissions);
|
||||||
yield fork(fetchCustomEmojis);
|
yield fork(fetchCustomEmojis);
|
||||||
yield fork(fetchRoles);
|
yield fork(fetchRoles);
|
||||||
|
@ -206,6 +208,10 @@ const handleSetUser = function* handleSetUser({ user }) {
|
||||||
const userId = yield select(state => state.login.user.id);
|
const userId = yield select(state => state.login.user.id);
|
||||||
yield put(setActiveUsers({ [userId]: user }));
|
yield put(setActiveUsers({ [userId]: user }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (user && user.statusLivechat) {
|
||||||
|
yield put(inquiryRequest());
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const root = function* root() {
|
const root = function* root() {
|
||||||
|
|
|
@ -19,6 +19,7 @@ import I18n from '../i18n';
|
||||||
import { SERVERS, TOKEN, SERVER_URL } from '../constants/userDefaults';
|
import { SERVERS, TOKEN, SERVER_URL } from '../constants/userDefaults';
|
||||||
import { BASIC_AUTH_KEY, setBasicAuth } from '../utils/fetch';
|
import { BASIC_AUTH_KEY, setBasicAuth } from '../utils/fetch';
|
||||||
import { appStart, ROOT_INSIDE, ROOT_OUTSIDE } from '../actions/app';
|
import { appStart, ROOT_INSIDE, ROOT_OUTSIDE } from '../actions/app';
|
||||||
|
import { inquiryReset } from '../actions/inquiry';
|
||||||
|
|
||||||
const getServerInfo = function* getServerInfo({ server, raiseError = true }) {
|
const getServerInfo = function* getServerInfo({ server, raiseError = true }) {
|
||||||
try {
|
try {
|
||||||
|
@ -65,6 +66,7 @@ const getServerInfo = function* getServerInfo({ server, raiseError = true }) {
|
||||||
|
|
||||||
const handleSelectServer = function* handleSelectServer({ server, version, fetchVersion }) {
|
const handleSelectServer = function* handleSelectServer({ server, version, fetchVersion }) {
|
||||||
try {
|
try {
|
||||||
|
yield put(inquiryReset());
|
||||||
const serversDB = database.servers;
|
const serversDB = database.servers;
|
||||||
yield RNUserDefaults.set('currentServer', server);
|
yield RNUserDefaults.set('currentServer', server);
|
||||||
const userId = yield RNUserDefaults.get(`${ RocketChat.TOKEN_KEY }-${ server }`);
|
const userId = yield RNUserDefaults.get(`${ RocketChat.TOKEN_KEY }-${ server }`);
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
|
||||||
|
const getInquiryQueue = state => state.inquiry.queued;
|
||||||
|
|
||||||
|
export const getInquiryQueueSelector = createSelector(
|
||||||
|
[getInquiryQueue],
|
||||||
|
queue => queue
|
||||||
|
);
|
|
@ -30,6 +30,7 @@ import PickerView from '../views/PickerView';
|
||||||
import ThreadMessagesView from '../views/ThreadMessagesView';
|
import ThreadMessagesView from '../views/ThreadMessagesView';
|
||||||
import MarkdownTableView from '../views/MarkdownTableView';
|
import MarkdownTableView from '../views/MarkdownTableView';
|
||||||
import ReadReceiptsView from '../views/ReadReceiptView';
|
import ReadReceiptsView from '../views/ReadReceiptView';
|
||||||
|
import QueueListView from '../views/QueueListView';
|
||||||
|
|
||||||
// Profile Stack
|
// Profile Stack
|
||||||
import ProfileView from '../views/ProfileView';
|
import ProfileView from '../views/ProfileView';
|
||||||
|
@ -163,6 +164,11 @@ const ChatsStackNavigator = () => {
|
||||||
component={ReadReceiptsView}
|
component={ReadReceiptsView}
|
||||||
options={ReadReceiptsView.navigationOptions}
|
options={ReadReceiptsView.navigationOptions}
|
||||||
/>
|
/>
|
||||||
|
<ChatsStack.Screen
|
||||||
|
name='QueueListView'
|
||||||
|
component={QueueListView}
|
||||||
|
options={QueueListView.navigationOptions}
|
||||||
|
/>
|
||||||
</ChatsStack.Navigator>
|
</ChatsStack.Navigator>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -41,6 +41,7 @@ import ScreenLockConfigView from '../../views/ScreenLockConfigView';
|
||||||
import AdminPanelView from '../../views/AdminPanelView';
|
import AdminPanelView from '../../views/AdminPanelView';
|
||||||
import NewMessageView from '../../views/NewMessageView';
|
import NewMessageView from '../../views/NewMessageView';
|
||||||
import CreateChannelView from '../../views/CreateChannelView';
|
import CreateChannelView from '../../views/CreateChannelView';
|
||||||
|
import QueueListView from '../../views/QueueListView';
|
||||||
|
|
||||||
// InsideStackNavigator
|
// InsideStackNavigator
|
||||||
import AttachmentView from '../../views/AttachmentView';
|
import AttachmentView from '../../views/AttachmentView';
|
||||||
|
@ -150,6 +151,11 @@ const ModalStackNavigator = React.memo(({ navigation }) => {
|
||||||
component={DirectoryView}
|
component={DirectoryView}
|
||||||
options={props => DirectoryView.navigationOptions({ ...props, isMasterDetail: true })}
|
options={props => DirectoryView.navigationOptions({ ...props, isMasterDetail: true })}
|
||||||
/>
|
/>
|
||||||
|
<ModalStack.Screen
|
||||||
|
name='QueueListView'
|
||||||
|
component={QueueListView}
|
||||||
|
options={props => QueueListView.navigationOptions({ ...props, isMasterDetail: true })}
|
||||||
|
/>
|
||||||
<ModalStack.Screen
|
<ModalStack.Screen
|
||||||
name='NotificationPrefView'
|
name='NotificationPrefView'
|
||||||
component={NotificationPrefView}
|
component={NotificationPrefView}
|
||||||
|
|
|
@ -60,6 +60,7 @@ export default {
|
||||||
RL_NAVIGATE_TO_NEW_MSG: 'rl_navigate_to_new_msg',
|
RL_NAVIGATE_TO_NEW_MSG: 'rl_navigate_to_new_msg',
|
||||||
RL_SEARCH: 'rl_search',
|
RL_SEARCH: 'rl_search',
|
||||||
RL_NAVIGATE_TO_DIRECTORY: 'rl_navigate_to_directory',
|
RL_NAVIGATE_TO_DIRECTORY: 'rl_navigate_to_directory',
|
||||||
|
RL_GO_QUEUE: 'rl_go_queue',
|
||||||
RL_GO_TO_ROOM: 'rl_go_to_room',
|
RL_GO_TO_ROOM: 'rl_go_to_room',
|
||||||
RL_FAVORITE_CHANNEL: 'rl_favorite_channel',
|
RL_FAVORITE_CHANNEL: 'rl_favorite_channel',
|
||||||
RL_UNFAVORITE_CHANNEL: 'rl_unfavorite_channel',
|
RL_UNFAVORITE_CHANNEL: 'rl_unfavorite_channel',
|
||||||
|
@ -77,6 +78,8 @@ export default {
|
||||||
RL_GROUP_CHANNELS_BY_FAVORITE: 'rl_group_channels_by_favorite',
|
RL_GROUP_CHANNELS_BY_FAVORITE: 'rl_group_channels_by_favorite',
|
||||||
RL_GROUP_CHANNELS_BY_UNREAD: 'rl_group_channels_by_unread',
|
RL_GROUP_CHANNELS_BY_UNREAD: 'rl_group_channels_by_unread',
|
||||||
|
|
||||||
|
QL_GO_ROOM: 'ql_go_room',
|
||||||
|
|
||||||
// DIRECTORY VIEW
|
// DIRECTORY VIEW
|
||||||
DIRECTORY_SEARCH_USERS: 'directory_search_users',
|
DIRECTORY_SEARCH_USERS: 'directory_search_users',
|
||||||
DIRECTORY_SEARCH_CHANNELS: 'directory_search_channels',
|
DIRECTORY_SEARCH_CHANNELS: 'directory_search_channels',
|
||||||
|
@ -193,5 +196,6 @@ export default {
|
||||||
ROOM_MSG_ACTION_PIN_F: 'room_msg_action_pin_f',
|
ROOM_MSG_ACTION_PIN_F: 'room_msg_action_pin_f',
|
||||||
ROOM_MSG_ACTION_REACTION: 'room_msg_action_reaction',
|
ROOM_MSG_ACTION_REACTION: 'room_msg_action_reaction',
|
||||||
ROOM_MSG_ACTION_REPORT: 'room_msg_action_report',
|
ROOM_MSG_ACTION_REPORT: 'room_msg_action_report',
|
||||||
ROOM_MSG_ACTION_REPORT_F: 'room_msg_action_report_f'
|
ROOM_MSG_ACTION_REPORT_F: 'room_msg_action_report_f',
|
||||||
|
ROOM_JOIN: 'room_join'
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,160 @@
|
||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { FlatList } from 'react-native';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import isEqual from 'react-fast-compare';
|
||||||
|
|
||||||
|
import I18n from '../i18n';
|
||||||
|
import RoomItem, { ROW_HEIGHT } from '../presentation/RoomItem';
|
||||||
|
import { MAX_SIDEBAR_WIDTH } from '../constants/tablet';
|
||||||
|
import { isTablet, isIOS } from '../utils/deviceInfo';
|
||||||
|
import { getUserSelector } from '../selectors/login';
|
||||||
|
import { withTheme } from '../theme';
|
||||||
|
import { withDimensions } from '../dimensions';
|
||||||
|
import SafeAreaView from '../containers/SafeAreaView';
|
||||||
|
import { themes } from '../constants/colors';
|
||||||
|
import StatusBar from '../containers/StatusBar';
|
||||||
|
import { goRoom } from '../utils/goRoom';
|
||||||
|
import { CloseModalButton } from '../containers/HeaderButton';
|
||||||
|
import RocketChat from '../lib/rocketchat';
|
||||||
|
import { logEvent, events } from '../utils/log';
|
||||||
|
import { getInquiryQueueSelector } from '../selectors/inquiry';
|
||||||
|
|
||||||
|
const INITIAL_NUM_TO_RENDER = isTablet ? 20 : 12;
|
||||||
|
const getItemLayout = (data, index) => ({
|
||||||
|
length: ROW_HEIGHT,
|
||||||
|
offset: ROW_HEIGHT * index,
|
||||||
|
index
|
||||||
|
});
|
||||||
|
const keyExtractor = item => item.rid;
|
||||||
|
|
||||||
|
class QueueListView extends React.Component {
|
||||||
|
static navigationOptions = ({ navigation, isMasterDetail }) => {
|
||||||
|
const options = {
|
||||||
|
title: I18n.t('Queued_chats')
|
||||||
|
};
|
||||||
|
if (isMasterDetail) {
|
||||||
|
options.headerLeft = () => <CloseModalButton navigation={navigation} testID='directory-view-close' />;
|
||||||
|
}
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
static propTypes = {
|
||||||
|
user: PropTypes.shape({
|
||||||
|
id: PropTypes.string,
|
||||||
|
username: PropTypes.string,
|
||||||
|
token: PropTypes.string
|
||||||
|
}),
|
||||||
|
isMasterDetail: PropTypes.bool,
|
||||||
|
width: PropTypes.number,
|
||||||
|
queued: PropTypes.array,
|
||||||
|
server: PropTypes.string,
|
||||||
|
useRealName: PropTypes.bool,
|
||||||
|
navigation: PropTypes.object,
|
||||||
|
theme: PropTypes.string
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldComponentUpdate(nextProps) {
|
||||||
|
const { queued } = this.props;
|
||||||
|
if (!isEqual(nextProps.queued, queued)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
onPressItem = (item = {}) => {
|
||||||
|
logEvent(events.QL_GO_ROOM);
|
||||||
|
const { navigation, isMasterDetail } = this.props;
|
||||||
|
if (isMasterDetail) {
|
||||||
|
navigation.navigate('DrawerNavigator');
|
||||||
|
} else {
|
||||||
|
navigation.navigate('RoomsListView');
|
||||||
|
}
|
||||||
|
|
||||||
|
goRoom({
|
||||||
|
item: {
|
||||||
|
...item,
|
||||||
|
// we're calling v as visitor on our mergeSubscriptionsRooms
|
||||||
|
visitor: item.v
|
||||||
|
},
|
||||||
|
isMasterDetail
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
getRoomTitle = item => RocketChat.getRoomTitle(item)
|
||||||
|
|
||||||
|
getRoomAvatar = item => RocketChat.getRoomAvatar(item)
|
||||||
|
|
||||||
|
getUidDirectMessage = room => RocketChat.getUidDirectMessage(room)
|
||||||
|
|
||||||
|
renderItem = ({ item }) => {
|
||||||
|
const {
|
||||||
|
user: {
|
||||||
|
id: userId,
|
||||||
|
username,
|
||||||
|
token
|
||||||
|
},
|
||||||
|
server,
|
||||||
|
useRealName,
|
||||||
|
theme,
|
||||||
|
isMasterDetail,
|
||||||
|
width
|
||||||
|
} = this.props;
|
||||||
|
const id = this.getUidDirectMessage(item);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RoomItem
|
||||||
|
item={item}
|
||||||
|
theme={theme}
|
||||||
|
id={id}
|
||||||
|
type={item.t}
|
||||||
|
userId={userId}
|
||||||
|
username={username}
|
||||||
|
token={token}
|
||||||
|
baseUrl={server}
|
||||||
|
onPress={this.onPressItem}
|
||||||
|
testID={`queue-list-view-item-${ item.name }`}
|
||||||
|
width={isMasterDetail ? MAX_SIDEBAR_WIDTH : width}
|
||||||
|
useRealName={useRealName}
|
||||||
|
getRoomTitle={this.getRoomTitle}
|
||||||
|
getRoomAvatar={this.getRoomAvatar}
|
||||||
|
visitor={item.v}
|
||||||
|
swipeEnabled={false}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { queued, theme } = this.props;
|
||||||
|
return (
|
||||||
|
<SafeAreaView testID='queue-list-view' theme={theme} style={{ backgroundColor: themes[theme].backgroundColor }}>
|
||||||
|
<StatusBar theme={theme} />
|
||||||
|
<FlatList
|
||||||
|
ref={this.getScrollRef}
|
||||||
|
data={queued}
|
||||||
|
extraData={queued}
|
||||||
|
keyExtractor={keyExtractor}
|
||||||
|
style={{ backgroundColor: themes[theme].backgroundColor }}
|
||||||
|
renderItem={this.renderItem}
|
||||||
|
getItemLayout={getItemLayout}
|
||||||
|
removeClippedSubviews={isIOS}
|
||||||
|
keyboardShouldPersistTaps='always'
|
||||||
|
initialNumToRender={INITIAL_NUM_TO_RENDER}
|
||||||
|
windowSize={9}
|
||||||
|
onEndReached={this.onEndReached}
|
||||||
|
onEndReachedThreshold={0.5}
|
||||||
|
/>
|
||||||
|
</SafeAreaView>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapStateToProps = state => ({
|
||||||
|
user: getUserSelector(state),
|
||||||
|
isMasterDetail: state.app.isMasterDetail,
|
||||||
|
server: state.server.server,
|
||||||
|
useRealName: state.settings.UI_Use_Real_Name,
|
||||||
|
queued: getInquiryQueueSelector(state)
|
||||||
|
});
|
||||||
|
export default connect(mapStateToProps)(withDimensions(withTheme(QueueListView)));
|
|
@ -91,7 +91,7 @@ class RoomActionsView extends React.Component {
|
||||||
this.mounted = true;
|
this.mounted = true;
|
||||||
const { room, member } = this.state;
|
const { room, member } = this.state;
|
||||||
if (room.rid) {
|
if (room.rid) {
|
||||||
if (!room.id) {
|
if (!room.id && !this.isOmnichannelPreview) {
|
||||||
try {
|
try {
|
||||||
const result = await RocketChat.getChannelInfo(room.rid);
|
const result = await RocketChat.getChannelInfo(room.rid);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
|
@ -135,6 +135,11 @@ class RoomActionsView extends React.Component {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get isOmnichannelPreview() {
|
||||||
|
const { room } = this.state;
|
||||||
|
return room.t === 'l' && room.status === 'queued';
|
||||||
|
}
|
||||||
|
|
||||||
onPressTouchable = (item) => {
|
onPressTouchable = (item) => {
|
||||||
if (item.route) {
|
if (item.route) {
|
||||||
const { navigation } = this.props;
|
const { navigation } = this.props;
|
||||||
|
@ -407,36 +412,38 @@ class RoomActionsView extends React.Component {
|
||||||
} else if (t === 'l') {
|
} else if (t === 'l') {
|
||||||
sections[2].data = [];
|
sections[2].data = [];
|
||||||
|
|
||||||
sections[2].data.push({
|
if (!this.isOmnichannelPreview) {
|
||||||
icon: 'close',
|
|
||||||
name: I18n.t('Close'),
|
|
||||||
event: this.closeLivechat
|
|
||||||
});
|
|
||||||
|
|
||||||
if (canForwardGuest) {
|
|
||||||
sections[2].data.push({
|
sections[2].data.push({
|
||||||
icon: 'user-forward',
|
icon: 'close',
|
||||||
name: I18n.t('Forward'),
|
name: I18n.t('Close'),
|
||||||
route: 'ForwardLivechatView',
|
event: this.closeLivechat
|
||||||
|
});
|
||||||
|
|
||||||
|
if (canForwardGuest) {
|
||||||
|
sections[2].data.push({
|
||||||
|
icon: 'user-forward',
|
||||||
|
name: I18n.t('Forward'),
|
||||||
|
route: 'ForwardLivechatView',
|
||||||
|
params: { rid }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (canReturnQueue) {
|
||||||
|
sections[2].data.push({
|
||||||
|
icon: 'undo',
|
||||||
|
name: I18n.t('Return'),
|
||||||
|
event: this.returnLivechat
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
sections[2].data.push({
|
||||||
|
icon: 'history',
|
||||||
|
name: I18n.t('Navigation_history'),
|
||||||
|
route: 'VisitorNavigationView',
|
||||||
params: { rid }
|
params: { rid }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (canReturnQueue) {
|
|
||||||
sections[2].data.push({
|
|
||||||
icon: 'undo',
|
|
||||||
name: I18n.t('Return'),
|
|
||||||
event: this.returnLivechat
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
sections[2].data.push({
|
|
||||||
icon: 'history',
|
|
||||||
name: I18n.t('Navigation_history'),
|
|
||||||
route: 'VisitorNavigationView',
|
|
||||||
params: { rid }
|
|
||||||
});
|
|
||||||
|
|
||||||
sections.push({
|
sections.push({
|
||||||
data: [notificationsAction],
|
data: [notificationsAction],
|
||||||
renderItem: this.renderItem
|
renderItem: this.renderItem
|
||||||
|
|
|
@ -44,7 +44,7 @@ const Livechat = ({ room, roomUser, theme }) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => { getRoom(); }, []);
|
useEffect(() => { getRoom(); }, [room]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -195,6 +195,7 @@ class RoomInfoView extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
loadRoom = async() => {
|
loadRoom = async() => {
|
||||||
|
const { room: roomState } = this.state;
|
||||||
const { route } = this.props;
|
const { route } = this.props;
|
||||||
let room = route.params?.room;
|
let room = route.params?.room;
|
||||||
if (room && room.observe) {
|
if (room && room.observe) {
|
||||||
|
@ -208,7 +209,7 @@ class RoomInfoView extends React.Component {
|
||||||
const result = await RocketChat.getRoomInfo(this.rid);
|
const result = await RocketChat.getRoomInfo(this.rid);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
({ room } = result);
|
({ room } = result);
|
||||||
this.setState({ room });
|
this.setState({ room: { ...roomState, ...room } });
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log(e);
|
log(e);
|
||||||
|
|
|
@ -282,6 +282,11 @@ class RoomView extends React.Component {
|
||||||
console.countReset(`${ this.constructor.name }.render calls`);
|
console.countReset(`${ this.constructor.name }.render calls`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get isOmnichannel() {
|
||||||
|
const { room } = this.state;
|
||||||
|
return room.t === 'l';
|
||||||
|
}
|
||||||
|
|
||||||
setHeader = () => {
|
setHeader = () => {
|
||||||
const { room, unreadsCount, roomUserId: stateRoomUserId } = this.state;
|
const { room, unreadsCount, roomUserId: stateRoomUserId } = this.state;
|
||||||
const {
|
const {
|
||||||
|
@ -678,8 +683,15 @@ class RoomView extends React.Component {
|
||||||
setLastOpen = lastOpen => this.setState({ lastOpen });
|
setLastOpen = lastOpen => this.setState({ lastOpen });
|
||||||
|
|
||||||
joinRoom = async() => {
|
joinRoom = async() => {
|
||||||
|
logEvent(events.ROOM_JOIN);
|
||||||
try {
|
try {
|
||||||
await RocketChat.joinRoom(this.rid, this.t);
|
const { room } = this.state;
|
||||||
|
|
||||||
|
if (this.isOmnichannel) {
|
||||||
|
await RocketChat.takeInquiry(room._id);
|
||||||
|
} else {
|
||||||
|
await RocketChat.joinRoom(this.rid, this.t);
|
||||||
|
}
|
||||||
this.internalSetState({
|
this.internalSetState({
|
||||||
joined: true
|
joined: true
|
||||||
});
|
});
|
||||||
|
@ -896,7 +908,7 @@ class RoomView extends React.Component {
|
||||||
style={[styles.joinRoomButton, { backgroundColor: themes[theme].actionTintColor }]}
|
style={[styles.joinRoomButton, { backgroundColor: themes[theme].actionTintColor }]}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
>
|
>
|
||||||
<Text style={[styles.joinRoomText, { color: themes[theme].buttonText }]} testID='room-view-join-button'>{I18n.t('Join')}</Text>
|
<Text style={[styles.joinRoomText, { color: themes[theme].buttonText }]} testID='room-view-join-button'>{I18n.t(this.isOmnichannel ? 'Take_it' : 'Join')}</Text>
|
||||||
</Touch>
|
</Touch>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|
|
@ -0,0 +1,49 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { View, Text, StyleSheet } from 'react-native';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import Touch from '../../../utils/touch';
|
||||||
|
import I18n from '../../../i18n';
|
||||||
|
import styles from '../styles';
|
||||||
|
import { themes } from '../../../constants/colors';
|
||||||
|
import { withTheme } from '../../../theme';
|
||||||
|
import UnreadBadge from '../../../presentation/UnreadBadge';
|
||||||
|
|
||||||
|
const Queue = React.memo(({
|
||||||
|
searching, goQueue, queueSize, inquiryEnabled, theme
|
||||||
|
}) => {
|
||||||
|
if (searching > 0 || !inquiryEnabled) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Touch
|
||||||
|
onPress={goQueue}
|
||||||
|
theme={theme}
|
||||||
|
style={{ backgroundColor: themes[theme].headerSecondaryBackground }}
|
||||||
|
>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
styles.dropdownContainerHeader,
|
||||||
|
{ borderBottomWidth: StyleSheet.hairlineWidth, borderColor: themes[theme].separatorColor }
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<Text style={[styles.sortToggleText, { color: themes[theme].auxiliaryText }]}>{I18n.t('Queued_chats')}</Text>
|
||||||
|
<UnreadBadge
|
||||||
|
style={styles.sortIcon}
|
||||||
|
unread={queueSize}
|
||||||
|
theme={theme}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</Touch>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
Queue.propTypes = {
|
||||||
|
searching: PropTypes.bool,
|
||||||
|
goQueue: PropTypes.func,
|
||||||
|
queueSize: PropTypes.number,
|
||||||
|
inquiryEnabled: PropTypes.bool,
|
||||||
|
theme: PropTypes.string
|
||||||
|
};
|
||||||
|
|
||||||
|
export default withTheme(Queue);
|
|
@ -1,6 +1,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import Queue from './Queue';
|
||||||
import Directory from './Directory';
|
import Directory from './Directory';
|
||||||
import Sort from './Sort';
|
import Sort from './Sort';
|
||||||
|
|
||||||
|
@ -8,11 +9,15 @@ const ListHeader = React.memo(({
|
||||||
searching,
|
searching,
|
||||||
sortBy,
|
sortBy,
|
||||||
toggleSort,
|
toggleSort,
|
||||||
goDirectory
|
goDirectory,
|
||||||
|
goQueue,
|
||||||
|
queueSize,
|
||||||
|
inquiryEnabled
|
||||||
}) => (
|
}) => (
|
||||||
<>
|
<>
|
||||||
<Directory searching={searching} goDirectory={goDirectory} />
|
<Directory searching={searching} goDirectory={goDirectory} />
|
||||||
<Sort searching={searching} sortBy={sortBy} toggleSort={toggleSort} />
|
<Sort searching={searching} sortBy={sortBy} toggleSort={toggleSort} />
|
||||||
|
<Queue searching={searching} goQueue={goQueue} queueSize={queueSize} inquiryEnabled={inquiryEnabled} />
|
||||||
</>
|
</>
|
||||||
));
|
));
|
||||||
|
|
||||||
|
@ -20,7 +25,10 @@ ListHeader.propTypes = {
|
||||||
searching: PropTypes.bool,
|
searching: PropTypes.bool,
|
||||||
sortBy: PropTypes.string,
|
sortBy: PropTypes.string,
|
||||||
toggleSort: PropTypes.func,
|
toggleSort: PropTypes.func,
|
||||||
goDirectory: PropTypes.func
|
goDirectory: PropTypes.func,
|
||||||
|
goQueue: PropTypes.func,
|
||||||
|
queueSize: PropTypes.number,
|
||||||
|
inquiryEnabled: PropTypes.bool
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ListHeader;
|
export default ListHeader;
|
||||||
|
|
|
@ -62,6 +62,8 @@ import { goRoom } from '../../utils/goRoom';
|
||||||
import SafeAreaView from '../../containers/SafeAreaView';
|
import SafeAreaView from '../../containers/SafeAreaView';
|
||||||
import Header, { getHeaderTitlePosition } from '../../containers/Header';
|
import Header, { getHeaderTitlePosition } from '../../containers/Header';
|
||||||
import { withDimensions } from '../../dimensions';
|
import { withDimensions } from '../../dimensions';
|
||||||
|
import { showErrorAlert } from '../../utils/info';
|
||||||
|
import { getInquiryQueueSelector } from '../../selectors/inquiry';
|
||||||
|
|
||||||
const INITIAL_NUM_TO_RENDER = isTablet ? 20 : 12;
|
const INITIAL_NUM_TO_RENDER = isTablet ? 20 : 12;
|
||||||
const CHATS_HEADER = 'Chats';
|
const CHATS_HEADER = 'Chats';
|
||||||
|
@ -90,7 +92,9 @@ const shouldUpdateProps = [
|
||||||
'appState',
|
'appState',
|
||||||
'theme',
|
'theme',
|
||||||
'isMasterDetail',
|
'isMasterDetail',
|
||||||
'refreshing'
|
'refreshing',
|
||||||
|
'queueSize',
|
||||||
|
'inquiryEnabled'
|
||||||
];
|
];
|
||||||
const getItemLayout = (data, index) => ({
|
const getItemLayout = (data, index) => ({
|
||||||
length: ROW_HEIGHT,
|
length: ROW_HEIGHT,
|
||||||
|
@ -131,7 +135,9 @@ class RoomsListView extends React.Component {
|
||||||
isMasterDetail: PropTypes.bool,
|
isMasterDetail: PropTypes.bool,
|
||||||
rooms: PropTypes.array,
|
rooms: PropTypes.array,
|
||||||
width: PropTypes.number,
|
width: PropTypes.number,
|
||||||
insets: PropTypes.object
|
insets: PropTypes.object,
|
||||||
|
queueSize: PropTypes.number,
|
||||||
|
inquiryEnabled: PropTypes.bool
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
@ -671,6 +677,20 @@ class RoomsListView extends React.Component {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
goQueue = () => {
|
||||||
|
logEvent(events.RL_GO_QUEUE);
|
||||||
|
const { navigation, isMasterDetail, queueSize } = this.props;
|
||||||
|
// prevent navigation to empty list
|
||||||
|
if (!queueSize) {
|
||||||
|
return showErrorAlert(I18n.t('Queue_is_empty'), I18n.t('Oops'));
|
||||||
|
}
|
||||||
|
if (isMasterDetail) {
|
||||||
|
navigation.navigate('ModalStackNavigator', { screen: 'QueueListView' });
|
||||||
|
} else {
|
||||||
|
navigation.navigate('QueueListView');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
goRoom = ({ item, isMasterDetail }) => {
|
goRoom = ({ item, isMasterDetail }) => {
|
||||||
logEvent(events.RL_GO_TO_ROOM);
|
logEvent(events.RL_GO_TO_ROOM);
|
||||||
const { item: currentItem } = this.state;
|
const { item: currentItem } = this.state;
|
||||||
|
@ -787,13 +807,16 @@ class RoomsListView extends React.Component {
|
||||||
|
|
||||||
renderListHeader = () => {
|
renderListHeader = () => {
|
||||||
const { searching } = this.state;
|
const { searching } = this.state;
|
||||||
const { sortBy } = this.props;
|
const { sortBy, queueSize, inquiryEnabled } = this.props;
|
||||||
return (
|
return (
|
||||||
<ListHeader
|
<ListHeader
|
||||||
searching={searching}
|
searching={searching}
|
||||||
sortBy={sortBy}
|
sortBy={sortBy}
|
||||||
toggleSort={this.toggleSort}
|
toggleSort={this.toggleSort}
|
||||||
goDirectory={this.goDirectory}
|
goDirectory={this.goDirectory}
|
||||||
|
goQueue={this.goQueue}
|
||||||
|
queueSize={queueSize}
|
||||||
|
inquiryEnabled={inquiryEnabled}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -960,7 +983,9 @@ const mapStateToProps = state => ({
|
||||||
useRealName: state.settings.UI_Use_Real_Name,
|
useRealName: state.settings.UI_Use_Real_Name,
|
||||||
appState: state.app.ready && state.app.foreground ? 'foreground' : 'background',
|
appState: state.app.ready && state.app.foreground ? 'foreground' : 'background',
|
||||||
StoreLastMessage: state.settings.Store_Last_Message,
|
StoreLastMessage: state.settings.Store_Last_Message,
|
||||||
rooms: state.room.rooms
|
rooms: state.room.rooms,
|
||||||
|
queueSize: getInquiryQueueSelector(state).length,
|
||||||
|
inquiryEnabled: state.inquiry.enabled
|
||||||
});
|
});
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => ({
|
const mapDispatchToProps = dispatch => ({
|
||||||
|
|
Loading…
Reference in New Issue