's here\n // or React will give console errors about invalid HTML in development mode\n
\n \n {sanitizeHtml(latestMessage, {\n allowedTags: [],\n })}\n \n\n {autoDisposesAtTimestamp && }\n \n }\n />\n \n );\n};\n\nexport default ContactItem;\n","import List from '@mui/material/List';\nimport Box from '@mui/system/Box';\nimport React, { useMemo } from 'react';\n\nimport EmptyState from '~/components/EmptyState';\nimport { AuthorRole, Customer } from '~services/AsyncManager/domain';\nimport {\n getKeysSortedByNewestConversationCreatedDate,\n getOrderedMessageKeysByMessageIdAndCreatedTimestamp,\n getOrderedNumberKeys,\n} from '~services/AsyncManager/helpers';\n\nimport CustomerItem from './CustomerItem';\n\ninterface ConversationContainerProps {\n selectedCustomerKey: string;\n customers: { [key: string]: Customer };\n setActiveCustomer: (customerId: string) => void;\n}\n\nconst ConversationContainer = ({ selectedCustomerKey, customers, setActiveCustomer }: ConversationContainerProps) => {\n const sortedCustomerKeys = useMemo(() => {\n return getKeysSortedByNewestConversationCreatedDate(customers);\n }, [Object.keys(customers).length]);\n\n const hasContacts = sortedCustomerKeys.length > 0;\n const contactsDisplay = sortedCustomerKeys.map((key: string) => {\n const isSelected = key === selectedCustomerKey;\n const orderedConversationKeys = getOrderedNumberKeys(customers[key].conversations);\n const convKey = orderedConversationKeys[orderedConversationKeys.length - 1];\n const conv = customers[key].conversations[convKey];\n const sortedMessageKeys = getOrderedMessageKeysByMessageIdAndCreatedTimestamp(conv.messages);\n const unseenMessageCount = sortedMessageKeys.filter((key) => {\n return conv.messages[key].readTimestamp === undefined && conv.messages[key].authorRole === AuthorRole.Customer;\n }).length;\n const hasAgentOrCustomerMessages =\n sortedMessageKeys.filter((key) => {\n return conv.messages[key].authorRole !== AuthorRole.System;\n }).length > 0;\n\n let latestMessage = '';\n let latestTimestamp = conv.createdTimestamp;\n if (sortedMessageKeys.length > 1) {\n const msgKey = sortedMessageKeys[sortedMessageKeys.length - 1];\n const msg = conv.messages[msgKey];\n\n if (msg !== undefined) {\n latestMessage = msg.text;\n\n if (msg.sentTimestamp !== undefined) {\n latestTimestamp = msg.sentTimestamp;\n }\n }\n }\n\n return (\n
setActiveCustomer(key)}\n />\n );\n });\n\n return (\n \n {!hasContacts && }\n {hasContacts && {contactsDisplay}
}\n \n );\n};\n\nexport default ConversationContainer;\n","import * as React from 'react';\n/**\n * @ignore - internal component.\n * @type {React.Context<{} | {expanded: boolean, disabled: boolean, toggle: () => void}>}\n */\n\nconst AccordionContext = /*#__PURE__*/React.createContext({});\n\nif (process.env.NODE_ENV !== 'production') {\n AccordionContext.displayName = 'AccordionContext';\n}\n\nexport default AccordionContext;","import { generateUtilityClass, generateUtilityClasses } from '@mui/base';\nexport function getAccordionUtilityClass(slot) {\n return generateUtilityClass('MuiAccordion', slot);\n}\nconst accordionClasses = generateUtilityClasses('MuiAccordion', ['root', 'rounded', 'expanded', 'disabled', 'gutters', 'region']);\nexport default accordionClasses;","import _objectWithoutPropertiesLoose from \"@babel/runtime/helpers/esm/objectWithoutPropertiesLoose\";\nimport _extends from \"@babel/runtime/helpers/esm/extends\";\nconst _excluded = [\"children\", \"className\", \"defaultExpanded\", \"disabled\", \"disableGutters\", \"expanded\", \"onChange\", \"square\", \"TransitionComponent\", \"TransitionProps\"];\nimport * as React from 'react';\nimport { isFragment } from 'react-is';\nimport PropTypes from 'prop-types';\nimport clsx from 'clsx';\nimport { chainPropTypes } from '@mui/utils';\nimport { unstable_composeClasses as composeClasses } from '@mui/base';\nimport styled from '../styles/styled';\nimport useThemeProps from '../styles/useThemeProps';\nimport Collapse from '../Collapse';\nimport Paper from '../Paper';\nimport AccordionContext from './AccordionContext';\nimport useControlled from '../utils/useControlled';\nimport accordionClasses, { getAccordionUtilityClass } from './accordionClasses';\nimport { jsx as _jsx } from \"react/jsx-runtime\";\nimport { jsxs as _jsxs } from \"react/jsx-runtime\";\n\nconst useUtilityClasses = ownerState => {\n const {\n classes,\n square,\n expanded,\n disabled,\n disableGutters\n } = ownerState;\n const slots = {\n root: ['root', !square && 'rounded', expanded && 'expanded', disabled && 'disabled', !disableGutters && 'gutters'],\n region: ['region']\n };\n return composeClasses(slots, getAccordionUtilityClass, classes);\n};\n\nconst AccordionRoot = styled(Paper, {\n name: 'MuiAccordion',\n slot: 'Root',\n overridesResolver: (props, styles) => {\n const {\n ownerState\n } = props;\n return [{\n [`& .${accordionClasses.region}`]: styles.region\n }, styles.root, !ownerState.square && styles.rounded, !ownerState.disableGutters && styles.gutters];\n }\n})(({\n theme\n}) => {\n const transition = {\n duration: theme.transitions.duration.shortest\n };\n return {\n position: 'relative',\n transition: theme.transitions.create(['margin'], transition),\n overflowAnchor: 'none',\n // Keep the same scrolling position\n '&:before': {\n position: 'absolute',\n left: 0,\n top: -1,\n right: 0,\n height: 1,\n content: '\"\"',\n opacity: 1,\n backgroundColor: (theme.vars || theme).palette.divider,\n transition: theme.transitions.create(['opacity', 'background-color'], transition)\n },\n '&:first-of-type': {\n '&:before': {\n display: 'none'\n }\n },\n [`&.${accordionClasses.expanded}`]: {\n '&:before': {\n opacity: 0\n },\n '&:first-of-type': {\n marginTop: 0\n },\n '&:last-of-type': {\n marginBottom: 0\n },\n '& + &': {\n '&:before': {\n display: 'none'\n }\n }\n },\n [`&.${accordionClasses.disabled}`]: {\n backgroundColor: (theme.vars || theme).palette.action.disabledBackground\n }\n };\n}, ({\n theme,\n ownerState\n}) => _extends({}, !ownerState.square && {\n borderRadius: 0,\n '&:first-of-type': {\n borderTopLeftRadius: (theme.vars || theme).shape.borderRadius,\n borderTopRightRadius: (theme.vars || theme).shape.borderRadius\n },\n '&:last-of-type': {\n borderBottomLeftRadius: (theme.vars || theme).shape.borderRadius,\n borderBottomRightRadius: (theme.vars || theme).shape.borderRadius,\n // Fix a rendering issue on Edge\n '@supports (-ms-ime-align: auto)': {\n borderBottomLeftRadius: 0,\n borderBottomRightRadius: 0\n }\n }\n}, !ownerState.disableGutters && {\n [`&.${accordionClasses.expanded}`]: {\n margin: '16px 0'\n }\n}));\nconst Accordion = /*#__PURE__*/React.forwardRef(function Accordion(inProps, ref) {\n const props = useThemeProps({\n props: inProps,\n name: 'MuiAccordion'\n });\n\n const {\n children: childrenProp,\n className,\n defaultExpanded = false,\n disabled = false,\n disableGutters = false,\n expanded: expandedProp,\n onChange,\n square = false,\n TransitionComponent = Collapse,\n TransitionProps\n } = props,\n other = _objectWithoutPropertiesLoose(props, _excluded);\n\n const [expanded, setExpandedState] = useControlled({\n controlled: expandedProp,\n default: defaultExpanded,\n name: 'Accordion',\n state: 'expanded'\n });\n const handleChange = React.useCallback(event => {\n setExpandedState(!expanded);\n\n if (onChange) {\n onChange(event, !expanded);\n }\n }, [expanded, onChange, setExpandedState]);\n const [summary, ...children] = React.Children.toArray(childrenProp);\n const contextValue = React.useMemo(() => ({\n expanded,\n disabled,\n disableGutters,\n toggle: handleChange\n }), [expanded, disabled, disableGutters, handleChange]);\n\n const ownerState = _extends({}, props, {\n square,\n disabled,\n disableGutters,\n expanded\n });\n\n const classes = useUtilityClasses(ownerState);\n return /*#__PURE__*/_jsxs(AccordionRoot, _extends({\n className: clsx(classes.root, className),\n ref: ref,\n ownerState: ownerState,\n square: square\n }, other, {\n children: [/*#__PURE__*/_jsx(AccordionContext.Provider, {\n value: contextValue,\n children: summary\n }), /*#__PURE__*/_jsx(TransitionComponent, _extends({\n in: expanded,\n timeout: \"auto\"\n }, TransitionProps, {\n children: /*#__PURE__*/_jsx(\"div\", {\n \"aria-labelledby\": summary.props.id,\n id: summary.props['aria-controls'],\n role: \"region\",\n className: classes.region,\n children: children\n })\n }))]\n }));\n});\nprocess.env.NODE_ENV !== \"production\" ? Accordion.propTypes\n/* remove-proptypes */\n= {\n // ----------------------------- Warning --------------------------------\n // | These PropTypes are generated from the TypeScript type definitions |\n // | To update them edit the d.ts file and run \"yarn proptypes\" |\n // ----------------------------------------------------------------------\n\n /**\n * The content of the component.\n */\n children: chainPropTypes(PropTypes.node.isRequired, props => {\n const summary = React.Children.toArray(props.children)[0];\n\n if (isFragment(summary)) {\n return new Error(\"MUI: The Accordion doesn't accept a Fragment as a child. \" + 'Consider providing an array instead.');\n }\n\n if (! /*#__PURE__*/React.isValidElement(summary)) {\n return new Error('MUI: Expected the first child of Accordion to be a valid element.');\n }\n\n return null;\n }),\n\n /**\n * Override or extend the styles applied to the component.\n */\n classes: PropTypes.object,\n\n /**\n * @ignore\n */\n className: PropTypes.string,\n\n /**\n * If `true`, expands the accordion by default.\n * @default false\n */\n defaultExpanded: PropTypes.bool,\n\n /**\n * If `true`, the component is disabled.\n * @default false\n */\n disabled: PropTypes.bool,\n\n /**\n * If `true`, it removes the margin between two expanded accordion items and the increase of height.\n * @default false\n */\n disableGutters: PropTypes.bool,\n\n /**\n * If `true`, expands the accordion, otherwise collapse it.\n * Setting this prop enables control over the accordion.\n */\n expanded: PropTypes.bool,\n\n /**\n * Callback fired when the expand/collapse state is changed.\n *\n * @param {React.SyntheticEvent} event The event source of the callback. **Warning**: This is a generic event not a change event.\n * @param {boolean} expanded The `expanded` state of the accordion.\n */\n onChange: PropTypes.func,\n\n /**\n * If `true`, rounded corners are disabled.\n * @default false\n */\n square: PropTypes.bool,\n\n /**\n * The system prop that allows defining system overrides as well as additional CSS styles.\n */\n sx: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.func, PropTypes.object, PropTypes.bool])), PropTypes.func, PropTypes.object]),\n\n /**\n * The component used for the transition.\n * [Follow this guide](/material-ui/transitions/#transitioncomponent-prop) to learn more about the requirements for this component.\n * @default Collapse\n */\n TransitionComponent: PropTypes.elementType,\n\n /**\n * Props applied to the transition element.\n * By default, the element is based on this [`Transition`](http://reactcommunity.org/react-transition-group/transition/) component.\n */\n TransitionProps: PropTypes.object\n} : void 0;\nexport default Accordion;","import { generateUtilityClass, generateUtilityClasses } from '@mui/base';\nexport function getAccordionActionsUtilityClass(slot) {\n return generateUtilityClass('MuiAccordionActions', slot);\n}\nconst accordionActionsClasses = generateUtilityClasses('MuiAccordionActions', ['root', 'spacing']);\nexport default accordionActionsClasses;","import _objectWithoutPropertiesLoose from \"@babel/runtime/helpers/esm/objectWithoutPropertiesLoose\";\nimport _extends from \"@babel/runtime/helpers/esm/extends\";\nconst _excluded = [\"className\", \"disableSpacing\"];\nimport * as React from 'react';\nimport PropTypes from 'prop-types';\nimport clsx from 'clsx';\nimport { unstable_composeClasses as composeClasses } from '@mui/base';\nimport styled from '../styles/styled';\nimport useThemeProps from '../styles/useThemeProps';\nimport { getAccordionActionsUtilityClass } from './accordionActionsClasses';\nimport { jsx as _jsx } from \"react/jsx-runtime\";\n\nconst useUtilityClasses = ownerState => {\n const {\n classes,\n disableSpacing\n } = ownerState;\n const slots = {\n root: ['root', !disableSpacing && 'spacing']\n };\n return composeClasses(slots, getAccordionActionsUtilityClass, classes);\n};\n\nconst AccordionActionsRoot = styled('div', {\n name: 'MuiAccordionActions',\n slot: 'Root',\n overridesResolver: (props, styles) => {\n const {\n ownerState\n } = props;\n return [styles.root, !ownerState.disableSpacing && styles.spacing];\n }\n})(({\n ownerState\n}) => _extends({\n display: 'flex',\n alignItems: 'center',\n padding: 8,\n justifyContent: 'flex-end'\n}, !ownerState.disableSpacing && {\n '& > :not(:first-of-type)': {\n marginLeft: 8\n }\n}));\nconst AccordionActions = /*#__PURE__*/React.forwardRef(function AccordionActions(inProps, ref) {\n const props = useThemeProps({\n props: inProps,\n name: 'MuiAccordionActions'\n });\n\n const {\n className,\n disableSpacing = false\n } = props,\n other = _objectWithoutPropertiesLoose(props, _excluded);\n\n const ownerState = _extends({}, props, {\n disableSpacing\n });\n\n const classes = useUtilityClasses(ownerState);\n return /*#__PURE__*/_jsx(AccordionActionsRoot, _extends({\n className: clsx(classes.root, className),\n ref: ref,\n ownerState: ownerState\n }, other));\n});\nprocess.env.NODE_ENV !== \"production\" ? AccordionActions.propTypes\n/* remove-proptypes */\n= {\n // ----------------------------- Warning --------------------------------\n // | These PropTypes are generated from the TypeScript type definitions |\n // | To update them edit the d.ts file and run \"yarn proptypes\" |\n // ----------------------------------------------------------------------\n\n /**\n * The content of the component.\n */\n children: PropTypes.node,\n\n /**\n * Override or extend the styles applied to the component.\n */\n classes: PropTypes.object,\n\n /**\n * @ignore\n */\n className: PropTypes.string,\n\n /**\n * If `true`, the actions do not have additional margin.\n * @default false\n */\n disableSpacing: PropTypes.bool,\n\n /**\n * The system prop that allows defining system overrides as well as additional CSS styles.\n */\n sx: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.func, PropTypes.object, PropTypes.bool])), PropTypes.func, PropTypes.object])\n} : void 0;\nexport default AccordionActions;","import { generateUtilityClass, generateUtilityClasses } from '@mui/base';\nexport function getAccordionDetailsUtilityClass(slot) {\n return generateUtilityClass('MuiAccordionDetails', slot);\n}\nconst accordionDetailsClasses = generateUtilityClasses('MuiAccordionDetails', ['root']);\nexport default accordionDetailsClasses;","import _extends from \"@babel/runtime/helpers/esm/extends\";\nimport _objectWithoutPropertiesLoose from \"@babel/runtime/helpers/esm/objectWithoutPropertiesLoose\";\nconst _excluded = [\"className\"];\nimport * as React from 'react';\nimport PropTypes from 'prop-types';\nimport clsx from 'clsx';\nimport { unstable_composeClasses as composeClasses } from '@mui/base';\nimport styled from '../styles/styled';\nimport useThemeProps from '../styles/useThemeProps';\nimport { getAccordionDetailsUtilityClass } from './accordionDetailsClasses';\nimport { jsx as _jsx } from \"react/jsx-runtime\";\n\nconst useUtilityClasses = ownerState => {\n const {\n classes\n } = ownerState;\n const slots = {\n root: ['root']\n };\n return composeClasses(slots, getAccordionDetailsUtilityClass, classes);\n};\n\nconst AccordionDetailsRoot = styled('div', {\n name: 'MuiAccordionDetails',\n slot: 'Root',\n overridesResolver: (props, styles) => styles.root\n})(({\n theme\n}) => ({\n padding: theme.spacing(1, 2, 2)\n}));\nconst AccordionDetails = /*#__PURE__*/React.forwardRef(function AccordionDetails(inProps, ref) {\n const props = useThemeProps({\n props: inProps,\n name: 'MuiAccordionDetails'\n });\n\n const {\n className\n } = props,\n other = _objectWithoutPropertiesLoose(props, _excluded);\n\n const ownerState = props;\n const classes = useUtilityClasses(ownerState);\n return /*#__PURE__*/_jsx(AccordionDetailsRoot, _extends({\n className: clsx(classes.root, className),\n ref: ref,\n ownerState: ownerState\n }, other));\n});\nprocess.env.NODE_ENV !== \"production\" ? AccordionDetails.propTypes\n/* remove-proptypes */\n= {\n // ----------------------------- Warning --------------------------------\n // | These PropTypes are generated from the TypeScript type definitions |\n // | To update them edit the d.ts file and run \"yarn proptypes\" |\n // ----------------------------------------------------------------------\n\n /**\n * The content of the component.\n */\n children: PropTypes.node,\n\n /**\n * Override or extend the styles applied to the component.\n */\n classes: PropTypes.object,\n\n /**\n * @ignore\n */\n className: PropTypes.string,\n\n /**\n * The system prop that allows defining system overrides as well as additional CSS styles.\n */\n sx: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.func, PropTypes.object, PropTypes.bool])), PropTypes.func, PropTypes.object])\n} : void 0;\nexport default AccordionDetails;","import { generateUtilityClass, generateUtilityClasses } from '@mui/base';\nexport function getAccordionSummaryUtilityClass(slot) {\n return generateUtilityClass('MuiAccordionSummary', slot);\n}\nconst accordionSummaryClasses = generateUtilityClasses('MuiAccordionSummary', ['root', 'expanded', 'focusVisible', 'disabled', 'gutters', 'contentGutters', 'content', 'expandIconWrapper']);\nexport default accordionSummaryClasses;","import _objectWithoutPropertiesLoose from \"@babel/runtime/helpers/esm/objectWithoutPropertiesLoose\";\nimport _extends from \"@babel/runtime/helpers/esm/extends\";\nconst _excluded = [\"children\", \"className\", \"expandIcon\", \"focusVisibleClassName\", \"onClick\"];\nimport * as React from 'react';\nimport PropTypes from 'prop-types';\nimport clsx from 'clsx';\nimport { unstable_composeClasses as composeClasses } from '@mui/base';\nimport styled from '../styles/styled';\nimport useThemeProps from '../styles/useThemeProps';\nimport ButtonBase from '../ButtonBase';\nimport AccordionContext from '../Accordion/AccordionContext';\nimport accordionSummaryClasses, { getAccordionSummaryUtilityClass } from './accordionSummaryClasses';\nimport { jsx as _jsx } from \"react/jsx-runtime\";\nimport { jsxs as _jsxs } from \"react/jsx-runtime\";\n\nconst useUtilityClasses = ownerState => {\n const {\n classes,\n expanded,\n disabled,\n disableGutters\n } = ownerState;\n const slots = {\n root: ['root', expanded && 'expanded', disabled && 'disabled', !disableGutters && 'gutters'],\n focusVisible: ['focusVisible'],\n content: ['content', expanded && 'expanded', !disableGutters && 'contentGutters'],\n expandIconWrapper: ['expandIconWrapper', expanded && 'expanded']\n };\n return composeClasses(slots, getAccordionSummaryUtilityClass, classes);\n};\n\nconst AccordionSummaryRoot = styled(ButtonBase, {\n name: 'MuiAccordionSummary',\n slot: 'Root',\n overridesResolver: (props, styles) => styles.root\n})(({\n theme,\n ownerState\n}) => {\n const transition = {\n duration: theme.transitions.duration.shortest\n };\n return _extends({\n display: 'flex',\n minHeight: 48,\n padding: theme.spacing(0, 2),\n transition: theme.transitions.create(['min-height', 'background-color'], transition),\n [`&.${accordionSummaryClasses.focusVisible}`]: {\n backgroundColor: (theme.vars || theme).palette.action.focus\n },\n [`&.${accordionSummaryClasses.disabled}`]: {\n opacity: (theme.vars || theme).palette.action.disabledOpacity\n },\n [`&:hover:not(.${accordionSummaryClasses.disabled})`]: {\n cursor: 'pointer'\n }\n }, !ownerState.disableGutters && {\n [`&.${accordionSummaryClasses.expanded}`]: {\n minHeight: 64\n }\n });\n});\nconst AccordionSummaryContent = styled('div', {\n name: 'MuiAccordionSummary',\n slot: 'Content',\n overridesResolver: (props, styles) => styles.content\n})(({\n theme,\n ownerState\n}) => _extends({\n display: 'flex',\n flexGrow: 1,\n margin: '12px 0'\n}, !ownerState.disableGutters && {\n transition: theme.transitions.create(['margin'], {\n duration: theme.transitions.duration.shortest\n }),\n [`&.${accordionSummaryClasses.expanded}`]: {\n margin: '20px 0'\n }\n}));\nconst AccordionSummaryExpandIconWrapper = styled('div', {\n name: 'MuiAccordionSummary',\n slot: 'ExpandIconWrapper',\n overridesResolver: (props, styles) => styles.expandIconWrapper\n})(({\n theme\n}) => ({\n display: 'flex',\n color: (theme.vars || theme).palette.action.active,\n transform: 'rotate(0deg)',\n transition: theme.transitions.create('transform', {\n duration: theme.transitions.duration.shortest\n }),\n [`&.${accordionSummaryClasses.expanded}`]: {\n transform: 'rotate(180deg)'\n }\n}));\nconst AccordionSummary = /*#__PURE__*/React.forwardRef(function AccordionSummary(inProps, ref) {\n const props = useThemeProps({\n props: inProps,\n name: 'MuiAccordionSummary'\n });\n\n const {\n children,\n className,\n expandIcon,\n focusVisibleClassName,\n onClick\n } = props,\n other = _objectWithoutPropertiesLoose(props, _excluded);\n\n const {\n disabled = false,\n disableGutters,\n expanded,\n toggle\n } = React.useContext(AccordionContext);\n\n const handleChange = event => {\n if (toggle) {\n toggle(event);\n }\n\n if (onClick) {\n onClick(event);\n }\n };\n\n const ownerState = _extends({}, props, {\n expanded,\n disabled,\n disableGutters\n });\n\n const classes = useUtilityClasses(ownerState);\n return /*#__PURE__*/_jsxs(AccordionSummaryRoot, _extends({\n focusRipple: false,\n disableRipple: true,\n disabled: disabled,\n component: \"div\",\n \"aria-expanded\": expanded,\n className: clsx(classes.root, className),\n focusVisibleClassName: clsx(classes.focusVisible, focusVisibleClassName),\n onClick: handleChange,\n ref: ref,\n ownerState: ownerState\n }, other, {\n children: [/*#__PURE__*/_jsx(AccordionSummaryContent, {\n className: classes.content,\n ownerState: ownerState,\n children: children\n }), expandIcon && /*#__PURE__*/_jsx(AccordionSummaryExpandIconWrapper, {\n className: classes.expandIconWrapper,\n ownerState: ownerState,\n children: expandIcon\n })]\n }));\n});\nprocess.env.NODE_ENV !== \"production\" ? AccordionSummary.propTypes\n/* remove-proptypes */\n= {\n // ----------------------------- Warning --------------------------------\n // | These PropTypes are generated from the TypeScript type definitions |\n // | To update them edit the d.ts file and run \"yarn proptypes\" |\n // ----------------------------------------------------------------------\n\n /**\n * The content of the component.\n */\n children: PropTypes.node,\n\n /**\n * Override or extend the styles applied to the component.\n */\n classes: PropTypes.object,\n\n /**\n * @ignore\n */\n className: PropTypes.string,\n\n /**\n * The icon to display as the expand indicator.\n */\n expandIcon: PropTypes.node,\n\n /**\n * This prop can help identify which element has keyboard focus.\n * The class name will be applied when the element gains the focus through keyboard interaction.\n * It's a polyfill for the [CSS :focus-visible selector](https://drafts.csswg.org/selectors-4/#the-focus-visible-pseudo).\n * The rationale for using this feature [is explained here](https://github.com/WICG/focus-visible/blob/HEAD/explainer.md).\n * A [polyfill can be used](https://github.com/WICG/focus-visible) to apply a `focus-visible` class to other components\n * if needed.\n */\n focusVisibleClassName: PropTypes.string,\n\n /**\n * @ignore\n */\n onClick: PropTypes.func,\n\n /**\n * The system prop that allows defining system overrides as well as additional CSS styles.\n */\n sx: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.func, PropTypes.object, PropTypes.bool])), PropTypes.func, PropTypes.object])\n} : void 0;\nexport default AccordionSummary;","import ExpandMoreIcon from '@mui/icons-material/ExpandMore';\nimport LoadingButton from '@mui/lab/LoadingButton';\nimport Accordion from '@mui/material/Accordion';\nimport AccordionActions from '@mui/material/AccordionActions';\nimport AccordionDetails from '@mui/material/AccordionDetails';\nimport AccordionSummary from '@mui/material/AccordionSummary';\nimport Autocomplete from '@mui/material/Autocomplete';\nimport Box from '@mui/material/Box';\nimport Button from '@mui/material/Button';\nimport blue from '@mui/material/colors/blue';\nimport grey from '@mui/material/colors/grey';\nimport Divider from '@mui/material/Divider';\nimport Grid from '@mui/material/Grid';\nimport Link from '@mui/material/Link';\nimport TextField from '@mui/material/TextField';\nimport Typography from '@mui/material/Typography';\nimport { DateTime } from 'luxon';\nimport React, { Fragment, useMemo, useState } from 'react';\nimport { Control, Controller, FieldErrors, useForm } from 'react-hook-form';\n\nimport ContentSpacer from '~components/ContentSpacer';\nimport { DataItem } from '~components/DataItem';\nimport organisations from '~organisations';\nimport {\n AuthorRole,\n Conversation as ConversationDomain,\n MessageType,\n QueueDisposition,\n} from '~services/AsyncManager/domain';\nimport { getOrderedMessageKeysByMessageIdAndCreatedTimestamp } from '~services/AsyncManager/helpers';\nimport { convertDictValuesToString, flattenObject, generatePathNoErrorThrow } from '~utils/Functions';\n\nimport MessageBubble from './MessageBubble';\n\ninterface ConversationProps {\n conversation: ConversationDomain;\n orgReference: string;\n attributeConfiguration: { key: string; label: string }[];\n contactPopLinks: { [key: string]: string };\n expanded?: boolean;\n // Tells us if collapsing of according is disabled\n disabled?: boolean;\n // Tells us where to put the new Message line above\n newMessagePos?: string;\n onDispose: (dispositionCode: string, dispositionSubCode: string, attributes: { [key: string]: string }) => void;\n}\n\ninterface DispositionFormData {\n disposition: QueueDisposition;\n // Note: This is used to support all additional dynamic outcome capture fields defined with the organisation's module\n // without having to worry about typing issues. Note: This union take over the whole interface\n // so QueueDisposition also needs to be defined here else the disposition's property will error.\n [key: string]: QueueDisposition | string | null;\n}\n\n// Note: This is used by the organisation's module to generically define additional form elements\nexport interface AdditionalFieldsProps {\n control: Control;\n errors: FieldErrors;\n selectedDisposition: QueueDisposition | null;\n formSubmitting: boolean;\n}\n\n// We do this as we need the value of disposition to be able to be null by default for the material UI auto select component to\n// stop having a fit over \"\" does not exist in list as a selectable option\ninterface FormDefaults extends Omit {\n disposition: QueueDisposition | null;\n}\n\nconst formatDate = (timestamp: string | undefined) => {\n if (timestamp === undefined) return '';\n\n const dateTime = new Date(timestamp);\n\n // Anything not a date timestamp will be caught here\n if (dateTime.toString() === 'Invalid Date') return '';\n\n const now = new Date();\n const time = DateTime.fromISO(timestamp).toFormat('h:mm a');\n const isToday =\n now.getDate() == dateTime.getDate() &&\n now.getMonth() == dateTime.getMonth() &&\n now.getFullYear() == dateTime.getFullYear();\n\n return isToday ? `Today at ${time}` : `${DateTime.fromISO(timestamp).toFormat('yyyy/MM/dd')} at ${time}`;\n};\n\nconst ATTRIBUTES_POS_INCREMENT = 3;\n\n// These are all known conversation attributes that are set by the backend on conversation creation.\n// If a value is not found within this dict, then it wont be displayed.\n//\n// Note: This should not include dynamic attributes that can be customised by clients. That should be configured\n// within the toml file under additional_conversation_attributes.\nconst KNOWN_ATTRIBUTE_DISPLAY_NAMES: { [key: string]: string } = {\n queue: 'Queue Identifier',\n queue_title: 'Queue Name',\n email: 'User Entered Email',\n};\n\nconst Conversation = ({\n conversation,\n orgReference,\n attributeConfiguration,\n contactPopLinks,\n expanded,\n disabled,\n newMessagePos,\n onDispose,\n}: ConversationProps) => {\n const [isExpanded, setIsExpanded] = useState(Boolean(expanded));\n // Note: we only set this to true on submit, it will stay forever loading on first submit click\n // this is because we have no way of knowing if the backend request has failed or not. If an agent is suffering from\n // a slow connection, this will prevent them from mas spamming requests leading to other potential issues\n const [isSubmitting, setIsSubmitting] = useState(false);\n // Wer want to display the first 3 attributes by default, with more being displayed when this position counter\n // increments.\n const [attributesIndexPos, setAttributesIndexPos] = useState(ATTRIBUTES_POS_INCREMENT - 1);\n const {\n formState: { errors },\n handleSubmit,\n control,\n watch,\n } = useForm({\n defaultValues: {\n disposition: null,\n },\n mode: 'all',\n reValidateMode: 'onChange',\n shouldUnregister: true,\n });\n const dispositionWatch: QueueDisposition | null = watch('disposition');\n const orderedMessageKeys = getOrderedMessageKeysByMessageIdAndCreatedTimestamp(conversation.messages);\n const displayableAttributes = useMemo(() => {\n let allowedKeys = { ...KNOWN_ATTRIBUTE_DISPLAY_NAMES };\n\n // let's add configured additional attribute keys\n for (let item of attributeConfiguration) {\n allowedKeys = { ...allowedKeys, [item.key]: item.label };\n }\n\n // Let's add all global attribute keys\n const contactPopLinkKeys = Object.keys(contactPopLinks);\n for (let key of contactPopLinkKeys) {\n allowedKeys = { ...allowedKeys, [key]: key };\n }\n\n const allAttributes = { ...conversation.attributes, ...contactPopLinks };\n return Object.keys(allAttributes)\n .filter((key) => Boolean(allowedKeys[key]))\n .map((key) => ({\n label: allowedKeys[key],\n value: allAttributes[key],\n }));\n }, [attributeConfiguration, contactPopLinks, conversation.attributes]);\n // We add 1 as index's start at zero\n const hasMoreAttributesToShow = attributesIndexPos + 1 < Object.keys(displayableAttributes).length;\n const AdditionalFields =\n organisations[orgReference]?.asyncAdditionalOutcomeCaptureFields[conversation.queue] || undefined;\n\n const onChange = () => {\n setIsExpanded((prev) => !prev);\n };\n\n const submitDisposition = handleSubmit((data: FormDefaults) => {\n const { disposition, ...others } = data;\n setIsSubmitting(true);\n\n // Should not ever be null as the constraint on this field is required. However, to support the clearing of selection\n // we had to make this field optional in the interface, leading to this check being needed to make typescript happy.\n if (disposition === null) {\n setIsSubmitting(false);\n return;\n }\n\n onDispose(disposition.code, disposition.subCode, convertDictValuesToString(others));\n setIsSubmitting(false);\n });\n\n const loadMoreAttributes = () => {\n setAttributesIndexPos((prev) => prev + ATTRIBUTES_POS_INCREMENT);\n };\n\n const messageDisplay = orderedMessageKeys.map((key) => {\n let messageContent;\n\n if (conversation.messages[key].type === MessageType.Message) {\n messageContent = (\n \n );\n }\n\n if (conversation.messages[key].type === MessageType.Event) {\n messageContent = (\n \n {conversation.messages[key].text}\n \n );\n }\n\n return (\n \n {newMessagePos === key && (\n \n New Messages\n \n )}\n {messageContent}\n \n );\n });\n\n const attributeItems = displayableAttributes\n // Filter out items that are greater than the current display position\n .filter((item, index) => index <= attributesIndexPos)\n // Loop and display DataItem in relevant format\n .map((item, index) => {\n let value: any;\n // Is image url\n if (/^https?:\\/\\/.+\\.(jpg|jpeg|png|webp|avif|gif|svg)$/.test(item.value)) {\n value = (\n \n

