├── .editorconfig ├── .eslintrc.js ├── .github └── workflows │ └── npm-publish.yml ├── .gitignore ├── .lintstagedrc.js ├── .yarnrc ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── babel.config.js ├── commitlint.config.js ├── package.json ├── rollup.config.js ├── scripts └── terser.js ├── src ├── components │ ├── Avatar │ │ ├── Avatar.d.ts │ │ ├── Avatar.jsx │ │ └── index.js │ ├── AvatarGroup │ │ ├── AvatarGroup.d.ts │ │ ├── AvatarGroup.jsx │ │ └── index.js │ ├── Buttons │ │ ├── AddUserButton.jsx │ │ ├── ArrowButton.jsx │ │ ├── AttachmentButton.jsx │ │ ├── Button.jsx │ │ ├── Buttons.d.ts │ │ ├── EllipsisButton.jsx │ │ ├── InfoButton.jsx │ │ ├── SendButton.jsx │ │ ├── StarButton.jsx │ │ ├── VideoCallButton.jsx │ │ ├── VoiceCallButton.jsx │ │ └── index.js │ ├── ChatContainer │ │ ├── ChatContainer.d.ts │ │ ├── ChatContainer.jsx │ │ └── index.js │ ├── ContentEditable │ │ ├── ContentEditable.jsx │ │ └── index.js │ ├── Conversation │ │ ├── Conversation.d.ts │ │ ├── Conversation.jsx │ │ ├── ConversationContent.jsx │ │ ├── ConversationOperations.jsx │ │ ├── cName.js │ │ └── index.js │ ├── ConversationHeader │ │ ├── ConversationHeader.d.ts │ │ ├── ConversationHeader.jsx │ │ ├── ConversationHeaderActions.jsx │ │ ├── ConversationHeaderBack.jsx │ │ ├── ConversationHeaderContent.jsx │ │ └── index.js │ ├── ConversationList │ │ ├── ConversationList.d.ts │ │ ├── ConversationList.jsx │ │ └── index.js │ ├── ExpansionPanel │ │ ├── ExpansionPanel.d.ts │ │ ├── ExpansionPanel.jsx │ │ └── index.js │ ├── InputToolbox │ │ ├── InputToolbox.d.ts │ │ ├── InputToolbox.jsx │ │ └── index.js │ ├── Loader │ │ ├── Loader.d.ts │ │ ├── Loader.jsx │ │ └── index.js │ ├── MainContainer │ │ ├── MainContainer.d.ts │ │ ├── MainContainer.jsx │ │ └── index.js │ ├── Message │ │ ├── Message.d.ts │ │ ├── Message.jsx │ │ ├── MessageCustomContent.jsx │ │ ├── MessageFooter.jsx │ │ ├── MessageHeader.jsx │ │ ├── MessageHtmlContent.jsx │ │ ├── MessageImageContent.jsx │ │ ├── MessageTextContent.jsx │ │ └── index.js │ ├── MessageGroup │ │ ├── MessageGroup.d.ts │ │ ├── MessageGroup.jsx │ │ ├── MessageGroupFooter.jsx │ │ ├── MessageGroupHeader.jsx │ │ ├── MessageGroupMessages.jsx │ │ └── index.js │ ├── MessageInput │ │ ├── MessageInput.d.ts │ │ ├── MessageInput.jsx │ │ └── index.js │ ├── MessageList │ │ ├── MessageList.d.ts │ │ ├── MessageList.jsx │ │ ├── MessageListContent.jsx │ │ └── index.js │ ├── MessageSeparator │ │ ├── MessageSeparator.d.ts │ │ ├── MessageSeparator.jsx │ │ └── index.js │ ├── Overlay │ │ ├── Overlay.d.ts │ │ ├── Overlay.jsx │ │ └── index.js │ ├── Scroll │ │ ├── LICENSE │ │ ├── README.md │ │ ├── ReactPerfectScrollbar.jsx │ │ ├── index.jsx │ │ └── perfect-scrollbar.esm.js │ ├── Search │ │ ├── Search.d.ts │ │ ├── Search.jsx │ │ └── index.js │ ├── Sidebar │ │ ├── Sidebar.d.ts │ │ ├── Sidebar.jsx │ │ └── index.js │ ├── Status │ │ ├── Status.d.ts │ │ ├── Status.jsx │ │ └── index.js │ ├── StatusList │ │ ├── StatusList.d.ts │ │ ├── StatusList.jsx │ │ └── index.js │ ├── TypingIndicator │ │ ├── TypingIndicator.d.ts │ │ ├── TypingIndicator.jsx │ │ └── index.js │ ├── enums.js │ ├── index.js │ ├── settings.js │ └── utils.js └── types │ ├── index.d.ts │ └── unions.d.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | 6 | [*.{js,json,ts,tsx,d.ts,html,jsx,mdx}] 7 | indent_style = space 8 | indent_size = 2 -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | commonjs: true, 5 | es6: true, 6 | node: true, 7 | }, 8 | plugins: ["react", "jsx-a11y", "@typescript-eslint"], 9 | extends: [ 10 | "eslint:recommended", 11 | "plugin:react/recommended", 12 | "plugin:react-hooks/recommended", 13 | "plugin:jsx-a11y/recommended", 14 | "plugin:@typescript-eslint/recommended", 15 | "plugin:@typescript-eslint/recommended-requiring-type-checking", 16 | ], 17 | parser: "@typescript-eslint/parser", 18 | parserOptions: { 19 | tsconfigRootDir: __dirname, 20 | ecmaVersion: 6, 21 | sourceType: "module", 22 | ecmaFeatures: { 23 | jsx: true, 24 | }, 25 | project: ["./tsconfig.json"], 26 | }, 27 | ignorePatterns: [".eslintrc.js", "perfect-scrollbar.esm.js"], 28 | overrides: [ 29 | { 30 | files: ["*.js", "*.jsx"], 31 | rules: { 32 | "@typescript-eslint/no-unsafe-assignment": 0, 33 | "@typescript-eslint/no-unsafe-member-access": 0, 34 | "@typescript-eslint/no-unsafe-argument": 0, 35 | "@typescript-eslint/no-unsafe-return": 0, 36 | "@typescript-eslint/restrict-template-expressions": 0, 37 | "@typescript-eslint/no-unsafe-call": 0, 38 | }, 39 | }, 40 | ], 41 | rules: { 42 | "linebreak-style": ["error", "unix"], 43 | semi: ["error", "always"], 44 | "no-console": ["warn"], 45 | "prefer-template": ["error"], 46 | }, 47 | settings: { 48 | react: { 49 | version: "detect", 50 | }, 51 | }, 52 | }; 53 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v3 14 | - name: Setup Node.js 15 | uses: actions/setup-node@v3 16 | with: 17 | node-version: 20 18 | registry-url: https://registry.npmjs.org/ 19 | scope: '@chatscope' 20 | - name: Install dependencies 21 | run: yarn 22 | - name: Build 23 | run: yarn build 24 | - name: Publish package 25 | env: 26 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 27 | NPM_TOKEN: ${{secrets.NPM_TOKEN}} 28 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 29 | GIT_AUTHOR_NAME: supersnager 30 | GIT_AUTHOR_EMAIL: ${{secrets.GIT_AUTHOR_EMAIL}} 31 | GIT_COMMITTER_NAME: supersnager 32 | GIT_COMMITTER_EMAIL: ${{secrets.GIT_AUTHOR_EMAIL}} 33 | run: npx semantic-release 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | .idea 4 | # dependencies 5 | /node_modules 6 | /.pnp 7 | .pnp.js 8 | 9 | # testing 10 | /coverage 11 | 12 | # production 13 | /build 14 | /dist 15 | /storybook-static 16 | 17 | # misc 18 | .DS_Store 19 | .env.local 20 | .env.development.local 21 | .env.test.local 22 | .env.production.local 23 | 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | .eslintcache -------------------------------------------------------------------------------- /.lintstagedrc.js: -------------------------------------------------------------------------------- 1 | const { CLIEngine } = require("eslint"); 2 | 3 | const cli = new CLIEngine({}); 4 | 5 | module.exports = { 6 | "*.js": (files) => 7 | "eslint --max-warnings=0 " + 8 | files.filter((file) => !cli.isPathIgnored(file)).join(" "), 9 | }; 10 | -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | version-git-tag false 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @chatscope/chat-ui-kit-react changelog 2 | 3 | ## [2.1.1](https://github.com/chatscope/chat-ui-kit-react/compare/v2.1.0...v2.1.1) (2025-05-15) 4 | 5 | 6 | ### Bug Fixes 7 | 8 | * add react 19 to peer dependencies ([#171](https://github.com/chatscope/chat-ui-kit-react/issues/171)) ([95a96c1](https://github.com/chatscope/chat-ui-kit-react/commit/95a96c15ad7506c9567c98f1ea7f60b49a94d38c)) 9 | 10 | # [2.1.0](https://github.com/chatscope/chat-ui-kit-react/compare/v2.0.3...v2.1.0) (2025-05-15) 11 | 12 | 13 | ### Features 14 | 15 | * upgrade fontawesome libs for fix defaultProps warning ([6d5adff](https://github.com/chatscope/chat-ui-kit-react/commit/6d5adffa412eb2f0c551b3cb875133aae54b3370)) 16 | 17 | ## [2.0.3](https://github.com/chatscope/chat-ui-kit-react/compare/v2.0.2...v2.0.3) (2024-03-03) 18 | 19 | 20 | ### Bug Fixes 21 | 22 | * added automatically provided github token to github workflow ([3da88a0](https://github.com/chatscope/chat-ui-kit-react/commit/3da88a06f08f1c47ed55bb07f412cf6efa9993f8)) 23 | * removed github token from github workflow ([a674c0c](https://github.com/chatscope/chat-ui-kit-react/commit/a674c0c8b648d23e9b000a5c3f40bb6f1eabb6ea)) 24 | 25 | ## [2.0.2](https://github.com/chatscope/chat-ui-kit-react/compare/v2.0.1...v2.0.2) (2024-03-03) 26 | 27 | 28 | ### Bug Fixes 29 | 30 | * bump github actions and semantic-release ([e87babc](https://github.com/chatscope/chat-ui-kit-react/commit/e87babc641df46937016e4fee1e08d5ebecb7b23)) 31 | 32 | ## [2.0.1](https://github.com/chatscope/chat-ui-kit-react/compare/v2.0.0...v2.0.1) (2024-03-03) 33 | 34 | 35 | ### Bug Fixes 36 | 37 | * bump node version in github workflow to 20 ([2695c4d](https://github.com/chatscope/chat-ui-kit-react/commit/2695c4d61eaa7009212c9b5d589bec05b09a2285)) 38 | 39 | # [2.0.0](https://github.com/chatscope/chat-ui-kit-react/compare/v1.10.1...v2.0.0) (2024-03-03) 40 | 41 | 42 | ### chore 43 | 44 | * removed default props from function components ([84a3302](https://github.com/chatscope/chat-ui-kit-react/commit/84a3302492f3f77a2ed4f68ab190c3bfad9c91e3)) 45 | 46 | 47 | ### BREAKING CHANGES 48 | 49 | * defaultProps have been removed from all function components. 50 | It should be backward compatible, but it's safer to release the major version. 51 | The default props of some internal class components have also been rewritten. 52 | Bumped react and react-dom to 18.2.0 in devDependencies. 53 | 54 | ## [1.10.1](https://github.com/chatscope/chat-ui-kit-react/compare/v1.10.0...v1.10.1) (2023-02-04) 55 | 56 | 57 | ### Bug Fixes 58 | 59 | * **typings:** expansion panel generic ([7bb6c0b](https://github.com/chatscope/chat-ui-kit-react/commit/7bb6c0b9c6da10a956e1637c041cbc5484d4ad33)) 60 | 61 | # [1.10.0](https://github.com/chatscope/chat-ui-kit-react/compare/v1.9.9...v1.10.0) (2023-02-04) 62 | 63 | 64 | ### Features 65 | 66 | * **expansion-panel:** added controlled opening state ([63c725b](https://github.com/chatscope/chat-ui-kit-react/commit/63c725bec4bf44406a1622caffdf759be184bbbc)) 67 | 68 | ## [1.9.9](https://github.com/chatscope/chat-ui-kit-react/compare/v1.9.8...v1.9.9) (2022-12-16) 69 | 70 | 71 | ### Bug Fixes 72 | 73 | * **types:** fixed typings for TypingIndicator, content can be a ReactNode ([4f5d2db](https://github.com/chatscope/chat-ui-kit-react/commit/4f5d2db26a7d5624042b689d27d70e09407cc43b)) 74 | 75 | ## [1.9.8](https://github.com/chatscope/chat-ui-kit-react/compare/v1.9.7...v1.9.8) (2022-11-16) 76 | 77 | 78 | ### Bug Fixes 79 | 80 | * **#76:** rendering lastSenderName as a ReactNode ([20d09fd](https://github.com/chatscope/chat-ui-kit-react/commit/20d09fdb433dd3cf50611962bca48032942e3191)), closes [#76](https://github.com/chatscope/chat-ui-kit-react/issues/76) 81 | 82 | ## [1.9.7](https://github.com/chatscope/chat-ui-kit-react/compare/v1.9.6...v1.9.7) (2022-07-18) 83 | 84 | 85 | ### Bug Fixes 86 | 87 | * **types:** fixed typings for ExpansionPanel ([9f9411f](https://github.com/chatscope/chat-ui-kit-react/commit/9f9411f77d031b0ccac9d4a17ba844e966a30c50)) 88 | 89 | ## [1.9.6](https://github.com/chatscope/chat-ui-kit-react/compare/v1.9.5...v1.9.6) (2022-07-13) 90 | 91 | 92 | ### Bug Fixes 93 | 94 | * **types:** added missing export of MessageListContent ([213b15d](https://github.com/chatscope/chat-ui-kit-react/commit/213b15d9229714528095748c57e7e04c67da9582)) 95 | 96 | ## [1.9.5](https://github.com/chatscope/chat-ui-kit-react/compare/v1.9.4...v1.9.5) (2022-07-12) 97 | 98 | 99 | ### Bug Fixes 100 | 101 | * **typings:** wrong TypingIndicator export ([de99cd8](https://github.com/chatscope/chat-ui-kit-react/commit/de99cd860e3baa9af7c3ad0145d397e28225c6c5)) 102 | 103 | ## [1.9.4](https://github.com/chatscope/chat-ui-kit-react/compare/v1.9.3...v1.9.4) (2022-07-12) 104 | 105 | 106 | ### Bug Fixes 107 | 108 | * [#71](https://github.com/chatscope/chat-ui-kit-react/issues/71) official support for react 18.2.0 ([cbab8b4](https://github.com/chatscope/chat-ui-kit-react/commit/cbab8b42c00f18b296e0bc4a55f3828f105f92f8)) 109 | 110 | ## [1.9.3](https://github.com/chatscope/chat-ui-kit-react/compare/v1.9.2...v1.9.3) (2022-06-15) 111 | 112 | 113 | ### Bug Fixes 114 | 115 | * **typings:** base components props intersection with html components props ([6637245](https://github.com/chatscope/chat-ui-kit-react/commit/663724576eed76a09cc832d61200076bd3c1e96e)) 116 | 117 | ## [1.9.2](https://github.com/chatscope/chat-ui-kit-react/compare/v1.9.1...v1.9.2) (2022-06-14) 118 | 119 | 120 | ### Bug Fixes 121 | 122 | * **typings:** renamed MessageGroup typings file ([59d374a](https://github.com/chatscope/chat-ui-kit-react/commit/59d374afc00b236ebbfe3081f02454a2f666f38e)) 123 | 124 | ## [1.9.1](https://github.com/chatscope/chat-ui-kit-react/compare/v1.9.0...v1.9.1) (2022-06-14) 125 | 126 | 127 | ### Bug Fixes 128 | 129 | * **typings:** added missing *.d.ts files to the build ([eeaa27e](https://github.com/chatscope/chat-ui-kit-react/commit/eeaa27e586d44dd6d8cf67c3b5be781cdf7c857c)) 130 | 131 | # [1.9.0](https://github.com/chatscope/chat-ui-kit-react/compare/v1.8.3...v1.9.0) (2022-06-14) 132 | 133 | 134 | ### Features 135 | 136 | * typescript typings ([12295ae](https://github.com/chatscope/chat-ui-kit-react/commit/12295ae762ad7f213c68000183a77f1df8d2bae0)) 137 | 138 | ## [1.8.3](https://github.com/chatscope/chat-ui-kit-react/compare/v1.8.2...v1.8.3) (2021-11-26) 139 | 140 | 141 | ### Bug Fixes 142 | 143 | * [#55](https://github.com/chatscope/chat-ui-kit-react/issues/55) changed lastactivitytime prop type to node ([7efcfb2](https://github.com/chatscope/chat-ui-kit-react/commit/7efcfb287dc1382893853ba4d4f74d03e0934bc5)) 144 | 145 | ## [1.8.2](https://github.com/chatscope/chat-ui-kit-react/compare/v1.8.1...v1.8.2) (2021-10-11) 146 | 147 | 148 | ### Bug Fixes 149 | 150 | * [#25](https://github.com/chatscope/chat-ui-kit-react/issues/25) official support for react 17 ([771eacb](https://github.com/chatscope/chat-ui-kit-react/commit/771eacb2fce597ac8ec7af6fd5a7b1a7820076f6)) 151 | 152 | ## [1.8.1](https://github.com/chatscope/chat-ui-kit-react/compare/v1.8.0...v1.8.1) (2021-06-03) 153 | 154 | 155 | ### Bug Fixes 156 | 157 | * **as:** custom component aliased with string is not displayed in production build ([cbf8a04](https://github.com/chatscope/chat-ui-kit-react/commit/cbf8a044c1789d4ffabc29a1fce8178585e7954e)), closes [#43](https://github.com/chatscope/chat-ui-kit-react/issues/43) 158 | 159 | # [1.8.0](https://github.com/chatscope/chat-ui-kit-react/compare/v1.7.2...v1.8.0) (2021-05-31) 160 | 161 | 162 | ### Features 163 | 164 | * **message-input:** prop to messageinput that prevents send on return ([98fc849](https://github.com/chatscope/chat-ui-kit-react/commit/98fc8498b10817d80b744946e0d0c4b27915e683)) 165 | 166 | ## [1.7.2](https://github.com/chatscope/chat-ui-kit-react/compare/v1.7.1...v1.7.2) (2021-05-16) 167 | 168 | ## [1.7.1](https://github.com/chatscope/chat-ui-kit-react/compare/v1.7.0...v1.7.1) (2021-05-16) 169 | 170 | # [1.7.0](https://github.com/chatscope/chat-ui-kit-react/compare/v1.6.1...v1.7.0) (2021-05-05) 171 | 172 | 173 | ### Features 174 | 175 | * **message-input:** added arguments to onChange and onSend ([9f8b2cc](https://github.com/chatscope/chat-ui-kit-react/commit/9f8b2ccc4996a47a8784ce12842d307adcb93a95)) 176 | 177 | ## [1.6.1](https://github.com/chatscope/chat-ui-kit-react/compare/v1.6.0...v1.6.1) (2021-04-19) 178 | 179 | 180 | ### Bug Fixes 181 | 182 | * **message-list:** disableOnYReachWhenNoScroll property type ([caeeead](https://github.com/chatscope/chat-ui-kit-react/commit/caeeeada83f18ee2ddde74119078a42ce1e3626a)) 183 | 184 | # [1.6.0](https://github.com/chatscope/chat-ui-kit-react/compare/v1.5.2...v1.6.0) (2021-04-19) 185 | 186 | 187 | ### Features 188 | 189 | * **message-list:** added disableOnYReachWhenNoScroll property ([3176f2e](https://github.com/chatscope/chat-ui-kit-react/commit/3176f2ec276e5cfdf157dd0a2aacd5f02eb008f7)) 190 | 191 | ## [1.5.2](https://github.com/chatscope/chat-ui-kit-react/compare/v1.5.1...v1.5.2) (2021-04-12) 192 | 193 | 194 | ### Bug Fixes 195 | 196 | * **messageinput, commenteditable:** fix bug that last korean character is entered twice ([2986e2b](https://github.com/chatscope/chat-ui-kit-react/commit/2986e2bb5375eafbd5a7f96e2c5341eaedeca248)) 197 | 198 | ## [1.5.1](https://github.com/chatscope/chat-ui-kit-react/compare/v1.5.0...v1.5.1) (2021-04-03) 199 | 200 | 201 | ### Bug Fixes 202 | 203 | * **message list:** cannot read property 'clientHeight' of null ([705ede2](https://github.com/chatscope/chat-ui-kit-react/commit/705ede25c395ed3e5bc236463af7945d2394d7d5)) 204 | 205 | # [1.5.0](https://github.com/chatscope/chat-ui-kit-react/compare/v1.4.0...v1.5.0) (2021-03-24) 206 | 207 | 208 | ### Features 209 | 210 | * **message list:** added autoScrollToBottomOnMount property ([6ba92a8](https://github.com/chatscope/chat-ui-kit-react/commit/6ba92a85db8c15d56ac2a51c149d87886ab78beb)) 211 | 212 | # [1.4.0](https://github.com/chatscope/chat-ui-kit-react/compare/v1.3.0...v1.4.0) (2021-03-21) 213 | 214 | 215 | ### Features 216 | 217 | * **message-list:** loading more loader at the bottom ([e540cbc](https://github.com/chatscope/chat-ui-kit-react/commit/e540cbc9c6db25657207669fe8cd89b8036da064)) 218 | 219 | # [1.3.0](https://github.com/chatscope/chat-ui-kit-react/compare/v1.2.3...v1.3.0) (2021-02-14) 220 | 221 | 222 | ### Features 223 | 224 | * **conversation-list:** loading more in conversation list ([e106ccb](https://github.com/chatscope/chat-ui-kit-react/commit/e106ccbef7727317ef02017a26c860784aa40cde)) 225 | 226 | ## [1.2.3](https://github.com/chatscope/chat-ui-kit-react/compare/v1.2.2...v1.2.3) (2021-02-06) 227 | 228 | 229 | ### Bug Fixes 230 | 231 | * **message-list:** scroll handling for grouped messages ([2c8c9be](https://github.com/chatscope/chat-ui-kit-react/commit/2c8c9becfa9b04c75b70ca0e967632fe798f0247)) 232 | 233 | ## [1.2.2](https://github.com/chatscope/chat-ui-kit-react/compare/v1.2.1...v1.2.2) (2021-01-24) 234 | 235 | 236 | ### Bug Fixes 237 | 238 | * **utils:** handling as attribute for forwarded ref ([73b95ab](https://github.com/chatscope/chat-ui-kit-react/commit/73b95ab7db74d5488e69507553b27916fda814fc)) 239 | * **utils:** is attribute changed to as ([8eca5d4](https://github.com/chatscope/chat-ui-kit-react/commit/8eca5d4b58bdfea689fcc15bb5ea81101fca7f88)) 240 | 241 | ## [1.2.1](https://github.com/chatscope/chat-ui-kit-react/compare/v1.2.0...v1.2.1) (2021-01-17) 242 | 243 | 244 | ### Bug Fixes 245 | 246 | * **messagelist:** resizeobserver has been disabled in browsers that do not support it ([a75859a](https://github.com/chatscope/chat-ui-kit-react/commit/a75859a5e0be9f8da3d8bd59692e027f993b26b4)) 247 | 248 | # [1.2.0](https://github.com/chatscope/chat-ui-kit-react/compare/v1.1.0...v1.2.0) (2021-01-04) 249 | 250 | 251 | ### Features 252 | 253 | * **#6:** api method for scrolling to bottom in message input ([3d0997a](https://github.com/chatscope/chat-ui-kit-react/commit/3d0997ac737d7943f638a3e9b396107cee37423e)), closes [#6](https://github.com/chatscope/chat-ui-kit-react/issues/6) 254 | 255 | # [1.1.0](https://github.com/chatscope/chat-ui-kit-react/compare/v1.0.3...v1.1.0) (2020-12-20) 256 | 257 | 258 | ### Features 259 | 260 | * added new message content types ([32439f7](https://github.com/chatscope/chat-ui-kit-react/commit/32439f703361ac951522f8892a01982b8e16ccf8)) 261 | 262 | ## [1.0.3](https://github.com/chatscope/chat-ui-kit-react/compare/v1.0.2...v1.0.3) (2020-11-10) 263 | 264 | 265 | ### Bug Fixes 266 | 267 | * better scroll handling ([6f60d88](https://github.com/chatscope/chat-ui-kit-react/commit/6f60d8867513f017c999691a3c1d30e1b46c6748)) 268 | 269 | ## [1.0.2](https://github.com/chatscope/chat-ui-kit-react/compare/v1.0.1...v1.0.2) (2020-09-23) 270 | 271 | 272 | ### Performance Improvements 273 | 274 | * changed fa imports for better tree-shaking ([4807854](https://github.com/chatscope/chat-ui-kit-react/commit/4807854f782b2a16d8e60db6bce59848bed91c1d)) 275 | 276 | ## [1.0.1](https://github.com/chatscope/chat-ui-kit-react/compare/v1.0.0...v1.0.1) (2020-09-22) 277 | 278 | # 1.0.0 (2020-09-22) 279 | 280 | 281 | ### chore 282 | 283 | * **release:** first release ([12e810a](https://github.com/chatscope/chat-ui-kit-react/commit/12e810a0b6562692ace9e6f300e78a88a3ef43a2)) 284 | 285 | 286 | ### BREAKING CHANGES 287 | 288 | * **release:** First stable release 289 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | Each component that is used as an element of the type parameter of the getChildren function must have **displayName** static property: 2 | https://github.com/chatscope/chat-ui-kit-react/issues/43 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020-2022 chatscope.io 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Chat UI Kit React 2 | 3 | [![Actions Status](https://github.com/chatscope/chat-ui-kit-react/workflows/build/badge.svg)](https://github.com/chatscope/chat-ui-kit-react/actions) [![npm version](https://img.shields.io/npm/v/@chatscope/chat-ui-kit-react.svg?style=flat)](https://npmjs.com/@chatscope/chat-ui-kit-react) [![](https://img.shields.io/npm/l/@chatscope/chat-ui-kit-react?dummy=unused)](https://github.com/chatscope/chat-ui-kit-react/blob/master/LICENSE) [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](https://github.com/prettier/prettier) [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release) [![Storybook](https://cdn.jsdelivr.net/gh/storybookjs/brand@master/badge/badge-storybook.svg)](https://chatscope.io/storybook/react/) 4 | 5 | Build your own chat UI with React components in a few minutes. 6 | The Chat UI Kit from chatscope is an open source UI toolkit for developing web chat applications. 7 | 8 | Tired of struggling with sticky scrollbars, contenteditable, responsiveness, css hacks? 9 | This kit is for you! [See all features](https://chatscope.io/features). 10 | 11 | **Chat UI Kit brings you chat UI development at warp speed** 🚀 12 | 13 | ## Demo 14 | 15 | - Full featured chat application: [https://demo.chatscope.io](https://demo.chatscope.io) 16 | - Zoe, Akane, Eliot and Joe: [https://chatscope.io/demo/chat-friends](https://chatscope.io/demo/chat-friends/) 17 | - Chat with the Martian (he is available sometimes): [https://mars.chatscope.io](https://mars.chatscope.io) 18 | 19 | Demos index: [https://chatscope.io/demo](https://chatscope.io/demo/). 20 | 21 | ## Install 22 | 23 | **Component library** 24 | 25 | Using yarn: 26 | 27 | ```sh 28 | yarn add @chatscope/chat-ui-kit-react 29 | ``` 30 | 31 | Using npm: 32 | 33 | ```sh 34 | npm install @chatscope/chat-ui-kit-react 35 | ``` 36 | 37 | **Styles** 38 | 39 | Using yarn: 40 | 41 | ```sh 42 | yarn add @chatscope/chat-ui-kit-styles 43 | ``` 44 | 45 | Using npm: 46 | 47 | ```sh 48 | npm install @chatscope/chat-ui-kit-styles 49 | ``` 50 | 51 | ## Usage 52 | 53 | ### ESM 54 | 55 | ```jsx 56 | import styles from "@chatscope/chat-ui-kit-styles/dist/default/styles.min.css"; 57 | import { 58 | MainContainer, 59 | ChatContainer, 60 | MessageList, 61 | Message, 62 | MessageInput, 63 | } from "@chatscope/chat-ui-kit-react"; 64 | 65 |
66 | 67 | 68 | 69 | 76 | 77 | 78 | 79 | 80 |
; 81 | ``` 82 | 83 | Yeah! Your first chat GUI is ready! 84 | 85 | ### UMD 86 | 87 | UMD usage is documented in our [Storybook](https://chatscope.io/storybook/react/). 88 | 89 | ## Documentation 90 | 91 | Check our friendly [Storybook](https://chatscope.io/storybook/react/). 92 | 93 | ## Typescript 94 | 95 | The library is written in Javascript, but Typescript typings are available in the package since version 1.9.3. 96 | 97 | ## See also 98 | 99 | [@chatscope/use-chat](https://github.com/chatscope/use-chat) is a React hook for state management in chat applications. 100 | Check it out and see how easy you can do the chat logic yourself. 101 | 102 | ## Show your support 103 | 104 | If you've made an awesome chat UI and you love this library, please ⭐ this repository! 105 | 106 | ## Community and support 107 | 108 | - Twitting via [@chatscope](https://twitter.com/chatscope) 109 | - Chatting at [Discord](https://discord.gg/TkUYWQRf2M) 110 | - Facebooking at [Facebook](https://www.facebook.com/chatscope) 111 | - Articles on the [chatscope blog](https://chatscope.io/blog/) 112 | 113 | ## Website 114 | 115 | [https://chatscope.io](https://chatscope.io) 116 | 117 | ## License 118 | 119 | [MIT](https://github.com/chatscope/chat-ui-kit-react/blob/master/LICENSE) 120 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | // ESM Build 4 | esm: { 5 | exclude: ["node_modules/**"], 6 | presets: [ 7 | [ 8 | "@babel/preset-env", 9 | { 10 | modules: false, // Code is written as ES2015 - no need to transform modules 11 | }, 12 | ], 13 | "@babel/preset-react", 14 | ], 15 | plugins: [ 16 | "@babel/plugin-proposal-class-properties", 17 | 18 | [ 19 | "transform-react-remove-prop-types", 20 | { 21 | mode: "unsafe-wrap", 22 | ignoreFilenames: ["node_modules"], 23 | }, 24 | ], 25 | ], 26 | }, 27 | 28 | // CommonJs build 29 | cjs: { 30 | exclude: ["node_modules/**"], 31 | presets: [ 32 | [ 33 | "@babel/preset-env", 34 | { 35 | modules: "commonjs", // Transform modules to CommonJs 36 | }, 37 | ], 38 | "@babel/preset-react", 39 | ], 40 | plugins: [ 41 | "@babel/plugin-proposal-class-properties", 42 | [ 43 | "transform-react-remove-prop-types", 44 | { 45 | mode: "unsafe-wrap", // Wrap propTypes in condition to remove by webpack in production build 46 | ignoreFilenames: ["node_modules"], 47 | }, 48 | ], 49 | ], 50 | }, 51 | }, 52 | }; 53 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["@commitlint/config-conventional"], 3 | ignores: [(message) => message.indexOf("WIP:") === 0], 4 | }; 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@chatscope/chat-ui-kit-react", 3 | "version": "2.1.1", 4 | "description": "React component library for creating chat interfaces", 5 | "license": "MIT", 6 | "homepage": "https://chatscope.io/", 7 | "keywords": [ 8 | "chat", 9 | "react", 10 | "reactjs", 11 | "ui", 12 | "user interface", 13 | "components", 14 | "ui kit", 15 | "communication", 16 | "conversation", 17 | "toolkit", 18 | "library", 19 | "frontend", 20 | "reusable", 21 | "feed", 22 | "comments", 23 | "social", 24 | "talk" 25 | ], 26 | "repository": { 27 | "type": "git", 28 | "url": "https://github.com/chatscope/chat-ui-kit-react.git" 29 | }, 30 | "main": "dist/cjs/index.js", 31 | "module": "dist/es/index.js", 32 | "types": "src/types/index.d.ts", 33 | "peerDependencies": { 34 | "prop-types": "^15.7.2", 35 | "react": "^16.12.0 || ^17.0.0 || ^18.2.0 || ^19.0.0", 36 | "react-dom": "^16.12.0 || ^17.0.0 || ^18.2.0 || ^19.0.0" 37 | }, 38 | "devDependencies": { 39 | "@babel/cli": "7.10.5", 40 | "@babel/core": "7.11.4", 41 | "@babel/plugin-proposal-class-properties": "7.10.4", 42 | "@babel/preset-env": "7.11.5", 43 | "@babel/preset-react": "7.10.4", 44 | "@commitlint/cli": "11.0.0", 45 | "@commitlint/config-conventional": "11.0.0", 46 | "@rollup/plugin-babel": "5.2.0", 47 | "@rollup/plugin-commonjs": "11.1.0", 48 | "@rollup/plugin-node-resolve": "7.1.3", 49 | "@semantic-release/changelog": "6.0.3", 50 | "@semantic-release/git": "10.0.1", 51 | "@semantic-release/github": "9.2.6", 52 | "@typescript-eslint/eslint-plugin": "^5.9.1", 53 | "@typescript-eslint/parser": "^5.9.1", 54 | "babel-eslint": "10.1.0", 55 | "babel-plugin-transform-react-remove-prop-types": "0.4.24", 56 | "chokidar-cli": "2.1.0", 57 | "eslint": "8.6.0", 58 | "eslint-plugin-jsx-a11y": "6.5.1", 59 | "eslint-plugin-react": "7.28.0", 60 | "eslint-plugin-react-hooks": "^4.3.0", 61 | "husky": "4.3.0", 62 | "lint-staged": "10.4.0", 63 | "prettier": "2.1.2", 64 | "react": "^18.2.0", 65 | "react-dom": "^18.2.0", 66 | "rollup": "2.26.5", 67 | "rollup-plugin-peer-deps-external": "2.2.3", 68 | "rollup-plugin-terser": "5.3.0", 69 | "semantic-release": "23.0.2", 70 | "typescript": "^4.5.4" 71 | }, 72 | "scripts": { 73 | "build:clean": "rm -Rf dist", 74 | "build:umd": "rollup -c", 75 | "build:cjs": "BABEL_ENV=cjs babel src/components -d dist/cjs", 76 | "build:esm": "BABEL_ENV=esm babel src/components -d dist/es", 77 | "build": "yarn run build:clean && yarn run build:esm && yarn run build:cjs && yarn run build:umd", 78 | "pack": "yarn pack", 79 | "watch": "chokidar 'src/**/*.*' -c 'yarn run build:esm'" 80 | }, 81 | "dependencies": { 82 | "@chatscope/chat-ui-kit-styles": "^1.2.0", 83 | "@fortawesome/fontawesome-free": "^6.5.2", 84 | "@fortawesome/fontawesome-svg-core": "^6.5.2", 85 | "@fortawesome/free-solid-svg-icons": "^6.5.2", 86 | "@fortawesome/react-fontawesome": "^0.2.2", 87 | "classnames": "^2.2.6", 88 | "prop-types": "^15.7.2" 89 | }, 90 | "husky": { 91 | "hooks": { 92 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS", 93 | "pre-commit": "lint-staged" 94 | } 95 | }, 96 | "lint-staged": { 97 | "*.{js,css,md,jsx}": "prettier --write" 98 | }, 99 | "files": [ 100 | "dist", 101 | "src/**/*.d.ts" 102 | ], 103 | "publishConfig": { 104 | "access": "public" 105 | }, 106 | "release": { 107 | "plugins": [ 108 | [ 109 | "@semantic-release/commit-analyzer", 110 | { 111 | "preset": "angular", 112 | "releaseRules": [ 113 | { 114 | "type": "docs", 115 | "scope": "readme", 116 | "release": "patch" 117 | }, 118 | { 119 | "scope": "no-release", 120 | "release": false 121 | } 122 | ] 123 | } 124 | ], 125 | "@semantic-release/release-notes-generator", 126 | [ 127 | "@semantic-release/changelog", 128 | { 129 | "changelogFile": "CHANGELOG.md", 130 | "changelogTitle": "# @chatscope/chat-ui-kit-react changelog" 131 | } 132 | ], 133 | "@semantic-release/github", 134 | "@semantic-release/npm", 135 | [ 136 | "@semantic-release/git", 137 | { 138 | "assets": [ 139 | "package.json", 140 | "CHANGELOG.md" 141 | ], 142 | "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}" 143 | } 144 | ] 145 | ] 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import resolve from "@rollup/plugin-node-resolve"; 2 | import commonjs from "@rollup/plugin-commonjs"; 3 | import babel from "@rollup/plugin-babel"; 4 | import peerDepsExternal from "rollup-plugin-peer-deps-external"; 5 | import { terser } from "rollup-plugin-terser"; 6 | 7 | export default [ 8 | // browser-friendly UMD build 9 | { 10 | input: "src/components/index.js", 11 | output: { 12 | name: "ChatUiKitReact", 13 | file: "dist/chat-ui-kit-react.min.js", 14 | format: "umd", 15 | noConflict: true, 16 | globals: { 17 | react: "React", 18 | "react-dom": "ReactDOM", 19 | "prop-types": "PropTypes", 20 | }, 21 | }, 22 | 23 | plugins: [ 24 | peerDepsExternal(), 25 | resolve({ 26 | extensions: [".mjs", ".js", ".json", ".node", ".jsx"], 27 | }), 28 | commonjs(), 29 | babel({ 30 | exclude: "node_modules/**", 31 | presets: ["@babel/preset-env", "@babel/preset-react"], 32 | babelHelpers: "bundled", 33 | compact: true, 34 | plugins: [ 35 | "@babel/plugin-proposal-class-properties", 36 | [ 37 | "transform-react-remove-prop-types", 38 | { 39 | mode: "remove", 40 | removeImport: true, 41 | ignoreFilenames: ["node_modules"], 42 | }, 43 | ], 44 | ], 45 | }), 46 | terser(), 47 | ], 48 | }, 49 | ]; 50 | -------------------------------------------------------------------------------- /scripts/terser.js: -------------------------------------------------------------------------------- 1 | // Warning this script is not used for now 2 | const Terser = require("terser"); 3 | const fs = require("fs"); 4 | const path = require("path"); 5 | 6 | function getAllFiles(dirPath, arrayOfFiles) { 7 | let files = fs.readdirSync(dirPath); 8 | 9 | arrayOfFiles = arrayOfFiles || []; 10 | 11 | files.forEach(function (file) { 12 | if (fs.statSync(dirPath + "/" + file).isDirectory()) { 13 | arrayOfFiles = getAllFiles(dirPath + "/" + file, arrayOfFiles); 14 | } else { 15 | arrayOfFiles.push(path.join(dirPath, "/", file)); 16 | } 17 | }); 18 | 19 | return arrayOfFiles.filter((path) => path.match(/\.js$/)); 20 | } 21 | 22 | function minifyFiles(filePaths) { 23 | filePaths.forEach((filePath) => { 24 | fs.writeFileSync( 25 | filePath, 26 | Terser.minify(fs.readFileSync(filePath, "utf8")).code 27 | ); 28 | }); 29 | } 30 | 31 | const files = getAllFiles("./es/"); 32 | minifyFiles(files); 33 | -------------------------------------------------------------------------------- /src/components/Avatar/Avatar.d.ts: -------------------------------------------------------------------------------- 1 | import type {Size, UserStatus, ChatComponentPropsChildrenRef} from "../../types"; 2 | import type {ReactElement} from "react"; 3 | 4 | export interface AvatarProps { 5 | name?:string; 6 | src?:string; 7 | size?: Size; 8 | status?: UserStatus; 9 | active?: boolean; 10 | } 11 | 12 | export declare const Avatar: (props:ChatComponentPropsChildrenRef) => ReactElement; 13 | 14 | export default Avatar; -------------------------------------------------------------------------------- /src/components/Avatar/Avatar.jsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, forwardRef, useImperativeHandle } from "react"; 2 | import PropTypes from "prop-types"; 3 | import { prefix } from "../settings"; 4 | import classNames from "classnames"; 5 | import { Status } from "../Status/Status"; 6 | import { SizeEnum, StatusEnum } from "../enums"; 7 | 8 | function AvatarInner( 9 | { name = "", src = "", size = "md", status, className, active = false, children, ...rest }, 10 | ref 11 | ) { 12 | const cName = `${prefix}-avatar`; 13 | const sizeClass = typeof size !== "undefined" ? ` ${cName}--${size}` : ""; 14 | 15 | const avatarRef = useRef(); 16 | 17 | useImperativeHandle(ref, () => ({ 18 | focus: () => avatarRef.current.focus(), 19 | })); 20 | 21 | return ( 22 |
31 | {children ? ( 32 | children 33 | ) : ( 34 | <> 35 | {name} 36 | {typeof status === "string" && ( 37 | 38 | )}{" "} 39 | 40 | )} 41 |
42 | ); 43 | } 44 | 45 | const Avatar = forwardRef(AvatarInner); 46 | Avatar.displayName = "Avatar"; 47 | 48 | Avatar.propTypes = { 49 | /** Primary content */ 50 | children: PropTypes.node, 51 | 52 | /** 53 | * User name/nickname/full name for displaying initials and image alt description 54 | */ 55 | name: PropTypes.string, 56 | 57 | /** Avatar image source */ 58 | src: PropTypes.string, 59 | 60 | /** Size */ 61 | size: PropTypes.oneOf(SizeEnum), 62 | 63 | /** Status. */ 64 | status: PropTypes.oneOf(StatusEnum), 65 | 66 | /** Active */ 67 | active: PropTypes.bool, 68 | 69 | /** Additional classes. */ 70 | className: PropTypes.string, 71 | }; 72 | 73 | AvatarInner.propTypes = Avatar.propTypes; 74 | 75 | export { Avatar }; 76 | export default Avatar; 77 | -------------------------------------------------------------------------------- /src/components/Avatar/index.js: -------------------------------------------------------------------------------- 1 | import Avatar from "./Avatar"; 2 | export * from "./Avatar"; 3 | export default Avatar; 4 | -------------------------------------------------------------------------------- /src/components/AvatarGroup/AvatarGroup.d.ts: -------------------------------------------------------------------------------- 1 | import type {ReactElement} from "react"; 2 | import type {ChatComponentPropsChildren, Size} from "../../types"; 3 | 4 | export interface AvatarGroupProps { 5 | max?:number; 6 | size?:Size; 7 | activeIndex?:number; 8 | hoverToFront?:boolean; 9 | } 10 | 11 | export declare const AvatarGroup: (props:ChatComponentPropsChildren) => ReactElement; 12 | 13 | export default AvatarGroup; -------------------------------------------------------------------------------- /src/components/AvatarGroup/AvatarGroup.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { allowedChildren } from "../utils"; 4 | import { prefix } from "../settings"; 5 | import classNames from "classnames"; 6 | import Avatar from "../Avatar"; 7 | 8 | export const AvatarGroup = ({ 9 | children, 10 | size = "md", 11 | className, 12 | max, 13 | activeIndex, 14 | hoverToFront, 15 | ...rest 16 | }) => { 17 | const cName = `${prefix}-avatar-group`; 18 | 19 | // Reverse because of css 20 | const avatars = 21 | typeof max === "number" && React.Children.count(children) > max 22 | ? React.Children.toArray(children).reverse().slice(0, max) 23 | : React.Children.toArray(children).reverse(); 24 | const reversedActiveIndex = 25 | typeof activeIndex === "number" 26 | ? avatars.length - activeIndex - 1 27 | : undefined; 28 | 29 | return ( 30 |
34 | {avatars.map((a, i) => { 35 | const newProps = 36 | typeof reversedActiveIndex === "number" 37 | ? { active: reversedActiveIndex === i } 38 | : {}; 39 | 40 | if (hoverToFront === true) { 41 | newProps.className = classNames( 42 | `${prefix}-avatar--active-on-hover`, 43 | a.props.className 44 | ); 45 | } 46 | 47 | return React.cloneElement(a, newProps); 48 | })} 49 |
50 | ); 51 | }; 52 | 53 | AvatarGroup.displayName = "AvatarGroup"; 54 | 55 | AvatarGroup.propTypes = { 56 | /** 57 | * Primary content. 58 | * Allowed node: 59 | * 60 | * * <Avatar /> 61 | */ 62 | children: allowedChildren([Avatar]), 63 | 64 | /** Additional classes. */ 65 | className: PropTypes.string, 66 | 67 | /** Maximum stacked children */ 68 | max: PropTypes.number, 69 | 70 | /** Size */ 71 | size: PropTypes.oneOf(["xs", "sm", "md", "lg", "fluid"]), 72 | 73 | /** Active index. 74 | * Active element has higher z-index independent of its order. 75 | */ 76 | activeIndex: PropTypes.number, 77 | 78 | /** Bring to front on hover */ 79 | hoverToFront: PropTypes.bool, 80 | }; 81 | 82 | export default AvatarGroup; 83 | -------------------------------------------------------------------------------- /src/components/AvatarGroup/index.js: -------------------------------------------------------------------------------- 1 | import AvatarGroup from "./AvatarGroup"; 2 | export * from "./AvatarGroup"; 3 | export default AvatarGroup; 4 | -------------------------------------------------------------------------------- /src/components/Buttons/AddUserButton.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import classNames from "classnames"; 4 | import { prefix } from "../settings"; 5 | import Button from "./Button"; 6 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 7 | import { faUserPlus } from "@fortawesome/free-solid-svg-icons/faUserPlus"; 8 | 9 | export const AddUserButton = ({ className = "", children, ...rest }) => { 10 | const cName = `${prefix}-button--adduser`; 11 | 12 | return ( 13 | 20 | ); 21 | }; 22 | 23 | AddUserButton.propTypes = { 24 | /** 25 | * Primary content. 26 | */ 27 | children: PropTypes.node, 28 | 29 | /** Additional classes. */ 30 | className: PropTypes.string, 31 | }; 32 | 33 | export default AddUserButton; 34 | -------------------------------------------------------------------------------- /src/components/Buttons/ArrowButton.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import classNames from "classnames"; 4 | import { prefix } from "../settings"; 5 | import Button from "./Button"; 6 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 7 | import { faArrowUp } from "@fortawesome/free-solid-svg-icons/faArrowUp"; 8 | import { faArrowRight } from "@fortawesome/free-solid-svg-icons/faArrowRight"; 9 | import { faArrowDown } from "@fortawesome/free-solid-svg-icons/faArrowDown"; 10 | import { faArrowLeft } from "@fortawesome/free-solid-svg-icons/faArrowLeft"; 11 | 12 | export const ArrowButton = ({ className = "", direction = "right", children, ...rest }) => { 13 | const cName = `${prefix}-button--arrow`; 14 | 15 | const icon = (() => { 16 | if (direction === "up") { 17 | return faArrowUp; 18 | } else if (direction === "right") { 19 | return faArrowRight; 20 | } else if (direction === "down") { 21 | return faArrowDown; 22 | } else if (direction === "left") { 23 | return faArrowLeft; 24 | } 25 | })(); 26 | 27 | return ( 28 | 35 | ); 36 | }; 37 | 38 | ArrowButton.propTypes = { 39 | /** 40 | * Primary content. 41 | */ 42 | children: PropTypes.node, 43 | 44 | /** Additional classes. */ 45 | className: PropTypes.string, 46 | 47 | direction: PropTypes.oneOf(["up", "right", "down", "left"]), 48 | }; 49 | 50 | export default ArrowButton; 51 | -------------------------------------------------------------------------------- /src/components/Buttons/AttachmentButton.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import classNames from "classnames"; 4 | import { prefix } from "../settings"; 5 | import Button from "./Button"; 6 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 7 | import { faPaperclip } from "@fortawesome/free-solid-svg-icons/faPaperclip"; 8 | 9 | export const AttachmentButton = ({ className = "", children, ...rest }) => { 10 | const cName = `${prefix}-button--attachment`; 11 | 12 | return ( 13 | 20 | ); 21 | }; 22 | 23 | AttachmentButton.propTypes = { 24 | /** Primary content. */ 25 | children: PropTypes.node, 26 | 27 | /** Additional classes. */ 28 | className: PropTypes.string, 29 | }; 30 | 31 | export default AttachmentButton; 32 | -------------------------------------------------------------------------------- /src/components/Buttons/Button.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import classNames from "classnames"; 4 | import { prefix } from "../settings"; 5 | 6 | export const Button = ({ 7 | children = undefined, 8 | className = "", 9 | icon = undefined, 10 | border = false, 11 | labelPosition = undefined, 12 | ...rest 13 | }) => { 14 | const cName = `${prefix}-button`; 15 | 16 | const lPos = typeof labelPosition !== "undefined" ? labelPosition : "right"; 17 | const labelPositionClassName = 18 | React.Children.count(children) > 0 ? `${cName}--${lPos}` : ""; 19 | const borderClassName = border === true ? `${cName}--border` : ""; 20 | return ( 21 | 34 | ); 35 | }; 36 | 37 | Button.propTypes = { 38 | /** Primary content */ 39 | children: PropTypes.node, 40 | /** Additional classes. */ 41 | className: PropTypes.string, 42 | icon: PropTypes.node, 43 | labelPosition: PropTypes.oneOf(["left", "right"]), 44 | border: PropTypes.bool, 45 | }; 46 | 47 | export default Button; 48 | -------------------------------------------------------------------------------- /src/components/Buttons/Buttons.d.ts: -------------------------------------------------------------------------------- 1 | import type {ReactElement, ReactNode} from "react"; 2 | import type {ChatComponentPropsChildren} from "../../types"; 3 | 4 | export interface ButtonProps { 5 | icon?:ReactNode; 6 | labelPosition?: "left" | "right"; 7 | border?: boolean; 8 | } 9 | 10 | export declare const Button: (props:ChatComponentPropsChildren) => ReactElement; 11 | 12 | export type AddUserButtonProps = Omit; 13 | 14 | export declare const AddUserButton: (props:ChatComponentPropsChildren) => ReactElement; 15 | 16 | export interface ArrowButtonProps extends Omit { 17 | direction?: "up" | "right" | "down" | "left" 18 | } 19 | 20 | export declare const ArrowButton: (props:ChatComponentPropsChildren) => ReactElement; 21 | 22 | export type AttachmentButtonProps = Omit; 23 | 24 | export declare const AttachmentButton: (props:ChatComponentPropsChildren) => ReactElement; 25 | 26 | export interface EllipsisButtonProps extends Omit { 27 | orientation?: "horizontal" | "vertical" 28 | } 29 | 30 | export declare const EllipsisButton: (props:ChatComponentPropsChildren) => ReactElement; 31 | 32 | export type InfoButtonProps = Omit; 33 | 34 | export declare const InfoButton: (props:ChatComponentPropsChildren) => ReactElement; 35 | 36 | export type SendButtonProps = Omit; 37 | 38 | export declare const SendButton: (props:ChatComponentPropsChildren) => ReactElement; 39 | 40 | export type StarButtonProps = Omit; 41 | 42 | export declare const StarButton: (props:ChatComponentPropsChildren) => ReactElement; 43 | 44 | export type VideoCallButtonProps = Omit; 45 | 46 | export declare const VideoCallButton: (props:ChatComponentPropsChildren) => ReactElement; 47 | 48 | export type VoiceCallButtonProps = Omit; 49 | 50 | export declare const VoiceCallButton: (props:ChatComponentPropsChildren) => ReactElement; -------------------------------------------------------------------------------- /src/components/Buttons/EllipsisButton.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import classNames from "classnames"; 4 | import { prefix } from "../settings"; 5 | import Button from "./Button"; 6 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 7 | import { faEllipsisV } from "@fortawesome/free-solid-svg-icons/faEllipsisV"; 8 | import { faEllipsisH } from "@fortawesome/free-solid-svg-icons/faEllipsisH"; 9 | 10 | export const EllipsisButton = ({ 11 | className = "", 12 | orientation = "horizontal", 13 | children, 14 | ...rest 15 | }) => { 16 | const cName = `${prefix}-button--ellipsis`; 17 | 18 | const icon = orientation === "vertical" ? faEllipsisV : faEllipsisH; 19 | 20 | return ( 21 | 28 | ); 29 | }; 30 | 31 | EllipsisButton.propTypes = { 32 | /** Primary content. */ 33 | children: PropTypes.node, 34 | 35 | /** Additional classes. */ 36 | className: PropTypes.string, 37 | 38 | orientation: PropTypes.oneOf(["horizontal", "vertical"]), 39 | }; 40 | 41 | export default EllipsisButton; 42 | -------------------------------------------------------------------------------- /src/components/Buttons/InfoButton.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import classNames from "classnames"; 4 | import { prefix } from "../settings"; 5 | import Button from "./Button"; 6 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 7 | import { faInfoCircle } from "@fortawesome/free-solid-svg-icons/faInfoCircle"; 8 | 9 | export const InfoButton = ({ className = "", children, ...rest }) => { 10 | const cName = `${prefix}-button--info`; 11 | 12 | return ( 13 | 20 | ); 21 | }; 22 | 23 | InfoButton.propTypes = { 24 | /** Primary content. */ 25 | children: PropTypes.node, 26 | 27 | /** Additional classes. */ 28 | className: PropTypes.string, 29 | }; 30 | 31 | export default InfoButton; 32 | -------------------------------------------------------------------------------- /src/components/Buttons/SendButton.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import classNames from "classnames"; 4 | import { prefix } from "../settings"; 5 | import Button from "./Button"; 6 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 7 | import { faPaperPlane } from "@fortawesome/free-solid-svg-icons/faPaperPlane"; 8 | 9 | export const SendButton = ({ className = "", children, ...rest }) => { 10 | const cName = `${prefix}-button--send`; 11 | 12 | return ( 13 | 20 | ); 21 | }; 22 | 23 | SendButton.propTypes = { 24 | /** Primary content. */ 25 | children: PropTypes.node, 26 | 27 | /** Additional classes. */ 28 | className: PropTypes.string, 29 | }; 30 | 31 | export default SendButton; 32 | -------------------------------------------------------------------------------- /src/components/Buttons/StarButton.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import classNames from "classnames"; 4 | import { prefix } from "../settings"; 5 | import Button from "./Button"; 6 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 7 | import { faStar } from "@fortawesome/free-solid-svg-icons/faStar"; 8 | 9 | export const StarButton = ({ className = "", children, ...rest }) => { 10 | const cName = `${prefix}-button--star`; 11 | 12 | return ( 13 | 20 | ); 21 | }; 22 | 23 | StarButton.propTypes = { 24 | /** Primary content. */ 25 | children: PropTypes.node, 26 | 27 | /** Additional classes. */ 28 | className: PropTypes.string, 29 | }; 30 | 31 | export default StarButton; 32 | -------------------------------------------------------------------------------- /src/components/Buttons/VideoCallButton.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import classNames from "classnames"; 4 | import { prefix } from "../settings"; 5 | import Button from "./Button"; 6 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 7 | import { faVideo } from "@fortawesome/free-solid-svg-icons/faVideo"; 8 | 9 | export const VideoCallButton = ({ className = "", children, ...rest }) => { 10 | const cName = `${prefix}-button--videocall`; 11 | 12 | return ( 13 | 20 | ); 21 | }; 22 | 23 | VideoCallButton.propTypes = { 24 | /** Primary content. */ 25 | children: PropTypes.node, 26 | 27 | /** Additional classes. */ 28 | className: PropTypes.string, 29 | }; 30 | 31 | 32 | export default VideoCallButton; 33 | -------------------------------------------------------------------------------- /src/components/Buttons/VoiceCallButton.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import classNames from "classnames"; 4 | import { prefix } from "../settings"; 5 | import Button from "./Button"; 6 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 7 | import { faPhoneAlt } from "@fortawesome/free-solid-svg-icons/faPhoneAlt"; 8 | 9 | export const VoiceCallButton = ({ className = "", children, ...rest }) => { 10 | const cName = `${prefix}-button--voicecall`; 11 | 12 | return ( 13 | 20 | ); 21 | }; 22 | 23 | VoiceCallButton.propTypes = { 24 | /** Primary content. */ 25 | children: PropTypes.node, 26 | 27 | /** Additional classes. */ 28 | className: PropTypes.string, 29 | }; 30 | 31 | export default VoiceCallButton; 32 | -------------------------------------------------------------------------------- /src/components/Buttons/index.js: -------------------------------------------------------------------------------- 1 | import Button from "./Button"; 2 | import ArrowButton from "./ArrowButton"; 3 | import InfoButton from "./InfoButton"; 4 | import VoiceCallButton from "./VoiceCallButton"; 5 | import VideoCallButton from "./VideoCallButton"; 6 | import StarButton from "./StarButton"; 7 | import AddUserButton from "./AddUserButton"; 8 | import EllipsisButton from "./EllipsisButton"; 9 | import SendButton from "./SendButton"; 10 | import AttachmentButton from "./AttachmentButton"; 11 | 12 | export * from "./Button"; 13 | export * from "./ArrowButton"; 14 | export * from "./InfoButton"; 15 | export * from "./VoiceCallButton"; 16 | export * from "./VideoCallButton"; 17 | export * from "./StarButton"; 18 | export * from "./EllipsisButton"; 19 | export * from "./SendButton"; 20 | export * from "./AttachmentButton"; 21 | 22 | export default { 23 | Button, 24 | ArrowButton, 25 | InfoButton, 26 | VoiceCallButton, 27 | VideoCallButton, 28 | StarButton, 29 | AddUserButton, 30 | EllipsisButton, 31 | SendButton, 32 | AttachmentButton, 33 | }; 34 | -------------------------------------------------------------------------------- /src/components/ChatContainer/ChatContainer.d.ts: -------------------------------------------------------------------------------- 1 | import type {ReactElement} from "react"; 2 | import type { 3 | ChatComponentPropsChildren, 4 | } from "../../types/index"; 5 | import {EmptyProps} from "../../types/index"; 6 | 7 | export type ChatContainerOwnProps = EmptyProps; 8 | export type ChatContainerProps = ChatComponentPropsChildren; 9 | 10 | export declare const ChatContainer: (props:ChatComponentPropsChildren) => ReactElement; 11 | 12 | export default ChatContainer; 13 | -------------------------------------------------------------------------------- /src/components/ChatContainer/ChatContainer.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { allowedChildren, getChildren } from "../utils"; 3 | import ConversationHeader from "../ConversationHeader"; 4 | import MessageList from "../MessageList"; 5 | import MessageInput from "../MessageInput"; 6 | import InputToolbox from "../InputToolbox"; 7 | import classNames from "classnames"; 8 | import { prefix } from "../settings"; 9 | import PropTypes from "prop-types"; 10 | 11 | export const ChatContainer = ({ children = undefined, className, ...rest }) => { 12 | const cName = `${prefix}-chat-container`; 13 | 14 | const [ 15 | header, 16 | messageList, 17 | messageInput, 18 | inputToolbox, 19 | ] = getChildren(children, [ 20 | ConversationHeader, 21 | MessageList, 22 | MessageInput, 23 | InputToolbox, 24 | ]); 25 | 26 | return ( 27 |
28 | {header} 29 | {messageList} 30 | {messageInput} 31 | {inputToolbox} 32 |
33 | ); 34 | }; 35 | 36 | ChatContainer.propTypes = { 37 | /** 38 | * Primary content. 39 | * Allowed elements: 40 | * 41 | * * <ConversationHeader /> 42 | * * <MessageList /> 43 | * * <MessageInput /> 44 | * * <InputToolbox /> 45 | */ 46 | children: allowedChildren([ 47 | ConversationHeader, 48 | MessageList, 49 | MessageInput, 50 | InputToolbox, 51 | ]), 52 | 53 | /** Additional classes. */ 54 | className: PropTypes.string, 55 | }; 56 | 57 | export default ChatContainer; 58 | -------------------------------------------------------------------------------- /src/components/ChatContainer/index.js: -------------------------------------------------------------------------------- 1 | import ChatContainer from "./ChatContainer"; 2 | export * from "./ChatContainer"; 3 | export default ChatContainer; 4 | -------------------------------------------------------------------------------- /src/components/ContentEditable/ContentEditable.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | const replaceCaret = (el, activateAfterChange) => { 5 | const isTargetFocused = document.activeElement === el; 6 | 7 | // Place the caret at the end of the element 8 | const target = document.createTextNode(""); 9 | 10 | // Put empty text node at the end of input 11 | el.appendChild(target); 12 | 13 | // do not move caret if element was not focused 14 | if ( 15 | target !== null && 16 | target.nodeValue !== null && 17 | (isTargetFocused || activateAfterChange) 18 | ) { 19 | const sel = window.getSelection(); 20 | if (sel !== null) { 21 | const range = document.createRange(); 22 | range.setStart(target, target.nodeValue.length); 23 | range.collapse(true); 24 | sel.removeAllRanges(); 25 | sel.addRange(range); 26 | } 27 | } 28 | }; 29 | 30 | export class ContentEditable extends Component { 31 | constructor(props) { 32 | super(props); 33 | this.msgRef = React.createRef(); 34 | } 35 | 36 | innerHTML = () => { 37 | const { 38 | props: { value }, 39 | } = this; 40 | 41 | return { 42 | __html: typeof value !== "undefined" ? value : "", 43 | }; 44 | }; 45 | 46 | handleKeyPress = (evt) => { 47 | const { 48 | props: { onKeyPress }, 49 | } = this; 50 | onKeyPress?.(evt); 51 | }; 52 | 53 | handleInput = (evt) => { 54 | const { 55 | props: { onChange }, 56 | } = this; 57 | 58 | const target = evt.target; 59 | onChange?.(target.innerHTML, target.textContent, target.innerText); 60 | }; 61 | 62 | // Public API 63 | focus() { 64 | if (typeof this.msgRef.current !== "undefined") { 65 | this.msgRef.current.focus(); 66 | } 67 | } 68 | 69 | componentDidMount() { 70 | if (this.props.autoFocus === true) { 71 | this.msgRef.current.focus(); 72 | } 73 | } 74 | 75 | shouldComponentUpdate(nextProps) { 76 | const { 77 | msgRef, 78 | props: { placeholder, disabled, activateAfterChange }, 79 | } = this; 80 | 81 | if (typeof msgRef.current === "undefined") { 82 | return true; 83 | } 84 | 85 | if (nextProps.value !== msgRef.current.innerHTML) { 86 | return true; 87 | } 88 | 89 | // DO NOT place callbacks here in comparison! 90 | return ( 91 | placeholder !== nextProps.placeholder || 92 | disabled !== nextProps.disabled || 93 | activateAfterChange !== nextProps.activateAfterChange 94 | ); 95 | } 96 | 97 | componentDidUpdate() { 98 | const { 99 | msgRef, 100 | props: { value, activateAfterChange }, 101 | } = this; 102 | 103 | if (value !== msgRef.current.innerHTML) { 104 | msgRef.current.innerHTML = typeof value === "string" ? value : ""; 105 | } 106 | 107 | replaceCaret(msgRef.current, activateAfterChange); 108 | } 109 | 110 | render() { 111 | const { 112 | msgRef, 113 | handleInput, 114 | handleKeyPress, 115 | innerHTML, 116 | props: { placeholder, disabled, className }, 117 | } = this, 118 | ph = typeof placeholder === "string" ? placeholder : ""; 119 | 120 | return ( 121 |
131 | ); 132 | } 133 | } 134 | 135 | ContentEditable.propTypes = { 136 | /** Value. */ 137 | value: PropTypes.string, 138 | 139 | /** Placeholder. */ 140 | placeholder: PropTypes.string, 141 | 142 | /** A input can show it is currently unable to be interacted with. */ 143 | disabled: PropTypes.bool, 144 | 145 | /** 146 | * Sets focus element and caret at the end of input 147 | * when value is changed programmatically (e.g) from button click and element is not active 148 | */ 149 | activateAfterChange: PropTypes.bool, 150 | 151 | /** Set focus after mount. */ 152 | autoFocus: PropTypes.bool, 153 | 154 | /** 155 | * onChange handler
156 | * @param {String} value 157 | */ 158 | onChange: PropTypes.func, 159 | 160 | /** 161 | * onKeyPress handler
162 | * @param {String} value 163 | */ 164 | onKeyPress: PropTypes.func, 165 | 166 | /** Additional classes. */ 167 | className: PropTypes.string, 168 | }; 169 | 170 | export default ContentEditable; 171 | -------------------------------------------------------------------------------- /src/components/ContentEditable/index.js: -------------------------------------------------------------------------------- 1 | import ContentEditable from "./ContentEditable"; 2 | export * from "./ContentEditable"; 3 | export default ContentEditable; 4 | -------------------------------------------------------------------------------- /src/components/Conversation/Conversation.d.ts: -------------------------------------------------------------------------------- 1 | import type {ReactElement, ReactNode} from "react"; 2 | import type {ChatComponentPropsChildren} from "../../types"; 3 | 4 | export interface ConversationOperationsProps { 5 | visible?:boolean; 6 | } 7 | 8 | declare const ConversationOperations: (props:ChatComponentPropsChildren) => ReactElement; 9 | 10 | export interface ConversationContentProps { 11 | name?:ReactNode; 12 | lastSenderName?: ReactNode; 13 | info?:ReactNode; 14 | } 15 | 16 | declare const ConversationContent: (props:ChatComponentPropsChildren) => ReactElement; 17 | 18 | export interface ConversationProps { 19 | name?:ReactNode; 20 | unreadCnt?:number; 21 | unreadDot?:boolean; 22 | lastSenderName?:ReactNode; 23 | info?:ReactNode; 24 | lastActivityTime?:ReactNode; 25 | active?:boolean; 26 | } 27 | 28 | export declare const Conversation: { 29 | (props:ChatComponentPropsChildren):ReactElement, 30 | Operations: typeof ConversationOperations; 31 | Content: typeof ConversationContent; 32 | }; 33 | 34 | export default Conversation; 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /src/components/Conversation/Conversation.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { allowedChildren, getChildren } from "../utils"; 4 | import classNames from "classnames"; 5 | import cName from "./cName"; 6 | import ConversationOperations from "./ConversationOperations"; 7 | import ConversationContent from "./ConversationContent"; 8 | import Avatar from "../Avatar"; 9 | import AvatarGroup from "../AvatarGroup"; 10 | 11 | const LastActivityTime = ({ time }) => ( 12 |
13 | {time} 14 |
15 | ); 16 | 17 | const UnreadDot = () =>
; 18 | 19 | export const Conversation = ({ 20 | name = undefined, 21 | unreadCnt = undefined, 22 | lastSenderName = undefined, 23 | info = undefined, 24 | lastActivityTime = undefined, 25 | unreadDot = false, 26 | children, 27 | className, 28 | active = false, 29 | ...rest 30 | }) => { 31 | const [avatar, avatarGroup, operations, content] = getChildren(children, [ 32 | Avatar, 33 | AvatarGroup, 34 | ConversationOperations, 35 | ConversationContent, 36 | ]); 37 | 38 | return ( 39 |
43 | {avatar} 44 | {avatarGroup} 45 | 46 | {(typeof name !== "undefined" || 47 | typeof lastSenderName !== "undefined" || 48 | typeof info !== "undefined") && ( 49 | 54 | )} 55 | 56 | {(typeof name === "undefined" || name === null) && 57 | (typeof lastSenderName === "undefined" || lastSenderName === null) && 58 | (typeof info === "undefined" || info === null) && 59 | content} 60 | 61 | {lastActivityTime !== null && typeof lastActivityTime !== "undefined" && ( 62 | 63 | )} 64 | 65 | {unreadDot && } 66 | 67 | {operations} 68 | {unreadCnt !== null && 69 | typeof unreadCnt !== "undefined" && 70 | parseInt(unreadCnt) > 0 && ( 71 |
72 | {unreadCnt} 73 |
74 | )} 75 |
76 | ); 77 | }; 78 | 79 | Conversation.propTypes = { 80 | /** 81 | * Primary content. 82 | * Allowed node: 83 | * 84 | * * <Avatar /> 85 | * * <AvatarGroup /> 86 | * * <Conversation.Content /> 87 | * * <Conversation.Operations /> 88 | */ 89 | children: allowedChildren([ 90 | Avatar, 91 | AvatarGroup, 92 | ConversationOperations, 93 | ConversationContent, 94 | ]), 95 | 96 | /** First text line in <Conversation.Content /> contact name etc. */ 97 | name: PropTypes.node, 98 | 99 | /** Unread messages quantity. */ 100 | unreadCnt: PropTypes.number, 101 | 102 | /** Unread dot visible. */ 103 | unreadDot: PropTypes.bool, 104 | 105 | /** Last sender in <Conversation.Content /> name. */ 106 | lastSenderName: PropTypes.node, 107 | 108 | /** Informational message / last message in <Conversation.Content />. */ 109 | info: PropTypes.node, 110 | 111 | /** Last activity time. */ 112 | lastActivityTime: PropTypes.node, 113 | 114 | /** Active (currently viewed) */ 115 | active: PropTypes.bool, 116 | 117 | /** Additional classes. */ 118 | className: PropTypes.string, 119 | }; 120 | 121 | Conversation.Operations = ConversationOperations; 122 | Conversation.Content = ConversationContent; 123 | 124 | export default Conversation; 125 | -------------------------------------------------------------------------------- /src/components/Conversation/ConversationContent.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import cName from "./cName"; 3 | import classNames from "classnames"; 4 | import PropTypes from "prop-types"; 5 | 6 | const LastSenderName = ({ name }) => ( 7 | <> 8 |
{name}
9 | {":"} 10 | 11 | ); 12 | 13 | LastSenderName.propTypes = { 14 | name: PropTypes.node, 15 | }; 16 | 17 | const InfoContent = ({ info }) => ( 18 |
{info}
19 | ); 20 | 21 | InfoContent.propTypes = { 22 | info: PropTypes.node, 23 | }; 24 | 25 | export const ConversationContent = ({ 26 | lastSenderName, 27 | info, 28 | name, 29 | children, 30 | className, 31 | ...rest 32 | }) => { 33 | const typeofLastSenderName = typeof lastSenderName; 34 | 35 | return ( 36 |
37 | {React.Children.count(children) > 0 ? ( 38 | children 39 | ) : ( 40 | <> 41 |
{name}
42 |
43 | {typeofLastSenderName !== "undefined" ? ( 44 | <> 45 | {typeofLastSenderName === "string" ? ( 46 | 47 | ) : ( 48 | lastSenderName 49 | )}{" "} 50 | 51 | ) : null} 52 | {typeof info !== "undefined" && } 53 |
54 | 55 | )} 56 |
57 | ); 58 | }; 59 | 60 | ConversationContent.displayName = "Conversation.Content"; 61 | 62 | ConversationContent.propTypes = { 63 | /** Primary content. */ 64 | children: PropTypes.node, 65 | 66 | /** Additional classes. */ 67 | className: PropTypes.string, 68 | 69 | /** First text line - contact name etc. */ 70 | name: PropTypes.node, 71 | 72 | /** Last sender name. */ 73 | lastSenderName: PropTypes.node, 74 | 75 | /** Informational message / last message. */ 76 | info: PropTypes.node, 77 | }; 78 | 79 | 80 | export default ConversationContent; 81 | -------------------------------------------------------------------------------- /src/components/Conversation/ConversationOperations.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import cName from "./cName"; 3 | import classNames from "classnames"; 4 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 5 | import { faEllipsisV } from "@fortawesome/free-solid-svg-icons/faEllipsisV"; 6 | import PropTypes from "prop-types"; 7 | 8 | export const ConversationOperations = ({ 9 | children, 10 | className, 11 | visible, 12 | ...rest 13 | }) => ( 14 |
22 | {React.Children.count(children) > 0 ? ( 23 | children 24 | ) : ( 25 | 26 | )} 27 |
28 | ); 29 | 30 | ConversationOperations.displayName = "Conversation.Operations"; 31 | 32 | ConversationOperations.propTypes = { 33 | /** Primary content. */ 34 | children: PropTypes.node, 35 | 36 | /** Additional classes. */ 37 | className: PropTypes.string, 38 | 39 | /** Always visible not only on hover */ 40 | visible: PropTypes.bool, 41 | }; 42 | 43 | export default ConversationOperations; 44 | -------------------------------------------------------------------------------- /src/components/Conversation/cName.js: -------------------------------------------------------------------------------- 1 | import { prefix } from "../settings"; 2 | export const cName = `${prefix}-conversation`; 3 | export default cName; 4 | -------------------------------------------------------------------------------- /src/components/Conversation/index.js: -------------------------------------------------------------------------------- 1 | import Conversation from "./Conversation"; 2 | export * from "./Conversation"; 3 | export default Conversation; 4 | -------------------------------------------------------------------------------- /src/components/ConversationHeader/ConversationHeader.d.ts: -------------------------------------------------------------------------------- 1 | import type {ReactElement, ReactNode} from "react"; 2 | import type {ChatComponentPropsChildren, EmptyProps} from "../../types"; 3 | 4 | export type ConversationHeaderActionsProps = EmptyProps; 5 | 6 | declare const ConversationHeaderActions: (props:ChatComponentPropsChildren) => ReactElement; 7 | 8 | export type ConversationHeaderBackProps = EmptyProps; 9 | 10 | declare const ConversationHeaderBack: (props:ChatComponentPropsChildren) => ReactElement; 11 | 12 | export interface ConversationHeaderContentProps { 13 | userName?:ReactNode; 14 | info?:ReactNode; 15 | } 16 | 17 | declare const ConversationHeaderContent: (props:ChatComponentPropsChildren) => ReactElement; 18 | 19 | export type ConversationHeaderOwnProps = EmptyProps; 20 | export type ConversationHeaderProps = ChatComponentPropsChildren; 21 | 22 | export declare const ConversationHeader: { 23 | (props:ConversationHeaderProps):ReactElement; 24 | Back: typeof ConversationHeaderBack; 25 | Actions: typeof ConversationHeaderActions; 26 | Content: typeof ConversationHeaderContent; 27 | }; 28 | 29 | export default ConversationHeader; -------------------------------------------------------------------------------- /src/components/ConversationHeader/ConversationHeader.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { prefix } from "../settings"; 3 | import { allowedChildren, getChildren } from "../utils"; 4 | import classNames from "classnames"; 5 | import Avatar from "../Avatar"; 6 | import AvatarGroup from "../AvatarGroup"; 7 | import ConversationHeaderBack from "./ConversationHeaderBack"; 8 | import ConversationHeaderActions from "./ConversationHeaderActions"; 9 | import ConversationHeaderContent from "./ConversationHeaderContent"; 10 | import PropTypes from "prop-types"; 11 | 12 | export const ConversationHeader = ({ children = undefined, className, ...rest }) => { 13 | const cName = `${prefix}-conversation-header`; 14 | 15 | const [back, avatar, avatarGroup, content, actions] = getChildren(children, [ 16 | ConversationHeaderBack, 17 | Avatar, 18 | AvatarGroup, 19 | ConversationHeaderContent, 20 | ConversationHeaderActions, 21 | ]); 22 | 23 | return ( 24 |
25 | {back} 26 | {avatar &&
{avatar}
} 27 | {!avatar && avatarGroup && ( 28 |
{avatarGroup}
29 | )} 30 | {content} 31 | {actions} 32 |
33 | ); 34 | }; 35 | 36 | ConversationHeader.displayName = "ConversationHeader"; 37 | 38 | ConversationHeader.propTypes = { 39 | /** 40 | * Primary content. 41 | * Available elements: 42 | * 43 | * * <Avatar /> 44 | * * <AvatarGroup /> 45 | * * <ConversationHeader.Back /> 46 | * * <ConversationHeader.Content /> 47 | * * <ConversationHeader.Actions /> 48 | */ 49 | children: allowedChildren([ 50 | ConversationHeaderBack, 51 | Avatar, 52 | AvatarGroup, 53 | ConversationHeaderContent, 54 | ConversationHeaderActions, 55 | ]), 56 | 57 | /** Additional classes. */ 58 | className: PropTypes.string, 59 | }; 60 | 61 | ConversationHeader.Back = ConversationHeaderBack; 62 | ConversationHeader.Actions = ConversationHeaderActions; 63 | ConversationHeader.Content = ConversationHeaderContent; 64 | export default ConversationHeader; 65 | -------------------------------------------------------------------------------- /src/components/ConversationHeader/ConversationHeaderActions.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { prefix } from "../settings"; 4 | import classNames from "classnames"; 5 | 6 | export const ConversationHeaderActions = ({ children = undefined, className, ...rest }) => { 7 | const cName = `${prefix}-conversation-header__actions`; 8 | 9 | return ( 10 |
11 | {children} 12 |
13 | ); 14 | }; 15 | 16 | ConversationHeaderActions.displayName = "ConversationHeader.Actions"; 17 | 18 | ConversationHeaderActions.propTypes = { 19 | /** Primary content. */ 20 | children: PropTypes.node, 21 | 22 | /** Additional classes. */ 23 | className: PropTypes.string, 24 | }; 25 | 26 | export default ConversationHeaderActions; 27 | -------------------------------------------------------------------------------- /src/components/ConversationHeader/ConversationHeaderBack.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import { prefix } from "../settings"; 4 | import classNames from "classnames"; 5 | import { ArrowButton } from "../Buttons"; 6 | 7 | export const ConversationHeaderBack = ({ 8 | onClick = () => {}, 9 | children = undefined, 10 | className, 11 | ...rest 12 | }) => { 13 | const cName = `${prefix}-conversation-header__back`; 14 | 15 | return ( 16 |
17 | {typeof children !== "undefined" ? ( 18 | children 19 | ) : ( 20 | 21 | )} 22 |
23 | ); 24 | }; 25 | 26 | ConversationHeaderBack.displayName = "ConversationHeader.Back"; 27 | 28 | ConversationHeaderBack.propTypes = { 29 | /** OnClick handler attached to button. */ 30 | onClick: PropTypes.func, 31 | 32 | /** Primary content - override default button. */ 33 | children: PropTypes.node, 34 | 35 | /** Additional classes. */ 36 | className: PropTypes.string, 37 | }; 38 | 39 | export default ConversationHeaderBack; 40 | -------------------------------------------------------------------------------- /src/components/ConversationHeader/ConversationHeaderContent.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import classNames from "classnames"; 4 | import { prefix } from "../settings"; 5 | 6 | export const ConversationHeaderContent = ({ 7 | userName = "", 8 | info = "", 9 | children = undefined, 10 | className, 11 | ...rest 12 | }) => { 13 | const cName = `${prefix}-conversation-header__content`; 14 | 15 | return ( 16 |
17 | {typeof children !== "undefined" ? ( 18 | children 19 | ) : ( 20 | <> 21 |
22 | {userName} 23 |
24 |
{info}
25 | 26 | )} 27 |
28 | ); 29 | }; 30 | 31 | ConversationHeaderContent.displayName = "ConversationHeader.Content"; 32 | 33 | ConversationHeaderContent.propTypes = { 34 | /** Primary content. Has precedence over userName and info properties. */ 35 | children: PropTypes.node, 36 | userName: PropTypes.node, 37 | info: PropTypes.node, 38 | 39 | /** Additional classes. */ 40 | className: PropTypes.string, 41 | }; 42 | 43 | export default ConversationHeaderContent; 44 | -------------------------------------------------------------------------------- /src/components/ConversationHeader/index.js: -------------------------------------------------------------------------------- 1 | import ConversationHeader from "./ConversationHeader"; 2 | export * from "./ConversationHeader"; 3 | export default ConversationHeader; 4 | -------------------------------------------------------------------------------- /src/components/ConversationList/ConversationList.d.ts: -------------------------------------------------------------------------------- 1 | import {ChatComponentPropsChildren} from "../../types"; 2 | import type {ReactElement} from "react"; 3 | 4 | export interface ConversationListOwnProps { 5 | scrollable?:boolean; 6 | loading?:boolean; 7 | loadingMore?:boolean; 8 | onYReachEnd?:(container:HTMLDivElement) => void; 9 | } 10 | 11 | export type ConversationListProps = ChatComponentPropsChildren; 12 | 13 | export declare const ConversationList: (props:ConversationListProps) => ReactElement; 14 | 15 | export default ConversationList; -------------------------------------------------------------------------------- /src/components/ConversationList/ConversationList.jsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from "react"; 2 | import PropTypes from "prop-types"; 3 | import { allowedChildren } from "../utils"; 4 | import { prefix } from "../settings"; 5 | import PerfectScrollbar from "../Scroll"; 6 | import classNames from "classnames"; 7 | import Overlay from "../Overlay"; 8 | import Loader from "../Loader"; 9 | import Conversation from "../Conversation"; 10 | 11 | export const ConversationList = ({ 12 | children = [], 13 | scrollable = true, 14 | loading = false, 15 | loadingMore = false, 16 | onYReachEnd, 17 | className = "", 18 | ...props 19 | }) => { 20 | const cName = `${prefix}-conversation-list`; 21 | 22 | // Memoize, to avoid re-render each time when props (children) changed 23 | const Tag = useMemo( 24 | () => ({ children }) => { 25 | // PerfectScrollbar for now cant be disabled, so render div instead of disabling it 26 | // https://github.com/goldenyz/react-perfect-scrollbar/issues/107 27 | if (scrollable === false || (scrollable === true && loading === true)) { 28 | return ( 29 |
30 | {loading && ( 31 | 32 | 33 | 34 | )} 35 | {children} 36 |
37 | ); 38 | } else { 39 | return ( 40 | 44 | {children} 45 | 46 | ); 47 | } 48 | }, 49 | [scrollable, loading] 50 | ); 51 | 52 | return ( 53 |
54 | 55 | {React.Children.count(children) > 0 && ( 56 |
    57 | {React.Children.map(children, (item) => ( 58 |
  • {item}
  • 59 | ))} 60 |
61 | )} 62 |
63 | {loadingMore && ( 64 |
65 | 66 |
67 | )} 68 |
69 | ); 70 | }; 71 | 72 | ConversationList.propTypes = { 73 | /** 74 | * Primary content. 75 | * Allowed components: 76 | * 77 | * * <Conversation /> 78 | * 79 | */ 80 | children: allowedChildren([Conversation]), 81 | 82 | /** Init scrollbar flag. */ 83 | scrollable: PropTypes.bool, 84 | 85 | /** Loading flag. */ 86 | loading: PropTypes.bool, 87 | 88 | /** Loading more flag for infinity scroll. */ 89 | loadingMore: PropTypes.bool, 90 | 91 | /** 92 | * This is fired when the scrollbar reaches the end on the y axis.
93 | * It can be used to load next conversations using the infinite scroll. 94 | */ 95 | onYReachEnd: PropTypes.func, 96 | 97 | /** Additional classes. */ 98 | className: PropTypes.string, 99 | }; 100 | 101 | export default ConversationList; 102 | -------------------------------------------------------------------------------- /src/components/ConversationList/index.js: -------------------------------------------------------------------------------- 1 | import ConversationList from "./ConversationList"; 2 | export * from "./ConversationList"; 3 | export default ConversationList; 4 | -------------------------------------------------------------------------------- /src/components/ExpansionPanel/ExpansionPanel.d.ts: -------------------------------------------------------------------------------- 1 | import type {ReactElement, MouseEvent} from "react"; 2 | import type {ChatComponentPropsChildren} from "../../types"; 3 | 4 | export type ExpansionPanelOnChangeHandler = IsOpened extends boolean ? (evt: MouseEvent) => void : (state: boolean, evt: MouseEvent ) => void; 5 | 6 | export interface ExpansionPanelProps { 7 | title?:string; 8 | open?:boolean; 9 | isOpened?: IsOpened; 10 | onChange?: ExpansionPanelOnChangeHandler; 11 | } 12 | 13 | export declare const ExpansionPanel: (props:ChatComponentPropsChildren,"div">) => ReactElement; 14 | 15 | export default ExpansionPanel; -------------------------------------------------------------------------------- /src/components/ExpansionPanel/ExpansionPanel.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useCallback, useMemo } from "react"; 2 | import PropTypes from "prop-types"; 3 | import { prefix } from "../settings"; 4 | import classNames from "classnames"; 5 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 6 | import { faChevronLeft } from "@fortawesome/free-solid-svg-icons/faChevronLeft"; 7 | import { faChevronDown } from "@fortawesome/free-solid-svg-icons/faChevronDown"; 8 | 9 | export const ExpansionPanel = ({ 10 | children = undefined, 11 | title = "", 12 | open: defaultOpen = false, 13 | isOpened, 14 | onChange, 15 | className, 16 | ...rest 17 | }) => { 18 | const cName = `${prefix}-expansion-panel`; 19 | 20 | const defaultOpenFlag = defaultOpen === true ? defaultOpen : false; 21 | 22 | const [open, setOpen] = useState(defaultOpenFlag); 23 | 24 | const opened = useMemo( 25 | () => (typeof isOpened === "boolean" ? isOpened : open), 26 | [isOpened, open] 27 | ); 28 | 29 | const openModifier = opened === true ? `${cName}--open` : ""; 30 | const icon = opened === true ? faChevronDown : faChevronLeft; 31 | 32 | const handleOpen = useCallback( 33 | (e) => { 34 | if (typeof isOpened === "boolean") { 35 | onChange?.(e); 36 | } else { 37 | setOpen(!opened); 38 | onChange?.(!opened, e); 39 | } 40 | }, 41 | [onChange, open, opened, isOpened] 42 | ); 43 | 44 | return ( 45 |
46 |
47 |
{title}
48 |
49 | 50 |
51 |
52 |
{children}
53 |
54 | ); 55 | }; 56 | 57 | ExpansionPanel.displayName = "ExpansionPanel"; 58 | 59 | ExpansionPanel.propTypes = { 60 | /** Primary content. */ 61 | children: PropTypes.node, 62 | 63 | /** Title. */ 64 | title: PropTypes.string, 65 | 66 | /** Default open state (uncontrolled mode). */ 67 | open: PropTypes.bool, 68 | 69 | /** If panel is opened (controlled mode). */ 70 | isOpened: PropTypes.bool, 71 | 72 | /** Additional classes. */ 73 | className: PropTypes.string, 74 | 75 | /** Called when the opening state changes. */ 76 | onChange: PropTypes.func, 77 | }; 78 | 79 | 80 | 81 | export default ExpansionPanel; 82 | -------------------------------------------------------------------------------- /src/components/ExpansionPanel/index.js: -------------------------------------------------------------------------------- 1 | import ExpansionPanel from "./ExpansionPanel"; 2 | export * from "./ExpansionPanel"; 3 | export default ExpansionPanel; 4 | -------------------------------------------------------------------------------- /src/components/InputToolbox/InputToolbox.d.ts: -------------------------------------------------------------------------------- 1 | import type {ChatComponentPropsChildren, EmptyProps} from "../../types"; 2 | import {ReactElement} from "react"; 3 | 4 | export type InputToolboxOwnProps = EmptyProps; 5 | export type InputToolboxProps = ChatComponentPropsChildren 6 | 7 | export declare const InputToolbox: (props:InputToolboxProps) => ReactElement; 8 | 9 | export default InputToolbox; -------------------------------------------------------------------------------- /src/components/InputToolbox/InputToolbox.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import classNames from "classnames"; 4 | import { prefix } from "../settings"; 5 | 6 | export const InputToolbox = ({ className, children, ...rest }) => { 7 | const cName = `${prefix}-input-toolbox`; 8 | 9 | return ( 10 |
11 | {children} 12 |
13 | ); 14 | }; 15 | 16 | InputToolbox.displayName = "InputToolbox"; 17 | 18 | InputToolbox.propTypes = { 19 | /** Primary content. */ 20 | children: PropTypes.node, 21 | 22 | /** Additional classes. */ 23 | className: PropTypes.string, 24 | }; 25 | 26 | export default InputToolbox; 27 | -------------------------------------------------------------------------------- /src/components/InputToolbox/index.js: -------------------------------------------------------------------------------- 1 | import InputToolbox from "./InputToolbox"; 2 | export * from "./InputToolbox"; 3 | export default InputToolbox; 4 | -------------------------------------------------------------------------------- /src/components/Loader/Loader.d.ts: -------------------------------------------------------------------------------- 1 | import type {ReactElement} from "react"; 2 | import type {ChatComponentPropsChildren} from "../../types"; 3 | import type {LoaderVariant} from "../../types/unions"; 4 | 5 | export interface LoaderProps { 6 | variant?:LoaderVariant; 7 | } 8 | 9 | export declare const Loader: (props:ChatComponentPropsChildren) => ReactElement; 10 | 11 | export default Loader; -------------------------------------------------------------------------------- /src/components/Loader/Loader.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import classNames from "classnames"; 4 | import { prefix } from "../settings"; 5 | 6 | export const Loader = ({ 7 | className = undefined, 8 | variant = "default", 9 | children, 10 | ...rest 11 | }) => { 12 | const cName = `${prefix}-loader`; 13 | const textClass = 14 | React.Children.count(children) > 0 ? `${cName}--content` : ""; 15 | return ( 16 |
26 | {children} 27 |
28 | ); 29 | }; 30 | 31 | Loader.propTypes = { 32 | /** Primary content. */ 33 | children: PropTypes.node, 34 | 35 | /** Additional classes. */ 36 | className: PropTypes.string, 37 | 38 | /** Loader variant */ 39 | variant: PropTypes.oneOf(["default"]), 40 | }; 41 | 42 | export default Loader; 43 | -------------------------------------------------------------------------------- /src/components/Loader/index.js: -------------------------------------------------------------------------------- 1 | import Loader from "./Loader"; 2 | export * from "./Loader"; 3 | export default Loader; 4 | -------------------------------------------------------------------------------- /src/components/MainContainer/MainContainer.d.ts: -------------------------------------------------------------------------------- 1 | import type {ReactElement} from "react"; 2 | import type {ChatComponentPropsChildren} from "../../types"; 3 | 4 | export interface MainContainerProps { 5 | responsive?:boolean; 6 | } 7 | 8 | export declare const MainContainer: (props:ChatComponentPropsChildren) => ReactElement; 9 | 10 | export default MainContainer; -------------------------------------------------------------------------------- /src/components/MainContainer/MainContainer.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import classNames from "classnames"; 4 | import { prefix } from "../settings"; 5 | 6 | export const MainContainer = ({ responsive = false, children = undefined, className, ...rest }) => { 7 | const cName = `${prefix}-main-container`; 8 | 9 | return ( 10 |
18 | {children} 19 |
20 | ); 21 | }; 22 | 23 | MainContainer.propTypes = { 24 | /** Primary content. */ 25 | children: PropTypes.node, 26 | 27 | /** Is container responsive. */ 28 | responsive: PropTypes.bool, 29 | 30 | /** Additional classes. */ 31 | className: PropTypes.string, 32 | }; 33 | 34 | export default MainContainer; 35 | -------------------------------------------------------------------------------- /src/components/MainContainer/index.js: -------------------------------------------------------------------------------- 1 | import MainContainer from "./MainContainer"; 2 | export * from "./MainContainer"; 3 | export default MainContainer; 4 | -------------------------------------------------------------------------------- /src/components/Message/Message.d.ts: -------------------------------------------------------------------------------- 1 | import type {ReactElement, ReactNode} from "react"; 2 | import type {ChatComponentPropsChildren, ChatComponentProps, EmptyProps, MessageType} from "../../types"; 3 | import type {AvatarPosition, MessageDirection} from "../../types/unions"; 4 | 5 | export type MessageCustomContentProps = EmptyProps; 6 | 7 | declare const MessageCustomContent: (props:ChatComponentPropsChildren) => ReactElement; 8 | 9 | export interface MessageHtmlContentProps { 10 | html?:string; 11 | } 12 | 13 | declare const MessageHtmlContent: (props:ChatComponentProps) => ReactElement; 14 | 15 | export interface MessageImageContentProps { 16 | src?:string; 17 | width?:string|number; 18 | height?:string|number; 19 | alt?:string; 20 | } 21 | 22 | declare const MessageImageContent: (props:ChatComponentProps) => ReactElement; 23 | 24 | export interface MessageTextContentProps { 25 | text?:string; 26 | } 27 | 28 | declare const MessageTextContent: (props:ChatComponentPropsChildren) => ReactElement; 29 | 30 | export interface MessageHeaderProps { 31 | sender?:string; 32 | sentTime?:string; 33 | } 34 | 35 | declare const MessageHeader: (props:ChatComponentPropsChildren) => ReactElement; 36 | 37 | export interface MessageFooterProps { 38 | sender?:string; 39 | sentTime?:string; 40 | } 41 | 42 | declare const MessageFooter: (props:ChatComponentPropsChildren) => ReactElement; 43 | 44 | /* eslint-disable @typescript-eslint/no-explicit-any */ 45 | export type MessagePayload = string | Record | MessageImageContentProps | ReactNode; 46 | 47 | export interface MessageModel { 48 | message?:string; 49 | sentTime?:string; 50 | sender?:string; 51 | direction: MessageDirection; 52 | position: "single" | "first" | "normal" | "last" | 0 | 1 | 2 | 3; 53 | type?: MessageType; 54 | payload?: MessagePayload; 55 | } 56 | 57 | export interface MessageProps { 58 | model?: MessageModel; 59 | avatarSpacer?:boolean; 60 | avatarPosition?: AvatarPosition; 61 | type?: MessageType; 62 | payload?: MessagePayload; 63 | } 64 | 65 | declare const Message: { 66 | (props:ChatComponentPropsChildren):ReactElement; 67 | Header: typeof MessageHeader; 68 | HtmlContent: typeof MessageHtmlContent; 69 | TextContent: typeof MessageTextContent; 70 | ImageContent: typeof MessageImageContent; 71 | CustomContent: typeof MessageCustomContent; 72 | Footer:typeof MessageFooter; 73 | }; 74 | 75 | export { 76 | MessageCustomContent, 77 | MessageHtmlContent, 78 | MessageImageContent, 79 | MessageTextContent, 80 | Message 81 | }; 82 | 83 | export default Message; -------------------------------------------------------------------------------- /src/components/Message/Message.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import classNames from "classnames"; 4 | import { allowedChildren, getChildren, getComponentName } from "../utils"; 5 | import { prefix } from "../settings"; 6 | import Avatar from "../Avatar"; 7 | import MessageHeader from "./MessageHeader"; 8 | import MessageFooter from "./MessageFooter"; 9 | import MessageCustomContent from "./MessageCustomContent"; 10 | import MessageImageContent from "./MessageImageContent"; 11 | import MessageHtmlContent from "./MessageHtmlContent"; 12 | import MessageTextContent from "./MessageTextContent"; 13 | 14 | /** 15 | * Chat message 16 | */ 17 | export const Message = ({ 18 | model: { 19 | message = "", 20 | sentTime = "", 21 | sender = "", 22 | direction = 1, 23 | position, 24 | type: modelType, 25 | payload: modelPayload, 26 | }, 27 | avatarSpacer = false, 28 | avatarPosition = undefined, 29 | type = "html", 30 | payload: argPayload, 31 | children, 32 | className, 33 | ...rest 34 | }) => { 35 | const cName = `${prefix}-message`; 36 | 37 | const [ 38 | avatar, 39 | header, 40 | footer, 41 | htmlContent, 42 | textContent, 43 | imageContent, 44 | customContent, 45 | ] = getChildren(children, [ 46 | Avatar, 47 | MessageHeader, 48 | MessageFooter, 49 | MessageHtmlContent, 50 | MessageTextContent, 51 | MessageImageContent, 52 | MessageCustomContent, 53 | ]); 54 | 55 | const directionClass = (() => { 56 | if (direction === 0 || direction === "incoming") { 57 | return `${cName}--incoming`; 58 | } else if (direction === 1 || direction === "outgoing") { 59 | return `${cName}--outgoing`; 60 | } 61 | })(); 62 | 63 | const avatarPositionClass = ((position) => { 64 | const classPrefix = `${cName}--avatar-`; 65 | if (position === 0 || position === "top-left" || position === "tl") { 66 | return `${classPrefix}tl`; 67 | } else if ( 68 | position === 1 || 69 | position === "top-right" || 70 | position === "tr" 71 | ) { 72 | return `${classPrefix}tr`; 73 | } else if ( 74 | position === 2 || 75 | position === "bottom-right" || 76 | position === "br" 77 | ) { 78 | return `${classPrefix}br`; 79 | } else if ( 80 | position === 3 || 81 | position === "bottom-left" || 82 | position === "bl" 83 | ) { 84 | return `${classPrefix}bl`; 85 | } else if ( 86 | position === 4 || 87 | position === "center-left" || 88 | position === "cl" 89 | ) { 90 | return `${classPrefix}cl`; 91 | } else if ( 92 | position === 5 || 93 | position === "center-right" || 94 | position === "cr" 95 | ) { 96 | return `${classPrefix}cr`; 97 | } 98 | })(avatarPosition); 99 | 100 | const positionClass = ((position) => { 101 | const classPrefix = `${prefix}-message--`; 102 | if (position === "single" || position === 0) { 103 | return `${classPrefix}single`; 104 | } else if (position === "first" || position === 1) { 105 | return `${classPrefix}first`; 106 | } else if (position === "normal" || position === 2) { 107 | return ""; 108 | } else if (position === "last" || position === 3) { 109 | return `${classPrefix}last`; 110 | } 111 | })(position); 112 | 113 | const ariaLabel = (() => { 114 | if (sender?.length > 0 && sentTime?.length > 0) { 115 | return `${sender}: ${sentTime}`; 116 | } else if ( 117 | sender?.length > 0 && 118 | (typeof sentTime === "undefined" || sentTime?.length === 0) 119 | ) { 120 | return sender; 121 | } else { 122 | return null; 123 | } 124 | })(); 125 | 126 | const childContent = 127 | htmlContent ?? textContent ?? imageContent ?? customContent; 128 | 129 | const messageContent = 130 | childContent ?? 131 | (() => { 132 | const messageType = modelType ?? type; 133 | 134 | const payloadFromModel = modelPayload ?? message; 135 | const payload = payloadFromModel ?? argPayload; 136 | 137 | const payloadName = 138 | typeof payload === "object" ? getComponentName(payload) : ""; 139 | 140 | if (messageType === "html" && payloadName !== "Message.CustomContent") { 141 | return ; 142 | } else if (messageType === "text") { 143 | return ; 144 | } else if (messageType === "image") { 145 | return ; 146 | } else if ( 147 | messageType === "custom" || 148 | payloadName === "Message.CustomContent" 149 | ) { 150 | return payload; 151 | } 152 | })(); 153 | 154 | return ( 155 |
168 | {typeof avatar !== "undefined" && ( 169 |
{avatar}
170 | )} 171 |
172 | {header} 173 |
{messageContent}
174 | {footer} 175 |
176 |
177 | ); 178 | }; 179 | 180 | Message.propTypes = { 181 | /** 182 | * Model object 183 | * **message**: string - Message to send 184 | * **sentTime**: string - Message sent time 185 | * **sender**: string - Sender name 186 | * **direction**: "incoming" | "outgoing" | 0 | 1 - Message direction 187 | * **position**: "single" | "first" | "normal" | "last" | 0 | 1 | 2 | 3 - Message position in feed 188 | * **type**: "html" | "text" | "image" | "custom" 189 | */ 190 | model: PropTypes.shape({ 191 | /** Chat message to display - content. */ 192 | message: PropTypes.string, 193 | sentTime: PropTypes.string, 194 | sender: PropTypes.string, 195 | direction: PropTypes.oneOf(["incoming", "outgoing", 0, 1]), 196 | 197 | /** Position. */ 198 | position: PropTypes.oneOf([ 199 | "single", 200 | "first", 201 | "normal", 202 | "last", 203 | 0, 204 | 1, 205 | 2, 206 | 3, 207 | ]), 208 | 209 | /** 210 | * Message type 211 | * This property can also be added directly to component, but property from model has precedence. 212 | * Each type can have payload defined in model.payload or in payload property. 213 | * Allowed payloads for different message are described in payload property. 214 | */ 215 | type: PropTypes.oneOf(["html", "text", "image", "custom"]), 216 | 217 | /** 218 | * Message payload. 219 | * Must be adequate to message type. 220 | * Allowed payloads for different message types: 221 | * html: String - Html string to render, 222 | * text: String - Text to render, 223 | * image: Object - for object properties please see **<Message.ImageContent />** properties, 224 | * custom: **Message.CustomContent** - Component 225 | */ 226 | payload: PropTypes.oneOfType([ 227 | PropTypes.string, 228 | PropTypes.object, 229 | allowedChildren([MessageCustomContent]), 230 | ]), 231 | }), 232 | avatarSpacer: PropTypes.bool, 233 | avatarPosition: PropTypes.oneOf([ 234 | "tl", 235 | "tr", 236 | "cl", 237 | "cr", 238 | "bl", 239 | "br", 240 | "top-left", 241 | "top-right", 242 | "center-left", 243 | "center-right", 244 | "bottom-left", 245 | "bottom-right", 246 | ]), 247 | 248 | /** 249 | * Primary content. 250 | * Content from payload has precedence over Message.*Content properties. 251 | * Whe 252 | * Allowed components: 253 | * 254 | * * <Avatar /> 255 | * * <Message.Header /> 256 | * * <Message.Footer /> 257 | * * <Message.HtmlContent /> 258 | * * <Message.TextContent /> 259 | * * <Message.ImageContent /> 260 | * * <Message.CustomContent /> 261 | */ 262 | children: allowedChildren([ 263 | Avatar, 264 | MessageHeader, 265 | MessageFooter, 266 | MessageHtmlContent, 267 | MessageTextContent, 268 | MessageImageContent, 269 | MessageCustomContent, 270 | ]), 271 | 272 | /** Additional classes. */ 273 | className: PropTypes.string, 274 | 275 | /** 276 | * Message type 277 | * This property can also exists in model. In that case value from model has precedence. 278 | **/ 279 | type: PropTypes.oneOf(["html", "text", "image", "custom"]), 280 | 281 | /** 282 | * Message payload. 283 | * Must be adequate to message type. 284 | * Allowed payloads for different message types: 285 | * html: String - Html string to render, 286 | * text: String - Text to render, 287 | * image: Object - for object properties please see **<Message.ImageContent >/>** properties, 288 | * custom: **Message.CustomContent** - Component 289 | */ 290 | payload: PropTypes.oneOfType([ 291 | PropTypes.string, 292 | allowedChildren([MessageCustomContent]), 293 | ]), 294 | }; 295 | 296 | 297 | 298 | Message.Header = MessageHeader; 299 | Message.HtmlContent = MessageHtmlContent; 300 | Message.TextContent = MessageTextContent; 301 | Message.ImageContent = MessageImageContent; 302 | Message.CustomContent = MessageCustomContent; 303 | Message.Footer = MessageFooter; 304 | 305 | export default Message; 306 | -------------------------------------------------------------------------------- /src/components/Message/MessageCustomContent.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import classNames from "classnames"; 4 | import { prefix } from "../settings"; 5 | 6 | export const MessageCustomContent = ({ children, className }) => { 7 | const cName = `${prefix}-message__custom-content`; 8 | 9 | return
{children}
; 10 | }; 11 | 12 | MessageCustomContent.displayName = "Message.CustomContent"; 13 | 14 | MessageCustomContent.propTypes = { 15 | /** Primary content. */ 16 | children: PropTypes.node, 17 | 18 | /** Additional classes. */ 19 | className: PropTypes.string, 20 | }; 21 | 22 | export default MessageCustomContent; 23 | -------------------------------------------------------------------------------- /src/components/Message/MessageFooter.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import classNames from "classnames"; 4 | 5 | import { prefix } from "../settings"; 6 | 7 | export const MessageFooter = ({ 8 | sender = "", 9 | sentTime = "", 10 | children = undefined, 11 | className, 12 | ...rest 13 | }) => { 14 | const cName = `${prefix}-message__footer`; 15 | 16 | return ( 17 |
18 | {typeof children !== "undefined" ? ( 19 | children 20 | ) : ( 21 | <> 22 |
{sender}
23 |
{sentTime}
24 | 25 | )} 26 |
27 | ); 28 | }; 29 | 30 | MessageFooter.displayName = "Message.Footer"; 31 | 32 | MessageFooter.propTypes = { 33 | sender: PropTypes.string, 34 | sentTime: PropTypes.string, 35 | 36 | /** Primary content. */ 37 | children: PropTypes.node, 38 | 39 | /** Additional classes. */ 40 | className: PropTypes.string, 41 | }; 42 | 43 | export default MessageFooter; 44 | -------------------------------------------------------------------------------- /src/components/Message/MessageHeader.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import classNames from "classnames"; 4 | import { prefix } from "../settings"; 5 | 6 | export const MessageHeader = ({ 7 | sender = "", 8 | sentTime = "", 9 | children = undefined, 10 | className, 11 | ...rest 12 | }) => { 13 | const cName = `${prefix}-message__header`; 14 | 15 | return ( 16 |
17 | {typeof children !== "undefined" ? ( 18 | children 19 | ) : ( 20 | <> 21 |
{sender}
22 |
{sentTime}
23 | 24 | )} 25 |
26 | ); 27 | }; 28 | 29 | MessageHeader.displayName = "Message.Header"; 30 | 31 | MessageHeader.propTypes = { 32 | sender: PropTypes.string, 33 | sentTime: PropTypes.string, 34 | 35 | /** Primary content. */ 36 | children: PropTypes.node, 37 | 38 | /** Additional classes. */ 39 | className: PropTypes.string, 40 | }; 41 | 42 | export default MessageHeader; 43 | -------------------------------------------------------------------------------- /src/components/Message/MessageHtmlContent.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import classNames from "classnames"; 4 | import { prefix } from "../settings"; 5 | 6 | export const MessageHtmlContent = ({ html, className }) => { 7 | const cName = `${prefix}-message__html-content`; 8 | 9 | const createMarkup = () => ({ __html: html }); 10 | 11 | return ( 12 |
16 | ); 17 | }; 18 | 19 | MessageHtmlContent.displayName = "Message.HtmlContent"; 20 | 21 | MessageHtmlContent.propTypes = { 22 | /** 23 | * Html string will be rendered in component using dangerouslySetInnerHTML 24 | */ 25 | html: PropTypes.string, 26 | 27 | /** Additional classes. */ 28 | className: PropTypes.string, 29 | }; 30 | 31 | export default MessageHtmlContent; 32 | -------------------------------------------------------------------------------- /src/components/Message/MessageImageContent.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import classNames from "classnames"; 4 | import { prefix } from "../settings"; 5 | 6 | export const MessageImageContent = ({ src, width, height, alt, className }) => { 7 | const cName = `${prefix}-message__image-content`; 8 | 9 | const style = { 10 | width: 11 | typeof width === "number" 12 | ? `${width}px` 13 | : typeof width === "string" 14 | ? width 15 | : undefined, 16 | height: 17 | typeof height === "number" 18 | ? `${height}px` 19 | : typeof height === "string" 20 | ? height 21 | : undefined, 22 | }; 23 | 24 | return ( 25 |
26 | {alt} 27 |
28 | ); 29 | }; 30 | 31 | MessageImageContent.displayName = "Message.ImageContent"; 32 | 33 | MessageImageContent.propTypes = { 34 | /** Image source*/ 35 | src: PropTypes.string, 36 | 37 | /** Image width */ 38 | width: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 39 | 40 | /** Image height */ 41 | height: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), 42 | 43 | /** Alternate text for image */ 44 | alt: PropTypes.string, 45 | 46 | /** Additional classes. */ 47 | className: PropTypes.string, 48 | }; 49 | 50 | export default MessageImageContent; 51 | -------------------------------------------------------------------------------- /src/components/Message/MessageTextContent.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import classNames from "classnames"; 4 | import { prefix } from "../settings"; 5 | 6 | export const MessageTextContent = ({ text, className, children }) => { 7 | const cName = `${prefix}-message__text-content`; 8 | 9 | const content = children ?? text; 10 | 11 | return
{content}
; 12 | }; 13 | 14 | MessageTextContent.displayName = "Message.TextContent"; 15 | 16 | MessageTextContent.propTypes = { 17 | /** Primary content - message text */ 18 | children: PropTypes.string, 19 | 20 | /** Message text. Property has precedence over children */ 21 | text: PropTypes.string, 22 | 23 | /** Additional classes. */ 24 | className: PropTypes.string, 25 | }; 26 | 27 | export default MessageTextContent; 28 | -------------------------------------------------------------------------------- /src/components/Message/index.js: -------------------------------------------------------------------------------- 1 | import Message from "./Message"; 2 | export * from "./Message"; 3 | export default Message; 4 | -------------------------------------------------------------------------------- /src/components/MessageGroup/MessageGroup.d.ts: -------------------------------------------------------------------------------- 1 | import type {ReactElement} from "react"; 2 | import type {ChatComponentPropsChildren, EmptyProps} from "../../types"; 3 | import type {AvatarPosition, MessageDirection} from "../../types/unions"; 4 | 5 | export type MessageGroupHeaderProps = EmptyProps; 6 | 7 | export declare const MessageGroupHeader: (props:ChatComponentPropsChildren) => ReactElement; 8 | 9 | export type MessageGroupFooterProps = EmptyProps; 10 | 11 | export declare const MessageGroupFooter: (props:ChatComponentPropsChildren) => ReactElement; 12 | 13 | 14 | export type MessageGroupMessagesProps = EmptyProps; 15 | 16 | export declare const MessageGroupMessages: (props:ChatComponentPropsChildren) => ReactElement; 17 | 18 | export interface MessageGroupProps { 19 | direction?:MessageDirection; 20 | avatarPosition?:AvatarPosition; 21 | sentTime?:string; 22 | sender?:string; 23 | } 24 | 25 | export declare const MessageGroup: { 26 | (props:ChatComponentPropsChildren):ReactElement; 27 | Header: typeof MessageGroupHeader; 28 | Footer: typeof MessageGroupFooter; 29 | Messages: typeof MessageGroupMessages; 30 | }; 31 | 32 | export default MessageGroup; -------------------------------------------------------------------------------- /src/components/MessageGroup/MessageGroup.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import classNames from "classnames"; 4 | import { allowedChildren, getChildren } from "../utils"; 5 | import { prefix } from "../settings"; 6 | import MessageGroupHeader from "./MessageGroupHeader"; 7 | import MessageGroupFooter from "./MessageGroupFooter"; 8 | import MessageGroupMessages from "./MessageGroupMessages"; 9 | import Avatar from "../Avatar"; 10 | 11 | export const MessageGroup = ({ 12 | direction = "incoming", 13 | avatarPosition = undefined, 14 | sender = "", 15 | sentTime = "", 16 | children, 17 | className, 18 | ...rest 19 | }) => { 20 | const cName = `${prefix}-message-group`; 21 | 22 | const directionClass = (() => { 23 | if (direction === 0 || direction === "incoming") { 24 | return `${cName}--incoming`; 25 | } else if (direction === 1 || direction === "outgoing") { 26 | return `${cName}--outgoing`; 27 | } 28 | })(); 29 | 30 | const avatarPositionClass = (() => { 31 | const prefix = `${cName}--avatar-`; 32 | if (typeof avatarPosition === "string") { 33 | if ( 34 | avatarPosition === "tl" || 35 | avatarPosition === "top-left" || 36 | avatarPosition === "tr" || 37 | avatarPosition === "top-right" || 38 | avatarPosition === "bl" || 39 | avatarPosition === "bottom-right" || 40 | avatarPosition === "br" || 41 | avatarPosition === "bottom-right" || 42 | avatarPosition === "cl" || 43 | avatarPosition === "center-left" || 44 | avatarPosition === "cr" || 45 | avatarPosition === "center-right" 46 | ) { 47 | return `${prefix}${avatarPosition}`; 48 | } 49 | } 50 | })(); 51 | 52 | const [avatar, header, footer, messages] = getChildren(children, [ 53 | Avatar, 54 | MessageGroupHeader, 55 | MessageGroupFooter, 56 | MessageGroupMessages, 57 | ]); 58 | 59 | const ariaLabel = (() => { 60 | if (sender.length > 0 && sentTime.length > 0) { 61 | return `${sender}: ${sentTime}`; 62 | } else if (sender.length > 0 && sentTime.length === 0) { 63 | return sender; 64 | } else { 65 | return null; 66 | } 67 | })(); 68 | 69 | return ( 70 |
81 | {typeof avatar !== "undefined" && ( 82 |
{avatar}
83 | )} 84 |
85 | {header} 86 | {messages} 87 | {footer} 88 |
89 |
90 | ); 91 | }; 92 | 93 | MessageGroup.propTypes = { 94 | /** Direction. */ 95 | direction: PropTypes.oneOf(["incoming", "outgoing", 0, 1]), 96 | 97 | /** Avatar position. */ 98 | avatarPosition: PropTypes.oneOf(["tl", "tr", "br", "bl", "cl", "cr"]), 99 | sentTime: PropTypes.string, 100 | sender: PropTypes.string, 101 | /** 102 | * Primary content. 103 | * Allowed nodes: 104 | * 105 | * * <Avatar /> 106 | * * <MessageGroup.Header /> 107 | * * <MessageGroup.Footer /> 108 | * * <MessageGroup.Messages /> 109 | * 110 | */ 111 | children: allowedChildren([ 112 | Avatar, 113 | MessageGroupHeader, 114 | MessageGroupFooter, 115 | MessageGroupMessages, 116 | ]), 117 | 118 | /** Additional classes. */ 119 | className: PropTypes.string, 120 | }; 121 | 122 | MessageGroup.Header = MessageGroupHeader; 123 | MessageGroup.Footer = MessageGroupFooter; 124 | MessageGroup.Messages = MessageGroupMessages; 125 | 126 | export default MessageGroup; 127 | -------------------------------------------------------------------------------- /src/components/MessageGroup/MessageGroupFooter.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import classNames from "classnames"; 4 | import { prefix } from "../settings"; 5 | 6 | export const MessageGroupFooter = ({ children = undefined, className, ...rest }) => { 7 | const cName = `${prefix}-message-group__footer`; 8 | 9 | return ( 10 |
11 | {children} 12 |
13 | ); 14 | }; 15 | 16 | MessageGroupFooter.displayName = "MessageGroup.Footer"; 17 | 18 | MessageGroupFooter.propTypes = { 19 | /** Primary content. */ 20 | children: PropTypes.node, 21 | 22 | /** Additional classes. */ 23 | className: PropTypes.string, 24 | }; 25 | 26 | export default MessageGroupFooter; 27 | -------------------------------------------------------------------------------- /src/components/MessageGroup/MessageGroupHeader.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import classNames from "classnames"; 4 | import { prefix } from "../settings"; 5 | 6 | export const MessageGroupHeader = ({ children = undefined, className, ...rest }) => { 7 | const cName = `${prefix}-message-group__header`; 8 | return ( 9 |
10 | {children} 11 |
12 | ); 13 | }; 14 | 15 | MessageGroupHeader.displayName = "MessageGroup.Header"; 16 | 17 | MessageGroupHeader.propTypes = { 18 | /** Primary content. */ 19 | children: PropTypes.node, 20 | 21 | /** Additional classes. */ 22 | className: PropTypes.string, 23 | }; 24 | 25 | export default MessageGroupHeader; 26 | -------------------------------------------------------------------------------- /src/components/MessageGroup/MessageGroupMessages.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import classNames from "classnames"; 4 | 5 | import { prefix } from "../settings"; 6 | 7 | export const MessageGroupMessages = ({ children = undefined, className, ...rest }) => { 8 | const cName = `${prefix}-message-group`; 9 | return ( 10 |
11 | {children} 12 |
13 | ); 14 | }; 15 | 16 | MessageGroupMessages.displayName = "MessageGroup.Messages"; 17 | 18 | MessageGroupMessages.propTypes = { 19 | /** 20 | * Messages. 21 | * Allowed node: 22 | * 23 | * * <Message /> 24 | */ 25 | children: PropTypes.node, 26 | 27 | /** Additional classes. */ 28 | className: PropTypes.string, 29 | }; 30 | 31 | export default MessageGroupMessages; 32 | -------------------------------------------------------------------------------- /src/components/MessageGroup/index.js: -------------------------------------------------------------------------------- 1 | import MessageGroup from "./MessageGroup"; 2 | export * from "./MessageGroup"; 3 | export default MessageGroup; 4 | -------------------------------------------------------------------------------- /src/components/MessageInput/MessageInput.d.ts: -------------------------------------------------------------------------------- 1 | import type {MouseEvent, ReactElement} from "react"; 2 | import type {ChatComponentPropsRef} from "../../types"; 3 | 4 | export interface MessageInputProps { 5 | value?: string; 6 | placeholder?: string; 7 | disabled?: boolean; 8 | sendOnReturnDisabled?: boolean; 9 | sendDisabled?: boolean; 10 | fancyScroll?: boolean; 11 | activateAfterChange?: boolean; 12 | autoFocus?: boolean; 13 | onChange?: (innerHtml: string, textContent: string, innerText: string, nodes: NodeList) => void; 14 | onSend?: (innerHtml: string, textContent: string, innerText: string, nodes: NodeList) => void; 15 | sendButton?: boolean; 16 | attachButton?: boolean; 17 | attachDisabled?: boolean; 18 | onAttachClick?: (evt: MouseEvent) => void; 19 | } 20 | 21 | export declare const MessageInput: (props: ChatComponentPropsRef) => ReactElement; 22 | 23 | export default MessageInput; -------------------------------------------------------------------------------- /src/components/MessageInput/MessageInput.jsx: -------------------------------------------------------------------------------- 1 | import React, { 2 | Component, 3 | useRef, 4 | useState, 5 | useEffect, 6 | useImperativeHandle, 7 | forwardRef, 8 | } from "react"; 9 | import { noop } from "../utils"; 10 | import PropTypes from "prop-types"; 11 | import classNames from "classnames"; 12 | import { prefix } from "../settings"; 13 | import ContentEditable from "../ContentEditable"; 14 | import SendButton from "../Buttons/SendButton"; 15 | import AttachmentButton from "../Buttons/AttachmentButton"; 16 | import PerfectScrollbar from "../Scroll"; 17 | 18 | // Because container depends on fancyScroll 19 | // it must be wrapped in additional container 20 | function editorContainer() { 21 | class Container extends Component { 22 | render() { 23 | const { 24 | props: { fancyScroll, children, forwardedRef, ...rest }, 25 | } = this; 26 | 27 | return ( 28 | <> 29 | {fancyScroll === true && ( 30 | (forwardedRef.current = elRef)} 32 | {...rest} 33 | options={{ suppressScrollX: true }} 34 | > 35 | {children} 36 | 37 | )} 38 | {fancyScroll === false && ( 39 |
40 | {children} 41 |
42 | )} 43 | 44 | ); 45 | } 46 | } 47 | 48 | return React.forwardRef((props, ref) => { 49 | return ; 50 | }); 51 | } 52 | 53 | const EditorContainer = editorContainer(); 54 | 55 | const useControllableState = (value, initialValue) => { 56 | const initial = typeof value !== "undefined" ? value : initialValue; 57 | const [stateValue, setStateValue] = useState(initial); 58 | const effectiveValue = typeof value !== "undefined" ? value : stateValue; 59 | 60 | return [ 61 | effectiveValue, 62 | (newValue) => { 63 | setStateValue(newValue); 64 | }, 65 | ]; 66 | }; 67 | 68 | function MessageInputInner( 69 | { 70 | value = undefined, 71 | onSend = noop, 72 | onChange = noop, 73 | autoFocus = false, 74 | placeholder = "", 75 | fancyScroll = true, 76 | className, 77 | activateAfterChange = false, 78 | disabled = false, 79 | sendDisabled, 80 | sendOnReturnDisabled = false, 81 | attachDisabled = false, 82 | sendButton = true, 83 | attachButton = true, 84 | onAttachClick = noop, 85 | ...rest 86 | }, 87 | ref 88 | ) { 89 | const scrollRef = useRef(); 90 | const msgRef = useRef(); 91 | const [stateValue, setStateValue] = useControllableState(value, ""); 92 | const [stateSendDisabled, setStateSendDisabled] = useControllableState( 93 | sendDisabled, 94 | true 95 | ); 96 | 97 | // Public API 98 | const focus = () => { 99 | if (typeof msgRef.current !== "undefined") { 100 | msgRef.current.focus(); 101 | } 102 | }; 103 | 104 | // Return object with public Api 105 | useImperativeHandle(ref, () => ({ 106 | focus, 107 | })); 108 | 109 | // Set focus 110 | useEffect(() => { 111 | if (autoFocus === true) { 112 | focus(); 113 | } 114 | }, []); 115 | 116 | // Update scroll 117 | useEffect(() => { 118 | if (typeof scrollRef.current.updateScroll === "function") { 119 | scrollRef.current.updateScroll(); 120 | } 121 | }); 122 | 123 | const getContent = () => { 124 | // Direct reference to contenteditable div 125 | const contentEditableRef = msgRef.current.msgRef.current; 126 | return [ 127 | contentEditableRef.textContent, 128 | contentEditableRef.innerText, 129 | contentEditableRef.cloneNode(true).childNodes, 130 | ]; 131 | }; 132 | 133 | const send = () => { 134 | if (stateValue.length > 0) { 135 | // Clear input only when it's uncontrolled mode 136 | if (value === undefined) { 137 | setStateValue(""); 138 | } 139 | 140 | // Disable send button only when it's uncontrolled mode 141 | if (typeof sendDisabled === "undefined") { 142 | setStateSendDisabled(true); 143 | } 144 | 145 | const content = getContent(); 146 | 147 | onSend(stateValue, content[0], content[1], content[2]); 148 | } 149 | }; 150 | 151 | const handleKeyPress = (evt) => { 152 | if ( 153 | evt.key === "Enter" && 154 | evt.shiftKey === false && 155 | sendOnReturnDisabled === false 156 | ) { 157 | evt.preventDefault(); 158 | send(); 159 | } 160 | }; 161 | 162 | const handleChange = (innerHTML, textContent, innerText) => { 163 | setStateValue(innerHTML); 164 | if (typeof sendDisabled === "undefined") { 165 | setStateSendDisabled(textContent.length === 0); 166 | } 167 | 168 | if (typeof scrollRef.current.updateScroll === "function") { 169 | scrollRef.current.updateScroll(); 170 | } 171 | 172 | const content = getContent(); 173 | 174 | onChange(innerHTML, textContent, innerText, content[2]); 175 | }; 176 | 177 | const cName = `${prefix}-message-input`, 178 | ph = typeof placeholder === "string" ? placeholder : ""; 179 | 180 | return ( 181 |
189 | {attachButton === true && ( 190 |
191 | 195 |
196 | )} 197 | 198 |
199 | 204 | 214 | 215 |
216 | {sendButton === true && ( 217 |
218 | 222 |
223 | )} 224 |
225 | ); 226 | } 227 | 228 | const MessageInput = forwardRef(MessageInputInner); 229 | MessageInput.displayName = "MessageInput"; 230 | 231 | MessageInput.propTypes = { 232 | /** Value. */ 233 | value: PropTypes.string, 234 | 235 | /** Placeholder. */ 236 | placeholder: PropTypes.string, 237 | 238 | /** A input can show it is currently unable to be interacted with. */ 239 | disabled: PropTypes.bool, 240 | 241 | /** Prevent that the input message is sent on a return press */ 242 | sendOnReturnDisabled: PropTypes.bool, 243 | 244 | /** Send button can be disabled.
245 | * It's state is tracked by component, but it can be forced */ 246 | sendDisabled: PropTypes.bool, 247 | 248 | /** 249 | * Fancy scroll 250 | * This property is set in constructor, and is not changing when component update. 251 | */ 252 | fancyScroll: PropTypes.bool, 253 | 254 | /** 255 | * Sets focus element and caret at the end of input
256 | * when value is changed programmatically (e.g) from button click and element is not active 257 | */ 258 | activateAfterChange: PropTypes.bool, 259 | 260 | /** Set focus after mount. */ 261 | autoFocus: PropTypes.bool, 262 | 263 | /** 264 | * onChange handler
265 | * @param {String} innerHtml 266 | * @param {String} textContent 267 | * @param {String} innerText 268 | * @param {NodeList} nodes 269 | */ 270 | onChange: PropTypes.func, 271 | 272 | /** 273 | * onSend handler
274 | * @param {String} innerHtml 275 | * @param {String} textContent 276 | * @param {String} innerText 277 | * @param {NodeList} nodes 278 | */ 279 | onSend: PropTypes.func, 280 | 281 | /** Additional classes. */ 282 | className: PropTypes.string, 283 | 284 | /** Show send button */ 285 | sendButton: PropTypes.bool, 286 | 287 | /** Show add attachment button */ 288 | attachButton: PropTypes.bool, 289 | 290 | /** Disable add attachment button */ 291 | attachDisabled: PropTypes.bool, 292 | 293 | /** 294 | * onAttachClick handler 295 | */ 296 | onAttachClick: PropTypes.func, 297 | }; 298 | 299 | MessageInputInner.propTypes = MessageInput.propTypes; 300 | 301 | export { MessageInput }; 302 | 303 | export default MessageInput; 304 | -------------------------------------------------------------------------------- /src/components/MessageInput/index.js: -------------------------------------------------------------------------------- 1 | import MessageInput from "./MessageInput"; 2 | export * from "./MessageInput"; 3 | export default MessageInput; 4 | -------------------------------------------------------------------------------- /src/components/MessageList/MessageList.d.ts: -------------------------------------------------------------------------------- 1 | import type {ReactElement, ReactNode} from "react"; 2 | import type {ChatComponentPropsChildrenRef, ChatComponentPropsChildren, EmptyProps} from "../../types"; 3 | 4 | export type MessageListContentProps = EmptyProps; 5 | 6 | declare const MessageListContent: (props:ChatComponentPropsChildren) => ReactElement; 7 | 8 | export interface MessageListOwnProps { 9 | typingIndicator?: ReactNode; 10 | loading?:boolean; 11 | loadingMore?:boolean; 12 | loadingMorePosition?:"top" | "bottom"; 13 | onYReachStart?: (container:HTMLDivElement) => void; 14 | onYReachEnd?: (container:HTMLDivElement) => void; 15 | disableOnYReachWhenNoScroll?:boolean; 16 | autoScrollToBottom?:boolean; 17 | autoScrollToBottomOnMount?:boolean; 18 | scrollBehavior?: "auto" | "smooth"; 19 | } 20 | 21 | export type MessageListProps = ChatComponentPropsChildrenRef 22 | 23 | declare const MessageList: { 24 | (props:MessageListProps):ReactElement; 25 | Content: typeof MessageListContent; 26 | }; 27 | 28 | export { 29 | MessageListContent, 30 | MessageList 31 | }; 32 | 33 | export default MessageList; -------------------------------------------------------------------------------- /src/components/MessageList/MessageList.jsx: -------------------------------------------------------------------------------- 1 | import React, { forwardRef, useImperativeHandle, useRef } from "react"; 2 | import PropTypes from "prop-types"; 3 | import classNames from "classnames"; 4 | import { allowedChildren, getChildren } from "../utils"; 5 | import { prefix } from "../settings"; 6 | import PerfectScrollbar from "../Scroll"; 7 | import Loader from "../Loader"; 8 | import Overlay from "../Overlay"; 9 | import Message from "../Message"; 10 | import MessageGroup from "../MessageGroup"; 11 | import MessageSeparator from "../MessageSeparator"; 12 | import MessageListContent from "./MessageListContent"; 13 | 14 | class MessageListInner extends React.Component { 15 | constructor(props) { 16 | super(props); 17 | 18 | this.scrollPointRef = React.createRef(); 19 | this.containerRef = React.createRef(); 20 | this.scrollRef = React.createRef(); 21 | this.lastClientHeight = 0; 22 | this.preventScrollTop = false; 23 | this.resizeObserver = undefined; 24 | this.scrollTicking = false; 25 | this.resizeTicking = false; 26 | this.noScroll = undefined; 27 | } 28 | 29 | getSnapshotBeforeUpdate() { 30 | const list = this.containerRef.current; 31 | 32 | const topHeight = Math.round(list.scrollTop + list.clientHeight); 33 | // 1 px fix for firefox 34 | const sticky = 35 | list.scrollHeight === topHeight || 36 | list.scrollHeight + 1 === topHeight || 37 | list.scrollHeight - 1 === topHeight; 38 | 39 | return { 40 | sticky, 41 | clientHeight: list.clientHeight, 42 | scrollHeight: list.scrollHeight, 43 | lastMessageOrGroup: this.getLastMessageOrGroup(), 44 | diff: list.scrollHeight - list.scrollTop, 45 | }; 46 | } 47 | 48 | handleResize = () => { 49 | // If container is smaller than before resize - scroll to End 50 | if (this.containerRef.current.clientHeight < this.lastClientHeight) { 51 | this.scrollToEnd(this.props.scrollBehavior); 52 | } 53 | 54 | this.scrollRef.current.updateScroll(); 55 | }; 56 | 57 | handleContainerResize = () => { 58 | if (this.resizeTicking === false) { 59 | window.requestAnimationFrame(() => { 60 | const list = this.containerRef.current; 61 | 62 | if (list) { 63 | const currentHeight = list.clientHeight; 64 | 65 | const diff = currentHeight - this.lastClientHeight; 66 | 67 | if (diff >= 1) { 68 | // Because fractional 69 | 70 | if (this.preventScrollTop === false) { 71 | list.scrollTop = Math.round(list.scrollTop) - diff; 72 | } 73 | } else { 74 | list.scrollTop = list.scrollTop - diff; 75 | } 76 | 77 | this.lastClientHeight = list.clientHeight; 78 | 79 | this.scrollRef.current.updateScroll(); 80 | } 81 | 82 | this.resizeTicking = false; 83 | }); 84 | 85 | this.resizeTicking = true; 86 | } 87 | }; 88 | 89 | isSticked = () => { 90 | const list = this.containerRef.current; 91 | 92 | return list.scrollHeight === Math.round(list.scrollTop + list.clientHeight); 93 | }; 94 | 95 | handleScroll = () => { 96 | if (this.scrollTicking === false) { 97 | window.requestAnimationFrame(() => { 98 | if (this.noScroll === false) { 99 | this.preventScrollTop = this.isSticked(); 100 | } else { 101 | this.noScroll = false; 102 | } 103 | 104 | this.scrollTicking = false; 105 | }); 106 | 107 | this.scrollTicking = true; 108 | } 109 | }; 110 | 111 | componentDidMount() { 112 | // Set scrollbar to bottom on start (getSnaphotBeforeUpdate is not invoked on mount) 113 | if (this.props.autoScrollToBottomOnMount === true) { 114 | this.scrollToEnd(this.props.scrollBehavior); 115 | } 116 | 117 | this.lastClientHeight = this.containerRef.current.clientHeight; 118 | 119 | window.addEventListener("resize", this.handleResize); 120 | 121 | if (typeof window.ResizeObserver === "function") { 122 | this.resizeObserver = new ResizeObserver(this.handleContainerResize); 123 | this.resizeObserver.observe(this.containerRef.current); 124 | } 125 | this.containerRef.current.addEventListener("scroll", this.handleScroll); 126 | } 127 | 128 | componentDidUpdate(prevProps, prevState, snapshot) { 129 | const { 130 | props: { autoScrollToBottom }, 131 | } = this; 132 | 133 | if (typeof snapshot !== "undefined") { 134 | const list = this.containerRef.current; 135 | 136 | const { lastElement, lastMessageInGroup } = this.getLastMessageOrGroup(); 137 | 138 | if (lastElement === snapshot.lastMessageOrGroup.lastElement) { 139 | // If lastMessageInGroup is defined last element is MessageGroup otherwise its Message 140 | if ( 141 | typeof lastMessageInGroup === "undefined" || 142 | lastMessageInGroup === snapshot.lastMessageOrGroup.lastMessageInGroup 143 | ) { 144 | list.scrollTop = 145 | list.scrollHeight - 146 | snapshot.diff + 147 | (this.lastClientHeight - list.clientHeight); 148 | } 149 | } 150 | 151 | if (snapshot.sticky === true) { 152 | if (autoScrollToBottom === true) { 153 | this.scrollToEnd(this.props.scrollBehavior); 154 | } 155 | this.preventScrollTop = true; 156 | } else { 157 | if (snapshot.clientHeight < this.lastClientHeight) { 158 | // If was sticky because scrollHeight is not changing, so here will be equal to lastHeight plus current scrollTop 159 | // 1px fix id for firefox 160 | const sHeight = list.scrollTop + this.lastClientHeight; 161 | if ( 162 | list.scrollHeight === sHeight || 163 | list.scrollHeight + 1 === sHeight || 164 | list.scrollHeight - 1 === sHeight 165 | ) { 166 | if (autoScrollToBottom === true) { 167 | this.scrollToEnd(this.props.scrollBehavior); 168 | this.preventScrollTop = true; 169 | } 170 | } else { 171 | this.preventScrollTop = false; 172 | } 173 | } else { 174 | this.preventScrollTop = false; 175 | 176 | if (lastElement === snapshot.lastMessageOrGroup.lastElement) { 177 | if ( 178 | typeof lastMessageInGroup === "undefined" || 179 | lastMessageInGroup === 180 | snapshot.lastMessageOrGroup.lastMessageInGroup 181 | ) { 182 | // New elements were not added at end 183 | // New elements were added at start 184 | if ( 185 | list.scrollTop === 0 && 186 | list.scrollHeight > snapshot.scrollHeight 187 | ) { 188 | list.scrollTop = list.scrollHeight - snapshot.scrollHeight; 189 | } 190 | } 191 | } 192 | } 193 | } 194 | 195 | this.lastClientHeight = snapshot.clientHeight; 196 | } 197 | } 198 | 199 | componentWillUnmount() { 200 | window.removeEventListener("resize", this.handleResize); 201 | if (typeof this.resizeObserver !== "undefined") { 202 | this.resizeObserver.disconnect(); 203 | } 204 | this.containerRef.current.removeEventListener("scroll", this.handleScroll); 205 | } 206 | 207 | scrollToEnd(scrollBehavior = this.props.scrollBehavior) { 208 | const list = this.containerRef.current; 209 | const scrollPoint = this.scrollPointRef.current; 210 | 211 | // https://stackoverflow.com/a/45411081/6316091 212 | const parentRect = list.getBoundingClientRect(); 213 | const childRect = scrollPoint.getBoundingClientRect(); 214 | 215 | // Scroll by offset relative to parent 216 | const scrollOffset = childRect.top + list.scrollTop - parentRect.top; 217 | 218 | if (list.scrollBy) { 219 | list.scrollBy({ top: scrollOffset, behavior: scrollBehavior }); 220 | } else { 221 | list.scrollTop = scrollOffset; 222 | } 223 | 224 | this.lastClientHeight = list.clientHeight; 225 | 226 | // Important flag! Blocks strange Chrome mobile behaviour - automatic scroll. 227 | // Chrome mobile sometimes trigger scroll when new content is entered to MessageInput. It's probably Chrome Bug - sth related with overflow-anchor 228 | this.noScroll = true; 229 | } 230 | 231 | getLastMessageOrGroup = () => { 232 | const lastElement = this.containerRef.current.querySelector( 233 | `[data-${prefix}-message-list]>[data-${prefix}-message]:last-of-type,[data-${prefix}-message-list]>[data-${prefix}-message-group]:last-of-type` 234 | ); 235 | 236 | const lastMessageInGroup = lastElement?.querySelector( 237 | `[data-${prefix}-message]:last-of-type` 238 | ); 239 | 240 | return { 241 | lastElement, 242 | lastMessageInGroup, 243 | }; 244 | }; 245 | 246 | render() { 247 | const { 248 | props: { 249 | children, 250 | typingIndicator, 251 | loading, 252 | loadingMore, 253 | loadingMorePosition, 254 | onYReachStart, 255 | onYReachEnd, 256 | className, 257 | disableOnYReachWhenNoScroll, 258 | scrollBehavior, // Just to remove rest 259 | autoScrollToBottom, // Just to remove rest 260 | autoScrollToBottomOnMount, // Just to remove rest 261 | ...rest 262 | }, 263 | } = this; 264 | 265 | const cName = `${prefix}-message-list`; 266 | 267 | const [customContent] = getChildren(children, [MessageListContent]); 268 | 269 | return ( 270 |
271 | {loadingMore && ( 272 |
278 | 279 |
280 | )} 281 | {loading && ( 282 | 283 | 284 | 285 | )} 286 | ps.update(disableOnYReachWhenNoScroll)} 290 | className={`${cName}__scroll-wrapper`} 291 | ref={this.scrollRef} 292 | containerRef={(ref) => (this.containerRef.current = ref)} 293 | options={{ suppressScrollX: true }} 294 | {...{ [`data-${prefix}-message-list`]: "" }} 295 | style={{ 296 | overscrollBehaviorY: "none", 297 | overflowAnchor: "auto", 298 | touchAction: "none", 299 | }} 300 | > 301 | {customContent ? customContent : children} 302 |
306 |
307 | {typeof typingIndicator !== "undefined" && ( 308 |
309 | {typingIndicator} 310 |
311 | )} 312 |
313 | ); 314 | } 315 | } 316 | 317 | MessageListInner.displayName = "MessageList"; 318 | 319 | function MessageListFunc(props, ref) { 320 | const msgListRef = useRef(); 321 | 322 | const scrollToBottom = (scrollBehavior) => 323 | msgListRef.current.scrollToEnd(scrollBehavior); 324 | 325 | // Return object with public Api 326 | useImperativeHandle(ref, () => ({ 327 | scrollToBottom, 328 | })); 329 | 330 | return ; 331 | } 332 | 333 | const MessageList = forwardRef(MessageListFunc); 334 | 335 | MessageList.propTypes = { 336 | /** 337 | * Primary content. Message elements 338 | * Allowed components: 339 | * 340 | * * <Message /> 341 | * * <MessageGroup /> 342 | * * <MessageSeparator /> 343 | * * <MessageListContent /> 344 | */ 345 | children: allowedChildren([ 346 | Message, 347 | MessageGroup, 348 | MessageSeparator, 349 | MessageListContent, 350 | ]), 351 | 352 | /** Typing indicator element. */ 353 | typingIndicator: PropTypes.node, 354 | 355 | /** Loading flag. */ 356 | loading: PropTypes.bool, 357 | 358 | /** Loading more flag for infinity scroll. */ 359 | loadingMore: PropTypes.bool, 360 | 361 | /** Loading more loader position. */ 362 | loadingMorePosition: PropTypes.oneOf(["top", "bottom"]), 363 | 364 | /** 365 | * This is fired when the scrollbar reaches the beginning on the y axis.
366 | * It can be used to load previous messages using the infinite scroll. 367 | */ 368 | onYReachStart: PropTypes.func, 369 | 370 | /** 371 | * This is fired when the scrollbar reaches the end on the y axis.
372 | * It can be used to load next messages using the infinite scroll. 373 | */ 374 | onYReachEnd: PropTypes.func, 375 | 376 | /** 377 | * Disables onYReachStart and onYReachEnd events from being fired
378 | * when the list is not scrollable. 379 | * This is set to false by default for backward compatibility. 380 | */ 381 | disableOnYReachWhenNoScroll: PropTypes.bool, 382 | 383 | /** 384 | * Auto scroll to bottom 385 | */ 386 | autoScrollToBottom: PropTypes.bool, 387 | 388 | /** 389 | * Auto scroll to bottom on mount 390 | */ 391 | autoScrollToBottomOnMount: PropTypes.bool, 392 | 393 | /** 394 | * Scroll behavior 395 | * https://developer.mozilla.org/en-US/docs/Web/API/ScrollToOptions/behavior 396 | */ 397 | scrollBehavior: PropTypes.oneOf(["auto", "smooth"]), 398 | 399 | /** Additional classes. */ 400 | className: PropTypes.string, 401 | }; 402 | 403 | MessageList.defaultProps = { 404 | typingIndicator: undefined, 405 | loading: false, 406 | loadingMore: false, 407 | loadingMorePosition: "top", 408 | disableOnYReachWhenNoScroll: false, 409 | autoScrollToBottom: true, 410 | autoScrollToBottomOnMount: true, 411 | scrollBehavior: "auto", 412 | }; 413 | 414 | MessageListInner.propTypes = MessageList.propTypes; 415 | MessageListInner.defaultProps = MessageList.defaultProps; 416 | 417 | MessageList.Content = MessageListContent; 418 | 419 | export default MessageList; 420 | -------------------------------------------------------------------------------- /src/components/MessageList/MessageListContent.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | export const MessageListContent = ({ className, children, ...rest }) => ( 5 |
6 | {children} 7 |
8 | ); 9 | 10 | MessageListContent.displayName = "MessageList.Content"; 11 | 12 | MessageListContent.propTypes = { 13 | /** Primary content. Message elements */ 14 | children: PropTypes.node, 15 | 16 | /** Additional classes. */ 17 | className: PropTypes.string, 18 | }; 19 | 20 | export default MessageListContent; 21 | -------------------------------------------------------------------------------- /src/components/MessageList/index.js: -------------------------------------------------------------------------------- 1 | import MessageList from "./MessageList"; 2 | export * from "./MessageList"; 3 | export default MessageList; 4 | -------------------------------------------------------------------------------- /src/components/MessageSeparator/MessageSeparator.d.ts: -------------------------------------------------------------------------------- 1 | import type {ReactElement} from "react"; 2 | import type {ChatComponentPropsChildren} from "../../types"; 3 | 4 | // TODO: How to type element TYPE HERE? 5 | export interface MessageSeparatorProps { 6 | as?:string; 7 | } 8 | 9 | export declare const MessageSeparator: (props:ChatComponentPropsChildren) => ReactElement; 10 | export default MessageSeparator; -------------------------------------------------------------------------------- /src/components/MessageSeparator/MessageSeparator.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import classNames from "classnames"; 4 | import { prefix } from "../settings"; 5 | import { isChildrenNil } from "../utils"; 6 | 7 | export const MessageSeparator = ({ 8 | content = undefined, 9 | as = "div", 10 | children = undefined, 11 | className, 12 | ...rest 13 | }) => { 14 | const cName = `${prefix}-message-separator`; 15 | 16 | const Tag = (() => { 17 | if (typeof as === "string" && as.length > 0) { 18 | return as; 19 | } else { 20 | return "div"; 21 | } 22 | })(); 23 | 24 | return ( 25 | 26 | {isChildrenNil(children) === true ? content : children} 27 | 28 | ); 29 | }; 30 | 31 | MessageSeparator.propTypes = { 32 | /** Primary content. */ 33 | children: PropTypes.node, 34 | 35 | /** Shorthand for primary content. */ 36 | content: PropTypes.node, 37 | 38 | /** An element type to render as. */ 39 | as: PropTypes.elementType, 40 | 41 | /** Additional classes. */ 42 | className: PropTypes.string, 43 | }; 44 | 45 | export default MessageSeparator; 46 | -------------------------------------------------------------------------------- /src/components/MessageSeparator/index.js: -------------------------------------------------------------------------------- 1 | import MessageSeparator from "./MessageSeparator"; 2 | export * from "./MessageSeparator"; 3 | export default MessageSeparator; 4 | -------------------------------------------------------------------------------- /src/components/Overlay/Overlay.d.ts: -------------------------------------------------------------------------------- 1 | import type {ReactElement} from "react"; 2 | import type {ChatComponentPropsChildren} from "../../types"; 3 | 4 | export interface OverlayProps { 5 | blur?:boolean; 6 | grayscale?:boolean; 7 | } 8 | 9 | export declare const Overlay: (props:ChatComponentPropsChildren) => ReactElement; 10 | 11 | export default Overlay; -------------------------------------------------------------------------------- /src/components/Overlay/Overlay.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import classNames from "classnames"; 4 | import { prefix } from "../settings"; 5 | 6 | export const Overlay = ({ className = "", children = undefined, blur = false, grayscale = false, ...rest }) => { 7 | const cName = `${prefix}-overlay`; 8 | const blurClass = `${cName}--blur`; 9 | const grayscaleClass = `${cName}--grayscale`; 10 | 11 | return ( 12 |
21 |
{children}
22 |
23 | ); 24 | }; 25 | 26 | Overlay.propTypes = { 27 | /** Primary content. */ 28 | children: PropTypes.node, 29 | 30 | /** Additional classes. */ 31 | className: PropTypes.string, 32 | 33 | /** 34 | * Blur overlayed content. 35 | * This feature is experimental and have limited browser support 36 | */ 37 | blur: PropTypes.bool, 38 | 39 | /** 40 | * Grayscale overlayed content. 41 | * This feature is experimental and have limited browser support 42 | */ 43 | grayscale: PropTypes.bool, 44 | }; 45 | 46 | export default Overlay; 47 | -------------------------------------------------------------------------------- /src/components/Overlay/index.js: -------------------------------------------------------------------------------- 1 | import Overlay from "./Overlay"; 2 | export * from "./Overlay"; 3 | export default Overlay; 4 | -------------------------------------------------------------------------------- /src/components/Scroll/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 chatscope.io 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /src/components/Scroll/README.md: -------------------------------------------------------------------------------- 1 | # Strange things 2 | 3 | **perfect-scrollbar.ems.js** 4 | Builded version of https://github.com/mdbootstrap/perfect-scrollbar with two fixes apllied manually: 5 | 6 | - Issue: https://github.com/mdbootstrap/perfect-scrollbar/issues/947 7 | Fix: https://github.com/mdbootstrap/perfect-scrollbar/pull/946/commits/8f9db2986da2d5fa3fa402b169f1c9b1ffe53369 8 | 9 | - Issue: https://github.com/mdbootstrap/perfect-scrollbar/issues/920#issuecomment-722570659 10 | Fix: https://github.com/mdbootstrap/perfect-scrollbar/commit/daeeddf5972c44a960f1d244a4b9719cb7c3d0b7 11 | 12 | - Issue and fix: https://github.com/mdbootstrap/perfect-scrollbar/issues/975 13 | 14 | - Issue: https://github.com/mdbootstrap/perfect-scrollbar/issues/975 15 | Fix: fixed by adding { passive: false } Yes! false not true! to some event handlers. Also added css property touch-action: none to MessageList scrollbar container. 16 | 17 | Modified code is based on v1.5.0 18 | 19 | **ReactPerfectScrollbar.jsx** 20 | Based on https://github.com/goldenyz/react-perfect-scrollbar/ but modified for using directly local PerfectScrollbar version. 21 | 22 | Modified code is based on v1.5.8 23 | 24 | # Why 25 | 26 | These hacks were made because the necessary Perfect Scrollbar fixes haven't been released for a very long time. 27 | 28 | # License 29 | 30 | Both libraries are licensed under the MIT license. 31 | -------------------------------------------------------------------------------- /src/components/Scroll/ReactPerfectScrollbar.jsx: -------------------------------------------------------------------------------- 1 | // https://github.com/goldenyz/react-perfect-scrollbar/ 2 | import React, { Component } from "react"; 3 | import { PropTypes } from "prop-types"; 4 | import PerfectScrollbar from "./perfect-scrollbar.esm.js"; 5 | 6 | const handlerNameByEvent = { 7 | "ps-scroll-y": "onScrollY", 8 | "ps-scroll-x": "onScrollX", 9 | "ps-scroll-up": "onScrollUp", 10 | "ps-scroll-down": "onScrollDown", 11 | "ps-scroll-left": "onScrollLeft", 12 | "ps-scroll-right": "onScrollRight", 13 | "ps-y-reach-start": "onYReachStart", 14 | "ps-y-reach-end": "onYReachEnd", 15 | "ps-x-reach-start": "onXReachStart", 16 | "ps-x-reach-end": "onXReachEnd", 17 | }; 18 | Object.freeze(handlerNameByEvent); 19 | 20 | export default class ScrollBar extends Component { 21 | constructor(props) { 22 | super(props); 23 | this.handleRef = this.handleRef.bind(this); 24 | this._handlerByEvent = {}; 25 | } 26 | 27 | componentDidMount() { 28 | if (this.props.option) { 29 | /* eslint-disable-next-line no-console */ 30 | console.warn( 31 | 'react-perfect-scrollbar: the "option" prop has been deprecated in favor of "options"' 32 | ); 33 | } 34 | 35 | this._ps = new PerfectScrollbar( 36 | this._container, 37 | this.props.options || this.props.option 38 | ); 39 | // hook up events 40 | this._updateEventHook(); 41 | this._updateClassName(); 42 | } 43 | 44 | componentDidUpdate(prevProps) { 45 | this._updateEventHook(prevProps); 46 | 47 | this.updateScroll(); 48 | 49 | if (prevProps.className !== this.props.className) { 50 | this._updateClassName(); 51 | } 52 | } 53 | 54 | componentWillUnmount() { 55 | // unhook up evens 56 | Object.keys(this._handlerByEvent).forEach((key) => { 57 | const value = this._handlerByEvent[key]; 58 | 59 | if (value) { 60 | this._container.removeEventListener(key, value, false); 61 | } 62 | }); 63 | this._handlerByEvent = {}; 64 | this._ps.destroy(); 65 | this._ps = null; 66 | } 67 | 68 | _updateEventHook(prevProps = {}) { 69 | // hook up events 70 | Object.keys(handlerNameByEvent).forEach((key) => { 71 | const callback = this.props[handlerNameByEvent[key]]; 72 | const prevCallback = prevProps[handlerNameByEvent[key]]; 73 | if (callback !== prevCallback) { 74 | if (prevCallback) { 75 | const prevHandler = this._handlerByEvent[key]; 76 | this._container.removeEventListener(key, prevHandler, false); 77 | this._handlerByEvent[key] = null; 78 | } 79 | if (callback) { 80 | const handler = () => callback(this._container); 81 | this._container.addEventListener(key, handler, false); 82 | this._handlerByEvent[key] = handler; 83 | } 84 | } 85 | }); 86 | } 87 | 88 | _updateClassName() { 89 | const { className } = this.props; 90 | 91 | const psClassNames = this._container.className 92 | .split(" ") 93 | .filter((name) => name.match(/^ps([-_].+|)$/)) 94 | .join(" "); 95 | 96 | if (this._container) { 97 | this._container.className = `scrollbar-container${ 98 | className ? ` ${className}` : "" 99 | }${psClassNames ? ` ${psClassNames}` : ""}`; 100 | } 101 | } 102 | 103 | updateScroll() { 104 | const onSync = this.props.onSync; 105 | if (typeof onSync === "function") { 106 | onSync(this._ps); 107 | } else { 108 | this._ps.update(); 109 | } 110 | } 111 | 112 | handleRef(ref) { 113 | this._container = ref; 114 | this.props.containerRef?.(ref); 115 | } 116 | 117 | render() { 118 | const { 119 | className, 120 | style, 121 | option, 122 | options, 123 | containerRef, 124 | onScrollY, 125 | onScrollX, 126 | onScrollUp, 127 | onScrollDown, 128 | onScrollLeft, 129 | onScrollRight, 130 | onYReachStart, 131 | onYReachEnd, 132 | onXReachStart, 133 | onXReachEnd, 134 | component, 135 | onSync, 136 | children, 137 | ...remainProps 138 | } = this.props; 139 | 140 | const Comp = typeof component === "undefined" ? "div" : component; 141 | 142 | return ( 143 | 144 | {children} 145 | 146 | ); 147 | } 148 | } 149 | 150 | ScrollBar.propTypes = { 151 | children: PropTypes.node.isRequired, 152 | className: PropTypes.string, 153 | style: PropTypes.object, 154 | option: PropTypes.object, 155 | options: PropTypes.object, 156 | containerRef: PropTypes.func, 157 | onScrollY: PropTypes.func, 158 | onScrollX: PropTypes.func, 159 | onScrollUp: PropTypes.func, 160 | onScrollDown: PropTypes.func, 161 | onScrollLeft: PropTypes.func, 162 | onScrollRight: PropTypes.func, 163 | onYReachStart: PropTypes.func, 164 | onYReachEnd: PropTypes.func, 165 | onXReachStart: PropTypes.func, 166 | onXReachEnd: PropTypes.func, 167 | onSync: PropTypes.func, 168 | component: PropTypes.string, 169 | }; 170 | -------------------------------------------------------------------------------- /src/components/Scroll/index.jsx: -------------------------------------------------------------------------------- 1 | import ScrollBar from "./ReactPerfectScrollbar"; 2 | export * from "./ReactPerfectScrollbar"; 3 | export default ScrollBar; 4 | -------------------------------------------------------------------------------- /src/components/Scroll/perfect-scrollbar.esm.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * perfect-scrollbar v1.5.0 3 | * Copyright 2020 Hyunje Jun, MDBootstrap and Contributors 4 | * Licensed under MIT 5 | */ 6 | 7 | function get(element) { 8 | return getComputedStyle(element); 9 | } 10 | 11 | function set(element, obj) { 12 | for (var key in obj) { 13 | var val = obj[key]; 14 | if (typeof val === "number") { 15 | val = val + "px"; 16 | } 17 | element.style[key] = val; 18 | } 19 | return element; 20 | } 21 | 22 | function div(className) { 23 | var div = document.createElement("div"); 24 | div.className = className; 25 | return div; 26 | } 27 | 28 | var elMatches = 29 | typeof Element !== "undefined" && 30 | (Element.prototype.matches || 31 | Element.prototype.webkitMatchesSelector || 32 | Element.prototype.mozMatchesSelector || 33 | Element.prototype.msMatchesSelector); 34 | 35 | function matches(element, query) { 36 | if (!elMatches) { 37 | throw new Error("No element matching method supported"); 38 | } 39 | 40 | return elMatches.call(element, query); 41 | } 42 | 43 | function remove(element) { 44 | if (element.remove) { 45 | element.remove(); 46 | } else { 47 | if (element.parentNode) { 48 | element.parentNode.removeChild(element); 49 | } 50 | } 51 | } 52 | 53 | function queryChildren(element, selector) { 54 | return Array.prototype.filter.call(element.children, function (child) { 55 | return matches(child, selector); 56 | }); 57 | } 58 | 59 | var cls = { 60 | main: "ps", 61 | rtl: "ps__rtl", 62 | element: { 63 | thumb: function (x) { 64 | return "ps__thumb-" + x; 65 | }, 66 | rail: function (x) { 67 | return "ps__rail-" + x; 68 | }, 69 | consuming: "ps__child--consume", 70 | }, 71 | state: { 72 | focus: "ps--focus", 73 | clicking: "ps--clicking", 74 | active: function (x) { 75 | return "ps--active-" + x; 76 | }, 77 | scrolling: function (x) { 78 | return "ps--scrolling-" + x; 79 | }, 80 | }, 81 | }; 82 | 83 | /* 84 | * Helper methods 85 | */ 86 | var scrollingClassTimeout = { x: null, y: null }; 87 | 88 | function addScrollingClass(i, x) { 89 | var classList = i.element.classList; 90 | var className = cls.state.scrolling(x); 91 | 92 | if (classList.contains(className)) { 93 | clearTimeout(scrollingClassTimeout[x]); 94 | } else { 95 | classList.add(className); 96 | } 97 | } 98 | 99 | function removeScrollingClass(i, x) { 100 | scrollingClassTimeout[x] = setTimeout(function () { 101 | return i.isAlive && i.element.classList.remove(cls.state.scrolling(x)); 102 | }, i.settings.scrollingThreshold); 103 | } 104 | 105 | function setScrollingClassInstantly(i, x) { 106 | addScrollingClass(i, x); 107 | removeScrollingClass(i, x); 108 | } 109 | 110 | var EventElement = function EventElement(element) { 111 | this.element = element; 112 | this.handlers = {}; 113 | }; 114 | 115 | var prototypeAccessors = { isEmpty: { configurable: true } }; 116 | 117 | EventElement.prototype.bind = function bind(eventName, handler) { 118 | if (typeof this.handlers[eventName] === "undefined") { 119 | this.handlers[eventName] = []; 120 | } 121 | this.handlers[eventName].push(handler); 122 | 123 | var evts = ["touchstart", "wheel", "touchmove"]; 124 | if (evts.indexOf(eventName) !== -1) { 125 | this.element.addEventListener(eventName, handler, { passive: false }); 126 | } else { 127 | this.element.addEventListener(eventName, handler, false); 128 | } 129 | }; 130 | 131 | EventElement.prototype.unbind = function unbind(eventName, target) { 132 | var this$1 = this; 133 | 134 | this.handlers[eventName] = this.handlers[eventName].filter(function ( 135 | handler 136 | ) { 137 | if (target && handler !== target) { 138 | return true; 139 | } 140 | this$1.element.removeEventListener(eventName, handler, false); 141 | return false; 142 | }); 143 | }; 144 | 145 | EventElement.prototype.unbindAll = function unbindAll() { 146 | for (var name in this.handlers) { 147 | this.unbind(name); 148 | } 149 | }; 150 | 151 | prototypeAccessors.isEmpty.get = function () { 152 | var this$1 = this; 153 | 154 | return Object.keys(this.handlers).every(function (key) { 155 | return this$1.handlers[key].length === 0; 156 | }); 157 | }; 158 | 159 | Object.defineProperties(EventElement.prototype, prototypeAccessors); 160 | 161 | var EventManager = function EventManager() { 162 | this.eventElements = []; 163 | }; 164 | 165 | EventManager.prototype.eventElement = function eventElement(element) { 166 | var ee = this.eventElements.filter(function (ee) { 167 | return ee.element === element; 168 | })[0]; 169 | if (!ee) { 170 | ee = new EventElement(element); 171 | this.eventElements.push(ee); 172 | } 173 | return ee; 174 | }; 175 | 176 | EventManager.prototype.bind = function bind(element, eventName, handler) { 177 | this.eventElement(element).bind(eventName, handler); 178 | }; 179 | 180 | EventManager.prototype.unbind = function unbind(element, eventName, handler) { 181 | var ee = this.eventElement(element); 182 | ee.unbind(eventName, handler); 183 | 184 | if (ee.isEmpty) { 185 | // remove 186 | this.eventElements.splice(this.eventElements.indexOf(ee), 1); 187 | } 188 | }; 189 | 190 | EventManager.prototype.unbindAll = function unbindAll() { 191 | this.eventElements.forEach(function (e) { 192 | return e.unbindAll(); 193 | }); 194 | this.eventElements = []; 195 | }; 196 | 197 | EventManager.prototype.once = function once(element, eventName, handler) { 198 | var ee = this.eventElement(element); 199 | var onceHandler = function (evt) { 200 | ee.unbind(eventName, onceHandler); 201 | handler(evt); 202 | }; 203 | ee.bind(eventName, onceHandler); 204 | }; 205 | 206 | function createEvent(name) { 207 | if (typeof window.CustomEvent === "function") { 208 | return new CustomEvent(name); 209 | } else { 210 | var evt = document.createEvent("CustomEvent"); 211 | evt.initCustomEvent(name, false, false, undefined); 212 | return evt; 213 | } 214 | } 215 | 216 | function processScrollDiff( 217 | i, 218 | axis, 219 | diff, 220 | useScrollingClass, 221 | forceFireReachEvent, 222 | disableOnYReachWhenNoScroll 223 | ) { 224 | if (useScrollingClass === void 0) useScrollingClass = true; 225 | if (forceFireReachEvent === void 0) forceFireReachEvent = false; 226 | 227 | var fields; 228 | if (axis === "top") { 229 | fields = [ 230 | "contentHeight", 231 | "containerHeight", 232 | "scrollTop", 233 | "y", 234 | "up", 235 | "down", 236 | ]; 237 | } else if (axis === "left") { 238 | fields = [ 239 | "contentWidth", 240 | "containerWidth", 241 | "scrollLeft", 242 | "x", 243 | "left", 244 | "right", 245 | ]; 246 | } else { 247 | throw new Error("A proper axis should be provided"); 248 | } 249 | 250 | processScrollDiff$1( 251 | i, 252 | diff, 253 | fields, 254 | useScrollingClass, 255 | forceFireReachEvent, 256 | disableOnYReachWhenNoScroll 257 | ); 258 | } 259 | 260 | function processScrollDiff$1( 261 | i, 262 | diff, 263 | ref, 264 | useScrollingClass, 265 | forceFireReachEvent, 266 | disableOnYReachWhenNoScroll 267 | ) { 268 | var contentHeight = ref[0]; 269 | var containerHeight = ref[1]; 270 | var scrollTop = ref[2]; 271 | var y = ref[3]; 272 | var up = ref[4]; 273 | var down = ref[5]; 274 | if (useScrollingClass === void 0) useScrollingClass = true; 275 | if (forceFireReachEvent === void 0) forceFireReachEvent = false; 276 | 277 | var element = i.element; 278 | 279 | // reset reach 280 | i.reach[y] = null; 281 | 282 | const eventFlag = 283 | disableOnYReachWhenNoScroll === true 284 | ? i[contentHeight] !== i[containerHeight] 285 | : true; 286 | 287 | // 1 for subpixel rounding 288 | if (eventFlag && element[scrollTop] < 1) { 289 | i.reach[y] = "start"; 290 | } 291 | 292 | // 1 for subpixel rounding 293 | if ( 294 | eventFlag && 295 | element[scrollTop] > i[contentHeight] - i[containerHeight] - 1 296 | ) { 297 | i.reach[y] = "end"; 298 | } 299 | 300 | if (diff) { 301 | element.dispatchEvent(createEvent("ps-scroll-" + y)); 302 | 303 | if (diff < 0) { 304 | element.dispatchEvent(createEvent("ps-scroll-" + up)); 305 | } else if (diff > 0) { 306 | element.dispatchEvent(createEvent("ps-scroll-" + down)); 307 | } 308 | 309 | if (useScrollingClass) { 310 | setScrollingClassInstantly(i, y); 311 | } 312 | } 313 | 314 | if (i.reach[y] && (diff || forceFireReachEvent)) { 315 | element.dispatchEvent(createEvent("ps-" + y + "-reach-" + i.reach[y])); 316 | } 317 | } 318 | 319 | function toInt(x) { 320 | return parseInt(x, 10) || 0; 321 | } 322 | 323 | function isEditable(el) { 324 | return ( 325 | matches(el, "input,[contenteditable]") || 326 | matches(el, "select,[contenteditable]") || 327 | matches(el, "textarea,[contenteditable]") || 328 | matches(el, "button,[contenteditable]") 329 | ); 330 | } 331 | 332 | function outerWidth(element) { 333 | var styles = get(element); 334 | return ( 335 | toInt(styles.width) + 336 | toInt(styles.paddingLeft) + 337 | toInt(styles.paddingRight) + 338 | toInt(styles.borderLeftWidth) + 339 | toInt(styles.borderRightWidth) 340 | ); 341 | } 342 | 343 | var env = { 344 | isWebKit: 345 | typeof document !== "undefined" && 346 | "WebkitAppearance" in document.documentElement.style, 347 | supportsTouch: 348 | typeof window !== "undefined" && 349 | ("ontouchstart" in window || 350 | ("maxTouchPoints" in window.navigator && 351 | window.navigator.maxTouchPoints > 0) || 352 | (window.DocumentTouch && document instanceof window.DocumentTouch)), 353 | supportsIePointer: 354 | typeof navigator !== "undefined" && navigator.msMaxTouchPoints, 355 | isChrome: 356 | typeof navigator !== "undefined" && 357 | /Chrome/i.test(navigator && navigator.userAgent), 358 | }; 359 | 360 | function updateGeometry(i) { 361 | var element = i.element; 362 | var roundedScrollTop = Math.floor(element.scrollTop); 363 | var rect = element.getBoundingClientRect(); 364 | 365 | i.containerWidth = Math.round(rect.width); 366 | i.containerHeight = Math.round(rect.height); 367 | i.contentWidth = element.scrollWidth; 368 | i.contentHeight = element.scrollHeight; 369 | 370 | if (!element.contains(i.scrollbarXRail)) { 371 | // clean up and append 372 | queryChildren(element, cls.element.rail("x")).forEach(function (el) { 373 | return remove(el); 374 | }); 375 | element.appendChild(i.scrollbarXRail); 376 | } 377 | if (!element.contains(i.scrollbarYRail)) { 378 | // clean up and append 379 | queryChildren(element, cls.element.rail("y")).forEach(function (el) { 380 | return remove(el); 381 | }); 382 | element.appendChild(i.scrollbarYRail); 383 | } 384 | 385 | if ( 386 | !i.settings.suppressScrollX && 387 | i.containerWidth + i.settings.scrollXMarginOffset < i.contentWidth 388 | ) { 389 | i.scrollbarXActive = true; 390 | i.railXWidth = i.containerWidth - i.railXMarginWidth; 391 | i.railXRatio = i.containerWidth / i.railXWidth; 392 | i.scrollbarXWidth = getThumbSize( 393 | i, 394 | toInt((i.railXWidth * i.containerWidth) / i.contentWidth) 395 | ); 396 | i.scrollbarXLeft = toInt( 397 | ((i.negativeScrollAdjustment + element.scrollLeft) * 398 | (i.railXWidth - i.scrollbarXWidth)) / 399 | (i.contentWidth - i.containerWidth) 400 | ); 401 | } else { 402 | i.scrollbarXActive = false; 403 | } 404 | 405 | if ( 406 | !i.settings.suppressScrollY && 407 | i.containerHeight + i.settings.scrollYMarginOffset < i.contentHeight 408 | ) { 409 | i.scrollbarYActive = true; 410 | i.railYHeight = i.containerHeight - i.railYMarginHeight; 411 | i.railYRatio = i.containerHeight / i.railYHeight; 412 | i.scrollbarYHeight = getThumbSize( 413 | i, 414 | toInt((i.railYHeight * i.containerHeight) / i.contentHeight) 415 | ); 416 | i.scrollbarYTop = toInt( 417 | (roundedScrollTop * (i.railYHeight - i.scrollbarYHeight)) / 418 | (i.contentHeight - i.containerHeight) 419 | ); 420 | } else { 421 | i.scrollbarYActive = false; 422 | } 423 | 424 | if (i.scrollbarXLeft >= i.railXWidth - i.scrollbarXWidth) { 425 | i.scrollbarXLeft = i.railXWidth - i.scrollbarXWidth; 426 | } 427 | if (i.scrollbarYTop >= i.railYHeight - i.scrollbarYHeight) { 428 | i.scrollbarYTop = i.railYHeight - i.scrollbarYHeight; 429 | } 430 | 431 | updateCss(element, i); 432 | 433 | if (i.scrollbarXActive) { 434 | element.classList.add(cls.state.active("x")); 435 | } else { 436 | element.classList.remove(cls.state.active("x")); 437 | i.scrollbarXWidth = 0; 438 | i.scrollbarXLeft = 0; 439 | element.scrollLeft = i.isRtl === true ? i.contentWidth : 0; 440 | } 441 | if (i.scrollbarYActive) { 442 | element.classList.add(cls.state.active("y")); 443 | } else { 444 | element.classList.remove(cls.state.active("y")); 445 | i.scrollbarYHeight = 0; 446 | i.scrollbarYTop = 0; 447 | element.scrollTop = 0; 448 | } 449 | } 450 | 451 | function getThumbSize(i, thumbSize) { 452 | if (i.settings.minScrollbarLength) { 453 | thumbSize = Math.max(thumbSize, i.settings.minScrollbarLength); 454 | } 455 | if (i.settings.maxScrollbarLength) { 456 | thumbSize = Math.min(thumbSize, i.settings.maxScrollbarLength); 457 | } 458 | return thumbSize; 459 | } 460 | 461 | function updateCss(element, i) { 462 | var xRailOffset = { width: i.railXWidth }; 463 | var roundedScrollTop = Math.floor(element.scrollTop); 464 | 465 | if (i.isRtl) { 466 | xRailOffset.left = 467 | i.negativeScrollAdjustment + 468 | element.scrollLeft + 469 | i.containerWidth - 470 | i.contentWidth; 471 | } else { 472 | xRailOffset.left = element.scrollLeft; 473 | } 474 | if (i.isScrollbarXUsingBottom) { 475 | xRailOffset.bottom = i.scrollbarXBottom - roundedScrollTop; 476 | } else { 477 | xRailOffset.top = i.scrollbarXTop + roundedScrollTop; 478 | } 479 | set(i.scrollbarXRail, xRailOffset); 480 | 481 | var yRailOffset = { top: roundedScrollTop, height: i.railYHeight }; 482 | if (i.isScrollbarYUsingRight) { 483 | if (i.isRtl) { 484 | yRailOffset.right = 485 | i.contentWidth - 486 | (i.negativeScrollAdjustment + element.scrollLeft) - 487 | i.scrollbarYRight - 488 | i.scrollbarYOuterWidth - 489 | 9; 490 | } else { 491 | yRailOffset.right = i.scrollbarYRight - element.scrollLeft; 492 | } 493 | } else { 494 | if (i.isRtl) { 495 | yRailOffset.left = 496 | i.negativeScrollAdjustment + 497 | element.scrollLeft + 498 | i.containerWidth * 2 - 499 | i.contentWidth - 500 | i.scrollbarYLeft - 501 | i.scrollbarYOuterWidth; 502 | } else { 503 | yRailOffset.left = i.scrollbarYLeft + element.scrollLeft; 504 | } 505 | } 506 | set(i.scrollbarYRail, yRailOffset); 507 | 508 | set(i.scrollbarX, { 509 | left: i.scrollbarXLeft, 510 | width: i.scrollbarXWidth - i.railBorderXWidth, 511 | }); 512 | set(i.scrollbarY, { 513 | top: i.scrollbarYTop, 514 | height: i.scrollbarYHeight - i.railBorderYWidth, 515 | }); 516 | } 517 | 518 | function clickRail(i) { 519 | var element = i.element; 520 | 521 | i.event.bind(i.scrollbarY, "mousedown", function (e) { 522 | return e.stopPropagation(); 523 | }); 524 | i.event.bind(i.scrollbarYRail, "mousedown", function (e) { 525 | var positionTop = 526 | e.pageY - 527 | window.pageYOffset - 528 | i.scrollbarYRail.getBoundingClientRect().top; 529 | var direction = positionTop > i.scrollbarYTop ? 1 : -1; 530 | 531 | i.element.scrollTop += direction * i.containerHeight; 532 | updateGeometry(i); 533 | 534 | e.stopPropagation(); 535 | }); 536 | 537 | i.event.bind(i.scrollbarX, "mousedown", function (e) { 538 | return e.stopPropagation(); 539 | }); 540 | i.event.bind(i.scrollbarXRail, "mousedown", function (e) { 541 | var positionLeft = 542 | e.pageX - 543 | window.pageXOffset - 544 | i.scrollbarXRail.getBoundingClientRect().left; 545 | var direction = positionLeft > i.scrollbarXLeft ? 1 : -1; 546 | 547 | i.element.scrollLeft += direction * i.containerWidth; 548 | updateGeometry(i); 549 | 550 | e.stopPropagation(); 551 | }); 552 | } 553 | 554 | function dragThumb(i) { 555 | bindMouseScrollHandler(i, [ 556 | "containerWidth", 557 | "contentWidth", 558 | "pageX", 559 | "railXWidth", 560 | "scrollbarX", 561 | "scrollbarXWidth", 562 | "scrollLeft", 563 | "x", 564 | "scrollbarXRail", 565 | ]); 566 | bindMouseScrollHandler(i, [ 567 | "containerHeight", 568 | "contentHeight", 569 | "pageY", 570 | "railYHeight", 571 | "scrollbarY", 572 | "scrollbarYHeight", 573 | "scrollTop", 574 | "y", 575 | "scrollbarYRail", 576 | ]); 577 | } 578 | 579 | function bindMouseScrollHandler(i, ref) { 580 | var containerHeight = ref[0]; 581 | var contentHeight = ref[1]; 582 | var pageY = ref[2]; 583 | var railYHeight = ref[3]; 584 | var scrollbarY = ref[4]; 585 | var scrollbarYHeight = ref[5]; 586 | var scrollTop = ref[6]; 587 | var y = ref[7]; 588 | var scrollbarYRail = ref[8]; 589 | 590 | var element = i.element; 591 | 592 | var startingScrollTop = null; 593 | var startingMousePageY = null; 594 | var scrollBy = null; 595 | 596 | function mouseMoveHandler(e) { 597 | if (e.touches && e.touches[0]) { 598 | e[pageY] = e.touches[0].pageY; 599 | } 600 | element[scrollTop] = 601 | startingScrollTop + scrollBy * (e[pageY] - startingMousePageY); 602 | addScrollingClass(i, y); 603 | updateGeometry(i); 604 | 605 | e.stopPropagation(); 606 | e.preventDefault(); 607 | } 608 | 609 | function mouseUpHandler() { 610 | removeScrollingClass(i, y); 611 | i[scrollbarYRail].classList.remove(cls.state.clicking); 612 | i.event.unbind(i.ownerDocument, "mousemove", mouseMoveHandler); 613 | } 614 | 615 | function bindMoves(e, touchMode) { 616 | startingScrollTop = element[scrollTop]; 617 | if (touchMode && e.touches) { 618 | e[pageY] = e.touches[0].pageY; 619 | } 620 | startingMousePageY = e[pageY]; 621 | scrollBy = 622 | (i[contentHeight] - i[containerHeight]) / 623 | (i[railYHeight] - i[scrollbarYHeight]); 624 | if (!touchMode) { 625 | i.event.bind(i.ownerDocument, "mousemove", mouseMoveHandler); 626 | i.event.once(i.ownerDocument, "mouseup", mouseUpHandler); 627 | e.preventDefault(); 628 | } else { 629 | i.event.bind(i.ownerDocument, "touchmove", mouseMoveHandler); 630 | } 631 | 632 | i[scrollbarYRail].classList.add(cls.state.clicking); 633 | 634 | e.stopPropagation(); 635 | } 636 | 637 | i.event.bind(i[scrollbarY], "mousedown", function (e) { 638 | bindMoves(e); 639 | }); 640 | i.event.bind(i[scrollbarY], "touchstart", function (e) { 641 | bindMoves(e, true); 642 | }); 643 | } 644 | 645 | function keyboard(i) { 646 | var element = i.element; 647 | 648 | var elementHovered = function () { 649 | return matches(element, ":hover"); 650 | }; 651 | var scrollbarFocused = function () { 652 | return matches(i.scrollbarX, ":focus") || matches(i.scrollbarY, ":focus"); 653 | }; 654 | 655 | function shouldPreventDefault(deltaX, deltaY) { 656 | var scrollTop = Math.floor(element.scrollTop); 657 | if (deltaX === 0) { 658 | if (!i.scrollbarYActive) { 659 | return false; 660 | } 661 | if ( 662 | (scrollTop === 0 && deltaY > 0) || 663 | (scrollTop >= i.contentHeight - i.containerHeight && deltaY < 0) 664 | ) { 665 | return !i.settings.wheelPropagation; 666 | } 667 | } 668 | 669 | var scrollLeft = element.scrollLeft; 670 | if (deltaY === 0) { 671 | if (!i.scrollbarXActive) { 672 | return false; 673 | } 674 | if ( 675 | (scrollLeft === 0 && deltaX < 0) || 676 | (scrollLeft >= i.contentWidth - i.containerWidth && deltaX > 0) 677 | ) { 678 | return !i.settings.wheelPropagation; 679 | } 680 | } 681 | return true; 682 | } 683 | 684 | i.event.bind(i.ownerDocument, "keydown", function (e) { 685 | if ( 686 | (e.isDefaultPrevented && e.isDefaultPrevented()) || 687 | e.defaultPrevented 688 | ) { 689 | return; 690 | } 691 | 692 | if (!elementHovered() && !scrollbarFocused()) { 693 | return; 694 | } 695 | 696 | var activeElement = document.activeElement 697 | ? document.activeElement 698 | : i.ownerDocument.activeElement; 699 | if (activeElement) { 700 | if (activeElement.tagName === "IFRAME") { 701 | activeElement = activeElement.contentDocument.activeElement; 702 | } else { 703 | // go deeper if element is a webcomponent 704 | while (activeElement.shadowRoot) { 705 | activeElement = activeElement.shadowRoot.activeElement; 706 | } 707 | } 708 | if (isEditable(activeElement)) { 709 | return; 710 | } 711 | } 712 | 713 | var deltaX = 0; 714 | var deltaY = 0; 715 | 716 | switch (e.which) { 717 | case 37: // left 718 | if (e.metaKey) { 719 | deltaX = -i.contentWidth; 720 | } else if (e.altKey) { 721 | deltaX = -i.containerWidth; 722 | } else { 723 | deltaX = -30; 724 | } 725 | break; 726 | case 38: // up 727 | if (e.metaKey) { 728 | deltaY = i.contentHeight; 729 | } else if (e.altKey) { 730 | deltaY = i.containerHeight; 731 | } else { 732 | deltaY = 30; 733 | } 734 | break; 735 | case 39: // right 736 | if (e.metaKey) { 737 | deltaX = i.contentWidth; 738 | } else if (e.altKey) { 739 | deltaX = i.containerWidth; 740 | } else { 741 | deltaX = 30; 742 | } 743 | break; 744 | case 40: // down 745 | if (e.metaKey) { 746 | deltaY = -i.contentHeight; 747 | } else if (e.altKey) { 748 | deltaY = -i.containerHeight; 749 | } else { 750 | deltaY = -30; 751 | } 752 | break; 753 | case 32: // space bar 754 | if (e.shiftKey) { 755 | deltaY = i.containerHeight; 756 | } else { 757 | deltaY = -i.containerHeight; 758 | } 759 | break; 760 | case 33: // page up 761 | deltaY = i.containerHeight; 762 | break; 763 | case 34: // page down 764 | deltaY = -i.containerHeight; 765 | break; 766 | case 36: // home 767 | deltaY = i.contentHeight; 768 | break; 769 | case 35: // end 770 | deltaY = -i.contentHeight; 771 | break; 772 | default: 773 | return; 774 | } 775 | 776 | if (i.settings.suppressScrollX && deltaX !== 0) { 777 | return; 778 | } 779 | if (i.settings.suppressScrollY && deltaY !== 0) { 780 | return; 781 | } 782 | 783 | element.scrollTop -= deltaY; 784 | 785 | element.scrollLeft += deltaX; 786 | updateGeometry(i); 787 | 788 | if (shouldPreventDefault(deltaX, deltaY)) { 789 | e.preventDefault(); 790 | } 791 | }); 792 | } 793 | 794 | function wheel(i) { 795 | var element = i.element; 796 | 797 | function shouldPreventDefault(deltaX, deltaY) { 798 | var roundedScrollTop = Math.floor(element.scrollTop); 799 | var isTop = element.scrollTop === 0; 800 | var isBottom = 801 | roundedScrollTop + element.offsetHeight === element.scrollHeight; 802 | var isLeft = element.scrollLeft === 0; 803 | var isRight = 804 | element.scrollLeft + element.offsetWidth === element.scrollWidth; 805 | 806 | var hitsBound; 807 | 808 | // pick axis with primary direction 809 | if (Math.abs(deltaY) > Math.abs(deltaX)) { 810 | hitsBound = isTop || isBottom; 811 | } else { 812 | hitsBound = isLeft || isRight; 813 | } 814 | 815 | return hitsBound ? !i.settings.wheelPropagation : true; 816 | } 817 | 818 | function getDeltaFromEvent(e) { 819 | var deltaX = e.deltaX; 820 | var deltaY = -1 * e.deltaY; 821 | 822 | if (typeof deltaX === "undefined" || typeof deltaY === "undefined") { 823 | // OS X Safari 824 | deltaX = (-1 * e.wheelDeltaX) / 6; 825 | deltaY = e.wheelDeltaY / 6; 826 | } 827 | 828 | if (e.deltaMode && e.deltaMode === 1) { 829 | // Firefox in deltaMode 1: Line scrolling 830 | deltaX *= 10; 831 | deltaY *= 10; 832 | } 833 | 834 | if (deltaX !== deltaX && deltaY !== deltaY /* NaN checks */) { 835 | // IE in some mouse drivers 836 | deltaX = 0; 837 | deltaY = e.wheelDelta; 838 | } 839 | 840 | if (e.shiftKey) { 841 | // reverse axis with shift key 842 | return [-deltaY, -deltaX]; 843 | } 844 | return [deltaX, deltaY]; 845 | } 846 | 847 | function shouldBeConsumedByChild(target, deltaX, deltaY) { 848 | // FIXME: this is a workaround for 95 | 102 |
103 | ); 104 | } 105 | 106 | const Search = forwardRef(SearchInner); 107 | 108 | Search.displayName = "Search"; 109 | 110 | Search.propTypes = { 111 | /** Placeholder. */ 112 | placeholder: PropTypes.string, 113 | 114 | /** Current value of the search input. Creates a controlled component */ 115 | value: PropTypes.string, 116 | 117 | /** OnInput handler. */ 118 | onChange: PropTypes.func, 119 | 120 | /** OnClearClick handler. */ 121 | onClearClick: PropTypes.func, 122 | 123 | /** Additional classes. */ 124 | className: PropTypes.string, 125 | 126 | /** Disabled */ 127 | disabled: PropTypes.bool, 128 | }; 129 | 130 | SearchInner.propTypes = Search.propTypes; 131 | 132 | export { Search }; 133 | export default Search; 134 | -------------------------------------------------------------------------------- /src/components/Search/index.js: -------------------------------------------------------------------------------- 1 | import Search from "./Search"; 2 | export * from "./Search"; 3 | export default Search; 4 | -------------------------------------------------------------------------------- /src/components/Sidebar/Sidebar.d.ts: -------------------------------------------------------------------------------- 1 | import type {ReactElement} from "react"; 2 | import type {ChatComponentPropsChildren} from "../../types"; 3 | 4 | export interface SidebarProps { 5 | position: "left" | "right"; 6 | scrollable?: boolean; 7 | loading?: boolean; 8 | } 9 | 10 | export declare const Sidebar: (props:ChatComponentPropsChildren) => ReactElement; 11 | 12 | export default Sidebar; -------------------------------------------------------------------------------- /src/components/Sidebar/Sidebar.jsx: -------------------------------------------------------------------------------- 1 | import React, { useMemo } from "react"; 2 | import PropTypes from "prop-types"; 3 | import { prefix } from "../settings"; 4 | import PerfectScrollbar from "../Scroll"; 5 | import classNames from "classnames"; 6 | import Overlay from "../Overlay"; 7 | import Loader from "../Loader"; 8 | 9 | export const Sidebar = ({ 10 | children = undefined, 11 | position = undefined, 12 | scrollable = true, 13 | loading = false, 14 | className = "", 15 | ...props 16 | }) => { 17 | const cName = `${prefix}-sidebar`; 18 | 19 | const sideClass = (() => { 20 | if (position === "left") { 21 | return `${cName}--left`; 22 | } else if (position === "right") { 23 | return `${cName}--right`; 24 | } else { 25 | return ``; 26 | } 27 | })(); 28 | 29 | /* eslint-disable react/display-name*/ 30 | const Tag = useMemo( 31 | () => ({ children, ...rest }) => { 32 | // PerfectScrollbar for now can't be disabled, so render div instead of disabling it 33 | // https://github.com/goldenyz/react-perfect-scrollbar/issues/107 34 | if (scrollable === false || (scrollable === true && loading === true)) { 35 | return ( 36 |
37 | {loading && ( 38 | 39 | 40 | 41 | )} 42 | {children} 43 |
44 | ); 45 | } else { 46 | return {children}; 47 | } 48 | }, 49 | [scrollable, loading] 50 | ); 51 | 52 | return ( 53 | 54 | {children} 55 | 56 | ); 57 | }; 58 | 59 | Sidebar.propTypes = { 60 | /** Primary content. */ 61 | children: PropTypes.node, 62 | 63 | /** Sidebar can be placed on two positions */ 64 | position: PropTypes.oneOf(["left", "right"]), 65 | 66 | /** Sidebar can be scrollable */ 67 | scrollable: PropTypes.bool, 68 | 69 | /** Loading flag. */ 70 | loading: PropTypes.bool, 71 | 72 | /** Additional classes. */ 73 | className: PropTypes.string, 74 | }; 75 | 76 | export default Sidebar; 77 | -------------------------------------------------------------------------------- /src/components/Sidebar/index.js: -------------------------------------------------------------------------------- 1 | import Sidebar from "./Sidebar"; 2 | export * from "./Sidebar"; 3 | export default Sidebar; 4 | -------------------------------------------------------------------------------- /src/components/Status/Status.d.ts: -------------------------------------------------------------------------------- 1 | import type {ReactElement} from "react"; 2 | import type {ChatComponentPropsChildren, Size, UserStatus} from "../../types"; 3 | 4 | export interface StatusProps { 5 | status: UserStatus; 6 | size: Size; 7 | name?:string; 8 | selected?:boolean; 9 | } 10 | 11 | export declare const Status: (props:ChatComponentPropsChildren) => ReactElement; 12 | 13 | export default Status; -------------------------------------------------------------------------------- /src/components/Status/Status.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import classNames from "classnames"; 4 | import { StatusEnum, SizeEnum } from "../enums"; 5 | import { prefix } from "../settings"; 6 | 7 | export const Status = ({ 8 | status, 9 | size = "md", 10 | className, 11 | name, 12 | selected, 13 | children, 14 | ...rest 15 | }) => { 16 | const cName = `${prefix}-status`; 17 | const bullet =
; 18 | const named = name || children; 19 | 20 | return ( 21 |
33 | {bullet} 34 | {named && ( 35 |
{name ? name : children}
36 | )} 37 |
38 | ); 39 | }; 40 | 41 | Status.propTypes = { 42 | /** Primary content */ 43 | children: PropTypes.node, 44 | 45 | /** Status. */ 46 | status: PropTypes.oneOf(StatusEnum).isRequired, 47 | 48 | /** Size. */ 49 | size: PropTypes.oneOf(SizeEnum), 50 | 51 | /** Name */ 52 | name: PropTypes.node, 53 | 54 | /** Selected */ 55 | selected: PropTypes.bool, 56 | 57 | /** Additional classes. */ 58 | className: PropTypes.string, 59 | }; 60 | 61 | export default Status; 62 | -------------------------------------------------------------------------------- /src/components/Status/index.js: -------------------------------------------------------------------------------- 1 | import Status from "./Status"; 2 | export * from "./Status"; 3 | export default Status; 4 | -------------------------------------------------------------------------------- /src/components/StatusList/StatusList.d.ts: -------------------------------------------------------------------------------- 1 | import type {ReactElement} from "react"; 2 | import type {ChatComponentPropsChildren, Size, UserStatus} from "../../types"; 3 | 4 | export interface StatusListProps { 5 | selected?: UserStatus; 6 | size?: Size; 7 | itemsTabIndex?:number; 8 | onChange?: (status:UserStatus) => void; 9 | } 10 | 11 | export declare const StatusList: (props:ChatComponentPropsChildren) => ReactElement; 12 | 13 | export default StatusList; -------------------------------------------------------------------------------- /src/components/StatusList/StatusList.jsx: -------------------------------------------------------------------------------- 1 | import React, { useImperativeHandle, forwardRef, useRef } from "react"; 2 | import PropTypes from "prop-types"; 3 | import classNames from "classnames"; 4 | import { noop, allowedChildren } from "../utils"; 5 | import { SizeEnum, StatusEnum } from "../enums"; 6 | import Status from "../Status"; 7 | import { prefix } from "../settings"; 8 | 9 | function StatusListInner( 10 | { className, children, size, selected, onChange = noop, itemsTabIndex, ...rest }, 11 | ref 12 | ) { 13 | const cName = `${prefix}-status-list`; 14 | 15 | const listRef = useRef(); 16 | 17 | // Return object with public Api 18 | useImperativeHandle(ref, () => ({ 19 | focus: (idx) => { 20 | const items = Array.from(listRef.current.querySelectorAll("li")); 21 | // For sure filter only direct children because querySelectorAll cant get only direct children 22 | const directChild = items.filter( 23 | (item) => item.parentNode === listRef.current 24 | ); 25 | if (typeof directChild[idx] !== "undefined") { 26 | directChild[idx].focus(); 27 | } 28 | }, 29 | })); 30 | 31 | let tabIndex = itemsTabIndex; 32 | return ( 33 |
    38 | {React.Children.map(children, (item) => { 39 | // If active argument is set, clear active flag for all elements except desired 40 | const newProps = {}; 41 | if (selected) { 42 | newProps.selected = item.props.status === selected; 43 | } 44 | 45 | if (onChange) { 46 | newProps.onClick = (evt) => { 47 | onChange(item.props.status); 48 | if (item.onClick) { 49 | item.onClick(evt); 50 | } 51 | }; 52 | } 53 | 54 | const onKeyPress = (evt) => { 55 | if (onChange) { 56 | if ( 57 | evt.key === "Enter" && 58 | evt.shiftKey === false && 59 | evt.altKey === false 60 | ) { 61 | onChange(item.props.status); 62 | } 63 | } 64 | }; 65 | 66 | const tIndex = (() => { 67 | if (typeof tabIndex === "number") { 68 | if (tabIndex > 0) { 69 | return tabIndex++; 70 | } else { 71 | return tabIndex; 72 | } 73 | } else { 74 | return undefined; 75 | } 76 | })(); 77 | 78 | return ( 79 |
  • 80 | {React.cloneElement(item, newProps)} 81 |
  • 82 | ); 83 | })} 84 |
85 | ); 86 | } 87 | 88 | const StatusList = forwardRef(StatusListInner); 89 | StatusList.displayName = "StatusList"; 90 | 91 | StatusList.propTypes = { 92 | /** 93 | * Primary content. 94 | * Allowed components: 95 | * 96 | * * <Status /> 97 | */ 98 | children: allowedChildren([Status]), 99 | 100 | /** Selected element */ 101 | selected: PropTypes.oneOf(StatusEnum), 102 | 103 | /** Size */ 104 | size: PropTypes.oneOf(SizeEnum), 105 | 106 | /** tabindex value for items. Any positive integer will be treated as start index for counting. Zero and negative values will be applied to all items */ 107 | itemsTabIndex: PropTypes.number, 108 | 109 | /** Additional classes. */ 110 | className: PropTypes.string, 111 | 112 | /** onChange handler */ 113 | onChange: PropTypes.func, 114 | }; 115 | 116 | StatusListInner.propTypes = StatusList.propTypes; 117 | 118 | export { StatusList }; 119 | export default StatusList; 120 | -------------------------------------------------------------------------------- /src/components/StatusList/index.js: -------------------------------------------------------------------------------- 1 | import StatusList from "./StatusList"; 2 | export * from "./StatusList"; 3 | export default StatusList; 4 | -------------------------------------------------------------------------------- /src/components/TypingIndicator/TypingIndicator.d.ts: -------------------------------------------------------------------------------- 1 | import type {ReactElement, ReactNode} from "react"; 2 | import type {ChatComponentProps} from "../../types"; 3 | 4 | export interface TypingIndicatorProps { 5 | content?:ReactNode; 6 | } 7 | 8 | export declare const TypingIndicator: (props:ChatComponentProps) => ReactElement; 9 | 10 | export default TypingIndicator; -------------------------------------------------------------------------------- /src/components/TypingIndicator/TypingIndicator.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import classNames from "classnames"; 4 | import { prefix } from "../settings"; 5 | 6 | export const TypingIndicator = ({ content = "", className, ...rest }) => { 7 | const cName = `${prefix}-typing-indicator`; 8 | 9 | return ( 10 |
11 |
12 |
13 |
14 |
15 |
16 |
{content}
17 |
18 | ); 19 | }; 20 | 21 | TypingIndicator.propTypes = { 22 | /** Indicator content. */ 23 | content: PropTypes.node, 24 | 25 | /** Additional classes. */ 26 | className: PropTypes.string, 27 | }; 28 | 29 | export default TypingIndicator; 30 | -------------------------------------------------------------------------------- /src/components/TypingIndicator/index.js: -------------------------------------------------------------------------------- 1 | import TypingIndicator from "./TypingIndicator"; 2 | export * from "./TypingIndicator"; 3 | export default TypingIndicator; 4 | -------------------------------------------------------------------------------- /src/components/enums.js: -------------------------------------------------------------------------------- 1 | export const StatusEnum = [ 2 | "available", 3 | "unavailable", 4 | "away", 5 | "dnd", 6 | "invisible", 7 | "eager", 8 | ]; 9 | 10 | export const SizeEnum = ["xs", "sm", "md", "lg", "fluid"]; 11 | 12 | export const MessageTypeEnum = ["html", "text", "image", "custom"]; 13 | 14 | export default { 15 | SizeEnum, 16 | StatusEnum, 17 | MessageTypeEnum, 18 | }; 19 | -------------------------------------------------------------------------------- /src/components/index.js: -------------------------------------------------------------------------------- 1 | export { default as Avatar } from "./Avatar"; 2 | export { default as AvatarGroup } from "./AvatarGroup"; 3 | export { default as ChatContainer } from "./ChatContainer"; 4 | export { default as Conversation } from "./Conversation"; 5 | export { default as ConversationHeader } from "./ConversationHeader"; 6 | export { default as ConversationList } from "./ConversationList"; 7 | export { default as ExpansionPanel } from "./ExpansionPanel"; 8 | export { default as InputToolbox } from "./InputToolbox"; 9 | export { default as MainContainer } from "./MainContainer"; 10 | export { default as Message } from "./Message"; 11 | export { default as MessageGroup } from "./MessageGroup"; 12 | export { default as MessageInput } from "./MessageInput"; 13 | export { default as MessageList } from "./MessageList"; 14 | export { default as MessageSeparator } from "./MessageSeparator"; 15 | export { default as Search } from "./Search"; 16 | export { default as Sidebar } from "./Sidebar"; 17 | export { default as Status } from "./Status"; 18 | export { default as TypingIndicator } from "./TypingIndicator"; 19 | export { default as Loader } from "./Loader"; 20 | export { default as Overlay } from "./Overlay"; 21 | export { default as StatusList } from "./StatusList"; 22 | 23 | // Buttons 24 | export { default as Buttons } from "./Buttons"; 25 | export { default as Button } from "./Buttons/Button"; 26 | export { default as ArrowButton } from "./Buttons/ArrowButton"; 27 | export { default as InfoButton } from "./Buttons/InfoButton"; 28 | export { default as VoiceCallButton } from "./Buttons/VoiceCallButton"; 29 | export { default as VideoCallButton } from "./Buttons/VideoCallButton"; 30 | export { default as StarButton } from "./Buttons/StarButton"; 31 | export { default as AddUserButton } from "./Buttons/AddUserButton"; 32 | export { default as EllipsisButton } from "./Buttons/EllipsisButton"; 33 | export { default as SendButton } from "./Buttons/SendButton"; 34 | export { default as AttachmentButton } from "./Buttons/AttachmentButton"; 35 | 36 | // Enums 37 | export { default as Enums } from "./enums"; 38 | -------------------------------------------------------------------------------- /src/components/settings.js: -------------------------------------------------------------------------------- 1 | const prefix = "cs"; 2 | 3 | export { prefix }; 4 | -------------------------------------------------------------------------------- /src/components/utils.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | /* eslint-disable @typescript-eslint/no-empty-function */ 4 | export const noop = () => {}; 5 | 6 | /** 7 | * Tests if children are nil in React and Preact. 8 | * @param {Object} children The children prop of a component. 9 | * @returns {Boolean} 10 | */ 11 | export const isChildrenNil = (children) => 12 | children === null || 13 | children === undefined || 14 | (Array.isArray(children) && children.length === 0); 15 | 16 | /** 17 | * Gets only specified types children 18 | * @param children 19 | * @param {Array} types 20 | * @returns {[]} 21 | */ 22 | export const getChildren = (children, types) => { 23 | const ret = []; 24 | const strTypes = types.map((t) => t.displayName || t.name); 25 | 26 | React.Children.toArray(children).forEach((item) => { 27 | const idx = types.indexOf(item.type); 28 | if (idx !== -1) { 29 | ret[idx] = item; 30 | } else { 31 | const is = item?.props?.as ?? item?.props?.is; 32 | const typeofIs = typeof is; 33 | if (typeofIs === "function") { 34 | // Type 35 | const fIdx = types.indexOf(is); 36 | if (fIdx !== -1) { 37 | ret[fIdx] = React.cloneElement(item, { ...item.props, as: null }); // Cloning to remove "as" attribute, which is not desirable 38 | } 39 | } else if (typeofIs === "object") { 40 | // forward ref 41 | 42 | const typeName = is.name || is.displayName; 43 | const tIdx = strTypes.indexOf(typeName); 44 | if (tIdx !== -1) { 45 | ret[tIdx] = React.cloneElement(item, { ...item.props, as: null }); // Cloning to remove "as" attribute, which is not desirable 46 | } 47 | } else if (typeofIs === "string") { 48 | const sIdx = strTypes.indexOf(is); 49 | if (sIdx !== -1) { 50 | ret[sIdx] = item; 51 | } 52 | } 53 | } 54 | }); 55 | 56 | return ret; 57 | }; 58 | 59 | export const getComponentName = (component) => { 60 | if (typeof component === "string") { 61 | return component; 62 | } 63 | 64 | if ("type" in component) { 65 | const componentType = typeof component.type; 66 | 67 | if (componentType === "function" || componentType === "object") { 68 | if ("displayName" in component.type) { 69 | return component.type.displayName; 70 | } 71 | 72 | if ("name" in component.type) { 73 | return component.type.name; 74 | } 75 | } else if (componentType === "string") { 76 | return component.type; 77 | } 78 | 79 | return "undefined"; 80 | } 81 | 82 | return "undefined"; 83 | }; 84 | 85 | /** 86 | * PropTypes validator. 87 | * Checks if all children is allowed by its types. 88 | * Empty string nodes are always allowed for convenience. 89 | * Returns function for propTypes 90 | * @param {Array} allowedTypes 91 | * @return {Function} 92 | */ 93 | export const allowedChildren = (allowedTypes) => ( 94 | props, 95 | propName, 96 | componentName 97 | ) => { 98 | const allowedTypesAsStrings = allowedTypes.map( 99 | (t) => t.name || t.displayName 100 | ); 101 | 102 | // Function as Child is not supported by React.Children... functions 103 | // and can be antipattern: https://americanexpress.io/faccs-are-an-antipattern/ 104 | // But we don't check fd function is passed as children and its intentional 105 | // Passing function as children has no effect in chat-ui-kit 106 | const forbidden = React.Children.toArray(props[propName]).find((item) => { 107 | if (typeof item === "string" && item.trim().length === 0) { 108 | // Ignore string 109 | return false; 110 | } 111 | 112 | if (allowedTypes.indexOf(item.type) === -1) { 113 | const is = item?.props?.as || item?.props?.is; 114 | 115 | const typeofIs = typeof is; 116 | 117 | if (typeofIs === "function") { 118 | // Type 119 | return allowedTypes.indexOf(is) === -1; 120 | } else if (typeofIs === "object") { 121 | // Forward ref 122 | const typeName = is.name || is.displayName; 123 | return allowedTypesAsStrings.indexOf(typeName) === -1; 124 | } else if (typeofIs === "string") { 125 | return allowedTypesAsStrings.indexOf(is) === -1; 126 | } else { 127 | return true; 128 | } 129 | } 130 | 131 | return undefined; 132 | }); 133 | 134 | if (typeof forbidden !== "undefined") { 135 | const typeName = getComponentName(forbidden); 136 | 137 | const allowedNames = allowedTypes 138 | .map((t) => t.name || t.displayName) 139 | .join(", "); 140 | const errMessage = `"${typeName}" is not a valid child for ${componentName}. Allowed types: ${allowedNames}`; 141 | 142 | return new Error(errMessage); 143 | } 144 | }; 145 | -------------------------------------------------------------------------------- /src/types/index.d.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | DetailedHTMLProps, HTMLAttributes, PropsWithChildren, 3 | ComponentPropsWithRef, ComponentPropsWithoutRef, ElementType 4 | } from "react"; 5 | 6 | /* eslint-disable @typescript-eslint/no-explicit-any */ 7 | export type EmptyProps = Record; 8 | 9 | export type ElementTypeOrHTMLElement = ElementType | HTMLElement; 10 | export type ChatComponentProps = 11 | P & (T extends ElementType ? Omit, "children" | keyof P> : Omit, "children" | keyof P>); 12 | 13 | export type ChatComponentPropsRef = 14 | P & (T extends ElementType ? Omit, "children" | keyof P > : Omit,T>, "children" | keyof P>); 15 | 16 | export type ChatComponentPropsChildren = PropsWithChildren>; 17 | 18 | export type ChatComponentPropsChildrenRef = PropsWithChildren>; 19 | 20 | export {Size, UserStatus, MessageType} from "./unions"; 21 | export * from "../components/Avatar/Avatar"; 22 | export * from "../components/AvatarGroup/AvatarGroup"; 23 | export * from "../components/Buttons/Buttons"; 24 | export * from "../components/ChatContainer/ChatContainer"; 25 | export * from "../components/Conversation/Conversation"; 26 | export * from "../components/ConversationHeader/ConversationHeader"; 27 | export * from "../components/ConversationList/ConversationList"; 28 | export * from "../components/ExpansionPanel/ExpansionPanel"; 29 | export * from "../components/InputToolbox/InputToolbox"; 30 | export * from "../components/Loader/Loader"; 31 | export * from "../components/MainContainer/MainContainer"; 32 | export * from "../components/Message/Message"; 33 | export * from "../components/MessageGroup/MessageGroup"; 34 | export * from "../components/MessageInput/MessageInput"; 35 | export * from "../components/MessageList/MessageList"; 36 | export * from "../components/MessageSeparator/MessageSeparator"; 37 | export * from "../components/Overlay/Overlay"; 38 | export * from "../components/Search/Search"; 39 | export * from "../components/Sidebar/Sidebar"; 40 | export * from "../components/Status/Status"; 41 | export * from "../components/StatusList/StatusList"; 42 | export * from "../components/TypingIndicator/TypingIndicator"; -------------------------------------------------------------------------------- /src/types/unions.d.ts: -------------------------------------------------------------------------------- 1 | export type Size = "xs" | "sm" | "md" | "lg" | "fluid"; 2 | 3 | export type MessageType = "html" | "text" | "image" |"custom"; 4 | 5 | export type UserStatus = "available" | "unavailable" | "away" | "dnd" | "invisible" | "eager"; 6 | 7 | export type LoaderVariant = "default"; 8 | 9 | export type MessageDirection = "incoming" | "outgoing" | 0 | 1; 10 | 11 | export type AvatarPosition = "tl" | "tr" | "cl" | "cr" | "bl" | "br" | "top-left" | "top-right" | "center-left" | "center-right" | "bottom-left" | "bottom-right"; -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "ES2015", 5 | "sourceMap": true, 6 | "declaration": false, 7 | "strict": true, 8 | "esModuleInterop": true, 9 | "isolatedModules": true, 10 | "jsx": "react", 11 | "checkJs": false, 12 | "allowJs": true 13 | }, 14 | "include": ["src/**/*"] 15 | } 16 | --------------------------------------------------------------------------------