@botpress/webchat 0.4.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -13,7 +13,7 @@
13
13
  | File | ✅ | |
14
14
  | Audio | ✅ | |
15
15
  | Video | ✅ | |
16
- | Location | | |
16
+ | Location | | |
17
17
 
18
18
  ### Receiving
19
19
 
@@ -23,9 +23,19 @@
23
23
  | Quick Reply | ✅ | |
24
24
  | Postback | ✅ | |
25
25
  | Say Something | ✅ | |
26
- | Voice | | |
26
+ | Voice | | |
27
27
  | Image | ❌ | |
28
28
  | File | ❌ | |
29
29
  | Audio | ❌ | |
30
30
  | Video | ❌ | |
31
31
  | Location | ❌ | |
32
+
33
+ ## Development
34
+
35
+ When working on the webchat, to see changes made to the codebase, you will need to follow the steps detailed [here](../inject/README.md). The inject script is the part that links any web page to a webchat. This is the easiest way to manually test the webchat.
36
+
37
+ ## Tests
38
+
39
+ To run automated E2E tests, simply run the command `yarn test:chat [--browser <chromium | firefox | edge | electron>]` (for the list of supported browsers see: https://docs.cypress.io/guides/guides/launching-browsers#Browsers).
40
+
41
+ Tests can be found in `tests/e2e` and uses Cypress as the test framework. Configurations for Cypress can be found in the `cypress.json` file at the root of the repository.
@@ -2,8 +2,8 @@
2
2
  declare const Avatar: ({ name, avatarUrl, height, width }: AvatarProps) => JSX.Element;
3
3
  export default Avatar;
4
4
  export interface AvatarProps {
5
- name: string;
6
- avatarUrl: string;
5
+ name?: string;
6
+ avatarUrl?: string;
7
7
  height: number;
8
8
  width: number;
9
9
  }
@@ -7,4 +7,4 @@ declare const _default: React.ForwardRefExoticComponent<import("react-intl").Omi
7
7
  WrappedComponent: React.ComponentType<BotInfoProps>;
8
8
  } & import("mobx-react").IWrappedComponent<unknown>;
9
9
  export default _default;
10
- declare type BotInfoProps = WrappedComponentProps & Pick<StoreDef, 'botInfo' | 'botName' | 'avatarUrl' | 'toggleBotInfo' | 'startConversation' | 'isConversationStarted' | 'updatePreferredLanguage' | 'preferredLanguage' | 'escapeHTML' | 'rtl'>;
10
+ declare type BotInfoProps = WrappedComponentProps & Pick<StoreDef, 'coverPictureUrl' | 'description' | 'phoneNumber' | 'website' | 'emailAddress' | 'termsConditions' | 'privacyPolicy' | 'botInfo' | 'botName' | 'avatarUrl' | 'toggleBotInfo' | 'updatePreferredLanguage' | 'preferredLanguage' | 'escapeHTML' | 'rtl'>;
@@ -31,8 +31,8 @@ const Phone_1 = __importDefault(require("../../../icons/Phone"));
31
31
  const Website_1 = __importDefault(require("../../../icons/Website"));
32
32
  const utils_1 = require("../../../utils");
33
33
  const Avatar_1 = __importDefault(require("../Avatar"));
