diff --git a/app/threads/client/components/ThreadComponent.tsx b/app/threads/client/components/ThreadComponent.tsx index fadcd0414277..36fe0a4655e8 100644 --- a/app/threads/client/components/ThreadComponent.tsx +++ b/app/threads/client/components/ThreadComponent.tsx @@ -100,7 +100,7 @@ const ThreadComponent: FC<{ }, [dispatchToastMessage, followMessage, unfollowMessage, mid]); const handleClose = useCallback(() => { - channelRoute.push(room.t === 'd' ? { rid: room._id } : { name: room.name }); + channelRoute.push(room.t === 'd' ? { rid: room._id } : { name: room.name || room._id }); }, [channelRoute, room._id, room.t, room.name]); const [viewData, setViewData] = useState(() => ({ diff --git a/app/ui-sidenav/client/roomList.js b/app/ui-sidenav/client/roomList.js index 06c6101682cf..bd706fc913d2 100644 --- a/app/ui-sidenav/client/roomList.js +++ b/app/ui-sidenav/client/roomList.js @@ -161,6 +161,11 @@ const mergeSubRoom = (subscription) => { retention: 1, teamId: 1, teamMain: 1, + onHold: 1, + metrics: 1, + servedBy: 1, + ts: 1, + waitingResponse: 1, }, }; @@ -168,32 +173,67 @@ const mergeSubRoom = (subscription) => { const lastRoomUpdate = room.lm || subscription.ts || subscription._updatedAt; - if (room.uids) { - subscription.uids = room.uids; - } - - if (room.v) { - subscription.v = room.v; - } - - subscription.usernames = room.usernames; + const { + encrypted, + description, + cl, + topic, + announcement, + broadcast, + archived, + retention, + lastMessage, + streamingOptions, + teamId, + teamMain, + uids, + usernames, + + v, + transcriptRequest, + servedBy, + onHold, + tags, + closedAt, + metrics, + waitingResponse, + responseBy, + priorityId, + livechatData, + ts, + } = room; - subscription.lastMessage = room.lastMessage; subscription.lm = subscription.lr ? new Date(Math.max(subscription.lr, lastRoomUpdate)) : lastRoomUpdate; - subscription.streamingOptions = room.streamingOptions; - - subscription.encrypted = room.encrypted; - subscription.description = room.description; - subscription.cl = room.cl; - subscription.topic = room.topic; - subscription.announcement = room.announcement; - subscription.broadcast = room.broadcast; - subscription.archived = room.archived; - subscription.retention = room.retention; - - subscription.teamId = room.teamId; - subscription.teamMain = room.teamMain; - return Object.assign(subscription, getLowerCaseNames(subscription)); + + return Object.assign(subscription, getLowerCaseNames(subscription), { + encrypted, + description, + cl, + topic, + announcement, + broadcast, + archived, + retention, + lastMessage, + streamingOptions, + teamId, + teamMain, + uids, + usernames, + + v, + transcriptRequest, + servedBy, + onHold, + tags, + closedAt, + metrics, + waitingResponse, + responseBy, + priorityId, + livechatData, + ts, + }); }; const mergeRoomSub = (room) => { @@ -201,25 +241,68 @@ const mergeRoomSub = (room) => { if (!sub) { return room; } + + const { + encrypted, + description, + cl, + topic, + announcement, + broadcast, + archived, + retention, + lastMessage, + streamingOptions, + teamId, + teamMain, + uids, + usernames, + + v, + transcriptRequest, + servedBy, + onHold, + tags, + closedAt, + metrics, + waitingResponse, + responseBy, + priorityId, + livechatData, + ts, + + } = room; + Subscriptions.update({ rid: room._id, }, { $set: { - encrypted: room.encrypted, - description: room.description, - cl: room.cl, - topic: room.topic, - announcement: room.announcement, - broadcast: room.broadcast, - archived: room.archived, - retention: room.retention, - ...Array.isArray(room.uids) && { uids: room.uids }, - ...Array.isArray(room.uids) && { usernames: room.usernames }, - ...room.v && { v: room.v }, - lastMessage: room.lastMessage, - streamingOptions: room.streamingOptions, - teamId: room.teamId, - teamMain: room.teamMain, + encrypted, + description, + cl, + topic, + announcement, + broadcast, + archived, + retention, + uids, + usernames, + lastMessage, + streamingOptions, + teamId, + teamMain, + v, + transcriptRequest, + servedBy, + onHold, + tags, + closedAt, + metrics, + waitingResponse, + responseBy, + priorityId, + livechatData, + ts, ...getLowerCaseNames(room, sub.name, sub.fname), }, }); diff --git a/client/components/ErrorBoundary.js b/client/components/ErrorBoundary.js new file mode 100644 index 000000000000..8ec5ce527154 --- /dev/null +++ b/client/components/ErrorBoundary.js @@ -0,0 +1,17 @@ +import { Component } from 'react'; + +export class ErrorBoundary extends Component { + state = { hasError: false }; + + static getDerivedStateFromError() { + return { hasError: true }; + } + + render() { + if (this.state.hasError) { + return null; + } + + return this.props.children; + } +} diff --git a/client/components/Omnichannel/modals/TranscriptModal.tsx b/client/components/Omnichannel/modals/TranscriptModal.tsx index 498b5bc7ee7b..4a5fc1be54e5 100644 --- a/client/components/Omnichannel/modals/TranscriptModal.tsx +++ b/client/components/Omnichannel/modals/TranscriptModal.tsx @@ -2,14 +2,14 @@ import { Field, Button, TextInput, Icon, ButtonGroup, Modal } from '@rocket.chat import { useAutoFocus } from '@rocket.chat/fuselage-hooks'; import React, { FC, useCallback, useEffect, useState, useMemo } from 'react'; -import { IRoom } from '../../../../definition/IRoom'; +import { IOmnichannelRoom } from '../../../../definition/IRoom'; import { useTranslation } from '../../../contexts/TranslationContext'; import { useComponentDidUpdate } from '../../../hooks/useComponentDidUpdate'; import { useForm } from '../../../hooks/useForm'; type TranscriptModalProps = { email: string; - room?: IRoom; + room: IOmnichannelRoom; onRequest: (email: string, subject: string) => void; onSend?: (email: string, subject: string, token: string) => void; onCancel: () => void; @@ -38,7 +38,7 @@ const TranscriptModal: FC = ({ const { handleEmail, handleSubject } = handlers; const [emailError, setEmailError] = useState(''); const [subjectError, setSubjectError] = useState(''); - const { transcriptRequest } = (room as unknown) as IRoom; + const { transcriptRequest } = room; const roomOpen = room && room.open; const token = room?.v?.token; diff --git a/client/lib/RoomManager.ts b/client/lib/RoomManager.ts index fc1fc3d77333..2d224956a398 100644 --- a/client/lib/RoomManager.ts +++ b/client/lib/RoomManager.ts @@ -148,11 +148,11 @@ const subscribeOpenedRoom: Subscription = { const fields = {}; -export const useHandleRoom = (rid: IRoom['_id']): AsyncState => { - const { resolve, update, ...state } = useAsyncState(); +export const useHandleRoom = (rid: IRoom['_id']): AsyncState => { + const { resolve, update, ...state } = useAsyncState(); const uid = useUserId(); - const subscription = (useUserSubscription(rid, fields) as unknown) as IRoom; - const _room = (useUserRoom(rid, fields) as unknown) as IRoom; + const subscription = (useUserSubscription(rid, fields) as unknown) as T; + const _room = (useUserRoom(rid, fields) as unknown) as T; const room = uid ? subscription || _room : _room; diff --git a/client/views/omnichannel/directory/ChatsContextualBar.js b/client/views/omnichannel/directory/ChatsContextualBar.js index 2d2a5ae5fbfe..f0ac473cde5e 100644 --- a/client/views/omnichannel/directory/ChatsContextualBar.js +++ b/client/views/omnichannel/directory/ChatsContextualBar.js @@ -1,10 +1,14 @@ +import { Box } from '@rocket.chat/fuselage'; import React from 'react'; import VerticalBar from '../../../components/VerticalBar'; import { useRoute, useRouteParameter } from '../../../contexts/RouterContext'; import { useTranslation } from '../../../contexts/TranslationContext'; +import { AsyncStatePhase } from '../../../hooks/useAsyncState'; +import { useEndpointData } from '../../../hooks/useEndpointData'; +import { FormSkeleton } from './Skeleton'; import Chat from './chats/Chat'; -import ChatInfo from './chats/contextualBar/ChatInfo'; +import ChatInfoDirectory from './chats/contextualBar/ChatInfoDirectory'; import RoomEditWithData from './chats/contextualBar/RoomEditWithData'; const ChatsContextualBar = ({ chatReload }) => { @@ -27,10 +31,26 @@ const ChatsContextualBar = ({ chatReload }) => { directoryRoute.push({ page: 'chats', id, bar: 'info' }); }; + const { value: data, phase: state, error, reload: reloadInfo } = useEndpointData( + `rooms.info?roomId=${id}`, + ); + if (bar === 'view') { return ; } + if (state === AsyncStatePhase.LOADING) { + return ( + + + + ); + } + + if (error || !data || !data.room) { + return {t('Room_not_found')}; + } + return ( @@ -53,12 +73,13 @@ const ChatsContextualBar = ({ chatReload }) => { )} - {bar === 'info' && } + {bar === 'info' && } {bar === 'edit' && ( )} diff --git a/client/views/omnichannel/directory/ContactContextualBar.js b/client/views/omnichannel/directory/ContactContextualBar.js index d8628923deb8..ce2a0b3c6f37 100644 --- a/client/views/omnichannel/directory/ContactContextualBar.js +++ b/client/views/omnichannel/directory/ContactContextualBar.js @@ -10,12 +10,8 @@ import ContactNewEdit from './contacts/contextualBar/ContactNewEdit'; const ContactContextualBar = ({ contactReload }) => { const directoryRoute = useRoute('omnichannel-directory'); const bar = useRouteParameter('bar'); - const page = useRouteParameter('page'); - const tab = useRouteParameter('tab'); const id = useRouteParameter('id'); - console.log(bar, tab, page, id); - const t = useTranslation(); const handleContactsVerticalBarCloseButtonClick = () => { diff --git a/client/views/omnichannel/directory/chats/contextualBar/ChatInfo.js b/client/views/omnichannel/directory/chats/contextualBar/ChatInfo.js index 110cf90da18d..31bb59534008 100644 --- a/client/views/omnichannel/directory/chats/contextualBar/ChatInfo.js +++ b/client/views/omnichannel/directory/chats/contextualBar/ChatInfo.js @@ -1,5 +1,6 @@ import { Box, Margins, Tag, Button, Icon, ButtonGroup } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; +import { Meteor } from 'meteor/meteor'; import moment from 'moment'; import React, { useEffect, useState } from 'react'; @@ -9,15 +10,14 @@ import { useRoute } from '../../../../../contexts/RouterContext'; import { useToastMessageDispatch } from '../../../../../contexts/ToastMessagesContext'; import { useTranslation } from '../../../../../contexts/TranslationContext'; import { useUserSubscription } from '../../../../../contexts/UserContext'; -import { AsyncStatePhase } from '../../../../../hooks/useAsyncState'; import { useEndpointData } from '../../../../../hooks/useEndpointData'; import { useFormatDateAndTime } from '../../../../../hooks/useFormatDateAndTime'; import { useFormatDuration } from '../../../../../hooks/useFormatDuration'; +import { useOmnichannelRoom } from '../../../../room/contexts/RoomContext'; import CustomField from '../../../components/CustomField'; import Field from '../../../components/Field'; import Info from '../../../components/Info'; import Label from '../../../components/Label'; -import { FormSkeleton } from '../../Skeleton'; import AgentField from './AgentField'; import ContactField from './ContactField'; import DepartmentField from './DepartmentField'; @@ -33,27 +33,29 @@ function ChatInfo({ id, route }) { ); const [customFields, setCustomFields] = useState([]); const formatDuration = useFormatDuration(); - const { value: data, phase: state, error } = useEndpointData(`rooms.info?roomId=${id}`); + + const room = useOmnichannelRoom(); + const { - room: { - ts, - tags, - closedAt, - departmentId, - v, - servedBy, - metrics, - topic, - waitingResponse, - responseBy, - priorityId, - livechatData, - }, - } = data || { room: { v: {} } }; + ts, + tags, + closedAt, + department, + v, + servedBy, + metrics, + topic, + waitingResponse, + responseBy, + priorityId, + livechatData, + } = room || { room: { v: {} } }; + const routePath = useRoute(route || 'omnichannel-directory'); const canViewCustomFields = () => hasPermission('view-livechat-room-customfields'); const subscription = useUserSubscription(id); const hasGlobalEditRoomPermission = hasPermission('save-others-livechat-room-info'); + const hasLocalEditRoomPermission = servedBy?._id === Meteor.userId(); const visitorId = v?._id; const dispatchToastMessage = useToastMessageDispatch(); @@ -73,7 +75,8 @@ function ChatInfo({ id, route }) { }; const onEditClick = useMutableCallback(() => { - const hasEditAccess = !!subscription || hasGlobalEditRoomPermission; + const hasEditAccess = + !!subscription || hasLocalEditRoomPermission || hasGlobalEditRoomPermission; if (!hasEditAccess) { return dispatchToastMessage({ type: 'error', message: t('Not_authorized') }); } @@ -93,26 +96,14 @@ function ChatInfo({ id, route }) { ); }); - if (state === AsyncStatePhase.LOADING) { - return ( - - - - ); - } - - if (error || !data || !data.room) { - return {t('Room_not_found')}; - } - return ( <> - + {room && v && } {visitorId && } {servedBy && } - {departmentId && } + {department && } {tags && tags.length > 0 && ( diff --git a/client/views/omnichannel/directory/chats/contextualBar/ChatInfoDirectory.js b/client/views/omnichannel/directory/chats/contextualBar/ChatInfoDirectory.js new file mode 100644 index 000000000000..5ef52dbdf010 --- /dev/null +++ b/client/views/omnichannel/directory/chats/contextualBar/ChatInfoDirectory.js @@ -0,0 +1,191 @@ +import { Box, Margins, Tag, Button, Icon, ButtonGroup } from '@rocket.chat/fuselage'; +import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; +import { Meteor } from 'meteor/meteor'; +import moment from 'moment'; +import React, { useEffect, useState } from 'react'; + +import { hasPermission } from '../../../../../../app/authorization/client'; +import VerticalBar from '../../../../../components/VerticalBar'; +import { useRoute } from '../../../../../contexts/RouterContext'; +import { useToastMessageDispatch } from '../../../../../contexts/ToastMessagesContext'; +import { useTranslation } from '../../../../../contexts/TranslationContext'; +import { useUserSubscription } from '../../../../../contexts/UserContext'; +import { useEndpointData } from '../../../../../hooks/useEndpointData'; +import { useFormatDateAndTime } from '../../../../../hooks/useFormatDateAndTime'; +import { useFormatDuration } from '../../../../../hooks/useFormatDuration'; +import CustomField from '../../../components/CustomField'; +import Field from '../../../components/Field'; +import Info from '../../../components/Info'; +import Label from '../../../components/Label'; +import AgentField from './AgentField'; +import ContactField from './ContactField'; +import DepartmentField from './DepartmentField'; +import PriorityField from './PriorityField'; +import VisitorClientInfo from './VisitorClientInfo'; + +function ChatInfoDirectory({ id, route, room }) { + const t = useTranslation(); + + const formatDateAndTime = useFormatDateAndTime(); + const { value: allCustomFields, phase: stateCustomFields } = useEndpointData( + 'livechat/custom-fields', + ); + const [customFields, setCustomFields] = useState([]); + const formatDuration = useFormatDuration(); + + const { + ts, + tags, + closedAt, + departmentId, + v, + servedBy, + metrics, + topic, + waitingResponse, + responseBy, + priorityId, + livechatData, + } = room || { room: { v: {} } }; + + const routePath = useRoute(route || 'omnichannel-directory'); + const canViewCustomFields = () => hasPermission('view-livechat-room-customfields'); + const subscription = useUserSubscription(id); + const hasGlobalEditRoomPermission = hasPermission('save-others-livechat-room-info'); + const hasLocalEditRoomPermission = servedBy?._id === Meteor.userId(); + const visitorId = v?._id; + + const dispatchToastMessage = useToastMessageDispatch(); + useEffect(() => { + if (allCustomFields) { + const { customFields: customFieldsAPI } = allCustomFields; + setCustomFields(customFieldsAPI); + } + }, [allCustomFields, stateCustomFields]); + + const checkIsVisibleAndScopeRoom = (key) => { + const field = customFields.find(({ _id }) => _id === key); + if (field && field.visibility === 'visible' && field.scope === 'room') { + return true; + } + return false; + }; + + const onEditClick = useMutableCallback(() => { + const hasEditAccess = + !!subscription || hasLocalEditRoomPermission || hasGlobalEditRoomPermission; + if (!hasEditAccess) { + return dispatchToastMessage({ type: 'error', message: t('Not_authorized') }); + } + + routePath.push( + route + ? { + tab: 'room-info', + context: 'edit', + id, + } + : { + page: 'chats', + id, + bar: 'edit', + }, + ); + }); + + return ( + <> + + + {room && v && } + {visitorId && } + {servedBy && } + {departmentId && } + {tags && tags.length > 0 && ( + + + + {tags.map((tag) => ( + + + {tag} + + + ))} + + + )} + {topic && ( + + + {topic} + + )} + {ts && ( + + + {servedBy ? ( + {moment(servedBy.ts).from(moment(ts), true)} + ) : ( + {moment(ts).fromNow(true)} + )} + + )} + {closedAt && ( + + + {moment(closedAt).from(moment(ts), true)} + + )} + {ts && ( + + + {formatDateAndTime(ts)} + + )} + {closedAt && ( + + + {formatDateAndTime(closedAt)} + + )} + {servedBy?.ts && ( + + + {formatDateAndTime(servedBy.ts)} + + )} + {metrics?.response?.avg && formatDuration(metrics.response.avg) && ( + + + {formatDuration(metrics.response.avg)} + + )} + {!waitingResponse && responseBy?.lastMessageTs && ( + + + {moment(responseBy.lastMessageTs).fromNow(true)} + + )} + {canViewCustomFields() && + livechatData && + Object.keys(livechatData).map( + (key) => + checkIsVisibleAndScopeRoom(key) && + livechatData[key] && , + )} + {priorityId && } + + + + + + + + + ); +} + +export default ChatInfoDirectory; diff --git a/client/views/omnichannel/directory/chats/contextualBar/RoomEdit.js b/client/views/omnichannel/directory/chats/contextualBar/RoomEdit.js index 760468594575..f662aad914ab 100644 --- a/client/views/omnichannel/directory/chats/contextualBar/RoomEdit.js +++ b/client/views/omnichannel/directory/chats/contextualBar/RoomEdit.js @@ -38,7 +38,7 @@ const getInitialValuesRoom = (room) => { }; }; -function RoomEdit({ room, visitor, reload, close }) { +function RoomEdit({ room, visitor, reload, reloadInfo, close }) { const t = useTranslation(); const { @@ -123,6 +123,7 @@ function RoomEdit({ room, visitor, reload, close }) { saveRoom(userData, roomData); dispatchToastMessage({ type: 'success', message: t('Saved') }); reload && reload(); + reloadInfo && reloadInfo(); close(); } catch (error) { dispatchToastMessage({ type: 'error', message: error }); diff --git a/client/views/omnichannel/directory/chats/contextualBar/RoomEditWithData.js b/client/views/omnichannel/directory/chats/contextualBar/RoomEditWithData.js index 941388a76667..ab8d815d9691 100644 --- a/client/views/omnichannel/directory/chats/contextualBar/RoomEditWithData.js +++ b/client/views/omnichannel/directory/chats/contextualBar/RoomEditWithData.js @@ -7,7 +7,7 @@ import { useEndpointData } from '../../../../../hooks/useEndpointData'; import { FormSkeleton } from '../../Skeleton'; import VisitorData from './VisitorData'; -function RoomEditWithData({ id, reload, close }) { +function RoomEditWithData({ id, reload, reloadInfo, close }) { const t = useTranslation(); const { value: roomData, phase: state, error } = useEndpointData(`rooms.info?roomId=${id}`); @@ -20,7 +20,7 @@ function RoomEditWithData({ id, reload, close }) { return {t('Room_not_found')}; } - return ; + return ; } export default RoomEditWithData; diff --git a/client/views/omnichannel/directory/chats/contextualBar/VisitorData.js b/client/views/omnichannel/directory/chats/contextualBar/VisitorData.js index 6b0e28a0bbf5..818066361723 100644 --- a/client/views/omnichannel/directory/chats/contextualBar/VisitorData.js +++ b/client/views/omnichannel/directory/chats/contextualBar/VisitorData.js @@ -7,7 +7,7 @@ import { useEndpointData } from '../../../../../hooks/useEndpointData'; import { FormSkeleton } from '../../Skeleton'; import RoomEdit from './RoomEdit'; -function VisitorData({ room, reload, close }) { +function VisitorData({ room, reload, reloadInfo, close }) { const t = useTranslation(); const { @@ -31,7 +31,15 @@ function VisitorData({ room, reload, close }) { const { visitor: visitorData } = visitor; const { room: roomData } = room; - return ; + return ( + + ); } export default VisitorData; diff --git a/client/views/omnichannel/directory/contacts/contextualBar/ContactEditWithData.js b/client/views/omnichannel/directory/contacts/contextualBar/ContactEditWithData.js index 7db862eae31f..bae6b56f01c4 100644 --- a/client/views/omnichannel/directory/contacts/contextualBar/ContactEditWithData.js +++ b/client/views/omnichannel/directory/contacts/contextualBar/ContactEditWithData.js @@ -7,11 +7,11 @@ import { useEndpointData } from '../../../../../hooks/useEndpointData'; import { FormSkeleton } from '../../Skeleton'; import ContactNewEdit from './ContactNewEdit'; -function ContactEditWithData({ id, reload, close }) { +function ContactEditWithData({ id, close }) { const t = useTranslation(); const { value: data, phase: state, error } = useEndpointData( `omnichannel/contact?contactId=${id}`, - ); + ); // TODO OMNICHANNEL if ([state].includes(AsyncStatePhase.LOADING)) { return ; @@ -21,7 +21,7 @@ function ContactEditWithData({ id, reload, close }) { return {t('Contact_not_found')}; } - return ; + return ; } export default ContactEditWithData; diff --git a/client/views/omnichannel/directory/contacts/contextualBar/ContactNewEdit.js b/client/views/omnichannel/directory/contacts/contextualBar/ContactNewEdit.js index 346b4621dc9f..303ee632b3cb 100644 --- a/client/views/omnichannel/directory/contacts/contextualBar/ContactNewEdit.js +++ b/client/views/omnichannel/directory/contacts/contextualBar/ContactNewEdit.js @@ -45,7 +45,7 @@ const getInitialValues = (data) => { }; }; -function ContactNewEdit({ id, data, reload, close }) { +function ContactNewEdit({ id, data, close }) { const t = useTranslation(); const canViewCustomFields = () => @@ -54,6 +54,7 @@ function ContactNewEdit({ id, data, reload, close }) { const { values, handlers, hasUnsavedChanges: hasUnsavedChangesContact } = useForm( getInitialValues(data), ); + const eeForms = useSubscription(formsSubscription); const { useContactManager = () => {} } = eeForms; @@ -182,7 +183,6 @@ function ContactNewEdit({ id, data, reload, close }) { try { await saveContact(payload); dispatchToastMessage({ type: 'success', message: t('Saved') }); - reload && reload(); close(); } catch (error) { dispatchToastMessage({ type: 'error', message: error }); diff --git a/client/views/omnichannel/directory/contacts/contextualBar/ContactsContextualBar.js b/client/views/omnichannel/directory/contacts/contextualBar/ContactsContextualBar.tsx similarity index 79% rename from client/views/omnichannel/directory/contacts/contextualBar/ContactsContextualBar.js rename to client/views/omnichannel/directory/contacts/contextualBar/ContactsContextualBar.tsx index 6f94d5086d3f..6df9ab1a0a71 100644 --- a/client/views/omnichannel/directory/contacts/contextualBar/ContactsContextualBar.js +++ b/client/views/omnichannel/directory/contacts/contextualBar/ContactsContextualBar.tsx @@ -1,16 +1,17 @@ -import React from 'react'; +import React, { FC } from 'react'; +import { IOmnichannelRoom } from '../../../../../../definition/IRoom'; import VerticalBar from '../../../../../components/VerticalBar'; import { useRoute, useRouteParameter } from '../../../../../contexts/RouterContext'; import { useTranslation } from '../../../../../contexts/TranslationContext'; -import { useRoom } from '../../../../room/providers/RoomProvider'; +import { useOmnichannelRoom } from '../../../../room/contexts/RoomContext'; import { useTabBarClose } from '../../../../room/providers/ToolboxProvider'; import ContactEditWithData from './ContactEditWithData'; import ContactInfo from './ContactInfo'; const PATH = 'live'; -const ContactsContextualBar = ({ rid }) => { +const ContactsContextualBar: FC<{ rid: IOmnichannelRoom['_id'] }> = ({ rid }) => { const t = useTranslation(); const closeContextualBar = useTabBarClose(); @@ -19,11 +20,11 @@ const ContactsContextualBar = ({ rid }) => { const context = useRouteParameter('context'); - const handleContactEditBarCloseButtonClick = () => { + const handleContactEditBarCloseButtonClick = (): void => { directoryRoute.push({ id: rid, tab: 'contact-profile' }); }; - const room = useRoom(); + const room = useOmnichannelRoom(); const { v: { _id }, diff --git a/client/views/room/Header/Omnichannel/QuickActions/QuickActions.tsx b/client/views/room/Header/Omnichannel/QuickActions/QuickActions.tsx index 810c1b78858e..887e3384d910 100644 --- a/client/views/room/Header/Omnichannel/QuickActions/QuickActions.tsx +++ b/client/views/room/Header/Omnichannel/QuickActions/QuickActions.tsx @@ -15,7 +15,7 @@ import toastr from 'toastr'; import { RoomManager } from '../../../../../../app/ui-utils/client'; import { handleError } from '../../../../../../app/utils/client'; -import { IRoom } from '../../../../../../definition/IRoom'; +import { IOmnichannelRoom } from '../../../../../../definition/IRoom'; import PlaceChatOnHoldModal from '../../../../../../ee/app/livechat-enterprise/client/components/modals/PlaceChatOnHoldModal'; import Header from '../../../../../components/Header'; import CloseChatModal from '../../../../../components/Omnichannel/modals/CloseChatModal'; @@ -35,7 +35,7 @@ import { QuickActionsActionConfig, QuickActionsEnum } from '../../../lib/QuickAc import { QuickActionsContext } from '../../../lib/QuickActions/QuickActionsContext'; type QuickActionsProps = { - room: IRoom; + room: IOmnichannelRoom; className?: ComponentProps['className']; }; @@ -49,7 +49,7 @@ const QuickActions: FC = ({ room, className }) => { ); const visibleActions = isMobile ? [] : actions.slice(0, 6); const [email, setEmail] = useState(''); - const visitorRoomId = room.v?._id; + const visitorRoomId = room.v._id; const rid = room._id; const uid = useUserId(); @@ -215,9 +215,9 @@ const QuickActions: FC = ({ room, className }) => { break; case QuickActionsEnum.CloseChat: setModal( - room?.departmentId ? ( + room.departmentId ? ( @@ -293,7 +293,6 @@ const QuickActions: FC = ({ room, className }) => { color, 'title': t(title as any), className, - 'tabId': id, index, 'primary': false, 'data-quick-actions': index, diff --git a/client/views/room/Header/RoomHeader.tsx b/client/views/room/Header/RoomHeader.tsx index 0e9f94ba63ad..ab80d5393b64 100644 --- a/client/views/room/Header/RoomHeader.tsx +++ b/client/views/room/Header/RoomHeader.tsx @@ -1,6 +1,6 @@ import React, { FC } from 'react'; -import { IRoom } from '../../../../definition/IRoom'; +import { IOmnichannelRoom } from '../../../../definition/IRoom'; import Header from '../../../components/Header'; import MarkdownText from '../../../components/MarkdownText'; import RoomAvatar from '../../../components/avatar/RoomAvatar'; @@ -13,7 +13,7 @@ import Favorite from './icons/Favorite'; import Translate from './icons/Translate'; export type RoomHeaderProps = { - room: IRoom; + room: IOmnichannelRoom; topic?: string; slots: { start?: unknown; diff --git a/client/views/room/MemberListRouter.js b/client/views/room/MemberListRouter.js index 666a5fceb5cf..429109b662e6 100644 --- a/client/views/room/MemberListRouter.js +++ b/client/views/room/MemberListRouter.js @@ -1,9 +1,9 @@ import React from 'react'; import { useUserId } from '../../contexts/UserContext'; +import { useRoom } from './contexts/RoomContext'; import RoomMembers from './contextualBar/RoomMembers'; import UserInfo from './contextualBar/UserInfo'; -import { useRoom } from './providers/RoomProvider'; import { useTab, useTabBarClose, useTabContext } from './providers/ToolboxProvider'; const getUid = (room, ownUserId) => { diff --git a/client/views/room/Room/Room.js b/client/views/room/Room/Room.js index 082f93b7fc6f..7920c87d63a8 100644 --- a/client/views/room/Room/Room.js +++ b/client/views/room/Room/Room.js @@ -1,13 +1,14 @@ import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; -import React, { useMemo } from 'react'; +import React, { useDebugValue, useMemo } from 'react'; +import { ErrorBoundary } from '../../../components/ErrorBoundary'; import { useTranslation } from '../../../contexts/TranslationContext'; import { useUserPreference } from '../../../contexts/UserContext'; import Header from '../Header'; import BlazeTemplate from '../components/BlazeTemplate'; import { RoomTemplate } from '../components/RoomTemplate/RoomTemplate'; import VerticalBarOldActions from '../components/VerticalBarOldActions'; -import { useRoom } from '../providers/RoomProvider'; +import { useRoom } from '../contexts/RoomContext'; import { useTab, useTabBarOpen, @@ -37,6 +38,8 @@ const Room = () => { openUserInfo, ]); + useDebugValue(room); + useDebugValue(tab); return ( @@ -53,24 +56,26 @@ const Room = () => { {tab && ( - {typeof tab.template === 'string' && ( - - )} - {typeof tab.template !== 'string' && ( - - )} + + {typeof tab.template === 'string' && ( + + )} + {typeof tab.template !== 'string' && ( + + )} + )} diff --git a/client/views/room/contexts/RoomContext.ts b/client/views/room/contexts/RoomContext.ts index f9a1d0d24407..a58569fccf77 100644 --- a/client/views/room/contexts/RoomContext.ts +++ b/client/views/room/contexts/RoomContext.ts @@ -1,6 +1,6 @@ -import { createContext } from 'react'; +import { createContext, useContext } from 'react'; -import { IRoom } from '../../../../definition/IRoom'; +import { IRoom, IOmnichannelRoom, isOmnichannelRoom } from '../../../../definition/IRoom'; export type RoomContextValue = { rid: IRoom['_id']; @@ -9,3 +9,22 @@ export type RoomContextValue = { }; export const RoomContext = createContext(null); + +export const useRoom = (): IRoom => { + const { room } = useContext(RoomContext) || {}; + if (!room) { + throw new Error('use useRoom only inside opened rooms'); + } + return room; +}; + +export const useOmnichannelRoom = (): IOmnichannelRoom => { + const { room } = useContext(RoomContext) || {}; + if (!room) { + throw new Error('use useRoom only inside opened rooms'); + } + if (!isOmnichannelRoom(room)) { + throw new Error('invalid room type'); + } + return room; +}; diff --git a/client/views/room/contextualBar/Call/BBB/D.tsx b/client/views/room/contextualBar/Call/BBB/D.tsx index 4388a9ca254c..7f35627eebdb 100644 --- a/client/views/room/contextualBar/Call/BBB/D.tsx +++ b/client/views/room/contextualBar/Call/BBB/D.tsx @@ -6,7 +6,7 @@ import { IRoom } from '../../../../../../definition/IRoom'; import { usePermission } from '../../../../../contexts/AuthorizationContext'; import { useMethod } from '../../../../../contexts/ServerContext'; import { useSetting } from '../../../../../contexts/SettingsContext'; -import { useRoom } from '../../../providers/RoomProvider'; +import { useRoom } from '../../../contexts/RoomContext'; import { useTabBarClose } from '../../../providers/ToolboxProvider'; import CallBBB from './CallBBB'; diff --git a/client/views/room/providers/RoomProvider.tsx b/client/views/room/providers/RoomProvider.tsx index fa65d596208f..ada05e29c237 100644 --- a/client/views/room/providers/RoomProvider.tsx +++ b/client/views/room/providers/RoomProvider.tsx @@ -1,4 +1,4 @@ -import React, { ReactNode, useContext, useMemo, memo, useEffect } from 'react'; +import React, { ReactNode, useMemo, memo, useEffect } from 'react'; import { roomTypes } from '../../../../app/utils/client'; import { IRoom } from '../../../../definition/IRoom'; @@ -43,6 +43,4 @@ const RoomProvider = ({ rid, children }: Props): JSX.Element => { ); }; - -export const useRoom = (): undefined | IRoom => useContext(RoomContext)?.room; export default memo(RoomProvider); diff --git a/definition/IRoom.ts b/definition/IRoom.ts index 1551a200ea37..ff6ac674214b 100644 --- a/definition/IRoom.ts +++ b/definition/IRoom.ts @@ -16,13 +16,14 @@ interface IRequestTranscript { export interface IRoom extends IRocketChatRecord { _id: RoomID; t: RoomType; - name: string; + name?: string; fname: string; msgs: number; default?: true; broadcast?: true; featured?: true; encrypted?: boolean; + topic: any; u: Pick; @@ -49,23 +50,13 @@ export interface IRoom extends IRocketChatRecord { teamMain?: boolean; teamId?: string; teamDefault?: boolean; - v?: { - _id?: string; - token?: string; - status?: string; - }; - transcriptRequest?: IRequestTranscript; open?: boolean; - servedBy?: { - _id: string; - }; - onHold?: boolean; + autoTranslateLanguage: string; autoTranslate?: boolean; unread?: number; alert?: boolean; hideUnreadStatus?: boolean; - departmentId?: string; } export interface IDirectMessageRoom extends Omit { @@ -75,9 +66,27 @@ export interface IDirectMessageRoom extends Omit { +export interface IOmnichannelRoom extends Omit { t: 'l'; v: { + _id?: string; + token?: string; status: 'online' | 'busy' | 'away' | 'offline'; }; + transcriptRequest?: IRequestTranscript; + servedBy?: { + _id: string; + }; + onHold?: boolean; + departmentId?: string; + + tags: any; + closedAt: any; + metrics: any; + waitingResponse: any; + responseBy: any; + priorityId: any; + livechatData: any; } + +export const isOmnichannelRoom = (room: IRoom): room is IOmnichannelRoom & IRoom => room.t === 'l'; diff --git a/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.js b/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.js index 11d276937226..8c11c466011a 100644 --- a/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.js +++ b/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.js @@ -5,7 +5,7 @@ import { Users } from '../../../../../app/models'; import { LivechatInquiry, OmnichannelQueue } from '../../../../../app/models/server/raw'; import LivechatUnit from '../../../models/server/models/LivechatUnit'; import LivechatTag from '../../../models/server/models/LivechatTag'; -import { LivechatRooms, Subscriptions, Messages } from '../../../../../app/models/server'; +import { LivechatRooms, Messages } from '../../../../../app/models/server'; import LivechatPriority from '../../../models/server/models/LivechatPriority'; import { addUserRoles, removeUserFromRoles } from '../../../../../app/authorization/server'; import { processWaitingQueue, removePriorityFromRooms, updateInquiryQueuePriority, updatePriorityInquiries, updateRoomPriorityHistory } from './Helper'; @@ -173,7 +173,6 @@ export const LivechatEnterprise = { return false; } LivechatRooms.setOnHold(roomId); - Subscriptions.setOnHold(roomId); Messages.createOnHoldHistoryWithRoomIdMessageAndUser(roomId, comment, onHoldBy); Meteor.defer(() => { @@ -191,7 +190,6 @@ export const LivechatEnterprise = { await AutoCloseOnHoldScheduler.unscheduleRoom(roomId); LivechatRooms.unsetAllOnHoldFieldsByRoomId(roomId); - Subscriptions.unsetOnHold(roomId); }, }; diff --git a/ee/app/livechat-enterprise/server/methods/resumeOnHold.ts b/ee/app/livechat-enterprise/server/methods/resumeOnHold.ts index 7a2ef789137d..5caae4ae074c 100644 --- a/ee/app/livechat-enterprise/server/methods/resumeOnHold.ts +++ b/ee/app/livechat-enterprise/server/methods/resumeOnHold.ts @@ -2,7 +2,7 @@ import { Meteor } from 'meteor/meteor'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import { LivechatRooms, LivechatInquiry, Messages, Users, LivechatVisitors } from '../../../../../app/models/server'; -import { RoutingManager } from '../../../../../app/livechat/server/lib/RoutingManager'; +import { LivechatEnterprise } from '../lib/LivechatEnterprise'; import { callbacks } from '../../../../../app/callbacks/server'; const resolveOnHoldCommentInfo = (options: { clientAction: boolean }, room: any, onHoldChatResumedBy: any): string => { @@ -33,14 +33,12 @@ Meteor.methods({ throw new Meteor.Error('room-closed', 'Room is not OnHold', { method: 'livechat:resumeOnHold' }); } - const { servedBy: { _id: agentId, username } } = room; - const inquiry = LivechatInquiry.findOneByRoomId(roomId, {}); if (!inquiry) { throw new Meteor.Error('inquiry-not-found', 'Error! No inquiry found for this room', { method: 'livechat:resumeOnHold' }); } - await RoutingManager.takeInquiry(inquiry, { agentId, username }, options); + LivechatEnterprise.releaseOnHoldChat(room); const onHoldChatResumedBy = options.clientAction ? Meteor.user() : Users.findOneById('rocket.cat'); diff --git a/server/modules/watchers/publishFields.ts b/server/modules/watchers/publishFields.ts index bc6f68a7a78d..33fb8aab1c04 100644 --- a/server/modules/watchers/publishFields.ts +++ b/server/modules/watchers/publishFields.ts @@ -36,6 +36,8 @@ export const subscriptionFields = { tunread: 1, tunreadGroup: 1, tunreadUser: 1, + + // Omnichannel fields v: 1, onHold: 1, }; @@ -69,14 +71,12 @@ export const roomFields = { usersCount: 1, // @TODO create an API to register this fields based on room type - livechatData: 1, tags: 1, sms: 1, facebook: 1, code: 1, joinCodeRequired: 1, open: 1, - v: 1, label: 1, ro: 1, reactWhenReadOnly: 1, @@ -87,10 +87,18 @@ export const roomFields = { broadcast: 1, encrypted: 1, e2eKeyId: 1, + + // Omnichannel fields + livechatData: 1, + priorityId: 1, + v: 1, departmentId: 1, servedBy: 1, - priorityId: 1, transcriptRequest: 1, + onHold: 1, + metrics: 1, + ts: 1, + waitingResponse: 1, // fields used by DMs usernames: 1,