\n
\n );\n } else if (/^https?:\\/\\/.+$/.test(item.value)) {\n // Strip out unneeded props and flatten object\n const flattenedConversation = flattenObject(\n Object.fromEntries(Object.entries(conversation).filter(([key]) => !key.includes('messages'))),\n );\n\n // Updated url params with value if found within the flattened conversation\n const url = generatePathNoErrorThrow(item.value, flattenedConversation);\n\n value = (\n \n {item.label}\n \n );\n } else {\n value = item.value;\n }\n\n return (\n \n \n \n );\n });\n\n return (\n \n \n }>\n {conversation.disposedTimestamp === undefined && (\n Conversation starting {formatDate(conversation.createdTimestamp)}\n )}\n {conversation.disposedTimestamp !== undefined && (\n Conversation Ended {formatDate(conversation.disposedTimestamp)}\n )}\n \n\n \n {attributeItems.length > 0 && (\n \n {attributeItems}\n\n {hasMoreAttributesToShow && (\n \n )}\n \n )}\n\n {messageDisplay}\n \n\n {conversation.disposedTimestamp !== undefined && (\n \n \n Conversation disposed of as {conversation.dispositionCode} / {conversation.dispositionSubCode}\n \n \n )}\n\n {conversation.canDispose === true && (\n \n \n \n )}\n \n \n );\n};\n\nexport default Conversation;\n","import { useState, useRef, useEffect, useMemo } from 'react';\nimport createDebounce from 'debounce';\n\nfunction useMeasure(_temp) {\n let {\n debounce,\n scroll,\n polyfill,\n offsetSize\n } = _temp === void 0 ? {\n debounce: 0,\n scroll: false,\n offsetSize: false\n } : _temp;\n const ResizeObserver = polyfill || (typeof window === 'undefined' ? class ResizeObserver {} : window.ResizeObserver);\n\n if (!ResizeObserver) {\n throw new Error('This browser does not support ResizeObserver out of the box. See: https://github.com/react-spring/react-use-measure/#resize-observer-polyfills');\n }\n\n const [bounds, set] = useState({\n left: 0,\n top: 0,\n width: 0,\n height: 0,\n bottom: 0,\n right: 0,\n x: 0,\n y: 0\n }); // keep all state in a ref\n\n const state = useRef({\n element: null,\n scrollContainers: null,\n resizeObserver: null,\n lastBounds: bounds\n }); // set actual debounce values early, so effects know if they should react accordingly\n\n const scrollDebounce = debounce ? typeof debounce === 'number' ? debounce : debounce.scroll : null;\n const resizeDebounce = debounce ? typeof debounce === 'number' ? debounce : debounce.resize : null; // make sure to update state only as long as the component is truly mounted\n\n const mounted = useRef(false);\n useEffect(() => {\n mounted.current = true;\n return () => void (mounted.current = false);\n }); // memoize handlers, so event-listeners know when they should update\n\n const [forceRefresh, resizeChange, scrollChange] = useMemo(() => {\n const callback = () => {\n if (!state.current.element) return;\n const {\n left,\n top,\n width,\n height,\n bottom,\n right,\n x,\n y\n } = state.current.element.getBoundingClientRect();\n const size = {\n left,\n top,\n width,\n height,\n bottom,\n right,\n x,\n y\n };\n\n if (state.current.element instanceof HTMLElement && offsetSize) {\n size.height = state.current.element.offsetHeight;\n size.width = state.current.element.offsetWidth;\n }\n\n Object.freeze(size);\n if (mounted.current && !areBoundsEqual(state.current.lastBounds, size)) set(state.current.lastBounds = size);\n };\n\n return [callback, resizeDebounce ? createDebounce(callback, resizeDebounce) : callback, scrollDebounce ? createDebounce(callback, scrollDebounce) : callback];\n }, [set, offsetSize, scrollDebounce, resizeDebounce]); // cleanup current scroll-listeners / observers\n\n function removeListeners() {\n if (state.current.scrollContainers) {\n state.current.scrollContainers.forEach(element => element.removeEventListener('scroll', scrollChange, true));\n state.current.scrollContainers = null;\n }\n\n if (state.current.resizeObserver) {\n state.current.resizeObserver.disconnect();\n state.current.resizeObserver = null;\n }\n } // add scroll-listeners / observers\n\n\n function addListeners() {\n if (!state.current.element) return;\n state.current.resizeObserver = new ResizeObserver(scrollChange);\n state.current.resizeObserver.observe(state.current.element);\n\n if (scroll && state.current.scrollContainers) {\n state.current.scrollContainers.forEach(scrollContainer => scrollContainer.addEventListener('scroll', scrollChange, {\n capture: true,\n passive: true\n }));\n }\n } // the ref we expose to the user\n\n\n const ref = node => {\n if (!node || node === state.current.element) return;\n removeListeners();\n state.current.element = node;\n state.current.scrollContainers = findScrollContainers(node);\n addListeners();\n }; // add general event listeners\n\n\n useOnWindowScroll(scrollChange, Boolean(scroll));\n useOnWindowResize(resizeChange); // respond to changes that are relevant for the listeners\n\n useEffect(() => {\n removeListeners();\n addListeners();\n }, [scroll, scrollChange, resizeChange]); // remove all listeners when the components unmounts\n\n useEffect(() => removeListeners, []);\n return [ref, bounds, forceRefresh];\n} // Adds native resize listener to window\n\n\nfunction useOnWindowResize(onWindowResize) {\n useEffect(() => {\n const cb = onWindowResize;\n window.addEventListener('resize', cb);\n return () => void window.removeEventListener('resize', cb);\n }, [onWindowResize]);\n}\n\nfunction useOnWindowScroll(onScroll, enabled) {\n useEffect(() => {\n if (enabled) {\n const cb = onScroll;\n window.addEventListener('scroll', cb, {\n capture: true,\n passive: true\n });\n return () => void window.removeEventListener('scroll', cb, true);\n }\n }, [onScroll, enabled]);\n} // Returns a list of scroll offsets\n\n\nfunction findScrollContainers(element) {\n const result = [];\n if (!element || element === document.body) return result;\n const {\n overflow,\n overflowX,\n overflowY\n } = window.getComputedStyle(element);\n if ([overflow, overflowX, overflowY].some(prop => prop === 'auto' || prop === 'scroll')) result.push(element);\n return [...result, ...findScrollContainers(element.parentElement)];\n} // Checks if element boundaries are equal\n\n\nconst keys = ['x', 'y', 'top', 'bottom', 'left', 'right', 'width', 'height'];\n\nconst areBoundsEqual = (a, b) => keys.every(key => a[key] === b[key]);\n\nexport { useMeasure as default };\n","import Box from '@mui/material/Box';\nimport Button from '@mui/material/Button';\nimport Divider from '@mui/material/Divider';\nimport Grid from '@mui/material/Grid';\nimport List from '@mui/material/List';\nimport MenuItem from '@mui/material/MenuItem';\nimport TextField from '@mui/material/TextField';\nimport Typography from '@mui/material/Typography';\nimport React, { ChangeEvent, useCallback, useEffect, useRef, useState } from 'react';\nimport useMeasure from 'react-use-measure';\n\nimport AsyncLoader from '~components/AsyncLoader';\nimport { DotLoader } from '~components/DotLoader';\nimport OberonCard from '~components/OberonCard';\nimport OberonDialog from '~components/OberonDialog';\nimport useDebounce from '~hooks/useDebounce';\nimport useMessageTemplate from '~pages/AsyncManagement/QueueDetails/MessageTemplates/useMessageTemplate';\n\ninterface Query {\n search: string;\n filter: string | '';\n}\n\ninterface MessageTemplateModalProps {\n open: boolean;\n queue: string;\n onSelect: (message: string) => void;\n onClose: () => void;\n}\n\nconst queryDefault: Query = Object.freeze({\n search: '',\n filter: '',\n});\n\nconst MessageTemplateModal = ({ open, queue, onSelect, onClose }: MessageTemplateModalProps) => {\n const [boxRef, { height: boxHeight }] = useMeasure();\n const [query, setQuery] = useState(queryDefault);\n const debouncedSearch = useDebounce(query.search, 500);\n const { messageTemplates, categories, loading, error, hasMore, getNextPage } = useMessageTemplate(\n queue,\n debouncedSearch,\n query.filter,\n );\n const observer = useRef(undefined);\n const noSearchOrFilterSet = query.search === '' && query.filter === '';\n const lastDataElement = useCallback(\n (node: any) => {\n if (loading) return;\n if (observer.current) observer.current.disconnect();\n observer.current = new IntersectionObserver((entries) => {\n if (entries[0].isIntersecting && hasMore) {\n getNextPage();\n }\n });\n if (node) observer.current.observe(node);\n },\n [loading, hasMore, getNextPage],\n );\n\n const resetQuery = useCallback(() => {\n setQuery(queryDefault);\n }, []);\n\n const onQueryChange = useCallback((e: ChangeEvent) => {\n const { name, value } = e.target;\n setQuery((prev) => ({ ...prev, [name]: value }));\n }, []);\n\n // Resets query search on unmount\n useEffect(() => {\n return () => {\n resetQuery();\n };\n }, []);\n\n let filterListDisplay = [\n ,\n ];\n filterListDisplay = [\n ...filterListDisplay,\n ...categories.map((val, index) => (\n \n )),\n ];\n\n const templateList = messageTemplates.map((item, index) => (\n onSelect(item.template)}\n titleFontWeight={700}\n title={item.title}\n subHeader={item.category}\n footer={\n \n {item.template}
\n \n }\n />\n ));\n\n return (\n \n \n \n \n \n \n\n \n \n {filterListDisplay}\n \n \n \n\n \n \n\n \n \n {messageTemplates.length > 0 && (\n <>\n {templateList}
\n {loading && messageTemplates.length > 0 && }\n\n {!loading && !hasMore && (\n \n No more results to display\n \n )}\n\n {error && messageTemplates.length > 0 && (\n \n Failed to load message templates\n \n )}\n >\n )}\n\n {messageTemplates.length == 0 && !noSearchOrFilterSet && (\n \n No message templates found matching your search criteria\n \n )}\n\n {messageTemplates.length === 0 && noSearchOrFilterSet && (\n \n No message templates available for queue {queue}\n \n )}\n \n \n \n }\n actionFooter={\n \n }\n />\n );\n};\n\nexport default MessageTemplateModal;\n","import SendIcon from '@mui/icons-material/Send';\nimport { ButtonBase } from '@mui/material';\nimport blue from '@mui/material/colors/blue';\nimport red from '@mui/material/colors/red';\nimport Divider from '@mui/material/Divider';\nimport IconButton from '@mui/material/IconButton';\nimport { styled } from '@mui/material/styles';\nimport { SvgIconProps } from '@mui/material/SvgIcon';\nimport TextareaAutosize from '@mui/material/TextareaAutosize';\nimport Tooltip from '@mui/material/Tooltip';\nimport Box from '@mui/system/Box';\nimport React, {\n ChangeEvent,\n ComponentClass,\n Fragment,\n FunctionComponent,\n KeyboardEvent,\n MouseEvent,\n useCallback,\n useEffect,\n useRef,\n} from 'react';\n\ninterface Toolbar {\n icon: ComponentClass | FunctionComponent;\n tooltip: string;\n action: () => void;\n}\n\ninterface SendMessageProps {\n toolbar: Toolbar[];\n value: string;\n onChange: (e: ChangeEvent) => void;\n sendMessage: (messageContent: string) => void;\n}\n\n// TextareaAutosize does not support sx system prop so we have to use a styled component so we can use\n// pseudo class styling\nconst CustomizedTextarea = styled(TextareaAutosize)({\n 'boxSizing': 'border-box',\n 'width': '100%',\n 'padding': '16px 66px 16px 16px',\n 'border': 'none',\n 'fontSize': 15,\n 'fontWeight': 400,\n 'fontFamily': '\"Roboto\",\"Helvetica\",\"Arial\",sans-serif',\n 'color': '#292929',\n 'outline': 'none',\n 'resize': 'none',\n\n ':disabled': {\n opacity: 0.6,\n border: '1px solid #e6e6e6',\n background: '#ffffff',\n },\n});\n\nconst MAX_MESSAGE_LENGTH = 2048;\n\nconst isKeypressEvent = (\n event: KeyboardEvent | MouseEvent,\n): event is KeyboardEvent => {\n return (event as KeyboardEvent).type === 'keypress';\n};\n\nconst isMouseEvent = (\n event: KeyboardEvent | MouseEvent,\n): event is MouseEvent => {\n return (event as MouseEvent).type === 'click';\n};\n\nconst MessageTextarea = ({ toolbar, value = '', onChange, sendMessage }: SendMessageProps) => {\n const inputRef = useRef(null);\n const isMessageTooLong = value.trim().length > MAX_MESSAGE_LENGTH;\n const templateVariablePresent = value.includes('{{') && value.includes('}}');\n const disableSend = value.trim().length === 0 || templateVariablePresent || isMessageTooLong;\n\n const onSendMessage = useCallback(\n (e: KeyboardEvent | MouseEvent) => {\n const msg = value.trim();\n\n if (disableSend) {\n if (isKeypressEvent(e) && e.key === 'Enter' && !e.shiftKey) {\n e.preventDefault();\n }\n return;\n }\n\n if ((isKeypressEvent(e) && e.key === 'Enter' && !e.shiftKey) || isMouseEvent(e)) {\n if (isKeypressEvent(e) && e.key === 'Enter' && !e.shiftKey) {\n e.preventDefault();\n }\n\n sendMessage(msg);\n\n if (inputRef.current !== null) {\n inputRef.current.focus();\n }\n }\n },\n [disableSend, value],\n );\n\n // Focus input on component mount\n useEffect(() => {\n if (inputRef.current !== null) {\n inputRef.current.focus();\n }\n }, []);\n\n return (\n <>\n {isMessageTooLong && (\n \n The message exceeds the {MAX_MESSAGE_LENGTH.toLocaleString()} character limit\n \n )}\n\n \n \n {toolbar.map((item, index) => {\n const Icon = item.icon;\n return (\n \n \n \n \n \n \n {(index !== toolbar.length - 1 || index === 0) && }\n \n );\n })}\n \n \n\n \n \n\n \n \n \n \n \n \n >\n );\n};\n\nexport default MessageTextarea;\n","import ChatBubbleIcon from '@mui/icons-material/ChatBubble';\nimport PersonIcon from '@mui/icons-material/Person';\nimport PhoneIcon from '@mui/icons-material/Phone';\nimport PhoneForwardedIcon from '@mui/icons-material/PhoneForwarded';\nimport ReplyIcon from '@mui/icons-material/Reply';\nimport Box from '@mui/material/Box';\nimport Chip from '@mui/material/Chip';\nimport blue from '@mui/material/colors/blue';\nimport blueGrey from '@mui/material/colors/blueGrey';\nimport green from '@mui/material/colors/green';\nimport red from '@mui/material/colors/red';\nimport IconButton from '@mui/material/IconButton';\nimport ListItem from '@mui/material/ListItem';\nimport ListItemSecondaryAction from '@mui/material/ListItemSecondaryAction';\nimport ListItemText from '@mui/material/ListItemText';\nimport { styled } from '@mui/material/styles';\nimport Typography from '@mui/material/Typography';\nimport React from 'react';\n\nimport { AgentState, AgentStateTypeWS } from '~services/AgentStatesManager/domain';\n\ninterface TransferableAgentListItem {\n variant?: 'voice' | 'messaging';\n divider: boolean;\n agent: AgentState;\n disabled?: boolean;\n onTransferToAgent: () => void;\n}\n\nconst CustomizedListItem = styled(ListItem)(() => ({\n padding: 16,\n}));\n\nconst CustomizedListItemText = styled(ListItemText)(() => ({\n '& .MuiListItemText-primary': {\n userSelect: 'none',\n display: 'block',\n fontSize: 16,\n paddingRight: 50,\n minWidth: 0,\n textOverflow: 'ellipsis',\n\n /* Required for text-overflow to do anything */\n whiteSpace: 'nowrap',\n overflow: 'hidden',\n },\n}));\n\ninterface Status {\n name: string;\n color: string;\n}\n\nconst stateTypeData: { [key in AgentStateTypeWS]: Status } = {\n [AgentStateTypeWS.Routable]: {\n name: 'Available',\n color: green[600],\n },\n [AgentStateTypeWS.NotRoutable]: {\n name: 'Not Available',\n color: red[800],\n },\n [AgentStateTypeWS.Offline]: {\n name: 'Offline',\n color: blueGrey[800],\n },\n};\n\nconst TransferableAgentListItem = ({\n variant = 'messaging',\n divider,\n agent,\n disabled,\n onTransferToAgent,\n}: TransferableAgentListItem) => {\n const isTransferable = agent.stateType === AgentStateTypeWS.Routable;\n const statusInfo: Status = stateTypeData[agent.stateType];\n // If backgroundColor unknown default to grey\n const statusName = statusInfo?.name || 'Unknown';\n // If lightColor unknown default to light grey variant\n const statusColor = statusInfo?.color || blueGrey['800'];\n const buttonIconStyles = variant === 'voice' ? {} : { transform: 'scaleX(-1)' };\n const ButtonIcon = variant === 'voice' ? PhoneForwardedIcon : ReplyIcon;\n const inVoiceCallColor = agent.inVoiceCall ? red[800] : blue[500];\n\n return (\n \n \n {agent.name}\n \n }\n secondary={\n <>\n \n {agent.username}\n \n\n \n \n {statusName}\n \n }\n />\n\n \n {agent.inVoiceCall ? 'In Call' : 'N/A'}\n \n }\n />\n\n \n {agent.messagingConcurrency}\n \n }\n />\n \n >\n }\n />\n\n \n onTransferToAgent()}\n size='medium'\n disabled={!isTransferable || Boolean(disabled)}\n aria-label={`Transfer to ${agent.name}`}>\n \n \n \n \n );\n};\n\nexport default TransferableAgentListItem;\n","import Box from '@mui/material/Box';\nimport Divider from '@mui/material/Divider';\nimport Grid from '@mui/material/Grid';\nimport List from '@mui/material/List';\nimport TextField from '@mui/material/TextField';\nimport Typography from '@mui/material/Typography';\nimport React, { ChangeEvent, useState } from 'react';\nimport useMeasure from 'react-use-measure';\n\nimport TransferableAgentListItem from '~components/TransferableAgentListItem';\nimport { useAgentStates } from '~providers/AgentStatesProvider';\n\ninterface AgentTransferPanelProps {\n onTransferToAgent: (agent: string) => void;\n}\n\nconst AgentTransferPanel = ({ onTransferToAgent }: AgentTransferPanelProps) => {\n const { agentAsyncList } = useAgentStates();\n const [search, setSearch] = useState('');\n const [boxRef, { height: boxHeight }] = useMeasure();\n\n const onSearchChange = (e: ChangeEvent) => {\n setSearch(e.target.value);\n };\n\n const searchTerm = search.split(/\\s+/).filter((term) => term.length > 0);\n const searchResultList = agentAsyncList\n .filter(\n (item) =>\n searchTerm.every((term) => item.name.toLowerCase().includes(term.toLowerCase())) ||\n searchTerm.every((term) => item.username.toLowerCase().includes(term.toLowerCase())),\n )\n .sort((x, y) => {\n if (x.username < y.username) {\n return -1;\n } else if (y.username < x.username) {\n return 1;\n }\n return 0;\n });\n const transferItemList = searchResultList.map((item, index) => (\n onTransferToAgent(item.username)}\n />\n ));\n\n return (\n <>\n \n \n \n \n \n \n\n \n \n\n \n {transferItemList.length === 0 && (\n \n {searchTerm.length === 0 ? \"There are no agent's to transfer to.\" : 'No search results found.'}\n \n )}\n\n {transferItemList.length > 0 && {transferItemList}
}\n \n >\n );\n};\n\nexport default AgentTransferPanel;\n","import ReplyIcon from '@mui/icons-material/Reply';\nimport Box from '@mui/material/Box';\nimport Divider from '@mui/material/Divider';\nimport Grid from '@mui/material/Grid';\nimport IconButton from '@mui/material/IconButton';\nimport List from '@mui/material/List';\nimport ListItem from '@mui/material/ListItem';\nimport ListItemSecondaryAction from '@mui/material/ListItemSecondaryAction';\nimport ListItemText from '@mui/material/ListItemText';\nimport { styled } from '@mui/material/styles';\nimport TextField from '@mui/material/TextField';\nimport Typography from '@mui/material/Typography';\nimport React, { ChangeEvent, useState } from 'react';\nimport useMeasure from 'react-use-measure';\n\nimport AsyncLoader from '~components/AsyncLoader';\nimport { DotLoader } from '~components/DotLoader';\nimport useDebounce from '~hooks/useDebounce';\nimport useAsyncQueueSearch from '~pages/AsyncManagement/useAsyncQueueSearch';\n\ninterface QueueTransferPanelProps {\n onTransferToQueue: (queue: string) => void;\n}\n\nconst CustomizedListItem = styled(ListItem)(({ theme }) => ({\n padding: 16,\n}));\n\nconst CustomizedListItemText = styled(ListItemText)(({ theme }) => ({\n '& .MuiListItemText-primary': {\n userSelect: 'none',\n display: 'block',\n fontSize: 16,\n paddingRight: 50,\n minWidth: 0,\n textOverflow: 'ellipsis',\n\n /* Required for text-overflow to do anything */\n whiteSpace: 'nowrap',\n overflow: 'hidden',\n },\n}));\n\nconst QueueTransferPanel = ({ onTransferToQueue }: QueueTransferPanelProps) => {\n const [search, setSearch] = useState('');\n const debouncedSearch = useDebounce(search, 500);\n const {\n loading,\n error,\n list,\n hasMore,\n intersectionObserverRef: lastDataElement,\n } = useAsyncQueueSearch(debouncedSearch);\n const [boxRef, { height: boxHeight }] = useMeasure();\n\n const onSearchChange = (e: ChangeEvent) => {\n setSearch(e.target.value);\n };\n\n const transferItemList = list.map((item, index) => (\n \n \n \n onTransferToQueue(item.queue)}\n size='medium'\n aria-label={`Transfer to ${item.title}`}>\n \n \n \n \n ));\n\n return (\n <>\n \n \n \n \n \n \n\n \n \n\n \n \n {list.length > 0 && (\n <>\n {transferItemList}
\n {loading && list.length > 0 && }\n\n {!loading && !hasMore && (\n \n No more results to display\n \n )}\n\n {error && list.length > 0 && (\n \n Failed to load queues\n \n )}\n >\n )}\n\n {list.length === 0 && (\n <>\n {!error && !debouncedSearch && (\n \n There are no queue's to transfer to.\n \n )}\n\n {!error && debouncedSearch && (\n \n No search results found.\n \n )}\n\n {error && (\n \n Failed to load queues\n \n )}\n >\n )}\n \n \n >\n );\n};\n\nexport default QueueTransferPanel;\n","import Button from '@mui/material/Button';\nimport Divider from '@mui/material/Divider';\nimport Tab from '@mui/material/Tab';\nimport Tabs from '@mui/material/Tabs';\nimport React, { useState } from 'react';\n\nimport OberonDialog from '~components/OberonDialog';\nimport { TabPanel } from '~components/TabPanel';\nimport { TransferTarget } from '~services/AsyncManager/domain';\n\nimport AgentTransferPanel from './AgentTransferPanel';\nimport QueueTransferPanel from './QueueTransferPanel';\n\nenum TabType {\n Queue,\n Agent,\n}\n\ninterface TransferModalProps {\n open: boolean;\n onTransferTo: (transferTarget: TransferTarget, value: string) => void;\n onClose: () => void;\n}\n\nconst TransferModal = ({ open, onTransferTo, onClose }: TransferModalProps) => {\n const [transferTab, setTransferTab] = useState(TabType.Queue);\n\n return (\n \n setTransferTab(value)}>\n \n \n \n \n\n \n onTransferTo(TransferTarget.Queue, queue)} />\n \n\n \n onTransferTo(TransferTarget.Agent, agent)} />\n \n >\n }\n actionFooter={\n \n }\n />\n );\n};\n\nexport default TransferModal;\n","import ArticleIcon from '@mui/icons-material/Article';\nimport SwapHorizIcon from '@mui/icons-material/SwapHoriz';\nimport Box from '@mui/material/Box';\nimport Button from '@mui/material/Button';\nimport Divider from '@mui/material/Divider';\nimport IconButton from '@mui/material/IconButton';\nimport Tooltip from '@mui/material/Tooltip';\nimport React, { ChangeEvent, useEffect, useLayoutEffect, useRef, useState } from 'react';\n\nimport EmptyState from '~components/EmptyState';\nimport { AuthorRole, Customer, TransferTarget } from '~services/AsyncManager/domain';\nimport {\n getOrderedMessageKeysByMessageIdAndCreatedTimestamp,\n getOrderedNumberKeys,\n} from '~services/AsyncManager/helpers';\n\nimport Conversation from './Conversation';\nimport MessageTemplateModal from './MessageTemplateModal';\nimport MessageTextarea from './MessageTextarea';\nimport TransferModal from './TransferModal';\n\ninterface MessageContainerProps {\n orgReference: string;\n attributeConfiguration: { key: string; label: string }[];\n contactPopLinks: { [key: string]: string };\n customer: Customer | undefined;\n markAsRead: (messageIds: number[]) => void;\n sendMessage: (conversationId: number, message: string) => void;\n loadMoreFrom: (conversationId: number) => void;\n transferTo: (transferTarget: TransferTarget, conversationId: number, value: string) => void;\n markConversationAsDisposed: (\n conversationId: number,\n dispositionCode: string,\n dispositionSubCode: string,\n attributes: { [key: string]: string },\n ) => void;\n}\n\nconst getUnreadCustomerMessageIds = (customer: Customer): number[] => {\n let unreadMessageIds: number[] = [];\n\n // for each conversation\n for (const conversationId in customer.conversations) {\n const conversation = customer.conversations[conversationId];\n // for each message\n for (const messageId in conversation.messages) {\n // mark the message as read if it came from the customer\n const message = conversation.messages[messageId];\n if (message.readTimestamp === undefined && message.authorRole === AuthorRole.Customer) {\n unreadMessageIds = [...unreadMessageIds, message.id];\n }\n }\n }\n\n return unreadMessageIds;\n};\n\nconst getMessageCountAcrossConversations = (customer: Customer | undefined): number => {\n let count = 0;\n\n if (customer === undefined) return count;\n\n for (const conversationId in customer.conversations) {\n const conversation = customer.conversations[conversationId];\n for (const _ in conversation.messages) {\n count += 1;\n }\n }\n\n return count;\n};\n\nconst getCanDisposeForCurrentlyActiveConversations = (customer: Customer | undefined): boolean => {\n if (customer === undefined) return false;\n\n const orderedConversationKeys = getOrderedNumberKeys(customer.conversations);\n\n if (orderedConversationKeys.length === 0) return false;\n\n return customer.conversations[orderedConversationKeys[orderedConversationKeys.length - 1]].canDispose;\n};\n\nconst geQueueForCurrentlyActiveConversation = (customer: Customer | undefined): string => {\n if (customer === undefined) return '';\n\n const orderedConversationKeys = getOrderedNumberKeys(customer.conversations);\n\n if (orderedConversationKeys.length === 0) return '';\n\n return customer.conversations[orderedConversationKeys[orderedConversationKeys.length - 1]].queue;\n};\n\nconst getMessageCountForCurrentlyActiveConversations = (customer: Customer | undefined): number => {\n let count = 0;\n\n if (customer === undefined) return count;\n\n const orderedConversationKeys = getOrderedNumberKeys(customer.conversations);\n const currentConversation = customer.conversations[orderedConversationKeys[orderedConversationKeys.length - 1]];\n for (const _ in currentConversation.messages) {\n count += 1;\n }\n\n return count;\n};\n\nconst getConversationIdToLoadFrom = (customer: Customer): number | undefined => {\n const orderedConversationKeys = getOrderedNumberKeys(customer.conversations);\n const selectedOldestConversationId = orderedConversationKeys[0];\n\n // Make sure the orderedConversationKeys was not an empty array and that the index value was not undefined\n if (!selectedOldestConversationId) {\n return undefined;\n }\n\n const oldestConversation = customer.conversations[selectedOldestConversationId];\n\n // Make sure that a conversation actually exists for the value selected\n if (!oldestConversation) {\n return undefined;\n }\n\n return oldestConversation.previousConversationId ? oldestConversation.id : undefined;\n};\n\nconst getNewestUnreadCustomerMessageKey = (customer: Customer): string | undefined => {\n const orderedConversationKeys = getOrderedNumberKeys(customer.conversations);\n const newestConversationId = orderedConversationKeys[orderedConversationKeys.length - 1];\n const messages = customer.conversations[newestConversationId].messages;\n const orderedMessageKeys = getOrderedMessageKeysByMessageIdAndCreatedTimestamp(messages);\n const filteredKeys = orderedMessageKeys.filter(\n (key) => messages[key].authorRole === AuthorRole.Customer && messages[key].readTimestamp === undefined,\n );\n\n if (filteredKeys.length === 0) {\n return undefined;\n }\n\n return filteredKeys[filteredKeys.length - 1];\n};\n\nconst MessageContainer = ({\n orgReference,\n attributeConfiguration,\n contactPopLinks,\n customer,\n markAsRead,\n sendMessage,\n loadMoreFrom,\n transferTo,\n markConversationAsDisposed,\n}: MessageContainerProps) => {\n const contentContainerRef = useRef(null);\n const [newMessagePos, setNewMessagePos] = useState(undefined);\n const [messageTemplateModalOpen, setMessageTemplateModalOpen] = useState(false);\n const [transferModalOpen, setTransferModalOpen] = useState(false);\n const [messageContent, setMessageContent] = useState('');\n const orderedConversationKeys = customer !== undefined ? getOrderedNumberKeys(customer.conversations) : [];\n const loadConversationsFromId = customer ? getConversationIdToLoadFrom(customer) : undefined;\n const activeConversationQueue = geQueueForCurrentlyActiveConversation(customer);\n\n const loadMoreConversationsFrom = () => {\n if (loadConversationsFromId !== undefined) {\n loadMoreFrom(loadConversationsFromId);\n }\n };\n\n const sendConversationMessage = (messageContent: string) => {\n if (orderedConversationKeys.length > 0) {\n const conversationId = orderedConversationKeys[orderedConversationKeys.length - 1];\n sendMessage(conversationId, messageContent);\n setNewMessagePos(undefined);\n setMessageContent('');\n }\n };\n\n const transferConversation = (transferTarget: TransferTarget, value: string) => {\n if (orderedConversationKeys.length > 0) {\n const conversationId = orderedConversationKeys[orderedConversationKeys.length - 1];\n transferTo(transferTarget, conversationId, value);\n toggleTransferModal();\n }\n };\n\n const disposeConversation =\n (conversationId: number) =>\n (dispositionCode: string, dispositionSubCode: string, attributes: { [key: string]: string }) => {\n markConversationAsDisposed(conversationId, dispositionCode, dispositionSubCode, attributes);\n };\n\n const onMessageChange = (e: ChangeEvent) => {\n const value = e.target.value;\n setMessageContent(value);\n };\n\n const onSelectMessageTemplate = (message: string) => {\n setMessageContent(message);\n toggleMessageTemplateModal();\n };\n\n const toggleMessageTemplateModal = () => {\n setMessageTemplateModalOpen((prev) => !prev);\n };\n\n const toggleTransferModal = () => {\n setTransferModalOpen((prev) => !prev);\n };\n\n // Mark messages as read and set new message position when applicable\n const totalMessageCount = getMessageCountAcrossConversations(customer);\n useEffect(() => {\n if (customer !== undefined) {\n if (newMessagePos === undefined) {\n const newestUnreadMessageKey = getNewestUnreadCustomerMessageKey(customer);\n if (newestUnreadMessageKey) {\n setNewMessagePos(newestUnreadMessageKey);\n }\n }\n\n const unreadMessageIds = getUnreadCustomerMessageIds(customer);\n if (unreadMessageIds.length > 0) {\n markAsRead(unreadMessageIds);\n }\n\n return () => {\n setNewMessagePos(undefined);\n };\n }\n }, [totalMessageCount]);\n\n // Resets message content when we switch customers\n // TODO: When we support draft messages this logic should take applying the draft over resetting the field if available\n useEffect(() => {\n setMessageContent('');\n }, [customer]);\n\n // Scroll to bottom of the container if:\n // - A new message is presented\n // - Agent can now dispose of a conversation\n // - The new message element exists in the dom or not\n const currentConversationTotalMessageCount = getMessageCountForCurrentlyActiveConversations(customer);\n const currentConversationCanDispose = getCanDisposeForCurrentlyActiveConversations(customer);\n useLayoutEffect(() => {\n if (contentContainerRef.current) {\n contentContainerRef.current.scrollTo(0, contentContainerRef.current.scrollHeight);\n }\n }, [currentConversationTotalMessageCount, currentConversationCanDispose, newMessagePos]);\n\n const conversations =\n customer !== undefined\n ? orderedConversationKeys.map((key) => {\n const isLastItem = key === orderedConversationKeys[orderedConversationKeys.length - 1];\n let isExpanded = false;\n\n if (\n key === orderedConversationKeys[orderedConversationKeys.length - 1] ||\n (orderedConversationKeys.length > 1 && key === orderedConversationKeys[orderedConversationKeys.length - 2])\n ) {\n isExpanded = true;\n }\n\n return (\n \n );\n })\n : [];\n\n return (\n <>\n {customer === undefined && (\n \n \n
\n )}\n\n {customer !== undefined && (\n <>\n \n \n \n \n \n \n \n \n \n \n\n \n\n \n {loadConversationsFromId !== undefined && (\n \n )}\n {conversations}\n \n\n \n \n \n\n \n\n \n >\n )}\n >\n );\n};\n\nexport default MessageContainer;\n","import Backdrop from '@mui/material/Backdrop';\nimport Button from '@mui/material/Button';\nimport Paper from '@mui/material/Paper';\nimport Typography from '@mui/material/Typography';\nimport Box from '@mui/system/Box';\nimport React, { useMemo } from 'react';\n\nimport { useAppConfiguration } from '~providers/AppConfigurationProvider';\nimport { useAsync } from '~providers/AsyncProvider';\nimport { useConnect } from '~providers/ConnectProvider';\nimport { AsyncAgentState } from '~services/AsyncManager/domain';\n\nimport ConversationContainer from './ConversationContainer';\nimport MessageContainer from './MessageContainer';\n\n// If the status bar's height OR margin's change these properties will have to be updated\nconst statusBarHeight = 100;\nconst statusBarBottomMargin = 16;\n\nconst AsyncView = () => {\n const { web, orgReference } = useAppConfiguration();\n const { customers, selectedCustomerKey, setActiveCustomer, agent: asyncAgent, asyncManager } = useAsync();\n const { agent: connectAgent } = useConnect();\n const conversationPopLinks = useMemo(\n () =>\n web.contactPopLinks.reduce(\n (state, currentValue) =>\n currentValue.contactType === 'messaging' ? { ...state, [currentValue.label]: currentValue.value } : state,\n {} as { [key: string]: string },\n ),\n [web.contactPopLinks],\n );\n const isMissedConversation = asyncAgent.state === AsyncAgentState.MissedConversation;\n\n const goBackToAvailable = () => {\n asyncManager.setAgentState(AsyncAgentState.Available);\n connectAgent.setOnline();\n };\n\n return (\n \n \n \n \n \n Are you still here?\n \n\n \n You have missed a conversation. Click the button below to continue messaging.\n \n \n\n \n \n \n\n \n\n \n \n );\n};\n\nexport default AsyncView;\n","import PhoneForwardedIcon from '@mui/icons-material/PhoneForwarded';\nimport IconButton from '@mui/material/IconButton';\nimport ListItem from '@mui/material/ListItem';\nimport ListItemSecondaryAction from '@mui/material/ListItemSecondaryAction';\nimport ListItemText from '@mui/material/ListItemText';\nimport { styled } from '@mui/material/styles';\nimport React from 'react';\n\ninterface DirectoryCardProps {\n name: string;\n extension?: number;\n divider: boolean;\n onDial: () => Promise;\n}\n\nconst CustomizedListItem = styled(ListItem)(({ theme }) => ({\n 'padding': 16,\n '&:hover': {\n backgroundColor: theme.palette.grey[100],\n },\n}));\n\nconst CustomizedListItemText = styled(ListItemText)(({ theme }) => ({\n '& .MuiListItemText-primary': {\n display: 'block',\n fontSize: 16,\n paddingRight: 50,\n minWidth: 0,\n textOverflow: 'ellipsis',\n\n /* Required for text-overflow to do anything */\n whiteSpace: 'nowrap',\n overflow: 'hidden',\n },\n}));\n\n// TODO: display phone number and extension\nconst DirectoryCard = ({ name, extension, divider, onDial }: DirectoryCardProps) => {\n return (\n \n \n \n \n \n \n \n \n );\n};\n\nexport default DirectoryCard;\n","import Divider from '@mui/material/Divider';\nimport List from '@mui/material/List';\nimport TextField from '@mui/material/TextField';\nimport Typography from '@mui/material/Typography';\nimport React, { ChangeEvent, useState } from 'react';\n\nimport DirectoryCard from '~components/CallDirectory/DirectoryCard';\nimport SectionCard from '~components/SectionCard';\nimport { CallDirectoryItem } from '~providers/AppConfigurationProvider/api';\n\ninterface CallDirectoryProps {\n list: CallDirectoryItem[];\n onDial: (phoneNumber: string, extension?: number) => Promise;\n}\n\nconst CallDirectory = ({ list, onDial }: CallDirectoryProps) => {\n const [search, setSearch] = useState('');\n\n const onSearchChange = (e: ChangeEvent) => {\n setSearch(e.target.value);\n };\n\n const directoryListFiltered = list.filter((item) => {\n // If search is empty we want everything\n if (!search) return true;\n // If search is not empty we want to see if value exists in the list or not and filter\n if (item.name.toLowerCase().includes(search.toLowerCase())) return true;\n\n return false;\n });\n\n const directoryList = directoryListFiltered.map((item, index) => (\n onDial(item.phoneNumber, item.extension)}\n />\n ));\n\n return (\n \n \n\n \n\n \n {directoryList.length === 0 && (\n \n {search.length === 0 ? 'Directory is empty.' : 'No search results found.'}\n \n )}\n\n {directoryList.length > 0 && {directoryList}
}\n
\n \n );\n};\n\nexport default CallDirectory;\n","import MicOffIcon from '@mui/icons-material/MicOff';\nimport Button from '@mui/material/Button';\nimport Dialog from '@mui/material/Dialog';\nimport DialogActions from '@mui/material/DialogActions';\nimport DialogContent from '@mui/material/DialogContent';\nimport DialogTitle from '@mui/material/DialogTitle';\nimport React, { useEffect } from 'react';\n\nimport { ConnectVoiceContact } from '~providers/ConnectProvider/domain';\n\ninterface MuteModalProps {\n open: boolean;\n onClose: () => void;\n contact?: ConnectVoiceContact;\n}\n\nconst MuteModal = ({ open, onClose, contact }: MuteModalProps) => {\n // Mute modal should be closed automatically if customer drops off from a call\n useEffect(() => {\n if (open && contact !== undefined && contact.hasActiveContactConnection === false) {\n console.log('+ Lost contact connection, closing MuteModal.');\n onClose();\n }\n }, [contact?.hasActiveContactConnection]);\n\n return (\n \n );\n};\n\nexport default MuteModal;\n","import LoadingButton from '@mui/lab/LoadingButton';\nimport Autocomplete, { AutocompleteRenderInputParams } from '@mui/material/Autocomplete';\nimport Button from '@mui/material/Button';\nimport Grid from '@mui/material/Grid';\nimport TextField from '@mui/material/TextField';\nimport { DateTime } from 'luxon';\nimport React, { useEffect, useState } from 'react';\nimport { Controller, useForm } from 'react-hook-form';\n\nimport OberonDialog from '~components/OberonDialog';\nimport { getTimezoneList } from '~pages/Dialler/api';\nimport { useAppConfiguration } from '~providers/AppConfigurationProvider';\n\nexport interface PrepareLeadFormData {\n endpoint: string;\n leadName: string;\n externalId: string;\n timezone: string;\n}\n\n// We do this as we need the value of timezone to be able to be null by default for the material UI auto select component to\n// stop having a fit over \"\" does not exist in list as a selectable option\ninterface FormDefaults extends Omit {\n timezone: string | null;\n}\n\ninterface PrepareOutboundCallModalProps {\n open: boolean;\n manualOutboundRequireDetails: boolean;\n onAccept: (data: PrepareLeadFormData) => Promise;\n onClose: () => void;\n}\n\nconst PrepareOutboundCallModal = ({\n open,\n manualOutboundRequireDetails,\n onAccept,\n onClose,\n}: PrepareOutboundCallModalProps) => {\n const appConfig = useAppConfiguration();\n const {\n formState: { errors },\n handleSubmit,\n reset,\n control,\n setError,\n clearErrors,\n setValue,\n } = useForm({\n defaultValues: {\n endpoint: '',\n leadName: '',\n externalId: '',\n timezone: null,\n },\n mode: 'all',\n reValidateMode: 'onChange',\n shouldUnregister: true,\n });\n const [isSubmitting, setIsSubmitting] = useState(false);\n const [timezoneList, setTimezoneList] = useState([]);\n\n // Handles form cleanup on modal close\n useEffect(() => {\n // Reset form on close\n return function cleanupPrepareLeadModal() {\n reset();\n };\n }, [open]);\n\n // Fetch timezone list\n useEffect(() => {\n if (open && manualOutboundRequireDetails) {\n const fetchTimezoneList = async () => {\n clearErrors('timezone');\n\n let resp: string[];\n try {\n resp = await getTimezoneList(appConfig.web.timezonePrefixes);\n } catch (e) {\n setError('timezone', { type: 'manual', message: 'Unable to fetch timezone list' });\n return;\n }\n\n setTimezoneList(resp);\n setValue('timezone', appConfig.defaultTimezone);\n };\n\n fetchTimezoneList();\n\n return () => {\n clearErrors('timezone');\n setTimezoneList([]);\n };\n }\n }, [open, manualOutboundRequireDetails, appConfig.web.timezonePrefixes]);\n\n const onSubmit = handleSubmit(async (data: FormDefaults) => {\n setIsSubmitting(true);\n\n try {\n await onAccept({\n endpoint: data.endpoint,\n leadName: data.leadName || data.endpoint,\n externalId: data.externalId || data.endpoint,\n timezone: data.timezone || DateTime.local().zoneName,\n });\n } catch (e) {\n return;\n }\n\n setIsSubmitting(false);\n reset();\n onClose();\n });\n\n return (\n \n \n (\n \n )}\n />\n \n\n {manualOutboundRequireDetails && (\n <>\n \n (\n \n )}\n />\n \n\n \n (\n \n )}\n />\n \n\n \n (\n field.onChange(data)}\n fullWidth\n options={timezoneList}\n filterSelectedOptions\n disabled={isSubmitting}\n renderInput={(params: AutocompleteRenderInputParams) => (\n \n )}\n />\n )}\n />\n \n >\n )}\n \n }\n actionFooter={\n <>\n \n\n \n Dial Lead\n \n >\n }\n />\n );\n};\n\nexport default PrepareOutboundCallModal;\n","import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';\nimport LoadingButton from '@mui/lab/LoadingButton';\nimport blueGrey from '@mui/material/colors/blueGrey';\nimport Menu from '@mui/material/Menu';\nimport MenuItem from '@mui/material/MenuItem';\nimport { styled } from '@mui/material/styles';\nimport React, { MouseEvent, useState } from 'react';\n\ninterface LoadingButtonDropdownProps {\n title: string;\n items: string[];\n disabled?: boolean;\n loading?: boolean;\n fullWidth?: boolean;\n onChange: (stateName: string) => void;\n}\n\nconst LoadingButtonCustomised = styled(LoadingButton)(({ theme }) => ({\n 'backgroundColor': blueGrey[800],\n 'color': '#ffffff',\n ':hover': {\n backgroundColor: blueGrey[700],\n },\n ':disabled': {\n color: theme.palette.getContrastText(`${blueGrey[800]}80`),\n backgroundColor: `${blueGrey[800]}80`,\n },\n}));\n\nconst LoadingButtonDropdown = ({\n title,\n items = [],\n disabled,\n loading,\n fullWidth,\n onChange,\n}: LoadingButtonDropdownProps) => {\n const [anchorEl, setAnchorEl] = useState(null);\n const open = Boolean(anchorEl);\n\n const onOpen = (event: MouseEvent) => {\n setAnchorEl(event.currentTarget);\n };\n\n const onClose = () => {\n setAnchorEl(null);\n };\n\n const onItemClick = (value: string) => {\n onChange(value);\n setAnchorEl(null);\n };\n\n const records = items.map((value, index) => {\n return (\n \n );\n });\n\n return (\n <>\n \n {title}\n \n \n\n \n >\n );\n};\n\nexport default LoadingButtonDropdown;\n","import LoadingButton from '@mui/lab/LoadingButton';\nimport Autocomplete, { AutocompleteRenderInputParams } from '@mui/material/Autocomplete';\nimport Checkbox from '@mui/material/Checkbox';\nimport FormControlLabel from '@mui/material/FormControlLabel';\nimport Grid from '@mui/material/Grid';\nimport TextField from '@mui/material/TextField/TextField';\nimport Typography from '@mui/material/Typography';\nimport { DateTimePicker } from '@mui/x-date-pickers';\nimport { DateTime } from 'luxon';\nimport React, { ChangeEvent, useEffect, useState } from 'react';\n\nimport useForm, { UseForm, ValidatorType } from '~hooks/useForm';\nimport organisations from '~organisations';\nimport { DiallingHours, Disposition, OutcomeType, PublicHoliday } from '~pages/CampaignManagement/domain';\nimport { getTimezoneList } from '~pages/Dialler/api';\nimport { Callback } from '~pages/Dialler/domain';\nimport { useAppConfiguration } from '~providers/AppConfigurationProvider';\nimport { useConnect } from '~providers/ConnectProvider';\nimport { convertDictValuesToString } from '~utils/Functions';\n\nimport Selectbox from '../../../../../components/Form/Selectbox';\nimport LoadingButtonDropdown from '../LoadingButtonDropdown';\n\ntype ChannelType = 'voice' | 'chat';\n\ninterface OutcomeCaptureProps {\n orgReference: string;\n channelType: ChannelType;\n campaignId?: number;\n dispositions?: Disposition[];\n diallingHours?: DiallingHours;\n publicHolidays?: PublicHoliday[];\n exclusionLists?: string[];\n timezone?: string;\n agentUsername?: string;\n isUnknownLead?: boolean;\n contactName?: string;\n endpoint?: string;\n disableSubmit?: boolean;\n disableNewLead?: boolean;\n onSubmit: (data: Outcome, switchToStatus: string | undefined) => Promise;\n}\n\n// Note: lead's endpoint comes from our amazon connect contact object\ninterface NewLead {\n endpoint: string;\n name: string;\n externalId: string;\n timezone: string;\n}\n\nexport interface Outcome {\n dispositionCode: string;\n dispositionSubCode: string;\n hasSystemIssue: boolean;\n systemIssueDescription: string;\n exclusionList?: string;\n callback?: Callback;\n newLead?: NewLead;\n attributes: Record;\n}\n\nexport interface OutcomesFormProps {\n form: UseForm;\n formSubmitting: boolean;\n channelType: ChannelType;\n}\n\ninterface AutoCompleteList {\n label: string;\n value: string;\n}\n\n// Used for disabling dates within disableDates function\nconst dayToWeekdayNumber: { [key: string]: number } = {\n Monday: 1,\n Tuesday: 2,\n Wednesday: 3,\n Thursday: 4,\n Friday: 5,\n Saturday: 6,\n Sunday: 7,\n};\n\n// Used for showing error messages for selected date range within onValidation function\nconst weekdayNumberToDay: { [key: number]: string } = {\n 1: 'Monday',\n 2: 'Tuesday',\n 3: 'Wednesday',\n 4: 'Thursday',\n 5: 'Friday',\n 6: 'Saturday',\n 7: 'Sunday',\n};\n\n// TODO: update to use react-hook-form so its much cleaner logically and less buggy\nconst OutcomeCapture = ({\n orgReference,\n channelType,\n campaignId,\n dispositions,\n diallingHours,\n publicHolidays,\n exclusionLists,\n timezone,\n agentUsername,\n isUnknownLead,\n contactName,\n endpoint,\n disableSubmit,\n disableNewLead,\n onSubmit,\n}: OutcomeCaptureProps) => {\n const appConfig = useAppConfiguration();\n const { agentStateList } = useConnect();\n const [viewFlag, setViewFlag] = useState<\n OutcomeType.Callback | OutcomeType.Snoozed | OutcomeType.Excluded | undefined\n >(undefined);\n const [formSubmitting, setFormSubmitting] = useState(false);\n const [requireLeadCreation, setRequireLeadCreation] = useState(false);\n const [timezoneList, setTimezoneList] = useState([]);\n const [timezoneFetchError, setTimezoneFetchError] = useState('');\n const AdditionalFields =\n campaignId !== undefined ? organisations[orgReference]?.additionalOutcomeCaptureFields[campaignId] : undefined;\n const form = useForm({\n disposition: {\n value: null,\n validators: [\n {\n type: ValidatorType.Required,\n message: 'A disposition is Required',\n },\n ],\n },\n hasSystemIssue: {\n value: false,\n validators: [],\n },\n });\n\n // Fetch timezone list\n useEffect(() => {\n if (isUnknownLead) {\n const fetchTimezoneList = async () => {\n setTimezoneFetchError('');\n\n let resp: string[];\n try {\n resp = await getTimezoneList(appConfig.web.timezonePrefixes);\n } catch (e) {\n setTimezoneFetchError('Unable to fetch timezone list');\n return;\n }\n\n setTimezoneList(resp);\n form.handleUnconventionalInputChange('leadTimezone', appConfig.defaultTimezone);\n };\n\n fetchTimezoneList();\n\n return () => {\n setTimezoneFetchError('');\n setTimezoneList([]);\n };\n }\n }, [isUnknownLead, appConfig.web.timezonePrefixes]);\n\n // Dynamically add/ remove unknown lead collection detail fields\n useEffect(() => {\n if (isUnknownLead === true) {\n form.addSchemaProperties({\n leadEndpoint: {\n value: '',\n validators: [\n {\n type: ValidatorType.ValidIfSetWith,\n fieldName: 'leadName',\n message: 'Lead endpoint is required',\n },\n {\n type: ValidatorType.ValidIfSetWith,\n fieldName: 'leadExternalId',\n message: 'Lead endpoint is required',\n },\n ],\n },\n leadName: {\n value: '',\n validators: [\n {\n type: ValidatorType.ValidIfSetWith,\n fieldName: 'leadEndpoint',\n message: 'Lead name is required',\n },\n {\n type: ValidatorType.ValidIfSetWith,\n fieldName: 'leadExternalId',\n message: 'Lead name is required',\n },\n ],\n },\n leadExternalId: {\n value: '',\n validators: [\n {\n type: ValidatorType.ValidIfSetWith,\n fieldName: 'leadEndpoint',\n message: 'External ID is required',\n },\n {\n type: ValidatorType.ValidIfSetWith,\n fieldName: 'leadName',\n message: 'External ID is required',\n },\n ],\n },\n leadTimezone: {\n value: '',\n validators: [],\n },\n });\n return;\n }\n\n form.removeSchemaProperties(['leadEndpoint', 'leadName', 'leadExternalId', 'leadTimezone']);\n }, [isUnknownLead]);\n\n // Managing if creating a new lead for unknown inbound is optional OR required\n useEffect(() => {\n if (isUnknownLead) {\n if (\n form.fields.disposition.value !== null &&\n form.fields.disposition.value.outcome === OutcomeType.Callback &&\n requireLeadCreation == false\n ) {\n // Mark all new lead fields as required\n form.addValidationTypeToField('leadEndpoint', {\n type: ValidatorType.Required,\n message: 'Lead endpoint is required.',\n });\n form.addValidationTypeToField('leadName', { type: ValidatorType.Required, message: 'Lead name is required.' });\n form.addValidationTypeToField('leadExternalId', {\n type: ValidatorType.Required,\n message: 'Lead external ID is required.',\n });\n form.addValidationTypeToField('leadTimezone', {\n type: ValidatorType.Required,\n message: 'Lead timezone is required.',\n });\n setRequireLeadCreation(true);\n } else if (\n form.fields.disposition.value !== null &&\n form.fields.disposition.value.outcome !== OutcomeType.Callback &&\n requireLeadCreation == true\n ) {\n // Remove all new lead fields required validations IF not callback disposition\n form.setAsyncFields({\n leadEndpoint: '',\n });\n form.removeValidationTypeFromField('leadEndpoint', ValidatorType.Required);\n form.removeValidationTypeFromField('leadName', ValidatorType.Required);\n form.removeValidationTypeFromField('leadExternalId', ValidatorType.Required);\n form.removeValidationTypeFromField('leadTimezone', ValidatorType.Required);\n setRequireLeadCreation(false);\n } else if (\n form.fields.disposition.value !== null &&\n form.fields.disposition.value.outcome === OutcomeType.Callback\n ) {\n form.setAsyncFields({\n leadEndpoint: form.fields.callbackEndpoint.value,\n });\n }\n }\n }, [isUnknownLead, form.fields.disposition.value, form.fields?.callbackEndpoint?.value]);\n\n // Dynamically add/ remove system issue description field based off of the system issue checkbox\n useEffect(() => {\n if (form.fields.hasSystemIssue.value === true) {\n form.addSchemaProperties({\n systemIssueDescription: {\n value: '',\n validators: [\n {\n type: ValidatorType.Required,\n message: 'A system issue description is required',\n },\n ],\n },\n });\n return;\n }\n\n form.removeSchemaProperties(['systemIssueDescription']);\n }, [form.fields.hasSystemIssue.value]);\n\n // Dynamically add/ remove new fields to form schema dependent on campaign and lead status type\n useEffect(() => {\n if (channelType === 'voice') {\n if (form.fields.disposition.value) {\n // If dispositions object does not exist then we skip everything below this\n if (dispositions === undefined) {\n console.error('OutcomeCapture Disposition fields Effect: dispositions do not exist');\n return;\n }\n const dispositionCode = form.fields.disposition.value.code;\n const dispositionSubCode = form.fields.disposition.value.subCode;\n const disposition = dispositions.find(\n (disposition) => disposition.code === dispositionCode && disposition.subCode === dispositionSubCode,\n );\n\n if (disposition?.outcome === OutcomeType.Callback || disposition?.outcome === OutcomeType.Snoozed) {\n form.addSchemaProperties({\n // Based on checkbox we either set the current agents username or null - default null\n callbackAssignment: {\n value: false,\n validators: [],\n },\n forAgent: {\n value: null,\n validators: [],\n },\n scheduled: {\n value: null,\n validators: [\n {\n type: ValidatorType.Required,\n message: 'A callback date/ time is required',\n },\n // TODO: remove this validation from useForm\n // when @material-ui/pickers is fully released and disable the time ranges instead\n {\n type: ValidatorType.Custom,\n onValidation: (value) => {\n // Hard and fast fail if value becomes null. Required check above validates if required or\n // not so we dont need to do it in here\n if (value === null) {\n return true;\n }\n\n // Hard and fast fail if diallingHours isn't defined. Required check above validates if required or\n // not so we don't need to do it in here\n if (diallingHours === undefined) {\n return true;\n }\n\n const date = DateTime.fromISO(value as string);\n const diallingDay = diallingHours.diallingDays[weekdayNumberToDay[date.weekday]];\n\n if (diallingDay === null || diallingDay === undefined) {\n return false;\n }\n\n // If select time is between the hours blocks we allow anything and say its valid\n if (date.hour > diallingDay.startTimeHour && date.hour < diallingDay.endTimeHour) {\n return true;\n }\n\n // Start and end edge cases\n if (\n (date.hour === diallingDay.startTimeHour && date.minute >= diallingDay.startTimeMin) ||\n (date.hour === diallingDay.endTimeHour && date.minute < diallingDay.endTimeMin)\n ) {\n return true;\n }\n\n return false;\n },\n message: (value) => {\n if (value === null) {\n return '';\n }\n\n const date = DateTime.fromISO(value);\n\n if (diallingHours === undefined) {\n return '';\n }\n\n const diallingDay = diallingHours.diallingDays[weekdayNumberToDay[date.weekday]];\n\n if (diallingDay === null || diallingDay === undefined) {\n return 'Invalid Date';\n }\n\n const startHour =\n diallingDay.startTimeHour < 10 ? `0${diallingDay.startTimeHour}` : diallingDay.startTimeHour;\n const endHour =\n diallingDay.endTimeHour < 10 ? `0${diallingDay.endTimeHour}` : diallingDay.endTimeHour;\n const startMinute =\n diallingDay.startTimeMin < 10 ? `0${diallingDay.startTimeMin}` : diallingDay.startTimeMin;\n const endMinute =\n diallingDay.endTimeMin < 10 ? `0${diallingDay.endTimeMin}` : diallingDay.endTimeMin;\n const startTime = `${startHour}:${startMinute}`;\n const endTime = `${endHour}:${endMinute}`;\n\n return `Callback schedule time must be between ${startTime} and ${endTime}`;\n },\n },\n ],\n },\n callbackEndpoint: {\n value: endpoint !== 'anonymous' ? endpoint : '',\n validators: [\n {\n type: ValidatorType.Required,\n message: 'A callback endpoint is required',\n },\n ],\n },\n });\n if (disposition?.outcome === OutcomeType.Callback) {\n form.addSchemaProperties({\n // Optional field input\n notes: {\n value: '',\n validators: [],\n },\n });\n setViewFlag(OutcomeType.Callback);\n } else {\n setViewFlag(OutcomeType.Snoozed);\n }\n\n return;\n }\n\n if (disposition?.outcome === OutcomeType.Excluded) {\n form.addSchemaProperties({\n exclusionList: {\n value: '',\n validators: [\n {\n type: ValidatorType.Required,\n message: 'A exclusion list must be selected',\n },\n ],\n },\n });\n setViewFlag(OutcomeType.Excluded);\n return;\n }\n\n form.removeSchemaProperties([\n 'callbackAssignment',\n 'forAgent',\n 'scheduled',\n 'callbackEndpoint',\n 'notes',\n 'exclusionList',\n ]);\n setViewFlag(undefined);\n }\n }\n }, [form.fields.disposition.value]);\n\n // Dynamic assignment of callback agent. Can either be the current agent or null (assigned to another agent via leads engine)\n useEffect(() => {\n // If for any reason the passed in username is undefined, we want to default to the null case\n const username = agentUsername || null;\n\n form.setAsyncFields({\n forAgent: form.fields?.callbackAssignment?.value ? username : null,\n });\n }, [form.fields?.callbackAssignment?.value]);\n\n // Hack of applying generic type on the DatePicker components as then it\n // inforces correct typing for this onChange function so it's typing is not messed up. This breaks visible\n // typing however even though it passed type checking. i.e. defers TDate generic from input value which is wrong\n // as minDate then also overrides this with its DateTime generic\n const handleDateTimeChange = (fieldName: string) => (date: DateTime | null) => {\n form.handleUnconventionalInputChange(fieldName, date);\n };\n\n const handleAutoCompleteInputChange = (fieldName: string) => (e: ChangeEvent<{}>, selectedObj: AutoCompleteList) => {\n form.handleUnconventionalInputChange(fieldName, selectedObj);\n };\n\n const disableDates = (date: DateTime | null) => {\n // Disable public holidays if not allowed\n if (!diallingHours?.allowPublicHolidays) {\n if (publicHolidays === undefined) return false;\n\n for (const ph of publicHolidays) {\n if (date && ph.year === date.year && ph.month === date.month && ph.day === date.day) {\n return true;\n }\n }\n }\n\n // Disable specific day of the week if no dialling day hours exist\n for (const day in diallingHours?.diallingDays) {\n if (diallingHours?.diallingDays[day] === null && dayToWeekdayNumber[day] === date?.weekday) {\n return true;\n }\n }\n\n return false;\n };\n\n const handleOutcomeSubmit = (switchToStatus?: string) =>\n form.handleSubmit(async (formData: Record) => {\n // Remove all expected fields leaving the other object to be custom attributes\n const {\n disposition,\n exclusionList,\n callbackAssignment,\n forAgent,\n scheduled,\n callbackEndpoint,\n notes,\n hasSystemIssue,\n systemIssueDescription,\n leadEndpoint,\n leadName,\n leadExternalId,\n leadTimezone,\n ...other\n } = formData;\n\n const attributes = convertDictValuesToString(other);\n const createNewLead = leadEndpoint != '' && leadName != '' && leadExternalId != '' && leadTimezone != '';\n\n const data: Outcome = {\n dispositionCode: disposition.code,\n dispositionSubCode: disposition.subCode,\n hasSystemIssue: hasSystemIssue,\n systemIssueDescription: systemIssueDescription ?? '',\n exclusionList: viewFlag === OutcomeType.Excluded ? exclusionList : undefined,\n callback:\n viewFlag === OutcomeType.Callback || viewFlag === OutcomeType.Snoozed\n ? {\n forAgent: forAgent,\n // Appending timezone info\n // Note: If this is an unknown inbound call, the timezone field will be undefined\n // so we use the timezone of the new lead we create with this callback outcome\n scheduled: DateTime.fromISO(scheduled, { zone: timezone || leadTimezone }).toISO(),\n endpoint: callbackEndpoint,\n notes: notes,\n }\n : undefined,\n newLead:\n isUnknownLead && createNewLead\n ? {\n endpoint: leadEndpoint,\n name: leadName,\n externalId: leadExternalId,\n timezone: leadTimezone,\n }\n : undefined,\n attributes: attributes,\n };\n\n try {\n setFormSubmitting(true);\n await onSubmit(data, switchToStatus);\n } catch (e) {\n return;\n } finally {\n setFormSubmitting(false);\n }\n\n setViewFlag(undefined);\n form.resetForm();\n });\n\n let scheduledHelpTextDefault = timezone ? `(local time in ${timezone})` : '(local time in new leads timezone)';\n if (timezone && contactName) {\n scheduledHelpTextDefault = `(${contactName}'s time in ${timezone})`;\n }\n const scheduledHelperText = form.errors.scheduled ? form.errors.scheduled[0]! : scheduledHelpTextDefault;\n const exclusionListItems = exclusionLists\n ? exclusionLists.map((value: string) => ({ label: value, value: value }))\n : [];\n\n return (\n \n );\n};\n\nexport default OutcomeCapture;\n","import { findIndexByProperty } from '~utils/Functions';\n\ntype JanusCallbackFn = (msg: JanusMessage) => void;\n\ninterface JanusMessageBody {\n request: string;\n name?: string;\n videocodec?: string;\n}\n\n/*\n{\n \"janus\": \"event\",\n \"session_id\": 6545736739550511,\n \"transaction\": \"k1sAuhTqsNhk\",\n \"sender\": 6629843288911152,\n \"plugindata\": {\n \"plugin\": \"janus.plugin.recordplay\",\n \"data\": {\n \"recordplay\": \"event\",\n \"result\": {\n \"status\": \"recording\",\n \"id\": 1006304919552918\n }\n }\n },\n \"jsep\": {\n \"type\": \"answer\",\n \"sdp\": \"v=0\\r\\no=- 1615616628213029 1 IN IP4 172.17.0.2\\r\\ns=Recording 1006304919552918\\r\\nt=0 0\\r\\na=group:BUNDLE 0\\r\\na=msid-semantic: WMS janus\\r\\nm=video 9 UDP/TLS/RTP/SAVPF 98 99\\r\\nc=IN IP4 172.17.0.2\\r\\na=recvonly\\r\\na=mid:0\\r\\na=rtcp-mux\\r\\na=ice-ufrag:DUzZ\\r\\na=ice-pwd:CrWCi8LG+sJfRJmeymRySQ\\r\\na=ice-options:trickle\\r\\na=fingerprint:sha-256 79:46:1C:DD:15:68:A7:75:CC:4D:78:78:0B:E6:20:B7:A1:E4:BE:9D:11:D9:78:07:56:42:77:D7:0D:BB:9C:7F\\r\\na=setup:active\\r\\na=rtpmap:98 VP9/90000\\r\\na=rtcp-fb:98 ccm fir\\r\\na=rtcp-fb:98 nack\\r\\na=rtcp-fb:98 nack pli\\r\\na=rtcp-fb:98 goog-remb\\r\\na=rtcp-fb:98 transport-cc\\r\\na=extmap:3 urn:3gpp:video-orientation\\r\\na=extmap:4 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\\r\\na=extmap:9 urn:ietf:params:rtp-hdrext:sdes:mid\\r\\na=extmap:10 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id\\r\\na=extmap:11 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id\\r\\na=fmtp:98 profile-id=0\\r\\na=rtpmap:99 rtx/90000\\r\\na=fmtp:99 apt=98\\r\\na=msid:janus janusv0\\r\\na=ssrc:1853536236 cname:janus\\r\\na=ssrc:1853536236 msid:janus janusv0\\r\\na=ssrc:1853536236 mslabel:janus\\r\\na=ssrc:1853536236 label:janusv0\\r\\na=ssrc:2795312626 cname:janus\\r\\na=ssrc:2795312626 msid:janus janusv0\\r\\na=ssrc:2795312626 mslabel:janus\\r\\na=ssrc:2795312626 label:janusv0\\r\\na=candidate:1 1 udp 2015363327 172.17.0.2 45121 typ host\\r\\na=end-of-candidates\\r\\n\"\n }\n}\n */\ninterface JanusMessageData {\n id: number;\n}\n\ninterface JanusMessagePluginDataResult {\n status: string;\n id: number;\n}\n\ninterface JanusMessagePluginDataPayload {\n result: JanusMessagePluginDataResult;\n}\n\ninterface JanusMessagePluginData {\n plugin: string;\n data: JanusMessagePluginDataPayload;\n}\n\ninterface JanusMessage {\n janus: string;\n plugin?: string;\n body?: JanusMessageBody;\n handle_id?: number;\n candidate?: RTCIceCandidate;\n jsep?: RTCSessionDescriptionInit;\n transaction?: string;\n session_id?: number;\n data?: JanusMessageData;\n plugindata?: JanusMessagePluginData;\n // Not sure on structure\n error?: any;\n}\n\ninterface ScreenInfo {\n // ID reference to associated connection (sid)\n sessionId: number;\n // ID of the screen shared media (second property in the media stream track label after split on ':')\n screenStreamId: string;\n // ID of the janus record play plugin associated with this screen share\n handleId: number;\n // The Track associated to the rtc stream\n track: MediaStreamTrack;\n timestamp: Date;\n // `${username}:${sessionId}:${screenStreamId}`\n // e.g. csesta:2605170409046623:459085175\n recordingName: string;\n // the recording id, which is, vitally, the filename we are streaming into\n recordingId: number;\n}\n\ninterface ScreenShareManagerCallbacksOptional {\n onShareSuccess?: (screenInfoItem: ScreenInfo) => void;\n onShareFailure?: (errorMessage: string) => void;\n onStopShareSuccess?: (sessionId: number) => void;\n onDisplayMediaRejection?: (errorMessage: string) => void;\n onSelectedScreenError?: (errorMessage: string) => void;\n onConnectionError?: (errorMessage: string) => void;\n onConnectionLost?: (errorMessage: string) => void;\n}\n\ninterface ScreenShareManagerCallbacks {\n onShareSuccess: (screenInfoItem: ScreenInfo) => void;\n onShareFailure: (errorMessage: string) => void;\n onStopShareSuccess: (sessionId: number) => void;\n onDisplayMediaRejection: (errorMessage: string) => void;\n onSelectedScreenError: (errorMessage: string) => void;\n onConnectionError: (errorMessage: string) => void;\n onConnectionLost: (errorMessage: string) => void;\n}\n\nconst randomString = (len: number): string => {\n const charSet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';\n let randomString = '';\n\n for (let i = 0; i < len; i++) {\n const randomPoz = Math.floor(Math.random() * charSet.length);\n randomString += charSet.substring(randomPoz, randomPoz + 1);\n }\n\n return randomString;\n};\n\nconst ScreenInfoModel = (\n sessionId: number,\n screenStreamId: string,\n handleId: number,\n track: MediaStreamTrack,\n recordingName: string,\n recordingId: number,\n): ScreenInfo => {\n return {\n sessionId: sessionId,\n screenStreamId: screenStreamId,\n handleId: handleId,\n track: track,\n recordingName: recordingName,\n recordingId: recordingId,\n timestamp: new Date(),\n };\n};\n\nclass JanusConnection {\n private ws: WebSocket | undefined = undefined;\n private transactions: { [key: string]: JanusCallbackFn } = {};\n private listeners: JanusCallbackFn[] = [];\n private keepAliveIntervalHandle: number | undefined = undefined;\n\n public sid: number = 0;\n\n constructor(address: string) {\n const ws = new WebSocket(address, 'janus-protocol');\n\n ws.onmessage = (e) => {\n const msg = JSON.parse(e.data);\n const tid = msg.transaction;\n\n if (msg.janus === 'ack') {\n // ack from server\n } else if (tid in this.transactions) {\n // response to a transaction\n this.transactions[tid](msg);\n } else if (msg.janus === 'event') {\n // other event\n for (const k in this.listeners) {\n this.listeners[k](msg);\n }\n } else if (msg.janus === 'webrtcup') {\n /**\n * {\n * janus: \"webrtcup\"\n * sender: 1048425022738133\n * session_id: 440874700038304\n * }\n */\n // TODO: event handler\n } else if (msg.janus === 'media') {\n /**\n * {\n * janus: \"media\"\n * receiving: true\n * sender: 1048425022738133\n * session_id: 440874700038304\n * type: \"video\"\n * }\n */\n // TODO: catch reciving true/ false\n }\n };\n\n ws.onclose = (e: CloseEvent) => {\n window.clearInterval(this.keepAliveIntervalHandle);\n this.ws = undefined;\n this.onClose(e);\n };\n\n ws.onerror = (e: Event) => {\n this.onError(e);\n };\n\n ws.onopen = async (e) => {\n let msg: JanusMessage;\n\n try {\n msg = await this.send({\n janus: 'create',\n });\n } catch (e) {\n console.error('Unable to create janus session. ', e);\n return;\n }\n\n if (msg.data !== undefined) {\n const sid = msg.data.id;\n this.sid = sid;\n\n this.keepAliveIntervalHandle = window.setInterval(() => {\n this.send({\n janus: 'keepalive',\n session_id: sid,\n });\n }, 30 * 1000);\n\n this.onOpen(e);\n }\n };\n\n this.ws = ws;\n }\n\n public endSession() {\n if (this.ws !== undefined) {\n this.ws.close(1000);\n }\n }\n\n public onOpen(e: Event) {\n // To be overriden\n }\n\n public onClose(e: CloseEvent) {\n // To be overriden\n }\n\n public onError(e: Event) {\n // To be overriden\n }\n\n public addListener(callback: JanusCallbackFn) {\n this.listeners.push(callback);\n }\n\n // send send the server a message and awaits a response\n public send(msg: JanusMessage): Promise {\n return new Promise((resolve, reject) => {\n if (this.ws) {\n const tid = randomString(12);\n msg.transaction = tid;\n\n if (this.sid > 0) {\n msg.session_id = this.sid;\n }\n\n this.transactions[tid] = (msg) => {\n if ('error' in msg) {\n reject(msg);\n } else {\n resolve(msg);\n }\n\n delete this.transactions[tid];\n };\n\n this.ws.send(JSON.stringify(msg));\n } else {\n reject();\n }\n });\n }\n}\n\n// 'ws://localhost:8188/'\nexport class ScreenShareManager {\n private address: string;\n private username: string;\n // sid as key\n private connections: { [key: number]: JanusConnection } = {};\n private sharedScreenInfoList: ScreenInfo[] = [];\n // Empty defaults so we dont have to perform undefined checks\n private callbacks: ScreenShareManagerCallbacks = {\n onShareSuccess: (screenInfoItem: ScreenInfo) => {},\n onShareFailure: (errorMessage: string) => {},\n onStopShareSuccess: (sessionId: number) => {},\n onDisplayMediaRejection: (errorMessage: string) => {},\n onSelectedScreenError: (errorMessage: string) => {},\n onConnectionError: (errorMessage: string) => {},\n onConnectionLost: (errorMessage: string) => {},\n };\n\n constructor(address: string, username: string, callbacks: ScreenShareManagerCallbacksOptional) {\n this.address = address;\n this.username = username;\n\n // Assign optional functions else default to empty default values\n this.callbacks.onShareSuccess = callbacks.onShareSuccess ?? this.callbacks.onShareSuccess;\n this.callbacks.onShareFailure = callbacks.onShareFailure ?? this.callbacks.onShareFailure;\n this.callbacks.onStopShareSuccess = callbacks.onStopShareSuccess ?? this.callbacks.onStopShareSuccess;\n this.callbacks.onDisplayMediaRejection =\n callbacks.onDisplayMediaRejection ?? this.callbacks.onDisplayMediaRejection;\n this.callbacks.onSelectedScreenError = callbacks.onSelectedScreenError ?? this.callbacks.onSelectedScreenError;\n this.callbacks.onConnectionError = callbacks.onConnectionError ?? this.callbacks.onConnectionError;\n this.callbacks.onConnectionLost = callbacks.onConnectionLost ?? this.callbacks.onConnectionLost;\n\n console.log('ScreenShare Manager Started');\n }\n\n public addScreen() {\n const janus = new JanusConnection(this.address);\n let existingConnection = false;\n let handleId: number | undefined = undefined;\n\n janus.onOpen = async (e: Event) => {\n let msg;\n const sessionId = janus.sid;\n\n // Once connection is setup add the janus object to the connections list\n this.connections = {\n ...this.connections,\n [sessionId]: janus,\n };\n\n try {\n msg = await janus.send({\n janus: 'attach',\n plugin: 'janus.plugin.recordplay',\n });\n } catch (e) {\n console.error(\"Unable to attach plugin 'janus.plugin.recordplay'. \", e);\n this.callbacks.onShareFailure('Failed to start screenshare');\n return;\n }\n\n if (msg.data !== undefined) {\n handleId = msg.data.id;\n let ms;\n\n try {\n ms = await navigator.mediaDevices.getDisplayMedia();\n } catch (e) {\n console.error('Unable to get display media stream. ', e);\n this.callbacks.onDisplayMediaRejection('You must share your screens to continue.');\n return;\n }\n\n // Assumes we should always have a track to pop from the list as it ignores the potentially undefined error\n // check via !\n const track = ms.getVideoTracks().pop()!;\n const labelSplit = track.label.split(':');\n const typeOfShare = labelSplit[0];\n const screenStreamId = labelSplit[1];\n\n if (typeOfShare !== 'screen') {\n console.error('Can only share type of screen only');\n // Stops the media stream track\n track.stop();\n this.callbacks.onSelectedScreenError(\"Can only share items from the 'Your entire screen' section.\");\n return;\n }\n\n const streamScreenIndex = findIndexByProperty(this.sharedScreenInfoList, 'screenStreamId', screenStreamId);\n\n if (streamScreenIndex !== -1) {\n console.error('Screen already shared');\n // Stops the media stream track\n track.stop();\n this.callbacks.onSelectedScreenError('You are already sharing the selected screen.');\n return;\n }\n\n const conn = new RTCPeerConnection();\n\n track.addEventListener('ended', () => {\n try {\n janus.send({\n janus: 'message',\n plugin: 'janus.plugin.recordplay',\n body: {\n request: 'stop',\n },\n handle_id: handleId,\n });\n } catch (e) {\n console.error('Unable to stop screen recording. ', e);\n }\n\n // End janus websocket connection and local globals cleanup\n janus.endSession();\n delete this.connections[sessionId];\n // Removes specified screenshareItem from the list\n this.sharedScreenInfoList = this.sharedScreenInfoList.filter((item) => item.sessionId !== sessionId);\n\n console.log('screen sharing stopped');\n this.callbacks.onStopShareSuccess(sessionId);\n });\n\n conn.addEventListener('icecandidate', (e) => {\n console.log('got ice', e);\n if (e.candidate) {\n janus.send({\n janus: 'trickle',\n candidate: e.candidate,\n handle_id: handleId,\n });\n }\n });\n\n conn.addEventListener('negotiationneeded', async (e) => {\n let offer: RTCSessionDescriptionInit;\n\n try {\n offer = await conn.createOffer();\n } catch (e) {\n console.error('Unable to get connection create offer ', e);\n this.callbacks.onShareFailure('Failed to start screenshare');\n return;\n }\n\n try {\n await conn.setLocalDescription(offer);\n } catch (e) {\n console.error('Unable to set connection local description. ', e);\n this.callbacks.onShareFailure('Failed to start screen share');\n return;\n }\n\n let recordMessage;\n // when used in janus: e.g. csesta:2605170409046623:459085175\n const recordingName = `${this.username}:${sessionId}:${screenStreamId}`;\n\n try {\n // let's see if we can't record something\n recordMessage = await janus.send({\n janus: 'message',\n plugin: 'janus.plugin.recordplay',\n body: {\n request: 'record',\n name: recordingName,\n videocodec: 'vp9',\n },\n handle_id: handleId,\n jsep: conn.localDescription as RTCSessionDescriptionInit,\n });\n } catch (e) {\n console.error('Unable to initiate screen recording. ', e);\n return;\n }\n\n const recordingId = recordMessage.plugindata?.data.result.id || 0;\n const screenInfoItem = ScreenInfoModel(\n sessionId,\n screenStreamId,\n handleId || 0,\n track,\n `${recordingId}:${recordingName}`,\n recordingId,\n );\n\n this.sharedScreenInfoList.push(screenInfoItem);\n this.callbacks.onShareSuccess(screenInfoItem);\n\n try {\n await conn.setRemoteDescription(recordMessage.jsep as RTCSessionDescriptionInit);\n } catch (e) {\n console.log('Unable to set connection remote description. ', e);\n return;\n }\n });\n\n conn.addTrack(track);\n existingConnection = true;\n } else {\n console.error('Unexpected message response from janus. Missing data object');\n this.callbacks.onShareFailure('Failed to start screenshare');\n return;\n }\n };\n\n janus.onError = (e: Event) => {\n // We only want this callback triggered if there was no existing connections\n if (!existingConnection) {\n console.error('Unable to communicate with recording server. ', e);\n this.callbacks.onConnectionError('Unable to communicate with recording server.');\n }\n };\n\n janus.onClose = (e: CloseEvent) => {\n // TODO: Fix message spam that occurs when connection with server is lots and multiple connections drop at once\n // Might be fixed by having ONE connection that handles multiple recordings IF thats even possible?\n if (e.code !== 1000) {\n if (existingConnection) {\n // Reset local globals\n this.connections = {};\n this.sharedScreenInfoList = [];\n this.callbacks.onConnectionLost('Connection with remote server lost. Screenshare has ended');\n return;\n }\n }\n };\n }\n\n public async removeScreen(sessionId: number) {\n if (sessionId === undefined || sessionId === null) {\n console.error('Unable to end screenshare as screenshareList session ID is undefined or null.');\n return;\n }\n\n const shareItemIndex = findIndexByProperty(this.sharedScreenInfoList, 'sessionId', sessionId);\n const sharedScreenInfo = this.sharedScreenInfoList[shareItemIndex];\n\n if (sharedScreenInfo === undefined) {\n console.error('Unable to end screenshare as requested stream does not exist.');\n return;\n }\n\n const janusConnection = this.connections[sharedScreenInfo.sessionId];\n\n if (janusConnection === undefined) {\n console.error('Unable to end screenshare as janus connection does not exist for this screenshareList item.');\n return;\n }\n\n try {\n await janusConnection.send({\n janus: 'message',\n plugin: 'janus.plugin.recordplay',\n body: {\n request: 'stop',\n },\n handle_id: sharedScreenInfo.handleId,\n });\n } catch (e) {\n console.error('Unable to stop screen recording. ', e);\n return;\n }\n\n // Stops the media stream track\n sharedScreenInfo.track.stop();\n\n // End janus websocket connection and local globals cleanup\n janusConnection.endSession();\n delete this.connections[sessionId];\n // Removes specified screenshareItem from the list\n this.sharedScreenInfoList = this.sharedScreenInfoList.filter((item) => item.sessionId !== sessionId);\n\n console.log('screen sharing stopped');\n this.callbacks.onStopShareSuccess(sessionId);\n }\n\n // Cleansup all active connections\n public destroy() {\n for (let i = 0; i < this.sharedScreenInfoList.length; i++) {\n this.removeScreen(this.sharedScreenInfoList[i].sessionId);\n }\n }\n}\n","import React, { useContext } from 'react';\nimport { ReactNode, createContext, useEffect, useState } from 'react';\n\nimport EmptyState from '~components/EmptyState';\nimport { useAppConfiguration } from '~providers/AppConfigurationProvider';\nimport { useAuth } from '~providers/AuthProvider';\nimport { ScreenShareManager } from '~services/ScreenShareManager';\nimport { pluraliseWord } from '~utils/Functions';\n\nimport { postScreenShare } from './api';\n\ninterface ScreenShareProviderProps {\n children: ReactNode;\n}\n\ninterface ScreenShareContextProps {\n serviceEnabled: boolean;\n recordingNames: string[];\n reconnect: () => void;\n connectionLost: boolean;\n}\n\nconst ScreenShareContext = createContext(undefined);\n\nexport const useScreenShare = (): ScreenShareContextProps => {\n return useContext(ScreenShareContext) as ScreenShareContextProps;\n};\n\n// TODO: to come from backend, possibly stored in user table via AD\n// This will be the amount of screens that need to be shared by the agent user\n// before they get access to the dialler\nconst screenCount = 1;\n\nconst ScreenShareProvider = ({ children }: ScreenShareProviderProps) => {\n const { extensions } = useAppConfiguration();\n const { username } = useAuth();\n const [manager, setManager] = useState(undefined);\n const [screensShared, setScreensShared] = useState(0);\n const [recordingNames, setRecordingNames] = useState([]);\n const [connectionLost, setConnectionLost] = useState(false);\n const extensionEnabled = Boolean(extensions.screenRecordings);\n const context: ScreenShareContextProps = {\n serviceEnabled: extensionEnabled,\n recordingNames,\n // Triggers a rerun of both the manager setup and add screen use effects by resetting view based\n // state properties\n reconnect: () => {\n setManager(undefined);\n setConnectionLost(false);\n },\n connectionLost,\n };\n\n // If the extension is not enabled we want to skip fast\n if (!extensionEnabled) {\n return {children};\n }\n\n // used to cleanup the manager connection on user navigate away from page\n useEffect(() => {\n return () => {\n if (manager !== undefined) {\n console.log('ScreenShareProvider: ScreenShareManager Class Cleanup');\n // Cleanup everything\n manager.destroy();\n }\n };\n }, [manager]);\n\n useEffect(() => {\n if (manager !== undefined) {\n if (!connectionLost && screensShared < screenCount) {\n manager.addScreen();\n return;\n }\n }\n }, [manager, screensShared]);\n\n const shareScreenAction = () => {\n if (manager === undefined && extensions.screenRecordings) {\n const mgr = new ScreenShareManager(extensions.screenRecordings.janusUrl, username, {\n onShareSuccess: async (screenInfoItem) => {\n setRecordingNames((prev) => [...prev, screenInfoItem.recordingName]);\n setScreensShared((prev) => prev + 1);\n\n try {\n await postScreenShare(\n screenInfoItem.sessionId.toString(),\n screenInfoItem.screenStreamId,\n screenInfoItem.recordingId.toString(),\n );\n } catch (e) {\n console.error('! Unable to post agent screen share details due to error: ', e);\n return;\n }\n\n console.log('+ successfully posted screen share details');\n },\n onSelectedScreenError: (errorMessage) => {\n alert(errorMessage);\n mgr.addScreen();\n },\n onDisplayMediaRejection: (errorMessage) => {\n mgr.addScreen();\n },\n onConnectionError: (errorMessage) => {\n alert(errorMessage);\n },\n onConnectionLost: (errorMessage) => {\n alert(errorMessage);\n setConnectionLost(true);\n setScreensShared(0);\n setRecordingNames([]);\n },\n });\n\n setManager(mgr);\n }\n };\n\n const pluralisedScreenText = pluraliseWord(screenCount, 'screen');\n // If any error occurs do not redraw this empty state until the reconnect function has been triggered\n if (!connectionLost && screensShared < screenCount) {\n return (\n \n );\n }\n\n return {children};\n};\n\nexport default ScreenShareProvider;\n","import axios from 'axios';\n\nimport { APIError } from '~services/Errors';\n\nexport const postScreenShare = async (sessionId: string, screenId: string, recordingId: string): Promise => {\n const path = '/api/screen-recordings';\n\n const body = {\n session_id: sessionId,\n screen_id: screenId,\n recording_id: recordingId,\n };\n\n try {\n await axios.request({\n method: 'POST',\n url: path,\n data: body,\n headers: {\n Accept: 'application/json',\n },\n });\n } catch (e) {\n if (axios.isAxiosError(e)) {\n // Response should always be defined if axios error\n throw new APIError(e.response!.status, e.message);\n }\n\n throw new APIError(-1, e as string);\n }\n};\n","import Button, { ButtonProps } from '@mui/material/Button';\nimport { styled } from '@mui/material/styles';\nimport { MUIStyledCommonProps, Theme } from '@mui/system';\n\nimport { MuiColorRange } from '~theme';\n\ntype ActionButtonProps = MUIStyledCommonProps & ButtonProps & { colorRange: MuiColorRange };\n\nconst ActionButton = styled(Button, {\n shouldForwardProp: (prop) => prop !== 'colorRange',\n})(({ theme, colorRange }) => ({\n 'display': 'block',\n 'margin': `${theme.spacing(1)} 0`,\n 'color': theme.palette.getContrastText(colorRange[800]),\n 'backgroundColor': colorRange[800],\n ':hover': {\n color: theme.palette.getContrastText(colorRange[700]),\n backgroundColor: colorRange[700],\n },\n ':disabled': {\n color: theme.palette.getContrastText(`${colorRange[800]}80`),\n backgroundColor: `${colorRange[800]}80`,\n },\n ':last-of-type': {\n marginBottom: 0,\n },\n}));\n\nexport default ActionButton;\n","import * as j from '@mojotech/json-type-validation';\n\nexport const NormaliseEndpointResponseDecoder: j.Decoder = j\n .object({\n phone_number: j.string(),\n })\n .map((item) => item.phone_number);\n","import axios, { AxiosResponse } from 'axios';\n\nimport { APIError, UnsupportedStructureError } from '~services/Errors';\n\nimport { NormaliseEndpointResponseDecoder } from './domain';\n\nexport const normalisePhoneNumber = async (phoneNumber: string): Promise => {\n const path = `/api/normalise-phone-number/${phoneNumber}`;\n let resp: AxiosResponse;\n\n try {\n resp = await axios({\n method: 'GET',\n url: path,\n headers: {\n 'Content-Type': 'application/json',\n },\n });\n } catch (e) {\n if (axios.isAxiosError(e)) {\n // Response should always be defined if axios error\n throw new APIError(e.response!.status, (e.response!.data as { error: string }).error || e.message);\n }\n\n throw new APIError(-1, e as string);\n }\n\n const decoded = NormaliseEndpointResponseDecoder.run(resp.data);\n\n if (decoded.ok === false) {\n const err = new UnsupportedStructureError(decoded.error.message);\n\n console.error(decoded.error);\n console.error(err);\n throw err;\n }\n\n return decoded.result;\n};\n","import PhoneForwardedIcon from '@mui/icons-material/PhoneForwarded';\nimport LoadingButton from '@mui/lab/LoadingButton';\nimport InputAdornment from '@mui/material/InputAdornment';\nimport TextField from '@mui/material/TextField';\nimport React, { CSSProperties, ChangeEvent, KeyboardEvent, useState } from 'react';\n\nimport { APIError } from '~services/Errors';\n\nimport { normalisePhoneNumber } from './api';\n\ninterface DialPhoneNumberInputProps {\n style?: CSSProperties;\n label: string;\n id: string;\n name: string;\n disabled?: boolean;\n onClick: (phoneNumber: string) => Promise;\n}\n\nconst DialPhoneNumberInput = ({ style, label, id, name, disabled, onClick }: DialPhoneNumberInputProps) => {\n const [phoneNumber, setPhoneNumber] = useState('');\n const [error, setError] = useState('');\n const [loading, setLoading] = useState(false);\n const disableInput = disabled || loading;\n const disableAction = phoneNumber === '' || disabled || loading;\n\n const onClickFn = async () => {\n setLoading(true);\n setError('');\n\n try {\n let ep;\n\n try {\n ep = await normalisePhoneNumber(phoneNumber);\n } catch (e) {\n setError((e as Error | APIError).message);\n return;\n }\n\n // If number is successfully formatted we pass down the formatted number to custom click action and perform it\n try {\n await onClick(ep);\n } catch (e) {\n setError((e as Error).message);\n return;\n }\n\n // Reset phone number to empty on success\n setPhoneNumber('');\n } finally {\n setLoading(false);\n }\n };\n\n const onChange = (e: ChangeEvent) => {\n setPhoneNumber(e.target.value);\n };\n\n // If the use clicks enter we want to trigger the onClick function\n const onEnter = (e: KeyboardEvent) => {\n if (e.key === 'Enter' && phoneNumber !== '') {\n onClickFn();\n }\n };\n\n return (\n \n \n \n \n \n ),\n }}\n />\n );\n};\n\nexport default DialPhoneNumberInput;\n","import * as j from '@mojotech/json-type-validation';\n\nexport const TransferType = {\n ARN: 'arn',\n PhoneNumber: 'phone-number',\n} as const;\n\nexport type TransferTypeValues = typeof TransferType[keyof typeof TransferType];\n\nexport type Transfer = {\n id: number;\n name: string;\n endpoint: string;\n description: string | undefined;\n type: TransferTypeValues;\n};\n\nexport type CreateTransfer = Omit;\nexport type UpdateTransfer = Partial>;\n\nexport type TransferResponse = {\n nextPageUrl: string | null;\n transfers: Transfer[];\n};\n\nconst TransferResponseDecoder: j.Decoder = j\n .object({\n transfer_id: j.number(),\n name: j.string(),\n endpoint: j.string(),\n description: j.union(j.string(), j.constant(null)),\n type: j.string(),\n })\n .map((item: any) => ({\n id: item.transfer_id,\n name: item.name,\n endpoint: item.endpoint,\n description: item.description || null,\n type: item.type,\n }));\n\nexport const TransfersResponseDecoder: j.Decoder = j\n .object({\n next_page_url: j.union(j.string(), j.constant(null)),\n transfers: j.array(TransferResponseDecoder),\n })\n .map((item: any) => ({\n nextPageUrl: item.next_page_url,\n transfers: item.transfers,\n }));\n","import axios, { CancelTokenSource } from 'axios';\n\nimport { APIError, UnsupportedStructureError } from '~services/Errors';\n\nimport { CreateTransfer, TransferResponse, TransfersResponseDecoder, UpdateTransfer } from './domain';\n\nexport const getTransfersByNextUrl = async (\n nextUrl: string,\n cancelToken?: CancelTokenSource,\n): Promise => {\n const path = nextUrl;\n let resp;\n\n try {\n resp = await axios.request({\n url: path,\n method: 'GET',\n headers: {\n Accept: 'application/json',\n },\n cancelToken: cancelToken ? cancelToken.token : undefined,\n });\n } catch (e) {\n if (axios.isCancel(e)) {\n return undefined;\n }\n\n if (axios.isAxiosError(e)) {\n // Response should always be defined if axios error\n throw new APIError(e.response!.status, (e.response!.data as { error: string }).error || e.message);\n }\n\n throw new APIError(-1, e as string);\n }\n\n const decoded = TransfersResponseDecoder.run(resp.data);\n\n if (decoded.ok === false) {\n const err = new UnsupportedStructureError(decoded.error.message);\n\n console.error(decoded.error);\n console.error(err);\n throw err;\n }\n\n return decoded.result;\n};\n\nexport const getTransfers = async (\n search: string | undefined,\n cancelToken?: CancelTokenSource,\n): Promise => {\n const path = '/api/transfers/';\n const params = {\n search: search ? search.toLowerCase() : undefined,\n };\n let resp;\n\n try {\n resp = await axios.request({\n url: path,\n method: 'GET',\n params: params,\n headers: {\n Accept: 'application/json',\n },\n cancelToken: cancelToken ? cancelToken.token : undefined,\n });\n } catch (e) {\n if (axios.isCancel(e)) {\n return undefined;\n }\n\n if (axios.isAxiosError(e)) {\n // Response should always be defined if axios error\n throw new APIError(e.response!.status, (e.response!.data as { error: string }).error || e.message);\n }\n\n throw new APIError(-1, e as string);\n }\n\n const decoded = TransfersResponseDecoder.run(resp.data);\n\n if (decoded.ok === false) {\n const err = new UnsupportedStructureError(decoded.error.message);\n\n console.error(decoded.error);\n console.error(err);\n throw err;\n }\n\n return decoded.result;\n};\n\nexport const createTransfer = async (data: CreateTransfer): Promise => {\n const path = '/api/transfers/';\n\n let body: any = {\n name: data.name,\n endpoint: data.endpoint,\n description: data.description,\n type: data.type,\n };\n\n try {\n await axios.request({\n method: 'POST',\n url: path,\n data: body,\n headers: {\n Accept: 'application/json',\n },\n });\n } catch (e) {\n if (axios.isAxiosError(e)) {\n // Response should always be defined if axios error\n throw new APIError(e.response!.status, (e.response!.data as { error: string }).error || e.message);\n }\n\n throw new APIError(-1, e as string);\n }\n};\n\nexport const updateTransfer = async (transferId: number, data: UpdateTransfer): Promise => {\n const path = `/api/transfers/${transferId}`;\n\n let body: any = {\n name: data.name || undefined,\n endpoint: data.endpoint || undefined,\n description: data.description || undefined,\n type: data.type || undefined,\n };\n\n try {\n await axios.request({\n method: 'PATCH',\n url: path,\n data: body,\n headers: {\n Accept: 'application/json',\n },\n });\n } catch (e) {\n if (axios.isAxiosError(e)) {\n // Response should always be defined if axios error\n throw new APIError(e.response!.status, (e.response!.data as { error: string }).error || e.message);\n }\n\n throw new APIError(-1, e as string);\n }\n};\n\nexport const removeTransfer = async (transferId: number): Promise => {\n const path = `/api/transfers/${transferId}`;\n\n try {\n await axios.request({\n method: 'DELETE',\n url: path,\n headers: {\n Accept: 'application/json',\n },\n });\n } catch (e) {\n if (axios.isAxiosError(e)) {\n // Response should always be defined if axios error\n throw new APIError(e.response!.status, (e.response!.data as { error: string }).error || e.message);\n }\n\n throw new APIError(-1, e as string);\n }\n};\n","import axios, { CancelTokenSource } from 'axios';\nimport { useCallback, useEffect, useRef, useState } from 'react';\n\nimport { createTransfer, getTransfers, getTransfersByNextUrl, removeTransfer, updateTransfer } from './api';\nimport { CreateTransfer, Transfer, TransferResponse, UpdateTransfer } from './domain';\n\ntype Options = {\n search?: string;\n\n shouldFetch?: boolean;\n};\n\nconst useTransfers = (\n options: Options = {\n search: undefined,\n shouldFetch: true,\n },\n) => {\n const [loading, setLoading] = useState(true);\n const [error, setError] = useState(false);\n const [list, setList] = useState([]);\n const [hasMore, setHasMore] = useState