├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ └── npmpublish.yml ├── .gitignore ├── .npmignore ├── README.md ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── manifest.json └── robots.txt ├── src ├── assets │ ├── loader.svg │ ├── logo.png │ ├── texts.ts │ └── translationLanguages.ts ├── components │ ├── FlexChat.tsx │ ├── FlexChatLoading.tsx │ ├── MessageBubbleHeader.tsx │ ├── NotificationButton.tsx │ ├── TranslationBubbleBody.tsx │ └── TranslationInfoHeader.tsx ├── config │ ├── chat │ │ ├── chatAppConfig.ts │ │ ├── chatTheme.ts │ │ ├── customActions.ts │ │ └── preEngagementForm.ts │ └── theme │ │ └── theme.ts ├── hooks │ ├── useChatActions.tsx │ └── useTranslation.tsx ├── index.tsx ├── interfaces │ ├── FlexChat.ts │ └── Translate.ts ├── react-app-env.d.ts └── styles │ ├── FlexChatError.css │ ├── FlexChatLoading.css │ ├── TranslationBubbleBody.css │ ├── header.css │ └── root.css ├── tsconfig.json └── tsconfig.types.json /.eslintignore: -------------------------------------------------------------------------------- 1 | # don't ever lint node_modules 2 | node_modules 3 | # don't lint build output (make sure it's set to your correct build folder name) 4 | dist 5 | es 6 | types 7 | bundle 8 | # don't lint nyc coverage output 9 | coverage 10 | src/serviceWorker.ts -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: '@typescript-eslint/parser', 4 | plugins: ['@typescript-eslint'], 5 | extends: [ 6 | 'eslint:recommended', 7 | 'plugin:@typescript-eslint/eslint-recommended', 8 | 'plugin:@typescript-eslint/recommended', 9 | 'plugin:react/recommended' 10 | ], 11 | parserOptions: { 12 | ecmaVersion: 2018, // Allows for the parsing of modern ECMAScript features 13 | sourceType: 'module', // Allows for the use of imports 14 | }, 15 | rules: { 16 | '@typescript-eslint/ban-ts-comment': 'off', 17 | '@typescript-eslint/explicit-function-return-type': 'off', 18 | 'react/prop-types': 'off' 19 | }, 20 | settings: { 21 | react: { 22 | version: 'detect', 23 | }, 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /.github/workflows/npmpublish.yml: -------------------------------------------------------------------------------- 1 | name: Build and deploy package to npm 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | defaults: 9 | run: 10 | shell: bash 11 | 12 | jobs: 13 | npm-publish: 14 | runs-on: ubuntu-latest 15 | steps: 16 | # Default git setup 17 | - uses: actions/checkout@v2 18 | 19 | # Setup .npmrc file to publish to npm 20 | - uses: actions/setup-node@v1 21 | with: 22 | node-version: "12.x" 23 | registry-url: "https://registry.npmjs.org" 24 | 25 | # Create .npmrc file with token from secret 26 | - uses: filipstefansson/set-npm-token-action@v1 27 | with: 28 | token: ${{ secrets.NPM_TOKEN }} 29 | 30 | # Install, build and publish 31 | - run: npm install 32 | - run: npm run build 33 | - run: npm publish -------------------------------------------------------------------------------- /.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 | /dist 14 | /es 15 | /types 16 | /bundle 17 | 18 | # misc 19 | .DS_Store 20 | .env 21 | .env.local 22 | .env.development.local 23 | .env.test.local 24 | .env.production.local 25 | 26 | npm-debug.log* 27 | yarn-debug.log* 28 | yarn-error.log* 29 | 30 | # VS Code setup 31 | .vscode/ 32 | /src/devSetup.tsx 33 | 34 | .eslintcache -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | Dockerfile 2 | .gitlab-ci.yml 3 | .prettierrc.js 4 | .eslint.js 5 | .env 6 | .eslintignore 7 | .gitlab-ci.yml 8 | src/ 9 | .github/ 10 | 11 | 12 | # rest from .gitignore 13 | build/ 14 | .cache/ 15 | **/cypress/videos 16 | **/cypress/screenshots 17 | .next/ 18 | styleguide_build/ 19 | 20 | # Compiled source # 21 | ################### 22 | *.com 23 | *.class 24 | *.dll 25 | *.exe 26 | *.o 27 | *.so 28 | 29 | # Packages # 30 | ############ 31 | # it's better to unpack these files and commit the raw source 32 | # git has its own built in compression methods 33 | *.7z 34 | *.dmg 35 | *.gz 36 | *.iso 37 | *.jar 38 | *.rar 39 | *.tar 40 | *.zip 41 | 42 | # Logs and databases # 43 | ###################### 44 | *.log 45 | *.sql 46 | *.sqlite 47 | 48 | # OS generated files # 49 | ###################### 50 | .DS_Store 51 | .DS_Store? 52 | ._* 53 | .Spotlight-V100 54 | .Trashes 55 | ehthumbs.db 56 | Thumbs.db 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Intility Chat 2 | 3 | ![Build and deploy package to NPM](https://github.com/Intility/IntilityFlexChat/workflows/Build%20and%20deploy%20package%20to%20npm/badge.svg) 4 | 5 | Intility Chat is a React Component that wraps [Twilio's WebChat UI](https://www.npmjs.com/package/@twilio/flex-webchat-ui) with basic configuration and theme. 6 | 7 | ## Purpose 8 | 9 | 10 | Intility Chat makes it easy for third parties to implement a customized version of Flex WebChat UI that is tailored to fit our design guidelines and then deliver an consistent user experience to the end user. 11 | 12 | ## Intended consumers 13 | 14 | 15 | This component is intended for customer facing web portals that isn't developed by Intility, but we use in production. 16 | 17 | ## Main technologies 18 | 19 | 20 | This project utilizes among others the following libraries: 21 | 22 | * [React](https://reactjs.org/) 23 | * [TypeScript](https://www.typescriptlang.org/) 24 | * [Babel](https://babeljs.io/) 25 | * [@twilio/flex-webchat-ui](https://www.npmjs.com/package/@twilio/flex-webchat-ui) 26 | * have a look in [package.json](package.json) for complete list of dependencies 27 | 28 | ## Getting Started 29 | 30 | ### Installation 31 | 32 | #### NPM 33 | 34 | ```bash 35 | npm i @intility/flex-chat 36 | ``` 37 | 38 | ### Required Configuration 39 | 40 | #### Environment variables 41 | 42 | **NOTE:** If you haven't got these keys, please contact your provided Intility Contact. 43 | 44 | ```env 45 | REACT_APP_ACCOUNT_SID=xxxxx 46 | REACT_APP_FLOW_SID=xxxxx 47 | ``` 48 | 49 | #### Properties 50 | 51 | ```ts 52 | type ConfigProps = { 53 | flexFlowSid: string; 54 | flexAccountSid: string; 55 | user: ChatContext; // User context 56 | loglevel?: 'debug' | 'superDebug'; 57 | closeOnInit?: boolean; 58 | theme?: ThemeConfig; 59 | preEngagementFormMessage?: MultiLangText; 60 | user: ChatContext; 61 | }; 62 | 63 | type FlexChatProps = { 64 | config: ConfigProps; 65 | isDarkMode: boolean; 66 | isDisabled?: boolean; 67 | }; 68 | ``` 69 | 70 | **NOTE:** The properties marked with `?` is optional. 71 | 72 | #### Authentication 73 | 74 | Since this package initializes an chat session with an Intility Support Agent there is a requirement to provide a authenticated users details in the config object. 75 | 76 | **NOTE:** This components does not give other requirements to authentication implementations but to provide the components with these values. 77 | 78 | ```ts 79 | type AllowedValues = string | number | boolean | undefined | null | string[] | number[] | boolean[] | object; 80 | 81 | type ChatContext = { 82 | // Value to be displayed as the sender in the chat. 83 | // This should be the authenitcated users full name if available, but fallback to the users login EMail. 84 | userPrincipalName: string; 85 | 86 | // The email used for login for the user 87 | mail: string; 88 | 89 | // Telephone number for the user 90 | mobilePhone: string; 91 | 92 | // The preferred language for the user. Should follow ISO 639-1 Code 93 | preferredLanguage: string; 94 | 95 | // Other user context is welcome and can be sent as other optional properties. 96 | [key: string]: AllowedValues; 97 | }; 98 | ``` 99 | 100 | ### Example Configuration 101 | 102 | ```ts 103 | import React, { useState } from 'react'; 104 | import ReactDOM from 'react-dom'; 105 | import { FlexChat, ConfigProps } from '@intility/flex-chat/dist/index'; 106 | 107 | const App: React.FC = (props) => { 108 | const [isDarkMode, setIsDarkMode] = useState(false); 109 | 110 | const config: ConfigProps = { 111 | flexAccountSid: process.env.REACT_APP_ACCOUNT_SID, 112 | flexFlowSid: process.env.REACT_APP_FLOW_SID, 113 | user: { 114 | userPrincipalName: 'ola.normann@intility.no', 115 | mail: 'ola.normann@intility.no', 116 | mobilePhone: '815-493-00', 117 | preferredLanguage: 'nb-NO' 118 | }, 119 | }; 120 | 121 | return ( 122 | 123 | ); 124 | }; 125 | 126 | ReactDOM.render(, document.getElementById('root')); 127 | ``` 128 | 129 | ### Optional Configuration 130 | 131 | #### Keep the chat closed on page load 132 | 133 | If you want to have the Chat component closed on initialization you can set the property named `closeOnInit` to `true` in the config object. 134 | Default behavior is for the chat component to expand on page load. 135 | 136 | **NOTE:** If there is an ongoing chat session the chat window will always expand on page load. 137 | 138 | ```ts 139 | const config: ConfigProps = { 140 | // ...required config 141 | closeOnInit: true 142 | }; 143 | ``` 144 | 145 | #### PreEngagementFormMessage 146 | 147 | If you want to add a message to the start page you can add a property named `preEngagementFormMessage` to the config object. 148 | 149 | This object is of type `MultiLangText` and consists of an English (en) and Norwegian (no). 150 | 151 | ```ts 152 | const config: ConfigProps = { 153 | // ...required config 154 | preEngagementFormMessage: { 155 | en: 'Du kan nå teknikere på ansvarlig avdeling innenfor tidspunktene 08:00 - 16:00 (CET/CEST)', 156 | no: 'You can reach technicians in the responsible department within the hours 08:00 - 16:00 (CET / CEST)'; 157 | }, 158 | }; 159 | ``` 160 | 161 | #### Theming 162 | 163 | You can change CSS properties on the `MainContainer`, `EntryPoint` and `CloseButton` components to make them fit your experience, e.g. hide the `EntryPoint` button or change the height, width or render properties like position. 164 | 165 | * MainContainer: The expanded chat box. 166 | * EntryPont: Toggle button in the bottom right corner. 167 | * CloseButton: Toggle button on the top bar in the `MainContainer` 168 | 169 | **NOTE:** some properties will be default from Twilio or overwritten by Intility's setup of the chat component. 170 | 171 | ```ts 172 | type ThemeConfig = { 173 | MainContainer?: CSSProps; 174 | EntryPoint?: CSSProps; 175 | CloseButton?: CSSProps; 176 | }; 177 | ``` 178 | 179 | ```ts 180 | const config: ConfigProps = { 181 | // ...required config 182 | theme: { 183 | MainContainer: { 184 | width: '800px', 185 | height: '87vh', 186 | '@media only screen and (min-width: 1415px)': { 187 | width: `1080px`, 188 | }, 189 | }, 190 | // Display EntryPoint for small devices (i.e. Smart phones) 191 | EntryPoint: { 192 | '@media only screen and (min-width: 767px)': { 193 | display: 'none !important', 194 | }, 195 | }, 196 | // Display CloseButton for small devices (i.e. Smart phones) 197 | CloseButton: { 198 | '@media only screen and (min-width: 767px)': { 199 | display: 'none', 200 | }, 201 | }, 202 | }, 203 | }; 204 | ``` 205 | 206 | ## Usage 207 | 208 | ### Hooks 209 | 210 | This component can be interacted with using integrated `useChatActions` hook. 211 | 212 | ```ts 213 | type UseChatActionsFuncs = { 214 | setInputFieldContent: (body: string) => Promise; 215 | setAndSendInputFieldContent: (body: string) => Promise; 216 | toggleChatVisibility: () => Promise; 217 | hasUserReadLastMessage: () => Promise; 218 | isChatOpen: () => Promise; 219 | }; 220 | ``` 221 | 222 | ## Illustration 223 | 224 | ![Successful chat setup](https://i.imgur.com/pMNk5mL.png) 225 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@intility/flex-chat", 3 | "description": "Wrapped component with basic setup and theming based on Twilios WebChat Component", 4 | "version": "1.3.11", 5 | "license": "MIT", 6 | "deprecated": false, 7 | "author": { 8 | "name": "Christian Marker / Intility AS", 9 | "email": "christian.marker@intility.no" 10 | }, 11 | "contributors": [ 12 | { 13 | "name": "Eva Maria Dahlø / Intility AS", 14 | "email": "eva.maria.dahlo@intility.no" 15 | } 16 | ], 17 | "repository": { 18 | "url": "https://github.com/Intility/IntilityFlexChat" 19 | }, 20 | "bundle": "bundle/bundle.min.js", 21 | "main": "dist/index.js", 22 | "module": "es/index.js", 23 | "types": "types/src/index.d.ts", 24 | "scripts": { 25 | "start": "react-scripts start", 26 | "build": "run-s lint clean:build build:cjs build:es build:types", 27 | "build:watch": "babel src --out-dir dist --extensions \".ts,.tsx\" --watch --copy-files", 28 | "build:compile": "babel src --out-dir dist --extensions \".ts,.tsx\" --copy-files", 29 | "build:types": "tsc --project tsconfig.types.json", 30 | "build:cjs": "babel src --out-dir dist --extensions \".ts,.tsx\" --copy-files", 31 | "build:es": "babel src --out-dir es --extensions \".ts,.tsx\" --copy-files --env-name es", 32 | "clean:build": "rimraf dist es types bundle", 33 | "lint": "tsc && eslint src/**/*.ts[x]", 34 | "lint:fix": "eslint src/**/*.ts[x] --fix", 35 | "test": "react-scripts test", 36 | "eject": "react-scripts eject" 37 | }, 38 | "dependencies": { 39 | "@fortawesome/fontawesome-svg-core": "1.2.35", 40 | "@fortawesome/free-solid-svg-icons": "5.15.3", 41 | "@fortawesome/react-fontawesome": "0.1.14", 42 | "@twilio/flex-webchat-ui": "2.9.0", 43 | "axios": "0.21.1", 44 | "react-scripts": "4.0.3", 45 | "uuid": "8.3.2" 46 | }, 47 | "devDependencies": { 48 | "@babel/cli": "7.14.3", 49 | "@babel/core": "7.14.3", 50 | "@babel/plugin-proposal-class-properties": "7.13.0", 51 | "@babel/preset-env": "7.14.2", 52 | "@babel/preset-react": "7.13.13", 53 | "@babel/preset-typescript": "7.13.0", 54 | "@types/jest": "26.0.23", 55 | "@types/node": "15.3.0", 56 | "@types/react": "17.0.5", 57 | "@types/react-dom": "17.0.5", 58 | "@types/uuid": "8.3.0", 59 | "@typescript-eslint/eslint-plugin": "4.24.0", 60 | "@typescript-eslint/parser": "4.24.0", 61 | "eslint-config-prettier": "8.3.0", 62 | "eslint-plugin-prettier": "3.4.0", 63 | "husky": "6.0.0", 64 | "npm-run-all": "4.1.5", 65 | "prettier": "2.3.0", 66 | "react": "17.0.2", 67 | "react-dom": "17.0.2", 68 | "rimraf": "3.0.2", 69 | "typescript": "4.2.4" 70 | }, 71 | "babel": { 72 | "presets": [ 73 | "@babel/preset-env", 74 | "@babel/react", 75 | "@babel/preset-typescript" 76 | ], 77 | "plugins": [ 78 | "@babel/plugin-proposal-class-properties" 79 | ] 80 | }, 81 | "husky": { 82 | "hooks": { 83 | "pre-commit": "echo -e \"\\e[34mRunning Precommit hook \\e[39m \" && npm run lint:fix" 84 | } 85 | }, 86 | "browserslist": { 87 | "production": [ 88 | ">0.2%", 89 | "not dead", 90 | "not op_mini all" 91 | ], 92 | "development": [ 93 | "last 1 chrome version", 94 | "last 1 firefox version", 95 | "last 1 safari version" 96 | ] 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intility/IntilityFlexChat/fda7f7dda4a31da9f04facddbda0beba07a2cca8/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Intility FlexChat 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Intility FlexChat", 3 | "name": "Intility FlexChat", 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/assets/loader.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/intility/IntilityFlexChat/fda7f7dda4a31da9f04facddbda0beba07a2cca8/src/assets/logo.png -------------------------------------------------------------------------------- /src/assets/texts.ts: -------------------------------------------------------------------------------- 1 | import { Strings } from '@twilio/flex-webchat-ui'; 2 | import { MultiLangText } from '../interfaces/FlexChat'; 3 | 4 | export const translateText = (text: MultiLangText, isNorwegian: boolean) => 5 | isNorwegian ? text.no : text.en; 6 | 7 | const preEngagementFormMessage: MultiLangText = { 8 | en: '', 9 | no: '', 10 | }; 11 | 12 | const entryPointLabel: MultiLangText = { 13 | en: 'Chat with us', 14 | no: 'Chat med oss', 15 | }; 16 | 17 | const predefinedMessage: MultiLangText = { 18 | en: `Welcome to Intility Chat.`, 19 | no: `Velkommen til Intility Chat.`, 20 | }; 21 | 22 | const norwegianUiTranslation: Strings = { 23 | MessageCanvasTrayContent: ` 24 |
Takk for at du snakket med oss!
25 |

