diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 602b838539fc..6f7c44c3c04b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -2,6 +2,7 @@ /packages/core-typings/ @RocketChat/chat-engine /packages/rest-typings/ @RocketChat/chat-engine /packages/eslint-config/ @RocketChat/chat-engine +/packages/livechat/ @RocketChat/frontend @RocketChat/chat-engine /.vscode/ @RocketChat/chat-engine /.github/ @RocketChat/chat-engine /_templates/ @RocketChat/chat-engine diff --git a/.github/no-js-action-config.json b/.github/no-js-action-config.json index 5e76f81ed62b..5dd97fccaf67 100644 --- a/.github/no-js-action-config.json +++ b/.github/no-js-action-config.json @@ -1,5 +1,5 @@ { "added": { - "ignore": ["packages/accounts-linkedin/**/*", "packages/linkedin-oauth/**/*", "tests/cypress/integration/08-resolutions.spec.js", "**/.eslintrc.js", "packages/eslint-config/**"] + "ignore": ["packages/accounts-linkedin/**/*", "packages/linkedin-oauth/**/*", "tests/cypress/integration/08-resolutions.spec.js", "**/.eslintrc.js", "packages/eslint-config/**", "**/babel.config.js"] } } diff --git a/apps/meteor/package.json b/apps/meteor/package.json index 20b5f5fe342b..df7489467d08 100644 --- a/apps/meteor/package.json +++ b/apps/meteor/package.json @@ -73,7 +73,7 @@ "@faker-js/faker": "6.1.2", "@playwright/test": "^1.21.0", "@rocket.chat/eslint-config": "workspace:^", - "@rocket.chat/livechat": "1.13.3", + "@rocket.chat/livechat": "workspace:^", "@settlin/spacebars-loader": "^1.0.9", "@storybook/addon-essentials": "~6.4.19", "@storybook/addon-interactions": "~6.4.19", diff --git a/apps/meteor/packages/rocketchat-livechat/plugin/build.sh b/apps/meteor/packages/rocketchat-livechat/plugin/build.sh index 41bb520ce19e..b4f2a4e0dd77 100644 --- a/apps/meteor/packages/rocketchat-livechat/plugin/build.sh +++ b/apps/meteor/packages/rocketchat-livechat/plugin/build.sh @@ -15,7 +15,7 @@ mkdir $LIVECHAT_ASSETS_DIR echo "Installing Livechat ${LATEST_LIVECHAT_VERSION}..." cd $LIVECHAT_DIR -cp -a $ROOT/node_modules/\@rocket.chat/livechat/build/. ./ +cp -a $ROOT/node_modules/\@rocket.chat/livechat/dist/. ./ # change to lowercase so all injected junk from rocket.chat is not sent: https://github.com/meteorhacks/meteor-inject-initial/blob/master/lib/inject-core.js#L10 # this is not harmful since doctype is case-insesitive: https://www.w3.org/TR/html5/syntax.html#the-doctype meteor node -e 'fs.writeFileSync("index.html", fs.readFileSync("index.html").toString().replace(" \ No newline at end of file diff --git a/packages/livechat/.storybook/main.js b/packages/livechat/.storybook/main.js new file mode 100644 index 000000000000..dd5ca2c73ae3 --- /dev/null +++ b/packages/livechat/.storybook/main.js @@ -0,0 +1,17 @@ +module.exports = { + addons: [ + { + name: '@storybook/addon-essentials', + options: { + backgrounds: false, + } + }, + '@storybook/addon-knobs', + ], + stories: [ + '../src/**/stories.js', + '../src/**/story.js', + '../src/**/*.stories.js', + '../src/**/*.story.js', + ], +}; diff --git a/packages/livechat/.storybook/manager.js b/packages/livechat/.storybook/manager.js new file mode 100644 index 000000000000..0c9997054ebc --- /dev/null +++ b/packages/livechat/.storybook/manager.js @@ -0,0 +1,15 @@ +import { addons } from '@storybook/addons'; +import { create } from '@storybook/theming/create'; +import manifest from '../package.json'; +import logo from './logo.svg'; + +addons.setConfig({ + theme: create({ + base: 'light', + brandTitle: manifest.name, + brandImage: logo, + brandUrl: manifest.homepage, + colorPrimary: '#cbced1', + colorSecondary: '#1d74f5', + }), +}); diff --git a/packages/livechat/.storybook/mocks/uiKit.js b/packages/livechat/.storybook/mocks/uiKit.js new file mode 100644 index 000000000000..db3b54093264 --- /dev/null +++ b/packages/livechat/.storybook/mocks/uiKit.js @@ -0,0 +1,24 @@ +import { action } from '@storybook/addon-actions'; + +export const UIKitInteractionType = { + MODAL_OPEN: 'modal.open', + MODAL_CLOSE: 'modal.close', + MODAL_UPDATE: 'modal.update', + ERRORS: 'errors', +}; + +export const UIKitIncomingInteractionType = { + BLOCK: 'blockAction', + VIEW_SUBMIT: 'viewSubmit', + VIEW_CLOSED: 'viewClosed', +}; + +export const UIKitIncomingInteractionContainerType = { + MESSAGE: 'message', + VIEW: 'view', +}; + +export const triggerAction = async (payload) => { + await new Promise((resolve) => setTimeout(resolve, 1000)); + action('dispatchAction')(payload); +}; diff --git a/packages/livechat/.storybook/preview-head.html b/packages/livechat/.storybook/preview-head.html new file mode 100644 index 000000000000..8a8e16adbd7b --- /dev/null +++ b/packages/livechat/.storybook/preview-head.html @@ -0,0 +1,6 @@ + diff --git a/packages/livechat/.storybook/preview.js b/packages/livechat/.storybook/preview.js new file mode 100644 index 000000000000..104f5955b236 --- /dev/null +++ b/packages/livechat/.storybook/preview.js @@ -0,0 +1,15 @@ +import { addDecorator, addParameters } from '@storybook/react'; +import 'loki/configure-react'; +import 'emoji-mart/css/emoji-mart.css'; +import '../src/styles/index.scss'; + +addParameters({ + grid: { + cellSize: 4, + }, + options: { + storySort: ([, a], [, b]) => { + return a.kind.localeCompare(b.kind); + }, + }, +}); diff --git a/packages/livechat/.storybook/webpack.config.js b/packages/livechat/.storybook/webpack.config.js new file mode 100644 index 000000000000..2912e86ad557 --- /dev/null +++ b/packages/livechat/.storybook/webpack.config.js @@ -0,0 +1,45 @@ +module.exports = ({ config }) => { + config.resolve.alias = { + ...config.resolve.alias, + react: require.resolve('preact/compat'), + 'react-dom': require.resolve('preact/compat'), + [require.resolve('../src/lib/uiKit')]: require.resolve('./mocks/uiKit'), + }; + + config.module.rules = config.module.rules.filter(({ loader }) => !/json-loader/.test(loader)); + + const fileLoader = config.module.rules.find(({ loader }) => /file-loader/.test(loader)); + fileLoader.test = /\.(ico|jpg|jpeg|png|gif|eot|otf|webp|ttf|woff|woff2|cur|ani|pdf|mp3|mp4)(\?.*)?$/; + + const urlLoader = config.module.rules.find(({ loader }) => /url-loader/.test(loader)); + urlLoader.test = /\.(webm|wav|m4a|aac|oga)(\?.*)?$/; + + config.module.rules.push({ + test: /\.scss$/, + use: [ + 'style-loader', + { + loader: 'css-loader', + options: { + sourceMap: true, + modules: true, + importLoaders: 1, + }, + }, + 'sass-loader', + ], + }); + + config.module.rules.push({ + test: /\.svg$/, + exclude: [ + __dirname, + ], + use: [ + 'desvg-loader/preact', + 'svg-loader', + ], + }); + + return config; +}; diff --git a/packages/livechat/.stylelintrc b/packages/livechat/.stylelintrc new file mode 100644 index 000000000000..7215370a9635 --- /dev/null +++ b/packages/livechat/.stylelintrc @@ -0,0 +1,329 @@ +{ + "plugins": [ + "stylelint-order" + ], + "rules": { + "at-rule-empty-line-before": [ "always", { + except: [ + "blockless-after-same-name-blockless", + "first-nested", + ], + ignore: ["after-comment"], + } ], + "at-rule-name-case": "lower", + "at-rule-name-space-after": "always", + "at-rule-semicolon-newline-after": "always", + "block-closing-brace-empty-line-before": "never", + "block-closing-brace-newline-after": "always", + "block-closing-brace-newline-before": "always", + "block-closing-brace-space-before": "never-single-line", + "block-no-empty": true, + "block-opening-brace-newline-after": "always", + "block-opening-brace-space-after": "never-single-line", + "block-opening-brace-space-before": "always", + "color-hex-case": "lower", + "color-hex-length": "long", + "color-no-invalid-hex": true, + "comment-empty-line-before": [ "always", { + except: ["first-nested"], + ignore: ["stylelint-commands"], + } ], + "comment-no-empty": true, + "comment-whitespace-inside": "always", + "custom-property-empty-line-before": "never", + "declaration-bang-space-after": "never", + "declaration-bang-space-before": "always", + "declaration-block-no-duplicate-properties": [ true, { + ignore: ["consecutive-duplicates-with-different-values"], + } ], + "declaration-block-no-redundant-longhand-properties": true, + "declaration-block-no-shorthand-property-overrides": true, + "declaration-block-semicolon-newline-after": "always", + "declaration-block-semicolon-space-after": "always-single-line", + "declaration-block-semicolon-space-before": "never", + "declaration-block-single-line-max-declarations": 1, + "declaration-block-trailing-semicolon": "always", + "declaration-colon-newline-after": "always-multi-line", + "declaration-colon-space-after": "always-single-line", + "declaration-colon-space-before": "never", + "font-family-no-duplicate-names": true, + "function-comma-newline-after": "always-multi-line", + "function-comma-space-after": "always-single-line", + "function-comma-space-before": "never", + "function-linear-gradient-no-nonstandard-direction": true, + "function-max-empty-lines": 0, + "function-name-case": "lower", + "function-parentheses-newline-inside": "always-multi-line", + "function-parentheses-space-inside": "never-single-line", + "function-whitespace-after": "always", + "indentation": "tab", + "keyframe-declaration-no-important": true, + "length-zero-no-unit": true, + "max-empty-lines": 1, + "media-feature-colon-space-after": "always", + "media-feature-colon-space-before": "never", + "media-feature-name-case": "lower", + "media-feature-name-no-unknown": true, + "media-feature-parentheses-space-inside": "never", + "media-feature-range-operator-space-after": "always", + "media-feature-range-operator-space-before": "always", + "media-query-list-comma-newline-after": "always-multi-line", + "media-query-list-comma-space-after": "always-single-line", + "media-query-list-comma-space-before": "never", + "no-duplicate-selectors": true, + "no-empty-source": true, + "no-eol-whitespace": true, + "no-extra-semicolons": true, + "no-missing-end-of-source-newline": true, + "number-leading-zero": "always", + "number-no-trailing-zeros": true, + "property-case": "lower", + "property-no-unknown": true, + "rule-empty-line-before": [ "always", { + except: ["first-nested"], + ignore: ["after-comment"], + } ], + "selector-attribute-brackets-space-inside": "never", + "selector-attribute-operator-space-after": "never", + "selector-attribute-operator-space-before": "never", + "selector-combinator-space-after": "always", + "selector-combinator-space-before": "always", + "selector-descendant-combinator-no-non-space": true, + "selector-list-comma-newline-after": "always", + "selector-list-comma-space-before": "never", + "selector-max-empty-lines": 0, + "selector-pseudo-class-case": "lower", + "selector-pseudo-class-no-unknown": [true, { + "ignorePseudoClasses": ["global"] + }], + "selector-pseudo-class-parentheses-space-inside": "never", + "selector-pseudo-element-case": "lower", + "selector-pseudo-element-colon-notation": "double", + "selector-pseudo-element-no-unknown": true, + "selector-type-case": "lower", + "selector-type-no-unknown": true, + "shorthand-property-no-redundant-values": true, + "string-no-newline": true, + "unit-case": "lower", + "unit-no-unknown": true, + "value-list-comma-newline-after": "always-multi-line", + "value-list-comma-space-after": "always-single-line", + "value-list-comma-space-before": "never", + "value-list-max-empty-lines": 0, + "order/properties-order": [ + [ + { + "emptyLineBefore": "always", + "order": "strict", + "properties": [ + "position", + "z-index", + "top", + "right", + "bottom", + "left" + ] + }, + { + "emptyLineBefore": "always", + "order": "strict", + "properties": [ + "display", + "visibility", + "float", + "clear", + "overflow", + "overflow-x", + "overflow-y", + "clip", + "zoom", + "flex-direction", + "flex-order", + "flex-pack", + "flex-align", + "flex" + ] + }, + { + "emptyLineBefore": "always", + "order": "strict", + "properties": [ + "box-sizing", + "width", + "min-width", + "max-width", + "height", + "min-height", + "max-height", + "margin", + "margin-top", + "margin-right", + "margin-bottom", + "margin-left", + "padding", + "padding-top", + "padding-right", + "padding-bottom", + "padding-left" + ] + }, + { + "emptyLineBefore": "always", + "order": "strict", + "properties": [ + "table-layout", + "empty-cells", + "caption-side", + "border-spacing", + "border-collapse", + "list-style", + "list-style-position", + "list-style-type", + "list-style-image" + ] + }, + { + "emptyLineBefore": "always", + "order": "strict", + "properties": [ + "content", + "quotes", + "counter-reset", + "counter-increment", + "resize", + "cursor", + "user-select", + "nav-index", + "nav-up", + "nav-right", + "nav-down", + "nav-left", + "transition", + "transition-delay", + "transition-timing-function", + "transition-duration", + "transition-property", + "transform", + "transform-origin", + "animation", + "animation-name", + "animation-duration", + "animation-play-state", + "animation-timing-function", + "animation-delay", + "animation-iteration-count", + "animation-direction", + "text-align", + "text-align-last", + "vertical-align", + "white-space", + "text-decoration", + "text-emphasis", + "text-emphasis-color", + "text-emphasis-style", + "text-emphasis-position", + "text-indent", + "text-justify", + "text-transform", + "letter-spacing", + "word-spacing", + "text-outline", + "text-transform", + "text-wrap", + "text-overflow", + "text-overflow-ellipsis", + "text-overflow-mode", + "word-wrap", + "word-break", + "tab-size", + "hyphens", + "pointer-events" + ] + }, + { + "emptyLineBefore": "always", + "order": "strict", + "properties": [ + "opacity", + "filter:progid:DXImageTransform.Microsoft.Alpha(Opacity", + "color", + "border", + "border-collapse", + "border-width", + "border-style", + "border-color", + "border-top", + "border-top-width", + "border-top-style", + "border-top-color", + "border-right", + "border-right-width", + "border-right-style", + "border-right-color", + "border-bottom", + "border-bottom-width", + "border-bottom-style", + "border-bottom-color", + "border-left", + "border-left-width", + "border-left-style", + "border-left-color", + "border-radius", + "border-top-left-radius", + "border-top-right-radius", + "border-bottom-right-radius", + "border-bottom-left-radius", + "border-image", + "border-image-source", + "border-image-slice", + "border-image-width", + "border-image-outset", + "border-image-repeat", + "outline", + "outline-width", + "outline-style", + "outline-color", + "outline-offset", + "background", + "filter:progid:DXImageTransform.Microsoft.AlphaImageLoader", + "background-color", + "background-image", + "background-repeat", + "background-attachment", + "background-position", + "background-position-x", + "background-position-y", + "background-clip", + "background-origin", + "background-size", + "box-decoration-break", + "box-shadow", + "filter:progid:DXImageTransform.Microsoft.gradient", + "text-shadow" + ] + }, + { + "emptyLineBefore": "always", + "order": "strict", + "properties": [ + "font", + "font-family", + "font-size", + "font-weight", + "font-style", + "font-variant", + "font-size-adjust", + "font-stretch", + "font-effect", + "font-emphasize", + "font-emphasize-position", + "font-emphasize-style", + "font-smooth", + "line-height" + ] + } + ], + { unspecified: "bottomAlphabetical" } + ] + } +} diff --git a/packages/livechat/CHANGELOG.md b/packages/livechat/CHANGELOG.md new file mode 100644 index 000000000000..65d1afb0539e --- /dev/null +++ b/packages/livechat/CHANGELOG.md @@ -0,0 +1,224 @@ +# @rocket.chat/livechat Change Log +All notable changes to this project will be documented in this file. +This project adheres to [Semantic Versioning](http://semver.org/). + +## 1.13.3 - 2022-04-20 +[FIX] Translations sync with admin config (#713) +[FIX] Broken triggers (#712) +[FIX] setting e-mail validation to RFC regex as the standard (#709) + +## 1.13.2 - 2022-04-18 +[FIX] Sync import for i18next load (#710) + +## 1.13.1 - 2022-04-08 +bump version + +## 1.13.0 - 2022-04-08 +Chore: Update cd.yml (#704) +Chore: Replace a / b with math.div(a, b) on SCSS files (#702) +[IMPROVE] Centralized e-mail validation under a library function #693 +[FIX] Fixes broken triggers. #695 +[IMPROVE] Add TypeScript (#694) +[IMPROVE] Replace i18n package (#657) +[FIX] Prevent html rendering on messages (#701) + +## 1.12.2 - 2022-03-29 +[FIX] Revert: LoadConfig after registering guest #696 + +## 1.12.1 - 2022-03-08 +[FIX] Making sure the 'hide agent info' hides the agent info even with department change. (#688) + +## 1.12.0 - 2022-01-20 +[NEW] Introduce Widget API method to manage Business Units (#677) +[IMPROVE] Update FA translations (#653) + +## 1.11.2 - 2022-01-10 +[FIX] IME not working properly #674 + +## 1.11.1 - 2021-12-30 +[FIX] Hide Livechat if Omnichannel is disabled #671 + +## 1.11.0 - 2021-12-09 +[NEW] Introduce clearLocalStorageWhenChatEnded setting logic (#666) +[IMPROVE] Change logic to generate token on Live Chat (#667) + +## 1.10.0 - 2021-11-22 +[NEW] Audio and Video calling in Livechat using WebRTC (#646) +[FIX] LoadConfig after registering guest (#640) +[FIX] Body styles getting overridden (#660) + +## 1.9.6 - 2021-10-20 +[FIX] 'Hide agent info' not working on system message (#651) +[FIX] Issues on Custom Livechat messages (#648) + +## 1.9.5 - 2021-09-14 +[IMPROVE] Readme enhancements (#557) +[IMPROVE] Swedish Translations (#573) +[FIX] Escaping HTML on paste/drop Text (#471) +[IMPROVE] Spanish translations (#370) +[IMPROVE] Russian translations (#644) +[IMPROVE] Add cookie to identify widget calls (#645) + +## 1.9.4 - 2021-08-19 +[FIX] Iframe overlay (#631) +[IMPROVE] German informal translation (#622) +[FIX] Translation error on department (#632) +[IMPROVE] Open links in another tab on Livechat widget (#610) +[IMPROVE] Dutch Translations (#601) + +## 1.9.3 - 2021-04-21 +[FIX] sound notification on/off (#567) +[FIX] Invalid font size for hiragana and katakana (#559) + +## 1.9.2 - 2021-04-13 +bump version + +## 1.9.1 - 2021-04-12 +[CHORE] Circle CI to github actions #577 +[CHORE] Remove circle CI #580 + +## 1.9.0 - 2021-03-22 +[FIX] Add sanitizer to prevent XSS attacks +[FIX] Wrong Hebrew word הדועה to הודעה #556 +[IMPROVE] add hover effect #566 + +## 1.8.0 - 2021-02-20 + +[IMPROVE] Flow of the widget registration form (#425) +[IMPROVE] System messages style (#554) +[FEATURE] New trigger messages style (#553) +[NEW] Display transfer history messages (#328) +[FIX] Registration form is no longer validating mandatory custom fields. (#550) + +## 1.7.6 - 2020-11-07 +[FIX] Livechat window cannot be restored in popout mode (#529) +[FIX] Visitor's messages are not aligned properly on Mozilla Firefox (#530) + +## 1.7.5 - 2020-10-25 +[FIX] Add zh.json missing translations (#478) +[FIX] Rendering emojis before transform markdown into HTML. (#522) +[FIX] UIKit ActionsBlock layout for smaller screen devices (#479) +[FIX] Emoji picker not rendering (#519) +[FIX] Scroll issues on Safari (#503) +[FIX] Support Webpack relative output path (#521) + +## 1.7.4 - 2020-09-18 +* [FIX] Select input field not working issue (#481) +* [FIX] Invisible div on top of page (#496) + +## 1.7.3 - 2020-09-09 +bump version + +## 1.7.2 - 2020-09-09 +* [FIX] IE11 Support (#492) + +## 1.7.1 - 2020-08-28 +* [FIX] UiKit interation using header as autorization (#483) +* [FIX] Transpile widget.js with Babel + +## 1.7.0 - 2020-08-21 +* [NEW] UiKit support (#474) +* [CHORE] Loki visual tests (#459) +* [IMPROVE] Translate to spanish (#413) +* [NEW] Message character limit feature (#443) +* [IMPROVE] Preact X (#457) +* [NEW] Add Emoji rendering support (#412) + +## 1.6.0 - 2020-06-29 +* [FIX] Improve the transcript request process. (#419) +* [FIX] Start chat disable with handler validate error (#432) +* [FIX] Loading should be flase when department switched is confirmed (#… … +* [FIX] Widget playing multiple sound notifications(Multiple tabs) (#435) +* [NEW] Translate to japanese +* [NEW] Translate i18n to Czech +* [NEW] Update fr.json +* [NEW] Translate to japanese + +## 1.5.0 - 2020-05-20 +* [NEW] Support Registration Form custom fields (#407) … +* [NEW] Translated to Hebrew (#348) +* [NEW] Update es.json (#357) +* [NEW] Russian translation (#359) +* [FIX] Dutch translations (#391) +* [IMPROVE] Update ro.json (397) +* [FIX] Dutch translations + +## 1.4.0 - 2020-03-19 +* [NEW] Add new API method the set the default Agent before chatting (#383) +* [NEW] Keep trigger messages after the conversation starts. (#384) +* [NEW] Widget API Methods (#381) +* [FIX] Livechat guest avatar using name instead of username (#380) + +## 1.3.1 - 2020-02-12 +* [FIX] Add Cross-tab communication on the same origin (#364) +* [FIX] Corrected German title for finished chat (#363) +* [FIX] Update polish translation (#324) + +## 1.3.0 - 2019-12-12 +* [NEW] Add Service Offline callback (#341) +* [NEW] Persian translation (#330) +* [NEW] French translation (#323) +* [NEW] Show/Hide Agent information (#279) +* [FIX] Fix date-fns (#340) (#320) (#315) (#314) + +## 1.2.5 - 2019-10-16 + +* [FIX] date-nfs format usage (#315) + +## 1.2.4 - 2019-10-16 + +* [FIX] date-nfs format usage (#314) + +## 1.2.3 - 2019-10-15 + +* [FIX] date-fns usage (#312) + +## 1.2.2 - 2019-10-15 + +* [FIX] API method calls + +## 1.2.1 - 2019-10-14 + +* [CHORE] fix gh-publish (#306) +* [NEW] Add setting to display a custom chat finished text (#305) + +## 1.2.0 - 2019-10-14 + +* [IMPROVE] Preact X (#302) +* Completed italian translation (#301) +* [FIX] Support Webpack relative output path (#292) +* [FIX] Registration Form changes not being detected (#300) + +## 1.1.6 - 2019-08-17 +* Publish correct package on npm + +## 1.1.5 - 2019-08-17 +* Publish correct package on npm + +## 1.1.4 - 2019-08-13 +* Update the SDK dependency to the latest alpha version(29) (#276) + +## 1.1.3 - 2019-08-09 +* [FIX] package.json files to /build (#273) +* [FIX] Make message markdown links open externally (#272) + +## 1.1.2 - 2019-08-08 +* [FIX] App.init until sdk.connect returns (#269); + +## 1.1.1 - 2019-08-07 +* [IMPROVE] German translations (#264) +* [FIX] remove version from publicPath (#266) + +## 1.1.0 - 2019-07-24 +* [CHORE] Code base maintenance +* [FIX] Sends the navigation history even when there is no room created +* [FIX] Auto reconnect when current connection closes +* [IMPROVE] German translation +* [NEW] Added two new API events +* [RFR] Improve French translation + +## 1.0.0 - 2019-03-13 +* Release Livechat client as a community feature + +## 0.0.1-1 - 2019-03-12 +* Initial release diff --git a/packages/livechat/LICENSE b/packages/livechat/LICENSE new file mode 100644 index 000000000000..c13d939669a1 --- /dev/null +++ b/packages/livechat/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Rocket.Chat + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/livechat/README.md b/packages/livechat/README.md new file mode 100644 index 000000000000..1b24a289a931 --- /dev/null +++ b/packages/livechat/README.md @@ -0,0 +1,88 @@ +# Rocket.Chat.Livechat +[![Language grade: JavaScript](https://img.shields.io/lgtm/grade/javascript/g/RocketChat/Rocket.Chat.Livechat.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/RocketChat/Rocket.Chat.Livechat/context:javascript) +[![Total alerts](https://img.shields.io/lgtm/alerts/g/RocketChat/Rocket.Chat.Livechat.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/RocketChat/Rocket.Chat.Livechat/alerts/) +[![Storybook](https://cdn.jsdelivr.net/gh/storybooks/brand@master/badge/badge-storybook.svg)](https://rocketchat.github.io/Rocket.Chat.Livechat) + +Currently, it's very common to find chat pop-ups when you're browsing websites. + +Those widgets, at Rocket.Chat, are called **LiveChat**. + +**LiveChat** is a small and lightweight application designed to provide B2C (Business-to-customer) communication between Agents and website visitors and is developed with [Preact](https://preactjs.com). + +## Running a development environment + +With your **Rocket.chat** running locally at http://localhost:3000 +
+ +1. Install all node dependencies. +``` bash +yarn +``` + +2. Build preact application to `/build` folder +``` bash +yarn dev +``` + +3. In another terminal, run webpack with hot reload at http://localhost:8080 +``` bash +yarn start +``` + +4. Open this file below in your browser +``` bash +widget-demo.html +``` + +*OBS: For a better performance, you can run this `widget-demo.html` on a [http server](https://github.com/http-party/http-server).* + +## Different host + +To select a different host on your local widget, check this configuration at `/src/api.js` file. + +``` javascript +const host = window.SERVER_URL + || queryString.parse(window.location.search).serverUrl + || (process.env.NODE_ENV === 'development' ? 'http://localhost:3000' : null); +``` + +Here, you can change to your new configuration. + +``` javascript +const host = window.SERVER_URL + || queryString.parse(window.location.search).serverUrl + || (process.env.NODE_ENV === 'development' ? 'https://your.rocketserver.com' : null); +``` + +## Available CLI Commands + +``` bash +# install dependencies +yarn + +# serve with hot reload at localhost:8080 +yarn start + +# build preact application to "build" folder +yarn dev + +# build for production with minification +yarn build + +# test the production build locally +yarn serve + +# run tests with jest and preact-render-spy +yarn test + +# run the storybook +yarn storybook + +## Screens: +![image](https://user-images.githubusercontent.com/5263975/44279585-497b2980-a228-11e8-81a2-36bc3389549e.png) +![image](https://user-images.githubusercontent.com/5263975/44279599-5730af00-a228-11e8-8873-553ef53ee25a.png) +![image](https://user-images.githubusercontent.com/5263975/44279626-6f083300-a228-11e8-8886-c430b28a8e75.png) +![image](https://user-images.githubusercontent.com/5263975/44279634-74657d80-a228-11e8-9583-bf8079972696.png) +![image](https://user-images.githubusercontent.com/5263975/44279639-7b8c8b80-a228-11e8-9815-1a0e3540c4f5.png) +![image](https://user-images.githubusercontent.com/5263975/44279643-847d5d00-a228-11e8-804e-27b973dee8b2.png) +![image](https://user-images.githubusercontent.com/5263975/44279655-90691f00-a228-11e8-8511-4a328a77e5bb.png) diff --git a/packages/livechat/babel.config.js b/packages/livechat/babel.config.js new file mode 100644 index 000000000000..c4be46054af2 --- /dev/null +++ b/packages/livechat/babel.config.js @@ -0,0 +1,21 @@ +module.exports = { + presets: [ + ['@babel/preset-env', { + useBuiltIns: 'entry', + corejs: 3, + }], + ], + plugins: [ + '@babel/plugin-proposal-class-properties', + '@babel/plugin-proposal-object-rest-spread', + ['@babel/plugin-transform-react-jsx', { pragma: 'h', pragmaFrag: 'Fragment' }], + ['babel-plugin-jsx-pragmatic', { + module: 'preact', + import: 'h', + export: 'h', + }], + ], + assumptions: { + setPublicClassFields: true, + }, +}; diff --git a/packages/livechat/package.json b/packages/livechat/package.json new file mode 100644 index 000000000000..358c0bd56867 --- /dev/null +++ b/packages/livechat/package.json @@ -0,0 +1,124 @@ +{ + "name": "@rocket.chat/livechat", + "version": "1.13.3", + "files": [ + "/build" + ], + "private": true, + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/RocketChat/Rocket.Chat.Livechat" + }, + "scripts": { + "clean": "rimraf dist", + "build": "webpack-cli --mode production", + "start": "webpack-dev-server --mode development", + "lint": "run-s eslint stylelint", + "eslint": "eslint src", + "stylelint": "stylelint 'src/**/*.scss'", + "storybook": "start-storybook -p 9001 -c .storybook", + "loki:test": "loki test --chromeDockerImage=chinello/alpine-chrome:latest --chromeFlags=\"--headless --no-sandbox --disable-gpu --disable-features=VizDisplayCompositor\" --verboseRenderer --requireReference --reactUri file:./storybook-static", + "loki:test-ci": "loki test --chromeFlags=\"--headless --no-sandbox --disable-gpu --disable-features=VizDisplayCompositor\" --verboseRenderer --requireReference --reactUri file:./storybook-static", + "loki:update": "loki update --chromeDockerImage=chinello/alpine-chrome:latest --chromeFlags=\"--headless --no-sandbox --disable-gpu --disable-features=VizDisplayCompositor\" --verboseRenderer --requireReference --reactUri file:./storybook-static", + "build-storybook": "build-storybook", + "build-storybook:loki": "cross-env NODE_ENV=loki build-storybook", + "update-storybook": "cross-env NODE_ENV=loki run-s build-storybook loki:update" + }, + "devDependencies": { + "@babel/preset-env": "^7.11.5", + "@rocket.chat/eslint-config": "^0.4.0", + "@storybook/addon-actions": "^6.0.12", + "@storybook/addon-backgrounds": "^6.0.12", + "@storybook/addon-essentials": "^6.0.16", + "@storybook/addon-knobs": "^6.0.12", + "@storybook/addon-viewport": "^6.0.12", + "@storybook/react": "^6.0.12", + "@storybook/storybook-deployer": "^2.8.6", + "@storybook/theming": "^6.0.12", + "@types/react-dom": "^18", + "autoprefixer": "^9.8.6", + "babel-eslint": "^10.1.0", + "babel-loader": "^8.1.0", + "babel-plugin-jsx-pragmatic": "^1.0.2", + "cross-env": "^7.0.2", + "css-loader": "^4.2.2", + "cssnano": "^4.1.10", + "desvg-loader": "^0.1.0", + "eslint": "^7.5.0", + "eslint-plugin-import": "^2.22.0", + "eslint-plugin-react": "^7.20.5", + "eslint-plugin-react-hooks": "^4.1.0", + "file-loader": "^6.1.0", + "gh-release": "^3.5.0", + "html-webpack-plugin": "^4.4.1", + "husky": "^4.2.5", + "if-env": "^1.0.4", + "image-webpack-loader": "^6.0.0", + "loki": "^0.24.0", + "lorem-ipsum": "^2.0.3", + "mini-css-extract-plugin": "^0.11.0", + "npm-run-all": "^4.1.5", + "postcss-css-variables": "^0.17.0", + "postcss-dir-pseudo-class": "^5.0.0", + "postcss-flexbugs-fixes": "^4.2.1", + "postcss-ie11-supports": "^0.1.3", + "postcss-loader": "^3.0.0", + "postcss-logical": "^4.0.2", + "postcss-selector-not": "^4.0.0", + "react-dom": "^18.1.0", + "rimraf": "^3.0.2", + "sass": "^1.49.10", + "sass-loader": "^9.0.2", + "serve": "^11.3.2", + "style-loader": "^1.2.1", + "stylelint": "^13.6.1", + "stylelint-order": "^4.1.0", + "svg-loader": "^0.0.2", + "ts-loader": "^8.3.0", + "typescript": "^4.6.3", + "url-loader": "^4.1.0", + "webpack": "^4.44.1", + "webpack-cli": "^3.3.12", + "webpack-dev-server": "^3.11.0" + }, + "dependencies": { + "@kossnocorp/desvg": "^0.2.0", + "@rocket.chat/sdk": "^1.0.0-alpha.42", + "@rocket.chat/ui-kit": "^0.14.1", + "crypto-js": "^4.1.1", + "css-vars-ponyfill": "^2.3.2", + "date-fns": "^2.15.0", + "desvg": "^1.0.2", + "emoji-mart": "^3.0.0", + "history": "^5.0.0", + "i18next": "^21.3.3", + "markdown-it": "^11.0.0", + "mem": "^6.1.0", + "mitt": "^2.1.0", + "preact": "^10.4.6", + "preact-router": "^3.2.1", + "query-string": "^6.13.1", + "react-i18next": "^11.13.0", + "whatwg-fetch": "^3.4.0" + }, + "browserslist": [ + "> 1%", + "last 2 versions", + "not ie < 11" + ], + "houston": { + "updateFiles": [ + "package.json" + ] + }, + "loki": { + "configurations": { + "chrome": { + "target": "chrome.docker", + "width": 365, + "height": 500 + } + } + } +} diff --git a/packages/livechat/src/api.ts b/packages/livechat/src/api.ts new file mode 100644 index 000000000000..85b6d01991ca --- /dev/null +++ b/packages/livechat/src/api.ts @@ -0,0 +1,9 @@ +import LivechatClient from '@rocket.chat/sdk/lib/clients/Livechat'; +import { parse } from 'query-string'; + +const host = window.SERVER_URL + || parse(window.location.search).serverUrl + || (process.env.NODE_ENV === 'development' ? 'http://localhost:3000' : null); +export const useSsl = Boolean((Array.isArray(host) ? host[0] : host)?.match(/^https:/)); + +export const Livechat = new LivechatClient({ host, protocol: 'ddp', useSsl }); diff --git a/packages/livechat/src/assets/favicon.ico b/packages/livechat/src/assets/favicon.ico new file mode 100644 index 000000000000..9e5bd34f07d4 Binary files /dev/null and b/packages/livechat/src/assets/favicon.ico differ diff --git a/packages/livechat/src/components/Alert/index.js b/packages/livechat/src/components/Alert/index.js new file mode 100644 index 000000000000..b22f8c7f7f35 --- /dev/null +++ b/packages/livechat/src/components/Alert/index.js @@ -0,0 +1,56 @@ +import { Component } from 'preact'; +import { withTranslation } from 'react-i18next'; + +import CloseIcon from '../../icons/close.svg'; +import { createClassName } from '../helpers'; +import styles from './styles.scss'; + + +class Alert extends Component { + static defaultProps = { + timeout: 3000, + hideCloseButton: false, + } + + handleDismiss = () => { + const { onDismiss, id } = this.props; + onDismiss && onDismiss(id); + } + + componentDidMount() { + const { timeout } = this.props; + if (Number.isFinite(timeout) && timeout > 0) { + this.dismissTimeout = setTimeout(this.handleDismiss, timeout); + } + } + + componentWillUnmount() { + clearTimeout(this.dismissTimeout); + } + + render = ({ success, warning, error, color, hideCloseButton, className, style = {}, children, t }) => ( +
+
+ {children} +
+ {!hideCloseButton && ( + + )} +
+ ) +} + +export default withTranslation()(Alert); diff --git a/packages/livechat/src/components/Alert/stories.js b/packages/livechat/src/components/Alert/stories.js new file mode 100644 index 000000000000..31fffbd78b97 --- /dev/null +++ b/packages/livechat/src/components/Alert/stories.js @@ -0,0 +1,83 @@ +import { action } from '@storybook/addon-actions'; +import { withKnobs, boolean, color, text } from '@storybook/addon-knobs'; +import { storiesOf } from '@storybook/react'; + +import Alert from '.'; +import { screenCentered, loremIpsum } from '../../helpers.stories'; + + +storiesOf('Components/Alert', module) + .addDecorator(withKnobs) + .addDecorator(screenCentered) + .add('default', () => ( + + {text('text', loremIpsum({ count: 3, units: 'words' }))} + + )) + .add('success', () => ( + + {text('text', loremIpsum({ count: 3, units: 'words' }))} + + )) + .add('warning', () => ( + + {text('text', loremIpsum({ count: 3, units: 'words' }))} + + )) + .add('error', () => ( + + {text('text', loremIpsum({ count: 3, units: 'words' }))} + + )) + .add('custom color', () => ( + + {text('text', loremIpsum({ count: 3, units: 'words' }))} + + )) + .add('with long text content', () => ( + + {text('text', loremIpsum({ count: 30, units: 'words' }))} + + )) + .add('without timeout', () => ( + + {text('text', loremIpsum({ count: 3, units: 'words' }))} + + )); diff --git a/packages/livechat/src/components/Alert/styles.scss b/packages/livechat/src/components/Alert/styles.scss new file mode 100644 index 000000000000..8aa979ccad24 --- /dev/null +++ b/packages/livechat/src/components/Alert/styles.scss @@ -0,0 +1,65 @@ +@import '../../styles/colors'; +@import '../../styles/helpers'; +@import '../../styles/variables'; + +$alert-color: $color-text-lighter; +$alert-font-family: $font-family; +$alert-background-color: $bg-color-darker; +$alert-success-background-color: $color-green; +$alert-warning-background-color: $color-yellow; +$alert-error-background-color: $color-red; + +.alert { + display: flex; + overflow: hidden; + + width: 100%; + height: 28px; + padding: 6px 16px; + + letter-spacing: 0; + + color: $alert-color; + background-color: $alert-background-color; + + font-family: $alert-font-family; + font-size: 12px; + font-weight: 600; + line-height: 16px; + align-items: center; + justify-content: space-between; + + &__content { + overflow: hidden; + + white-space: nowrap; + text-overflow: ellipsis; + } + + &__close { + display: flex; + + padding: 0; + + cursor: pointer; + + color: $alert-color; + border: none; + outline: none; + background: none; + + @include pressable-button(1px); + } + + &--success { + background-color: $alert-success-background-color; + } + + &--warning { + background-color: $alert-warning-background-color; + } + + &--error { + background-color: $alert-error-background-color; + } +} diff --git a/packages/livechat/src/components/App/App.js b/packages/livechat/src/components/App/App.js new file mode 100644 index 000000000000..f1893bee6c20 --- /dev/null +++ b/packages/livechat/src/components/App/App.js @@ -0,0 +1,246 @@ +import i18next from 'i18next'; +import { Component } from 'preact'; +import { Router, route } from 'preact-router'; +import queryString from 'query-string'; +import { withTranslation } from 'react-i18next'; + +import history from '../../history'; +import Connection from '../../lib/connection'; +import CustomFields from '../../lib/customFields'; +import Hooks from '../../lib/hooks'; +import { parentCall } from '../../lib/parentCall'; +import Triggers from '../../lib/triggers'; +import userPresence from '../../lib/userPresence'; +import { ChatConnector } from '../../routes/Chat'; +import ChatFinished from '../../routes/ChatFinished'; +import GDPRAgreement from '../../routes/GDPRAgreement'; +import LeaveMessage from '../../routes/LeaveMessage'; +import Register from '../../routes/Register'; +import SwitchDepartment from '../../routes/SwitchDepartment'; +import TriggerMessage from '../../routes/TriggerMessage'; +import { store } from '../../store'; +import { visibility, isActiveSession, setInitCookies } from '../helpers'; + +function isRTL(s) { + const rtlChars = '\u0591-\u07FF\u200F\u202B\u202E\uFB1D-\uFDFD\uFE70-\uFEFC'; + const rtlDirCheck = new RegExp(`^[^${ rtlChars }]*?[${ rtlChars }]`); + + return rtlDirCheck.test(s); +} + +export class App extends Component { + state = { + initialized: false, + poppedOut: false, + } + + handleRoute = async () => { + setTimeout(() => { + const { + config: { + settings: { + registrationForm, + nameFieldRegistrationForm, + emailFieldRegistrationForm, + forceAcceptDataProcessingConsent: gdprRequired, + }, + online, + departments = [], + }, + gdpr: { + accepted: gdprAccepted, + }, + triggered, + user, + } = this.props; + + setInitCookies(); + + if (gdprRequired && !gdprAccepted) { + return route('/gdpr'); + } + + if (!online) { + parentCall('callback', 'no-agent-online'); + return route('/leave-message'); + } + + const showDepartment = departments.filter((dept) => dept.showOnRegistration).length > 0; + + const showRegistrationForm = ( + registrationForm + && (nameFieldRegistrationForm || emailFieldRegistrationForm || showDepartment) + ) + && !triggered + && !(user && user.token); + if (showRegistrationForm) { + return route('/register'); + } + }, 100); + } + + handleTriggers() { + const { config: { online, enabled } } = this.props; + if (online && enabled) { + Triggers.init(); + } + + Triggers.processTriggers(); + } + + handleEnableNotifications = () => { + const { dispatch, sound = {} } = this.props; + dispatch({ sound: { ...sound, enabled: true } }); + } + + handleDisableNotifications = () => { + const { dispatch, sound = {} } = this.props; + dispatch({ sound: { ...sound, enabled: false } }); + } + + handleMinimize = () => { + parentCall('minimizeWindow'); + const { dispatch } = this.props; + dispatch({ minimized: true }); + } + + handleRestore = () => { + parentCall('restoreWindow'); + const { dispatch, undocked } = this.props; + const dispatchRestore = () => dispatch({ minimized: false, undocked: false }); + const dispatchEvent = () => { + dispatchRestore(); + store.off('storageSynced', dispatchEvent); + }; + if (undocked) { + store.on('storageSynced', dispatchEvent); + } else { + dispatchRestore(); + } + } + + handleOpenWindow = () => { + parentCall('openPopout'); + const { dispatch } = this.props; + dispatch({ undocked: true, minimized: false }); + } + + handleDismissAlert = (id) => { + const { dispatch, alerts = [] } = this.props; + dispatch({ alerts: alerts.filter((alert) => alert.id !== id) }); + } + + handleVisibilityChange = async () => { + const { dispatch } = this.props; + await dispatch({ visible: !visibility.hidden }); + } + + handleLanguageChange = () => { + this.forceUpdate(); + } + + dismissNotification = () => !isActiveSession(); + + initWidget() { + const { minimized, iframe: { visible }, dispatch } = this.props; + parentCall(minimized ? 'minimizeWindow' : 'restoreWindow'); + parentCall(visible ? 'showWidget' : 'hideWidget'); + + visibility.addListener(this.handleVisibilityChange); + this.handleVisibilityChange(); + window.addEventListener('beforeunload', () => { + visibility.removeListener(this.handleVisibilityChange); + dispatch({ minimized: true, undocked: false }); + }); + + i18next.on('languageChanged', this.handleLanguageChange); + } + + checkPoppedOutWindow() { + // Checking if the window is poppedOut and setting parent minimized if yes for the restore purpose + const { dispatch } = this.props; + const poppedOut = queryString.parse(window.location.search).mode === 'popout'; + this.setState({ poppedOut }); + if (poppedOut) { + dispatch({ minimized: false }); + } + } + + async initialize() { + // TODO: split these behaviors into composable components + await Connection.init(); + CustomFields.init(); + userPresence.init(); + Hooks.init(); + this.handleTriggers(); + this.initWidget(); + this.checkPoppedOutWindow(); + this.setState({ initialized: true }); + parentCall('ready'); + } + + async finalize() { + CustomFields.reset(); + userPresence.reset(); + visibility.removeListener(this.handleVisibilityChange); + } + + componentDidMount() { + this.initialize(); + } + + componentWillUnmount() { + this.finalize(); + } + + componentDidUpdate() { + const { i18n } = this.props; + + if (i18n.t) { + document.dir = isRTL(i18n.t('yes')) ? 'rtl' : 'ltr'; + } + } + + render = ({ + sound, + undocked, + minimized, + expanded, + alerts, + modal, + }, { initialized, poppedOut }) => { + if (!initialized) { + return null; + } + const screenProps = { + notificationsEnabled: sound && sound.enabled, + minimized: !poppedOut && (minimized || undocked), + expanded: !minimized && expanded, + windowed: !minimized && poppedOut, + sound, + alerts, + modal, + onEnableNotifications: this.handleEnableNotifications, + onDisableNotifications: this.handleDisableNotifications, + onMinimize: this.handleMinimize, + onRestore: this.handleRestore, + onOpenWindow: this.handleOpenWindow, + onDismissAlert: this.handleDismissAlert, + dismissNotification: this.dismissNotification, + }; + + return ( + + + + + + + + + + ); + } +} + +export default withTranslation()(App); diff --git a/packages/livechat/src/components/App/index.js b/packages/livechat/src/components/App/index.js new file mode 100644 index 000000000000..1a71971f048a --- /dev/null +++ b/packages/livechat/src/components/App/index.js @@ -0,0 +1,39 @@ +import { Provider as StoreProvider, Consumer as StoreConsumer } from '../../store'; +import App from './App'; + +const AppConnector = () =>
+ + + {({ + config, + user, + triggered, + gdpr, + sound, + undocked, + minimized = true, + expanded = false, + alerts, + modal, + dispatch, + iframe, + }) => ( + + )} + + +
; +export default AppConnector; diff --git a/packages/livechat/src/components/Avatar/index.js b/packages/livechat/src/components/Avatar/index.js new file mode 100644 index 000000000000..e586403a17c5 --- /dev/null +++ b/packages/livechat/src/components/Avatar/index.js @@ -0,0 +1,44 @@ +import { Component } from 'preact'; + +import { createClassName } from '../helpers'; +import styles from './styles.scss'; + + +export class Avatar extends Component { + static getDerivedStateFromProps(props) { + if (props.src) { + return { errored: false }; + } + + return null; + } + + state = { + errored: false, + } + + handleError = () => { + this.setState({ errored: true }); + } + + render = ({ small, large, src, description, status, className, style }, { errored }) => ( +
+ {(src && !errored) && ( + {description} + )} + + {status && ( + + )} +
+ ) +} diff --git a/packages/livechat/src/components/Avatar/profile.png b/packages/livechat/src/components/Avatar/profile.png new file mode 100644 index 000000000000..e52833a26f6d Binary files /dev/null and b/packages/livechat/src/components/Avatar/profile.png differ diff --git a/packages/livechat/src/components/Avatar/stories.js b/packages/livechat/src/components/Avatar/stories.js new file mode 100644 index 000000000000..46054035c687 --- /dev/null +++ b/packages/livechat/src/components/Avatar/stories.js @@ -0,0 +1,93 @@ +import { withKnobs, boolean, text, select } from '@storybook/addon-knobs'; +import { storiesOf } from '@storybook/react'; + +import { Avatar } from '.'; +import { avatarResolver, centered } from '../../helpers.stories'; + + +const defaultSrc = avatarResolver('guilherme.gazzo'); +const defaultDescription = 'user description'; +const statuses = [null, 'offline', 'away', 'busy', 'online']; + +storiesOf('Components/Avatar', module) + .addDecorator(centered) + .addDecorator(withKnobs) + .add('default', () => ( + + )) + .add('large', () => ( + + )) + .add('small', () => ( + + )) + .add('as placeholder', () => ( +
+ + + +
+ )) + .add('with status indicator', () => ( +
+ + + + +
+ )); diff --git a/packages/livechat/src/components/Avatar/styles.scss b/packages/livechat/src/components/Avatar/styles.scss new file mode 100644 index 000000000000..8a429935a81a --- /dev/null +++ b/packages/livechat/src/components/Avatar/styles.scss @@ -0,0 +1,93 @@ +@import '../../styles/colors'; +@import '../../styles/variables'; + +$avatar-size-small: 20px; +$avatar-status-indicator-size-small: 10px; +$avatar-size-medium: 32px; +$avatar-status-indicator-size-medium: 12px; +$avatar-size-large: 46px; +$avatar-status-indicator-size-large: 14px; + +.avatar { + position: relative; + + flex: 0 0 auto; + + width: $avatar-size-medium; + height: $avatar-size-medium; + + border-radius: $default-border-radius; + background-color: #000000; + background-image: url(./profile.png); + background-repeat: no-repeat; + background-position: right; + background-size: contain; + + &__image { + width: 100%; + height: 100%; + + color: transparent; + border-radius: $default-border-radius; + object-fit: cover; + } + + &__status { + position: absolute; + right: -2px; + bottom: -3px; + + overflow: hidden; + + width: $avatar-status-indicator-size-medium; + height: $avatar-status-indicator-size-medium; + + border: 2px solid var(--color, transparent); + border-radius: 50%; + background-color: $bg-color-grey; + + &--small { + right: -2px; + bottom: -2px; + + width: $avatar-status-indicator-size-small; + height: $avatar-status-indicator-size-small; + } + + &--large { + right: -2px; + bottom: -4px; + + width: $avatar-status-indicator-size-large; + height: $avatar-status-indicator-size-large; + } + + &--status { + &-online { + background-color: $color-green; + } + + &-away { + background-color: $color-yellow; + } + + &-busy { + background-color: $color-red; + } + } + } + + &--nobg { + background: none; + } + + &--small { + width: $avatar-size-small; + height: $avatar-size-small; + } + + &--large { + width: $avatar-size-large; + height: $avatar-size-large; + } +} diff --git a/packages/livechat/src/components/Button/index.js b/packages/livechat/src/components/Button/index.js new file mode 100644 index 000000000000..6f1660036bdf --- /dev/null +++ b/packages/livechat/src/components/Button/index.js @@ -0,0 +1,50 @@ +import { createClassName, memo } from '../helpers'; +import styles from './styles.scss'; + + +const handleMouseUp = ({ target }) => target.blur(); + +export const Button = memo(({ + submit, + disabled, + outline, + nude, + danger, + secondary, + stack, + small, + loading, + badge, + icon, + onClick, + className, + style = {}, + children, + img, +}) => ( + +)); diff --git a/packages/livechat/src/components/Button/stories.js b/packages/livechat/src/components/Button/stories.js new file mode 100644 index 000000000000..6e59f2f8b3e7 --- /dev/null +++ b/packages/livechat/src/components/Button/stories.js @@ -0,0 +1,212 @@ +import { action } from '@storybook/addon-actions'; +import { withKnobs, boolean, text } from '@storybook/addon-knobs'; +import { storiesOf } from '@storybook/react'; + +import { Button } from '.'; +import { avatarResolver, centered } from '../../helpers.stories'; +import ChatIcon from '../../icons/chat.svg'; + + +const defaultSrc = avatarResolver('guilherme.gazzo'); + +const defaultText = 'Powered by Rocket.Chat'; +const defaultBadge = 'badged'; + +storiesOf('Components/Button', module) + .addDecorator(centered) + .addDecorator(withKnobs) + .add('normal', () => ( + + )) + .add('disabled', () => ( + + )) + .add('outline', () => ( + + )) + .add('nude', () => ( + + )) + .add('danger', () => ( + + )) + .add('secondary', () => ( + + )) + .add('stack', () => ( + + )) + .add('small', () => ( + + )) + .add('loading', () => ( + + )) + .add('with badge', () => ( + + )) + .add('with icon', () => ( + + )) + .add('transparent with background image', () => ( + + )); diff --git a/packages/livechat/src/components/Button/styles.scss b/packages/livechat/src/components/Button/styles.scss new file mode 100644 index 000000000000..9380e3224796 --- /dev/null +++ b/packages/livechat/src/components/Button/styles.scss @@ -0,0 +1,217 @@ +@use 'sass:math'; + +@import '../../styles/colors'; +@import '../../styles/helpers'; +@import '../../styles/variables'; + +$button-border-width: $default-border; +$button-border-radius: $default-border-radius; +$button-padding: (0.75 * $default-gap - $default-border) (1.5 * $default-gap - $default-border); +$button-small-padding: (0.25 * $default-gap - math.div($default-border, 2)) (1.5 * $default-gap - $default-border); + +$button-active-displacement: 2px; + +$button-color: $color-text-lighter; +$button-background-color: $color-blue; +$button-danger-background-color: $color-dark-red; +$button-secondary-background-color: $color-text-grey; + +$button-font-family: $font-family; +$button-font-size: 0.875rem; +$button-font-weight: 500; +$button-line-height: 1.25rem; + +$button-disabled-opacity: $disabled-opacity; + +$button-loading-border-width: (2 * $default-border); +$button-loading-gap: math.div($default-gap, 2); +$button-loading-size: $button-line-height; +$button-loading-color: #ffffff; + +$button-badge-size: 1.5rem; +$button-badge-background-color: $color-red; +$button-badge-color: $color-text-lighter; +$button-badge-font-family: $font-family; +$button-badge-font-size: 0.8125rem; +$button-badge-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2); + +$button-icon-size: 54px; +$button-icon-border-radius: 4px; +$button-icon-padding: 10px; + +.button { + @mixin state($color, $alpha-color: rgba($color, 0.5)) { + border-color: $color; + background: $color; + + &.button--outline, + &.button--nude { + color: $color; + + &.button--loading::after { + border-color: $color $alpha-color $alpha-color $alpha-color; + } + } + } + + position: relative; + + display: flex; + flex-direction: row; + + box-sizing: border-box; + padding: $button-padding; + + cursor: pointer; + user-select: none; + transition: + color $default-time-animation, + background-color $default-time-animation, + border-color $default-time-animation, + transform math.div($default-time-animation, 2); + white-space: nowrap; + text-decoration: none; + + color: var(--font-color, $button-color); + border: $button-border-width solid; + border-radius: $button-border-radius; + + font-family: $button-font-family; + font-size: $button-font-size; + font-weight: $button-font-weight; + line-height: $button-line-height; + justify-content: center; + + @include state(var(--color, $button-background-color), transparent); + @include pressable-button($button-active-displacement, $button-border-width); + + &:focus { + box-shadow: 0 0 5px rgba(#000000, 0.5); + } + + &--danger { + color: $button-color; + + @include state($button-danger-background-color); + } + + &--secondary { + color: $button-color; + + @include state($button-secondary-background-color); + } + + &--outline { + background: none; + } + + &--nude { + border-color: transparent; + background: none; + + &:focus { + box-shadow: none; + } + } + + &--img { + border: 0; + background-position: center; + background-size: cover; + box-shadow: 0 0 5px rgba(#000000, 0.5) !important; + + &:focus { + box-shadow: none; + } + } + + &--stack { + width: 100%; + } + + &--small { + padding: $button-small-padding; + } + + &--disabled { + cursor: not-allowed; + + opacity: $button-disabled-opacity; + } + + &--loading { + &::after { + position: relative; + left: $button-loading-gap; + + display: inline-flex; + + box-sizing: border-box; + width: $button-line-height; + height: $button-line-height; + + content: ""; + animation: button-loading-rotation 1s linear infinite; + + border: $button-loading-border-width solid; + + border-color: var(--font-color, $button-loading-color) transparent transparent transparent; + border-radius: 50%; + } + + &.button--danger::after, + &.button--secondary::after { + border-color: + $button-loading-color + rgba($button-loading-color, 0.5) + rgba($button-loading-color, 0.5) + rgba($button-loading-color, 0.5); + } + } + + &--icon { + width: $button-icon-size; + height: $button-icon-size; + padding: $button-icon-padding; + + border-radius: $button-icon-border-radius; + } + + &__badge { + position: absolute; + top: math.div(-$button-badge-size, 3); + right: math.div(-$button-badge-size, 3); + + min-width: $button-badge-size; + height: $button-badge-size; + padding: 0 math.div($button-badge-font-size, 2); + + text-align: center; + letter-spacing: 0; + + color: $button-badge-color; + border-radius: math.div($button-badge-size, 2); + + background-color: $button-badge-background-color; + box-shadow: $button-badge-shadow; + + font-family: $button-badge-font-family; + font-size: $button-badge-font-size; + font-weight: bold; + line-height: $button-badge-size; + } + + svg { + flex: 1; + } +} + +@keyframes button-loading-rotation { + 0% { + transform: rotate(0); + } + + 100% { + transform: rotate(360deg); + } +} diff --git a/packages/livechat/src/components/ButtonGroup/index.js b/packages/livechat/src/components/ButtonGroup/index.js new file mode 100644 index 000000000000..120fe1aff4e5 --- /dev/null +++ b/packages/livechat/src/components/ButtonGroup/index.js @@ -0,0 +1,12 @@ +import { cloneElement, toChildArray } from 'preact'; + + +import { createClassName, memo } from '../helpers'; +import styles from './styles.scss'; + + +export const ButtonGroup = memo(({ children }) => ( +
+ {toChildArray(children).map((child) => cloneElement(child, { className: createClassName(styles, 'button-group__item') }))} +
+)); diff --git a/packages/livechat/src/components/ButtonGroup/stories.js b/packages/livechat/src/components/ButtonGroup/stories.js new file mode 100644 index 000000000000..8238132c2676 --- /dev/null +++ b/packages/livechat/src/components/ButtonGroup/stories.js @@ -0,0 +1,39 @@ +import { withKnobs, text } from '@storybook/addon-knobs'; +import { storiesOf } from '@storybook/react'; + +import { ButtonGroup } from '.'; +import { centered } from '../../helpers.stories'; +import { Button } from '../Button'; + + +storiesOf('Components/ButtonGroup', module) + .addDecorator(centered) + .addDecorator(withKnobs) + .add('with buttons of same size', () => ( + + + + + + )) + .add('with buttons of different sizes', () => ( + + + + + + )) + .add('with only small buttons', () => ( + + + + + + )) + .add('with stacked buttons', () => ( + + + + + + )); diff --git a/packages/livechat/src/components/ButtonGroup/styles.scss b/packages/livechat/src/components/ButtonGroup/styles.scss new file mode 100644 index 000000000000..2459a1a72292 --- /dev/null +++ b/packages/livechat/src/components/ButtonGroup/styles.scss @@ -0,0 +1,18 @@ +@use 'sass:math'; + +@import '../../styles/variables'; + +$button-group-margin: math.div($default-gap, 4); + +.button-group { + display: flex; + + margin: -$button-group-margin; + align-items: center; + flex-flow: row wrap; + + &__item { + margin: $button-group-margin; + flex-grow: 1; + } +} diff --git a/packages/livechat/src/components/Calls/CallIFrame.js b/packages/livechat/src/components/Calls/CallIFrame.js new file mode 100644 index 000000000000..445e0eadbe5f --- /dev/null +++ b/packages/livechat/src/components/Calls/CallIFrame.js @@ -0,0 +1,30 @@ +import { Livechat } from '../../api'; +import store from '../../store'; +import { createClassName } from '../helpers'; +import { CallStatus } from './CallStatus'; +import styles from './styles.scss'; + + +export const CallIframe = () => { + const { token, room, incomingCallAlert, ongoingCall } = store.state; + const url = `${ Livechat.client.host }/meet/${ room._id }?token=${ token }&layout=embedded`; + window.handleIframeClose = () => store.setState({ incomingCallAlert: { ...incomingCallAlert, show: false } }); + window.expandCall = () => { + window.open( + `${ Livechat.client.host }/meet/${ room._id }?token=${ token }`, + room._id, + ); + return store.setState({ + incomingCallAlert: { ...incomingCallAlert, show: false }, + ongoingCall: { + ...ongoingCall, + callStatus: CallStatus.IN_PROGRESS_DIFFERENT_TAB, + }, + }); + }; + return ( +
+