@financial-times/qanda-ui 0.0.1-beta.25 → 0.0.1-beta.27
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 +1 -1
- package/dist/cjs/components/Calendar/Apps.js +5 -2
- package/dist/cjs/components/Calendar/index.js +2 -1
- package/dist/cjs/components/ErrorMessage/index.js +0 -3
- package/dist/cjs/components/Expander/index.js +2 -1
- package/dist/cjs/components/FloatingActionBar/index.js +15 -12
- package/dist/cjs/components/QandaBlock/index.js +3 -2
- package/dist/cjs/components/QandaContainer.js +3 -11
- package/dist/cjs/components/QuestionForm/index.js +5 -4
- package/dist/cjs/components/QuestionForm/messagesByUserType.js +5 -4
- package/dist/cjs/components/Stream/index.js +1 -1
- package/dist/cjs/components/Survey/index.js +2 -30
- package/dist/cjs/services/comments-api.js +43 -8
- package/dist/cjs/services/listeners-middleware.js +103 -0
- package/dist/cjs/store/index.js +2 -0
- package/dist/cjs/store/stream.js +7 -2
- package/dist/cjs/utils/tracking.js +25 -2
- package/dist/esm/components/Calendar/Apps.js +5 -2
- package/dist/esm/components/Calendar/index.js +2 -1
- package/dist/esm/components/ErrorMessage/index.js +0 -3
- package/dist/esm/components/Expander/index.js +2 -1
- package/dist/esm/components/FloatingActionBar/index.js +15 -12
- package/dist/esm/components/QandaBlock/index.js +3 -2
- package/dist/esm/components/QandaContainer.js +3 -11
- package/dist/esm/components/QuestionForm/index.js +6 -5
- package/dist/esm/components/QuestionForm/messagesByUserType.js +5 -4
- package/dist/esm/components/Stream/index.js +1 -1
- package/dist/esm/components/Survey/index.js +2 -30
- package/dist/esm/services/comments-api.js +41 -6
- package/dist/esm/services/listeners-middleware.js +100 -0
- package/dist/esm/store/index.js +2 -0
- package/dist/esm/store/stream.js +5 -1
- package/dist/esm/utils/tracking.js +23 -1
- package/dist/qanda.scss +8 -7
- package/dist/types/components/FloatingActionBar/index.d.ts +2 -2
- package/dist/types/components/QandaBlock/index.d.ts +1 -1
- package/dist/types/components/Survey/index.d.ts +2 -1
- package/dist/types/services/comments-api.d.ts +13 -1
- package/dist/types/services/listeners-middleware.d.ts +11 -0
- package/dist/types/store/index.d.ts +41 -1
- package/dist/types/store/stream.d.ts +2 -1
- package/dist/types/utils/tracking.d.ts +4 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const jsx_runtime_1 = require("preact/jsx-runtime");
|
|
4
4
|
const qanda_1 = require("../../config/qanda");
|
|
5
|
+
const tracking_1 = require("../../utils/tracking");
|
|
5
6
|
function formatDateForIcs(date) {
|
|
6
7
|
return date.replace(/-|:|\.\d+/g, '');
|
|
7
8
|
}
|
|
@@ -11,7 +12,9 @@ function createIcsFile(title, startDateTime, endDateTime, url) {
|
|
|
11
12
|
`.trim();
|
|
12
13
|
return URL.createObjectURL(new Blob([icsContent], { type: 'title/calendar' }));
|
|
13
14
|
}
|
|
14
|
-
const CalendarLink = ({ href, children, }) =>
|
|
15
|
+
const CalendarLink = ({ href, children, }) => {
|
|
16
|
+
return ((0, jsx_runtime_1.jsx)("a", { href: href, target: "_blank", rel: "noopener noreferrer", role: "menuitem", "data-trackable": (0, tracking_1.trackingNS)('calendar-link'), className: "live-qa-calendar-list__item__link", children: children }));
|
|
17
|
+
};
|
|
15
18
|
function itemChildren(name, icon) {
|
|
16
19
|
return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("span", { className: "live-qa-calendar-list__item__icon", children: icon }), (0, jsx_runtime_1.jsx)("span", { className: "live-qa-calendar-list__item__text", children: name })] }));
|
|
17
20
|
}
|
|
@@ -44,6 +47,6 @@ exports.default = apps.map(({ name, icon, getLink, isDownload }) => ({
|
|
|
44
47
|
icon,
|
|
45
48
|
link: ({ title, startDateTime, endDateTime, url, }) => {
|
|
46
49
|
const href = getLink({ title, startDateTime, endDateTime, url });
|
|
47
|
-
return isDownload ? ((0, jsx_runtime_1.jsx)("a", { href: href, download: "event.ics", role: "menuitem", "data-trackable":
|
|
50
|
+
return isDownload ? ((0, jsx_runtime_1.jsx)("a", { href: href, download: "event.ics", role: "menuitem", "data-trackable": (0, tracking_1.trackingNS)('calendar-link-download'), className: "live-qa-calendar-list__item__link", children: itemChildren(name, icon) })) : ((0, jsx_runtime_1.jsx)(CalendarLink, { href: href, children: itemChildren(name, icon) }));
|
|
48
51
|
},
|
|
49
52
|
}));
|
|
@@ -9,6 +9,7 @@ const react_1 = require("react");
|
|
|
9
9
|
const CalendarList_1 = __importDefault(require("./CalendarList"));
|
|
10
10
|
const comments_api_1 = require("../../services/comments-api");
|
|
11
11
|
const QandaProvider_1 = require("../QandaProvider");
|
|
12
|
+
const tracking_1 = require("../../utils/tracking");
|
|
12
13
|
function Calendar() {
|
|
13
14
|
const [dropDownVisible, setDropDownVisible] = (0, react_1.useState)(false);
|
|
14
15
|
const containerRef = (0, react_1.useRef)(null);
|
|
@@ -41,7 +42,7 @@ function Calendar() {
|
|
|
41
42
|
'aria-controls': 'menu',
|
|
42
43
|
id: 'live-qa-calendar-button-menu',
|
|
43
44
|
'data-testid': 'live-qa-calendar-button',
|
|
44
|
-
'data-trackable': '
|
|
45
|
+
'data-trackable': (0, tracking_1.trackingNS)('add-to-calendar-button'),
|
|
45
46
|
}, onClick: () => setDropDownVisible(!dropDownVisible) }), dropDownVisible && (0, jsx_runtime_1.jsx)(CalendarList_1.default, {})] })) }));
|
|
46
47
|
}
|
|
47
48
|
exports.default = Calendar;
|
|
@@ -42,10 +42,7 @@ function ErrorMessage({ error, storyId, errorMapping = {}, operation = 'not-spec
|
|
|
42
42
|
// Critical operation are tracked by the reliability service using AWS RUM
|
|
43
43
|
// We still log this to monitor untracked messages to the users.
|
|
44
44
|
(0, tracking_1.trackEvent)({
|
|
45
|
-
category: 'component',
|
|
46
|
-
action: 'act',
|
|
47
45
|
content: {
|
|
48
|
-
asset_type: 'article',
|
|
49
46
|
uuid: storyId,
|
|
50
47
|
},
|
|
51
48
|
trigger_action: operation,
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
const jsx_runtime_1 = require("preact/jsx-runtime");
|
|
4
4
|
const react_1 = require("react");
|
|
5
5
|
const o3_button_1 = require("@financial-times/o3-button");
|
|
6
|
+
const tracking_1 = require("../../utils/tracking");
|
|
6
7
|
// Generic debounce function
|
|
7
8
|
function debounce(func, delay) {
|
|
8
9
|
let timerId;
|
|
@@ -59,7 +60,7 @@ function Expander({ className = '', children = [], expandLabel = 'Expand', colla
|
|
|
59
60
|
const componentClassName = `${expandCollapseClass} ${className}`;
|
|
60
61
|
return ((0, jsx_runtime_1.jsxs)("div", { className: `${componentClassName}`, children: [(0, jsx_runtime_1.jsx)("div", { className: `expander__text ${requiresExpander ? 'expander__text--multiline' : ''}`, id: expanderContentID, ref: textRef, children: (0, jsx_runtime_1.jsx)(EnsureParagraph, { children: children }) }), requiresExpander && ((0, jsx_runtime_1.jsx)(o3_button_1.Button, { label: label, type: "ghost", theme: "neutral", icon: state === 'expanded' ? 'chevron-up' : 'chevron-down', iconOnly: false, iconPosition: "end", attributes: {
|
|
61
62
|
className: 'expander__link',
|
|
62
|
-
'data-trackable':
|
|
63
|
+
'data-trackable': (0, tracking_1.trackingNS)('expander'),
|
|
63
64
|
'data-trackable-context-action': label,
|
|
64
65
|
'aria-expanded': state === 'expanded' ? true : false,
|
|
65
66
|
'aria-controls': expanderContentID,
|
|
@@ -10,10 +10,11 @@ const comments_api_1 = require("../../services/comments-api");
|
|
|
10
10
|
const updateComments_1 = require("../../store/updateComments");
|
|
11
11
|
const react_1 = require("react");
|
|
12
12
|
const QandaProvider_1 = require("../QandaProvider");
|
|
13
|
+
const tracking_1 = require("../../utils/tracking");
|
|
13
14
|
function LiveIndicator() {
|
|
14
15
|
return ((0, jsx_runtime_1.jsx)("div", { className: "o-labels-indicator--live o3-type-label floating-action-bar__live-container", children: (0, jsx_runtime_1.jsx)("span", { className: "o-labels-indicator__status floating-action-bar__live-label", children: "Live" }) }));
|
|
15
16
|
}
|
|
16
|
-
function FloatingActionBar({
|
|
17
|
+
function FloatingActionBar({ isLive = true }) {
|
|
17
18
|
const dispatch = (0, react_redux_1.useDispatch)();
|
|
18
19
|
const isMobile = (0, useIsMobile_1.useIsMobile)();
|
|
19
20
|
const { storyId, useStaging, commentsAPIUrl } = (0, react_1.useContext)(QandaProvider_1.QandaContext);
|
|
@@ -21,39 +22,41 @@ function FloatingActionBar({ showLiveIndicator = true, }) {
|
|
|
21
22
|
storyId,
|
|
22
23
|
useStaging,
|
|
23
24
|
commentsAPIUrl,
|
|
25
|
+
}, {
|
|
26
|
+
skip: !isLive,
|
|
24
27
|
});
|
|
25
28
|
const handleShowUpdates = () => {
|
|
26
29
|
(0, updateComments_1.updateComments)(dispatch, updatedComments, storyId, useStaging, commentsAPIUrl);
|
|
27
|
-
// Scrolling to start of qanda
|
|
28
|
-
const element = document.getElementById('
|
|
30
|
+
// Scrolling to start of qanda stream title
|
|
31
|
+
const element = document.getElementById('qanda__stream-title');
|
|
29
32
|
if (element) {
|
|
30
33
|
element.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
31
34
|
}
|
|
32
35
|
};
|
|
33
36
|
const showExpertView = (0, react_redux_1.useSelector)((state) => state.expert.expertView);
|
|
34
37
|
const userConfig = {
|
|
35
|
-
|
|
38
|
+
description: 'Ask a question',
|
|
36
39
|
testIdSuffix: 'ask-a-question-button',
|
|
37
40
|
onClick: () => dispatch((0, questionSlice_1.openForm)()),
|
|
38
41
|
dataTrackable: 'open_question_form',
|
|
39
42
|
};
|
|
40
43
|
const expertConfig = {
|
|
41
|
-
|
|
44
|
+
description: 'Answer Q&A',
|
|
42
45
|
testIdSuffix: 'answer-a-qanda-button',
|
|
43
46
|
onClick: () => dispatch((0, expertSlice_1.openExpertForm)()),
|
|
44
47
|
dataTrackable: 'open_expert_drawer',
|
|
45
48
|
};
|
|
46
49
|
const view = showExpertView ? expertConfig : userConfig;
|
|
47
|
-
return ((0, jsx_runtime_1.jsxs)("div", { className: "floating-action-bar__container", "data-trackable":
|
|
48
|
-
ariaLabel: view.
|
|
49
|
-
'data-trackable': view.dataTrackable,
|
|
50
|
-
'data-trackable-context-action': view.
|
|
50
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: "floating-action-bar__container", "data-trackable": (0, tracking_1.trackingNS)('floating-action-bar'), children: [(0, jsx_runtime_1.jsx)("div", { className: "floating-action-bar__fade-overlay" }), (0, jsx_runtime_1.jsxs)("div", { className: "floating-action-bar", "data-testid": "floating-action-bar", children: [isLive && (0, jsx_runtime_1.jsx)(LiveIndicator, {}), (0, jsx_runtime_1.jsxs)("div", { className: "floating-action-bar__buttons", children: [(0, jsx_runtime_1.jsx)(o3_button_1.Button, { attributes: {
|
|
51
|
+
ariaLabel: view.description,
|
|
52
|
+
'data-trackable': (0, tracking_1.trackingNS)(view.dataTrackable),
|
|
53
|
+
'data-trackable-context-action': view.description,
|
|
51
54
|
'data-testid': `floating-action-bar__${view.testIdSuffix}`,
|
|
52
|
-
}, label: view.
|
|
55
|
+
}, label: view.description, type: "ghost", theme: "mono", icon: "edit", iconOnly: isMobile, onClick: view.onClick }), (0, jsx_runtime_1.jsx)(o3_button_1.Button, { attributes: {
|
|
53
56
|
ariaLabel: 'Back to top',
|
|
54
|
-
'data-trackable': 'back_to_top',
|
|
57
|
+
'data-trackable': (0, tracking_1.trackingNS)('back_to_top'),
|
|
55
58
|
'data-trackable-context-action': 'Back to top',
|
|
56
59
|
'data-testid': 'floating-action-bar__back-to-top-button',
|
|
57
|
-
}, label: "Back to top", type: "ghost", theme: "mono", icon: "scroll-to", iconOnly: isMobile, onClick: handleShowUpdates })] }), updatedComments?.length > 0 && ((0, jsx_runtime_1.jsx)("div", { className: "floating-action-bar__updates-counter o3-type-detail", children: updatedComments.length }))] })] }));
|
|
60
|
+
}, label: "Back to top", type: "ghost", theme: "mono", icon: "scroll-to", iconOnly: isMobile, onClick: handleShowUpdates })] }), updatedComments?.length > 0 && ((0, jsx_runtime_1.jsx)("div", { className: "floating-action-bar__updates-counter o3-type-detail", "aria-label": `${updatedComments.length} new ${updatedComments.length === 1 ? 'update' : 'updates'} available`, role: "status", "aria-live": "polite", children: updatedComments.length }))] })] }));
|
|
58
61
|
}
|
|
59
62
|
exports.default = FloatingActionBar;
|
|
@@ -10,6 +10,7 @@ require("../Expander/index");
|
|
|
10
10
|
const index_1 = __importDefault(require("../Expander/index"));
|
|
11
11
|
const Author_1 = __importDefault(require("../Author"));
|
|
12
12
|
const react_1 = require("react");
|
|
13
|
+
const tracking_1 = require("../../utils/tracking");
|
|
13
14
|
function renderTime(publishedDate) {
|
|
14
15
|
const date = new Date(publishedDate);
|
|
15
16
|
return `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
|
|
@@ -23,7 +24,7 @@ function interpolateColor(factor, colorStart = 'ccc1b7', colorEnd = '4d4845', al
|
|
|
23
24
|
const alpha = alphaStart + factor * (alphaEnd - alphaStart);
|
|
24
25
|
return `rgba(${rgb.join(', ')}, ${alpha})`;
|
|
25
26
|
}
|
|
26
|
-
function QandaBlock({ id, body, children, byline,
|
|
27
|
+
function QandaBlock({ id, body, children, byline, summary, showAnswerTime = false, }) {
|
|
27
28
|
(0, react_1.useEffect)(() => {
|
|
28
29
|
let currentBubble = null;
|
|
29
30
|
const factorRef = { current: 0 };
|
|
@@ -73,6 +74,6 @@ function QandaBlock({ id, body, children, byline, subtitle, showAnswerTime = fal
|
|
|
73
74
|
observer.disconnect();
|
|
74
75
|
};
|
|
75
76
|
}, []);
|
|
76
|
-
return ((0, jsx_runtime_1.jsxs)("div", { className: "qanda-block", children: [(0, jsx_runtime_1.jsxs)("div", { className: "qanda-block__comment qanda-block__question", id: id, "data-testid": `qanda-block__question-${id}`, "data-trackable":
|
|
77
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: "qanda-block", children: [(0, jsx_runtime_1.jsxs)("div", { className: "qanda-block__comment qanda-block__question", id: id, "data-testid": `qanda-block__question-${id}`, "data-trackable": (0, tracking_1.trackingNS)('question'), children: [summary && ((0, jsx_runtime_1.jsx)("div", { className: "o3-type-headline-sm qanda-block__question-summary", children: summary })), (0, jsx_runtime_1.jsx)("div", { className: "o3-type-body-highlight qanda-block__comment-byline", children: (0, jsx_runtime_1.jsx)(Byline_1.default, { structuredContent: byline, showEditedBy: false }) }), (0, jsx_runtime_1.jsx)("div", { className: "o3-type-body-base qanda-block__comment-text", children: (0, jsx_runtime_1.jsx)(index_1.default, { className: "qanda-block__text", expandLabel: "Expand question", collapseLabel: "Collapse question", id: id, children: (0, jsx_runtime_1.jsx)(RichText_1.default, { structuredContent: body.structured }) }) })] }, id), children?.map((child) => child.body && ((0, jsx_runtime_1.jsxs)("div", { className: "qanda-block__comment qanda-block__answer", "data-testid": `qanda-block__answer-${child.id}`, "data-trackable": (0, tracking_1.trackingNS)('answer'), children: [(0, jsx_runtime_1.jsxs)("div", { className: "qanda-block__answer-header", children: [(0, jsx_runtime_1.jsx)("div", { className: "o3-type-body-highlight qanda-block__comment-byline", children: child.author ? ((0, jsx_runtime_1.jsx)(Author_1.default, { byline: child.byline, headshot: child.author.headshot, prefLabel: child.author.prefLabel, role: child.author.role, streamPage: child.author.streamPage })) : ((0, jsx_runtime_1.jsx)(Byline_1.default, { structuredContent: child.byline, showEditedBy: false })) }), showAnswerTime && ((0, jsx_runtime_1.jsx)("span", { className: "o3-type-detail", children: renderTime(child.publishedDate) }))] }), (0, jsx_runtime_1.jsx)("div", { className: "o3-type-body-base qanda-block__comment-text", children: (0, jsx_runtime_1.jsx)(index_1.default, { className: "qanda-block__text", expandLabel: "Expand answer", collapseLabel: "Collapse answer", id: id, children: (0, jsx_runtime_1.jsx)(RichText_1.default, { structuredContent: child.body.structured }) }) })] }, child.id)))] }));
|
|
77
78
|
}
|
|
78
79
|
exports.default = QandaBlock;
|
|
@@ -20,10 +20,10 @@ const QandaProvider_1 = require("./QandaProvider");
|
|
|
20
20
|
const o_tracking_1 = __importDefault(require("@financial-times/o-tracking"));
|
|
21
21
|
const comments_api_1 = require("../services/comments-api");
|
|
22
22
|
const qanda_1 = require("../config/qanda");
|
|
23
|
+
const tracking_1 = require("../utils/tracking");
|
|
23
24
|
function QandaContainer({ embedFormFieldContainer, countdownTimerContainer, }) {
|
|
24
25
|
const { storyId, useStaging, commentsAPIUrl } = (0, react_1.useContext)(QandaProvider_1.QandaContext);
|
|
25
26
|
const dispatch = (0, react_redux_1.useDispatch)();
|
|
26
|
-
const [liveMessage, setLiveMessage] = (0, react_1.useState)('');
|
|
27
27
|
const { data, error, isLoading } = (0, comments_api_1.useGetQandAStreamQuery)({
|
|
28
28
|
storyId,
|
|
29
29
|
useStaging,
|
|
@@ -76,15 +76,7 @@ function QandaContainer({ embedFormFieldContainer, countdownTimerContainer, }) {
|
|
|
76
76
|
selector: '.qanda-container',
|
|
77
77
|
getContextData: () => {
|
|
78
78
|
return {
|
|
79
|
-
|
|
80
|
-
name: 'liveqa',
|
|
81
|
-
type: 'container',
|
|
82
|
-
id: '663ab2ca-77ca-4b50-873d-1fd2ff21ff08',
|
|
83
|
-
},
|
|
84
|
-
content: {
|
|
85
|
-
asset_type: 'article',
|
|
86
|
-
uuid: storyId,
|
|
87
|
-
},
|
|
79
|
+
...tracking_1.baseTrackingData,
|
|
88
80
|
custom: [
|
|
89
81
|
{
|
|
90
82
|
name: 'status',
|
|
@@ -95,6 +87,6 @@ function QandaContainer({ embedFormFieldContainer, countdownTimerContainer, }) {
|
|
|
95
87
|
},
|
|
96
88
|
});
|
|
97
89
|
}, [data, status]);
|
|
98
|
-
return ((0, jsx_runtime_1.jsxs)("div", { className: "qanda-container",
|
|
90
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: "qanda-container", children: [error && ((0, jsx_runtime_1.jsxs)("p", { children: ["We are not able to load ", qanda_1.PRODUCT_NAME, " at the moment. Please try again later. Error:", error] })), !isLoading && !error && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [status !== 'close' && ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [showExpertView ? (0, jsx_runtime_1.jsx)(ExpertDrawer_1.default, {}) : (0, jsx_runtime_1.jsx)(QuestionForm_1.default, {}), (0, jsx_runtime_1.jsx)(CountdownTimer_1.default, { container: countdownTimerContainer })] })), status === 'pre-live' && (0, jsx_runtime_1.jsx)(Calendar_1.default, {}), (0, jsx_runtime_1.jsx)(Stream_1.default, { status: status }), (0, jsx_runtime_1.jsx)(Survey_1.default, { storyId: storyId, status: status }), (status === 'live' || (status === 'pre-live' && showExpertView)) && ((0, jsx_runtime_1.jsx)(FloatingActionBar_1.default, { isLive: status === 'live' })), status !== 'close' && !showExpertView && ((0, jsx_runtime_1.jsx)(EmbedFormField_1.default, { container: embedFormFieldContainer }))] })), (0, jsx_runtime_1.jsx)("div", { "aria-live": "polite", "aria-atomic": "true", className: "o3-visually-hidden", children: ariaMessage })] }));
|
|
99
91
|
}
|
|
100
92
|
exports.default = QandaContainer;
|
|
@@ -78,8 +78,9 @@ function QuestionForm() {
|
|
|
78
78
|
if (updatedProfile?.displayName) {
|
|
79
79
|
// NB: we are using a component act event as analytics team can only support existing categories and actions at this time
|
|
80
80
|
(0, tracking_1.trackEvent)({
|
|
81
|
-
|
|
82
|
-
|
|
81
|
+
content: {
|
|
82
|
+
uuid: storyId,
|
|
83
|
+
},
|
|
83
84
|
trigger_action: 'success_set_displayname',
|
|
84
85
|
});
|
|
85
86
|
}
|
|
@@ -113,13 +114,13 @@ function QuestionForm() {
|
|
|
113
114
|
(0, jsx_runtime_1.jsxs)(Overlay_1.default, { title: "Ask a question", closeCallback: () => {
|
|
114
115
|
setNewDisplayName(null);
|
|
115
116
|
dispatch((0, questionSlice_1.closeForm)());
|
|
116
|
-
}, children: [profile.isSubscribed && ((0, jsx_runtime_1.jsx)("div", { className: "overlay__content-box overlay__form-box", "data-testid": "question-form-overlay", children: (0, jsx_runtime_1.jsxs)("form", { onSubmit: handleSubmit, "data-trackable": `
|
|
117
|
+
}, children: [profile.isSubscribed && ((0, jsx_runtime_1.jsx)("div", { className: "overlay__content-box overlay__form-box", "data-testid": "question-form-overlay", children: (0, jsx_runtime_1.jsxs)("form", { onSubmit: handleSubmit, "data-trackable": (0, tracking_1.trackingNS)(`submit_question_form_${postAnonymously ? 'anonymous' : 'displayname'}`), children: [(0, jsx_runtime_1.jsxs)("div", { className: "overlay__user-container", children: [!postAnonymously && profile?.displayName && ((0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: (0, jsx_runtime_1.jsxs)("span", { className: "overlay__user-container-display-name o3-type-body-highlight", children: [(0, jsx_runtime_1.jsx)("i", { className: "overlay__message-icon", style: { maskImage: 'var(--o3-icon-user)' } }), profile.displayName] }) })), !profile?.displayName && ((0, jsx_runtime_1.jsxs)("label", { for: "display-name", className: "question-form__displayname-input o-forms-field", children: [(0, jsx_runtime_1.jsx)("span", { className: "o-forms-title", children: (0, jsx_runtime_1.jsx)("span", { className: "o3-type-body-highlight", children: "Choose a display name" }) }), (0, jsx_runtime_1.jsxs)("span", { className: (0, classnames_1.default)([
|
|
117
118
|
'question-form__displayname__input',
|
|
118
119
|
'o-forms-input',
|
|
119
120
|
'o-forms-input--text',
|
|
120
121
|
{ 'o-forms-input--invalid': !!displayNameError },
|
|
121
122
|
]), children: [(0, jsx_runtime_1.jsx)("input", { className: "question-form__displayname__input__field o3-type-body-lg", id: "display-name", value: newDisplayName || '', onChange: (e) => setNewDisplayName(e.currentTarget.value), type: postAnonymously ? 'password' : 'text', "aria-invalid": displayNameError ? 'true' : 'false', "aria-errormessage": displayNameError ??
|
|
122
|
-
'question_form__displayname-error-message' }), displayNameError && ((0, jsx_runtime_1.jsx)("span", { className: "question-form__displayname__input__error o3-type-detail", id: "question_form__displayname-error-message", children: (0, jsx_runtime_1.jsx)("span", { children: displayNameError }) }))] })] }))] }), (0, jsx_runtime_1.jsx)("div", { className: "o-forms-field", children: (0, jsx_runtime_1.jsxs)("label", { className: "o-forms-input o-forms-input--checkbox", htmlFor: "post-anonymously", children: [(0, jsx_runtime_1.jsx)("input", { type: "checkbox", name: "post-anonymously", id: "post-anonymously", value: "true", onChange: handleHideDisplayName, "data-trackable-context-action": `Post anonymously ${postAnonymously ? 'off' : 'on'}`, "data-trackable": `question-form__post_anonymously-${postAnonymously ? 'off' : 'on'}` }), (0, jsx_runtime_1.jsx)("span", { className: "o-forms-input__label", children: "Post anonymously (optional)" })] }) }), (0, jsx_runtime_1.jsxs)("label", { for: "question-text", class: "o-forms-field question-form__question__input", children: [(0, jsx_runtime_1.jsx)("span", { class: "o-forms-title", children: (0, jsx_runtime_1.jsx)("span", { class: "o3-type-body-highlight", children: "Enter your question" }) }), (0, jsx_runtime_1.jsx)("span", { class: "o-forms-input o-forms-input--textarea", children: (0, jsx_runtime_1.jsx)("textarea", { id: "question-text", name: "question", value: question, onChange: (e) => dispatch((0, questionSlice_1.updateQuestion)(e.currentTarget.value)), "aria-label": "Your question", placeholder: "Type your question", maxLength: maxQuestionLength, "aria-invalid": error ? 'true' : undefined, "aria-errormessage": "question-form-error-message" }) })] }), (0, jsx_runtime_1.jsx)("span", { className: "o3-type-detail", children: "Questions must be at least 20 characters long." }), (0, jsx_runtime_1.jsx)(o3_button_1.Button, { attributes: {
|
|
123
|
+
'question_form__displayname-error-message' }), displayNameError && ((0, jsx_runtime_1.jsx)("span", { className: "question-form__displayname__input__error o3-type-detail", id: "question_form__displayname-error-message", children: (0, jsx_runtime_1.jsx)("span", { children: displayNameError }) }))] })] }))] }), (0, jsx_runtime_1.jsx)("div", { className: "o-forms-field", children: (0, jsx_runtime_1.jsxs)("label", { className: "o-forms-input o-forms-input--checkbox", htmlFor: "post-anonymously", children: [(0, jsx_runtime_1.jsx)("input", { type: "checkbox", name: "post-anonymously", id: "post-anonymously", value: "true", onChange: handleHideDisplayName, "data-trackable-context-action": `Post anonymously ${postAnonymously ? 'off' : 'on'}`, "data-trackable": (0, tracking_1.trackingNS)(`question-form__post_anonymously-${postAnonymously ? 'off' : 'on'}`) }), (0, jsx_runtime_1.jsx)("span", { className: "o-forms-input__label", children: "Post anonymously (optional)" })] }) }), (0, jsx_runtime_1.jsxs)("label", { for: "question-text", class: "o-forms-field question-form__question__input", children: [(0, jsx_runtime_1.jsx)("span", { class: "o-forms-title", children: (0, jsx_runtime_1.jsx)("span", { class: "o3-type-body-highlight", children: "Enter your question" }) }), (0, jsx_runtime_1.jsx)("span", { class: "o-forms-input o-forms-input--textarea", children: (0, jsx_runtime_1.jsx)("textarea", { id: "question-text", name: "question", value: question, onChange: (e) => dispatch((0, questionSlice_1.updateQuestion)(e.currentTarget.value)), "aria-label": "Your question", placeholder: "Type your question", maxLength: maxQuestionLength, "aria-invalid": error ? 'true' : undefined, "aria-errormessage": "question-form-error-message" }) })] }), (0, jsx_runtime_1.jsx)("span", { className: "o3-type-detail", children: "Questions must be at least 20 characters long." }), (0, jsx_runtime_1.jsx)(o3_button_1.Button, { attributes: {
|
|
123
124
|
className: 'overlay__submit-button',
|
|
124
125
|
type: 'submit',
|
|
125
126
|
disabled: !submitEnabled,
|
|
@@ -5,6 +5,7 @@ exports.getTitleByUser = getTitleByUser;
|
|
|
5
5
|
exports.getTextByUser = getTextByUser;
|
|
6
6
|
const jsx_runtime_1 = require("preact/jsx-runtime");
|
|
7
7
|
const qanda_1 = require("../../config/qanda");
|
|
8
|
+
const tracking_1 = require("../../utils/tracking");
|
|
8
9
|
exports.SIGN_IN_URL = 'https://www.ft.com/login';
|
|
9
10
|
exports.SUBSCRIBE_URL = 'https://subs.ft.com/products';
|
|
10
11
|
exports.COMMUNITY_GUIDELINES_URL = 'https://help.ft.com/help/article-queries/commenting-on-an-article/commenting-guidelines';
|
|
@@ -21,14 +22,14 @@ function getTitleByUser(profile) {
|
|
|
21
22
|
}
|
|
22
23
|
function getTextByUser(profile) {
|
|
23
24
|
if (profile.isSubscribed) {
|
|
24
|
-
return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: ["Keep your comments respectful and easy to understand for our global audience. By submitting, you agree to follow our", ' ', (0, jsx_runtime_1.jsx)("a", { className: "message-box__content-link", "data-trackable":
|
|
25
|
+
return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: ["Keep your comments respectful and easy to understand for our global audience. By submitting, you agree to follow our", ' ', (0, jsx_runtime_1.jsx)("a", { className: "message-box__content-link", "data-trackable": (0, tracking_1.trackingNS)('question-form__community-guidelines'), "data-trackable-context-action": "Community guidelines", href: exports.COMMUNITY_GUIDELINES_URL, target: "_parent", children: "community guidelines" }), ' ', "and", ' ', (0, jsx_runtime_1.jsx)("a", { className: "message-box__content-link", "data-trackable": (0, tracking_1.trackingNS)('question-form__terms-and-conditions'), "data-trackable-context-action": "Terms and conditions", href: exports.TERMS_AND_CONDITIONS_URL, target: "_parent", children: "terms" }), "."] }));
|
|
25
26
|
}
|
|
26
27
|
if (profile.isTrialist) {
|
|
27
|
-
return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: ["View our full", ' ', (0, jsx_runtime_1.jsx)("a", { className: "message-box__content-link", "data-trackable":
|
|
28
|
+
return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: ["View our full", ' ', (0, jsx_runtime_1.jsx)("a", { className: "message-box__content-link", "data-trackable": (0, tracking_1.trackingNS)('question-form__subscribe-trialist'), "data-trackable-context-action": "Subscribe", href: exports.SUBSCRIBE_URL, target: "_parent", children: "subscription packages" }), ' ', "to join the conversation."] }));
|
|
28
29
|
}
|
|
29
30
|
if (profile.isRegistered) {
|
|
30
|
-
return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("a", { className: "message-box__content-link", "data-trackable":
|
|
31
|
+
return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("a", { className: "message-box__content-link", "data-trackable": (0, tracking_1.trackingNS)('question-form__subscribe-registered'), "data-trackable-context-action": "Subscribe", href: exports.SUBSCRIBE_URL, target: "_parent", children: "Subscribe" }), ' ', "to join the conversation."] }));
|
|
31
32
|
}
|
|
32
33
|
// Anonymous users
|
|
33
|
-
return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: ["Please", ' ', (0, jsx_runtime_1.jsx)("a", { className: "message-box__content-link", "data-trackable":
|
|
34
|
+
return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: ["Please", ' ', (0, jsx_runtime_1.jsx)("a", { className: "message-box__content-link", "data-trackable": (0, tracking_1.trackingNS)('question-form__sign-in-anonymous'), "data-trackable-context-action": "Sign in", href: exports.SIGN_IN_URL, target: "_parent", children: "sign in" }), ' ', "or", ' ', (0, jsx_runtime_1.jsx)("a", { className: "message-box__content-link", "data-trackable": (0, tracking_1.trackingNS)('question-form__subscribe-anonymous'), "data-trackable-context-action": "Subscribe", href: exports.SUBSCRIBE_URL, target: "_parent", children: "subscribe" }), ' ', "to join the conversation."] }));
|
|
34
35
|
}
|
|
@@ -28,7 +28,7 @@ function Stream({ status = 'pre-live' }) {
|
|
|
28
28
|
const element = document.getElementById(latestAnsweredQuestionId);
|
|
29
29
|
element?.scrollIntoView({ behavior: 'smooth' });
|
|
30
30
|
}, [latestAnsweredQuestionId]);
|
|
31
|
-
return ((0, jsx_runtime_1.jsxs)("section", { className: "qanda__stream", children: [data && data.qandas.length > 0 && ((0, jsx_runtime_1.jsx)("h2", { className: "o3-type-title-md qanda__stream-title", children: data.status && data.status === 'pre-live'
|
|
31
|
+
return ((0, jsx_runtime_1.jsxs)("section", { className: "qanda__stream", children: [data && data.qandas.length > 0 && ((0, jsx_runtime_1.jsx)("h2", { className: "o3-type-title-md qanda__stream-title", id: "qanda__stream-title", children: data.status && data.status === 'pre-live'
|
|
32
32
|
? 'Top questions so far'
|
|
33
33
|
: 'What our readers are asking' })), (0, jsx_runtime_1.jsxs)("div", { className: "qanda__blocks-container", "data-testid": "qanda__blocks-container", id: "qanda__blocks-container", "aria-busy": isLoading, children: [isLoading && ((0, jsx_runtime_1.jsx)(Loader_1.default, { theme: "dark", extraClassNames: "qanda__stream-loader" })), error && ((0, jsx_runtime_1.jsx)(ErrorMessage_1.default, { error: error, storyId: storyId, operation: "loading-stream" })), data &&
|
|
34
34
|
data.qandas &&
|
|
@@ -1,36 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
const jsx_runtime_1 = require("preact/jsx-runtime");
|
|
4
|
+
const tracking_1 = require("../../utils/tracking");
|
|
4
5
|
function Survey({ storyId, status }) {
|
|
5
|
-
|
|
6
|
-
const event = new CustomEvent('oTracking.event', {
|
|
7
|
-
detail: trackingData,
|
|
8
|
-
bubbles: true,
|
|
9
|
-
});
|
|
10
|
-
document.body.dispatchEvent(event);
|
|
11
|
-
};
|
|
12
|
-
const handleClick = () => {
|
|
13
|
-
trackEvent({
|
|
14
|
-
category: 'component',
|
|
15
|
-
component: {
|
|
16
|
-
name: 'liveqa',
|
|
17
|
-
type: 'container',
|
|
18
|
-
id: '663ab2ca-77ca-4b50-873d-1fd2ff21ff08',
|
|
19
|
-
},
|
|
20
|
-
action: 'act',
|
|
21
|
-
trigger_action: 'liveqa_survey_open',
|
|
22
|
-
content: {
|
|
23
|
-
asset_type: 'article',
|
|
24
|
-
uuid: storyId,
|
|
25
|
-
},
|
|
26
|
-
custom: [
|
|
27
|
-
{
|
|
28
|
-
name: 'status',
|
|
29
|
-
value: status,
|
|
30
|
-
},
|
|
31
|
-
],
|
|
32
|
-
});
|
|
33
|
-
};
|
|
34
|
-
return ((0, jsx_runtime_1.jsx)("div", { className: "o-message o-message--action o-message--inform o-message--survey", style: { marginBottom: 'var(--o3-spacing-l)' }, "data-o-message-close": "false", "data-o-component": "o-message", children: (0, jsx_runtime_1.jsx)("div", { className: "o-message__container", children: (0, jsx_runtime_1.jsxs)("div", { className: "o-message__content", children: [(0, jsx_runtime_1.jsx)("p", { className: "o-message__content-main", style: { fontSize: 'var(--o3-type-body-base-font-size)' }, children: "How was your experience?" }), (0, jsx_runtime_1.jsx)("div", { className: "o-message__actions", children: (0, jsx_runtime_1.jsx)("a", { href: `https://www.feedback.ft.com/c/a/6HTDVvrHSyaKLZ5281McAo?content_id=${storyId}&qa_status=${status}`, onClick: handleClick, className: "o-message__actions__primary", "data-trackable": "liveqa_survey_link", children: "Share your feedback" }) })] }) }) }));
|
|
6
|
+
return ((0, jsx_runtime_1.jsx)("div", { className: "o-message o-message--action o-message--inform o-message--survey", style: { marginBottom: 'var(--o3-spacing-l)' }, "data-o-message-close": "false", "data-o-component": "o-message", children: (0, jsx_runtime_1.jsx)("div", { className: "o-message__container", children: (0, jsx_runtime_1.jsxs)("div", { className: "o-message__content", children: [(0, jsx_runtime_1.jsx)("p", { className: "o-message__content-main", style: { fontSize: 'var(--o3-type-body-base-font-size)' }, children: "How was your experience?" }), (0, jsx_runtime_1.jsx)("div", { className: "o-message__actions", children: (0, jsx_runtime_1.jsx)("a", { href: `https://www.feedback.ft.com/c/a/6HTDVvrHSyaKLZ5281McAo?content_id=${storyId}&qa_status=${status}`, className: "o-message__actions__primary", "data-trackable": (0, tracking_1.trackingNS)('survey_link'), children: "Share your feedback" }) })] }) }) }));
|
|
35
7
|
}
|
|
36
8
|
exports.default = Survey;
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.useUpdatedQA = exports.useUpdatedComments = exports.usePostQuestionMutation = exports.useGetUserRolesQuery = exports.useGetQandAUpdatesQuery = exports.useGetQandAStreamQuery = exports.nextCommentsApi = exports.eventSource = void 0;
|
|
3
|
+
exports.useUpdatedQA = exports.useUpdatedComments = exports.usePostQuestionMutation = exports.useGetUserRolesQuery = exports.useGetQandAUpdatesQuery = exports.useGetQandAStreamQuery = exports.nextCommentsApi = exports.eventSource = exports.updateTypes = void 0;
|
|
4
4
|
const react_1 = require("@reduxjs/toolkit/query/react");
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
const stream_1 = require("../store/stream");
|
|
6
|
+
const reliability_1 = require("./reliability");
|
|
7
|
+
// NB: this could do with a refactor to split things out into smaller files: see https://redux-toolkit.js.org/rtk-query/usage/code-splitting
|
|
8
|
+
// NB: adding retry limit would be a good idea https://financialtimes.atlassian.net/browse/CI-2715
|
|
7
9
|
const retryDelay = 1000;
|
|
8
|
-
|
|
10
|
+
exports.updateTypes = {
|
|
9
11
|
QA_NEW_REPLY: 'updatedComments',
|
|
10
12
|
QA_UPDATE: 'updatedQA',
|
|
11
13
|
};
|
|
@@ -37,13 +39,28 @@ exports.nextCommentsApi = (0, react_1.createApi)({
|
|
|
37
39
|
queryFn: async ({ storyId, useStaging, commentsAPIUrl, }) => {
|
|
38
40
|
return { data: {} };
|
|
39
41
|
},
|
|
40
|
-
async onCacheEntryAdded(arg, { updateCachedData, cacheDataLoaded, cacheEntryRemoved }) {
|
|
42
|
+
async onCacheEntryAdded(arg, { updateCachedData, cacheDataLoaded, cacheEntryRemoved, dispatch, getState, }) {
|
|
41
43
|
await cacheDataLoaded;
|
|
42
44
|
const connectToSSE = () => {
|
|
43
45
|
const sseEndpoint = `${arg.commentsAPIUrl}/story/${arg.storyId}/updates${arg.useStaging ? '?staging=1' : ''}`;
|
|
44
46
|
exports.eventSource = new EventSource(sseEndpoint);
|
|
45
|
-
exports.eventSource.onopen = () => {
|
|
47
|
+
exports.eventSource.onopen = async () => {
|
|
46
48
|
console.log(`Connected to SSE: ${sseEndpoint}`);
|
|
49
|
+
const state = getState();
|
|
50
|
+
const lastEventId = state.stream.mostRecentUpdate;
|
|
51
|
+
if (lastEventId !== null && lastEventId !== undefined) {
|
|
52
|
+
try {
|
|
53
|
+
await dispatch(exports.nextCommentsApi.endpoints.getCatchupUpdates.initiate({
|
|
54
|
+
storyId: arg.storyId,
|
|
55
|
+
useStaging: arg.useStaging,
|
|
56
|
+
commentsAPIUrl: arg.commentsAPIUrl,
|
|
57
|
+
lastEventId: `${lastEventId}`,
|
|
58
|
+
}));
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
reliability_1.qandaReliability.failure('qa-updates', error);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
47
64
|
};
|
|
48
65
|
exports.eventSource.onmessage = (event) => {
|
|
49
66
|
try {
|
|
@@ -51,10 +68,14 @@ exports.nextCommentsApi = (0, react_1.createApi)({
|
|
|
51
68
|
console.log('Received SSE update:', data);
|
|
52
69
|
if (data && data.type) {
|
|
53
70
|
updateCachedData((draft) => {
|
|
54
|
-
const updatedData = updateTypes[data.type];
|
|
71
|
+
const updatedData = exports.updateTypes[data.type];
|
|
55
72
|
draft[updatedData] = draft[updatedData] || [];
|
|
56
73
|
draft[updatedData].push(data.payload);
|
|
57
74
|
});
|
|
75
|
+
// Store the lastEventId for use in reconnection
|
|
76
|
+
if (event.lastEventId) {
|
|
77
|
+
dispatch((0, stream_1.setMostRecentUpdate)(event.lastEventId));
|
|
78
|
+
}
|
|
58
79
|
}
|
|
59
80
|
}
|
|
60
81
|
catch (error) {
|
|
@@ -94,6 +115,19 @@ exports.nextCommentsApi = (0, react_1.createApi)({
|
|
|
94
115
|
}
|
|
95
116
|
},
|
|
96
117
|
}),
|
|
118
|
+
// CATCHUP UPDATES
|
|
119
|
+
getCatchupUpdates: builder.query({
|
|
120
|
+
query: ({ storyId, useStaging, commentsAPIUrl, lastEventId, }) => `${commentsAPIUrl}/story/${storyId}/catchup-updates?${useStaging ? 'staging=1&' : ''}${`lastEventId=${lastEventId}`}`,
|
|
121
|
+
transformResponse: (response) => {
|
|
122
|
+
const updatedComments = response?.comments.map((comment) => {
|
|
123
|
+
return comment.data;
|
|
124
|
+
});
|
|
125
|
+
return {
|
|
126
|
+
updatedComments,
|
|
127
|
+
lastEventId: response?.lastEventId,
|
|
128
|
+
};
|
|
129
|
+
},
|
|
130
|
+
}),
|
|
97
131
|
// USER ROLES
|
|
98
132
|
getUserRoles: builder.query({
|
|
99
133
|
query: ({ storyId, useStaging, commentsAPIUrl, token, }) => ({
|
|
@@ -130,11 +164,12 @@ exports.nextCommentsApi = (0, react_1.createApi)({
|
|
|
130
164
|
exports.useGetQandAStreamQuery = exports.nextCommentsApi.useGetQandAStreamQuery, exports.useGetQandAUpdatesQuery = exports.nextCommentsApi.useGetQandAUpdatesQuery, exports.useGetUserRolesQuery = exports.nextCommentsApi.useGetUserRolesQuery, exports.usePostQuestionMutation = exports.nextCommentsApi.usePostQuestionMutation;
|
|
131
165
|
// UPDATES QUERY CUSTOM HOOKS
|
|
132
166
|
// custom hook for getting just the updated comments from the updates, so we are more efficient
|
|
133
|
-
const useUpdatedComments = ({ storyId, useStaging, commentsAPIUrl,
|
|
167
|
+
const useUpdatedComments = ({ storyId, useStaging, commentsAPIUrl }, options) => {
|
|
134
168
|
return exports.nextCommentsApi.useGetQandAUpdatesQuery({ storyId, useStaging, commentsAPIUrl }, {
|
|
135
169
|
selectFromResult: ({ data }) => ({
|
|
136
170
|
updatedComments: data?.updatedComments || [],
|
|
137
171
|
}),
|
|
172
|
+
skip: options?.skip || false,
|
|
138
173
|
}).updatedComments;
|
|
139
174
|
};
|
|
140
175
|
exports.useUpdatedComments = useUpdatedComments;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @fileoverview RTK Query listener middleware for Q&A stream events
|
|
4
|
+
*
|
|
5
|
+
* This module contains Redux Toolkit listeners that respond to RTK Query events
|
|
6
|
+
* and update the application state accordingly. Currently handles:
|
|
7
|
+
* - Tracking the most recent update timestamp from live Q&A streams
|
|
8
|
+
*
|
|
9
|
+
* @see https://redux-toolkit.js.org/api/createListenerMiddleware
|
|
10
|
+
*/
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.listenerMiddleware = void 0;
|
|
13
|
+
const toolkit_1 = require("@reduxjs/toolkit");
|
|
14
|
+
const comments_api_1 = require("./comments-api");
|
|
15
|
+
const stream_1 = require("../store/stream");
|
|
16
|
+
exports.listenerMiddleware = (0, toolkit_1.createListenerMiddleware)();
|
|
17
|
+
/**
|
|
18
|
+
* Calculates the most recent update timestamp from a list of Q&A questions
|
|
19
|
+
*
|
|
20
|
+
* @param qandas - Array of Comment objects representing questions with nested answers
|
|
21
|
+
* @returns The timestamp (in milliseconds) of the most recent answer, 0 if no answers found
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* const timestamp = getMostRecentUpdateTime(qandas);
|
|
25
|
+
* // Returns: 167253120 (example timestamp)
|
|
26
|
+
*/
|
|
27
|
+
// we have a v similar functin in src/utils/qandas.ts
|
|
28
|
+
function getMostRecentUpdateTime(qandas) {
|
|
29
|
+
if (!qandas.length ||
|
|
30
|
+
!Array.isArray(qandas[0].children) ||
|
|
31
|
+
!qandas[0].children.length) {
|
|
32
|
+
return 0; // no answers or no content at all
|
|
33
|
+
}
|
|
34
|
+
// we should be getting the comments back in a date desc by most recent answer from the API
|
|
35
|
+
const mostRecentUpdate = qandas[0].children.reduce((mostRecent, answer) => {
|
|
36
|
+
const publishedDateTimestamp = new Date(answer.publishedDate).getTime();
|
|
37
|
+
return publishedDateTimestamp > mostRecent
|
|
38
|
+
? publishedDateTimestamp
|
|
39
|
+
: mostRecent;
|
|
40
|
+
}, 0);
|
|
41
|
+
return mostRecentUpdate;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Listener: Updates most recent timestamp when live Q&A stream data is fetched
|
|
45
|
+
*
|
|
46
|
+
* Responds to successful getQandAStream queries with status='live' and extracts
|
|
47
|
+
* the most recent answer timestamp to store in Redux state. This timestamp is
|
|
48
|
+
* used for reconnection logic and catching up on missed updates.
|
|
49
|
+
*
|
|
50
|
+
* @listens nextCommentsApi.endpoints.getQandAStream.matchFulfilled
|
|
51
|
+
* @dispatches setMostRecentUpdate - Updates stream.mostRecentUpdate in Redux store
|
|
52
|
+
*/
|
|
53
|
+
exports.listenerMiddleware.startListening({
|
|
54
|
+
matcher: (0, toolkit_1.isAnyOf)(comments_api_1.nextCommentsApi.endpoints.getQandAStream.matchFulfilled),
|
|
55
|
+
effect: (action, listenerApi) => {
|
|
56
|
+
const { payload, meta } = action;
|
|
57
|
+
const { storyId, useStaging, commentsAPIUrl, status } = meta.arg.originalArgs;
|
|
58
|
+
// Check if this is a live stream QUERY, not response. If it is a live query we get questions and answers.
|
|
59
|
+
const isLiveStream = status === 'live';
|
|
60
|
+
if (isLiveStream && payload.qandas) {
|
|
61
|
+
const mostRecentUpdate = getMostRecentUpdateTime(payload.qandas);
|
|
62
|
+
// Update the store with the most recent update as a string
|
|
63
|
+
if (mostRecentUpdate !== undefined) {
|
|
64
|
+
console.log(`Setting most recent update for story ${storyId}: ${mostRecentUpdate}`);
|
|
65
|
+
listenerApi.dispatch((0, stream_1.setMostRecentUpdate)(`${mostRecentUpdate}`));
|
|
66
|
+
}
|
|
67
|
+
listenerApi.dispatch(comments_api_1.nextCommentsApi.endpoints.getCatchupUpdates.initiate({
|
|
68
|
+
storyId: storyId,
|
|
69
|
+
useStaging: useStaging,
|
|
70
|
+
commentsAPIUrl: commentsAPIUrl,
|
|
71
|
+
lastEventId: `${mostRecentUpdate}`,
|
|
72
|
+
}));
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
/**
|
|
77
|
+
* Listener: Updates updates cache and most recent update when catchup comments are received
|
|
78
|
+
*
|
|
79
|
+
* Responds to successful getCatchupUpdates queries and adds them to the updates list
|
|
80
|
+
*
|
|
81
|
+
* @listens nextCommentsApi.endpoints.getCatchupUpdates.matchFulfilled
|
|
82
|
+
* @dispatches nextCommentsApi.util.updateQueryData - Updates the cache of updated comments in the store
|
|
83
|
+
* @dispatches setMostRecentUpdate - Updates stream.mostRecentUpdate in Redux store
|
|
84
|
+
*/
|
|
85
|
+
exports.listenerMiddleware.startListening({
|
|
86
|
+
matcher: comments_api_1.nextCommentsApi.endpoints.getCatchupUpdates.matchFulfilled,
|
|
87
|
+
effect: (action, listenerApi) => {
|
|
88
|
+
const { payload, meta } = action;
|
|
89
|
+
const { storyId, useStaging, commentsAPIUrl } = meta.arg.originalArgs;
|
|
90
|
+
const { updatedComments, lastEventId } = payload;
|
|
91
|
+
console.log('Received catchup updates, new event id:', lastEventId);
|
|
92
|
+
const updateType = comments_api_1.updateTypes['QA_NEW_REPLY'];
|
|
93
|
+
if (Array.isArray(updatedComments) && updatedComments.length) {
|
|
94
|
+
listenerApi.dispatch(comments_api_1.nextCommentsApi.util.updateQueryData('getQandAUpdates', { storyId, useStaging, commentsAPIUrl }, (draft) => {
|
|
95
|
+
draft[updateType] = draft[updateType] || [];
|
|
96
|
+
draft[updateType].push(...updatedComments);
|
|
97
|
+
}));
|
|
98
|
+
}
|
|
99
|
+
if (typeof lastEventId === 'string') {
|
|
100
|
+
listenerApi.dispatch((0, stream_1.setMostRecentUpdate)(lastEventId));
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
});
|
package/dist/cjs/store/index.js
CHANGED
|
@@ -12,6 +12,7 @@ const user_1 = require("./user");
|
|
|
12
12
|
const expertSlice_1 = __importDefault(require("./expertSlice"));
|
|
13
13
|
const comments_api_1 = require("../services/comments-api");
|
|
14
14
|
const error_middleware_1 = require("../services/error-middleware");
|
|
15
|
+
const listeners_middleware_1 = require("../services/listeners-middleware");
|
|
15
16
|
exports.rootReducer = (0, toolkit_1.combineReducers)({
|
|
16
17
|
stream: stream_1.stream.reducer,
|
|
17
18
|
question: questionSlice_1.default,
|
|
@@ -22,6 +23,7 @@ exports.rootReducer = (0, toolkit_1.combineReducers)({
|
|
|
22
23
|
exports.store = (0, toolkit_1.configureStore)({
|
|
23
24
|
reducer: exports.rootReducer,
|
|
24
25
|
middleware: (getDefaultMiddleware) => getDefaultMiddleware()
|
|
26
|
+
.prepend(listeners_middleware_1.listenerMiddleware.middleware)
|
|
25
27
|
.concat(comments_api_1.nextCommentsApi.middleware)
|
|
26
28
|
.concat(error_middleware_1.rtkQueryErrorLogger),
|
|
27
29
|
});
|
package/dist/cjs/store/stream.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var _a;
|
|
2
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.setLatestAnsweredQuestionId = exports.stream = exports.initialState = void 0;
|
|
4
|
+
exports.setMostRecentUpdate = exports.setLatestAnsweredQuestionId = exports.stream = exports.initialState = void 0;
|
|
4
5
|
const toolkit_1 = require("@reduxjs/toolkit");
|
|
5
6
|
exports.initialState = {
|
|
6
7
|
latestAnsweredQuestionId: '',
|
|
8
|
+
mostRecentUpdate: null,
|
|
7
9
|
};
|
|
8
10
|
exports.stream = (0, toolkit_1.createSlice)({
|
|
9
11
|
name: 'stream',
|
|
@@ -12,6 +14,9 @@ exports.stream = (0, toolkit_1.createSlice)({
|
|
|
12
14
|
setLatestAnsweredQuestionId: (state, action) => {
|
|
13
15
|
state.latestAnsweredQuestionId = action.payload;
|
|
14
16
|
},
|
|
17
|
+
setMostRecentUpdate: (state, action) => {
|
|
18
|
+
state.mostRecentUpdate = action.payload;
|
|
19
|
+
},
|
|
15
20
|
},
|
|
16
21
|
});
|
|
17
|
-
|
|
22
|
+
_a = exports.stream.actions, exports.setLatestAnsweredQuestionId = _a.setLatestAnsweredQuestionId, exports.setMostRecentUpdate = _a.setMostRecentUpdate;
|