Skip to content

Commit 25ffb4f

Browse files
authored
Carousel Messages and Quick Replies (#36)
* WIP new message controller * update code * only register scroll when carousel is connected * extract a message builder function and attach the card attachment to the new message * build a message object * attach the new messageg * remove width and height attributes from the attachment before inserting * update key names * update docs * update payload structure for quick replies * add tests * add more tests and respond to setId * properly handle carousel messages * update code * add reply after typing indicator when visible * update tests
1 parent d056da3 commit 25ffb4f

File tree

13 files changed

+1890
-27
lines changed

13 files changed

+1890
-27
lines changed

__tests__/controllers/message_controller_test.js

Lines changed: 621 additions & 0 deletions
Large diffs are not rendered by default.

__tests__/controllers/webchat_controller_test.js

Lines changed: 579 additions & 0 deletions
Large diffs are not rendered by default.

dist/hellotext.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/webchat.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,20 @@ The webchat emits the following events which can be listened to, to add an event
102102
}
103103
```
104104

105+
The payload may contain additional information about the product card clicked from a carousel message, the following
106+
is an example of a payload of a card click
107+
108+
```javascript
109+
{
110+
id: 'xxxxxx'
111+
body: 'The message the client sent',
112+
attachments: [], // An array of File objects associated with the card
113+
replied_to: 'xxxx', // The ID of the message that was replied to by the button click.
114+
product: 'xxxx', // The ID of the product associated with the cart. You can fetch information about the product in https://www.hellotext.com/api#products
115+
button: 'xxxx' // The ID of the button that was clicked.
116+
}
117+
```
118+
105119
- `webchat:message:received` - Emitted when a message is received by the webchat from Hellotext. The message is passed as an argument to the callback, containing the following properties
106120

107121
```javascript
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
"use strict";
2+
3+
Object.defineProperty(exports, "__esModule", {
4+
value: true
5+
});
6+
exports.default = void 0;
7+
var _stimulus = require("@hotwired/stimulus");
8+
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
9+
function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, _toPropertyKey(descriptor.key), descriptor); } }
10+
function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; }
11+
function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
12+
function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); }
13+
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); Object.defineProperty(subClass, "prototype", { writable: false }); if (superClass) _setPrototypeOf(subClass, superClass); }
14+
function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf ? Object.setPrototypeOf.bind() : function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }
15+
function _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; }
16+
function _possibleConstructorReturn(self, call) { if (call && (typeof call === "object" || typeof call === "function")) { return call; } else if (call !== void 0) { throw new TypeError("Derived constructors may only return object or undefined"); } return _assertThisInitialized(self); }
17+
function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; }
18+
function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); return true; } catch (e) { return false; } }
19+
function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf.bind() : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }
20+
let _default = /*#__PURE__*/function (_Controller) {
21+
_inherits(_default, _Controller);
22+
var _super = _createSuper(_default);
23+
function _default() {
24+
_classCallCheck(this, _default);
25+
return _super.apply(this, arguments);
26+
}
27+
_createClass(_default, [{
28+
key: "connect",
29+
value: function connect() {
30+
this.updateFades();
31+
}
32+
}, {
33+
key: "setId",
34+
value: function setId({
35+
detail: id
36+
}) {
37+
this.idValue = id;
38+
this.element.id = id;
39+
}
40+
}, {
41+
key: "onScroll",
42+
value: function onScroll() {
43+
this.updateFades();
44+
}
45+
}, {
46+
key: "quickReply",
47+
value: function quickReply({
48+
currentTarget
49+
}) {
50+
const card = currentTarget.closest('[data-hellotext--message-target="carouselCard"]');
51+
this.dispatch('quickReply', {
52+
detail: {
53+
id: this.idValue,
54+
product: card.dataset.id,
55+
buttonId: currentTarget.dataset.id,
56+
body: currentTarget.dataset.text,
57+
cardElement: card
58+
}
59+
});
60+
}
61+
}, {
62+
key: "moveToLeft",
63+
value: function moveToLeft() {
64+
if (!this.hasCarouselContainerTarget) return;
65+
const scrollAmount = this.getScrollAmount();
66+
this.carouselContainerTarget.scrollBy({
67+
left: -scrollAmount,
68+
behavior: 'smooth'
69+
});
70+
}
71+
}, {
72+
key: "moveToRight",
73+
value: function moveToRight() {
74+
if (!this.hasCarouselContainerTarget) return;
75+
const scrollAmount = this.getScrollAmount();
76+
this.carouselContainerTarget.scrollBy({
77+
left: scrollAmount,
78+
behavior: 'smooth'
79+
});
80+
}
81+
}, {
82+
key: "getScrollAmount",
83+
value: function getScrollAmount() {
84+
// Get the actual card width from DOM
85+
const firstCard = this.carouselContainerTarget.querySelector('.message__carousel_card');
86+
if (!firstCard) {
87+
return 280; // Fallback to default desktop card width
88+
}
89+
90+
const cardWidth = firstCard.offsetWidth;
91+
const gap = 16; // gap-x-4 = 1rem = 16px
92+
93+
return cardWidth + gap;
94+
}
95+
}, {
96+
key: "updateFades",
97+
value: function updateFades() {
98+
if (!this.hasCarouselContainerTarget) return;
99+
const scrollLeft = this.carouselContainerTarget.scrollLeft;
100+
const maxScroll = this.carouselContainerTarget.scrollWidth - this.carouselContainerTarget.clientWidth;
101+
102+
// Show left fade if scrolled past start
103+
if (scrollLeft > 0) {
104+
this.leftFadeTarget.classList.remove('hidden');
105+
} else {
106+
this.leftFadeTarget.classList.add('hidden');
107+
}
108+
109+
// Show right fade if not at end
110+
if (scrollLeft < maxScroll - 1) {
111+
// -1 for rounding errors
112+
this.rightFadeTarget.classList.remove('hidden');
113+
} else {
114+
this.rightFadeTarget.classList.add('hidden');
115+
}
116+
}
117+
}]);
118+
return _default;
119+
}(_stimulus.Controller);
120+
exports.default = _default;
121+
_default.values = {
122+
id: String
123+
};
124+
_default.targets = ['carouselContainer', 'leftFade', 'rightFade', 'carouselCard'];
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
2+
function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, _toPropertyKey(descriptor.key), descriptor); } }
3+
function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); Object.defineProperty(Constructor, "prototype", { writable: false }); return Constructor; }
4+
function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
5+
function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); }
6+
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); Object.defineProperty(subClass, "prototype", { writable: false }); if (superClass) _setPrototypeOf(subClass, superClass); }
7+
function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf ? Object.setPrototypeOf.bind() : function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); }
8+
function _createSuper(Derived) { var hasNativeReflectConstruct = _isNativeReflectConstruct(); return function _createSuperInternal() { var Super = _getPrototypeOf(Derived), result; if (hasNativeReflectConstruct) { var NewTarget = _getPrototypeOf(this).constructor; result = Reflect.construct(Super, arguments, NewTarget); } else { result = Super.apply(this, arguments); } return _possibleConstructorReturn(this, result); }; }
9+
function _possibleConstructorReturn(self, call) { if (call && (typeof call === "object" || typeof call === "function")) { return call; } else if (call !== void 0) { throw new TypeError("Derived constructors may only return object or undefined"); } return _assertThisInitialized(self); }
10+
function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; }
11+
function _isNativeReflectConstruct() { if (typeof Reflect === "undefined" || !Reflect.construct) return false; if (Reflect.construct.sham) return false; if (typeof Proxy === "function") return true; try { Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); return true; } catch (e) { return false; } }
12+
function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf.bind() : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); }
13+
import { Controller } from '@hotwired/stimulus';
14+
var _default = /*#__PURE__*/function (_Controller) {
15+
_inherits(_default, _Controller);
16+
var _super = _createSuper(_default);
17+
function _default() {
18+
_classCallCheck(this, _default);
19+
return _super.apply(this, arguments);
20+
}
21+
_createClass(_default, [{
22+
key: "connect",
23+
value: function connect() {
24+
this.updateFades();
25+
}
26+
}, {
27+
key: "setId",
28+
value: function setId(_ref) {
29+
var {
30+
detail: id
31+
} = _ref;
32+
this.idValue = id;
33+
this.element.id = id;
34+
}
35+
}, {
36+
key: "onScroll",
37+
value: function onScroll() {
38+
this.updateFades();
39+
}
40+
}, {
41+
key: "quickReply",
42+
value: function quickReply(_ref2) {
43+
var {
44+
currentTarget
45+
} = _ref2;
46+
var card = currentTarget.closest('[data-hellotext--message-target="carouselCard"]');
47+
this.dispatch('quickReply', {
48+
detail: {
49+
id: this.idValue,
50+
product: card.dataset.id,
51+
buttonId: currentTarget.dataset.id,
52+
body: currentTarget.dataset.text,
53+
cardElement: card
54+
}
55+
});
56+
}
57+
}, {
58+
key: "moveToLeft",
59+
value: function moveToLeft() {
60+
if (!this.hasCarouselContainerTarget) return;
61+
var scrollAmount = this.getScrollAmount();
62+
this.carouselContainerTarget.scrollBy({
63+
left: -scrollAmount,
64+
behavior: 'smooth'
65+
});
66+
}
67+
}, {
68+
key: "moveToRight",
69+
value: function moveToRight() {
70+
if (!this.hasCarouselContainerTarget) return;
71+
var scrollAmount = this.getScrollAmount();
72+
this.carouselContainerTarget.scrollBy({
73+
left: scrollAmount,
74+
behavior: 'smooth'
75+
});
76+
}
77+
}, {
78+
key: "getScrollAmount",
79+
value: function getScrollAmount() {
80+
// Get the actual card width from DOM
81+
var firstCard = this.carouselContainerTarget.querySelector('.message__carousel_card');
82+
if (!firstCard) {
83+
return 280; // Fallback to default desktop card width
84+
}
85+
86+
var cardWidth = firstCard.offsetWidth;
87+
var gap = 16; // gap-x-4 = 1rem = 16px
88+
89+
return cardWidth + gap;
90+
}
91+
}, {
92+
key: "updateFades",
93+
value: function updateFades() {
94+
if (!this.hasCarouselContainerTarget) return;
95+
var scrollLeft = this.carouselContainerTarget.scrollLeft;
96+
var maxScroll = this.carouselContainerTarget.scrollWidth - this.carouselContainerTarget.clientWidth;
97+
98+
// Show left fade if scrolled past start
99+
if (scrollLeft > 0) {
100+
this.leftFadeTarget.classList.remove('hidden');
101+
} else {
102+
this.leftFadeTarget.classList.add('hidden');
103+
}
104+
105+
// Show right fade if not at end
106+
if (scrollLeft < maxScroll - 1) {
107+
// -1 for rounding errors
108+
this.rightFadeTarget.classList.remove('hidden');
109+
} else {
110+
this.rightFadeTarget.classList.add('hidden');
111+
}
112+
}
113+
}]);
114+
return _default;
115+
}(Controller);
116+
_default.values = {
117+
id: String
118+
};
119+
_default.targets = ['carouselContainer', 'leftFade', 'rightFade', 'carouselCard'];
120+
export { _default as default };

0 commit comments

Comments
 (0)