diff --git a/app/actions/actionsTypes.js b/app/actions/actionsTypes.js index 65c39d9c6..3fec00201 100644 --- a/app/actions/actionsTypes.js +++ b/app/actions/actionsTypes.js @@ -33,6 +33,7 @@ export const ROOMS = createRequestTypes('ROOMS', [ 'CLOSE_SEARCH_HEADER' ]); 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 MESSAGES = createRequestTypes('MESSAGES', ['REPLY_BROADCAST']); export const CREATE_CHANNEL = createRequestTypes('CREATE_CHANNEL', [...defaultTypes]); diff --git a/app/actions/inquiry.js b/app/actions/inquiry.js new file mode 100644 index 000000000..0f3402119 --- /dev/null +++ b/app/actions/inquiry.js @@ -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 + }; +} diff --git a/app/i18n/locales/en.js b/app/i18n/locales/en.js index e6038732e..72ebbdb4e 100644 --- a/app/i18n/locales/en.js +++ b/app/i18n/locales/en.js @@ -483,6 +483,7 @@ export default { Tags: 'Tags', Take_a_photo: 'Take a photo', Take_a_video: 'Take a video', + Take_it: 'Take it!', tap_to_change_status: 'tap to change status', Tap_to_view_servers_list: 'Tap to view servers list', Terms_of_Service: ' Terms of Service ', @@ -621,5 +622,7 @@ export default { Passcode_app_locked_title: 'App locked', Passcode_app_locked_subtitle: 'Try again in {{timeLeft}} seconds', 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' }; diff --git a/app/i18n/locales/pt-BR.js b/app/i18n/locales/pt-BR.js index 5e1d2342b..074bea9c1 100644 --- a/app/i18n/locales/pt-BR.js +++ b/app/i18n/locales/pt-BR.js @@ -551,5 +551,7 @@ export default { Passcode_app_locked_title: 'Aplicativo bloqueado', Passcode_app_locked_subtitle: 'Tente novamente em {{timeLeft}} segundos', 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' }; diff --git a/app/lib/methods/subscriptions/inquiry.js b/app/lib/methods/subscriptions/inquiry.js new file mode 100644 index 000000000..014335dc1 --- /dev/null +++ b/app/lib/methods/subscriptions/inquiry.js @@ -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(); + } +} diff --git a/app/lib/rocketchat.js b/app/lib/rocketchat.js index 1353b7364..a11e8998c 100644 --- a/app/lib/rocketchat.js +++ b/app/lib/rocketchat.js @@ -20,6 +20,7 @@ import { } from '../actions/share'; import subscribeRooms from './methods/subscriptions/rooms'; +import subscribeInquiry from './methods/subscriptions/inquiry'; import getUsersPresence, { getUserPresence, subscribeUsersPresence } from './methods/getUsersPresence'; 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, createChannel({ name, users, type, readOnly, broadcast @@ -203,6 +213,11 @@ const RocketChat = { this.roomsSub = null; } + if (this.inquirySub) { + this.inquirySub.stop(); + this.inquirySub = null; + } + if (this.sdk) { this.sdk.disconnect(); this.sdk = null; @@ -816,7 +831,7 @@ const RocketChat = { }, getAgentDepartments(uid) { // RC 2.4.0 - return this.sdk.get(`livechat/agents/${ uid }/departments`); + return this.sdk.get(`livechat/agents/${ uid }/departments?enabledDepartmentsOnly=true`); }, getCustomFields() { // RC 2.2.0 @@ -826,6 +841,16 @@ const RocketChat = { // RC 0.26.0 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) { const { id: userId } = reduxStore.getState().login.user; @@ -963,6 +988,14 @@ const RocketChat = { // RC 0.47.0 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) { const db = database.active; const subsCollection = db.collections.get('subscriptions'); diff --git a/app/presentation/RoomItem/RoomItem.js b/app/presentation/RoomItem/RoomItem.js index 189840bae..d0496ef98 100644 --- a/app/presentation/RoomItem/RoomItem.js +++ b/app/presentation/RoomItem/RoomItem.js @@ -4,7 +4,7 @@ import { View } from 'react-native'; import styles from './styles'; import Wrapper from './Wrapper'; -import UnreadBadge from './UnreadBadge'; +import UnreadBadge from '../UnreadBadge'; import TypeIcon from './TypeIcon'; import LastMessage from './LastMessage'; import Title from './Title'; @@ -41,6 +41,7 @@ const RoomItem = ({ groupMentions, roomUpdatedAt, testID, + swipeEnabled, onPress, toggleFav, toggleRead, @@ -59,6 +60,7 @@ const RoomItem = ({ type={type} theme={theme} isFocused={isFocused} + swipeEnabled={swipeEnabled} > { const [, setForceUpdate] = useState(1); @@ -125,6 +126,7 @@ const RoomItemContainer = React.memo(({ useRealName={useRealName} unread={item.unread} groupMentions={item.groupMentions} + swipeEnabled={swipeEnabled} /> ); }, arePropsEqual); @@ -153,7 +155,8 @@ RoomItemContainer.propTypes = { getRoomTitle: PropTypes.func, getRoomAvatar: PropTypes.func, getIsGroupChat: PropTypes.func, - getIsRead: PropTypes.func + getIsRead: PropTypes.func, + swipeEnabled: PropTypes.bool }; RoomItemContainer.defaultProps = { @@ -163,7 +166,8 @@ RoomItemContainer.defaultProps = { getRoomTitle: () => 'title', getRoomAvatar: () => '', getIsGroupChat: () => false, - getIsRead: () => false + getIsRead: () => false, + swipeEnabled: true }; const mapStateToProps = (state, ownProps) => { diff --git a/app/presentation/RoomItem/styles.js b/app/presentation/RoomItem/styles.js index ae7086c80..fb110e4ce 100644 --- a/app/presentation/RoomItem/styles.js +++ b/app/presentation/RoomItem/styles.js @@ -51,23 +51,6 @@ export default StyleSheet.create({ updateAlert: { ...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: { marginLeft: 4, marginRight: 7, diff --git a/app/presentation/RoomItem/UnreadBadge.js b/app/presentation/UnreadBadge.js similarity index 53% rename from app/presentation/RoomItem/UnreadBadge.js rename to app/presentation/UnreadBadge.js index 83fadca60..88f7db416 100644 --- a/app/presentation/RoomItem/UnreadBadge.js +++ b/app/presentation/UnreadBadge.js @@ -1,12 +1,32 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { View, Text } from 'react-native'; +import { View, Text, StyleSheet } from 'react-native'; -import styles from './styles'; -import { themes } from '../../constants/colors'; +import sharedStyles from '../views/Styles'; +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(({ - theme, unread, userMentions, groupMentions + theme, unread, userMentions, groupMentions, style }) => { if (!unread || unread <= 0) { return; @@ -27,7 +47,8 @@ const UnreadBadge = React.memo(({ { unread } + >{unread} ); @@ -45,7 +66,8 @@ UnreadBadge.propTypes = { theme: PropTypes.string, unread: PropTypes.number, userMentions: PropTypes.number, - groupMentions: PropTypes.number + groupMentions: PropTypes.number, + style: PropTypes.object }; export default UnreadBadge; diff --git a/app/reducers/index.js b/app/reducers/index.js index 1ac810c33..968254ffd 100644 --- a/app/reducers/index.js +++ b/app/reducers/index.js @@ -16,6 +16,7 @@ import activeUsers from './activeUsers'; import usersTyping from './usersTyping'; import inviteLinks from './inviteLinks'; import createDiscussion from './createDiscussion'; +import inquiry from './inquiry'; export default combineReducers({ settings, @@ -34,5 +35,6 @@ export default combineReducers({ activeUsers, usersTyping, inviteLinks, - createDiscussion + createDiscussion, + inquiry }); diff --git a/app/reducers/inquiry.js b/app/reducers/inquiry.js new file mode 100644 index 000000000..230280c83 --- /dev/null +++ b/app/reducers/inquiry.js @@ -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; + } +} diff --git a/app/sagas/index.js b/app/sagas/index.js index 27886be83..772df5717 100644 --- a/app/sagas/index.js +++ b/app/sagas/index.js @@ -10,6 +10,7 @@ import state from './state'; import deepLinking from './deepLinking'; import inviteLinks from './inviteLinks'; import createDiscussion from './createDiscussion'; +import inquiry from './inquiry'; const root = function* root() { yield all([ @@ -23,7 +24,8 @@ const root = function* root() { state(), deepLinking(), inviteLinks(), - createDiscussion() + createDiscussion(), + inquiry() ]); }; diff --git a/app/sagas/inquiry.js b/app/sagas/inquiry.js new file mode 100644 index 000000000..9ee7e27fe --- /dev/null +++ b/app/sagas/inquiry.js @@ -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; diff --git a/app/sagas/login.js b/app/sagas/login.js index 749e1d18a..01ab28453 100644 --- a/app/sagas/login.js +++ b/app/sagas/login.js @@ -15,6 +15,7 @@ import { loginFailure, loginSuccess, setUser, logout } from '../actions/login'; import { roomsRequest } from '../actions/rooms'; +import { inquiryRequest } from '../actions/inquiry'; import { toMomentLocale } from '../utils/moment'; import RocketChat from '../lib/rocketchat'; import log, { logEvent, events } from '../utils/log'; @@ -93,6 +94,7 @@ const handleLoginSuccess = function* handleLoginSuccess({ user }) { const server = yield select(getServer); yield put(roomsRequest()); + yield put(inquiryRequest()); yield fork(fetchPermissions); yield fork(fetchCustomEmojis); yield fork(fetchRoles); @@ -206,6 +208,10 @@ const handleSetUser = function* handleSetUser({ user }) { const userId = yield select(state => state.login.user.id); yield put(setActiveUsers({ [userId]: user })); } + + if (user && user.statusLivechat) { + yield put(inquiryRequest()); + } }; const root = function* root() { diff --git a/app/sagas/selectServer.js b/app/sagas/selectServer.js index 429b2642c..51728b8a8 100644 --- a/app/sagas/selectServer.js +++ b/app/sagas/selectServer.js @@ -19,6 +19,7 @@ import I18n from '../i18n'; import { SERVERS, TOKEN, SERVER_URL } from '../constants/userDefaults'; import { BASIC_AUTH_KEY, setBasicAuth } from '../utils/fetch'; import { appStart, ROOT_INSIDE, ROOT_OUTSIDE } from '../actions/app'; +import { inquiryReset } from '../actions/inquiry'; const getServerInfo = function* getServerInfo({ server, raiseError = true }) { try { @@ -65,6 +66,7 @@ const getServerInfo = function* getServerInfo({ server, raiseError = true }) { const handleSelectServer = function* handleSelectServer({ server, version, fetchVersion }) { try { + yield put(inquiryReset()); const serversDB = database.servers; yield RNUserDefaults.set('currentServer', server); const userId = yield RNUserDefaults.get(`${ RocketChat.TOKEN_KEY }-${ server }`); diff --git a/app/selectors/inquiry.js b/app/selectors/inquiry.js new file mode 100644 index 000000000..baeb09ef8 --- /dev/null +++ b/app/selectors/inquiry.js @@ -0,0 +1,8 @@ +import { createSelector } from 'reselect'; + +const getInquiryQueue = state => state.inquiry.queued; + +export const getInquiryQueueSelector = createSelector( + [getInquiryQueue], + queue => queue +); diff --git a/app/stacks/InsideStack.js b/app/stacks/InsideStack.js index 8a0f2e919..e9a4a7a74 100644 --- a/app/stacks/InsideStack.js +++ b/app/stacks/InsideStack.js @@ -30,6 +30,7 @@ import PickerView from '../views/PickerView'; import ThreadMessagesView from '../views/ThreadMessagesView'; import MarkdownTableView from '../views/MarkdownTableView'; import ReadReceiptsView from '../views/ReadReceiptView'; +import QueueListView from '../views/QueueListView'; // Profile Stack import ProfileView from '../views/ProfileView'; @@ -163,6 +164,11 @@ const ChatsStackNavigator = () => { component={ReadReceiptsView} options={ReadReceiptsView.navigationOptions} /> + ); }; diff --git a/app/stacks/MasterDetailStack/index.js b/app/stacks/MasterDetailStack/index.js index 854112c92..800bad1d7 100644 --- a/app/stacks/MasterDetailStack/index.js +++ b/app/stacks/MasterDetailStack/index.js @@ -41,6 +41,7 @@ import ScreenLockConfigView from '../../views/ScreenLockConfigView'; import AdminPanelView from '../../views/AdminPanelView'; import NewMessageView from '../../views/NewMessageView'; import CreateChannelView from '../../views/CreateChannelView'; +import QueueListView from '../../views/QueueListView'; // InsideStackNavigator import AttachmentView from '../../views/AttachmentView'; @@ -150,6 +151,11 @@ const ModalStackNavigator = React.memo(({ navigation }) => { component={DirectoryView} options={props => DirectoryView.navigationOptions({ ...props, isMasterDetail: true })} /> + QueueListView.navigationOptions({ ...props, isMasterDetail: true })} + /> ({ + 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 = () => ; + } + 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 ( + + ); + } + + render() { + const { queued, theme } = this.props; + return ( + + + + + ); + } +} + +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))); diff --git a/app/views/RoomActionsView/index.js b/app/views/RoomActionsView/index.js index 20cff6c2b..9d7c56b6f 100644 --- a/app/views/RoomActionsView/index.js +++ b/app/views/RoomActionsView/index.js @@ -91,7 +91,7 @@ class RoomActionsView extends React.Component { this.mounted = true; const { room, member } = this.state; if (room.rid) { - if (!room.id) { + if (!room.id && !this.isOmnichannelPreview) { try { const result = await RocketChat.getChannelInfo(room.rid); 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) => { if (item.route) { const { navigation } = this.props; @@ -407,36 +412,38 @@ class RoomActionsView extends React.Component { } else if (t === 'l') { sections[2].data = []; - sections[2].data.push({ - icon: 'close', - name: I18n.t('Close'), - event: this.closeLivechat - }); - - if (canForwardGuest) { + if (!this.isOmnichannelPreview) { sections[2].data.push({ - icon: 'user-forward', - name: I18n.t('Forward'), - route: 'ForwardLivechatView', + icon: 'close', + name: I18n.t('Close'), + 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 } }); } - 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({ data: [notificationsAction], renderItem: this.renderItem diff --git a/app/views/RoomInfoView/Livechat.js b/app/views/RoomInfoView/Livechat.js index a93ea1714..09e14a608 100644 --- a/app/views/RoomInfoView/Livechat.js +++ b/app/views/RoomInfoView/Livechat.js @@ -44,7 +44,7 @@ const Livechat = ({ room, roomUser, theme }) => { } }; - useEffect(() => { getRoom(); }, []); + useEffect(() => { getRoom(); }, [room]); return ( <> diff --git a/app/views/RoomInfoView/index.js b/app/views/RoomInfoView/index.js index 36993de3b..0ccad794c 100644 --- a/app/views/RoomInfoView/index.js +++ b/app/views/RoomInfoView/index.js @@ -195,6 +195,7 @@ class RoomInfoView extends React.Component { } loadRoom = async() => { + const { room: roomState } = this.state; const { route } = this.props; let room = route.params?.room; if (room && room.observe) { @@ -208,7 +209,7 @@ class RoomInfoView extends React.Component { const result = await RocketChat.getRoomInfo(this.rid); if (result.success) { ({ room } = result); - this.setState({ room }); + this.setState({ room: { ...roomState, ...room } }); } } catch (e) { log(e); diff --git a/app/views/RoomView/index.js b/app/views/RoomView/index.js index d68aca41b..c7165be8d 100644 --- a/app/views/RoomView/index.js +++ b/app/views/RoomView/index.js @@ -282,6 +282,11 @@ class RoomView extends React.Component { console.countReset(`${ this.constructor.name }.render calls`); } + get isOmnichannel() { + const { room } = this.state; + return room.t === 'l'; + } + setHeader = () => { const { room, unreadsCount, roomUserId: stateRoomUserId } = this.state; const { @@ -678,8 +683,15 @@ class RoomView extends React.Component { setLastOpen = lastOpen => this.setState({ lastOpen }); joinRoom = async() => { + logEvent(events.ROOM_JOIN); 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({ joined: true }); @@ -896,7 +908,7 @@ class RoomView extends React.Component { style={[styles.joinRoomButton, { backgroundColor: themes[theme].actionTintColor }]} theme={theme} > - {I18n.t('Join')} + {I18n.t(this.isOmnichannel ? 'Take_it' : 'Join')} ); diff --git a/app/views/RoomsListView/ListHeader/Queue.js b/app/views/RoomsListView/ListHeader/Queue.js new file mode 100644 index 000000000..0a85d657a --- /dev/null +++ b/app/views/RoomsListView/ListHeader/Queue.js @@ -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 ( + + + {I18n.t('Queued_chats')} + + + + ); +}); + +Queue.propTypes = { + searching: PropTypes.bool, + goQueue: PropTypes.func, + queueSize: PropTypes.number, + inquiryEnabled: PropTypes.bool, + theme: PropTypes.string +}; + +export default withTheme(Queue); diff --git a/app/views/RoomsListView/ListHeader/index.js b/app/views/RoomsListView/ListHeader/index.js index dec38b506..5df248787 100644 --- a/app/views/RoomsListView/ListHeader/index.js +++ b/app/views/RoomsListView/ListHeader/index.js @@ -1,6 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; +import Queue from './Queue'; import Directory from './Directory'; import Sort from './Sort'; @@ -8,11 +9,15 @@ const ListHeader = React.memo(({ searching, sortBy, toggleSort, - goDirectory + goDirectory, + goQueue, + queueSize, + inquiryEnabled }) => ( <> + )); @@ -20,7 +25,10 @@ ListHeader.propTypes = { searching: PropTypes.bool, sortBy: PropTypes.string, toggleSort: PropTypes.func, - goDirectory: PropTypes.func + goDirectory: PropTypes.func, + goQueue: PropTypes.func, + queueSize: PropTypes.number, + inquiryEnabled: PropTypes.bool }; export default ListHeader; diff --git a/app/views/RoomsListView/index.js b/app/views/RoomsListView/index.js index 617b0fd51..5717de25c 100644 --- a/app/views/RoomsListView/index.js +++ b/app/views/RoomsListView/index.js @@ -62,6 +62,8 @@ import { goRoom } from '../../utils/goRoom'; import SafeAreaView from '../../containers/SafeAreaView'; import Header, { getHeaderTitlePosition } from '../../containers/Header'; import { withDimensions } from '../../dimensions'; +import { showErrorAlert } from '../../utils/info'; +import { getInquiryQueueSelector } from '../../selectors/inquiry'; const INITIAL_NUM_TO_RENDER = isTablet ? 20 : 12; const CHATS_HEADER = 'Chats'; @@ -90,7 +92,9 @@ const shouldUpdateProps = [ 'appState', 'theme', 'isMasterDetail', - 'refreshing' + 'refreshing', + 'queueSize', + 'inquiryEnabled' ]; const getItemLayout = (data, index) => ({ length: ROW_HEIGHT, @@ -131,7 +135,9 @@ class RoomsListView extends React.Component { isMasterDetail: PropTypes.bool, rooms: PropTypes.array, width: PropTypes.number, - insets: PropTypes.object + insets: PropTypes.object, + queueSize: PropTypes.number, + inquiryEnabled: PropTypes.bool }; 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 }) => { logEvent(events.RL_GO_TO_ROOM); const { item: currentItem } = this.state; @@ -787,13 +807,16 @@ class RoomsListView extends React.Component { renderListHeader = () => { const { searching } = this.state; - const { sortBy } = this.props; + const { sortBy, queueSize, inquiryEnabled } = this.props; return ( ); }; @@ -960,7 +983,9 @@ const mapStateToProps = state => ({ useRealName: state.settings.UI_Use_Real_Name, appState: state.app.ready && state.app.foreground ? 'foreground' : 'background', 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 => ({