├── .gitignore ├── CODEOWNERS ├── README.md ├── build ├── asset-manifest.json ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json ├── robots.txt └── static │ ├── css │ ├── main.a5c84cc7.css │ └── main.a5c84cc7.css.map │ ├── js │ ├── 453.bcce1d62.chunk.js │ ├── 453.bcce1d62.chunk.js.map │ ├── main.d90dfe46.js │ ├── main.d90dfe46.js.LICENSE.txt │ └── main.d90dfe46.js.map │ └── media │ └── AppLogo.756605568b186bd69dd0.png ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt └── src ├── App.css ├── App.js ├── App.test.js ├── AppLogo.png ├── bootstrapMessaging.css ├── bootstrapMessaging.js ├── components ├── conversation.js ├── conversationEntry.css ├── conversationEntry.js ├── messagingBody.css ├── messagingBody.js ├── messagingButton.css ├── messagingButton.js ├── messagingHeader.css ├── messagingHeader.js ├── messagingInputFooter.css ├── messagingInputFooter.js ├── messagingWindow.css ├── messagingWindow.js ├── participantChange.css ├── participantChange.js ├── prechat.css ├── prechat.js ├── textMessage.css ├── textMessage.js ├── typingIndicator.css └── typingIndicator.js ├── helpers ├── common.js ├── constants.js ├── conversationEntryUtil.js ├── countdownTimer.js ├── eventsource-polyfill.js ├── prechatUtil.js └── webstorageUtils.js ├── index.css ├── index.js ├── logo.svg ├── reportWebVitals.js ├── services ├── dataProvider.js ├── eventSourceService.js └── messagingService.js ├── setupTests.js └── ui-effects └── draggable.js /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | # /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Comment line immediately above ownership line is reserved for related gus information. Please be careful while editing. 2 | #GUSINFO:Embedded Service for Web,Service Community Experience (SCE) 3 | * 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Salesforce Messaging for Web API Sample App 2 | 3 | A repository holding a sample app created using React JS library to demonstrate the Messaging experience using Messaging for In-App and Web Public (aka v2.0) REST APIs. 4 | Make sure to always refer to our [Wiki](https://github.com/Salesforce-Async-Messaging/messaging-web-api-sample-app/wiki/Current-App-Support) for the supported features in the app. 5 | 6 | ## REST API Documentation 7 | [https://developer.salesforce.com/docs/service/messaging-api](https://developer.salesforce.com/docs/service/messaging-api) 8 | 9 | ## Prerequisites 10 | Ensure you have an Embedded Service deployment for Messaging for In-App and Web created of type Custom Client. 11 | 12 | ## Launch Application Remotely 13 | Go to [https://salesforce-async-messaging.github.io/messaging-web-api-sample-app/build/index.html](https://salesforce-async-messaging.github.io/messaging-web-api-sample-app/build/index.html) 14 | 15 | ## Local Development and Testing Setup 16 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 17 | For issues with the sample app, please contact Salesforce Support. 18 | 19 | ### Local Environment requirements 20 | For local app development and testing, make sure you have `npm` or `yarn` installed. 21 | 22 | ### Installation 23 | #### Clone this repo 24 | ``` 25 | $ git clone https://github.com/Salesforce-Async-Messaging/messaging-web-api-sample-app.git 26 | ``` 27 | 28 | #### Install build dependencies 29 | ``` 30 | $ cd messaging-web-api-sample-app 31 | $ npm install 32 | $ npm run build 33 | ``` 34 | Builds the app for production to the `build` folder.\ 35 | It correctly bundles React in production mode and optimizes the build for the best performance. 36 | The build is minified and the filenames include the hashes.\ 37 | Your app is ready to be deployed! 38 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 39 | 40 | #### Launch Application from Local Setup 41 | ``` 42 | $ npm start 43 | ``` 44 | - Runs the app in the development mode.\ 45 | - The page will reload when you make changes.\ 46 | - You may also see any lint errors in the console. 47 | - [Local Testing Only] - Disable Strict Mode in the React App by going to `src/index.js` and comment out `React.StrictMode` encapsulating the `` 48 | - After the app is running, open [http://localhost:3000](http://localhost:3000) in your browser to get started. 49 | 50 | ## Test Sample App 51 | - Once the Sample App page is launched either Remotely or via Local Setup, input your Embedded Service deployment details in the form and submit. 52 | - The deployment details can be found under the Code Snippet panel under Embedded Service deployment setup in Salesforce. 53 | - Click on the 'Let's Chat' Button to get started with a new conversation. 54 | -------------------------------------------------------------------------------- /build/asset-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": { 3 | "main.css": "./static/css/main.a5c84cc7.css", 4 | "main.js": "./static/js/main.d90dfe46.js", 5 | "static/js/453.bcce1d62.chunk.js": "./static/js/453.bcce1d62.chunk.js", 6 | "static/media/AppLogo.png": "./static/media/AppLogo.756605568b186bd69dd0.png", 7 | "index.html": "./index.html", 8 | "main.a5c84cc7.css.map": "./static/css/main.a5c84cc7.css.map", 9 | "main.d90dfe46.js.map": "./static/js/main.d90dfe46.js.map", 10 | "453.bcce1d62.chunk.js.map": "./static/js/453.bcce1d62.chunk.js.map" 11 | }, 12 | "entrypoints": [ 13 | "static/css/main.a5c84cc7.css", 14 | "static/js/main.d90dfe46.js" 15 | ] 16 | } -------------------------------------------------------------------------------- /build/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Salesforce-Async-Messaging/messaging-web-api-sample-app/e57cd4870b04a0d52570ac9625a65aa3029fc810/build/favicon.ico -------------------------------------------------------------------------------- /build/index.html: -------------------------------------------------------------------------------- 1 | Messaging for Web API Sample App
-------------------------------------------------------------------------------- /build/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Salesforce-Async-Messaging/messaging-web-api-sample-app/e57cd4870b04a0d52570ac9625a65aa3029fc810/build/logo192.png -------------------------------------------------------------------------------- /build/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Salesforce-Async-Messaging/messaging-web-api-sample-app/e57cd4870b04a0d52570ac9625a65aa3029fc810/build/logo512.png -------------------------------------------------------------------------------- /build/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /build/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /build/static/css/main.a5c84cc7.css: -------------------------------------------------------------------------------- 1 | body{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif;margin:0}code{font-family:source-code-pro,Menlo,Monaco,Consolas,Courier New,monospace}.App{text-align:center}.App-logo{height:25vmin;pointer-events:none;position:absolute;top:25px}.App-header{align-items:center;background-color:#282c34;color:#fff;display:flex;flex-direction:column;font-size:calc(10px + 2vmin);justify-content:center;min-height:100vh}.App-link{color:#61dafb}@keyframes App-logo-spin{0%{transform:rotate(0deg)}to{transform:rotate(1turn)}}.messagingWindow{background-color:#fff;border:1px solid #ddd;border-radius:15px;bottom:50px;box-shadow:0 20px 20px #00000014;display:inline-block;height:600px;margin:0 auto;max-width:550px;right:10px;width:550px;z-index:9999999999}.messagingWindow[class~=hide]{display:none}.messagingHeader{background-color:#625b5b;border-radius:15px 15px 0 0;font-family:inherit;height:50px;width:inherit}.messagingHeaderCloseButton{background:#0000;border:none;cursor:pointer;float:right;height:50px;margin-right:20px;pointer-events:all;width:-webkit-max-content;width:max-content}.messagingHeaderCloseButtonIcon{fill:#f5f5f5;transform:scale(2)}.messagingHeaderCloseButtonIcon:hover{opacity:.5;transform:scale(2)}.messagingBody{background:#f5f5f5;border-bottom:1px solid #ddd;border-radius:inherit;font-family:inherit;height:500px;overflow:auto}.conversationEndTimeText,.conversationStartTimeText{color:#625b5b;font-size:12px;font-weight:700;margin-top:10px;text-align:center}.conversationEntriesListView{height:auto;margin-bottom:0;margin-top:0;padding:5px 10px}.conversationEntryContainer{margin-bottom:10px;margin-top:10px;overflow:auto;position:relative}.textMessageSenderContent{color:#999;font-size:10px;font-weight:400;margin:0}.textMessageSenderContent[class~=incoming]{text-align:left}.textMessageSenderContent[class~=outgoing]{text-align:right}.textMessageBubbleContainer{overflow:auto}.textMessageBubble{background-color:#9e9a9a;border-radius:20px;margin:1px auto;max-width:350px;min-width:50px;padding:8px 14px;width:-webkit-fit-content}.textMessageBubble[class~=incoming]{float:left}.textMessageBubble[class~=outgoing]{float:right}.textMessageContent{color:#fff;font-size:16px;font-weight:300;margin:0}.participantChangeText{background-color:linen;border:1px solid #999;border-radius:10px;color:#625b5b;display:inline-block;font-size:12px;font-weight:700;margin-top:10px;text-align:center;width:-webkit-max-content;width:max-content}.typingIndicatorContainer{display:flex;flex-direction:column}.typingIndicatorBubbleContainer{padding-left:10px;padding-right:10px}.typingIndicatorBubble{background-color:#9e9a9a;border-radius:20px;float:left;margin:1px auto;max-width:350px;min-width:50px;padding:4px 14px;width:-webkit-fit-content}.typingIndicatorSenderContent{color:#999;font-size:10px;font-weight:400;margin:0;padding-left:10px;padding-right:10px;text-align:left}.loadingBall{animation-delay:-.5s;animation:typingIndicatorAnimation 2.5s ease-in-out infinite;background-color:#fff;border-radius:50%;display:inline-flex;height:10px;margin:0 1.5px;max-width:100%;text-align:center;width:10px}.second{animation-delay:3s}.third{animation-delay:4s}@keyframes typingIndicatorAnimation{0%{opacity:0}35%{opacity:1}50%{opacity:1}}.messagingFooter{background-color:#f5f5f5;border-radius:inherit;display:flex;font-family:inherit;height:50px;width:100%}.messagingFooter textarea{align-content:center;border-radius:15px;font-family:inherit;margin:5px;resize:none;width:inherit}.sendButton{background:#0000;border:0}.sendButtonIcon{fill:#625b5b;transform:scale(2)}.sendButton:hover:not(:disabled){cursor:pointer;opacity:.7}.sendButton:disabled{opacity:.3}.messagingInputTextarea:disabled,.sendButton:disabled{cursor:not-allowed}.messagingPrechat{background:#f5f5f5;border-radius:inherit}.prechatForm{background:#0000;border-bottom:1px solid #ddd;border-radius:inherit;font-family:inherit;height:500px;overflow:auto}.prechatFormFieldsListView{height:inherit;list-style-type:none;margin:0;padding:0}.prechatFormFieldContainer{margin:15px 0;max-height:100px;min-width:100px;padding:0 14px}.prechatFormFieldName{color:#000;display:flex;flex-direction:column;font-size:medium;margin:0;text-align:left}.prechatFormField{border:1px solid;width:100%}.checkbox{margin-right:auto;width:4%}select{border-radius:5px;height:35px}.requiredFieldIndicator{color:red;display:contents}.startConversationButton{background-color:#888c8c;border:1px;border-radius:5px;font-weight:700;height:35px;margin:10px 0;width:75%}.startConversationButton:hover{cursor:pointer;opacity:.8;pointer-events:all}.messagingButton{align-self:center;background-color:#3fc56e;border:0;border-radius:5px;box-shadow:0 2px 2px #d3d3d3;color:#fff;cursor:pointer;font-weight:700;margin:10px 0;outline:0;padding:15px 30px;position:relative;top:150px;transition:background-color .25s ease;width:10rem}.messagingButton:hover:not(:disabled){background-color:#2d8645}.messagingButton:disabled{cursor:not-allowed;opacity:.7}.messagingButtonLoadingUI{--color:#fff;--size-mid:4vmin;--size-dot:1.5vmin;--size-bar:0.4vmin;--size-square:3vmin;bottom:30px;display:block;position:relative;width:50%}.messagingButtonLoadingUI:after,.messagingButtonLoadingUI:before{box-sizing:border-box;content:"";position:absolute}.messagingButtonLoadingUI.loadingBalls:before{animation:loadingAnimation 1s linear infinite;border:4px solid #fff;border-radius:50%;border-top-color:#0000;height:var(--size-mid);width:var(--size-mid)}.messagingButtonLoadingUI.loadingBalls:after{animation:loadingAnimation .6s linear infinite reverse;border:2px solid #0000;border-radius:50%;border-top-color:#fff;height:calc(var(--size-mid) - 2px);width:calc(var(--size-mid) - 2px)}@keyframes loadingAnimation{to{transform:rotate(1turn)}}.deploymentDetailsForm{display:flex;flex-direction:column}input{align-self:center;border-radius:5px;height:2rem;width:20rem}.deploymentDetailsFormSubmitButton{align-self:center;background-color:#3f51b5;border:0;border-radius:5px;box-shadow:0 2px 2px #d3d3d3;color:#fff;cursor:pointer;font-weight:700;margin:10px 0;outline:0;padding:15px 30px;transition:background-color .25s ease;width:10rem}.deploymentDetailsFormSubmitButton:hover:not(:disabled){background-color:#283593}.deploymentDetailsFormSubmitButton:disabled{cursor:not-allowed;opacity:.7} 2 | /*# sourceMappingURL=main.a5c84cc7.css.map*/ -------------------------------------------------------------------------------- /build/static/css/main.a5c84cc7.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"static/css/main.a5c84cc7.css","mappings":"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","sources":["index.css","App.css","components/messagingWindow.css","components/messagingHeader.css","components/messagingBody.css","components/conversationEntry.css","components/textMessage.css","components/participantChange.css","components/typingIndicator.css","components/messagingInputFooter.css","components/prechat.css","components/messagingButton.css","bootstrapMessaging.css"],"sourcesContent":["body {\n margin: 0;\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',\n 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',\n sans-serif;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n}\n\ncode {\n font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',\n monospace;\n}\n",".App {\n text-align: center;\n}\n\n.App-logo {\n height: 25vmin;\n pointer-events: none;\n position: absolute;\n top: 25px;\n}\n\n/* @media (prefers-reduced-motion: no-preference) {\n .App-logo {\n animation: App-logo-spin infinite 20s linear;\n }\n} */\n\n.App-header {\n background-color: #282c34;\n min-height: 100vh;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n font-size: calc(10px + 2vmin);\n color: white;\n}\n\n.App-link {\n color: #61dafb;\n}\n\n@keyframes App-logo-spin {\n from {\n transform: rotate(0deg);\n }\n to {\n transform: rotate(360deg);\n }\n}\n",".messagingWindow {\n z-index: 9999999999;\n background-color: white;\n box-shadow: 0 20px 20px rgba(0, 0, 0, 0.08);\n border-radius: 15px;\n max-width: 550px;\n margin: 0 auto;\n border: 1px solid #ddd;\n /* position: fixed; */\n width: 550px;\n height: 600px;\n display: inline-block;\n bottom: 50px;\n right: 10px;\n}\n\n.messagingWindow[class~=\"hide\"] {\n display: none;\n}",".messagingHeader {\n width: inherit;\n height: 50px;\n background-color: rgb(98 91 91);\n border-radius: 15px 15px 0px 0px;\n font-family: inherit;\n}\n\n.messagingHeaderCloseButton {\n float: right;\n margin-right: 20px;\n height: 50px;\n width: max-content;\n background: transparent;\n border: none;\n pointer-events: all;\n cursor: pointer;\n}\n\n.messagingHeaderCloseButtonIcon {\n transform: scale(2);\n fill: whitesmoke;\n}\n\n.messagingHeaderCloseButtonIcon:hover{\n opacity: 0.5;\n transform: scale(2);\n}",".messagingBody {\n height: 500px;\n border-bottom: 1px solid #ddd;\n overflow: auto;\n background: whitesmoke;\n border-radius: inherit;\n font-family: inherit;\n}\n\n.conversationStartTimeText, .conversationEndTimeText {\n color: rgb(98 91 91);\n margin-top: 10px;\n font-size: 12px;\n font-weight: bold;\n text-align: center;\n}\n\n.conversationEntriesListView {\n height: auto;\n padding: 5px 10px;\n margin-top: 0;\n margin-bottom: 0;\n}",".conversationEntryContainer {\n position: relative;\n overflow: auto;\n margin-top: 10px;\n margin-bottom: 10px;\n}",".textMessageSenderContent {\n color: rgb(153, 153, 153);\n margin: 0px;\n font-size: 10px;\n font-weight: 400;\n}\n\n.textMessageSenderContent[class~=\"incoming\"] {\n text-align: left;\n}\n\n.textMessageSenderContent[class~=\"outgoing\"] {\n text-align: right;\n}\n\n.textMessageBubbleContainer {\n overflow: auto;\n}\n\n.textMessageBubble {\n min-width: 50px;\n max-width: 350px;\n width: -webkit-fit-content;\n background-color: rgb(158 154 154);\n border-radius: 20px;\n margin: 1px auto;\n padding: 8px 14px;\n}\n\n.textMessageBubble[class~=\"incoming\"] {\n float: left;\n}\n\n.textMessageBubble[class~=\"outgoing\"] {\n float: right;\n}\n\n.textMessageContent {\n color: rgb(255, 255, 255);\n margin: 0px;\n font-size: 16px;\n font-weight: 300;\n}",".participantChangeText {\n color: rgb(98 91 91);\n background-color: linen;\n margin-top: 10px;\n font-size: 12px;\n font-weight: bold;\n text-align: center;\n border: 1px solid rgb(153, 153, 153);\n border-radius: 10px;\n width: max-content;\n display: inline-block;\n}",".typingIndicatorContainer {\n display: flex;\n flex-direction: column;\n}\n\n.typingIndicatorBubbleContainer {\n padding-right: 10px;\n padding-left: 10px;\n} \n\n.typingIndicatorBubble {\n float: left;\n min-width: 50px;\n max-width: 350px;\n width: -webkit-fit-content;\n background-color: rgb(158 154 154);\n border-radius: 20px;\n margin: 1px auto;\n padding: 4px 14px;\n}\n\n.typingIndicatorSenderContent {\n color: rgb(153, 153, 153);\n margin: 0px;\n font-size: 10px;\n font-weight: 400;\n text-align: left;\n padding-right: 10px;\n padding-left: 10px;\n}\n \n .loadingBall {\n display: inline-flex;\n text-align: center;\n margin: 0px 1.5px;\n max-width: 100%;\n width: 10px;\n height: 10px;\n border-radius: 50%;\n background-color: white;\n animation-delay: -0.5s;\n animation: typingIndicatorAnimation 2.5s infinite ease-in-out;\n}\n\n.second {\n animation-delay: 3s;\n}\n\n.third {\n animation-delay: 4s;\n}\n\n@keyframes typingIndicatorAnimation {\n 0% {\n opacity: 0;\n }\n\n 35% {\n opacity: 1;\n }\n\n 50% {\n opacity: 1;\n }\n}",".messagingFooter {\n width: 100%;\n display: flex;\n height: 50px;\n font-family: inherit;\n border-radius: inherit;\n background-color: whitesmoke;\n}\n\n.messagingFooter textarea {\n width: inherit;\n align-content: center;\n font-family: inherit;\n border-radius: 15px;\n margin: 5px;\n resize: none;\n}\n\n.sendButton {\n background: transparent;\n border: 0;\n}\n\n.sendButtonIcon{\n transform: scale(2);\n fill: rgb(98 91 91);\n}\n\n.sendButton:hover:not(:disabled){\n opacity: 0.7;\n cursor: pointer;\n}\n\n.sendButton:disabled {\n opacity: 0.3;\n}\n\n.messagingInputTextarea:disabled, .sendButton:disabled {\n cursor: not-allowed;\n}",".messagingPrechat {\n background: whitesmoke;\n border-radius: inherit;\n}\n\n.prechatForm {\n height: 500px;\n border-bottom: 1px solid #ddd;\n overflow: auto;\n background: transparent;\n border-radius: inherit;\n font-family: inherit;\n}\n\n.prechatFormFieldsListView {\n height: inherit;\n margin: 0;\n padding: 0;\n list-style-type: none;\n}\n\n.prechatFormFieldContainer {\n min-width: 100px;\n padding: 0 14px;\n max-height: 100px;\n margin: 15px 0;\n}\n\n.prechatFormFieldName {\n color: black;\n font-size: medium;\n margin: 0;\n text-align: left;\n display: flex;\n flex-direction: column;\n}\n\n.prechatFormField {\n width: 100%;\n border: 1px solid;\n}\n\n.checkbox {\n width: 4%;\n margin-right: auto;\n}\n\nselect {\n height: 35px;\n border-radius: 5px;\n}\n\n.requiredFieldIndicator {\n color: red;\n display: contents;\n}\n\n.startConversationButton {\n width: 75%;\n height: 35px;\n margin: 10px 0;\n border-radius: 5px;\n background-color: #888c8c;\n border: 1px;\n font-weight: bold;\n}\n\n.startConversationButton:hover {\n opacity: 0.8;\n pointer-events: all;\n cursor: pointer;\n}",".messagingButton {\n width: 10rem;\n background-color: #3fc56e;\n color: white;\n padding: 15px 30px;\n border-radius: 5px;\n outline: 0;\n border: 0; \n margin: 10px 0px;\n cursor: pointer;\n box-shadow: 0px 2px 2px lightgray;\n transition: ease background-color 250ms;\n font-weight: bold;\n position: relative;\n align-self: center;\n top: 150px;\n}\n\n.messagingButton:hover:not(:disabled) {\n background-color: #2d8645;\n}\n\n.messagingButton:disabled {\n cursor: not-allowed;\n opacity: 0.7;\n}\n\n/**\n * Styles related to the spinner shown on Messaging Button\n */\n.messagingButtonLoadingUI {\n --color: white;\n --size-mid: 4vmin;\n --size-dot: 1.5vmin;\n --size-bar: 0.4vmin;\n --size-square: 3vmin;\n position: relative;\n width: 50%;\n display: block;\n bottom: 30px;\n}\n\n.messagingButtonLoadingUI::before,\n.messagingButtonLoadingUI::after {\n\tcontent: '';\n\tbox-sizing: border-box;\n\tposition: absolute;\n}\n\n.messagingButtonLoadingUI.loadingBalls::before {\n\twidth: var(--size-mid);\n\theight: var(--size-mid);\n\tborder: 4px solid white;\n\tborder-top-color: transparent;\n\tborder-radius: 50%;\n\tanimation: loadingAnimation 1s linear infinite;\n}\n\n.messagingButtonLoadingUI.loadingBalls::after {\n\twidth: calc(var(--size-mid) - 2px);\n\theight: calc(var(--size-mid) - 2px);\n\tborder: 2px solid transparent;\n\tborder-top-color: white;\n\tborder-radius: 50%;\n\tanimation: loadingAnimation 0.6s linear reverse infinite;\n}\n\n@keyframes loadingAnimation {\n\t100% {\n\t\ttransform: rotate(1turn);\n\t}\n}",".deploymentDetailsForm {\n display: flex;\n flex-direction: column;\n}\n\ninput {\n height: 2rem;\n width: 20rem;\n align-self: center;\n border-radius: 5px;\n}\n\n.deploymentDetailsFormSubmitButton {\n width: 10rem;\n background-color: #3f51b5;\n color: white;\n padding: 15px 30px;\n border-radius: 5px;\n outline: 0;\n border: 0; \n margin: 10px 0px;\n cursor: pointer;\n box-shadow: 0px 2px 2px lightgray;\n transition: ease background-color 250ms;\n font-weight: bold;\n align-self: center;\n}\n\n.deploymentDetailsFormSubmitButton:hover:not(:disabled) {\n background-color: #283593;\n}\n\n.deploymentDetailsFormSubmitButton:disabled {\n cursor: not-allowed;\n opacity: 0.7;\n}"],"names":[],"sourceRoot":""} -------------------------------------------------------------------------------- /build/static/js/453.bcce1d62.chunk.js: -------------------------------------------------------------------------------- 1 | "use strict";(self.webpackChunkmessagingforweb_sample_app=self.webpackChunkmessagingforweb_sample_app||[]).push([[453],{453:(e,t,n)=>{n.r(t),n.d(t,{getCLS:()=>y,getFCP:()=>g,getFID:()=>C,getLCP:()=>P,getTTFB:()=>D});var i,r,a,o,u=function(e,t){return{name:e,value:void 0===t?-1:t,delta:0,entries:[],id:"v2-".concat(Date.now(),"-").concat(Math.floor(8999999999999*Math.random())+1e12)}},c=function(e,t){try{if(PerformanceObserver.supportedEntryTypes.includes(e)){if("first-input"===e&&!("PerformanceEventTiming"in self))return;var n=new PerformanceObserver((function(e){return e.getEntries().map(t)}));return n.observe({type:e,buffered:!0}),n}}catch(e){}},s=function(e,t){var n=function n(i){"pagehide"!==i.type&&"hidden"!==document.visibilityState||(e(i),t&&(removeEventListener("visibilitychange",n,!0),removeEventListener("pagehide",n,!0)))};addEventListener("visibilitychange",n,!0),addEventListener("pagehide",n,!0)},f=function(e){addEventListener("pageshow",(function(t){t.persisted&&e(t)}),!0)},m=function(e,t,n){var i;return function(r){t.value>=0&&(r||n)&&(t.delta=t.value-(i||0),(t.delta||void 0===i)&&(i=t.value,e(t)))}},p=-1,v=function(){return"hidden"===document.visibilityState?0:1/0},d=function(){s((function(e){var t=e.timeStamp;p=t}),!0)},l=function(){return p<0&&(p=v(),d(),f((function(){setTimeout((function(){p=v(),d()}),0)}))),{get firstHiddenTime(){return p}}},g=function(e,t){var n,i=l(),r=u("FCP"),a=function(e){"first-contentful-paint"===e.name&&(s&&s.disconnect(),e.startTime-1&&e(t)},r=u("CLS",0),a=0,o=[],p=function(e){if(!e.hadRecentInput){var t=o[0],i=o[o.length-1];a&&e.startTime-i.startTime<1e3&&e.startTime-t.startTime<5e3?(a+=e.value,o.push(e)):(a=e.value,o=[e]),a>r.value&&(r.value=a,r.entries=o,n())}},v=c("layout-shift",p);v&&(n=m(i,r,t),s((function(){v.takeRecords().map(p),n(!0)})),f((function(){a=0,T=-1,r=u("CLS",0),n=m(i,r,t)})))},E={passive:!0,capture:!0},w=new Date,L=function(e,t){i||(i=t,r=e,a=new Date,F(removeEventListener),S())},S=function(){if(r>=0&&r1e12?new Date:performance.now())-e.timeStamp;"pointerdown"==e.type?function(e,t){var n=function(){L(e,t),r()},i=function(){r()},r=function(){removeEventListener("pointerup",n,E),removeEventListener("pointercancel",i,E)};addEventListener("pointerup",n,E),addEventListener("pointercancel",i,E)}(t,e):L(t,e)}},F=function(e){["mousedown","keydown","touchstart","pointerdown"].forEach((function(t){return e(t,b,E)}))},C=function(e,t){var n,a=l(),p=u("FID"),v=function(e){e.startTimeperformance.now())return;n.entries=[t],e(n)}catch(e){}},"complete"===document.readyState?setTimeout(t,0):addEventListener("load",(function(){return setTimeout(t,0)}))}}}]); 2 | //# sourceMappingURL=453.bcce1d62.chunk.js.map -------------------------------------------------------------------------------- /build/static/js/453.bcce1d62.chunk.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"static/js/453.bcce1d62.chunk.js","mappings":"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","sources":["../node_modules/web-vitals/dist/web-vitals.js"],"sourcesContent":["var e,t,n,i,r=function(e,t){return{name:e,value:void 0===t?-1:t,delta:0,entries:[],id:\"v2-\".concat(Date.now(),\"-\").concat(Math.floor(8999999999999*Math.random())+1e12)}},a=function(e,t){try{if(PerformanceObserver.supportedEntryTypes.includes(e)){if(\"first-input\"===e&&!(\"PerformanceEventTiming\"in self))return;var n=new PerformanceObserver((function(e){return e.getEntries().map(t)}));return n.observe({type:e,buffered:!0}),n}}catch(e){}},o=function(e,t){var n=function n(i){\"pagehide\"!==i.type&&\"hidden\"!==document.visibilityState||(e(i),t&&(removeEventListener(\"visibilitychange\",n,!0),removeEventListener(\"pagehide\",n,!0)))};addEventListener(\"visibilitychange\",n,!0),addEventListener(\"pagehide\",n,!0)},u=function(e){addEventListener(\"pageshow\",(function(t){t.persisted&&e(t)}),!0)},c=function(e,t,n){var i;return function(r){t.value>=0&&(r||n)&&(t.delta=t.value-(i||0),(t.delta||void 0===i)&&(i=t.value,e(t)))}},f=-1,s=function(){return\"hidden\"===document.visibilityState?0:1/0},m=function(){o((function(e){var t=e.timeStamp;f=t}),!0)},v=function(){return f<0&&(f=s(),m(),u((function(){setTimeout((function(){f=s(),m()}),0)}))),{get firstHiddenTime(){return f}}},d=function(e,t){var n,i=v(),o=r(\"FCP\"),f=function(e){\"first-contentful-paint\"===e.name&&(m&&m.disconnect(),e.startTime-1&&e(t)},f=r(\"CLS\",0),s=0,m=[],v=function(e){if(!e.hadRecentInput){var t=m[0],i=m[m.length-1];s&&e.startTime-i.startTime<1e3&&e.startTime-t.startTime<5e3?(s+=e.value,m.push(e)):(s=e.value,m=[e]),s>f.value&&(f.value=s,f.entries=m,n())}},h=a(\"layout-shift\",v);h&&(n=c(i,f,t),o((function(){h.takeRecords().map(v),n(!0)})),u((function(){s=0,l=-1,f=r(\"CLS\",0),n=c(i,f,t)})))},T={passive:!0,capture:!0},y=new Date,g=function(i,r){e||(e=r,t=i,n=new Date,w(removeEventListener),E())},E=function(){if(t>=0&&t1e12?new Date:performance.now())-e.timeStamp;\"pointerdown\"==e.type?function(e,t){var n=function(){g(e,t),r()},i=function(){r()},r=function(){removeEventListener(\"pointerup\",n,T),removeEventListener(\"pointercancel\",i,T)};addEventListener(\"pointerup\",n,T),addEventListener(\"pointercancel\",i,T)}(t,e):g(t,e)}},w=function(e){[\"mousedown\",\"keydown\",\"touchstart\",\"pointerdown\"].forEach((function(t){return e(t,S,T)}))},L=function(n,f){var s,m=v(),d=r(\"FID\"),p=function(e){e.startTimeperformance.now())return;n.entries=[t],e(n)}catch(e){}},\"complete\"===document.readyState?setTimeout(t,0):addEventListener(\"load\",(function(){return setTimeout(t,0)}))};export{h as getCLS,d as getFCP,L as getFID,F as getLCP,P as getTTFB};\n"],"names":["e","t","n","i","r","name","value","delta","entries","id","concat","Date","now","Math","floor","random","a","PerformanceObserver","supportedEntryTypes","includes","self","getEntries","map","observe","type","buffered","o","document","visibilityState","removeEventListener","addEventListener","u","persisted","c","f","s","m","timeStamp","v","setTimeout","firstHiddenTime","d","disconnect","startTime","push","window","performance","getEntriesByName","requestAnimationFrame","p","l","h","hadRecentInput","length","takeRecords","T","passive","capture","y","g","w","E","entryType","target","cancelable","processingStart","forEach","S","L","b","F","once","P","getEntriesByType","timing","max","navigationStart","responseStart","readyState"],"sourceRoot":""} -------------------------------------------------------------------------------- /build/static/js/main.d90dfe46.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /* ! 2 | * The buffer module from node.js, for the browser. 3 | * 4 | * @author Feross Aboukhadijeh 5 | * @license MIT 6 | */ 7 | 8 | /** 9 | * @license React 10 | * react-dom.production.min.js 11 | * 12 | * Copyright (c) Facebook, Inc. and its affiliates. 13 | * 14 | * This source code is licensed under the MIT license found in the 15 | * LICENSE file in the root directory of this source tree. 16 | */ 17 | 18 | /** 19 | * @license React 20 | * react-jsx-runtime.production.min.js 21 | * 22 | * Copyright (c) Facebook, Inc. and its affiliates. 23 | * 24 | * This source code is licensed under the MIT license found in the 25 | * LICENSE file in the root directory of this source tree. 26 | */ 27 | 28 | /** 29 | * @license React 30 | * react.production.min.js 31 | * 32 | * Copyright (c) Facebook, Inc. and its affiliates. 33 | * 34 | * This source code is licensed under the MIT license found in the 35 | * LICENSE file in the root directory of this source tree. 36 | */ 37 | 38 | /** 39 | * @license React 40 | * scheduler.production.min.js 41 | * 42 | * Copyright (c) Facebook, Inc. and its affiliates. 43 | * 44 | * This source code is licensed under the MIT license found in the 45 | * LICENSE file in the root directory of this source tree. 46 | */ 47 | -------------------------------------------------------------------------------- /build/static/media/AppLogo.756605568b186bd69dd0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Salesforce-Async-Messaging/messaging-web-api-sample-app/e57cd4870b04a0d52570ac9625a65aa3029fc810/build/static/media/AppLogo.756605568b186bd69dd0.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "homepage": ".", 3 | "name": "messagingforweb-sample-app", 4 | "version": "0.1.0", 5 | "private": true, 6 | "dependencies": { 7 | "@testing-library/jest-dom": "^5.17.0", 8 | "@testing-library/react": "^13.4.0", 9 | "@testing-library/user-event": "^13.5.0", 10 | "react": "^18.3.1", 11 | "react-dom": "^18.3.1", 12 | "react-icons": "^5.2.1", 13 | "react-scripts": "5.0.1", 14 | "web-vitals": "^2.1.4" 15 | }, 16 | "scripts": { 17 | "start": "react-scripts start", 18 | "build": "react-scripts build", 19 | "test": "react-scripts test", 20 | "eject": "react-scripts eject" 21 | }, 22 | "eslintConfig": { 23 | "extends": [ 24 | "react-app", 25 | "react-app/jest" 26 | ] 27 | }, 28 | "browserslist": { 29 | "production": [ 30 | ">0.2%", 31 | "not dead", 32 | "not op_mini all" 33 | ], 34 | "development": [ 35 | "last 1 chrome version", 36 | "last 1 firefox version", 37 | "last 1 safari version" 38 | ] 39 | }, 40 | "description": "A repository holding a sample app created using React JS library to demonstrate Messaging for In-App and Web Public (aka v2.0) REST APIs.", 41 | "main": "index.js", 42 | "repository": { 43 | "type": "git", 44 | "url": "https://git.soma.salesforce.com/embedded-service-for-web/messagingforweb-sample-app.git" 45 | }, 46 | "author": "Salesforce Embedded Service for Web", 47 | "license": "" 48 | } 49 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Salesforce-Async-Messaging/messaging-web-api-sample-app/e57cd4870b04a0d52570ac9625a65aa3029fc810/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | Messaging for Web API Sample App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Salesforce-Async-Messaging/messaging-web-api-sample-app/e57cd4870b04a0d52570ac9625a65aa3029fc810/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Salesforce-Async-Messaging/messaging-web-api-sample-app/e57cd4870b04a0d52570ac9625a65aa3029fc810/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 25vmin; 7 | pointer-events: none; 8 | position: absolute; 9 | top: 25px; 10 | } 11 | 12 | /* @media (prefers-reduced-motion: no-preference) { 13 | .App-logo { 14 | animation: App-logo-spin infinite 20s linear; 15 | } 16 | } */ 17 | 18 | .App-header { 19 | background-color: #282c34; 20 | min-height: 100vh; 21 | display: flex; 22 | flex-direction: column; 23 | align-items: center; 24 | justify-content: center; 25 | font-size: calc(10px + 2vmin); 26 | color: white; 27 | } 28 | 29 | .App-link { 30 | color: #61dafb; 31 | } 32 | 33 | @keyframes App-logo-spin { 34 | from { 35 | transform: rotate(0deg); 36 | } 37 | to { 38 | transform: rotate(360deg); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import AppLogo from './AppLogo.png' 2 | import './App.css'; 3 | import BootstrapMessaging from './bootstrapMessaging'; 4 | 5 | function App() { 6 | return ( 7 |
8 |
9 | logo 10 | 11 |
12 |
13 | ); 14 | } 15 | 16 | export default App; 17 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import App from './App'; 3 | 4 | test('renders learn react link', () => { 5 | render(); 6 | const linkElement = screen.getByText(/learn react/i); 7 | expect(linkElement).toBeInTheDocument(); 8 | }); 9 | -------------------------------------------------------------------------------- /src/AppLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Salesforce-Async-Messaging/messaging-web-api-sample-app/e57cd4870b04a0d52570ac9625a65aa3029fc810/src/AppLogo.png -------------------------------------------------------------------------------- /src/bootstrapMessaging.css: -------------------------------------------------------------------------------- 1 | .deploymentDetailsForm { 2 | display: flex; 3 | flex-direction: column; 4 | } 5 | 6 | input { 7 | height: 2rem; 8 | width: 20rem; 9 | align-self: center; 10 | border-radius: 5px; 11 | } 12 | 13 | .deploymentDetailsFormSubmitButton { 14 | width: 10rem; 15 | background-color: #3f51b5; 16 | color: white; 17 | padding: 15px 30px; 18 | border-radius: 5px; 19 | outline: 0; 20 | border: 0; 21 | margin: 10px 0px; 22 | cursor: pointer; 23 | box-shadow: 0px 2px 2px lightgray; 24 | transition: ease background-color 250ms; 25 | font-weight: bold; 26 | align-self: center; 27 | } 28 | 29 | .deploymentDetailsFormSubmitButton:hover:not(:disabled) { 30 | background-color: #283593; 31 | } 32 | 33 | .deploymentDetailsFormSubmitButton:disabled { 34 | cursor: not-allowed; 35 | opacity: 0.7; 36 | } -------------------------------------------------------------------------------- /src/bootstrapMessaging.js: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState, useEffect } from "react"; 4 | 5 | // Import children components to render. 6 | import MessagingWindow from "./components/messagingWindow"; 7 | import MessagingButton from "./components/messagingButton"; 8 | 9 | import './bootstrapMessaging.css'; 10 | 11 | import { storeOrganizationId, storeDeploymentDeveloperName, storeSalesforceMessagingUrl } from './services/dataProvider'; 12 | import { determineStorageType, initializeWebStorage, getItemInWebStorageByKey, getItemInPayloadByKey } from './helpers/webstorageUtils'; 13 | import { APP_CONSTANTS, STORAGE_KEYS } from './helpers/constants'; 14 | 15 | import Draggable from "./ui-effects/draggable"; 16 | 17 | export default function BootstrapMessaging() { 18 | let [shouldShowMessagingButton, setShowMessagingButton] = useState(false); 19 | let [orgId, setOrgId] = useState(''); 20 | let [deploymentDevName, setDeploymentDevName] = useState(''); 21 | let [messagingURL, setMessagingURL] = useState(''); 22 | let [shouldDisableMessagingButton, setShouldDisableMessagingButton] = useState(false); 23 | let [shouldShowMessagingWindow, setShouldShowMessagingWindow] = useState(false); 24 | let [showMessagingButtonSpinner, setShowMessagingButtonSpinner] = useState(false); 25 | let [isExistingConversation, setIsExistingConversation] = useState(false); 26 | 27 | useEffect(() => { 28 | const storage = determineStorageType(); 29 | if (!storage) { 30 | console.error(`Cannot initialize the app. Web storage is required for the app to function.`); 31 | return; 32 | } 33 | 34 | const messaging_webstorage_key = Object.keys(storage).filter(item => item.startsWith(APP_CONSTANTS.WEB_STORAGE_KEY))[0]; 35 | 36 | if (messaging_webstorage_key) { 37 | const webStoragePayload = storage.getItem(messaging_webstorage_key); 38 | const orgId = getItemInPayloadByKey(webStoragePayload, STORAGE_KEYS.ORGANIZATION_ID); 39 | const deploymentDevName = getItemInPayloadByKey(webStoragePayload, STORAGE_KEYS.DEPLOYMENT_DEVELOPER_NAME); 40 | const messagingUrl = getItemInPayloadByKey(webStoragePayload, STORAGE_KEYS.MESSAGING_URL); 41 | 42 | if (!isValidOrganizationId(orgId)) { 43 | console.warn(`Invalid organization id exists in the web storage: ${orgId}. Cleaning up the invalid object from the web storage.`); 44 | storage.removeItem(messaging_webstorage_key); 45 | // New conversation. 46 | setIsExistingConversation(false); 47 | return; 48 | } 49 | 50 | // Re-Initialize state variables from the values in the web storage. This also re-populates app's deployment parameters input form fields with the previously entered data, in case of a messaging session continuation (e.g. page reload). 51 | setOrgId(orgId); 52 | setDeploymentDevName(deploymentDevName); 53 | setMessagingURL(messagingUrl); 54 | 55 | // Initialize messaging client. 56 | initializeMessagingClient(orgId, deploymentDevName, messagingUrl); 57 | 58 | const messagingJwt = getItemInWebStorageByKey(STORAGE_KEYS.JWT); 59 | if (messagingJwt) { 60 | // Existing conversation. 61 | setIsExistingConversation(true); 62 | setShowMessagingButton(true); 63 | setShouldDisableMessagingButton(true); 64 | setShouldShowMessagingWindow(true); 65 | } else { 66 | // New conversation. 67 | setIsExistingConversation(false); 68 | } 69 | } else { 70 | // New conversation. 71 | setIsExistingConversation(false); 72 | } 73 | 74 | return () => { 75 | showMessagingWindow(false); 76 | }; 77 | }, []); 78 | 79 | /** 80 | * Initialize the messaging client by 81 | * 1. internally initializing the Embedded Service deployment paramaters in-memory. 82 | * 2. initializing Salesforce Organization Id in the browser web storage. 83 | */ 84 | function initializeMessagingClient(ord_id, deployment_dev_name, messaging_url) { 85 | // Initialize helpers. 86 | initializeWebStorage(ord_id || orgId); 87 | storeOrganizationId(ord_id || orgId); 88 | storeDeploymentDeveloperName(deployment_dev_name || deploymentDevName); 89 | storeSalesforceMessagingUrl(messaging_url || messagingURL); 90 | } 91 | 92 | /** 93 | * Validates whether the supplied string is a valid Salesforce Organization Id. 94 | * @returns {boolean} 95 | */ 96 | function isValidOrganizationId(id) { 97 | return typeof id === "string" && (id.length === 18 || id.length === 15) && id.substring(0, 3) === APP_CONSTANTS.ORGANIZATION_ID_PREFIX; 98 | } 99 | 100 | /** 101 | * Validates whether the supplied string is a valid Salesforce Embedded Service Deployment Developer Name. 102 | * @returns {boolean} 103 | */ 104 | function isValidDeploymentDeveloperName(name) { 105 | return typeof name === "string" && name.length > 0; 106 | } 107 | 108 | /** 109 | * Determines whether the supplied url is a Salesforce Url. 110 | * @returns {boolean} 111 | */ 112 | function isSalesforceUrl(url) { 113 | try { 114 | return typeof url === "string" && url.length > 0 && url.slice(-19) === APP_CONSTANTS.SALESFORCE_MESSAGING_SCRT_URL; 115 | } catch (err) { 116 | console.error(`Something went wrong in validating whether the url is a Salesforce url: ${err}`); 117 | return false; 118 | } 119 | } 120 | 121 | /** 122 | * Validates whether the supplied string has a valid protocol and is a Salesforce Url. 123 | * @returns {boolean} 124 | */ 125 | function isValidUrl(url) { 126 | try { 127 | const urlToValidate = new URL(url); 128 | return isSalesforceUrl(url) && urlToValidate.protocol === APP_CONSTANTS.HTTPS_PROTOCOL; 129 | } catch (err) { 130 | console.error(`Something went wrong in validating the url provided: ${err}`); 131 | return false; 132 | } 133 | } 134 | 135 | /** 136 | * Handle a click action from the Deployment-Details-Form Submit Button. If the inputted parameters are valid, initialize the Messaging Client and render the Messaging Button. 137 | * @param {object} evt - button click event 138 | */ 139 | function handleDeploymentDetailsFormSubmit(evt) { 140 | if (evt) { 141 | if(!isValidOrganizationId(orgId)) { 142 | alert(`Invalid OrganizationId Input Value: ${orgId}`); 143 | setShowMessagingButton(false); 144 | return; 145 | } 146 | if(!isValidDeploymentDeveloperName(deploymentDevName)) { 147 | alert(`Expected a valid Embedded Service Deployment Developer Name value to be a string but received: ${deploymentDevName}.`); 148 | setShowMessagingButton(false); 149 | return; 150 | } 151 | if(!isValidUrl(messagingURL)) { 152 | alert(`Expected a valid Salesforce Messaging URL value to be a string but received: ${messagingURL}.`); 153 | setShowMessagingButton(false); 154 | return; 155 | } 156 | 157 | // Initialize the Messaging Client. 158 | initializeMessagingClient(); 159 | // New conversation. 160 | setIsExistingConversation(false); 161 | // Render the Messaging Button. 162 | setShowMessagingButton(true); 163 | } 164 | } 165 | 166 | /** 167 | * Determines whether the Deployment-Details-Form Submit Button should be enabled/disabled. 168 | * @returns {boolean} TRUE - disabled the button and FALSE - otherwise 169 | */ 170 | function shouldDisableFormSubmitButton() { 171 | return (orgId && orgId.length === 0) || (deploymentDevName && deploymentDevName.length === 0) || (messagingURL && messagingURL.length === 0); 172 | } 173 | 174 | /** 175 | * Handle a click action from the Messaging Button. 176 | * @param {object} evt - button click event 177 | */ 178 | function handleMessagingButtonClick(evt) { 179 | if (evt) { 180 | console.log("Messaging Button clicked."); 181 | setShowMessagingButtonSpinner(true); 182 | showMessagingWindow(true); 183 | } 184 | } 185 | 186 | /** 187 | * Determines whether to render the Messaging Window based on the supplied parameter. 188 | * @param {boolean} shouldShow - TRUE - render the Messaging WINDOW and FALSE - Do not render the Messaging Window & Messaging Button 189 | */ 190 | function showMessagingWindow(shouldShow) { 191 | setShouldShowMessagingWindow(Boolean(shouldShow)); 192 | if (!shouldShow) { 193 | // Enable Messaging Button again when Messaging Window is closed. 194 | setShouldDisableMessagingButton(false); 195 | // Remove the spinner on the Messaging Button. 196 | setShowMessagingButtonSpinner(false); 197 | // Hide Messaging Button to re-initialize the client with form submit. 198 | setShowMessagingButton(false); 199 | } 200 | } 201 | 202 | /** 203 | * Handles the app UI readiness i.e. Messaging Button updates based on whether the Messaging Window UI is rendered. 204 | * @param {boolean} isReady - TRUE - disable the Messaging Button & remove the spinner and FALSE - otherwise. 205 | */ 206 | function appUiReady(isReady) { 207 | // Disable Messaging Button if the app is UI ready. 208 | setShouldDisableMessagingButton(isReady); 209 | // Remove the spinner on the Messaging Button if the app is UI ready. 210 | setShowMessagingButtonSpinner(!isReady); 211 | } 212 | 213 | return ( 214 | <> 215 |

Messaging for Web - Sample App

216 |
217 |

Input your Embedded Service (Custom Client) deployment details below

218 | 219 | setOrgId(e.target.value.trim())} 223 | disabled={shouldShowMessagingButton}> 224 | 225 | 226 | setDeploymentDevName(e.target.value.trim())} 230 | disabled={shouldShowMessagingButton}> 231 | 232 | 233 | setMessagingURL(e.target.value.trim())} 237 | disabled={shouldShowMessagingButton}> 238 | 239 | 246 |
247 | {shouldShowMessagingButton && 248 | } 252 | {shouldShowMessagingWindow && 253 | 254 | 258 | 259 | } 260 | 261 | ); 262 | } -------------------------------------------------------------------------------- /src/components/conversation.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import * as EventSourcePolyfill from "../helpers/eventsource-polyfill.js"; 3 | 4 | // Import children components to plug in and render. 5 | import MessagingHeader from "./messagingHeader"; 6 | import MessagingBody from "./messagingBody"; 7 | import MessagingInputFooter from "./messagingInputFooter"; 8 | 9 | import { setJwt, setLastEventId, storeConversationId, getConversationId, getJwt, clearInMemoryData, setDeploymentConfiguration } from "../services/dataProvider"; 10 | import { subscribeToEventSource, closeEventSource } from '../services/eventSourceService'; 11 | import { sendTypingIndicator, sendTextMessage, getContinuityJwt, listConversations, listConversationEntries, closeConversation, getUnauthenticatedAccessToken, createConversation } from "../services/messagingService"; 12 | import * as ConversationEntryUtil from "../helpers/conversationEntryUtil"; 13 | import { CONVERSATION_CONSTANTS, STORAGE_KEYS, CLIENT_CONSTANTS } from "../helpers/constants"; 14 | import { setItemInWebStorage, clearWebStorage } from "../helpers/webstorageUtils"; 15 | import { util } from "../helpers/common"; 16 | import { prechatUtil } from "../helpers/prechatUtil.js"; 17 | import Prechat from "./prechat.js"; 18 | import CountdownTimer from "../helpers/countdownTimer.js"; 19 | 20 | export default function Conversation(props) { 21 | // Initialize a list of conversation entries. 22 | let [conversationEntries, setConversationEntries] = useState([]); 23 | // Initialize the conversation status. 24 | let [conversationStatus, setConversationStatus] = useState(CONVERSATION_CONSTANTS.ConversationStatus.NOT_STARTED_CONVERSATION); 25 | // Tracks whether Pre-Chat form should be shown. 26 | let [showPrechatForm, setShowPrechatForm] = useState(false); 27 | // Tracks the most recent conversation message that was failed to send. 28 | let [failedMessage, setFailedMessage] = useState(undefined); 29 | 30 | // Initialize current participants that are actively typing (except end user). 31 | // Each key is the participant's `senderName` or `role` and each value is a reference to a CountdownTimer object. 32 | let [currentTypingParticipants, setCurrentTypingParticipants] = useState({}); 33 | // Initialize whether at least 1 participant (not including end user) is typing. 34 | let [isAnotherParticipantTyping, setIsAnotherParticipantTyping] = useState(false); 35 | 36 | useEffect(() => { 37 | let conversationStatePromise; 38 | 39 | conversationStatePromise = props.isExistingConversation ? handleExistingConversation() : handleNewConversation(); 40 | conversationStatePromise 41 | .then(() => { 42 | handleSubscribeToEventSource() 43 | .then(props.uiReady(true)) // Let parent (i.e. MessagingWindow) know the app is UI ready so that the parent can decide to show the actual Messaging window UI. 44 | .catch(() => { 45 | props.showMessagingWindow(false); 46 | }) 47 | }); 48 | 49 | return () => { 50 | conversationStatePromise 51 | .then(() => { 52 | cleanupMessagingData(); 53 | }); 54 | }; 55 | }, []); 56 | 57 | /** 58 | * Update conversation status state based on the event from a child component i.e. MessagingHeader. 59 | * Updating conversation status state re-renders the current component as well as the child components and the child components can reactively use the updated conversation status to make any changes. 60 | * 61 | * @param {string} status - e.g. CLOSED. 62 | */ 63 | function updateConversationStatus(status) { 64 | setConversationStatus(status); 65 | } 66 | 67 | /** 68 | * Handles a new conversation. 69 | * 70 | * 1. Fetch an Unauthenticated Access Token i.e. Messaging JWT. 71 | * 2. Create a new conversation. 72 | * @returns {Promise} 73 | */ 74 | function handleNewConversation() { 75 | return handleGetUnauthenticatedJwt() 76 | .then(() => { 77 | if (prechatUtil.shouldDisplayPrechatForm()) { 78 | console.log("Pre-Chat is enabled. Continuing to render a Pre-Chat form."); 79 | setShowPrechatForm(true); 80 | return; 81 | } 82 | console.log("Pre-Chat is not enabled. Continuing to create a new conversation."); 83 | return handleCreateNewConversation() 84 | .then(() => { 85 | console.log(`Completed initializing a new conversation with conversationId: ${getConversationId()}`); 86 | }) 87 | .catch(err => { 88 | console.error(`${err}`); 89 | }); 90 | }) 91 | .catch(err => { 92 | console.error(`${err}`); 93 | }); 94 | } 95 | 96 | /** 97 | * Handles an existing conversation. 98 | * 99 | * 1. Fetch a Continuation Access Token i.e. Messaging JWT. 100 | * 2. Lists the available conversations and loads the current (also most-recent) conversation that is OPEN. 101 | * 3. Fetch the entries for the current conversation. 102 | * @returns {Promise} 103 | */ 104 | function handleExistingConversation() { 105 | return handleGetContinuityJwt() 106 | .then(() => { 107 | return handleListConversations() 108 | .then(() => { 109 | console.log(`Successfully listed the conversations.`); 110 | handleListConversationEntries() 111 | .then(console.log(`Successfully retrieved entries for the current conversation: ${getConversationId()}`)) 112 | .catch(err => { 113 | console.error(`${err}`); 114 | }); 115 | }) 116 | .catch(err => { 117 | console.error(`${err}`); 118 | }); 119 | }) 120 | .catch(err => { 121 | console.error(`${err}`); 122 | }); 123 | } 124 | 125 | /** 126 | * Handles fetching an Unauthenticated Access Token i.e. Messaging JWT. 127 | * 128 | * 1. If a JWT already exists, simply return. 129 | * 2. Makes a request to Unauthenticated Access Token endpoint. 130 | * 3. Updates the web storage with the latest JWT. 131 | * 4. Performs a cleanup - clears messaging data and closes the Messaging Window, if the request is unsuccessful. 132 | * @returns {Promise} 133 | */ 134 | function handleGetUnauthenticatedJwt() { 135 | if (getJwt()) { 136 | console.warn("Messaging access token (JWT) already exists in the web storage. Discontinuing to create a new Unauthenticated access token."); 137 | return handleExistingConversation().then(Promise.reject()); 138 | } 139 | 140 | return getUnauthenticatedAccessToken() 141 | .then((response) => { 142 | console.log("Successfully fetched an Unauthenticated access token."); 143 | // Parse the response object which includes access-token (JWT), configutation data. 144 | if (typeof response === "object") { 145 | setJwt(response.accessToken); 146 | setItemInWebStorage(STORAGE_KEYS.JWT, response.accessToken); 147 | setLastEventId(response.lastEventId); 148 | setDeploymentConfiguration(response.context && response.context.configuration && response.context.configuration.embeddedServiceConfig); 149 | } 150 | }) 151 | .catch((err) => { 152 | console.error(`Something went wrong in fetching an Unauthenticated access token: ${err && err.message ? err.message : err}`); 153 | handleMessagingErrors(err); 154 | cleanupMessagingData(); 155 | props.showMessagingWindow(false); 156 | throw new Error("Failed to fetch an Unauthenticated access token."); 157 | }); 158 | } 159 | 160 | /** 161 | * Handles creating a new conversation. 162 | * 163 | * 1. If a conversation is already open, simply return. 164 | * 2. Generate a new unique conversation-id and initialize in-memory. 165 | * 3. Makes a request to Create Conversation endpoint. 166 | * 4. Updates the conversation status internally to OPENED, for the associated components to reactively update. 167 | * 5. Performs a cleanup - clears messaging data and closes the Messaging Window, if the request is unsuccessful. 168 | * @returns {Promise} 169 | */ 170 | function handleCreateNewConversation(routingAttributes) { 171 | if (conversationStatus === CONVERSATION_CONSTANTS.ConversationStatus.OPENED_CONVERSATION) { 172 | console.warn("Cannot create a new conversation while a conversation is currently open."); 173 | return Promise.reject(); 174 | } 175 | 176 | // Initialize a new unique conversation-id in-memory. 177 | storeConversationId(util.generateUUID()); 178 | return createConversation(getConversationId(), routingAttributes) 179 | .then(() => { 180 | console.log(`Successfully created a new conversation with conversation-id: ${getConversationId()}`); 181 | updateConversationStatus(CONVERSATION_CONSTANTS.ConversationStatus.OPENED_CONVERSATION); 182 | props.showMessagingWindow(true); 183 | }) 184 | .catch((err) => { 185 | console.error(`Something went wrong in creating a new conversation with conversation-id: ${getConversationId()}: ${err && err.message ? err.message : err}`); 186 | handleMessagingErrors(err); 187 | cleanupMessagingData(); 188 | props.showMessagingWindow(false); 189 | throw new Error("Failed to create a new conversation."); 190 | }); 191 | } 192 | 193 | /** 194 | * Handles fetching a Continuation Access Token i.e. Messaging JWT. 195 | * 196 | * 1. Makes a request to Continuation Access Token endpoint. 197 | * 2. Updates the web storage with the latest JWT. 198 | * 3. Performs a cleanup - clears messaging data and closes the Messaging Window, if the request is unsuccessful. 199 | * @returns {Promise} 200 | */ 201 | function handleGetContinuityJwt() { 202 | return getContinuityJwt() 203 | .then((response) => { 204 | setJwt(response.accessToken); 205 | setItemInWebStorage(STORAGE_KEYS.JWT, response.accessToken); 206 | }) 207 | .catch((err) => { 208 | console.error(`Something went wrong in fetching a Continuation Access Token: ${err && err.message ? err.message : err}`); 209 | handleMessagingErrors(err); 210 | throw new Error("Failed to fetch a Continuation access token."); 211 | }); 212 | } 213 | 214 | /** 215 | * Handles fetching a list of all conversations available. This returns only conversations which are OPEN, unless otherwise specified in the request. 216 | * 217 | * 1. Makes a request to List Conversations endpoint. 218 | * 2. If there are multiple OPEN conversations, loads the conversation with the most-recent start time. 219 | * 3. Performs a cleanup - clears messaging data and closes the Messaging Window, if the request is unsuccessful. 220 | * @returns {Promise} 221 | */ 222 | function handleListConversations() { 223 | return listConversations() 224 | .then((response) => { 225 | if (response && response.openConversationsFound > 0 && response.conversations.length) { 226 | const openConversations = response.conversations; 227 | if (openConversations.length > 1) { 228 | console.warn(`Expected the user to be participating in 1 open conversation but instead found ${openConversations.length}. Loading the conversation with latest startTimestamp.`); 229 | openConversations.sort((conversationA, conversationB) => conversationB.startTimestamp - conversationA.startTimestamp); 230 | } 231 | // Update conversation-id with the one from service. 232 | storeConversationId(openConversations[0].conversationId); 233 | updateConversationStatus(CONVERSATION_CONSTANTS.ConversationStatus.OPENED_CONVERSATION); 234 | props.showMessagingWindow(true); 235 | } else { 236 | // No open conversations found. 237 | cleanupMessagingData(); 238 | props.showMessagingWindow(false); 239 | } 240 | }) 241 | .catch((err) => { 242 | console.error(`Something went wrong in fetching a list of conversations: ${err && err.message ? err.message : err}`); 243 | handleMessagingErrors(err); 244 | throw new Error("Failed to list the conversations."); 245 | }); 246 | } 247 | 248 | /** 249 | * Handles fetching a list of all conversation entries for the current conversation. 250 | * 251 | * 1. Makes a request to List Conversation Entries endpoint. 252 | * 2. Renders the conversation entries based on their Entry Type. 253 | * @returns {Promise} 254 | */ 255 | function handleListConversationEntries() { 256 | return listConversationEntries(getConversationId()) 257 | .then((response) => { 258 | if (Array.isArray(response)) { 259 | response.reverse().forEach(entry => { 260 | const conversationEntry = generateConversationEntryForCurrentConversation(entry); 261 | if (!conversationEntry) { 262 | return; 263 | } 264 | 265 | switch (conversationEntry.entryType) { 266 | case CONVERSATION_CONSTANTS.EntryTypes.CONVERSATION_MESSAGE: 267 | conversationEntry.isEndUserMessage = ConversationEntryUtil.isMessageFromEndUser(conversationEntry); 268 | addConversationEntry(conversationEntry); 269 | break; 270 | case CONVERSATION_CONSTANTS.EntryTypes.PARTICIPANT_CHANGED: 271 | case CONVERSATION_CONSTANTS.EntryTypes.ROUTING_RESULT: 272 | addConversationEntry(conversationEntry); 273 | break; 274 | default: 275 | console.log(`Unrecognized conversation entry type: ${conversationEntry.entryType}.`); 276 | } 277 | }); 278 | } else { 279 | console.error(`Expecting a response of type Array from listConversationEntries but instead received: ${response}`); 280 | } 281 | }) 282 | .catch((err) => { 283 | console.error(`Something went wrong while processing entries from listConversationEntries response: ${err && err.message ? err.message : err}`); 284 | handleMessagingErrors(err); 285 | throw new Error("Failed to list the conversation entries for the current conversation."); 286 | }); 287 | } 288 | 289 | /** 290 | * Handles establishing a connection to the EventSource i.e. SSE. 291 | * Selectively listens to the supported events in the app by adding the corresponding event listeners. 292 | * Note: Update the list of events/event-listeners to add/remove support for the available events. Refer https://developer.salesforce.com/docs/service/messaging-api/references/about/server-sent-events-structure.html 293 | * @returns {Promise} 294 | */ 295 | function handleSubscribeToEventSource() { 296 | return subscribeToEventSource({ 297 | [CONVERSATION_CONSTANTS.EventTypes.CONVERSATION_MESSAGE]: handleConversationMessageServerSentEvent, 298 | [CONVERSATION_CONSTANTS.EventTypes.CONVERSATION_ROUTING_RESULT]: handleRoutingResultServerSentEvent, 299 | [CONVERSATION_CONSTANTS.EventTypes.CONVERSATION_PARTICIPANT_CHANGED]: handleParticipantChangedServerSentEvent, 300 | [CONVERSATION_CONSTANTS.EventTypes.CONVERSATION_TYPING_STARTED_INDICATOR]: handleTypingStartedIndicatorServerSentEvent, 301 | [CONVERSATION_CONSTANTS.EventTypes.CONVERSATION_TYPING_STOPPED_INDICATOR]: handleTypingStoppedIndicatorServerSentEvent, 302 | [CONVERSATION_CONSTANTS.EventTypes.CONVERSATION_DELIVERY_ACKNOWLEDGEMENT]: handleConversationDeliveryAcknowledgementServerSentEvent, 303 | [CONVERSATION_CONSTANTS.EventTypes.CONVERSATION_READ_ACKNOWLEDGEMENT]: handleConversationReadAcknowledgementServerSentEvent, 304 | [CONVERSATION_CONSTANTS.EventTypes.CONVERSATION_CLOSE_CONVERSATION]: handleCloseConversationServerSentEvent 305 | }) 306 | .then(() => { 307 | console.log("Subscribed to the Event Source (SSE)."); 308 | }) 309 | .catch((err) => { 310 | handleMessagingErrors(err); 311 | throw new Error(err); 312 | }); 313 | } 314 | 315 | /** 316 | * Generate a Conversation Entry object from the server sent event. 317 | * 318 | * 1. Create a Conversation Entry object from the parsed event data. 319 | * 2. Return the Conversation Entry if the conversationEntry is for the current conversation and undefined, otherwise. 320 | * @param {object} event - Event data payload from server-sent event. 321 | * @returns {object|undefined} 322 | */ 323 | function generateConversationEntryForCurrentConversation(parsedEventData) { 324 | const conversationEntry = ConversationEntryUtil.createConversationEntry(parsedEventData); 325 | 326 | // Handle server sent events only for the current conversation 327 | if (parsedEventData.conversationId === getConversationId()) { 328 | return conversationEntry; 329 | } 330 | console.log(`Current conversation-id: ${getConversationId()} does not match the conversation-id in server sent event: ${parsedEventData.conversationId}. Ignoring the event.`); 331 | return undefined; 332 | } 333 | 334 | /** 335 | * Adds a Conversation Entry object to the list of conversation entries. Updates the state of the list of conversation entries for the component(s) to be updated in-turn, reactively. 336 | * @param {object} conversationEntry - entry object for the current conversation. 337 | */ 338 | function addConversationEntry(conversationEntry) { 339 | conversationEntries.push(conversationEntry); 340 | setConversationEntries([...conversationEntries]); 341 | } 342 | 343 | /** 344 | * Handle a CONVERSATION_MESSAGE server-sent event. 345 | * 346 | * This includes: 347 | * 1. Parse, populate, and create ConversationEntry object based on its entry type 348 | * NOTE: Skip processing CONVERSATION_MESSAGE if the newly created ConversationEntry is undefined or invalid or not from the current conversation. 349 | * 2. Updates in-memory list of conversation entries and the updated list gets reactively passed on to MessagingBody. 350 | * @param {object} event - Event data payload from server-sent event. 351 | */ 352 | function handleConversationMessageServerSentEvent(event) { 353 | try { 354 | console.log(`Successfully handling conversation message server sent event.`); 355 | // Update in-memory to the latest lastEventId 356 | if (event && event.lastEventId) { 357 | setLastEventId(event.lastEventId); 358 | } 359 | 360 | const parsedEventData = ConversationEntryUtil.parseServerSentEventData(event); 361 | const conversationEntry = generateConversationEntryForCurrentConversation(parsedEventData); 362 | if (!conversationEntry) { 363 | return; 364 | } 365 | 366 | if (ConversationEntryUtil.isMessageFromEndUser(conversationEntry)) { 367 | conversationEntry.isEndUserMessage = true; 368 | 369 | // Since message is echoed back by the server, mark the conversation entry as sent. 370 | conversationEntry.isSent = true; 371 | 372 | console.log(`End user successfully sent a message.`); 373 | } else { 374 | conversationEntry.isEndUserMessage = false; 375 | console.log(`Successfully received a message from ${conversationEntry.actorType}`); 376 | } 377 | 378 | addConversationEntry(conversationEntry); 379 | } catch(err) { 380 | console.error(`Something went wrong in handling conversation message server sent event: ${err}`); 381 | } 382 | } 383 | 384 | /** 385 | * Handle a ROUTING_RESULT server-sent event. 386 | * 387 | * This includes: 388 | * 1. Parse, populate, and create ConversationEntry object based on its entry type. 389 | * NOTE: Skip processing ROUTING_RESULT if the newly created ConversationEntry is undefined or invalid or not from the current conversation. 390 | * 2. Updates in-memory list of conversation entries and the updated list gets reactively passed on to MessagingBody. 391 | * 392 | * NOTE: Update the chat client based on the latest routing result. E.g. if the routing type is transfer, set an internal flag like `isTransferring` to 'true' and use that to show a transferring indicator in the ui. 393 | * @param {object} event - Event data payload from server-sent event. 394 | */ 395 | function handleRoutingResultServerSentEvent(event) { 396 | try { 397 | console.log(`Successfully handling routing result server sent event.`); 398 | // Update in-memory to the latest lastEventId 399 | if (event && event.lastEventId) { 400 | setLastEventId(event.lastEventId); 401 | } 402 | 403 | const parsedEventData = ConversationEntryUtil.parseServerSentEventData(event); 404 | const conversationEntry = generateConversationEntryForCurrentConversation(parsedEventData); 405 | if (!conversationEntry) { 406 | return; 407 | } 408 | 409 | if (conversationEntry.messageType === CONVERSATION_CONSTANTS.RoutingTypes.INITIAL) { 410 | // Render reasonForNotRouting when initial routing fails. 411 | switch (conversationEntry.content.failureType) { 412 | case CONVERSATION_CONSTANTS.RoutingFailureTypes.NO_ERROR: 413 | case CONVERSATION_CONSTANTS.RoutingFailureTypes.SUBMISSION_ERROR: 414 | case CONVERSATION_CONSTANTS.RoutingFailureTypes.ROUTING_ERROR: 415 | case CONVERSATION_CONSTANTS.RoutingFailureTypes.UNKNOWN_ERROR: 416 | addConversationEntry(conversationEntry); 417 | break; 418 | default: 419 | console.error(`Unrecognized initial routing failure type: ${conversationEntry.content.failureType}`); 420 | } 421 | // Handle when a conversation is being transferred. 422 | } else if (conversationEntry.messageType === CONVERSATION_CONSTANTS.RoutingTypes.TRANSFER) { 423 | switch (conversationEntry.content.failureType) { 424 | case CONVERSATION_CONSTANTS.RoutingFailureTypes.NO_ERROR: 425 | // Render transfer timestamp when transfer is requested successfully. 426 | // TODO: Add a transfer state ui update. 427 | addConversationEntry(conversationEntry); 428 | break; 429 | case CONVERSATION_CONSTANTS.RoutingFailureTypes.SUBMISSION_ERROR: 430 | case CONVERSATION_CONSTANTS.RoutingFailureTypes.ROUTING_ERROR: 431 | case CONVERSATION_CONSTANTS.RoutingFailureTypes.UNKNOWN_ERROR: 432 | break; 433 | default: 434 | console.error(`Unrecognized transfer routing failure type: ${conversationEntry.content.failureType}`); 435 | } 436 | } else { 437 | console.error(`Unrecognized routing type: ${conversationEntry.messageType}`); 438 | } 439 | } catch (err) { 440 | console.error(`Something went wrong in handling routing result server sent event: ${err}`); 441 | } 442 | } 443 | 444 | /** 445 | * Handle a PARTICIPANT_CHANGED server-sent event. 446 | * 447 | * This includes: 448 | * 1. Parse, populate, and create ConversationEntry object based on its entry type. 449 | * NOTE: Skip processing PARTICIPANT_CHANGED if the newly created ConversationEntry is undefined or invalid or not from the current conversation. 450 | * 2. Updates in-memory list of conversation entries and the updated list gets reactively passed on to MessagingBody. 451 | * @param {object} event - Event data payload from server-sent event. 452 | */ 453 | function handleParticipantChangedServerSentEvent(event) { 454 | try { 455 | console.log(`Successfully handling participant changed server sent event.`); 456 | // Update in-memory to the latest lastEventId 457 | if (event && event.lastEventId) { 458 | setLastEventId(event.lastEventId); 459 | } 460 | 461 | const parsedEventData = ConversationEntryUtil.parseServerSentEventData(event); 462 | const conversationEntry = generateConversationEntryForCurrentConversation(parsedEventData); 463 | if (!conversationEntry) { 464 | return; 465 | } 466 | addConversationEntry(conversationEntry); 467 | } catch (err) { 468 | console.error(`Something went wrong in handling participant changed server sent event: ${err}`); 469 | } 470 | } 471 | 472 | /** 473 | * Handle a TYPING_STARTED_INDICATOR server-sent event. 474 | * @param {object} event - Event data payload from server-sent event. 475 | */ 476 | function handleTypingStartedIndicatorServerSentEvent(event) { 477 | try { 478 | // Update in-memory to the latest lastEventId 479 | if (event && event.lastEventId) { 480 | setLastEventId(event.lastEventId); 481 | } 482 | 483 | const parsedEventData = ConversationEntryUtil.parseServerSentEventData(event); 484 | // Handle typing indicators only for the current conversation 485 | if (getConversationId() === parsedEventData.conversationId) { 486 | const senderName = ConversationEntryUtil.getSenderDisplayName(parsedEventData) || ConversationEntryUtil.getSenderRole(parsedEventData); 487 | const typingParticipantTimer = currentTypingParticipants[senderName] && currentTypingParticipants[senderName].countdownTimer; 488 | 489 | // If we have received typing indicator from this sender within the past 5 seconds, reset the timer 490 | // Otherwise, start a new timer 491 | if (ConversationEntryUtil.getSenderRole(parsedEventData) !== CONVERSATION_CONSTANTS.ParticipantRoles.ENDUSER) { 492 | console.log(`Successfully handling typing started indicator server sent event.`); 493 | 494 | if (typingParticipantTimer) { 495 | typingParticipantTimer.reset(Date.now()); 496 | } else { 497 | currentTypingParticipants[senderName] = { 498 | countdownTimer: new CountdownTimer(() => { 499 | delete currentTypingParticipants[senderName]; 500 | 501 | if (!Object.keys(currentTypingParticipants).length) { 502 | setIsAnotherParticipantTyping(false); 503 | } 504 | }, CLIENT_CONSTANTS.TYPING_INDICATOR_DISPLAY_TIMEOUT, Date.now()), 505 | role: parsedEventData.conversationEntry.sender.role 506 | }; 507 | 508 | currentTypingParticipants[senderName].countdownTimer.start(); 509 | } 510 | 511 | setIsAnotherParticipantTyping(true); 512 | } 513 | } 514 | } catch (err) { 515 | console.error(`Something went wrong in handling typing started indicator server sent event: ${err}`); 516 | } 517 | } 518 | 519 | /** 520 | * Handle a TYPING_STOPPED_INDICATOR server-sent event. 521 | * @param {object} event - Event data payload from server-sent event. 522 | */ 523 | function handleTypingStoppedIndicatorServerSentEvent(event) { 524 | try { 525 | // Update in-memory to the latest lastEventId 526 | if (event && event.lastEventId) { 527 | setLastEventId(event.lastEventId); 528 | } 529 | 530 | const parsedEventData = ConversationEntryUtil.parseServerSentEventData(event); 531 | // Handle typing indicators only for the current conversation 532 | if (getConversationId() === parsedEventData.conversationId) { 533 | const senderName = ConversationEntryUtil.getSenderDisplayName(parsedEventData) || ConversationEntryUtil.getSenderRole(parsedEventData); 534 | 535 | delete currentTypingParticipants[senderName]; 536 | 537 | if (!Object.keys(currentTypingParticipants).length) { 538 | setIsAnotherParticipantTyping(false); 539 | } 540 | } 541 | } catch (err) { 542 | console.error(`Something went wrong in handling typing stopped indicator server sent event: ${err}`); 543 | } 544 | } 545 | 546 | /** 547 | * Handle a server-sent event CONVERSATION_DELIVERY_ACKNOWLEDGEMENT: 548 | * - Parse server-sent event and update a conversation entry (if it exists) with data from the event. 549 | * - Store delivery acknowledgement timestamp on conversation entry to show receipt timestamp. 550 | * @param {object} event - Event data payload from server-sent event. 551 | */ 552 | function handleConversationDeliveryAcknowledgementServerSentEvent(event) { 553 | try { 554 | console.log(`Successfully handling conversation delivery acknowledgement server sent event.`); 555 | // Update in-memory to the latest lastEventId 556 | if (event && event.lastEventId) { 557 | setLastEventId(event.lastEventId); 558 | } 559 | 560 | const parsedEventData = ConversationEntryUtil.parseServerSentEventData(event); 561 | // Handle delivery acknowledgements only for the current conversation 562 | if (getConversationId() === parsedEventData.conversationId) { 563 | if (parsedEventData.conversationEntry && parsedEventData.conversationEntry.relatedRecords.length && parsedEventData.conversationEntry.entryPayload.length) { 564 | // Store acknowledgement timestamp and identifier of message that was delivered. 565 | const deliveredMessageId = parsedEventData.conversationEntry.relatedRecords[0]; 566 | const deliveryAcknowledgementTimestamp = JSON.parse(parsedEventData.conversationEntry.entryPayload).acknowledgementTimestamp; 567 | 568 | // Make deep copy of conversation entries to find matching messageId. 569 | util.createDeepCopy(conversationEntries).filter((conversationEntry) => { 570 | if (conversationEntry.messageId === deliveredMessageId) { 571 | conversationEntry.isDelivered = true; 572 | conversationEntry.deliveryAcknowledgementTimestamp = deliveryAcknowledgementTimestamp; 573 | } 574 | }); 575 | } 576 | } 577 | } catch (err) { 578 | console.error(`Something went wrong in handling conversation delivery acknowledgement server sent event: ${err}`); 579 | } 580 | } 581 | 582 | /** 583 | * Handle a server-sent event CONVERSATION_READ_ACKNOWLEDGEMENT: 584 | * - Parse server-sent event and update a conversation entry (if it exists) with data from the event. 585 | * - Store read acknowledgement timestamp on conversation entry to show receipt timestamp. 586 | * @param {object} event - Event data payload from server-sent event. 587 | */ 588 | function handleConversationReadAcknowledgementServerSentEvent(event) { 589 | try { 590 | console.log(`Successfully handling conversation read acknowledgement server sent event.`); 591 | // Update in-memory to the latest lastEventId 592 | if (event && event.lastEventId) { 593 | setLastEventId(event.lastEventId); 594 | } 595 | 596 | const parsedEventData = ConversationEntryUtil.parseServerSentEventData(event); 597 | // Handle read acknowledgements only for the current conversation 598 | if (getConversationId() === parsedEventData.conversationId) { 599 | if (parsedEventData.conversationEntry && parsedEventData.conversationEntry.relatedRecords.length && parsedEventData.conversationEntry.entryPayload.length) { 600 | // Store acknowledgement timestamp and identifier of message that was read. 601 | const readMessageId = parsedEventData.conversationEntry.relatedRecords[0]; 602 | const readAcknowledgementTimestamp = JSON.parse(parsedEventData.conversationEntry.entryPayload).acknowledgementTimestamp; 603 | 604 | // Make deep copy of conversation entries to find matching messageId. 605 | util.createDeepCopy(conversationEntries).filter((conversationEntry) => { 606 | if (conversationEntry.messageId === readMessageId) { 607 | conversationEntry.isRead = true; 608 | conversationEntry.readAcknowledgementTimestamp = readAcknowledgementTimestamp; 609 | } 610 | }); 611 | } 612 | } 613 | } catch (err) { 614 | console.error(`Something went wrong in handling conversation read acknowledgement server sent event: ${err}`); 615 | } 616 | } 617 | 618 | /** 619 | * Handle a CONVERSATION_CLOSED server-sent event. 620 | * 621 | * @param {object} event - Event data payload from server-sent event. 622 | */ 623 | function handleCloseConversationServerSentEvent(event) { 624 | try { 625 | console.log(`Successfully handling close conversation server sent event.`); 626 | // Update in-memory to the latest lastEventId 627 | if (event && event.lastEventId) { 628 | setLastEventId(event.lastEventId); 629 | } 630 | 631 | const parsedEventData = ConversationEntryUtil.parseServerSentEventData(event); 632 | 633 | // Do not render conversation ended text if the conversation entry is not for the current conversation. 634 | if (getConversationId() === parsedEventData.conversationId) { 635 | // Update state to conversation closed status. 636 | updateConversationStatus(CONVERSATION_CONSTANTS.ConversationStatus.CLOSED_CONVERSATION); 637 | } 638 | } catch (err) { 639 | console.error(`Something went wrong while handling conversation closed server sent event in conversation ${getConversationId()}: ${err}`); 640 | } 641 | } 642 | 643 | /** 644 | * Handle sending a static text message in a conversation. 645 | * @param {string} conversationId - Identifier of the conversation. 646 | * @param {string} value - Actual text of the message. 647 | * @param {string} messageId - Unique identifier of the message. 648 | * @param {string} inReplyToMessageId - Identifier of another message where this message is being replied to. 649 | * @param {boolean} isNewMessagingSession - Whether it is a new messaging session. 650 | * @param {object} routingAttributes - Pre-Chat fields and its values, if configured. 651 | * @param {object} language - language code. 652 | * 653 | * @returns {Promise} 654 | */ 655 | function handleSendTextMessage(conversationId, value, messageId, inReplyToMessageId, isNewMessagingSession, routingAttributes, language) { 656 | return sendTextMessage(conversationId, value, messageId, inReplyToMessageId, isNewMessagingSession, routingAttributes, language) 657 | .catch((err) => { 658 | console.error(`Something went wrong while sending a message to conversation ${conversationId}: ${err}`); 659 | setFailedMessage(Object.assign({}, {messageId, value, inReplyToMessageId, isNewMessagingSession, routingAttributes, language})); 660 | handleMessagingErrors(err); 661 | }); 662 | } 663 | 664 | /** 665 | * Close messaging window handler for the event from a child component i.e. MessagingHeader. 666 | * When such event is received, invoke the parent's handler to close the messaging window if the conversation status is closed or not yet started. 667 | */ 668 | function endConversation() { 669 | if (conversationStatus === CONVERSATION_CONSTANTS.ConversationStatus.OPENED_CONVERSATION) { 670 | // End the conversation if it is currently opened. 671 | return closeConversation(getConversationId()) 672 | .then(() => { 673 | console.log(`Successfully closed the conversation with conversation-id: ${getConversationId()}`); 674 | }) 675 | .catch((err) => { 676 | console.error(`Something went wrong in closing the conversation with conversation-id ${getConversationId()}: ${err}`); 677 | }) 678 | .finally(() => { 679 | cleanupMessagingData(); 680 | }); 681 | } 682 | } 683 | 684 | /** 685 | * Close messaging window handler for the event from a child component i.e. MessagingHeader. 686 | * When such event is received, invoke the parent's handler to close the messaging window if the conversation status is closed or not yet started. 687 | */ 688 | function closeMessagingWindow() { 689 | if (conversationStatus === CONVERSATION_CONSTANTS.ConversationStatus.CLOSED_CONVERSATION || conversationStatus === CONVERSATION_CONSTANTS.ConversationStatus.NOT_STARTED_CONVERSATION) { 690 | props.showMessagingWindow(false); 691 | } 692 | } 693 | 694 | /** 695 | * Performs a cleanup in the app. 696 | * 1. Closes the EventSource connection. 697 | * 2. Clears the web storage. 698 | * 3. Clears the in-memory messaging data. 699 | * 4. Update the internal conversation status to CLOSED. 700 | */ 701 | function cleanupMessagingData() { 702 | closeEventSource() 703 | .then(console.log("Closed the Event Source (SSE).")) 704 | .catch((err) => { 705 | console.error(`Something went wrong in closing the Event Source (SSE): ${err}`); 706 | }); 707 | 708 | clearWebStorage(); 709 | clearInMemoryData(); 710 | 711 | // Update state to conversation closed status. 712 | updateConversationStatus(CONVERSATION_CONSTANTS.ConversationStatus.CLOSED_CONVERSATION); 713 | } 714 | 715 | /** 716 | * Handles the errors from messaging endpoint requests. 717 | * If a request is failed due to an Unauthorized error (i.e. 401), peforms a cleanup and resets the app and console logs otherwise. 718 | */ 719 | function handleMessagingErrors(err) { 720 | if (typeof err === "object") { 721 | if (err.status) { 722 | switch (err.status) { 723 | case 401: 724 | console.error(`Unauthenticated request: ${err.message}`); 725 | cleanupMessagingData(); 726 | props.showMessagingWindow(false); 727 | break; 728 | case 400: 729 | console.error(`Invalid request parameters. Please check your data before retrying: ${err.message}`); 730 | break; 731 | case 404: 732 | console.error(`Resource not found. Please check your data before retrying: ${err.message}`); 733 | break; 734 | case 429: 735 | console.warn(`Too many requests issued from the app. Try again in sometime: ${err.message}`); 736 | break; 737 | /** 738 | * HTTP error code returned by the API(s) when a message is sent and failed because no messaging session exists. This error indicates client 739 | * to surface the Pre-Chat form if configured to show for every new messaging session and then retry the failed message with routingAttributes. 740 | */ 741 | case 417: 742 | if (prechatUtil.shouldDisplayPrechatForm() && prechatUtil.shouldDisplayPrechatEveryMessagingSession()) { 743 | console.log("Pre-Chat configured to show for every new messaging session. Continuing to display the Pre-Chat form."); 744 | setShowPrechatForm(true); 745 | } 746 | case 500: 747 | console.error(`Something went wrong in the request, please try again: ${err.message}`); 748 | break; 749 | default: 750 | console.error(`Unhandled/Unknown http error: ${err}`); 751 | cleanupMessagingData(); 752 | props.showMessagingWindow(false); 753 | } 754 | return; 755 | } 756 | console.error(`Something went wrong: ${err && err.message ? err.message : err}`); 757 | } 758 | return; 759 | } 760 | 761 | /** 762 | * Handles submitting a Pre-Chat form to either start a new conversation or a new messaging session. 763 | * @param {object} prechatData - Object containing key-value pairs of Pre-Chat form fields and their corresponding values. 764 | */ 765 | function handlePrechatSubmit(prechatData) { 766 | let prechatSubmitPromise; 767 | console.log(`Pre-Chat fields values on submit: ${prechatData}.`); 768 | 769 | // If there is a failed message being tracked while submitting Pre-Chat form, consider it is an existing conversation but a new messaging session. Resend the failed message with the routing attributes to begin a new messaging session. 770 | if (failedMessage) { 771 | prechatSubmitPromise = handleSendTextMessage(getConversationId(), failedMessage.value, failedMessage.messageId, failedMessage.inReplyToMessageId, true, prechatData, failedMessage.language); 772 | } else { 773 | // If there is no failed message being tracked while submitting Pre-Chat form, create a new conversation. 774 | prechatSubmitPromise = handleCreateNewConversation(prechatData); 775 | } 776 | prechatSubmitPromise 777 | .then(() => { 778 | setShowPrechatForm(false); 779 | }); 780 | } 781 | 782 | return ( 783 | <> 784 | 788 | {!showPrechatForm && 789 | <> 790 | 795 | 799 | 800 | } 801 | { 802 | showPrechatForm && 803 | 804 | } 805 | 806 | ); 807 | } -------------------------------------------------------------------------------- /src/components/conversationEntry.css: -------------------------------------------------------------------------------- 1 | .conversationEntryContainer { 2 | position: relative; 3 | overflow: auto; 4 | margin-top: 10px; 5 | margin-bottom: 10px; 6 | } -------------------------------------------------------------------------------- /src/components/conversationEntry.js: -------------------------------------------------------------------------------- 1 | import "./conversationEntry.css"; 2 | import * as ConversationEntryUtil from "../helpers/conversationEntryUtil"; 3 | 4 | // Import children components to plug in and render. 5 | import TextMessage from "./textMessage"; 6 | import ParticipantChange from "./participantChange"; 7 | 8 | export default function ConversationEntry({conversationEntry}) { 9 | 10 | return ( 11 | <> 12 |
13 | {/* Render component for a conversation entry of type Text Message. */} 14 | {ConversationEntryUtil.isTextMessage(conversationEntry) && } 15 | {/* Render component for a conversation entry of type Participant Change. */} 16 | {ConversationEntryUtil.isParticipantChangeEvent(conversationEntry) && } 17 |
18 | 19 | ); 20 | } -------------------------------------------------------------------------------- /src/components/messagingBody.css: -------------------------------------------------------------------------------- 1 | .messagingBody { 2 | height: 500px; 3 | border-bottom: 1px solid #ddd; 4 | overflow: auto; 5 | background: whitesmoke; 6 | border-radius: inherit; 7 | font-family: inherit; 8 | } 9 | 10 | .conversationStartTimeText, .conversationEndTimeText { 11 | color: rgb(98 91 91); 12 | margin-top: 10px; 13 | font-size: 12px; 14 | font-weight: bold; 15 | text-align: center; 16 | } 17 | 18 | .conversationEntriesListView { 19 | height: auto; 20 | padding: 5px 10px; 21 | margin-top: 0; 22 | margin-bottom: 0; 23 | } -------------------------------------------------------------------------------- /src/components/messagingBody.js: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import "./messagingBody.css"; 3 | import { util } from "../helpers/common"; 4 | import { CONVERSATION_CONSTANTS } from "../helpers/constants"; 5 | 6 | // Import children components to plug in and render. 7 | import ConversationEntry from "./conversationEntry"; 8 | import TypingIndicator from "./typingIndicator"; 9 | 10 | export default function MessagingBody({ conversationEntries, conversationStatus, typingParticipants, showTypingIndicator }) { 11 | 12 | useEffect(() => { 13 | if (conversationStatus === CONVERSATION_CONSTANTS.ConversationStatus.CLOSED_CONVERSATION) { 14 | // Render conversation closed message. 15 | 16 | // Remove typing indicator. 17 | typingParticipants = []; 18 | showTypingIndicator = false; 19 | } 20 | }, [conversationStatus]); 21 | 22 | /** 23 | * Builds a list of conversation entries where each conversation-entry represents an object of type defined in constants#CONVERSATION_CONSTANTS.EntryTypes. 24 | * @returns {string} 25 | */ 26 | const conversationEntriesListView = conversationEntries.map(conversationEntry => 27 | 30 | ); 31 | 32 | /** 33 | * Generates a text with conversation start date and time. 34 | * @returns {string} 35 | */ 36 | function generateConversationStartTimeText() { 37 | if (conversationEntries.length) { 38 | const conversationStartTimestamp = conversationEntries[0].transcriptedTimestamp; 39 | const startDate = util.getFormattedDate(conversationStartTimestamp); 40 | const startTime = util.getFormattedTime(conversationStartTimestamp); 41 | const conversationStartTimeText = `Conversation started: ${startDate} at ${startTime}`; 42 | return conversationStartTimeText; 43 | } 44 | return ""; 45 | } 46 | 47 | /** 48 | * Generates a text with conversation end date and time. 49 | * @returns {string} 50 | */ 51 | function generateConversationEndTimeText() { 52 | const conversationEndTimestamp = Date.now(); 53 | const endDate = util.getFormattedDate(conversationEndTimestamp); 54 | const endTime = util.getFormattedTime(conversationEndTimestamp); 55 | const conversationEndTimeText = `Conversation ended: ${endDate} at ${endTime}`; 56 | 57 | return conversationEndTimeText; 58 | } 59 | 60 | return ( 61 |
62 | {conversationEntries.length > 0 &&

{generateConversationStartTimeText()}

} 63 |
    64 | {conversationEntriesListView} 65 |
66 | {showTypingIndicator && } 67 | {conversationStatus === CONVERSATION_CONSTANTS.ConversationStatus.CLOSED_CONVERSATION &&

{generateConversationEndTimeText()}

} 68 |
69 | ); 70 | } -------------------------------------------------------------------------------- /src/components/messagingButton.css: -------------------------------------------------------------------------------- 1 | .messagingButton { 2 | width: 10rem; 3 | background-color: #3fc56e; 4 | color: white; 5 | padding: 15px 30px; 6 | border-radius: 5px; 7 | outline: 0; 8 | border: 0; 9 | margin: 10px 0px; 10 | cursor: pointer; 11 | box-shadow: 0px 2px 2px lightgray; 12 | transition: ease background-color 250ms; 13 | font-weight: bold; 14 | position: relative; 15 | align-self: center; 16 | top: 150px; 17 | } 18 | 19 | .messagingButton:hover:not(:disabled) { 20 | background-color: #2d8645; 21 | } 22 | 23 | .messagingButton:disabled { 24 | cursor: not-allowed; 25 | opacity: 0.7; 26 | } 27 | 28 | /** 29 | * Styles related to the spinner shown on Messaging Button 30 | */ 31 | .messagingButtonLoadingUI { 32 | --color: white; 33 | --size-mid: 4vmin; 34 | --size-dot: 1.5vmin; 35 | --size-bar: 0.4vmin; 36 | --size-square: 3vmin; 37 | position: relative; 38 | width: 50%; 39 | display: block; 40 | bottom: 30px; 41 | } 42 | 43 | .messagingButtonLoadingUI::before, 44 | .messagingButtonLoadingUI::after { 45 | content: ''; 46 | box-sizing: border-box; 47 | position: absolute; 48 | } 49 | 50 | .messagingButtonLoadingUI.loadingBalls::before { 51 | width: var(--size-mid); 52 | height: var(--size-mid); 53 | border: 4px solid white; 54 | border-top-color: transparent; 55 | border-radius: 50%; 56 | animation: loadingAnimation 1s linear infinite; 57 | } 58 | 59 | .messagingButtonLoadingUI.loadingBalls::after { 60 | width: calc(var(--size-mid) - 2px); 61 | height: calc(var(--size-mid) - 2px); 62 | border: 2px solid transparent; 63 | border-top-color: white; 64 | border-radius: 50%; 65 | animation: loadingAnimation 0.6s linear reverse infinite; 66 | } 67 | 68 | @keyframes loadingAnimation { 69 | 100% { 70 | transform: rotate(1turn); 71 | } 72 | } -------------------------------------------------------------------------------- /src/components/messagingButton.js: -------------------------------------------------------------------------------- 1 | import './messagingButton.css'; 2 | 3 | export default function MessagingButton(props) { 4 | 5 | return ( 6 | <> 7 | 14 | 15 | ); 16 | } -------------------------------------------------------------------------------- /src/components/messagingHeader.css: -------------------------------------------------------------------------------- 1 | .messagingHeader { 2 | width: inherit; 3 | height: 50px; 4 | background-color: rgb(98 91 91); 5 | border-radius: 15px 15px 0px 0px; 6 | font-family: inherit; 7 | } 8 | 9 | .messagingHeaderCloseButton { 10 | float: right; 11 | margin-right: 20px; 12 | height: 50px; 13 | width: max-content; 14 | background: transparent; 15 | border: none; 16 | pointer-events: all; 17 | cursor: pointer; 18 | } 19 | 20 | .messagingHeaderCloseButtonIcon { 21 | transform: scale(2); 22 | fill: whitesmoke; 23 | } 24 | 25 | .messagingHeaderCloseButtonIcon:hover{ 26 | opacity: 0.5; 27 | transform: scale(2); 28 | } -------------------------------------------------------------------------------- /src/components/messagingHeader.js: -------------------------------------------------------------------------------- 1 | import "./messagingHeader.css"; 2 | import { FaCircleXmark } from "react-icons/fa6"; 3 | 4 | import { CONVERSATION_CONSTANTS } from "../helpers/constants"; 5 | 6 | export default function MessagingHeader(props) { 7 | /** 8 | * Handle Close ('X') button click based on the current conversation status. 9 | * If the conversation is open, invoke the parent's handlers to end the current conversation. 10 | * If the conversation is either closed or not yet started, invoke the parent's handler to close the messaging window. 11 | * @param {object} evt - click event from the Close ('X') button 12 | */ 13 | function handleCloseButtonClick(evt) { 14 | if (evt) { 15 | if (props.conversationStatus === CONVERSATION_CONSTANTS.ConversationStatus.OPENED_CONVERSATION) { 16 | // End the conversation if it is currently opened. 17 | props.endConversation(); 18 | } else if (props.conversationStatus === CONVERSATION_CONSTANTS.ConversationStatus.CLOSED_CONVERSATION || props.conversationStatus === CONVERSATION_CONSTANTS.ConversationStatus.NOT_STARTED_CONVERSATION) { 19 | // Close the messaging window if the conversation is in closed state or not yet started. 20 | props.closeMessagingWindow(); 21 | } 22 | } 23 | } 24 | 25 | /** 26 | * Generates a title text for the header Close ('X') button based on the current conversation status. 27 | * @returns {string} 28 | */ 29 | function generateCloseButtonTitle() { 30 | return `${props.conversationStatus === CONVERSATION_CONSTANTS.ConversationStatus.OPENED_CONVERSATION ? `End conversation` : `Close window`}`; 31 | } 32 | 33 | return ( 34 |
35 | 41 |
42 | ) 43 | } -------------------------------------------------------------------------------- /src/components/messagingInputFooter.css: -------------------------------------------------------------------------------- 1 | .messagingFooter { 2 | width: 100%; 3 | display: flex; 4 | height: 50px; 5 | font-family: inherit; 6 | border-radius: inherit; 7 | background-color: whitesmoke; 8 | } 9 | 10 | .messagingFooter textarea { 11 | width: inherit; 12 | align-content: center; 13 | font-family: inherit; 14 | border-radius: 15px; 15 | margin: 5px; 16 | resize: none; 17 | } 18 | 19 | .sendButton { 20 | background: transparent; 21 | border: 0; 22 | } 23 | 24 | .sendButtonIcon{ 25 | transform: scale(2); 26 | fill: rgb(98 91 91); 27 | } 28 | 29 | .sendButton:hover:not(:disabled){ 30 | opacity: 0.7; 31 | cursor: pointer; 32 | } 33 | 34 | .sendButton:disabled { 35 | opacity: 0.3; 36 | } 37 | 38 | .messagingInputTextarea:disabled, .sendButton:disabled { 39 | cursor: not-allowed; 40 | } -------------------------------------------------------------------------------- /src/components/messagingInputFooter.js: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | import "./messagingInputFooter.css"; 4 | import { VscSend } from "react-icons/vsc"; 5 | import CountdownTimer from "../helpers/countdownTimer"; 6 | import { util } from "../helpers/common"; 7 | import { CONVERSATION_CONSTANTS, CLIENT_CONSTANTS } from '../helpers/constants'; 8 | import { getConversationId } from '../services/dataProvider'; 9 | 10 | export default function MessagingInputFooter(props) { 11 | // Initialize the Textarea value to empty. 12 | let [textareaContent, setTextareaContent] = useState(''); 13 | 14 | // Initialize whether end user is actively typing. 15 | // This holds a reference to a CountdownTimer object. 16 | let [typingIndicatorTimer, setTypingIndicatorTimer] = useState(undefined); 17 | 18 | /** 19 | * Handle 'change' event in Textarea to reactively update the Textarea value. 20 | * @param {object} event 21 | */ 22 | function handleTextareaContentChange(event) { 23 | if (event && event.target && typeof event.target.value === "string") { 24 | setTextareaContent(event.target.value); 25 | } 26 | 27 | // Handle when end user is actively typing. 28 | if (textareaContent !== "" && textareaContent.length !== 0) { 29 | // If the end user has typed within the last 5 seconds, reset the timer for another 5 seconds. 30 | // Otherwise, send a new started typing indicator and start a new countdown. 31 | if (typingIndicatorTimer) { 32 | typingIndicatorTimer.reset(Date.now()); 33 | } else { 34 | handleSendTypingIndicator(CONVERSATION_CONSTANTS.EntryTypes.TYPING_STARTED_INDICATOR); 35 | typingIndicatorTimer = new CountdownTimer(() => { 36 | handleSendTypingIndicator(CONVERSATION_CONSTANTS.EntryTypes.TYPING_STOPPED_INDICATOR); 37 | typingIndicatorTimer = undefined; 38 | }, CLIENT_CONSTANTS.TYPING_INDICATOR_DISPLAY_TIMEOUT, Date.now()); 39 | typingIndicatorTimer.start(); 40 | } 41 | } 42 | } 43 | 44 | /** 45 | * Handle 'key' event in Textarea. If the key is 'Enter', send a message. 46 | * @param {object} event 47 | */ 48 | function handleTextareaKeyChange(event) { 49 | if (event.key === "Enter" && !event.altKey) { 50 | event.preventDefault(); 51 | 52 | handleSendButtonClick(); 53 | } 54 | } 55 | 56 | /** 57 | * Handle 'click' event in Textarea to put focus on Textarea. 58 | * @param {object} event 59 | */ 60 | function handleTextareaClick(event) { 61 | if (event) { 62 | event.target.focus(); 63 | } 64 | } 65 | 66 | /** 67 | * Clears the Textarea i.e. resets to empty. 68 | */ 69 | function clearMessageContent() { 70 | setTextareaContent(""); 71 | } 72 | 73 | /** 74 | * Determines whether to disable the Textarea. 75 | * TRUE - disables if the conversation is either closed or not started and FALSE - otherwise. 76 | */ 77 | function shouldDisableTextarea() { 78 | return props.conversationStatus === CONVERSATION_CONSTANTS.ConversationStatus.CLOSED_CONVERSATION || props.conversationStatus === CONVERSATION_CONSTANTS.ConversationStatus.NOT_STARTED_CONVERSATION; 79 | } 80 | 81 | /** 82 | * Determines whether to disable the Send Button. 83 | * TRUE - disables if the Textarea is either empty or if the conversation is not open and FALSE - otherwise. 84 | */ 85 | function shouldDisableSendButton() { 86 | return textareaContent.trim().length === 0 || props.conversationStatus !== CONVERSATION_CONSTANTS.ConversationStatus.OPENED_CONVERSATION; 87 | } 88 | 89 | /** 90 | * Handle Send Button click. If the Button is enabled, send a message. 91 | */ 92 | function handleSendButtonClick() { 93 | if (!shouldDisableSendButton()) { 94 | handleSendMessage(); 95 | } 96 | } 97 | 98 | /** 99 | * Handle sending a text message by generating a new unique message-id and invoke the parent's handler to send a message with the typed text. 100 | */ 101 | function handleSendMessage() { 102 | // Required parameters. 103 | const conversationId = getConversationId(); 104 | const messageId = util.generateUUID(); 105 | const value = textareaContent; 106 | // Optional parameters. 107 | let inReplyToMessageId; 108 | let isNewMessagingSession; 109 | let routingAttributes; 110 | let language; 111 | 112 | props.sendTextMessage(conversationId, value, messageId, inReplyToMessageId, isNewMessagingSession, routingAttributes, language) 113 | .then(() => { 114 | console.log(`Successfully sent a text message to conversation: ${conversationId}`); 115 | // Clear textarea value. 116 | clearMessageContent(); 117 | }); 118 | } 119 | 120 | /** 121 | * Handle calling a sendTypingIndicator when the timer fires with started/stopped indicator. 122 | * @param {string} typingIndicator - whether to send a typing started or stopped indicator. 123 | */ 124 | function handleSendTypingIndicator(typingIndicator) { 125 | const conversationId = getConversationId(); 126 | 127 | props.sendTypingIndicator(conversationId, typingIndicator) 128 | .then(() => { 129 | console.log(`Successfully sent ${typingIndicator} to conversation: ${conversationId}`); 130 | }) 131 | .catch(error => { 132 | console.error(`Something went wrong while sending a typing indicator to conversation ${conversationId}: ${error}`); 133 | }); 134 | } 135 | 136 | return( 137 |
138 |