34
- const CoverPicture = ({ botInfo }) => (react_1.default.createElement("div", { className: 'bpw-botinfo-cover-picture-wrapper' },
35
- react_1.default.createElement("img", { className: 'bpw-botinfo-cover-picture', src: (botInfo === null || botInfo === void 0 ? void 0 : botInfo.details.coverPictureUrl) || `https://via.placeholder.com/400x175?text=${(botInfo === null || botInfo === void 0 ? void 0 : botInfo.name) || ''}` })));
34
+ const CoverPicture = ({ coverPictureUrl }) => (react_1.default.createElement("div", { className: 'bpw-botinfo-cover-picture-wrapper' },
35
+ react_1.default.createElement("img", { className: 'bpw-botinfo-cover-picture', src: coverPictureUrl })));
36
36
  class BotInfoPage extends react_1.default.Component {
37
37
  constructor() {
38
38
  super(...arguments);
@@ -50,51 +50,56 @@ class BotInfoPage extends react_1.default.Component {
50
50
  return react_1.default.createElement("div", { className: 'bpw-botinfo-description', dangerouslySetInnerHTML: { __html: html } });
51
51
  }
52
52
  render() {
53
- const { botInfo, botName, avatarUrl } = this.props;
54
- const onDismiss = this.props.isConversationStarted ? this.props.toggleBotInfo : this.props.startConversation;
53
+ const { botName, avatarUrl, coverPictureUrl, description, phoneNumber, website, emailAddress, termsConditions, privacyPolicy, botInfo } = this.props;
55
54
  return (react_1.default.createElement(react_1.Fragment, null,
56
55
  react_1.default.createElement("link", { rel: "stylesheet", href: "style.scss" }),
57
56
  react_1.default.createElement("div", { className: (0, classnames_1.default)('bpw-botinfo-container', {
58
57
  'bpw-rtl': this.props.rtl
59
58
  }) },
60
- react_1.default.createElement(CoverPicture, { botInfo: botInfo }),
59
+ coverPictureUrl ? react_1.default.createElement(CoverPicture, { coverPictureUrl: coverPictureUrl }) : react_1.default.createElement("div", { style: { height: '42px' } }),
61
60
  react_1.default.createElement("div", { className: 'bpw-botinfo-summary' },
62
61
  react_1.default.createElement(Avatar_1.default, { name: botName, avatarUrl: avatarUrl, height: 64, width: 64 }),
63
- react_1.default.createElement("h3", null, botName),
64
- this.renderDescription(botInfo.description)),
65
- botInfo.details && (react_1.default.createElement(react_1.default.Fragment, null,
62
+ react_1.default.createElement("h3", { style: { marginBottom: '10px' } }, botName),
63
+ description && this.renderDescription(description)),
64
+ react_1.default.createElement(react_1.default.Fragment, null,
66
65
  react_1.default.createElement("div", { className: 'bpw-botinfo-links' },
67
- botInfo.details.phoneNumber && (react_1.default.createElement("div", { className: 'bpw-botinfo-link' },
66
+ phoneNumber && (react_1.default.createElement("div", { className: 'bpw-botinfo-link' },
68
67
  react_1.default.createElement("i", null,
69
68
  react_1.default.createElement(Phone_1.default, null)),
70
- react_1.default.createElement("a", { target: '_blank', href: `tel:${botInfo.details.phoneNumber}` }, botInfo.details.phoneNumber))),
71
- botInfo.details.website && (react_1.default.createElement("div", { className: 'bpw-botinfo-link' },
69
+ react_1.default.createElement("a", { target: '_blank', rel: "noopener noreferrer", href: `tel:${phoneNumber}` }, phoneNumber))),
70
+ website && (react_1.default.createElement("div", { className: 'bpw-botinfo-link' },
72
71
  react_1.default.createElement("i", null,
73
72
  react_1.default.createElement(Website_1.default, null)),
74
- react_1.default.createElement("a", { target: '_blank', href: botInfo.details.website }, botInfo.details.website))),
75
- botInfo.details.emailAddress && (react_1.default.createElement("div", { className: 'bpw-botinfo-link' },
73
+ react_1.default.createElement("a", { target: '_blank', rel: "noopener noreferrer", href: website }, website))),
74
+ emailAddress && (react_1.default.createElement("div", { className: 'bpw-botinfo-link' },
76
75
  react_1.default.createElement("i", null,
77
76
  react_1.default.createElement(Email_1.default, null)),
78
- react_1.default.createElement("a", { target: '_blank', href: `mailto:${botInfo.details.emailAddress}` }, botInfo.details.emailAddress)))),
79
- botInfo.details.termsConditions && (react_1.default.createElement("div", { className: 'bpw-botinfo-terms' },
80
- react_1.default.createElement("a", { target: '_blank', href: botInfo.details.termsConditions },
77
+ react_1.default.createElement("a", { target: '_blank', rel: "noopener noreferrer", href: `mailto:${emailAddress}` }, emailAddress)))),
78
+ termsConditions && (react_1.default.createElement("div", { className: 'bpw-botinfo-terms' },
79
+ react_1.default.createElement("a", { target: '_blank', rel: "noopener noreferrer", href: termsConditions },
81
80
  react_1.default.createElement(react_intl_1.FormattedMessage, { id: 'botInfo.termsAndConditions' })))),
82
- botInfo.details.privacyPolicy && (react_1.default.createElement("div", { className: 'bpw-botinfo-terms' },
83
- react_1.default.createElement("a", { target: '_blank', href: botInfo.details.privacyPolicy },
84
- react_1.default.createElement(react_intl_1.FormattedMessage, { id: 'botInfo.privacyPolicy' })))))),
85
- botInfo.languages.length > 1 && (react_1.default.createElement("div", { className: 'bpw-botinfo-preferred-language' },
81
+ privacyPolicy && (react_1.default.createElement("div", { className: 'bpw-botinfo-terms' },
82
+ react_1.default.createElement("a", { target: '_blank', rel: "noopener noreferrer", href: privacyPolicy },
83
+ react_1.default.createElement(react_intl_1.FormattedMessage, { id: 'botInfo.privacyPolicy' }))))),
84
+ (botInfo === null || botInfo === void 0 ? void 0 : botInfo.languages) && botInfo.languages.length > 1 && (react_1.default.createElement("div", { className: 'bpw-botinfo-preferred-language' },
86
85
  react_1.default.createElement(react_intl_1.FormattedMessage, { id: 'botInfo.preferredLanguage' }),
87
86
  react_1.default.createElement("select", { value: this.props.preferredLanguage, onChange: this.changeLanguage }, botInfo.languages.map((lang) => (react_1.default.createElement("option", { key: lang, value: lang }, lang.toUpperCase())))))),
88
- react_1.default.createElement("button", { tabIndex: 1, ref: (el) => (this.btnEl = el), className: 'bpw-botinfo-start-button', onClick: onDismiss.bind(this, undefined) }, this.props.isConversationStarted ? (react_1.default.createElement(react_intl_1.FormattedMessage, { id: 'botInfo.backToConversation' })) : (react_1.default.createElement(react_intl_1.FormattedMessage, { id: 'botInfo.startConversation' }))))));
87
+ react_1.default.createElement("button", { tabIndex: 1, ref: (el) => (this.btnEl = el), className: 'bpw-botinfo-start-button', onClick: this.props.toggleBotInfo.bind(this, undefined) },
88
+ react_1.default.createElement(react_intl_1.FormattedMessage, { id: 'botInfo.backToConversation' })))));
89
89
  }
90
90
  }
91
91
  exports.default = (0, mobx_react_1.inject)(({ store }) => ({
92
+ coverPictureUrl: store.coverPictureUrl,
93
+ description: store.description,
94
+ phoneNumber: store.phoneNumber,
95
+ website: store.website,
96
+ emailAddress: store.emailAddress,
97
+ termsConditions: store.termsConditions,
98
+ privacyPolicy: store.privacyPolicy,
92
99
  botName: store.botName,
93
100
  botInfo: store.botInfo,
94
101
  avatarUrl: store.botAvatarUrl,
95
- startConversation: store.startConversation,
96
102
  toggleBotInfo: store.view.toggleBotInfo,
97
- isConversationStarted: store.isConversationStarted,
98
103
  updatePreferredLanguage: store.updatePreferredLanguage,
99
104
  preferredLanguage: store.preferredLanguage,
100
105
  escapeHTML: store.escapeHTML,
@@ -8,4 +8,4 @@ declare const _default: React.ForwardRefExoticComponent<import("react-intl").Omi
8
8
  WrappedComponent: React.ComponentType<MessageProps>;
9
9
  } & import("mobx-react").IWrappedComponent<unknown>;
10
10
  export default _default;
11
- declare type MessageProps = Renderer.Message & WrappedComponentProps & Pick<StoreDef, 'intl'>;
11
+ declare type MessageProps = Renderer.Message & WrappedComponentProps & Pick<StoreDef, 'intl' | 'selectedMessageId' | 'config'>;
@@ -27,6 +27,7 @@ const classnames_1 = __importDefault(require("classnames"));
27
27
  const mobx_react_1 = require("mobx-react");
28
28
  const react_1 = __importStar(require("react"));
29
29
  const react_intl_1 = require("react-intl");
30
+ const webchatEvents_1 = require("../../utils/webchatEvents");
30
31
  class Message extends react_1.Component {
31
32
  constructor() {
32
33
  super(...arguments);
@@ -34,6 +35,17 @@ class Message extends react_1.Component {
34
35
  hasError: false,
35
36
  showMore: false
36
37
  };
38
+ this.onMessageClick = () => {
39
+ var _a, _b;
40
+ (_a = this.props.store) === null || _a === void 0 ? void 0 : _a.setSelectedMessage(this.props.messageId);
41
+ (0, webchatEvents_1.postMessageToParent)('MESSAGE.SELECTED', {
42
+ id: this.props.messageId,
43
+ conversationId: (_b = this.props.store) === null || _b === void 0 ? void 0 : _b.currentConversationId,
44
+ sentOn: this.props.sentOn,
45
+ payload: this.props.payload,
46
+ from: this.props.isBotMessage ? 'bot' : 'user'
47
+ }, this.props.config.chatId);
48
+ };
37
49
  }
38
50
  static getDerivedStateFromError(_error) {
39
51
  return { hasError: true };
@@ -70,11 +82,14 @@ class Message extends react_1.Component {
70
82
  return null;
71
83
  }
72
84
  const additionalStyle = (this.props.payload && this.props.payload['web-style']) || {};
85
+ const messageSelectedClass = {
86
+ 'bpw-message-selected': this.props.selectedMessageId === this.props.messageId
87
+ };
73
88
  if (this.props.noBubble || ((_b = (_a = this.props.payload) === null || _a === void 0 ? void 0 : _a.wrapped) === null || _b === void 0 ? void 0 : _b.noBubble)) {
74
- return (react_1.default.createElement("div", { className: (0, classnames_1.default)(this.props.className, wrappedClass), style: additionalStyle }, rendered));
89
+ return (react_1.default.createElement("div", { className: (0, classnames_1.default)(this.props.className, wrappedClass, messageSelectedClass), style: additionalStyle, onClick: this.onMessageClick }, rendered));
75
90
  }
76
91
  return (react_1.default.createElement("div", { className: (0, classnames_1.default)(this.props.className, wrappedClass, 'bpw-chat-bubble', `bpw-bubble-${type}`), "data-from": this.props.fromLabel, tabIndex: -1, style: additionalStyle },
77
- react_1.default.createElement("div", { tabIndex: -1, className: "bpw-chat-bubble-content" },
92
+ react_1.default.createElement("div", { tabIndex: -1, className: (0, classnames_1.default)('bpw-chat-bubble-content', messageSelectedClass), onClick: this.onMessageClick },
78
93
  react_1.default.createElement("span", { className: "sr-only" }, this.props.store.intl.formatMessage({
79
94
  id: this.props.isBotMessage ? 'message.botSaid' : 'message.iSaid',
80
95
  defaultMessage: this.props.isBotMessage ? 'Virtual assistant said : ' : 'I said : '
@@ -85,5 +100,7 @@ class Message extends react_1.Component {
85
100
  }
86
101
  }
87
102
  exports.default = (0, mobx_react_1.inject)(({ store }) => ({
88
- intl: store.intl
103
+ intl: store.intl,
104
+ config: store.config,
105
+ selectedMessageId: store.selectedMessageId
89
106
  }))((0, react_intl_1.injectIntl)((0, mobx_react_1.observer)(Message)));
@@ -20,4 +20,4 @@ declare type Props = {
20
20
  messages: MessageDetails[];
21
21
  isLastGroup: boolean;
22
22
  store?: RootStore;
23
- } & Pick<StoreDef, 'sendFeedback' | 'sendData'>;
23
+ } & Pick<StoreDef, 'sendFeedback' | 'sendData' | 'selectedMessageId'>;
@@ -43,7 +43,9 @@ class MessageGroup extends react_1.default.Component {
43
43
  }) },
44
44
  avatar,
45
45
  react_1.default.createElement("div", { role: "region", className: 'bpw-message-container' },
46
- react_1.default.createElement("div", { "aria-live": "assertive", role: "log", className: 'bpw-message-group' },
46
+ react_1.default.createElement("div", { "aria-live": "assertive", role: "log", className: (0, classnames_1.default)('bpw-message-group', {
47
+ 'bpw-message-group-selected': !!this.props.messages.find((m) => m.id === this.props.selectedMessageId)
48
+ }) },
47
49
  react_1.default.createElement("span", { "data-from": fromLabel, className: "from hidden", "aria-hidden": "true" }, fromLabel),
48
50
  (0, sortBy_1.default)(messages, ['sent_on', 'eventId']).map((message, i, messages) => {
49
51
  const isLastMsg = i === messages.length - 1;
@@ -56,5 +58,6 @@ class MessageGroup extends react_1.default.Component {
56
58
  exports.default = (0, mobx_react_1.inject)(({ store }) => ({
57
59
  store,
58
60
  sendFeedback: store.sendFeedback,
59
- sendData: store.sendData
61
+ sendData: store.sendData,
62
+ selectedMessageId: store.selectedMessageId
60
63
  }))(MessageGroup);
@@ -1,90 +1,88 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
5
+ }) : (function(o, m, k, k2) {
6
+ if (k2 === undefined) k2 = k;
7
+ o[k2] = m[k];
8
+ }));
9
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
10
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
11
+ }) : function(o, v) {
12
+ o["default"] = v;
13
+ });
14
+ var __importStar = (this && this.__importStar) || function (mod) {
15
+ if (mod && mod.__esModule) return mod;
16
+ var result = {};
17
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
18
+ __setModuleDefault(result, mod);
19
+ return result;
20
+ };
2
21
  var __importDefault = (this && this.__importDefault) || function (mod) {
3
22
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
23
  };
5
24
  Object.defineProperty(exports, "__esModule", { value: true });
6
- const resize_observer_1 = require("@juggle/resize-observer");
7
25
  const difference_in_minutes_1 = __importDefault(require("date-fns/difference_in_minutes"));
8
- const debounce_1 = __importDefault(require("lodash/debounce"));
26
+ const last_1 = __importDefault(require("lodash/last"));
9
27
  const mobx_1 = require("mobx");
10
28
  const mobx_react_1 = require("mobx-react");
11
- const react_1 = __importDefault(require("react"));
29
+ const react_1 = __importStar(require("react"));
12
30
  const react_intl_1 = require("react-intl");
31
+ const react_scroll_to_bottom_1 = __importStar(require("react-scroll-to-bottom"));
13
32
  const constants_1 = __importDefault(require("../../core/constants"));
14
33
  const Avatar_1 = __importDefault(require("../common/Avatar"));
15
34
  const MessageGroup_1 = __importDefault(require("./MessageGroup"));
16
35
  class MessageList extends react_1.default.Component {
17
- constructor() {
18
- super(...arguments);
19
- this.state = { showNewMessageIndicator: false, manualScroll: false };
20
- this.shouldDisplayMessage = (m) => {
21
- return m.payload.type !== 'postback';
22
- };
23
- this.handleScroll = (0, debounce_1.default)((e) => {
24
- const scroll = this.messagesDiv.scrollHeight - this.messagesDiv.scrollTop - this.messagesDiv.clientHeight;
25
- const manualScroll = scroll >= 150;
26
- const showNewMessageIndicator = this.state.showNewMessageIndicator && manualScroll;
27
- this.setState({ manualScroll, showNewMessageIndicator });
28
- }, 50);
29
- }
30
36
  componentDidMount() {
31
- this.tryScrollToBottom(true);
32
37
  (0, mobx_1.observe)(this.props.focusedArea, (focus) => {
33
38
  focus.newValue === 'convo' && this.messagesDiv.focus();
34
39
  });
35
- if (this.props.currentMessages) {
36
- (0, mobx_1.observe)(this.props.currentMessages, (messages) => {
37
- if (this.state.manualScroll) {
38
- if (!this.state.showNewMessageIndicator) {
39
- this.setState({ showNewMessageIndicator: true });
40
- }
41
- return;
42
- }
43
- this.tryScrollToBottom();
44
- });
45
- }
46
- // this should account for keyboard rendering as it triggers a resize of the messagesDiv
47
- this.divSizeObserver = new resize_observer_1.ResizeObserver((0, debounce_1.default)((_divResizeEntry) => {
48
- // we don't need to do anything with the resize entry
49
- this.tryScrollToBottom();
50
- }, 200, { trailing: true }));
51
- this.divSizeObserver.observe(this.messagesDiv);
52
40
  }
53
- componentWillUnmount() {
54
- this.divSizeObserver.disconnect();
41
+ render() {
42
+ return (react_1.default.createElement(react_scroll_to_bottom_1.default, { mode: 'bottom', initialScrollBehavior: 'auto', tabIndex: 0, className: 'bpw-msg-list-scroll-container', scrollViewClassName: 'bpw-msg-list', ref: (m) => {
43
+ this.messagesDiv = m;
44
+ }, followButtonClassName: 'bpw-msg-list-follow' },
45
+ react_1.default.createElement(Content, Object.assign({}, this.props))));
55
46
  }
56
- componentDidUpdate() {
57
- if (this.state.manualScroll) {
58
- return;
47
+ }
48
+ const Content = (0, mobx_react_1.observer)((props) => {
49
+ var _a, _b;
50
+ const [state, setState] = (0, react_1.useState)({
51
+ showNewMessageIndicator: false,
52
+ messagesLength: undefined
53
+ });
54
+ const scrollToBottom = (0, react_scroll_to_bottom_1.useScrollToBottom)();
55
+ const [sticky] = (0, react_scroll_to_bottom_1.useSticky)();
56
+ (0, react_1.useEffect)(() => {
57
+ var _a, _b;
58
+ const stateUpdate = Object.assign(Object.assign({}, state), { messagesLength: (_a = props === null || props === void 0 ? void 0 : props.currentMessages) === null || _a === void 0 ? void 0 : _a.length });
59
+ if (!sticky && state.messagesLength !== ((_b = props === null || props === void 0 ? void 0 : props.currentMessages) === null || _b === void 0 ? void 0 : _b.length)) {
60
+ setState(Object.assign(Object.assign({}, stateUpdate), { showNewMessageIndicator: true }));
59
61
  }
60
- this.tryScrollToBottom();
61
- }
62
- tryScrollToBottom(delayed) {
63
- setTimeout(() => {
64
- try {
65
- this.messagesDiv.scrollTop = this.messagesDiv.scrollHeight;
66
- }
67
- catch (err) {
68
- // Discard the error
69
- }
70
- }, delayed ? 250 : 0);
71
- }
72
- renderDate(date) {
62
+ else {
63
+ setState(Object.assign(Object.assign({}, stateUpdate), { showNewMessageIndicator: false }));
64
+ }
65
+ }, [(_a = props === null || props === void 0 ? void 0 : props.currentMessages) === null || _a === void 0 ? void 0 : _a.length, sticky]);
66
+ const shouldDisplayMessage = (m) => {
67
+ return m.payload.type !== 'postback';
68
+ };
69
+ const renderDate = (date) => {
73
70
  return (react_1.default.createElement("div", { className: 'bpw-date-container' },
74
- new Intl.DateTimeFormat(this.props.intl.locale || 'en', {
71
+ new Intl.DateTimeFormat(props.intl.locale || 'en', {
75
72
  month: 'short',
76
73
  day: 'numeric',
77
74
  hour: 'numeric',
78
75
  minute: 'numeric'
79
76
  }).format(new Date(date)),
80
77
  react_1.default.createElement("div", { className: 'bpw-small-line' })));
81
- }
82
- renderAvatar(name, url) {
78
+ };
79
+ const renderAvatar = (name, url) => {
83
80
  const avatarSize = 40;
84
81
  return react_1.default.createElement(Avatar_1.default, { name: name, avatarUrl: url, height: avatarSize, width: avatarSize });
85
- }
86
- renderMessageGroups() {
87
- const messages = (this.props.currentMessages || []).filter((m) => this.shouldDisplayMessage(m));
82
+ };
83
+ const renderMessageGroups = () => {
84
+ var _a;
85
+ const messages = (props.currentMessages || []).filter((m) => shouldDisplayMessage(m));
88
86
  const groups = [];
89
87
  let lastSpeaker = undefined;
90
88
  let lastDate = undefined;
@@ -106,7 +104,7 @@ class MessageList extends react_1.default.Component {
106
104
  lastSpeaker = speaker;
107
105
  lastDate = date;
108
106
  });
109
- if (this.props.isBotTyping.get()) {
107
+ if ((_a = props === null || props === void 0 ? void 0 : props.isBotTyping) === null || _a === void 0 ? void 0 : _a.get()) {
110
108
  if (lastSpeaker !== 'bot') {
111
109
  currentGroup = [];
112
110
  groups.push(currentGroup);
@@ -123,24 +121,20 @@ class MessageList extends react_1.default.Component {
123
121
  const groupDate = group === null || group === void 0 ? void 0 : group[0].sentOn;
124
122
  const isDateNeeded = !groups[i - 1] ||
125
123
  (0, difference_in_minutes_1.default)(new Date(groupDate), new Date(lastDate)) > constants_1.default.TIME_BETWEEN_DATES;
126
- const [{ authorId }] = group;
127
- const avatar = !authorId && this.renderAvatar(this.props.botName, this.props.botAvatarUrl);
124
+ const { authorId } = (0, last_1.default)(group);
125
+ const avatar = !authorId && renderAvatar(props.botName, props.botAvatarUrl);
128
126
  return (react_1.default.createElement("div", { key: i },
129
- isDateNeeded && this.renderDate(group[0].sentOn),
127
+ isDateNeeded && renderDate(group[0].sentOn),
130
128
  react_1.default.createElement(MessageGroup_1.default, { isBot: !authorId, avatar: avatar, key: `msg-group-${i}`, isLastGroup: i >= groups.length - 1, messages: group })));
131
129
  })));
132
- }
133
- render() {
134
- return (react_1.default.createElement("div", { tabIndex: 0, className: 'bpw-msg-list', ref: (m) => {
135
- this.messagesDiv = m;
136
- }, onScroll: this.handleScroll },
137
- this.state.showNewMessageIndicator && (react_1.default.createElement("div", { className: "bpw-new-messages-indicator", onClick: (e) => this.tryScrollToBottom() },
138
- react_1.default.createElement("span", null, this.props.intl.formatMessage({
139
- id: `messages.newMessage${this.props.currentMessages.length === 1 ? '' : 's'}`
140
- })))),
141
- this.renderMessageGroups()));
142
- }
143
- }
130
+ };
131
+ return (react_1.default.createElement(react_1.default.Fragment, null,
132
+ state.showNewMessageIndicator && (react_1.default.createElement("div", { className: "bpw-new-messages-indicator", onClick: (e) => scrollToBottom() },
133
+ react_1.default.createElement("span", null, props.intl.formatMessage({
134
+ id: `messages.newMessage${((_b = props === null || props === void 0 ? void 0 : props.currentMessages) === null || _b === void 0 ? void 0 : _b.length) === 1 ? '' : 's'}`
135
+ })))),
136
+ renderMessageGroups()));
137
+ });
144
138
  exports.default = (0, mobx_react_1.inject)(({ store }) => ({
145
139
  intl: store.intl,
146
140
  botName: store.botName,
@@ -8,7 +8,6 @@ export default class BpSocket {
8
8
  constructor(config: Config);
9
9
  setup(): void;
10
10
  sendPayload(payload: any): Promise<Message>;
11
- postToParent: (_type: string, payload: any) => void;
12
11
  connect(): Promise<void>;
13
12
  reload(config: Config): Promise<void>;
14
13
  private getCreds;
@@ -10,13 +10,10 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
12
  const messaging_socket_1 = require("@botpress/messaging-socket");
13
+ const webchatEvents_1 = require("../utils/webchatEvents");
13
14
  class BpSocket {
14
15
  constructor(config) {
15
16
  this.config = config;
16
- this.postToParent = (_type, payload) => {
17
- var _a;
18
- (_a = window.parent) === null || _a === void 0 ? void 0 : _a.postMessage(Object.assign(Object.assign({}, payload), { chatId: this.chatId }), '*');
19
- };
20
17
  this.chatId = config.chatId;
21
18
  this.socket = new messaging_socket_1.MessagingSocket({ url: config.messagingUrl, clientId: config.clientId });
22
19
  window.websocket = this.socket;
@@ -38,7 +35,7 @@ class BpSocket {
38
35
  if (this.socket.userId) {
39
36
  const userId = this.socket.userId;
40
37
  window.BP_STORAGE.set('creds', this.socket.creds);
41
- this.postToParent('', { userId });
38
+ (0, webchatEvents_1.postMessageToParent)('USER.CONNECTED', { userId }, this.chatId);
42
39
  }
43
40
  });
44
41
  }
@@ -1 +1,2 @@
1
1
  declare module '*.scss';
2
+ declare module 'react-scroll-to-bottom';
package/dist/main.d.ts CHANGED
@@ -10,4 +10,4 @@ declare const _default: React.ForwardRefExoticComponent<import("react-intl").Omi
10
10
  export default _default;
11
11
  declare type MainProps = {
12
12
  store?: RootStore;
13
- } & WrappedComponentProps & Pick<StoreDef, 'config' | 'initializeChat' | 'botInfo' | 'fetchBotInfo' | 'sendMessage' | 'sendData' | 'intl' | 'updateTyping' | 'updateBotUILanguage' | 'hideChat' | 'showChat' | 'toggleBotInfo' | 'widgetTransition' | 'activeView' | 'isFullscreen' | 'unreadCount' | 'hasUnreadMessages' | 'showWidgetButton' | 'addEventToConversation' | 'clearMessages' | 'updateConfig' | 'mergeConfig' | 'isWebchatReady' | 'incrementUnread' | 'displayWidgetView' | 'resetUnread' | 'setLoadingCompleted' | 'dimensions' | 'updateLastMessage' | 'fetchConversation' | 'setIntlProvider' | 'setSocket' | 'currentConversationId' | 'resetConversation'>;
13
+ } & WrappedComponentProps & Pick<StoreDef, 'config' | 'initializeChat' | 'botInfo' | 'fetchBotInfo' | 'sendMessage' | 'sendData' | 'intl' | 'updateTyping' | 'updateBotUILanguage' | 'hideChat' | 'showChat' | 'toggleBotInfo' | 'widgetTransition' | 'activeView' | 'isFullscreen' | 'unreadCount' | 'hasUnreadMessages' | 'showWidgetButton' | 'addEventToConversation' | 'clearMessages' | 'updateConfig' | 'mergeConfig' | 'isWebchatReady' | 'incrementUnread' | 'displayWidgetView' | 'resetUnread' | 'setLoadingCompleted' | 'dimensions' | 'updateLastMessage' | 'fetchConversation' | 'createConversation' | 'setIntlProvider' | 'setSocket' | 'currentConversationId' | 'currentConversation' | 'resetConversation'>;
package/dist/main.js CHANGED
@@ -34,7 +34,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
34
34
  exports.DEFAULT_TYPING_DELAY = void 0;
35
35
  const classnames_1 = __importDefault(require("classnames"));
36
36
  const debounce_1 = __importDefault(require("lodash/debounce"));
37
- const set_1 = __importDefault(require("lodash/set"));
38
37
  const mobx_1 = require("mobx");
39
38
  const mobx_react_1 = require("mobx-react");
40
39
  const query_string_1 = __importDefault(require("query-string"));
@@ -46,6 +45,7 @@ const socket_1 = __importDefault(require("./core/socket"));
46
45
  const Chat_1 = __importDefault(require("./icons/Chat"));
47
46
  const utils_1 = require("./utils");
48
47
  const analytics_1 = require("./utils/analytics");
48
+ const webchatEvents_1 = require("./utils/webchatEvents");
49
49
  exports.DEFAULT_TYPING_DELAY = 1000;
50
50
  class Web extends react_1.default.Component {
51
51
  constructor(props) {
@@ -93,12 +93,14 @@ class Web extends react_1.default.Component {
93
93
  (0, analytics_1.trackWebchatState)('toggle');
94
94
  }
95
95
  else if (type === 'message') {
96
- (0, analytics_1.trackMessage)('sent');
97
96
  yield this.props.sendMessage(text);
98
97
  }
99
98
  else if (type === 'loadConversation') {
100
99
  yield this.props.fetchConversation(conversationId);
101
100
  }
101
+ else if (type === 'createConversation') {
102
+ yield this.props.createConversation();
103
+ }
102
104
  else if (type === 'toggleBotInfo') {
103
105
  this.props.toggleBotInfo();
104
106
  }
@@ -110,7 +112,7 @@ class Web extends react_1.default.Component {
110
112
  }
111
113
  });
112
114
  this.handleNewMessage = (event) => __awaiter(this, void 0, void 0, function* () {
113
- var _b;
115
+ var _b, _c, _d;
114
116
  if (!this.isCurrentConversation(event)) {
115
117
  // don't do anything, it's a message from another conversation
116
118
  return;
@@ -123,7 +125,15 @@ class Web extends react_1.default.Component {
123
125
  // don't do anything, it's the system message
124
126
  return;
125
127
  }
126
- (0, analytics_1.trackMessage)('received');
128
+ if (((_c = this.props.currentConversation) === null || _c === void 0 ? void 0 : _c.userId) !== event.authorId) {
129
+ (0, analytics_1.trackMessage)('received');
130
+ (0, webchatEvents_1.postMessageToParent)('MESSAGE.RECEIVED', event, this.props.config.chatId);
131
+ // This is to handle a special case for the emulator, setting the selected css class to the last message group
132
+ // This needs a rethinking
133
+ if (event.id) {
134
+ (_d = this.props.store) === null || _d === void 0 ? void 0 : _d.setSelectedMessage(event.id);
135
+ }
136
+ }
127
137
  this.props.updateLastMessage(event.conversationId, event);
128
138
  yield this.props.addEventToConversation(event);
129
139
  // there's no focus on the actual conversation
@@ -137,8 +147,8 @@ class Web extends react_1.default.Component {
137
147
  yield this.props.updateTyping(event);
138
148
  });
139
149
  this.playSound = (0, debounce_1.default)(() => __awaiter(this, void 0, void 0, function* () {
140
- var _c;
141
- const disableNotificationSound = this.config.disableNotificationSound || ((_c = this.props.config) === null || _c === void 0 ? void 0 : _c.disableNotificationSound);
150
+ var _e;
151
+ const disableNotificationSound = this.config.disableNotificationSound || ((_e = this.props.config) === null || _e === void 0 ? void 0 : _e.disableNotificationSound);
142
152
  if (disableNotificationSound || this.audio.readyState < 2) {
143
153
  return;
144
154
  }
@@ -192,12 +202,9 @@ class Web extends react_1.default.Component {
192
202
  return __awaiter(this, void 0, void 0, function* () {
193
203
  this.config = this.extractConfig();
194
204
  this.props.updateConfig(this.config);
195
- if (this.config.exposeStore) {
196
- const storePath = this.config.chatId ? `${this.config.chatId}.webchat_store` : 'webchat_store';
197
- (0, set_1.default)(window.parent, storePath, this.props.store);
198
- }
205
+ // is this necessary ?
199
206
  if (this.config.containerWidth) {
200
- this.postMessageToParent('setWidth', this.config.containerWidth);
207
+ (0, webchatEvents_1.postMessageToParent)('UI.RESIZE', this.config.containerWidth, this.config.chatId);
201
208
  }
202
209
  yield this.props.fetchBotInfo();
203
210
  if (!this.isLazySocket()) {
@@ -206,10 +213,6 @@ class Web extends react_1.default.Component {
206
213
  this.setupObserver();
207
214
  });
208
215
  }
209
- postMessageToParent(type, value) {
210
- var _a;
211
- (_a = window.parent) === null || _a === void 0 ? void 0 : _a.postMessage({ type, value, chatId: this.config.chatId }, '*');
212
- }
213
216
  extractConfig() {
214
217
  let userConfig = Object.assign({}, constants_1.default.DEFAULT_CONFIG, this.props.config);
215
218
  const { options } = query_string_1.default.parse(location.search);
@@ -239,7 +242,8 @@ class Web extends react_1.default.Component {
239
242
  setupObserver() {
240
243
  (0, mobx_1.observe)(this.props.dimensions, 'container', (data) => {
241
244
  if (data.newValue) {
242
- this.postMessageToParent('setWidth', data.newValue);
245
+ // is this necessary ?
246
+ (0, webchatEvents_1.postMessageToParent)('UI.RESIZE', data.newValue, this.config.chatId);
243
247
  }
244
248
  });
245
249
  }
@@ -267,8 +271,8 @@ class Web extends react_1.default.Component {
267
271
  [(_a = this.props.config) === null || _a === void 0 ? void 0 : _a.className]: !!((_b = this.props.config) === null || _b === void 0 ? void 0 : _b.className)
268
272
  });
269
273
  if (this.parentClass !== parentClass) {
270
- this.postMessageToParent('setClass', parentClass);
271
274
  this.parentClass = parentClass;
275
+ (0, webchatEvents_1.postMessageToParent)('UI.SET-CLASS', parentClass, this.config.chatId);
272
276
  }
273
277
  const stylesheet = this.props.config.stylesheet;
274
278
  const extraStylesheet = (_c = this.props.botInfo) === null || _c === void 0 ? void 0 : _c.extraStylesheet;
@@ -323,8 +327,10 @@ exports.default = (0, mobx_react_1.inject)(({ store }) => ({
323
327
  sendFeedback: store.sendFeedback,
324
328
  updateLastMessage: store.updateLastMessage,
325
329
  fetchConversation: store.fetchConversation,
330
+ createConversation: store.createConversation,
326
331
  setIntlProvider: store.setIntlProvider,
327
332
  setSocket: store.setSocket,
333
+ currentConversation: store.currentConversation,
328
334
  currentConversationId: store.currentConversationId,
329
335
  resetConversation: store.resetConversation
330
336
  }))((0, react_intl_1.injectIntl)((0, mobx_react_1.observer)(Web)));
@@ -13,6 +13,7 @@ declare class RootStore {
13
13
  private api;
14
14
  conversations: RecentConversation[];
15
15
  currentConversation?: CurrentConversation;
16
+ selectedMessageId?: string;
16
17
  botInfo: BotInfo;
17
18
  config: Config;
18
19
  preferredLanguage: string;
@@ -27,15 +28,22 @@ declare class RootStore {
27
28
  }, config?: Config);
28
29
  setIntlProvider(provider: IntlShape): void;
29
30
  setSocket(socket: BpSocket): void;
31
+ setSelectedMessage(messageId: string): void;
30
32
  get isConversationStarted(): boolean;
31
33
  get botName(): string;
32
34
  get hasBotInfoDescription(): boolean;
33
35
  get botAvatarUrl(): string | undefined;
36
+ get coverPictureUrl(): string | undefined;
37
+ get description(): string | undefined;
38
+ get website(): string | undefined;
39
+ get phoneNumber(): string | undefined;
40
+ get termsConditions(): string | undefined;
41
+ get privacyPolicy(): string | undefined;
42
+ get emailAddress(): string | undefined;
34
43
  get rtl(): boolean;
35
44
  get escapeHTML(): boolean;
36
45
  get currentMessages(): Message[];
37
46
  get currentConversationId(): uuid | undefined;
38
- postMessage(name: string, payload?: any): void;
39
47
  updateMessages(messages: Message[]): void;
40
48
  updateLastMessage(conversationId: string, message?: Message): void;
41
49
  clearMessages(): void;
@@ -51,7 +59,7 @@ declare class RootStore {
51
59
  /** Fetch the specified conversation ID, or try to fetch a valid one from the list */
52
60
  fetchConversation(convoId?: uuid): Promise<uuid | undefined>;
53
61
  /** Sends the specified message, or fetch the message in the composer */
54
- sendMessage(message?: string): Promise<void>;
62
+ sendMessage(textMessage?: string): Promise<void>;
55
63
  /** Sends an event to start conversation & hide the bot info page */
56
64
  startConversation(): Promise<void>;
57
65
  /** Creates a new conversation and switches to it */
@@ -61,7 +69,7 @@ declare class RootStore {
61
69
  sendFeedback(feedback: number, messageId: string): Promise<void>;
62
70
  downloadConversation(): Promise<void>;
63
71
  /** Sends an event or a message, depending on how the backend manages those types */
64
- sendData(data: any): Promise<void>;
72
+ sendData(data: any): Promise<Message | void>;
65
73
  /** Sends a message of type voice */
66
74
  sendVoiceMessage(voice: Buffer, ext: string): Promise<void>;
67
75
  /** Use this method to replace a value or add a new config */
@@ -69,7 +77,6 @@ declare class RootStore {
69
77
  /** This replaces all the configurations by this object */
70
78
  updateConfig(config: Config): void;
71
79
  private _applyConfig;
72
- publishConfigChanged(): void;
73
80
  updatePreferredLanguage(lang: string): void;
74
81
  /** Starts a timer to remove the typing animation when it's completed */
75
82
  private _startTypingTimer;
@@ -30,6 +30,7 @@ const main_1 = require("../main");
30
30
  const translations_1 = require("../translations");
31
31
  const utils_1 = require("../utils");
32
32
  const analytics_1 = require("../utils/analytics");
33
+ const webchatEvents_1 = require("../utils/webchatEvents");
33
34
  const composer_1 = __importDefault(require("./composer"));
34
35
  const view_1 = __importDefault(require("./view"));
35
36
  class RootStore {
@@ -50,6 +51,9 @@ class RootStore {
50
51
  setSocket(socket) {
51
52
  this.api = new api_1.default(socket);
52
53
  }
54
+ setSelectedMessage(messageId) {
55
+ this.selectedMessageId = messageId;
56
+ }
53
57
  get isConversationStarted() {
54
58
  var _a;
55
59
  return !!((_a = this.currentConversation) === null || _a === void 0 ? void 0 : _a.messages.length);
@@ -66,6 +70,34 @@ class RootStore {
66
70
  var _a, _b, _c;
67
71
  return ((_b = (_a = this.botInfo) === null || _a === void 0 ? void 0 : _a.details) === null || _b === void 0 ? void 0 : _b.avatarUrl) || ((_c = this.config) === null || _c === void 0 ? void 0 : _c.avatarUrl) || undefined;
68
72
  }
73
+ get coverPictureUrl() {
74
+ var _a, _b, _c, _d;
75
+ return (_c = (_b = (_a = this.botInfo) === null || _a === void 0 ? void 0 : _a.details) === null || _b === void 0 ? void 0 : _b.coverPictureUrl) !== null && _c !== void 0 ? _c : (_d = this.config) === null || _d === void 0 ? void 0 : _d.coverPictureUrl;
76
+ }
77
+ get description() {
78
+ var _a;
79
+ return (_a = this.config) === null || _a === void 0 ? void 0 : _a.botConversationDescription;
80
+ }
81
+ get website() {
82
+ var _a, _b, _c, _d;
83
+ return (_c = (_b = (_a = this.botInfo) === null || _a === void 0 ? void 0 : _a.details) === null || _b === void 0 ? void 0 : _b.website) !== null && _c !== void 0 ? _c : (_d = this.config) === null || _d === void 0 ? void 0 : _d.website;
84
+ }
85
+ get phoneNumber() {
86
+ var _a, _b, _c, _d;
87
+ return (_c = (_b = (_a = this.botInfo) === null || _a === void 0 ? void 0 : _a.details) === null || _b === void 0 ? void 0 : _b.phoneNumber) !== null && _c !== void 0 ? _c : (_d = this.config) === null || _d === void 0 ? void 0 : _d.phoneNumber;
88
+ }
89
+ get termsConditions() {
90
+ var _a, _b, _c, _d;
91
+ return (_c = (_b = (_a = this.botInfo) === null || _a === void 0 ? void 0 : _a.details) === null || _b === void 0 ? void 0 : _b.termsConditions) !== null && _c !== void 0 ? _c : (_d = this.config) === null || _d === void 0 ? void 0 : _d.termsConditions;
92
+ }
93
+ get privacyPolicy() {
94
+ var _a, _b, _c, _d;
95
+ return (_c = (_b = (_a = this.botInfo) === null || _a === void 0 ? void 0 : _a.details) === null || _b === void 0 ? void 0 : _b.privacyPolicy) !== null && _c !== void 0 ? _c : (_d = this.config) === null || _d === void 0 ? void 0 : _d.privacyPolicy;
96
+ }
97
+ get emailAddress() {
98
+ var _a, _b, _c, _d;
99
+ return (_c = (_b = (_a = this.botInfo) === null || _a === void 0 ? void 0 : _a.details) === null || _b === void 0 ? void 0 : _b.emailAddress) !== null && _c !== void 0 ? _c : (_d = this.config) === null || _d === void 0 ? void 0 : _d.emailAddress;
100
+ }
69
101
  get rtl() {
70
102
  return (0, translations_1.isRTLLocale)(this.preferredLanguage);
71
103
  }
@@ -81,10 +113,6 @@ class RootStore {
81
113
  var _a;
82
114
  return (_a = this.currentConversation) === null || _a === void 0 ? void 0 : _a.id;
83
115
  }
84
- postMessage(name, payload) {
85
- const chatId = this.config.chatId;
86
- window.parent.postMessage({ name, chatId, payload }, '*');
87
- }
88
116
  updateMessages(messages) {
89
117
  if (this.currentConversation) {
90
118
  this.currentConversation.messages = messages;
@@ -160,7 +188,7 @@ class RootStore {
160
188
  yield this.fetchConversation();
161
189
  (0, mobx_1.runInAction)('-> setInitialized', () => {
162
190
  this.isInitialized = true;
163
- this.postMessage('webchatReady');
191
+ (0, webchatEvents_1.postMessageToParent)('LIFECYCLE.READY', undefined, this.config.chatId);
164
192
  });
165
193
  }
166
194
  catch (err) {
@@ -229,23 +257,24 @@ class RootStore {
229
257
  });
230
258
  }
231
259
  /** Sends the specified message, or fetch the message in the composer */
232
- sendMessage(message) {
260
+ sendMessage(textMessage) {
233
261
  return __awaiter(this, void 0, void 0, function* () {
234
- if (message) {
235
- return this.sendData({ type: 'text', text: message });
236
- }
237
- const userMessage = this.composer.message;
238
- if (!userMessage || !userMessage.length) {
262
+ textMessage = textMessage || this.composer.message;
263
+ if (!textMessage) {
239
264
  return;
240
265
  }
241
266
  this.composer.updateMessage('');
242
267
  try {
243
- yield this.sendData({ type: 'text', text: userMessage });
268
+ const message = yield this.sendData({ type: 'text', text: textMessage });
244
269
  (0, analytics_1.trackMessage)('sent');
245
- this.composer.addMessageToHistory(userMessage);
270
+ if (message) {
271
+ (0, webchatEvents_1.postMessageToParent)('MESSAGE.SENT', message, this.config.chatId);
272
+ }
273
+ this.composer.addMessageToHistory(textMessage);
246
274
  }
247
275
  catch (e) {
248
- this.composer.updateMessage(userMessage);
276
+ this.composer.updateMessage(textMessage);
277
+ console.error('Webchat cloud not send message');
249
278
  throw e;
250
279
  }
251
280
  });
@@ -328,6 +357,7 @@ class RootStore {
328
357
  }
329
358
  const message = yield this.api.sendMessage(data, this.currentConversationId);
330
359
  this.updateLastMessage(this.currentConversationId, message);
360
+ return message;
331
361
  });
332
362
  }
333
363
  /** Sends a message of type voice */
@@ -364,10 +394,7 @@ class RootStore {
364
394
  const locale = (0, translations_1.getUserLocale)(this.config.locale);
365
395
  this.updateBotUILanguage(locale);
366
396
  document.documentElement.setAttribute('lang', locale);
367
- this.publishConfigChanged();
368
- }
369
- publishConfigChanged() {
370
- this.postMessage('configChanged', JSON.stringify(this.config, undefined, 2));
397
+ (0, webchatEvents_1.postMessageToParent)('CONFIG.SET', Object.assign({}, this.config), this.config.chatId);
371
398
  }
372
399
  updatePreferredLanguage(lang) {
373
400
  this.preferredLanguage = lang;
@@ -439,6 +466,9 @@ __decorate([
439
466
  __decorate([
440
467
  mobx_1.observable
441
468
  ], RootStore.prototype, "currentConversation", void 0);
469
+ __decorate([
470
+ mobx_1.observable
471
+ ], RootStore.prototype, "selectedMessageId", void 0);
442
472
  __decorate([
443
473
  mobx_1.observable
444
474
  ], RootStore.prototype, "botInfo", void 0);
@@ -463,6 +493,9 @@ __decorate([
463
493
  __decorate([
464
494
  mobx_1.action.bound
465
495
  ], RootStore.prototype, "setSocket", null);
496
+ __decorate([
497
+ mobx_1.action.bound
498
+ ], RootStore.prototype, "setSelectedMessage", null);
466
499
  __decorate([
467
500
  mobx_1.computed
468
501
  ], RootStore.prototype, "isConversationStarted", null);
@@ -487,9 +520,6 @@ __decorate([
487
520
  __decorate([
488
521
  mobx_1.computed
489
522
  ], RootStore.prototype, "currentConversationId", null);
490
- __decorate([
491
- mobx_1.action.bound
492
- ], RootStore.prototype, "postMessage", null);
493
523
  __decorate([
494
524
  mobx_1.action.bound
495
525
  ], RootStore.prototype, "updateMessages", null);
@@ -556,9 +586,6 @@ __decorate([
556
586
  __decorate([
557
587
  mobx_1.action.bound
558
588
  ], RootStore.prototype, "updateConfig", null);
559
- __decorate([
560
- mobx_1.action.bound
561
- ], RootStore.prototype, "publishConfigChanged", null);
562
589
  __decorate([
563
590
  mobx_1.action.bound
564
591
  ], RootStore.prototype, "updatePreferredLanguage", null);
@@ -12,6 +12,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
12
12
  const merge_1 = __importDefault(require("lodash/merge"));
13
13
  const mobx_1 = require("mobx");
14
14
  const constants_1 = __importDefault(require("../core/constants"));
15
+ const webchatEvents_1 = require("../utils/webchatEvents");
15
16
  class ViewStore {
16
17
  constructor(rootStore, fullscreen) {
17
18
  /** If false, probably embedded on a website or on the studio */
@@ -39,7 +40,9 @@ class ViewStore {
39
40
  return (_a = this.rootStore.config) === null || _a === void 0 ? void 0 : _a.showConversationsButton;
40
41
  }
41
42
  get showBotInfoButton() {
42
- return !this.isConversationsDisplayed && this.rootStore.botInfo && this.rootStore.botInfo.showBotInfoPage;
43
+ var _a;
44
+ return (!this.isConversationsDisplayed &&
45
+ (((_a = this.rootStore.botInfo) === null || _a === void 0 ? void 0 : _a.showBotInfoPage) || !!this.rootStore.config.showBotInfoPage));
43
46
  }
44
47
  get showDownloadButton() {
45
48
  return !this.isConversationsDisplayed && !this.isBotInfoDisplayed && this.rootStore.config.enableTranscriptDownload;
@@ -48,7 +51,8 @@ class ViewStore {
48
51
  return (!this.isConversationsDisplayed && !this.isBotInfoDisplayed && this.rootStore.config.enableConversationDeletion);
49
52
  }
50
53
  get showCloseButton() {
51
- return !this.isFullscreen;
54
+ var _a, _b;
55
+ return (_b = (!this.isFullscreen && ((_a = this.rootStore.config) === null || _a === void 0 ? void 0 : _a.showCloseButton))) !== null && _b !== void 0 ? _b : true;
52
56
  }
53
57
  get showWidgetButton() {
54
58
  var _a;
@@ -61,7 +65,8 @@ class ViewStore {
61
65
  return !this._isLoading && this.activeView;
62
66
  }
63
67
  get isBotInfoDisplayed() {
64
- return this._showBotInfo && this.rootStore.botInfo && this.rootStore.botInfo.showBotInfoPage;
68
+ var _a;
69
+ return this._showBotInfo && (((_a = this.rootStore.botInfo) === null || _a === void 0 ? void 0 : _a.showBotInfoPage) || !!this.rootStore.config.showBotInfoPage);
65
70
  }
66
71
  /** Returns the active transition for the side panel, like fade in or out */
67
72
  get sideTransition() {
@@ -111,7 +116,7 @@ class ViewStore {
111
116
  }
112
117
  setLoadingCompleted() {
113
118
  this._isLoading = false;
114
- this.rootStore.postMessage('webchatLoaded');
119
+ (0, webchatEvents_1.postMessageToParent)('LIFECYCLE.LOADED', undefined, this.rootStore.config.chatId);
115
120
  }
116
121
  showPoweredBy() {
117
122
  this.isPoweredByDisplayed = true;
@@ -157,7 +162,7 @@ class ViewStore {
157
162
  showChat() {
158
163
  if (this.disableAnimations) {
159
164
  this.activeView = 'side';
160
- this.rootStore.postMessage('webchatOpened');
165
+ (0, webchatEvents_1.postMessageToParent)('UI.OPENED', undefined, this.rootStore.config.chatId);
161
166
  return this._updateTransitions({ widgetTransition: undefined, sideTransition: 'none' });
162
167
  }
163
168
  this._updateTransitions({ widgetTransition: 'fadeOut' });
@@ -165,7 +170,7 @@ class ViewStore {
165
170
  this._updateTransitions({ sideTransition: 'fadeIn' });
166
171
  }, constants_1.default.ANIMATION_DURATION + 10);
167
172
  this._endAnimation('side');
168
- this.rootStore.postMessage('webchatOpened');
173
+ (0, webchatEvents_1.postMessageToParent)('UI.OPENED', undefined, this.rootStore.config.chatId);
169
174
  }
170
175
  hideChat() {
171
176
  if (this.isFullscreen) {
@@ -173,7 +178,7 @@ class ViewStore {
173
178
  }
174
179
  if (this.disableAnimations) {
175
180
  this.activeView = 'widget';
176
- this.rootStore.postMessage('webchatClosed');
181
+ (0, webchatEvents_1.postMessageToParent)('UI.CLOSED', undefined, this.rootStore.config.chatId);
177
182
  return this._updateTransitions({ widgetTransition: undefined, sideTransition: undefined });
178
183
  }
179
184
  this._updateTransitions({ sideTransition: 'fadeOut' });
@@ -183,7 +188,7 @@ class ViewStore {
183
188
  }, constants_1.default.ANIMATION_DURATION + 10);
184
189
  }
185
190
  this._endAnimation('widget');
186
- this.rootStore.postMessage('webchatClosed');
191
+ (0, webchatEvents_1.postMessageToParent)('UI.CLOSED', undefined, this.rootStore.config.chatId);
187
192
  }
188
193
  _endAnimation(finalView) {
189
194
  setTimeout(() => {
package/dist/typings.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  /// <reference types="react" />
2
+ import { UserCredentials } from '@botpress/messaging-socket';
2
3
  import { RootStore } from './store';
3
4
  import { BPStorage } from './utils/storage';
4
5
  declare global {
@@ -35,7 +36,7 @@ export declare namespace Renderer {
35
36
  isLastMessage?: boolean;
36
37
  sentOn?: Date;
37
38
  inlineFeedback?: any;
38
- onSendData?: (data: any) => Promise<void>;
39
+ onSendData?: (data: any) => Promise<Message | void>;
39
40
  onFileUpload?: (label: string, payload: any, file: File) => Promise<void>;
40
41
  /** Allows to autoplay voice messages coming from the bot */
41
42
  onAudioEnded?: () => void;
@@ -135,11 +136,27 @@ export interface StudioConnector {
135
136
  getModuleInjector: any;
136
137
  loadModuleView: any;
137
138
  }
139
+ export declare type WebchatEventType = 'LIFECYCLE.LOADED' | 'LIFECYCLE.READY' | 'UI.OPENED' | 'UI.CLOSED' | 'UI.RESIZE' | 'UI.SET-CLASS' | 'CONFIG.SET' | 'MESSAGE.SENT' | 'MESSAGE.RECEIVED' | 'MESSAGE.SELECTED' | 'USER.CONNECTED';
140
+ export interface WebchatEvent {
141
+ type: WebchatEventType;
142
+ value: any;
143
+ chatId: string;
144
+ }
138
145
  export interface Config {
139
146
  /** Url of the messaging server */
140
147
  messagingUrl: string;
141
148
  /** Id of your messaging client */
142
149
  clientId: string;
150
+ /**
151
+ * Refers to a specific webchat reference in parent window. Useful when using multiple chat window
152
+ * @default 'bp-web-widget'
153
+ */
154
+ chatId: string;
155
+ /**
156
+ * Url where the webchat bundle is hosted
157
+ * @default: '/'
158
+ */
159
+ hostUrl?: string;
143
160
  /**
144
161
  * Url of the Media File Service where we fetch the bot info
145
162
  * @default ''
@@ -250,22 +267,12 @@ export interface Config {
250
267
  * Experimental: expose the store to the parent frame for more control on the webchat's behavior
251
268
  * @default false
252
269
  */
253
- exposeStore?: boolean;
254
- /**
255
- * If true, Websocket is created when the Webchat is opened. Bot cannot be proactive.
256
- * @default false
257
- */
258
270
  lazySocket?: boolean;
259
271
  /**
260
272
  * If true, chat will no longer play the notification sound for new messages.
261
273
  * @default false
262
274
  */
263
275
  disableNotificationSound?: boolean;
264
- /**
265
- * Refers to a specific webchat reference in parent window. Useful when using multiple chat window
266
- * @default ''
267
- */
268
- chatId?: string;
269
276
  /**
270
277
  * CSS class to be applied to iframe
271
278
  * @default ''
@@ -280,10 +287,39 @@ export interface Config {
280
287
  /**
281
288
  * Allows setting a custom user id
282
289
  */
283
- customUser?: {
284
- userId: string;
285
- userToken: string;
286
- };
290
+ customUser?: UserCredentials;
291
+ /**
292
+ * Displays the bot's website in the conversation page
293
+ */
294
+ website?: string;
295
+ /**
296
+ * Displays the bot's contact phone number in the conversation page
297
+ */
298
+ phoneNumber?: string;
299
+ /**
300
+ * Displays the bot's terms of service in the conversation page
301
+ */
302
+ termsConditions?: string;
303
+ /**
304
+ * Displays the bot's privacy policy in the conversation page
305
+ */
306
+ privacyPolicy?: string;
307
+ /**
308
+ * Displays the bot's email address in the conversation page
309
+ */
310
+ emailAddress?: string;
311
+ /**
312
+ * Displays the bot's cover picture in the conversation page
313
+ */
314
+ coverPictureUrl?: string;
315
+ /**
316
+ * Enables the bot's information page in the webchat
317
+ */
318
+ showBotInfoPage?: boolean;
319
+ /**
320
+ * Display's the webchat close button when the webchat is opened
321
+ */
322
+ showCloseButton?: boolean;
287
323
  }
288
324
  export interface BotDetails {
289
325
  website?: string;
@@ -0,0 +1,2 @@
1
+ import { WebchatEventType } from '../typings';
2
+ export declare const postMessageToParent: (type: WebchatEventType, value: any, chatId: string) => void;
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.postMessageToParent = void 0;
7
+ const cloneDeep_1 = __importDefault(require("lodash/cloneDeep"));
8
+ const postMessageToParent = (type, value, chatId) => {
9
+ var _a;
10
+ //cloneDeep necessary because of potentially nested mobx proxy object isn't serializable
11
+ const evt = { type, value: (0, cloneDeep_1.default)(value), chatId };
12
+ (_a = window.parent) === null || _a === void 0 ? void 0 : _a.postMessage(evt, '*');
13
+ };
14
+ exports.postMessageToParent = postMessageToParent;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botpress/webchat",
3
- "version": "0.4.0",
3
+ "version": "0.5.1",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "source": "src/index.tsx",
@@ -29,7 +29,7 @@
29
29
  },
30
30
  "dependencies": {
31
31
  "@blueprintjs/core": "^3.23.1",
32
- "@botpress/messaging-components": "0.4.0",
32
+ "@botpress/messaging-components": "0.4.3",
33
33
  "@botpress/messaging-socket": "1.2.0",
34
34
  "@formatjs/intl-pluralrules": "^4.1.6",
35
35
  "@formatjs/intl-utils": "^3.8.4",
@@ -48,6 +48,7 @@
48
48
  "react-dom": "^17.0.2",
49
49
  "react-ga": "^2.7.0",
50
50
  "react-intl": "^3.12.1",
51
+ "react-scroll-to-bottom": "^4.2.0",
51
52
  "snarkdown": "^2.0.0",
52
53
  "uuid": "^8.3.2"
53
54
  }