diff --git a/app/containers/MessageBox/index.tsx b/app/containers/MessageBox/index.tsx index cd81bedcc..dd604d602 100644 --- a/app/containers/MessageBox/index.tsx +++ b/app/containers/MessageBox/index.tsx @@ -46,12 +46,22 @@ import { withActionSheet } from '../ActionSheet'; import { sanitizeLikeString } from '../../lib/database/utils'; import { CustomIcon } from '../CustomIcon'; import { forceJpgExtension } from './forceJpgExtension'; -import { IBaseScreen, IPreviewItem, IUser, TGetCustomEmoji, TSubscriptionModel, TThreadModel, IMessage } from '../../definitions'; +import { + IApplicationState, + IBaseScreen, + IPreviewItem, + IUser, + TGetCustomEmoji, + TSubscriptionModel, + TThreadModel, + IMessage +} from '../../definitions'; import { MasterDetailInsideStackParamList } from '../../stacks/MasterDetailStack/types'; import { getPermalinkMessage, search, sendFileMessage } from '../../lib/methods'; import { hasPermission, debounce, isAndroid, isTablet } from '../../lib/methods/helpers'; import { Services } from '../../lib/services'; import { TSupportedThemes } from '../../theme'; +import { ChatsStackParamList } from '../../stacks/types'; if (isAndroid) { require('./EmojiKeyboard'); @@ -75,7 +85,7 @@ const videoPickerConfig: Options = { mediaType: 'video' }; -export interface IMessageBoxProps extends IBaseScreen { +export interface IMessageBoxProps extends IBaseScreen { rid: string; baseUrl: string; message: IMessage; @@ -107,6 +117,7 @@ export interface IMessageBoxProps extends IBaseScreen void | null; } interface IMessageBoxState { @@ -305,7 +316,17 @@ class MessageBox extends Component { permissionToUpload } = this.state; - const { roomType, replying, editing, isFocused, message, theme, usedCannedResponse, uploadFilePermission } = this.props; + const { + roomType, + replying, + editing, + isFocused, + message, + theme, + usedCannedResponse, + uploadFilePermission, + goToCannedResponses + } = this.props; if (nextProps.theme !== theme) { return true; } @@ -357,12 +378,15 @@ class MessageBox extends Component { if (nextProps.usedCannedResponse !== usedCannedResponse) { return true; } + if (nextProps.goToCannedResponses !== goToCannedResponses) { + return true; + } return false; } componentDidUpdate(prevProps: IMessageBoxProps) { - const { uploadFilePermission } = this.props; - if (!dequal(prevProps.uploadFilePermission, uploadFilePermission)) { + const { uploadFilePermission, goToCannedResponses } = this.props; + if (!dequal(prevProps.uploadFilePermission, uploadFilePermission) || prevProps.goToCannedResponses !== goToCannedResponses) { this.setOptions(); } } @@ -783,9 +807,16 @@ class MessageBox extends Component { showMessageBoxActions = () => { logEvent(events.ROOM_SHOW_BOX_ACTIONS); const { permissionToUpload } = this.state; - const { showActionSheet } = this.props; + const { showActionSheet, goToCannedResponses } = this.props; const options = []; + if (goToCannedResponses) { + options.push({ + title: I18n.t('Canned_Responses'), + icon: 'canned-response', + onPress: () => goToCannedResponses() + }); + } if (permissionToUpload) { options.push( { @@ -1170,7 +1201,7 @@ class MessageBox extends Component { } } -const mapStateToProps = (state: any) => ({ +const mapStateToProps = (state: IApplicationState) => ({ isMasterDetail: state.app.isMasterDetail, baseUrl: state.server.server, threadsEnabled: state.settings.Threads_enabled, diff --git a/app/definitions/IRoom.ts b/app/definitions/IRoom.ts index 895f1c8c1..1444d4fef 100644 --- a/app/definitions/IRoom.ts +++ b/app/definitions/IRoom.ts @@ -198,7 +198,7 @@ export interface IServerRoom extends IRocketChatRecord { unread?: number; alert?: boolean; hideUnreadStatus?: boolean; - + status?: string; sysMes?: string[]; muted?: string[]; unmuted?: string[]; diff --git a/app/i18n/locales/ar.json b/app/i18n/locales/ar.json index 87fe5f894..82029db29 100644 --- a/app/i18n/locales/ar.json +++ b/app/i18n/locales/ar.json @@ -421,7 +421,6 @@ "Reset_password": "إعادة تعيين كلمة المرور", "resetting_password": "إعادة تعيين كلمة المرور", "RESET": "إعادة", - "Return": "العودة", "Review_app_title": "هل أنت مستمتع بهذا التطبيق؟", "Review_app_desc": "قم بإعطائنا 5 نجوم {{store}}", "Review_app_yes": "أكيد!", diff --git a/app/i18n/locales/de.json b/app/i18n/locales/de.json index 61b8d57c4..12b186047 100644 --- a/app/i18n/locales/de.json +++ b/app/i18n/locales/de.json @@ -427,7 +427,6 @@ "Reset_password": "Passwort zurücksetzen", "resetting_password": "Passwort zurücksetzen", "RESET": "ZURÜCKSETZEN", - "Return": "Zurück", "Review_app_title": "Gefällt Ihnen diese App?", "Review_app_desc": "Geben Sie uns 5 Sterne im {{store}}", "Review_app_yes": "Sicher!", diff --git a/app/i18n/locales/en.json b/app/i18n/locales/en.json index a89c45e31..5fa60c40f 100644 --- a/app/i18n/locales/en.json +++ b/app/i18n/locales/en.json @@ -431,7 +431,7 @@ "Reset_password": "Reset password", "resetting_password": "resetting password", "RESET": "RESET", - "Return": "Return", + "Return_to_waiting_line": "Return to waiting line", "Review_app_title": "Are you enjoying this app?", "Review_app_desc": "Give us 5 stars on {{store}}", "Review_app_yes": "Sure!", diff --git a/app/i18n/locales/fr.json b/app/i18n/locales/fr.json index 57a2eab73..089264f08 100644 --- a/app/i18n/locales/fr.json +++ b/app/i18n/locales/fr.json @@ -427,7 +427,6 @@ "Reset_password": "Réinitialiser le mot de passe", "resetting_password": "réinitialisation du mot de passe", "RESET": "RÉINITIALISER", - "Return": "Retour", "Review_app_title": "Appréciez-vous cette application ?", "Review_app_desc": "Donnez-nous 5 étoiles sur {{store}}", "Review_app_yes": "Bien sûr !", diff --git a/app/i18n/locales/it.json b/app/i18n/locales/it.json index b825241ac..52c8af272 100644 --- a/app/i18n/locales/it.json +++ b/app/i18n/locales/it.json @@ -416,7 +416,6 @@ "Reset_password": "Ripristina password", "resetting_password": "ripristinando password", "RESET": "RIPRISTINA", - "Return": "Ritorno", "Review_app_title": "Ti piace questa app?", "Review_app_desc": "Dacci 5 stesse su {{store}}", "Review_app_yes": "Certo!", diff --git a/app/i18n/locales/nl.json b/app/i18n/locales/nl.json index ce0377bf8..ccac7069d 100644 --- a/app/i18n/locales/nl.json +++ b/app/i18n/locales/nl.json @@ -427,7 +427,6 @@ "Reset_password": "Wachtwoord resetten", "resetting_password": "wachtwoord aan het resetten", "RESET": "RESET", - "Return": "Terug", "Review_app_title": "Geniet je van deze app?", "Review_app_desc": "Geef ons 5 sterren op {{store}}", "Review_app_yes": "Zeker!", diff --git a/app/i18n/locales/pt-BR.json b/app/i18n/locales/pt-BR.json index 1e1939b39..3a2d534a0 100644 --- a/app/i18n/locales/pt-BR.json +++ b/app/i18n/locales/pt-BR.json @@ -403,7 +403,6 @@ "Reset_password": "Resetar senha", "resetting_password": "redefinindo senha", "RESET": "RESETAR", - "Return": "Retornar", "Review_app_title": "Você está gostando do app?", "Review_app_desc": "Nos dê 5 estrelas na {{store}}", "Review_app_yes": "Claro!", diff --git a/app/i18n/locales/ru.json b/app/i18n/locales/ru.json index eb4b7037c..94a323444 100644 --- a/app/i18n/locales/ru.json +++ b/app/i18n/locales/ru.json @@ -427,7 +427,6 @@ "Reset_password": "Сброс пароля", "resetting_password": "сброс пароля", "RESET": "СБРОС", - "Return": "Возврат", "Review_app_title": "Нравится ли вам это приложение?", "Review_app_desc": "Поставьте нам 5 звезд в {{store}}", "Review_app_yes": "Конечно!", diff --git a/app/i18n/locales/tr.json b/app/i18n/locales/tr.json index 64d88304f..10dfcb4cd 100644 --- a/app/i18n/locales/tr.json +++ b/app/i18n/locales/tr.json @@ -417,7 +417,6 @@ "Reset_password": "Şifre sıfırla", "resetting_password": "şifre sıfırlanıyor", "RESET": "SIFIRLA", - "Return": "Geri dön", "Review_app_title": "Uygulama hoşunuza gitti mi?", "Review_app_desc": "{{store}} üzerinde bize 5 yıldız verin", "Review_app_yes": "Elbette!", diff --git a/app/i18n/locales/zh-CN.json b/app/i18n/locales/zh-CN.json index c95e15330..0b850b959 100644 --- a/app/i18n/locales/zh-CN.json +++ b/app/i18n/locales/zh-CN.json @@ -414,7 +414,6 @@ "Reset_password": "重置密码", "resetting_password": "正在重置密码", "RESET": "重置", - "Return": "返回", "Review_app_title": "对此 App 满意吗?", "Review_app_desc": "请在 {{store}} 给予我们 5 星好评", "Review_app_yes": "没问题", diff --git a/app/i18n/locales/zh-TW.json b/app/i18n/locales/zh-TW.json index d153add8e..7110d2f0d 100644 --- a/app/i18n/locales/zh-TW.json +++ b/app/i18n/locales/zh-TW.json @@ -416,7 +416,6 @@ "Reset_password": "重置密碼", "resetting_password": "正在重置密碼", "RESET": "重置", - "Return": "返回", "Review_app_title": "對此 App 滿意嗎?", "Review_app_desc": "請在 {{store}} 給予我們 5 星好評", "Review_app_yes": "沒問題", diff --git a/app/lib/methods/helpers/log/events.ts b/app/lib/methods/helpers/log/events.ts index 98b505a73..0faa7da62 100644 --- a/app/lib/methods/helpers/log/events.ts +++ b/app/lib/methods/helpers/log/events.ts @@ -193,6 +193,7 @@ export default { ROOM_AUDIO_FINISH_F: 'room_audio_finish_f', ROOM_AUDIO_CANCEL: 'room_audio_cancel', ROOM_AUDIO_CANCEL_F: 'room_audio_cancel_f', + ROOM_SHOW_MORE_ACTIONS: 'room_show_more_actions', ROOM_SHOW_BOX_ACTIONS: 'room_show_box_actions', ROOM_BOX_ACTION_PHOTO: 'room_box_action_photo', ROOM_BOX_ACTION_PHOTO_F: 'room_box_action_photo_f', diff --git a/app/stacks/MasterDetailStack/types.ts b/app/stacks/MasterDetailStack/types.ts index a21c63ebd..9d69428a0 100644 --- a/app/stacks/MasterDetailStack/types.ts +++ b/app/stacks/MasterDetailStack/types.ts @@ -32,6 +32,12 @@ export type ModalStackParamList = { rid: string; t: SubscriptionType; joined: boolean; + omnichannelPermissions?: { + canForwardGuest: boolean; + canReturnQueue: boolean; + canViewCannedResponse: boolean; + canPlaceLivechatOnHold: boolean; + }; }; RoomInfoView: { room: ISubscription; diff --git a/app/stacks/types.ts b/app/stacks/types.ts index a93b84f4d..aa96c9829 100644 --- a/app/stacks/types.ts +++ b/app/stacks/types.ts @@ -34,6 +34,7 @@ export type ChatsStackParamList = { jumpToThreadId?: string; roomUserId?: string | null; usedCannedResponse?: string; + status?: string; } | undefined; // Navigates back to RoomView already on stack RoomActionsView: { @@ -42,6 +43,12 @@ export type ChatsStackParamList = { rid: string; t: SubscriptionType; joined: boolean; + omnichannelPermissions?: { + canForwardGuest: boolean; + canReturnQueue: boolean; + canViewCannedResponse: boolean; + canPlaceLivechatOnHold: boolean; + }; }; SelectListView: { data?: TDataSelect[]; diff --git a/app/views/RoomActionsView/index.tsx b/app/views/RoomActionsView/index.tsx index aea34b828..2f93d6360 100644 --- a/app/views/RoomActionsView/index.tsx +++ b/app/views/RoomActionsView/index.tsx @@ -64,12 +64,10 @@ interface IRoomActionsViewProps extends IBaseScreen { @@ -98,6 +91,12 @@ class RoomActionsView extends React.Component; private subscription?: Subscription; @@ -122,6 +121,7 @@ class RoomActionsView extends React.Component { if (this.mounted) { - this.setState({ room: changes, isOnHold: !!changes?.onHold }); + this.setState({ room: changes }); } else { // @ts-ignore this.state.room = changes; @@ -214,28 +209,6 @@ class RoomActionsView extends React.Component { - const { room } = this.state; - const { transferLivechatGuestPermission } = this.props; - const { rid } = room; - const permissions = await hasPermission([transferLivechatGuestPermission], rid); - return permissions[0]; - }; - - canViewCannedResponse = async () => { - const { room } = this.state; - const { viewCannedResponsesPermission } = this.props; - const { rid } = room; - const permissions = await hasPermission([viewCannedResponsesPermission], rid); - return permissions[0]; - }; - - canPlaceLivechatOnHold = (): boolean => { - const { livechatAllowManualOnHold } = this.props; - const { room } = this.state; - - return !!(livechatAllowManualOnHold && !room?.lastMessage?.token && room?.lastMessage?.u && !room.onHold); - }; - - canReturnQueue = async () => { - try { - const { returnQueue } = await Services.getRoutingConfig(); - return returnQueue; - } catch { - return false; - } - }; - renderEncryptedSwitch = () => { const { room, canToggleEncryption, canEdit } = this.state; const { encrypted } = room; @@ -1047,20 +988,86 @@ class RoomActionsView extends React.Component { + const { room } = this.state; + const { rid, t } = room; + const { theme } = this.props; + + if (t !== 'l' || this.isOmnichannelPreview) { + return null; + } + + return ( + + {this.omnichannelPermissions?.canForwardGuest ? ( + <> + + this.onPressTouchable({ + route: 'ForwardLivechatView', + params: { rid } + }) + } + left={() => } + showActionIndicator + /> + + + ) : null} + + {this.omnichannelPermissions?.canPlaceLivechatOnHold ? ( + <> + + this.onPressTouchable({ + event: this.placeOnHoldLivechat + }) + } + left={() => } + showActionIndicator + /> + + + ) : null} + + {this.omnichannelPermissions?.canReturnQueue ? ( + <> + + this.onPressTouchable({ + event: this.returnLivechat + }) + } + left={() => } + showActionIndicator + /> + + + ) : null} + + <> + + this.onPressTouchable({ + event: this.closeLivechat + }) + } + left={() => } + showActionIndicator + /> + + + + ); + }; + render() { - const { - room, - membersCount, - canViewMembers, - canAddUser, - canInviteUser, - joined, - canAutoTranslate, - canForwardGuest, - canReturnQueue, - canViewCannedResponse, - canPlaceLivechatOnHold - } = this.state; + const { room, membersCount, canViewMembers, canAddUser, canInviteUser, joined, canAutoTranslate } = this.state; const { rid, t, prid } = room; const isGroupChatHandler = isGroupChat(room); @@ -1149,6 +1156,18 @@ class RoomActionsView extends React.Component ) : null} + {['l'].includes(t) && !this.isOmnichannelPreview && this.omnichannelPermissions?.canViewCannedResponse ? ( + <> + this.onPressTouchable({ route: 'CannedResponsesListView', params: { rid } })} + left={() => } + showActionIndicator + /> + + + ) : null} + {['c', 'p', 'd'].includes(t) ? ( <> - this.onPressTouchable({ route: 'CannedResponsesListView', params: { rid } })} - left={() => } - showActionIndicator - /> - - - ) : null} - - {['l'].includes(t) && !this.isOmnichannelPreview ? ( - <> - - this.onPressTouchable({ - event: this.closeLivechat - }) - } - left={() => } - showActionIndicator - /> - - - ) : null} - - {['l'].includes(t) && !this.isOmnichannelPreview && canForwardGuest ? ( - <> - - this.onPressTouchable({ - route: 'ForwardLivechatView', - params: { rid } - }) - } - left={() => } - showActionIndicator - /> - - - ) : null} - - {['l'].includes(t) && !this.isOmnichannelPreview && canPlaceLivechatOnHold ? ( - <> - - this.onPressTouchable({ - event: this.placeOnHoldLivechat - }) - } - left={() => } - showActionIndicator - /> - - - ) : null} - - {['l'].includes(t) && !this.isOmnichannelPreview && canReturnQueue ? ( - <> - - this.onPressTouchable({ - event: this.returnLivechat - }) - } - left={() => } - showActionIndicator - /> - - - ) : null} - + {this.renderOmnichannelSection()} {this.renderLastSection()} @@ -1377,12 +1319,9 @@ const mapStateToProps = (state: IApplicationState) => ({ editRoomPermission: state.permissions['edit-room'], toggleRoomE2EEncryptionPermission: state.permissions['toggle-room-e2e-encryption'], viewBroadcastMemberListPermission: state.permissions['view-broadcast-member-list'], - transferLivechatGuestPermission: state.permissions['transfer-livechat-guest'], createTeamPermission: state.permissions['create-team'], addTeamChannelPermission: state.permissions['add-team-channel'], - convertTeamPermission: state.permissions['convert-team'], - viewCannedResponsesPermission: state.permissions['view-canned-responses'], - livechatAllowManualOnHold: state.settings.Livechat_allow_manual_on_hold as boolean + convertTeamPermission: state.permissions['convert-team'] }); export default connect(mapStateToProps)(withTheme(withDimensions(RoomActionsView))); diff --git a/app/views/RoomView/RightButtons.tsx b/app/views/RoomView/RightButtons.tsx index ca952532c..a7b110a81 100644 --- a/app/views/RoomView/RightButtons.tsx +++ b/app/views/RoomView/RightButtons.tsx @@ -2,6 +2,7 @@ import React, { Component } from 'react'; import { connect } from 'react-redux'; import { dequal } from 'dequal'; import { Observable, Subscription } from 'rxjs'; +import { Dispatch } from 'redux'; import { StackNavigationProp } from '@react-navigation/stack'; import * as HeaderButton from '../../containers/HeaderButton'; @@ -11,19 +12,33 @@ import { events, logEvent } from '../../lib/methods/helpers/log'; import { isTeamRoom } from '../../lib/methods/helpers/room'; import { IApplicationState, SubscriptionType, TMessageModel, TSubscriptionModel } from '../../definitions'; import { ChatsStackParamList } from '../../stacks/types'; +import { TActionSheetOptions, TActionSheetOptionsItem, withActionSheet } from '../../containers/ActionSheet'; +import i18n from '../../i18n'; +import { showConfirmationAlert, showErrorAlert } from '../../lib/methods/helpers'; +import { closeRoom } from '../../actions/room'; +import { onHoldLivechat, returnLivechat } from '../../lib/services/restApi'; interface IRightButtonsProps { userId?: string; threadsEnabled: boolean; - rid?: string; + rid: string; t: string; tmid?: string; teamId?: string; isMasterDetail: boolean; toggleFollowThread: Function; joined: boolean; + status?: string; + dispatch: Dispatch; encrypted?: boolean; + showActionSheet: (item: TActionSheetOptions) => void; + transferLivechatGuestPermission: boolean; navigation: StackNavigationProp; + omnichannelPermissions: { + canForwardGuest: boolean; + canReturnQueue: boolean; + canPlaceLivechatOnHold: boolean; + }; } interface IRigthButtonsState { @@ -71,13 +86,22 @@ class RightButtonsContainer extends Component { + const { rid } = this.props; + showConfirmationAlert({ + message: i18n.t('Would_you_like_to_return_the_inquiry'), + confirmationText: i18n.t('Yes'), + onPress: async () => { + try { + await returnLivechat(rid); + } catch (e: any) { + showErrorAlert(e.reason, i18n.t('Oops')); + } + } + }); + }; + + placeOnHoldLivechat = () => { + const { navigation, rid } = this.props; + showConfirmationAlert({ + title: i18n.t('Are_you_sure_question_mark'), + message: i18n.t('Would_like_to_place_on_hold'), + confirmationText: i18n.t('Yes'), + onPress: async () => { + try { + await onHoldLivechat(rid); + navigation.navigate('RoomsListView'); + } catch (e: any) { + showErrorAlert(e.data?.error, i18n.t('Oops')); + } + } + }); + }; + + closeLivechat = () => { + const { dispatch, rid } = this.props; + dispatch(closeRoom(rid)); + }; + + showMoreActions = () => { + logEvent(events.ROOM_SHOW_MORE_ACTIONS); + const { showActionSheet, rid, navigation, omnichannelPermissions } = this.props; + + const options = [] as TActionSheetOptionsItem[]; + if (omnichannelPermissions.canPlaceLivechatOnHold) { + options.push({ + title: i18n.t('Place_chat_on_hold'), + icon: 'pause', + onPress: () => this.placeOnHoldLivechat() + }); + } + + if (omnichannelPermissions.canForwardGuest) { + options.push({ + title: i18n.t('Forward_Chat'), + icon: 'chat-forward', + onPress: () => navigation.navigate('ForwardLivechatView', { rid }) + }); + } + + if (omnichannelPermissions.canReturnQueue) { + options.push({ + title: i18n.t('Return_to_waiting_line'), + icon: 'move-to-the-queue', + onPress: () => this.returnLivechat() + }); + } + + options.push({ + title: i18n.t('Close'), + icon: 'chat-close', + onPress: () => this.closeLivechat(), + danger: true + }); + + showActionSheet({ options }); + }; + goSearchView = () => { logEvent(events.ROOM_GO_SEARCH); const { rid, t, navigation, isMasterDetail, encrypted } = this.props; @@ -183,10 +283,23 @@ class RightButtonsContainer extends Component { + const { status } = this.props; + return status === 'queued'; + }; + render() { const { isFollowingThread, tunread, tunreadUser, tunreadGroup } = this.state; const { t, tmid, threadsEnabled, teamId, joined } = this.props; + if (t === 'l') { + if (!this.isOmnichannelPreview()) { + return ( + + + + ); + } return null; } if (tmid) { @@ -225,4 +338,4 @@ const mapStateToProps = (state: IApplicationState) => ({ isMasterDetail: state.app.isMasterDetail }); -export default connect(mapStateToProps)(RightButtonsContainer); +export default connect(mapStateToProps)(withActionSheet(RightButtonsContainer)); diff --git a/app/views/RoomView/index.tsx b/app/views/RoomView/index.tsx index 5bf99bba4..4fa3805ca 100644 --- a/app/views/RoomView/index.tsx +++ b/app/views/RoomView/index.tsx @@ -9,6 +9,7 @@ import { dequal } from 'dequal'; import { EdgeInsets, withSafeAreaInsets } from 'react-native-safe-area-context'; import { Subscription } from 'rxjs'; +import { getRoutingConfig } from '../../lib/services/restApi'; import Touch from '../../lib/methods/helpers/touch'; import { replyBroadcast } from '../../actions/messages'; import database from '../../lib/database'; @@ -63,6 +64,7 @@ import { IApplicationState, IAttachment, IBaseScreen, + ILastMessage, ILoggedUser, IMessage, IOmnichannelSource, @@ -94,7 +96,8 @@ import { canAutoTranslate as canAutoTranslateMethod, debounce, isIOS, - isTablet + isTablet, + hasPermission } from '../../lib/methods/helpers'; import { Services } from '../../lib/services'; @@ -112,7 +115,10 @@ const stateAttrsUpdate = [ 'reacting', 'readOnly', 'member', - 'showingBlockingLoader' + 'showingBlockingLoader', + 'canForwardGuest', + 'canReturnQueue', + 'canViewCannedResponse' ] as TStateAttrsUpdate[]; type TRoomUpdate = keyof TSubscriptionModel; @@ -138,6 +144,8 @@ const roomAttrsUpdate = [ 'joinCodeRequired', 'teamMain', 'teamId', + 'status', + 'lastMessage', 'onHold' ] as TRoomUpdate[]; @@ -158,6 +166,9 @@ interface IRoomViewProps extends IBaseScreen { width: number; height: number; insets: EdgeInsets; + transferLivechatGuestPermission?: string[]; // TODO: Check if its the correct type + viewCannedResponsesPermission?: string[]; // TODO: Check if its the correct type + livechatAllowManualOnHold?: boolean; } interface IRoomViewState { @@ -165,7 +176,18 @@ interface IRoomViewState { joined: boolean; room: | TSubscriptionModel - | { rid: string; t: string; name?: string; fname?: string; prid?: string; joinCodeRequired?: boolean; sysMes?: boolean }; + | { + rid: string; + t: string; + name?: string; + fname?: string; + prid?: string; + joinCodeRequired?: boolean; + status?: boolean; + lastMessage?: ILastMessage; + sysMes?: boolean; + onHold?: boolean; + }; roomUpdate: { [K in TRoomUpdate]?: any; }; @@ -197,6 +219,7 @@ class RoomView extends React.Component { private flatList: TListRef; private mounted: boolean; private offset = 0; + private subObserveQuery?: Subscription; private subSubscription?: Subscription; private queryUnreads?: Subscription; private retryInit = 0; @@ -256,8 +279,14 @@ class RoomView extends React.Component { reacting: false, readOnly: false, unreadsCount: null, - roomUserId + roomUserId, + canViewCannedResponse: false, + canForwardGuest: false, + canReturnQueue: false, + canPlaceLivechatOnHold: false, + isOnHold: false }; + this.setHeader(); if ('id' in room) { @@ -275,6 +304,10 @@ class RoomView extends React.Component { this.flatList = React.createRef(); this.mounted = false; + if (this.t === 'l') { + this.updateOmnichannel(); + } + // we don't need to subscribe to threads if (this.rid && !this.tmid) { this.sub = new RoomClass(this.rid); @@ -314,7 +347,7 @@ class RoomView extends React.Component { shouldComponentUpdate(nextProps: IRoomViewProps, nextState: IRoomViewState) { const { state } = this; - const { roomUpdate, member } = state; + const { roomUpdate, member, isOnHold } = state; const { appState, theme, insets, route } = this.props; if (theme !== nextProps.theme) { return true; @@ -325,7 +358,9 @@ class RoomView extends React.Component { if (member.statusText !== nextState.member.statusText) { return true; } - + if (isOnHold !== nextState.isOnHold) { + return true; + } const stateUpdated = stateAttrsUpdate.some(key => nextState[key] !== state[key]); if (stateUpdated) { return true; @@ -340,7 +375,7 @@ class RoomView extends React.Component { } componentDidUpdate(prevProps: IRoomViewProps, prevState: IRoomViewState) { - const { roomUpdate } = this.state; + const { roomUpdate, joined } = this.state; const { appState, insets, route } = this.props; if (route?.params?.jumpToMessageId && route?.params?.jumpToMessageId !== prevProps.route?.params?.jumpToMessageId) { @@ -365,8 +400,13 @@ class RoomView extends React.Component { } // If it's a livechat room if (this.t === 'l') { - if (!dequal(prevState.roomUpdate.visitor, roomUpdate.visitor)) { - this.setHeader(); + if ( + !dequal(prevState.roomUpdate.lastMessage?.token, roomUpdate.lastMessage?.token) || + !dequal(prevState.roomUpdate.visitor, roomUpdate.visitor) || + !dequal(prevState.roomUpdate.status, roomUpdate.status) || + prevState.joined !== joined + ) { + this.updateOmnichannel(); } } if (roomUpdate.teamMain !== prevState.roomUpdate.teamMain || roomUpdate.teamId !== prevState.roomUpdate.teamId) { @@ -387,6 +427,17 @@ class RoomView extends React.Component { this.setReadOnly(); } + updateOmnichannel = async () => { + const canForwardGuest = await this.canForwardGuest(); + const canPlaceLivechatOnHold = this.canPlaceLivechatOnHold(); + const canReturnQueue = await this.canReturnQueue(); + const canViewCannedResponse = await this.canViewCannedResponse(); + this.setState({ canForwardGuest, canReturnQueue, canViewCannedResponse, canPlaceLivechatOnHold }); + if (this.mounted) { + this.setHeader(); + } + }; + async componentWillUnmount() { const { editing, room } = this.state; const db = database.active; @@ -424,15 +475,16 @@ class RoomView extends React.Component { if (this.subSubscription && this.subSubscription.unsubscribe) { this.subSubscription.unsubscribe(); } + + if (this.subObserveQuery && this.subObserveQuery.unsubscribe) { + this.subObserveQuery.unsubscribe(); + } if (this.queryUnreads && this.queryUnreads.unsubscribe) { this.queryUnreads.unsubscribe(); } if (this.retryInitTimeout) { clearTimeout(this.retryInitTimeout); } - if (this.retryFindTimeout) { - clearTimeout(this.retryFindTimeout); - } EventEmitter.removeListener('connected', this.handleConnected); if (isTablet) { EventEmitter.removeListener(KEY_COMMAND, this.handleCommands); @@ -441,13 +493,61 @@ class RoomView extends React.Component { console.countReset(`${this.constructor.name}.render calls`); } + canForwardGuest = async () => { + const { transferLivechatGuestPermission } = this.props; + const permissions = await hasPermission([transferLivechatGuestPermission], this.rid); + return permissions[0] as boolean; + }; + + canPlaceLivechatOnHold = () => { + const { livechatAllowManualOnHold } = this.props; + const { room } = this.state; + return !!(livechatAllowManualOnHold && !room?.lastMessage?.token && room?.lastMessage?.u && !room.onHold); + }; + + canViewCannedResponse = async () => { + const { viewCannedResponsesPermission } = this.props; + const permissions = await hasPermission([viewCannedResponsesPermission], this.rid); + return permissions[0] as boolean; + }; + + canReturnQueue = async () => { + try { + const { returnQueue } = await getRoutingConfig(); + return returnQueue; + } catch { + return false; + } + }; + + observeSubscriptions = () => { + try { + const db = database.active; + const observeSubCollection = db + .get('subscriptions') + .query(Q.where('rid', this.rid as string)) + .observe(); + this.subObserveQuery = observeSubCollection.subscribe(data => { + if (data[0]) { + if (this.subObserveQuery && this.subObserveQuery.unsubscribe) { + this.observeRoom(data[0]); + this.setState({ room: data[0] }); + this.subObserveQuery.unsubscribe(); + } + } + }); + } catch (e) { + console.log("observeSubscriptions: Can't find subscription to observe"); + } + }; + get isOmnichannel() { const { room } = this.state; return room.t === 'l'; } setHeader = () => { - const { room, unreadsCount, roomUserId, joined } = this.state; + const { room, unreadsCount, roomUserId, joined, canForwardGuest, canReturnQueue, canPlaceLivechatOnHold } = this.state; const { navigation, isMasterDetail, theme, baseUrl, user, route } = this.props; const { rid, tmid } = this; if (!room.rid) { @@ -474,6 +574,7 @@ class RoomView extends React.Component { let token: string | undefined; let avatar: string | undefined; let visitor: IVisitor | undefined; + let status: string | undefined; let sourceType: IOmnichannelSource | undefined; if ('id' in room) { subtitle = room.topic; @@ -484,6 +585,7 @@ class RoomView extends React.Component { ({ id: userId, token } = user); avatar = room.name; visitor = room.visitor; + status = room.status; } if ('source' in room) { @@ -493,11 +595,13 @@ class RoomView extends React.Component { } let numIconsRight = 2; - if (tmid) { + if (tmid || (status && joined)) { numIconsRight = 1; } else if (teamId && isTeamRoom({ teamId, joined })) { numIconsRight = 3; } + const omnichannelPermissions = { canForwardGuest, canReturnQueue, canPlaceLivechatOnHold }; + const paddingRight = this.getPaddingLeft(numIconsRight, isMasterDetail); navigation.setOptions({ headerShown: true, @@ -542,6 +646,8 @@ class RoomView extends React.Component { tmid={tmid} teamId={teamId} joined={joined} + status={room.status} + omnichannelPermissions={omnichannelPermissions} t={this.t || t} encrypted={encrypted} navigation={navigation} @@ -560,7 +666,7 @@ class RoomView extends React.Component { goRoomActionsView = (screen?: keyof ModalStackParamList) => { logEvent(events.ROOM_GO_RA); - const { room, member, joined } = this.state; + const { room, member, joined, canForwardGuest, canReturnQueue, canViewCannedResponse, canPlaceLivechatOnHold } = this.state; const { navigation, isMasterDetail } = this.props; if (isMasterDetail) { // @ts-ignore @@ -572,7 +678,8 @@ class RoomView extends React.Component { room: room as ISubscription, member, showCloseModal: !!screen, - joined + joined, + omnichannelPermissions: { canForwardGuest, canReturnQueue, canViewCannedResponse, canPlaceLivechatOnHold } } }); } else if (this.rid && this.t) { @@ -581,7 +688,8 @@ class RoomView extends React.Component { t: this.t as SubscriptionType, room: room as TSubscriptionModel, member, - joined + joined, + omnichannelPermissions: { canForwardGuest, canReturnQueue, canViewCannedResponse, canPlaceLivechatOnHold } }); } }; @@ -669,15 +777,7 @@ class RoomView extends React.Component { this.internalSetState({ joined: false }); } if (this.rid) { - // We navigate to RoomView before the Room is inserted to the local db - // So we retry just to make sure we have the right content - this.retryFindCount = this.retryFindCount + 1 || 1; - if (this.retryFindCount <= 3) { - this.retryFindTimeout = setTimeout(() => { - this.findAndObserveRoom(rid); - this.init(); - }, 300); - } + this.observeSubscriptions(); } } }; @@ -697,7 +797,7 @@ class RoomView extends React.Component { return ret; }, {}); if (this.mounted) { - this.internalSetState({ room: changes, roomUpdate }); + this.internalSetState({ room: changes, roomUpdate, isOnHold: !!changes?.onHold }); } else { // @ts-ignore this.state.room = changes; @@ -1186,6 +1286,11 @@ class RoomView extends React.Component { }); }; + goToCannedResponses = () => { + const { room } = this.state; + Navigation.navigate('CannedResponsesListView', { rid: room.rid }); + }; + renderItem = (item: TAnyMessageModel, previousItem: TAnyMessageModel, highlightedMessage?: string) => { const { room, lastOpen, canAutoTranslate } = this.state; const { user, Message_GroupingPeriod, Message_TimeFormat, useRealName, baseUrl, Message_Read_Receipt_Enabled, theme } = @@ -1203,7 +1308,6 @@ class RoomView extends React.Component { dateSeparator = item.ts; } } - let content = null; if (item.t && MESSAGE_TYPE_ANY_LOAD.includes(item.t as MessageTypeLoad)) { content = ( @@ -1271,7 +1375,8 @@ class RoomView extends React.Component { }; renderFooter = () => { - const { joined, room, selectedMessage, editing, replying, replyWithMention, readOnly, loading } = this.state; + const { joined, room, selectedMessage, editing, replying, replyWithMention, readOnly, loading, canViewCannedResponse } = + this.state; const { navigation, theme, route } = this.props; const usedCannedResponse = route?.params?.usedCannedResponse; @@ -1338,9 +1443,11 @@ class RoomView extends React.Component { return ( ({ baseUrl: state.server.server, serverVersion: state.server.version, Message_Read_Receipt_Enabled: state.settings.Message_Read_Receipt_Enabled as boolean, - Hide_System_Messages: state.settings.Hide_System_Messages as string[] + Hide_System_Messages: state.settings.Hide_System_Messages as string[], + transferLivechatGuestPermission: state.permissions['transfer-livechat-guest'], + viewCannedResponsesPermission: state.permissions['view-canned-responses'], + livechatAllowManualOnHold: state.settings.Livechat_allow_manual_on_hold as boolean }); export default connect(mapStateToProps)(withDimensions(withTheme(withSafeAreaInsets(RoomView))));