[NEW] Call to Action buttons for Omnichannel (#4060)

* add: kebab and bottom sheet for omnichannel

* update: `RoomActionsView`

* chore: add canned responses to MessageBox

* chore: add omnichannel permissions to RightButtons

chore: missing changes to previous commit on RoomView

* chore: make icon available on first re-render after joining the room

* refactor: omnichannel permissions logic outside RightButtons

* refactor: extract omnichannel permissions' logic from RoomActionsView

* fix: omnichannel permissions on RightButton component

* add: omnichannelPermissions to RoomActions route props

* update: RoomView

* remove: setOmnichannelPermissions function

* refactor: `omnichannelPermissions` to object

* refactor: extract `on-hold` Omnichannel to `RoomView`

* add: `canPlaceLivechatOnHold` to `Omnichannel`s actions

* update: type for `showActionSheet`

* update: canned responses permission on

* fix: place on-hold permission not updating properly

* update: validation for on-hold

remove: unused variable

* remove: unnecessary param

* update: MessageBox

* fix: Omnichannel permissions on taking chat

* undo: comment on Reactotron

* Fix place chat on-hold

* fix setOptions in messageBox

* resolve lint

* change return to return to waiting line

* remove joined

* fix console.log

* fix kebab button

* fix subscription to room outside of db

Co-authored-by: Reinaldo Neto <reinaldonetof@hotmail.com>
Co-authored-by: Reinaldo Neto <47038980+reinaldonetof@users.noreply.github.com>
This commit is contained in:
Gerzon Z 2022-06-09 10:42:12 -04:00 committed by GitHub
parent d47320733a
commit 20a03611a7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 412 additions and 215 deletions

View File

@ -46,12 +46,22 @@ import { withActionSheet } from '../ActionSheet';
import { sanitizeLikeString } from '../../lib/database/utils'; import { sanitizeLikeString } from '../../lib/database/utils';
import { CustomIcon } from '../CustomIcon'; import { CustomIcon } from '../CustomIcon';
import { forceJpgExtension } from './forceJpgExtension'; 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 { MasterDetailInsideStackParamList } from '../../stacks/MasterDetailStack/types';
import { getPermalinkMessage, search, sendFileMessage } from '../../lib/methods'; import { getPermalinkMessage, search, sendFileMessage } from '../../lib/methods';
import { hasPermission, debounce, isAndroid, isTablet } from '../../lib/methods/helpers'; import { hasPermission, debounce, isAndroid, isTablet } from '../../lib/methods/helpers';
import { Services } from '../../lib/services'; import { Services } from '../../lib/services';
import { TSupportedThemes } from '../../theme'; import { TSupportedThemes } from '../../theme';
import { ChatsStackParamList } from '../../stacks/types';
if (isAndroid) { if (isAndroid) {
require('./EmojiKeyboard'); require('./EmojiKeyboard');
@ -75,7 +85,7 @@ const videoPickerConfig: Options = {
mediaType: 'video' mediaType: 'video'
}; };
export interface IMessageBoxProps extends IBaseScreen<MasterDetailInsideStackParamList, any> { export interface IMessageBoxProps extends IBaseScreen<ChatsStackParamList & MasterDetailInsideStackParamList, any> {
rid: string; rid: string;
baseUrl: string; baseUrl: string;
message: IMessage; message: IMessage;
@ -107,6 +117,7 @@ export interface IMessageBoxProps extends IBaseScreen<MasterDetailInsideStackPar
usedCannedResponse: string; usedCannedResponse: string;
uploadFilePermission: string[]; uploadFilePermission: string[];
serverVersion: string; serverVersion: string;
goToCannedResponses: () => void | null;
} }
interface IMessageBoxState { interface IMessageBoxState {
@ -305,7 +316,17 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
permissionToUpload permissionToUpload
} = this.state; } = 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) { if (nextProps.theme !== theme) {
return true; return true;
} }
@ -357,12 +378,15 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
if (nextProps.usedCannedResponse !== usedCannedResponse) { if (nextProps.usedCannedResponse !== usedCannedResponse) {
return true; return true;
} }
if (nextProps.goToCannedResponses !== goToCannedResponses) {
return true;
}
return false; return false;
} }
componentDidUpdate(prevProps: IMessageBoxProps) { componentDidUpdate(prevProps: IMessageBoxProps) {
const { uploadFilePermission } = this.props; const { uploadFilePermission, goToCannedResponses } = this.props;
if (!dequal(prevProps.uploadFilePermission, uploadFilePermission)) { if (!dequal(prevProps.uploadFilePermission, uploadFilePermission) || prevProps.goToCannedResponses !== goToCannedResponses) {
this.setOptions(); this.setOptions();
} }
} }
@ -783,9 +807,16 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
showMessageBoxActions = () => { showMessageBoxActions = () => {
logEvent(events.ROOM_SHOW_BOX_ACTIONS); logEvent(events.ROOM_SHOW_BOX_ACTIONS);
const { permissionToUpload } = this.state; const { permissionToUpload } = this.state;
const { showActionSheet } = this.props; const { showActionSheet, goToCannedResponses } = this.props;
const options = []; const options = [];
if (goToCannedResponses) {
options.push({
title: I18n.t('Canned_Responses'),
icon: 'canned-response',
onPress: () => goToCannedResponses()
});
}
if (permissionToUpload) { if (permissionToUpload) {
options.push( options.push(
{ {
@ -1170,7 +1201,7 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
} }
} }
const mapStateToProps = (state: any) => ({ const mapStateToProps = (state: IApplicationState) => ({
isMasterDetail: state.app.isMasterDetail, isMasterDetail: state.app.isMasterDetail,
baseUrl: state.server.server, baseUrl: state.server.server,
threadsEnabled: state.settings.Threads_enabled, threadsEnabled: state.settings.Threads_enabled,

View File

@ -198,7 +198,7 @@ export interface IServerRoom extends IRocketChatRecord {
unread?: number; unread?: number;
alert?: boolean; alert?: boolean;
hideUnreadStatus?: boolean; hideUnreadStatus?: boolean;
status?: string;
sysMes?: string[]; sysMes?: string[];
muted?: string[]; muted?: string[];
unmuted?: string[]; unmuted?: string[];

View File

@ -421,7 +421,6 @@
"Reset_password": "إعادة تعيين كلمة المرور", "Reset_password": "إعادة تعيين كلمة المرور",
"resetting_password": "إعادة تعيين كلمة المرور", "resetting_password": "إعادة تعيين كلمة المرور",
"RESET": "إعادة", "RESET": "إعادة",
"Return": "العودة",
"Review_app_title": "هل أنت مستمتع بهذا التطبيق؟", "Review_app_title": "هل أنت مستمتع بهذا التطبيق؟",
"Review_app_desc": "قم بإعطائنا 5 نجوم {{store}}", "Review_app_desc": "قم بإعطائنا 5 نجوم {{store}}",
"Review_app_yes": "أكيد!", "Review_app_yes": "أكيد!",

View File

@ -427,7 +427,6 @@
"Reset_password": "Passwort zurücksetzen", "Reset_password": "Passwort zurücksetzen",
"resetting_password": "Passwort zurücksetzen", "resetting_password": "Passwort zurücksetzen",
"RESET": "ZURÜCKSETZEN", "RESET": "ZURÜCKSETZEN",
"Return": "Zurück",
"Review_app_title": "Gefällt Ihnen diese App?", "Review_app_title": "Gefällt Ihnen diese App?",
"Review_app_desc": "Geben Sie uns 5 Sterne im {{store}}", "Review_app_desc": "Geben Sie uns 5 Sterne im {{store}}",
"Review_app_yes": "Sicher!", "Review_app_yes": "Sicher!",

View File

@ -431,7 +431,7 @@
"Reset_password": "Reset password", "Reset_password": "Reset password",
"resetting_password": "resetting password", "resetting_password": "resetting password",
"RESET": "RESET", "RESET": "RESET",
"Return": "Return", "Return_to_waiting_line": "Return to waiting line",
"Review_app_title": "Are you enjoying this app?", "Review_app_title": "Are you enjoying this app?",
"Review_app_desc": "Give us 5 stars on {{store}}", "Review_app_desc": "Give us 5 stars on {{store}}",
"Review_app_yes": "Sure!", "Review_app_yes": "Sure!",

View File

@ -427,7 +427,6 @@
"Reset_password": "Réinitialiser le mot de passe", "Reset_password": "Réinitialiser le mot de passe",
"resetting_password": "réinitialisation du mot de passe", "resetting_password": "réinitialisation du mot de passe",
"RESET": "RÉINITIALISER", "RESET": "RÉINITIALISER",
"Return": "Retour",
"Review_app_title": "Appréciez-vous cette application ?", "Review_app_title": "Appréciez-vous cette application ?",
"Review_app_desc": "Donnez-nous 5 étoiles sur {{store}}", "Review_app_desc": "Donnez-nous 5 étoiles sur {{store}}",
"Review_app_yes": "Bien sûr !", "Review_app_yes": "Bien sûr !",

View File

@ -416,7 +416,6 @@
"Reset_password": "Ripristina password", "Reset_password": "Ripristina password",
"resetting_password": "ripristinando password", "resetting_password": "ripristinando password",
"RESET": "RIPRISTINA", "RESET": "RIPRISTINA",
"Return": "Ritorno",
"Review_app_title": "Ti piace questa app?", "Review_app_title": "Ti piace questa app?",
"Review_app_desc": "Dacci 5 stesse su {{store}}", "Review_app_desc": "Dacci 5 stesse su {{store}}",
"Review_app_yes": "Certo!", "Review_app_yes": "Certo!",

View File

@ -427,7 +427,6 @@
"Reset_password": "Wachtwoord resetten", "Reset_password": "Wachtwoord resetten",
"resetting_password": "wachtwoord aan het resetten", "resetting_password": "wachtwoord aan het resetten",
"RESET": "RESET", "RESET": "RESET",
"Return": "Terug",
"Review_app_title": "Geniet je van deze app?", "Review_app_title": "Geniet je van deze app?",
"Review_app_desc": "Geef ons 5 sterren op {{store}}", "Review_app_desc": "Geef ons 5 sterren op {{store}}",
"Review_app_yes": "Zeker!", "Review_app_yes": "Zeker!",

View File

@ -403,7 +403,6 @@
"Reset_password": "Resetar senha", "Reset_password": "Resetar senha",
"resetting_password": "redefinindo senha", "resetting_password": "redefinindo senha",
"RESET": "RESETAR", "RESET": "RESETAR",
"Return": "Retornar",
"Review_app_title": "Você está gostando do app?", "Review_app_title": "Você está gostando do app?",
"Review_app_desc": "Nos dê 5 estrelas na {{store}}", "Review_app_desc": "Nos dê 5 estrelas na {{store}}",
"Review_app_yes": "Claro!", "Review_app_yes": "Claro!",

View File

@ -427,7 +427,6 @@
"Reset_password": "Сброс пароля", "Reset_password": "Сброс пароля",
"resetting_password": "сброс пароля", "resetting_password": "сброс пароля",
"RESET": "СБРОС", "RESET": "СБРОС",
"Return": "Возврат",
"Review_app_title": "Нравится ли вам это приложение?", "Review_app_title": "Нравится ли вам это приложение?",
"Review_app_desc": "Поставьте нам 5 звезд в {{store}}", "Review_app_desc": "Поставьте нам 5 звезд в {{store}}",
"Review_app_yes": "Конечно!", "Review_app_yes": "Конечно!",

View File

@ -417,7 +417,6 @@
"Reset_password": "Şifre sıfırla", "Reset_password": "Şifre sıfırla",
"resetting_password": "şifre sıfırlanıyor", "resetting_password": "şifre sıfırlanıyor",
"RESET": "SIFIRLA", "RESET": "SIFIRLA",
"Return": "Geri dön",
"Review_app_title": "Uygulama hoşunuza gitti mi?", "Review_app_title": "Uygulama hoşunuza gitti mi?",
"Review_app_desc": "{{store}} üzerinde bize 5 yıldız verin", "Review_app_desc": "{{store}} üzerinde bize 5 yıldız verin",
"Review_app_yes": "Elbette!", "Review_app_yes": "Elbette!",

View File

@ -414,7 +414,6 @@
"Reset_password": "重置密码", "Reset_password": "重置密码",
"resetting_password": "正在重置密码", "resetting_password": "正在重置密码",
"RESET": "重置", "RESET": "重置",
"Return": "返回",
"Review_app_title": "对此 App 满意吗?", "Review_app_title": "对此 App 满意吗?",
"Review_app_desc": "请在 {{store}} 给予我们 5 星好评", "Review_app_desc": "请在 {{store}} 给予我们 5 星好评",
"Review_app_yes": "没问题", "Review_app_yes": "没问题",

View File

@ -416,7 +416,6 @@
"Reset_password": "重置密碼", "Reset_password": "重置密碼",
"resetting_password": "正在重置密碼", "resetting_password": "正在重置密碼",
"RESET": "重置", "RESET": "重置",
"Return": "返回",
"Review_app_title": "對此 App 滿意嗎?", "Review_app_title": "對此 App 滿意嗎?",
"Review_app_desc": "請在 {{store}} 給予我們 5 星好評", "Review_app_desc": "請在 {{store}} 給予我們 5 星好評",
"Review_app_yes": "沒問題", "Review_app_yes": "沒問題",

View File

@ -193,6 +193,7 @@ export default {
ROOM_AUDIO_FINISH_F: 'room_audio_finish_f', ROOM_AUDIO_FINISH_F: 'room_audio_finish_f',
ROOM_AUDIO_CANCEL: 'room_audio_cancel', ROOM_AUDIO_CANCEL: 'room_audio_cancel',
ROOM_AUDIO_CANCEL_F: 'room_audio_cancel_f', 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_SHOW_BOX_ACTIONS: 'room_show_box_actions',
ROOM_BOX_ACTION_PHOTO: 'room_box_action_photo', ROOM_BOX_ACTION_PHOTO: 'room_box_action_photo',
ROOM_BOX_ACTION_PHOTO_F: 'room_box_action_photo_f', ROOM_BOX_ACTION_PHOTO_F: 'room_box_action_photo_f',

View File

@ -32,6 +32,12 @@ export type ModalStackParamList = {
rid: string; rid: string;
t: SubscriptionType; t: SubscriptionType;
joined: boolean; joined: boolean;
omnichannelPermissions?: {
canForwardGuest: boolean;
canReturnQueue: boolean;
canViewCannedResponse: boolean;
canPlaceLivechatOnHold: boolean;
};
}; };
RoomInfoView: { RoomInfoView: {
room: ISubscription; room: ISubscription;

View File

@ -34,6 +34,7 @@ export type ChatsStackParamList = {
jumpToThreadId?: string; jumpToThreadId?: string;
roomUserId?: string | null; roomUserId?: string | null;
usedCannedResponse?: string; usedCannedResponse?: string;
status?: string;
} }
| undefined; // Navigates back to RoomView already on stack | undefined; // Navigates back to RoomView already on stack
RoomActionsView: { RoomActionsView: {
@ -42,6 +43,12 @@ export type ChatsStackParamList = {
rid: string; rid: string;
t: SubscriptionType; t: SubscriptionType;
joined: boolean; joined: boolean;
omnichannelPermissions?: {
canForwardGuest: boolean;
canReturnQueue: boolean;
canViewCannedResponse: boolean;
canPlaceLivechatOnHold: boolean;
};
}; };
SelectListView: { SelectListView: {
data?: TDataSelect[]; data?: TDataSelect[];

View File

@ -64,12 +64,10 @@ interface IRoomActionsViewProps extends IBaseScreen<ChatsStackParamList, 'RoomAc
editRoomPermission?: string[]; editRoomPermission?: string[];
toggleRoomE2EEncryptionPermission?: string[]; toggleRoomE2EEncryptionPermission?: string[];
viewBroadcastMemberListPermission?: string[]; viewBroadcastMemberListPermission?: string[];
transferLivechatGuestPermission?: string[];
createTeamPermission?: string[]; createTeamPermission?: string[];
addTeamChannelPermission?: string[]; addTeamChannelPermission?: string[];
convertTeamPermission?: string[]; convertTeamPermission?: string[];
viewCannedResponsesPermission?: string[]; viewCannedResponsesPermission?: string[];
livechatAllowManualOnHold?: boolean;
} }
interface IRoomActionsViewState { interface IRoomActionsViewState {
@ -81,16 +79,11 @@ interface IRoomActionsViewState {
canAutoTranslate: boolean; canAutoTranslate: boolean;
canAddUser: boolean; canAddUser: boolean;
canInviteUser: boolean; canInviteUser: boolean;
canForwardGuest: boolean;
canReturnQueue: boolean;
canEdit: boolean; canEdit: boolean;
canToggleEncryption: boolean; canToggleEncryption: boolean;
canCreateTeam: boolean; canCreateTeam: boolean;
canAddChannelToTeam: boolean; canAddChannelToTeam: boolean;
canConvertTeam: boolean; canConvertTeam: boolean;
canViewCannedResponse: boolean;
canPlaceLivechatOnHold: boolean;
isOnHold: boolean;
} }
class RoomActionsView extends React.Component<IRoomActionsViewProps, IRoomActionsViewState> { class RoomActionsView extends React.Component<IRoomActionsViewProps, IRoomActionsViewState> {
@ -98,6 +91,12 @@ class RoomActionsView extends React.Component<IRoomActionsViewProps, IRoomAction
private rid: string; private rid: string;
private t: string; private t: string;
private joined: boolean; private joined: boolean;
private omnichannelPermissions?: {
canForwardGuest: boolean;
canReturnQueue: boolean;
canViewCannedResponse: boolean;
canPlaceLivechatOnHold: boolean;
};
private roomObservable?: Observable<TSubscriptionModel>; private roomObservable?: Observable<TSubscriptionModel>;
private subscription?: Subscription; private subscription?: Subscription;
@ -122,6 +121,7 @@ class RoomActionsView extends React.Component<IRoomActionsViewProps, IRoomAction
this.rid = props.route.params?.rid; this.rid = props.route.params?.rid;
this.t = props.route.params?.t; this.t = props.route.params?.t;
this.joined = props.route.params?.joined; this.joined = props.route.params?.joined;
this.omnichannelPermissions = props.route.params?.omnichannelPermissions;
this.state = { this.state = {
room: room || { rid: this.rid, t: this.t }, room: room || { rid: this.rid, t: this.t },
membersCount: 0, membersCount: 0,
@ -131,22 +131,17 @@ class RoomActionsView extends React.Component<IRoomActionsViewProps, IRoomAction
canAutoTranslate: false, canAutoTranslate: false,
canAddUser: false, canAddUser: false,
canInviteUser: false, canInviteUser: false,
canForwardGuest: false,
canReturnQueue: false,
canEdit: false, canEdit: false,
canToggleEncryption: false, canToggleEncryption: false,
canCreateTeam: false, canCreateTeam: false,
canAddChannelToTeam: false, canAddChannelToTeam: false,
canConvertTeam: false, canConvertTeam: false
canViewCannedResponse: false,
canPlaceLivechatOnHold: false,
isOnHold: false
}; };
if (room && room.observe && room.rid) { if (room && room.observe && room.rid) {
this.roomObservable = room.observe(); this.roomObservable = room.observe();
this.subscription = this.roomObservable.subscribe(changes => { this.subscription = this.roomObservable.subscribe(changes => {
if (this.mounted) { if (this.mounted) {
this.setState({ room: changes, isOnHold: !!changes?.onHold }); this.setState({ room: changes });
} else { } else {
// @ts-ignore // @ts-ignore
this.state.room = changes; this.state.room = changes;
@ -214,28 +209,6 @@ class RoomActionsView extends React.Component<IRoomActionsViewProps, IRoomAction
canAddChannelToTeam, canAddChannelToTeam,
canConvertTeam canConvertTeam
}); });
// livechat permissions
if (room.t === 'l') {
const canForwardGuest = await this.canForwardGuest();
const canReturnQueue = await this.canReturnQueue();
const canViewCannedResponse = await this.canViewCannedResponse();
const canPlaceLivechatOnHold = this.canPlaceLivechatOnHold();
this.setState({ canForwardGuest, canReturnQueue, canViewCannedResponse, canPlaceLivechatOnHold });
}
}
}
componentDidUpdate(prevProps: IRoomActionsViewProps, prevState: IRoomActionsViewState) {
const { livechatAllowManualOnHold } = this.props;
const { room, isOnHold } = this.state;
if (
room.t === 'l' &&
(isOnHold !== prevState.isOnHold || prevProps.livechatAllowManualOnHold !== livechatAllowManualOnHold)
) {
const canPlaceLivechatOnHold = this.canPlaceLivechatOnHold();
this.setState({ canPlaceLivechatOnHold });
} }
} }
@ -372,38 +345,6 @@ class RoomActionsView extends React.Component<IRoomActionsViewProps, IRoomAction
return result; return result;
}; };
canForwardGuest = async () => {
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 = () => { renderEncryptedSwitch = () => {
const { room, canToggleEncryption, canEdit } = this.state; const { room, canToggleEncryption, canEdit } = this.state;
const { encrypted } = room; const { encrypted } = room;
@ -1047,20 +988,86 @@ class RoomActionsView extends React.Component<IRoomActionsViewProps, IRoomAction
); );
}; };
renderOmnichannelSection = () => {
const { room } = this.state;
const { rid, t } = room;
const { theme } = this.props;
if (t !== 'l' || this.isOmnichannelPreview) {
return null;
}
return (
<List.Section>
{this.omnichannelPermissions?.canForwardGuest ? (
<>
<List.Item
title='Forward'
onPress={() =>
this.onPressTouchable({
route: 'ForwardLivechatView',
params: { rid }
})
}
left={() => <List.Icon name='chat-forward' color={themes[theme].titleText} />}
showActionIndicator
/>
<List.Separator />
</>
) : null}
{this.omnichannelPermissions?.canPlaceLivechatOnHold ? (
<>
<List.Item
title='Place_chat_on_hold'
onPress={() =>
this.onPressTouchable({
event: this.placeOnHoldLivechat
})
}
left={() => <List.Icon name='pause' color={themes[theme].titleText} />}
showActionIndicator
/>
<List.Separator />
</>
) : null}
{this.omnichannelPermissions?.canReturnQueue ? (
<>
<List.Item
title='Return_to_waiting_line'
onPress={() =>
this.onPressTouchable({
event: this.returnLivechat
})
}
left={() => <List.Icon name='move-to-the-queue' color={themes[theme].titleText} />}
showActionIndicator
/>
<List.Separator />
</>
) : null}
<>
<List.Item
title='Close'
color={themes[theme].dangerColor}
onPress={() =>
this.onPressTouchable({
event: this.closeLivechat
})
}
left={() => <List.Icon name='chat-close' color={themes[theme].dangerColor} />}
showActionIndicator
/>
<List.Separator />
</>
</List.Section>
);
};
render() { render() {
const { const { room, membersCount, canViewMembers, canAddUser, canInviteUser, joined, canAutoTranslate } = this.state;
room,
membersCount,
canViewMembers,
canAddUser,
canInviteUser,
joined,
canAutoTranslate,
canForwardGuest,
canReturnQueue,
canViewCannedResponse,
canPlaceLivechatOnHold
} = this.state;
const { rid, t, prid } = room; const { rid, t, prid } = room;
const isGroupChatHandler = isGroupChat(room); const isGroupChatHandler = isGroupChat(room);
@ -1149,6 +1156,18 @@ class RoomActionsView extends React.Component<IRoomActionsViewProps, IRoomAction
</> </>
) : null} ) : null}
{['l'].includes(t) && !this.isOmnichannelPreview && this.omnichannelPermissions?.canViewCannedResponse ? (
<>
<List.Item
title='Canned_Responses'
onPress={() => this.onPressTouchable({ route: 'CannedResponsesListView', params: { rid } })}
left={() => <List.Icon name='canned-response' />}
showActionIndicator
/>
<List.Separator />
</>
) : null}
{['c', 'p', 'd'].includes(t) ? ( {['c', 'p', 'd'].includes(t) ? (
<> <>
<List.Item <List.Item
@ -1276,85 +1295,8 @@ class RoomActionsView extends React.Component<IRoomActionsViewProps, IRoomAction
{this.teamChannelActions(t, room)} {this.teamChannelActions(t, room)}
{this.teamToChannelActions(t, room)} {this.teamToChannelActions(t, room)}
{['l'].includes(t) && !this.isOmnichannelPreview && canViewCannedResponse ? (
<>
<List.Item
title='Canned_Responses'
onPress={() => this.onPressTouchable({ route: 'CannedResponsesListView', params: { rid } })}
left={() => <List.Icon name='canned-response' />}
showActionIndicator
/>
<List.Separator />
</>
) : null}
{['l'].includes(t) && !this.isOmnichannelPreview ? (
<>
<List.Item
title='Close'
onPress={() =>
this.onPressTouchable({
event: this.closeLivechat
})
}
left={() => <List.Icon name='close' />}
showActionIndicator
/>
<List.Separator />
</>
) : null}
{['l'].includes(t) && !this.isOmnichannelPreview && canForwardGuest ? (
<>
<List.Item
title='Forward'
onPress={() =>
this.onPressTouchable({
route: 'ForwardLivechatView',
params: { rid }
})
}
left={() => <List.Icon name='user-forward' />}
showActionIndicator
/>
<List.Separator />
</>
) : null}
{['l'].includes(t) && !this.isOmnichannelPreview && canPlaceLivechatOnHold ? (
<>
<List.Item
title='Place_chat_on_hold'
onPress={() =>
this.onPressTouchable({
event: this.placeOnHoldLivechat
})
}
left={() => <List.Icon name='pause' />}
showActionIndicator
/>
<List.Separator />
</>
) : null}
{['l'].includes(t) && !this.isOmnichannelPreview && canReturnQueue ? (
<>
<List.Item
title='Return'
onPress={() =>
this.onPressTouchable({
event: this.returnLivechat
})
}
left={() => <List.Icon name='undo' />}
showActionIndicator
/>
<List.Separator />
</>
) : null}
</List.Section> </List.Section>
{this.renderOmnichannelSection()}
{this.renderLastSection()} {this.renderLastSection()}
</List.Container> </List.Container>
</SafeAreaView> </SafeAreaView>
@ -1377,12 +1319,9 @@ const mapStateToProps = (state: IApplicationState) => ({
editRoomPermission: state.permissions['edit-room'], editRoomPermission: state.permissions['edit-room'],
toggleRoomE2EEncryptionPermission: state.permissions['toggle-room-e2e-encryption'], toggleRoomE2EEncryptionPermission: state.permissions['toggle-room-e2e-encryption'],
viewBroadcastMemberListPermission: state.permissions['view-broadcast-member-list'], viewBroadcastMemberListPermission: state.permissions['view-broadcast-member-list'],
transferLivechatGuestPermission: state.permissions['transfer-livechat-guest'],
createTeamPermission: state.permissions['create-team'], createTeamPermission: state.permissions['create-team'],
addTeamChannelPermission: state.permissions['add-team-channel'], addTeamChannelPermission: state.permissions['add-team-channel'],
convertTeamPermission: state.permissions['convert-team'], convertTeamPermission: state.permissions['convert-team']
viewCannedResponsesPermission: state.permissions['view-canned-responses'],
livechatAllowManualOnHold: state.settings.Livechat_allow_manual_on_hold as boolean
}); });
export default connect(mapStateToProps)(withTheme(withDimensions(RoomActionsView))); export default connect(mapStateToProps)(withTheme(withDimensions(RoomActionsView)));

View File

@ -2,6 +2,7 @@ import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { dequal } from 'dequal'; import { dequal } from 'dequal';
import { Observable, Subscription } from 'rxjs'; import { Observable, Subscription } from 'rxjs';
import { Dispatch } from 'redux';
import { StackNavigationProp } from '@react-navigation/stack'; import { StackNavigationProp } from '@react-navigation/stack';
import * as HeaderButton from '../../containers/HeaderButton'; 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 { isTeamRoom } from '../../lib/methods/helpers/room';
import { IApplicationState, SubscriptionType, TMessageModel, TSubscriptionModel } from '../../definitions'; import { IApplicationState, SubscriptionType, TMessageModel, TSubscriptionModel } from '../../definitions';
import { ChatsStackParamList } from '../../stacks/types'; 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 { interface IRightButtonsProps {
userId?: string; userId?: string;
threadsEnabled: boolean; threadsEnabled: boolean;
rid?: string; rid: string;
t: string; t: string;
tmid?: string; tmid?: string;
teamId?: string; teamId?: string;
isMasterDetail: boolean; isMasterDetail: boolean;
toggleFollowThread: Function; toggleFollowThread: Function;
joined: boolean; joined: boolean;
status?: string;
dispatch: Dispatch;
encrypted?: boolean; encrypted?: boolean;
showActionSheet: (item: TActionSheetOptions) => void;
transferLivechatGuestPermission: boolean;
navigation: StackNavigationProp<ChatsStackParamList, 'RoomView'>; navigation: StackNavigationProp<ChatsStackParamList, 'RoomView'>;
omnichannelPermissions: {
canForwardGuest: boolean;
canReturnQueue: boolean;
canPlaceLivechatOnHold: boolean;
};
} }
interface IRigthButtonsState { interface IRigthButtonsState {
@ -71,13 +86,22 @@ class RightButtonsContainer extends Component<IRightButtonsProps, IRigthButtonsS
shouldComponentUpdate(nextProps: IRightButtonsProps, nextState: IRigthButtonsState) { shouldComponentUpdate(nextProps: IRightButtonsProps, nextState: IRigthButtonsState) {
const { isFollowingThread, tunread, tunreadUser, tunreadGroup } = this.state; const { isFollowingThread, tunread, tunreadUser, tunreadGroup } = this.state;
const { teamId } = this.props; const { teamId, status, joined, omnichannelPermissions } = this.props;
if (nextProps.teamId !== teamId) { if (nextProps.teamId !== teamId) {
return true; return true;
} }
if (nextProps.status !== status) {
return true;
}
if (nextProps.joined !== joined) {
return true;
}
if (nextState.isFollowingThread !== isFollowingThread) { if (nextState.isFollowingThread !== isFollowingThread) {
return true; return true;
} }
if (!dequal(nextProps.omnichannelPermissions, omnichannelPermissions)) {
return true;
}
if (!dequal(nextState.tunread, tunread)) { if (!dequal(nextState.tunread, tunread)) {
return true; return true;
} }
@ -157,6 +181,82 @@ class RightButtonsContainer extends Component<IRightButtonsProps, IRigthButtonsS
} }
}; };
returnLivechat = () => {
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 = () => { goSearchView = () => {
logEvent(events.ROOM_GO_SEARCH); logEvent(events.ROOM_GO_SEARCH);
const { rid, t, navigation, isMasterDetail, encrypted } = this.props; const { rid, t, navigation, isMasterDetail, encrypted } = this.props;
@ -183,10 +283,23 @@ class RightButtonsContainer extends Component<IRightButtonsProps, IRigthButtonsS
} }
}; };
isOmnichannelPreview = () => {
const { status } = this.props;
return status === 'queued';
};
render() { render() {
const { isFollowingThread, tunread, tunreadUser, tunreadGroup } = this.state; const { isFollowingThread, tunread, tunreadUser, tunreadGroup } = this.state;
const { t, tmid, threadsEnabled, teamId, joined } = this.props; const { t, tmid, threadsEnabled, teamId, joined } = this.props;
if (t === 'l') { if (t === 'l') {
if (!this.isOmnichannelPreview()) {
return (
<HeaderButton.Container>
<HeaderButton.Item iconName='kebab' onPress={this.showMoreActions} testID='room-view-header-omnichannel-kebab' />
</HeaderButton.Container>
);
}
return null; return null;
} }
if (tmid) { if (tmid) {
@ -225,4 +338,4 @@ const mapStateToProps = (state: IApplicationState) => ({
isMasterDetail: state.app.isMasterDetail isMasterDetail: state.app.isMasterDetail
}); });
export default connect(mapStateToProps)(RightButtonsContainer); export default connect(mapStateToProps)(withActionSheet(RightButtonsContainer));

View File

@ -9,6 +9,7 @@ import { dequal } from 'dequal';
import { EdgeInsets, withSafeAreaInsets } from 'react-native-safe-area-context'; import { EdgeInsets, withSafeAreaInsets } from 'react-native-safe-area-context';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { getRoutingConfig } from '../../lib/services/restApi';
import Touch from '../../lib/methods/helpers/touch'; import Touch from '../../lib/methods/helpers/touch';
import { replyBroadcast } from '../../actions/messages'; import { replyBroadcast } from '../../actions/messages';
import database from '../../lib/database'; import database from '../../lib/database';
@ -63,6 +64,7 @@ import {
IApplicationState, IApplicationState,
IAttachment, IAttachment,
IBaseScreen, IBaseScreen,
ILastMessage,
ILoggedUser, ILoggedUser,
IMessage, IMessage,
IOmnichannelSource, IOmnichannelSource,
@ -94,7 +96,8 @@ import {
canAutoTranslate as canAutoTranslateMethod, canAutoTranslate as canAutoTranslateMethod,
debounce, debounce,
isIOS, isIOS,
isTablet isTablet,
hasPermission
} from '../../lib/methods/helpers'; } from '../../lib/methods/helpers';
import { Services } from '../../lib/services'; import { Services } from '../../lib/services';
@ -112,7 +115,10 @@ const stateAttrsUpdate = [
'reacting', 'reacting',
'readOnly', 'readOnly',
'member', 'member',
'showingBlockingLoader' 'showingBlockingLoader',
'canForwardGuest',
'canReturnQueue',
'canViewCannedResponse'
] as TStateAttrsUpdate[]; ] as TStateAttrsUpdate[];
type TRoomUpdate = keyof TSubscriptionModel; type TRoomUpdate = keyof TSubscriptionModel;
@ -138,6 +144,8 @@ const roomAttrsUpdate = [
'joinCodeRequired', 'joinCodeRequired',
'teamMain', 'teamMain',
'teamId', 'teamId',
'status',
'lastMessage',
'onHold' 'onHold'
] as TRoomUpdate[]; ] as TRoomUpdate[];
@ -158,6 +166,9 @@ interface IRoomViewProps extends IBaseScreen<ChatsStackParamList, 'RoomView'> {
width: number; width: number;
height: number; height: number;
insets: EdgeInsets; insets: EdgeInsets;
transferLivechatGuestPermission?: string[]; // TODO: Check if its the correct type
viewCannedResponsesPermission?: string[]; // TODO: Check if its the correct type
livechatAllowManualOnHold?: boolean;
} }
interface IRoomViewState { interface IRoomViewState {
@ -165,7 +176,18 @@ interface IRoomViewState {
joined: boolean; joined: boolean;
room: room:
| TSubscriptionModel | 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: { roomUpdate: {
[K in TRoomUpdate]?: any; [K in TRoomUpdate]?: any;
}; };
@ -197,6 +219,7 @@ class RoomView extends React.Component<IRoomViewProps, IRoomViewState> {
private flatList: TListRef; private flatList: TListRef;
private mounted: boolean; private mounted: boolean;
private offset = 0; private offset = 0;
private subObserveQuery?: Subscription;
private subSubscription?: Subscription; private subSubscription?: Subscription;
private queryUnreads?: Subscription; private queryUnreads?: Subscription;
private retryInit = 0; private retryInit = 0;
@ -256,8 +279,14 @@ class RoomView extends React.Component<IRoomViewProps, IRoomViewState> {
reacting: false, reacting: false,
readOnly: false, readOnly: false,
unreadsCount: null, unreadsCount: null,
roomUserId roomUserId,
canViewCannedResponse: false,
canForwardGuest: false,
canReturnQueue: false,
canPlaceLivechatOnHold: false,
isOnHold: false
}; };
this.setHeader(); this.setHeader();
if ('id' in room) { if ('id' in room) {
@ -275,6 +304,10 @@ class RoomView extends React.Component<IRoomViewProps, IRoomViewState> {
this.flatList = React.createRef(); this.flatList = React.createRef();
this.mounted = false; this.mounted = false;
if (this.t === 'l') {
this.updateOmnichannel();
}
// we don't need to subscribe to threads // we don't need to subscribe to threads
if (this.rid && !this.tmid) { if (this.rid && !this.tmid) {
this.sub = new RoomClass(this.rid); this.sub = new RoomClass(this.rid);
@ -314,7 +347,7 @@ class RoomView extends React.Component<IRoomViewProps, IRoomViewState> {
shouldComponentUpdate(nextProps: IRoomViewProps, nextState: IRoomViewState) { shouldComponentUpdate(nextProps: IRoomViewProps, nextState: IRoomViewState) {
const { state } = this; const { state } = this;
const { roomUpdate, member } = state; const { roomUpdate, member, isOnHold } = state;
const { appState, theme, insets, route } = this.props; const { appState, theme, insets, route } = this.props;
if (theme !== nextProps.theme) { if (theme !== nextProps.theme) {
return true; return true;
@ -325,7 +358,9 @@ class RoomView extends React.Component<IRoomViewProps, IRoomViewState> {
if (member.statusText !== nextState.member.statusText) { if (member.statusText !== nextState.member.statusText) {
return true; return true;
} }
if (isOnHold !== nextState.isOnHold) {
return true;
}
const stateUpdated = stateAttrsUpdate.some(key => nextState[key] !== state[key]); const stateUpdated = stateAttrsUpdate.some(key => nextState[key] !== state[key]);
if (stateUpdated) { if (stateUpdated) {
return true; return true;
@ -340,7 +375,7 @@ class RoomView extends React.Component<IRoomViewProps, IRoomViewState> {
} }
componentDidUpdate(prevProps: IRoomViewProps, prevState: IRoomViewState) { componentDidUpdate(prevProps: IRoomViewProps, prevState: IRoomViewState) {
const { roomUpdate } = this.state; const { roomUpdate, joined } = this.state;
const { appState, insets, route } = this.props; const { appState, insets, route } = this.props;
if (route?.params?.jumpToMessageId && route?.params?.jumpToMessageId !== prevProps.route?.params?.jumpToMessageId) { if (route?.params?.jumpToMessageId && route?.params?.jumpToMessageId !== prevProps.route?.params?.jumpToMessageId) {
@ -365,8 +400,13 @@ class RoomView extends React.Component<IRoomViewProps, IRoomViewState> {
} }
// If it's a livechat room // If it's a livechat room
if (this.t === 'l') { if (this.t === 'l') {
if (!dequal(prevState.roomUpdate.visitor, roomUpdate.visitor)) { if (
this.setHeader(); !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) { if (roomUpdate.teamMain !== prevState.roomUpdate.teamMain || roomUpdate.teamId !== prevState.roomUpdate.teamId) {
@ -387,6 +427,17 @@ class RoomView extends React.Component<IRoomViewProps, IRoomViewState> {
this.setReadOnly(); 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() { async componentWillUnmount() {
const { editing, room } = this.state; const { editing, room } = this.state;
const db = database.active; const db = database.active;
@ -424,15 +475,16 @@ class RoomView extends React.Component<IRoomViewProps, IRoomViewState> {
if (this.subSubscription && this.subSubscription.unsubscribe) { if (this.subSubscription && this.subSubscription.unsubscribe) {
this.subSubscription.unsubscribe(); this.subSubscription.unsubscribe();
} }
if (this.subObserveQuery && this.subObserveQuery.unsubscribe) {
this.subObserveQuery.unsubscribe();
}
if (this.queryUnreads && this.queryUnreads.unsubscribe) { if (this.queryUnreads && this.queryUnreads.unsubscribe) {
this.queryUnreads.unsubscribe(); this.queryUnreads.unsubscribe();
} }
if (this.retryInitTimeout) { if (this.retryInitTimeout) {
clearTimeout(this.retryInitTimeout); clearTimeout(this.retryInitTimeout);
} }
if (this.retryFindTimeout) {
clearTimeout(this.retryFindTimeout);
}
EventEmitter.removeListener('connected', this.handleConnected); EventEmitter.removeListener('connected', this.handleConnected);
if (isTablet) { if (isTablet) {
EventEmitter.removeListener(KEY_COMMAND, this.handleCommands); EventEmitter.removeListener(KEY_COMMAND, this.handleCommands);
@ -441,13 +493,61 @@ class RoomView extends React.Component<IRoomViewProps, IRoomViewState> {
console.countReset(`${this.constructor.name}.render calls`); 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() { get isOmnichannel() {
const { room } = this.state; const { room } = this.state;
return room.t === 'l'; return room.t === 'l';
} }
setHeader = () => { 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 { navigation, isMasterDetail, theme, baseUrl, user, route } = this.props;
const { rid, tmid } = this; const { rid, tmid } = this;
if (!room.rid) { if (!room.rid) {
@ -474,6 +574,7 @@ class RoomView extends React.Component<IRoomViewProps, IRoomViewState> {
let token: string | undefined; let token: string | undefined;
let avatar: string | undefined; let avatar: string | undefined;
let visitor: IVisitor | undefined; let visitor: IVisitor | undefined;
let status: string | undefined;
let sourceType: IOmnichannelSource | undefined; let sourceType: IOmnichannelSource | undefined;
if ('id' in room) { if ('id' in room) {
subtitle = room.topic; subtitle = room.topic;
@ -484,6 +585,7 @@ class RoomView extends React.Component<IRoomViewProps, IRoomViewState> {
({ id: userId, token } = user); ({ id: userId, token } = user);
avatar = room.name; avatar = room.name;
visitor = room.visitor; visitor = room.visitor;
status = room.status;
} }
if ('source' in room) { if ('source' in room) {
@ -493,11 +595,13 @@ class RoomView extends React.Component<IRoomViewProps, IRoomViewState> {
} }
let numIconsRight = 2; let numIconsRight = 2;
if (tmid) { if (tmid || (status && joined)) {
numIconsRight = 1; numIconsRight = 1;
} else if (teamId && isTeamRoom({ teamId, joined })) { } else if (teamId && isTeamRoom({ teamId, joined })) {
numIconsRight = 3; numIconsRight = 3;
} }
const omnichannelPermissions = { canForwardGuest, canReturnQueue, canPlaceLivechatOnHold };
const paddingRight = this.getPaddingLeft(numIconsRight, isMasterDetail); const paddingRight = this.getPaddingLeft(numIconsRight, isMasterDetail);
navigation.setOptions({ navigation.setOptions({
headerShown: true, headerShown: true,
@ -542,6 +646,8 @@ class RoomView extends React.Component<IRoomViewProps, IRoomViewState> {
tmid={tmid} tmid={tmid}
teamId={teamId} teamId={teamId}
joined={joined} joined={joined}
status={room.status}
omnichannelPermissions={omnichannelPermissions}
t={this.t || t} t={this.t || t}
encrypted={encrypted} encrypted={encrypted}
navigation={navigation} navigation={navigation}
@ -560,7 +666,7 @@ class RoomView extends React.Component<IRoomViewProps, IRoomViewState> {
goRoomActionsView = (screen?: keyof ModalStackParamList) => { goRoomActionsView = (screen?: keyof ModalStackParamList) => {
logEvent(events.ROOM_GO_RA); 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; const { navigation, isMasterDetail } = this.props;
if (isMasterDetail) { if (isMasterDetail) {
// @ts-ignore // @ts-ignore
@ -572,7 +678,8 @@ class RoomView extends React.Component<IRoomViewProps, IRoomViewState> {
room: room as ISubscription, room: room as ISubscription,
member, member,
showCloseModal: !!screen, showCloseModal: !!screen,
joined joined,
omnichannelPermissions: { canForwardGuest, canReturnQueue, canViewCannedResponse, canPlaceLivechatOnHold }
} }
}); });
} else if (this.rid && this.t) { } else if (this.rid && this.t) {
@ -581,7 +688,8 @@ class RoomView extends React.Component<IRoomViewProps, IRoomViewState> {
t: this.t as SubscriptionType, t: this.t as SubscriptionType,
room: room as TSubscriptionModel, room: room as TSubscriptionModel,
member, member,
joined joined,
omnichannelPermissions: { canForwardGuest, canReturnQueue, canViewCannedResponse, canPlaceLivechatOnHold }
}); });
} }
}; };
@ -669,15 +777,7 @@ class RoomView extends React.Component<IRoomViewProps, IRoomViewState> {
this.internalSetState({ joined: false }); this.internalSetState({ joined: false });
} }
if (this.rid) { if (this.rid) {
// We navigate to RoomView before the Room is inserted to the local db this.observeSubscriptions();
// 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);
}
} }
} }
}; };
@ -697,7 +797,7 @@ class RoomView extends React.Component<IRoomViewProps, IRoomViewState> {
return ret; return ret;
}, {}); }, {});
if (this.mounted) { if (this.mounted) {
this.internalSetState({ room: changes, roomUpdate }); this.internalSetState({ room: changes, roomUpdate, isOnHold: !!changes?.onHold });
} else { } else {
// @ts-ignore // @ts-ignore
this.state.room = changes; this.state.room = changes;
@ -1186,6 +1286,11 @@ class RoomView extends React.Component<IRoomViewProps, IRoomViewState> {
}); });
}; };
goToCannedResponses = () => {
const { room } = this.state;
Navigation.navigate('CannedResponsesListView', { rid: room.rid });
};
renderItem = (item: TAnyMessageModel, previousItem: TAnyMessageModel, highlightedMessage?: string) => { renderItem = (item: TAnyMessageModel, previousItem: TAnyMessageModel, highlightedMessage?: string) => {
const { room, lastOpen, canAutoTranslate } = this.state; const { room, lastOpen, canAutoTranslate } = this.state;
const { user, Message_GroupingPeriod, Message_TimeFormat, useRealName, baseUrl, Message_Read_Receipt_Enabled, theme } = const { user, Message_GroupingPeriod, Message_TimeFormat, useRealName, baseUrl, Message_Read_Receipt_Enabled, theme } =
@ -1203,7 +1308,6 @@ class RoomView extends React.Component<IRoomViewProps, IRoomViewState> {
dateSeparator = item.ts; dateSeparator = item.ts;
} }
} }
let content = null; let content = null;
if (item.t && MESSAGE_TYPE_ANY_LOAD.includes(item.t as MessageTypeLoad)) { if (item.t && MESSAGE_TYPE_ANY_LOAD.includes(item.t as MessageTypeLoad)) {
content = ( content = (
@ -1271,7 +1375,8 @@ class RoomView extends React.Component<IRoomViewProps, IRoomViewState> {
}; };
renderFooter = () => { 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 { navigation, theme, route } = this.props;
const usedCannedResponse = route?.params?.usedCannedResponse; const usedCannedResponse = route?.params?.usedCannedResponse;
@ -1338,9 +1443,11 @@ class RoomView extends React.Component<IRoomViewProps, IRoomViewState> {
return ( return (
<MessageBox <MessageBox
ref={this.messagebox} ref={this.messagebox}
goToCannedResponses={canViewCannedResponse ? this.goToCannedResponses : null}
onSubmit={this.handleSendMessage} onSubmit={this.handleSendMessage}
rid={this.rid} rid={this.rid}
tmid={this.tmid} tmid={this.tmid}
joined={joined}
roomType={room.t} roomType={room.t}
isFocused={navigation.isFocused} isFocused={navigation.isFocused}
theme={theme} theme={theme}
@ -1454,7 +1561,10 @@ const mapStateToProps = (state: IApplicationState) => ({
baseUrl: state.server.server, baseUrl: state.server.server,
serverVersion: state.server.version, serverVersion: state.server.version,
Message_Read_Receipt_Enabled: state.settings.Message_Read_Receipt_Enabled as boolean, 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)))); export default connect(mapStateToProps)(withDimensions(withTheme(withSafeAreaInsets(RoomView))));