Hvis du har noen flere spørsmål. Vennligst ta kontakt med oss igjen.

`, 26 | MessageCanvasTrayButton: `Start en ny chat`, 27 | InputPlaceHolder: `Skriv melding`, 28 | Today: `I dag`, 29 | Yesterday: `I går`, 30 | WelcomeMessage: `Velkommen til kundesupport`, 31 | SendMessageTooltip: `Send melding`, 32 | AttachFileImageTooltip: `Legg til fil`, 33 | Read: `Lest`, 34 | FieldValidationRequiredField: `Ugyldig innhold`, 35 | Save: 'Lagre', 36 | AttachFileInvalidSize: 'Filen er for stor. Maks størrelse: 10MB', 37 | AttachFileInvalidType: `Ugyldig filtype`, 38 | Connecting: 'Kobler til', 39 | Disconnected: 'Kobler fra', 40 | TypingIndicator: '{{name}} skriver...' 41 | }; 42 | 43 | const notificationBlocked: MultiLangText = { 44 | en: 'Notifications are blocked by the browser', 45 | no: 'Varsler er blokkert i nettleseren', 46 | }; 47 | 48 | const notificationAllowed: MultiLangText = { 49 | en: 'Notifications are enabled', 50 | no: 'Varsler er skrudd på', 51 | }; 52 | 53 | const notificationToggle: MultiLangText = { 54 | en: 'Enable notifications', 55 | no: 'Skru på varsler', 56 | }; 57 | 58 | const notificationSettings: MultiLangText = { 59 | en: 'This can be reset by clicking the lock icon next to the URL.', 60 | no: 'Endre ved å trykke på hengelåsikonet i adresselinjefeltet.', 61 | }; 62 | 63 | export default { 64 | entryPointLabel, 65 | predefinedMessage, 66 | norwegianUiTranslation, 67 | notificationBlocked, 68 | notificationAllowed, 69 | notificationToggle, 70 | notificationSettings, 71 | preEngagementFormMessage, 72 | }; 73 | -------------------------------------------------------------------------------- /src/assets/translationLanguages.ts: -------------------------------------------------------------------------------- 1 | export const languageNameFromCode = (code: string): string => { 2 | switch (code) { 3 | case 'fr-FR': 4 | return 'French / Française'; 5 | case 'es-ES': 6 | return 'Spanish / Español'; 7 | case 'it-IT': 8 | return 'Italian / Italiano'; 9 | case 'ja-JP': 10 | return 'Japanese / 日本語'; 11 | case 'de-DE': 12 | return 'German / Deutsch'; 13 | case 'en-US': 14 | return 'English'; 15 | case 'nb-NO': 16 | return 'Norwegian / Norsk'; 17 | default: 18 | return `Unknown: ${code}`; 19 | } 20 | }; 21 | 22 | export const generateLanguages = (preferredLanguage: string) => { 23 | const allowedLanguages: string[] = [ 24 | 'en-US', 25 | 'es-ES', 26 | 'it-IT', 27 | 'nb-NO', 28 | ]; 29 | 30 | return allowedLanguages 31 | .sort((a, b) => languageNameFromCode(a).localeCompare(languageNameFromCode(b))) 32 | .map((code) => ({ 33 | value: code, 34 | label: languageNameFromCode(code), 35 | selected: allowedLanguages.includes(preferredLanguage) 36 | ? code === preferredLanguage 37 | : code === 'en-US', 38 | })); 39 | }; 40 | -------------------------------------------------------------------------------- /src/components/FlexChat.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { 3 | Manager, 4 | EntryPoint, 5 | MainHeader, 6 | MessagingCanvas, 7 | ContextProvider, 8 | RootContainer, 9 | MessageBubble, 10 | Actions, 11 | MainContainer, 12 | } from '@twilio/flex-webchat-ui'; 13 | import chatConfigBase from '../config/chat/chatAppConfig'; 14 | import logo from '../assets/logo.png'; 15 | import FlexChatLoading from './FlexChatLoading'; 16 | import { ManagerState, FlexChatProps } from '../interfaces/FlexChat'; 17 | import { faExclamationCircle } from '@fortawesome/free-solid-svg-icons'; 18 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 19 | import Loader from '../assets/loader.svg'; 20 | import { version } from '../../package.json'; 21 | 22 | import '../styles/FlexChatError.css'; 23 | import '../styles/header.css'; 24 | import MessageBubbleHeader from './MessageBubbleHeader'; 25 | import initActions from '../config/chat/customActions'; 26 | import useChatActions from '../hooks/useChatActions'; 27 | import NotificationButton from './NotificationButton'; 28 | import Texts, { translateText } from '../assets/texts'; 29 | import TranslationBubbleBody from './TranslationBubbleBody'; 30 | import TranslationInfoHeader from './TranslationInfoHeader'; 31 | import useTranslation from '../hooks/useTranslation'; 32 | import { v4 as uuidv4 } from 'uuid'; 33 | 34 | const defaultManagerState = { 35 | loading: false, 36 | manager: undefined, 37 | error: undefined, 38 | }; 39 | 40 | const FlexChat: React.FC = ({ 41 | config, 42 | isDarkMode = false, 43 | isDisabled = false, 44 | isDev = false, 45 | enableTranslation = false, 46 | }) => { 47 | const [managerState, setManagerState] = useState(defaultManagerState); 48 | const { manager, loading, error } = managerState; 49 | const { flexFlowSid, flexAccountSid, user } = config; 50 | const isNorwegian = user.preferredLanguage?.includes('-NO'); 51 | 52 | const { toggleChatVisibility } = useChatActions(); 53 | const { translateTextAsync } = useTranslation(); 54 | 55 | const translationRef = React.createRef(); 56 | 57 | useEffect(() => { 58 | if (!flexFlowSid) { 59 | throw new Error('Missing required flexFlowSid in config object'); 60 | } else if (!flexAccountSid) { 61 | throw new Error('Missing required flexAccountSid in config object'); 62 | } 63 | }, [flexFlowSid, flexAccountSid]); 64 | 65 | useEffect(() => { 66 | if (flexFlowSid && flexAccountSid && user) { 67 | // If Twilio Chat Manager is initialized update config 68 | if (manager) { 69 | console.info('Intility FlexChat: Updating chat config'); 70 | manager.updateConfig( 71 | chatConfigBase(config, isDarkMode, enableTranslation, isDisabled), 72 | ); 73 | return; 74 | } 75 | 76 | setManagerState({ 77 | ...defaultManagerState, 78 | loading: true, 79 | }); 80 | 81 | console.info(`Intility FlexChat: Initializing Intility Chat - v${version}`); 82 | 83 | // Build chat config 84 | const chatConfig = chatConfigBase(config, isDarkMode, enableTranslation, isDisabled); 85 | 86 | EntryPoint.defaultProps.tagline = translateText(Texts.entryPointLabel, isNorwegian); 87 | MainHeader.defaultProps.imageUrl = logo; 88 | MainHeader.defaultProps.titleText = isDev ? 'Support Dev' : 'Support'; 89 | 90 | // If In-Browser notifications is supported. Add the custom Notification button to the chat header 91 | if ('Notification' in window && navigator.permissions) { 92 | MainHeader.Content.add( 93 | , 94 | ); 95 | } 96 | 97 | // Since the messages in the chat header is a bit difficult with formatting of timestamps, we are enforcing 24h clock in chat message 98 | //MessageBubble.Content.remove('header'); 99 | MessageBubble.Content.remove('header'); 100 | MessageBubble.Content.add(, { sortOrder: 0 }); 101 | 102 | if (enableTranslation && user.preferredLanguage) { 103 | console.log('#1'); 104 | MessageBubble.Content.remove('body'); 105 | 106 | new Promise((res) => { 107 | console.log(MessageBubble.Content.fragments); 108 | 109 | MessageBubble.Content.fragments 110 | //@ts-ignore 111 | .map((e) => e.props.children.key) 112 | .filter((key: string | string[]) => key.includes('translationBody')) 113 | .forEach((f) => MessageBubble.Content.remove(f)); 114 | 115 | MainContainer.Content.fragments 116 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 117 | //@ts-ignore 118 | .map((e) => e.props.children.key) 119 | .filter((key) => key.includes('translationInfoHeader')) 120 | .forEach((f) => MainContainer.Content.remove(f)); 121 | 122 | //MainContainer.Content.remove('translationInfoHeader'); 123 | MessageBubble.Content.remove('body'); 124 | console.log('Removed'); 125 | res(true); 126 | }).then(() => { 127 | MessageBubble.Content.add( 128 | , 133 | { 134 | sortOrder: 1, 135 | }, 136 | ); 137 | 138 | MainContainer.Content.add( 139 | , 140 | { 141 | sortOrder: 0, 142 | }, 143 | ); 144 | console.log('Added'); 145 | }); 146 | } 147 | 148 | MessagingCanvas.defaultProps.predefinedMessage = { 149 | body: translateText(Texts.predefinedMessage, isNorwegian), 150 | authorName: 'Intility Support', 151 | isFromMe: false, 152 | } 153 | //MessagingCanvas.defaultProps.predefinedMessage = false; 154 | 155 | // Start init of the Twilio Chat Manager with custom chatConfig 156 | Manager.create(chatConfig) 157 | .then((manager) => { 158 | // Translate default string values. 159 | if (isNorwegian) { 160 | manager.strings = { 161 | ...manager.strings, 162 | ...Texts.norwegianUiTranslation, 163 | }; 164 | } 165 | 166 | setManagerState({ 167 | ...defaultManagerState, 168 | manager, 169 | }); 170 | 171 | if (enableTranslation) { 172 | Actions.replaceAction('SendMessage', async (payload, original) => { 173 | return translateTextAsync(payload.body, 'en') 174 | .then((response) => { 175 | const translatedText = response.data[0].translations.find( 176 | (t) => t.to === 'en', 177 | )?.text; 178 | 179 | const replacedPayload = Object.assign({}, payload); 180 | replacedPayload.body = translatedText; 181 | 182 | original(replacedPayload); 183 | }) 184 | .catch(() => original(payload)); 185 | }); 186 | } 187 | 188 | // Send the initialize message 189 | Actions.addListener('afterStartEngagement', async (value) => { 190 | 191 | const initMessage = isDev ? 'danitest123' : 'Start Chat'; 192 | 193 | const { channelSid } = manager.store.getState().flex.session; 194 | const channel = await manager.chatClient.getChannelBySid(channelSid) 195 | 196 | await channel.sendMessage(initMessage) 197 | if (enableTranslation) { 198 | await channel.sendMessage('🌍 This chat is AI translated 🌍') 199 | } 200 | 201 | if (enableTranslation && user.preferredLanguage) { 202 | localStorage.setItem('chosenLanguage', user.preferredLanguage); 203 | manager.configuration.context.user.chosenLanguage = user.preferredLanguage; 204 | 205 | new Promise((res) => { 206 | MessageBubble.Content.fragments 207 | .map((e) => (e.props.children as any).key) 208 | .filter((key) => key.includes('translationBody')) 209 | .forEach((f) => MessageBubble.Content.remove(f)); 210 | 211 | MainContainer.Content.fragments 212 | .map((e) => (e.props.children as any).key) 213 | .filter((key) => key.includes('translationInfoHeader')) 214 | .forEach((f) => MainContainer.Content.remove(f)); 215 | 216 | //MainContainer.Content.remove('translationInfoHeader'); 217 | MessageBubble.Content.remove('body'); 218 | console.log('Removed'); 219 | res(true); 220 | }).then(() => { 221 | MessageBubble.Content.add( 222 | , 227 | { sortOrder: 1 } 228 | ); 229 | 230 | MainContainer.Content.add( 231 | , 234 | { sortOrder: 0 } 235 | ); 236 | console.log('Added'); 237 | }); 238 | } 239 | }); 240 | 241 | // Initialize the custom actions 242 | initActions(manager); 243 | 244 | if (!config.closeOnInit) { 245 | // Always open the chat on init 246 | if (!manager.store.getState().flex.session.isEntryPointExpanded) { 247 | toggleChatVisibility(); 248 | } 249 | } 250 | 251 | console.info(`Intility FlexChat: Chat Manager successfully initialized.`); 252 | }) 253 | .catch((error) => { 254 | console.error(`Intility FlexChat: Flex chat error`, error); 255 | setManagerState({ 256 | ...defaultManagerState, 257 | error: error.message, 258 | }); 259 | }); 260 | } 261 | }, [config, flexAccountSid, flexFlowSid, isDarkMode, manager, user]); 262 | 263 | return ( 264 | <> 265 | {error && ( 266 | 267 |

Error Initializing Chat

268 | 273 |
274 | )} 275 | {loading && ( 276 | 277 | {isNorwegian ?

Laster inn Chat

:

Loading Chat

} 278 | Loading spinner 283 |
284 | )} 285 | {manager && ( 286 | 287 | 288 | 289 | )} 290 | 291 | ); 292 | }; 293 | 294 | export default FlexChat; 295 | -------------------------------------------------------------------------------- /src/components/FlexChatLoading.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import '../styles/FlexChatLoading.css'; 3 | 4 | const FlexChatLoading: React.FC = (props) => { 5 | return
{props.children}
; 6 | }; 7 | 8 | export default FlexChatLoading; 9 | -------------------------------------------------------------------------------- /src/components/MessageBubbleHeader.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ChatChannelState } from '@twilio/flex-webchat-ui'; 3 | 4 | interface MessageBubbleHeaderProps { 5 | key: string; 6 | message?: ChatChannelState.MessageState; 7 | member?: ChatChannelState.MemberState; 8 | } 9 | 10 | // Bascially the same as the one from Twilio, except for the timestamp. 11 | const MessageBubbleHeader: React.FC = ({ member, message }) => ( 12 |
13 | 25 | {member?.friendlyName} 26 | 27 | 28 | {message?.source.timestamp.toTimeString().slice(0, 5)} 29 | 30 |
31 | ); 32 | 33 | export default MessageBubbleHeader; 34 | -------------------------------------------------------------------------------- /src/components/NotificationButton.tsx: -------------------------------------------------------------------------------- 1 | import { faExclamationCircle, faBell } from '@fortawesome/free-solid-svg-icons'; 2 | import React, { useState, useEffect } from 'react'; 3 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 4 | import Text, { translateText } from '../assets/texts'; 5 | 6 | interface Props { 7 | isNorwegian: boolean; 8 | } 9 | 10 | const NotificationButton: React.FC = ({ isNorwegian }) => { 11 | const [text, setText] = useState(''); 12 | const [classes, setClasses] = useState('notificationsRequestDiv'); 13 | const [icon, setIcon] = useState(faBell); 14 | 15 | const update = () => { 16 | if (Notification.permission === 'denied') { 17 | setText(translateText(Text.notificationBlocked, isNorwegian)); 18 | setClasses('notificationsRequestDiv'); 19 | setIcon(faExclamationCircle); 20 | } else if (Notification.permission === 'granted') { 21 | setText(translateText(Text.notificationAllowed, isNorwegian)); 22 | setClasses('notificationsRequestDiv'); 23 | setIcon(faBell); 24 | } else { 25 | setText(translateText(Text.notificationToggle, isNorwegian)); 26 | setClasses('notificationsRequestDiv clickable'); 27 | setIcon(faBell); 28 | } 29 | }; 30 | 31 | useEffect(() => { 32 | navigator.permissions.query({ name: 'notifications' }).then((permissionStatus) => { 33 | update(); 34 | 35 | permissionStatus.onchange = function () { 36 | update(); 37 | }; 38 | }); 39 | }, []); 40 | 41 | return ( 42 |
{ 50 | if (Notification.permission === 'default') { 51 | Notification.requestPermission(); 52 | } 53 | }} 54 | > 55 |

{text}

56 | 57 |
58 | ); 59 | }; 60 | 61 | export default NotificationButton; 62 | -------------------------------------------------------------------------------- /src/components/TranslationBubbleBody.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import useTranslation from '../hooks/useTranslation'; 3 | import { TranslateResponse } from '../interfaces/Translate'; 4 | import '../styles/TranslationBubbleBody.css'; 5 | 6 | interface TranslationBubbleBodyProps { 7 | key: string; 8 | message?: any; 9 | preferredLanguage: string; 10 | ref?: any; 11 | } 12 | 13 | const TranslationBubbleBody: React.FC = (props) => { 14 | const { message, preferredLanguage } = props; 15 | const { translateTextAsync } = useTranslation(); 16 | 17 | const [translatedText, setTranslatedText] = useState(); 18 | const [currentTranslation, setCurrentTranslation] = useState('en'); 19 | const [isLoading, setIsLoading] = useState(false); 20 | 21 | const textBoxStyle: React.CSSProperties = { 22 | padding: '0 12px 5px 12px', 23 | marginTop: '3px', 24 | marginBottom: '0px', 25 | fontSize: '13px', 26 | lineHeight: '1.54', 27 | overflowWrap: 'break-word', 28 | wordWrap: 'break-word', 29 | }; 30 | 31 | const textStyle: React.CSSProperties = { 32 | marginTop: '0', 33 | marginBottom: '0', 34 | }; 35 | 36 | // Update default translation language with users preferred language 37 | useEffect(() => { 38 | const shortenedLanguageCode = preferredLanguage.split('-')[0]; 39 | setCurrentTranslation(shortenedLanguageCode); 40 | }, [preferredLanguage]); 41 | 42 | // Make request to translate text 43 | useEffect(() => { 44 | // message.index is populated for messages sent in chat. 45 | if (!message.isFromMe && message.index) { 46 | const shortenedLanguageCode = preferredLanguage.split('-')[0]; 47 | 48 | setIsLoading(true); 49 | 50 | if (message?.source?.body) { 51 | 52 | translateTextAsync(message.source.body, [shortenedLanguageCode, 'en']) 53 | .then((res) => setTranslatedText(res.data[0])) 54 | .catch((err) => console.error('Translation error:', err)) 55 | .finally(() => setIsLoading(false)); 56 | 57 | } else if (message?.source?.state?.body) { 58 | 59 | translateTextAsync(message.source.state.body, [shortenedLanguageCode, 'en']) 60 | .then((res) => setTranslatedText(res.data[0])) 61 | .catch((err) => console.error('Translation error:', err)) 62 | .finally(() => setIsLoading(false)); 63 | 64 | } 65 | } 66 | }, [message, preferredLanguage]); 67 | 68 | /* 69 | Order by translation 70 | 1. Users preferred language 71 | 2. English 72 | 3. Original message 73 | */ 74 | const nextTranslation = () => { 75 | switch (currentTranslation) { 76 | case 'en': 77 | return 'original'; 78 | case 'original': 79 | return preferredLanguage.split('-')[0]; 80 | default: 81 | return 'en'; 82 | } 83 | }; 84 | 85 | // Get translated text from response object 86 | const getTranslation = () => 87 | translatedText?.translations.find((t) => t.to === currentTranslation); 88 | 89 | const toggleTranslation = () => setCurrentTranslation(nextTranslation); 90 | 91 | return ( 92 |
93 |

94 | {getTranslation()?.text || message?.source?.state?.body || message?.source?.body} 95 |

96 | {!message.isFromMe && (message?.source?.state || message?.source?.body) && ( 97 |
105 | {isLoading ? ( 106 | 114 | ) : ( 115 | 123 | )} 124 |
125 | )} 126 |
127 | ); 128 | }; 129 | 130 | export default TranslationBubbleBody; 131 | -------------------------------------------------------------------------------- /src/components/TranslationInfoHeader.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 3 | import { faInfoCircle } from '@fortawesome/free-solid-svg-icons'; 4 | 5 | const TranslationInfoHeader: React.FC = () => { 6 | return ( 7 |
15 | 16 |

17 | Automatic translation, errors may occur. 18 |

19 |
20 | ); 21 | }; 22 | 23 | export default TranslationInfoHeader; 24 | -------------------------------------------------------------------------------- /src/config/chat/chatAppConfig.ts: -------------------------------------------------------------------------------- 1 | import { generateTheme } from './chatTheme'; 2 | import { Config } from '@twilio/flex-webchat-ui/src/state/AppConfig'; 3 | import { ConfigProps } from '../../interfaces/FlexChat'; 4 | import preEngagementConfig from './preEngagementForm'; 5 | import texts, { translateText } from '../../assets/texts'; 6 | 7 | const config = ( 8 | config: ConfigProps, 9 | isDarkMode: boolean, 10 | enableTranslation: boolean, 11 | isDisabled = false, 12 | ): Config => ({ 13 | available: !isDisabled, 14 | accountSid: config.flexAccountSid, 15 | flexFlowSid: config.flexFlowSid, 16 | colorTheme: { 17 | overrides: generateTheme(isDarkMode, config.theme), 18 | }, 19 | context: { 20 | locationOrigin: window.location.href, 21 | friendlyName: config.user.userPrincipalName, 22 | user: config.user, 23 | }, 24 | markdownSupport: { 25 | enabled: true, 26 | }, 27 | logLevel: config.loglevel ? 'debug' : undefined, 28 | //logLevel: 'debug', 29 | startEngagementOnInit: false, 30 | fileAttachment: { 31 | enabled: true, 32 | maxFileSize: 10 * 1024 * 1024, //10MB 33 | acceptedExtensions: ['png', 'gif', 'jpg', 'jpeg', 'pdf'], 34 | }, 35 | preEngagementConfig: preEngagementConfig( 36 | enableTranslation, 37 | translateText( 38 | config.preEngagementFormMessage || texts.preEngagementFormMessage, 39 | config.user.preferredLanguage?.includes('-NO'), 40 | ), 41 | config.user.preferredLanguage, 42 | ), 43 | sdkOptions: { 44 | chat: { 45 | logLevel: config.loglevel === 'superDebug' ? 'debug' : undefined, 46 | }, 47 | }, 48 | }); 49 | 50 | export default config; 51 | -------------------------------------------------------------------------------- /src/config/chat/chatTheme.ts: -------------------------------------------------------------------------------- 1 | import { theme } from '../theme/theme'; 2 | import { DeepPartial, Theme, CSSProps } from '@twilio/flex-webchat-ui'; 3 | import { ThemeConfig } from '../../interfaces/FlexChat'; 4 | 5 | const fontFamily: CSSProps = { 6 | fontFamily: 'Open Sans, sans-serif', 7 | fontStyle: 'normal', 8 | fontWeight: 'normal', 9 | }; 10 | 11 | const primaryColors: CSSProps = { 12 | background: theme.primaryColor, 13 | color: theme.textLight, 14 | }; 15 | 16 | const primaryColorsOutlined: CSSProps = { 17 | background: 'none', 18 | color: theme.primaryColor, 19 | border: `2px solid ${theme.primaryColor}`, 20 | borderRadius: '3px', 21 | padding: '5.5px 8px', 22 | textTransform: 'none', 23 | letterSpacing: 'normal', 24 | fontSize: '14px', 25 | fontWeight: 'bold', 26 | }; 27 | 28 | const primaryColorsHover: CSSProps = { 29 | background: theme.primaryColorHover, 30 | color: theme.textLight, 31 | }; 32 | 33 | export const generateTheme = ( 34 | isDarkMode: boolean, 35 | themeConfig: ThemeConfig = {}, 36 | ): DeepPartial => ({ 37 | MainContainer: { 38 | ...fontFamily, 39 | ...themeConfig.MainContainer, 40 | width: themeConfig.MainContainer?.width || '500px', 41 | height: themeConfig.MainContainer?.height, 42 | }, 43 | Chat: { 44 | MessagingCanvas: { 45 | Container: { 46 | ...fontFamily, 47 | background: isDarkMode ? theme.darkGreyBackground : theme.lightBackground, 48 | }, 49 | }, 50 | MessageList: { 51 | DateSeparatorLine: { 52 | color: isDarkMode ? theme.textLight : theme.textDark, 53 | }, 54 | TypingIndicator: { 55 | color: isDarkMode ? theme.textLight : theme.textDark, 56 | }, 57 | DateSeparatorText: { 58 | ...fontFamily, 59 | color: isDarkMode ? theme.textLight : theme.textDark, 60 | }, 61 | }, 62 | WelcomeMessage: { 63 | Container: { 64 | ...fontFamily, 65 | fontSize: '14px !important', 66 | color: isDarkMode ? theme.textLight : theme.textDark, 67 | }, 68 | Icon: { 69 | color: isDarkMode ? theme.textLight : theme.textDark, 70 | fontSize: '14px !important', 71 | }, 72 | }, 73 | MessageListItem: { 74 | FromOthers: { 75 | Bubble: { 76 | ...fontFamily, 77 | background: isDarkMode ? theme.mediumGreyBackground : theme.lightGreyBackground, 78 | color: isDarkMode ? theme.textLight : theme.textDark, 79 | }, 80 | Avatar: { 81 | background: isDarkMode ? theme.mediumGreyBackground : theme.lightGreyBackground, 82 | color: isDarkMode ? theme.textLight : theme.textDark, 83 | }, 84 | Header: { 85 | ...fontFamily, 86 | color: isDarkMode ? theme.textLight : theme.textDark, 87 | }, 88 | }, 89 | FromMe: { 90 | Bubble: { 91 | ...fontFamily, 92 | ...primaryColors, 93 | }, 94 | Avatar: { 95 | ...primaryColors, 96 | }, 97 | Header: { 98 | ...fontFamily, 99 | ...primaryColors, 100 | }, 101 | }, 102 | ReadStatus: { 103 | ...fontFamily, 104 | color: isDarkMode ? theme.textLight : theme.textDark, 105 | }, 106 | }, 107 | MessageInput: { 108 | AttachFileButton: { 109 | ...primaryColors, 110 | ...fontFamily, 111 | '&:hover': { 112 | ...primaryColorsHover, 113 | backgroundBlendMode: 'color', 114 | }, 115 | }, 116 | FileBox: { 117 | background: isDarkMode ? theme.mediumGreyBackground : theme.lightGreyBackground, 118 | border: `1px solid ${isDarkMode ? 'rgb(71,71,72)' : 'rgb(217,217,217)'}`, 119 | borderRadius: '4px', 120 | }, 121 | 122 | Container: { 123 | background: isDarkMode ? theme.mediumGreyBackground : theme.lightGreyBackground, 124 | color: isDarkMode ? theme.textLight : theme.textDark, 125 | ...fontFamily, 126 | fontSize: '14px !important', 127 | borderRadius: '4px', 128 | '::placeholder': { 129 | color: isDarkMode ? theme.textLight : theme.textDark, 130 | }, 131 | }, 132 | Button: { 133 | ...primaryColors, 134 | ...fontFamily, 135 | '&:hover': { 136 | ...primaryColorsHover, 137 | backgroundBlendMode: 'color', 138 | }, 139 | }, 140 | }, 141 | 142 | MessageCanvasTray: { 143 | Button: { 144 | ...fontFamily, 145 | ...primaryColorsOutlined, 146 | borderRadius: '4px', 147 | }, 148 | Container: { 149 | ...fontFamily, 150 | background: isDarkMode ? theme.mediumGreyBackground : theme.lightGreyBackground, 151 | color: isDarkMode ? theme.textLight : theme.textDark, 152 | fontSize: '1rem', 153 | }, 154 | }, 155 | }, 156 | 157 | MainHeader: { 158 | Container: { 159 | ...fontFamily, 160 | background: isDarkMode ? theme.darkBackground : theme.darkGreyBackground, 161 | color: theme.textLight, 162 | '& .Twilio-Icon-Close': { 163 | ...themeConfig.CloseButton, 164 | }, 165 | '&:hover': { 166 | cursor: 'default', 167 | }, 168 | }, 169 | }, 170 | 171 | EntryPoint: { 172 | Container: { 173 | ...themeConfig.EntryPoint, 174 | ...primaryColors, 175 | ...fontFamily, 176 | boxShadow: 'none', 177 | '&:hover': { 178 | ...primaryColorsHover, 179 | backgroundBlendMode: 'color', 180 | }, 181 | }, 182 | }, 183 | FormComponents: { 184 | Input: { 185 | display: 'none', 186 | }, 187 | Select: { 188 | ...fontFamily, 189 | color: isDarkMode ? theme.textLight : theme.textDark, 190 | }, 191 | TextArea: { 192 | ...fontFamily, 193 | color: isDarkMode ? theme.textLight : theme.textDark, 194 | '& div textarea': { 195 | borderRadius: '4px', 196 | backgroundColor: '#E6E6E6', 197 | border: '1px solid #808080', 198 | color: isDarkMode ? theme.textLight : theme.textDark, 199 | fontSize: '14px', 200 | }, 201 | '& div textarea::placeholder': { 202 | color: isDarkMode ? theme.textLight : theme.textDark, 203 | }, 204 | '& div textarea:hover': { 205 | background: isDarkMode ? theme.darkGreyBackground : theme.lightBackground, 206 | }, 207 | '& div textarea:focus': { 208 | background: isDarkMode ? theme.darkGreyBackground : theme.lightBackground, 209 | }, 210 | }, 211 | }, 212 | PreEngagementCanvas: { 213 | Container: { 214 | ...fontFamily, 215 | background: isDarkMode ? theme.darkGreyBackground : theme.lightBackground, 216 | color: isDarkMode ? theme.textLight : theme.textDark, 217 | paddingLeft: '10%', 218 | paddingRight: '10%', 219 | display: 'flex', 220 | justifyContent: 'center', 221 | '& *': { 222 | ...fontFamily, 223 | background: isDarkMode ? theme.darkGreyBackground : theme.lightBackground, 224 | }, 225 | '& div form': { 226 | maxWidth: '100% !important', 227 | padding: '0 2px 0 2px', 228 | }, 229 | '@media only screen and (max-width: 767px)': { 230 | paddingLeft: '10px', 231 | paddingRight: '10px', 232 | }, 233 | // Make form fit content 234 | '& div': { 235 | flexGrow: 0, 236 | }, 237 | // Style Form description 238 | '& div form > div:first-child': { 239 | fontSize: '19px', 240 | }, 241 | // Style form message 242 | '& .message': { 243 | fontSize: '14px', 244 | lineHeight: '1.5rem', 245 | }, 246 | }, 247 | Form: { 248 | Label: { 249 | ...fontFamily, 250 | color: isDarkMode ? theme.textLight : theme.textDark, 251 | fontSize: '14px', 252 | }, 253 | SubmitButton: { 254 | ...primaryColorsOutlined, 255 | ...fontFamily, 256 | marginTop: '8px', 257 | }, 258 | }, 259 | }, 260 | PendingEngagementCanvas: { 261 | Container: { 262 | backgroundColor: isDarkMode ? theme.darkGreyBackground : theme.lightBackground, 263 | }, 264 | }, 265 | }); 266 | -------------------------------------------------------------------------------- /src/config/chat/customActions.ts: -------------------------------------------------------------------------------- 1 | import { Manager, Actions } from '@twilio/flex-webchat-ui'; 2 | 3 | const initActions = (manager: Manager) => { 4 | Actions.registerAction('customSetInputText', async ({ body }) => { 5 | const channelSid = Object.keys(manager.store.getState().flex.chat.channels)[0]; 6 | 7 | if (channelSid) { 8 | await Actions.invokeAction('SetInputText', { channelSid, body }); 9 | } 10 | }); 11 | 12 | Actions.registerAction('customSendMessage', async ({ body }) => { 13 | const channelSid = Object.keys(manager.store.getState().flex.chat.channels)[0]; 14 | 15 | if (channelSid) { 16 | await Actions.invokeAction('SendMessage', { channelSid, body }); 17 | } 18 | }); 19 | 20 | Actions.registerAction('isChatOpen', async () => { 21 | return manager.store.getState().flex.session.isEntryPointExpanded; 22 | }); 23 | 24 | Actions.registerAction('hasUserReadLastMessage', async () => { 25 | const channelSid = Object.keys(manager.store.getState().flex.chat.channels)[0]; 26 | const channel = manager.store.getState().flex.chat.channels[channelSid]; 27 | 28 | if (channelSid) { 29 | const lastRecievedMessageIndex = channel.lastConsumedMessageIndex; 30 | const lastReadMessageIndex = channel.lastConsumedMessageByCurrentUserIndex; 31 | return lastReadMessageIndex === lastRecievedMessageIndex; 32 | } 33 | }); 34 | }; 35 | 36 | export default initActions; 37 | -------------------------------------------------------------------------------- /src/config/chat/preEngagementForm.ts: -------------------------------------------------------------------------------- 1 | import { FormAttributes } from '@twilio/flex-webchat-ui'; 2 | 3 | // @ts-expect-error 4 | const languageNamesInEnglish = new Intl.DisplayNames(['en'], { type: 'language' }); 5 | 6 | const preEngagementConfig = ( 7 | enableTranslation: boolean, 8 | message: string, 9 | preferredLanguage: string, 10 | ): FormAttributes => ({ 11 | description: 'Velkommen til Intility Chat', 12 | message: enableTranslation 13 | ? `Your registred language is: ${languageNamesInEnglish.of(preferredLanguage)}. We will translate your messages to English, and the agents message will be translated to your language.` 14 | : message, 15 | submitLabel: 'Start Chat', 16 | fields: [ 17 | { 18 | label: 'Please describe your issue', 19 | type: 'InputItem', 20 | attributes: { 21 | name: 'question', 22 | type: 'text', 23 | placeholder: 'Type your issue here', 24 | required: false, 25 | }, 26 | } 27 | ], 28 | }); 29 | 30 | export default preEngagementConfig; 31 | -------------------------------------------------------------------------------- /src/config/theme/theme.ts: -------------------------------------------------------------------------------- 1 | export const theme = { 2 | darkBackground: 'rgb(26,26,26)', 3 | darkGreyBackground: 'rgb(38,38,38)', 4 | mediumGreyBackground: 'rgb(54,54,54)', 5 | lightGreyBackground: 'rgb(235,235,235)', 6 | lightBackground: 'rgb(255,255,255)', 7 | textLight: 'rgb(255,255,255)', 8 | textDark: 'rgb(26,26,26)', 9 | primaryColor: 'rgb(64,105,151)', 10 | primaryColorHover: 'rgb(61,99,143)', 11 | }; 12 | 13 | export const fontSize = '13px !important'; 14 | -------------------------------------------------------------------------------- /src/hooks/useChatActions.tsx: -------------------------------------------------------------------------------- 1 | import * as Flex from '@twilio/flex-webchat-ui'; 2 | 3 | export type UseChatActionsFuncs = { 4 | setInputFieldContent: (body: string) => Promise; 5 | setAndSendInputFieldContent: (body: string) => Promise; 6 | toggleChatVisibility: () => Promise; 7 | hasUserReadLastMessage: () => Promise; 8 | isChatOpen: () => Promise; 9 | }; 10 | 11 | const useChatActions = (): UseChatActionsFuncs => { 12 | /** 13 | * Sets the value of the input field to the given value. 14 | * 15 | * Use `setAndSendInputFieldContent` to automatially send the given message. 16 | * 17 | * @param {string} body the text to insert 18 | */ 19 | const setInputFieldContent = (body: string) => 20 | Flex.Actions.invokeAction('customSetInputText', { body }); 21 | 22 | /** 23 | * Sets the value of the input field to the given value and sends the message. 24 | * 25 | * @param {string} body the text to send 26 | */ 27 | const setAndSendInputFieldContent = (body: string) => 28 | Flex.Actions.invokeAction('customSendMessage', { body }); 29 | 30 | /** 31 | * Toggle the chat between minified and expanded state 32 | * 33 | */ 34 | const toggleChatVisibility = () => Flex.Actions.invokeAction('ToggleChatVisibility'); 35 | 36 | /** 37 | * Retunrs if the user has read last message sent from agent. 38 | * 39 | */ 40 | const hasUserReadLastMessage = () => Flex.Actions.invokeAction('hasUserReadLastMessage'); 41 | 42 | /** 43 | * Returns if the chat is in its expanded state 44 | * 45 | */ 46 | const isChatOpen = () => Flex.Actions.invokeAction('isChatOpen'); 47 | 48 | return { 49 | setInputFieldContent, 50 | setAndSendInputFieldContent, 51 | toggleChatVisibility, 52 | hasUserReadLastMessage, 53 | isChatOpen, 54 | }; 55 | }; 56 | 57 | export default useChatActions; 58 | -------------------------------------------------------------------------------- /src/hooks/useTranslation.tsx: -------------------------------------------------------------------------------- 1 | import axios, { AxiosResponse } from 'axios'; 2 | import { TranslateResponse, TranslateRequest } from '../interfaces/Translate'; 3 | 4 | interface UseTranslation { 5 | detectLanguageAsync: (text: string) => Promise>; 6 | translateTextAsync: (text: string, to: string | string[]) => Promise> 7 | } 8 | 9 | const useTranslation = (): UseTranslation => { 10 | const { REACT_APP_AZURE_TRANSLATOR_KEY } = process.env; 11 | const instance = axios.create({ 12 | baseURL: 'https://api.cognitive.microsofttranslator.com', 13 | params: { 14 | 'api-version': '3.0', 15 | }, 16 | headers: { 17 | 'Content-type': 'application/json', 18 | 'Ocp-Apim-Subscription-Key': REACT_APP_AZURE_TRANSLATOR_KEY, 19 | 'Ocp-Apim-Subscription-Region': 'westeurope', 20 | }, 21 | }); 22 | 23 | const detectLanguageAsync = (text: string) => instance.post('/detect', [{ text }]); 24 | 25 | const translateTextAsync = (text: string, to: string | string[]) => 26 | instance.post>( 27 | `/translate`, 28 | [{ text }], 29 | { 30 | params: { 31 | ...instance.defaults.params, 32 | to: to instanceof Array ? to.join(',') : to, 33 | }, 34 | }, 35 | ); 36 | 37 | return { detectLanguageAsync, translateTextAsync }; 38 | }; 39 | 40 | export default useTranslation; 41 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import './styles/FlexChatLoading.css'; 2 | import './styles/root.css'; 3 | 4 | //import App from './devSetup'; 5 | //import React from 'react'; 6 | //import ReactDOM from 'react-dom'; 7 | 8 | export * from './interfaces/FlexChat'; 9 | export { Manager } from '@twilio/flex-webchat-ui'; 10 | export { default as FlexChat } from './components/FlexChat'; 11 | export { default as useChatActions } from './hooks/useChatActions'; 12 | export { default as UseChatActionsFuncs } from './hooks/useChatActions'; 13 | 14 | export type { CSSProps, FormAttributes } from '@twilio/flex-webchat-ui'; 15 | 16 | //ReactDOM.render(, document.getElementById('root')); 17 | -------------------------------------------------------------------------------- /src/interfaces/FlexChat.ts: -------------------------------------------------------------------------------- 1 | import { Manager, CSSProps } from '@twilio/flex-webchat-ui'; 2 | import { ReactNode } from 'react'; 3 | 4 | export type FlexChatProps = { 5 | children?: ReactNode; 6 | config: ConfigProps; 7 | isDev?: boolean; 8 | isDarkMode?: boolean; 9 | isDisabled?: boolean; 10 | enableTranslation?: boolean; 11 | }; 12 | 13 | export type MultiLangText = { 14 | en: string; 15 | no: string; 16 | }; 17 | 18 | export type ConfigProps = { 19 | flexFlowSid?: string; 20 | flexAccountSid?: string; 21 | translatorKey?: string; 22 | closeInInit?: boolean; 23 | loglevel?: 'debug' | 'superDebug'; 24 | closeOnInit?: boolean; 25 | theme?: ThemeConfig; 26 | preEngagementFormMessage?: MultiLangText; 27 | user: ChatContext; 28 | }; 29 | 30 | export type ThemeConfig = { 31 | MainContainer?: CSSProps; 32 | EntryPoint?: CSSProps; 33 | CloseButton?: CSSProps; 34 | }; 35 | 36 | export type ChatContext = { 37 | userPrincipalName: string; 38 | mail: string; 39 | mobilePhone: string; 40 | preferredLanguage: string; 41 | [key: string]: AllowedValues; 42 | }; 43 | 44 | export type AllowedValues = 45 | | string 46 | | number 47 | | boolean 48 | | undefined 49 | | null 50 | | string[] 51 | | number[] 52 | | boolean[] 53 | | Record; 54 | 55 | export type ManagerState = { 56 | loading: boolean; 57 | manager?: Manager; 58 | error?: string; 59 | }; 60 | -------------------------------------------------------------------------------- /src/interfaces/Translate.ts: -------------------------------------------------------------------------------- 1 | export type AvailableLanguages = 'nb' | 'en' | 'original'; 2 | 3 | export interface TranslateRequest { 4 | text: string; 5 | } 6 | 7 | export interface TranslateResponse { 8 | detectedLanguage: DetectedLanguage; 9 | translations: Translations[]; 10 | } 11 | 12 | interface DetectedLanguage { 13 | language: string; 14 | score: number; 15 | } 16 | 17 | interface Translations { 18 | text: string; 19 | to: string; 20 | } 21 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/styles/FlexChatError.css: -------------------------------------------------------------------------------- 1 | .flexChatError { 2 | font-size: 1.3rem; 3 | min-width: 24px; 4 | height: 24px; 5 | padding: 0px; 6 | } -------------------------------------------------------------------------------- /src/styles/FlexChatLoading.css: -------------------------------------------------------------------------------- 1 | .flexLoader { 2 | display: flex; 3 | justify-content: center; 4 | align-content: center; 5 | align-items: center; 6 | flex-direction: row; 7 | 8 | position: fixed !important; 9 | right: 24px; 10 | bottom: 16px; 11 | 12 | min-width: 24px; 13 | height: 24px; 14 | 15 | background-color: rgb(64,105,151); 16 | border: none; 17 | border-radius: 200px; 18 | padding: 10px; 19 | } 20 | 21 | .flexLoader p { 22 | font-size: 14px; 23 | font-weight: 400; 24 | overflow: hidden; 25 | margin: auto; 26 | margin-right: auto; 27 | margin-right: 20px; 28 | margin-left: 10px; 29 | font-family: Arial, Helvetica, sans-serif; 30 | font-weight: 400; 31 | color: rgb(230,230,230); 32 | } -------------------------------------------------------------------------------- /src/styles/TranslationBubbleBody.css: -------------------------------------------------------------------------------- 1 | .translate__info__text { 2 | margin: 0; 3 | font-size: 11px; 4 | color: #969696; 5 | } 6 | 7 | .showTranslation__button { 8 | display: inline-block; 9 | border: none; 10 | margin: 0; 11 | margin-right: 5px; 12 | padding: 0; 13 | text-decoration: none; 14 | background: none; 15 | color: blue; 16 | font-family: 'Open Sans', sans-serif; 17 | font-size: 12px; 18 | cursor: pointer; 19 | line-height: normal; 20 | text-align: center; 21 | -webkit-appearance: none; 22 | -moz-appearance: none; 23 | } 24 | 25 | .showTranslation__button:hover { 26 | text-decoration: underline; 27 | } -------------------------------------------------------------------------------- /src/styles/header.css: -------------------------------------------------------------------------------- 1 | .notificationIcon { 2 | min-width: 24px; 3 | height: 16px; 4 | color: #FFF; 5 | } 6 | 7 | .notificationsRequestText { 8 | color: #FFF; 9 | display: inline; 10 | } 11 | 12 | .clickable { 13 | cursor: pointer; 14 | text-decoration: underline; 15 | } 16 | 17 | .notificationsRequestDiv { 18 | margin-left: auto; 19 | margin-top: auto; 20 | margin-bottom: auto; 21 | } 22 | -------------------------------------------------------------------------------- /src/styles/root.css: -------------------------------------------------------------------------------- 1 | .Twilio-MessageCanvasTray-default { 2 | border-top: 1px solid rgba(10, 10, 10, 0.2); 3 | } 4 | 5 | .Twilio-MessageBubble-Body { 6 | font-size: 13px !important; 7 | } 8 | 9 | .Twilio-MessageBubble-Body li { 10 | margin-left: 15px; 11 | } 12 | 13 | .Twilio-MessagingCanvas { 14 | max-width: 100% !important; 15 | } 16 | 17 | #menu-chosenLanguage { 18 | font-family: Open Sans, sans-serif; 19 | } 20 | 21 | .Twilio-Dropdown { 22 | width: 0 !important; 23 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": [ 4 | "dom", 5 | "dom.iterable", 6 | "esnext" 7 | ], 8 | "allowJs": false, 9 | "skipLibCheck": true, 10 | "allowSyntheticDefaultImports": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "esnext", 13 | "resolveJsonModule": true, 14 | "jsx": "react-jsx", 15 | "target": "ES2015", 16 | "moduleResolution": "node", 17 | "noEmit": true, 18 | "strict": true, 19 | "isolatedModules": true, 20 | "esModuleInterop": true, 21 | "noFallthroughCasesInSwitch": true, 22 | "noImplicitAny": false 23 | }, 24 | "include": [ 25 | "src/**/*" 26 | ], 27 | "exclude": [ 28 | "node_modules", 29 | "dist", 30 | "es", 31 | "bundle" 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /tsconfig.types.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig", 3 | "compilerOptions": { 4 | "outDir": "types", 5 | "declaration": true, 6 | "declarationMap": true, 7 | "isolatedModules": false, 8 | "noEmit": false, 9 | "allowJs": false, 10 | "emitDeclarationOnly": true 11 | }, 12 | "exclude": [ 13 | "**/*.test.ts" 14 | ] 15 | } --------------------------------------------------------------------------------