diff --git a/CHANGELOG.md b/CHANGELOG.md index 4fe3e29722..273d225ba0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -62,6 +62,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - `component`: Remove [`react`](https://www.npmjs.com/package/react) and [`react-dom`](https://www.npmjs.com/package/react-dom) from `devDependencies` - `playground`: Remove [`react`](https://www.npmjs.com/package/react) and [`react-dom`](https://www.npmjs.com/package/react-dom) from `dependencies` - `samples/*`: Move to production version of Web Chat, and bump to [`react@16.8.6`](https://www.npmjs.com/package/react) and [`react-dom@16.8.6`](https://www.npmjs.com/package/react-dom) +- Moved the typing indicator to the send box and removed the typing indicator logic from the sagas, by [@tdurnford](https://github.com/tdurnford), in PR [#2321](https://github.com/microsoft/BotFramework-WebChat/pull/2321) ### Fixed diff --git a/__tests__/__image_snapshots__/chrome-docker/send-typing-indicator-js-typing-indicator-should-display-in-send-box-1-snap.png b/__tests__/__image_snapshots__/chrome-docker/send-typing-indicator-js-typing-indicator-should-display-in-send-box-1-snap.png new file mode 100644 index 0000000000..bf0ec1f2fc Binary files /dev/null and b/__tests__/__image_snapshots__/chrome-docker/send-typing-indicator-js-typing-indicator-should-display-in-send-box-1-snap.png differ diff --git a/__tests__/__image_snapshots__/chrome-docker/send-typing-indicator-js-typing-indicator-should-not-display-after-second-activity-1-snap.png b/__tests__/__image_snapshots__/chrome-docker/send-typing-indicator-js-typing-indicator-should-not-display-after-second-activity-1-snap.png new file mode 100644 index 0000000000..a2ac36b798 Binary files /dev/null and b/__tests__/__image_snapshots__/chrome-docker/send-typing-indicator-js-typing-indicator-should-not-display-after-second-activity-1-snap.png differ diff --git a/__tests__/sendTypingIndicator.js b/__tests__/sendTypingIndicator.js index f289a1166b..8d4567a2ba 100644 --- a/__tests__/sendTypingIndicator.js +++ b/__tests__/sendTypingIndicator.js @@ -1,7 +1,10 @@ import { By } from 'selenium-webdriver'; -import { timeouts } from './constants.json'; +import { imageSnapshotOptions, timeouts } from './constants.json'; import minNumActivitiesShown from './setup/conditions/minNumActivitiesShown'; +import typingActivityReceived from './setup/conditions/typingActivityReceived'; +import typingAnimationBackgroundImage from './setup/assets/typingIndicator'; +import uiConnected from './setup/conditions/uiConnected'; // selenium-webdriver API doc: // https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/index_exports_WebDriver.html @@ -20,21 +23,52 @@ test('Send typing indicator', async () => { await input.sendKeys('ABC'); // Typing indicator takes longer to come back - await driver.wait(minNumActivitiesShown(3), 5000); + await driver.wait(typingActivityReceived(), timeouts.directLine); }); // TODO: [P3] Take this deprecation code out when releasing on or after January 13 2020 test('Send typing indicator using deprecated props', async () => { - const { driver, pageObjects } = await setupWebDriver({ props: { sendTyping: true } }); + const { driver, pageObjects } = await setupWebDriver({ + props: { sendTyping: true } + }); - await pageObjects.sendMessageViaSendBox('echo-typing', { waitForSend: true }); + await driver.wait(uiConnected(), timeouts.directLine); - await driver.wait(minNumActivitiesShown(2), timeouts.directLine); + await pageObjects.sendMessageViaSendBox('echo-typing', { waitForSend: true }); const input = await driver.findElement(By.css('input[type="text"]')); await input.sendKeys('ABC'); // Typing indicator takes longer to come back - await driver.wait(minNumActivitiesShown(3), 5000); + await driver.wait(typingActivityReceived(), timeouts.directLine); +}); + +test('typing indicator should display in SendBox', async () => { + const { driver, pageObjects } = await setupWebDriver({ props: { styleOptions: { typingAnimationBackgroundImage } } }); + + await driver.wait(uiConnected(), timeouts.directLine); + + await pageObjects.sendMessageViaSendBox('typing 1', { waitForSend: true }); + + // Typing indicator takes longer to come back + await driver.wait(typingActivityReceived(), timeouts.directLine); + + const base64PNG = await driver.takeScreenshot(); + + expect(base64PNG).toMatchImageSnapshot(imageSnapshotOptions); +}); + +test('typing indicator should not display after second activity', async () => { + const { driver, pageObjects } = await setupWebDriver({ + props: { + styleOptions: { typingAnimationBackgroundImage, typingAnimationDuration: 10000 } + } + }); + + await pageObjects.sendMessageViaSendBox('typing', { waitForSend: true }); + await driver.wait(minNumActivitiesShown(2), 5000); + + const base64PNG = await driver.takeScreenshot(); + expect(base64PNG).toMatchImageSnapshot(imageSnapshotOptions); }); diff --git a/__tests__/setup/assets/typingIndicator.js b/__tests__/setup/assets/typingIndicator.js new file mode 100644 index 0000000000..0a2c39ed64 --- /dev/null +++ b/__tests__/setup/assets/typingIndicator.js @@ -0,0 +1 @@ +export default "url('data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAAUACgDASIAAhEBAxEB/8QAGgABAQACAwAAAAAAAAAAAAAAAAYCBwMFCP/EACsQAAECBQIEBQUAAAAAAAAAAAECAwAEBQYRBxITIjFBMlFhccFScoGh8f/EABQBAQAAAAAAAAAAAAAAAAAAAAD/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwD0lctx023JVD9UeKOIcNoSNylkdcCMbauSmXHLOPUx8r4ZAcQtO1SM9Mj5iO1gtWo1syc7S2zMKYSptbIPNgnII8/5HBpRZ9RpaKjNVVCpUzLPAQ1nmA7qPl6fmAondRrcaqhkVTiiQrYXgglsH7vnpHc3DcNNoEimaqT4Q2s4bCRuUs+gEaLd05uNFVMmiS3o3YEwFDhlP1Z7e3WLzUuzahUKHRk0zM07TmeApvOFLGEjcM9+Xp6wFnbN0Uu5GnF0x4qW1je2tO1Sc9Djy9oRD6QWlU6PPzVSqjRlgtksttKPMcqBKiO3h/cIDacIQgEIQgEIQgP/2Q==')"; diff --git a/__tests__/setup/conditions/typingActivityReceived.js b/__tests__/setup/conditions/typingActivityReceived.js new file mode 100644 index 0000000000..3536dbeaf5 --- /dev/null +++ b/__tests__/setup/conditions/typingActivityReceived.js @@ -0,0 +1,23 @@ +import { Condition } from 'selenium-webdriver'; + +export default function typingActivityReceived() { + return new Condition( + `Waiting for typing activity`, + async driver => + await driver.executeScript( + () => + ~window.WebChatTest.actions + .filter(({ type }) => type === 'DIRECT_LINE/INCOMING_ACTIVITY') + .findIndex( + ({ + payload: { + activity: { + from: { role }, + type + } + } + }) => role === 'bot' && type === 'typing' + ) + ) + ); +} diff --git a/packages/bundle/src/index-es5.ts b/packages/bundle/src/index-es5.ts index 1d61212669..f09455ab50 100644 --- a/packages/bundle/src/index-es5.ts +++ b/packages/bundle/src/index-es5.ts @@ -14,6 +14,7 @@ import 'core-js/modules/es.array.iterator'; import 'core-js/modules/es.math.sign'; import 'core-js/modules/es.number.is-finite'; import 'core-js/modules/es.object.assign'; +import 'core-js/modules/es.object.values'; import 'core-js/modules/es.promise'; import 'core-js/modules/es.promise.finally'; import 'core-js/modules/es.string.starts-with'; diff --git a/packages/component/src/Activity/StackedLayout.js b/packages/component/src/Activity/StackedLayout.js index bff89c8fb4..6e5775fc0d 100644 --- a/packages/component/src/Activity/StackedLayout.js +++ b/packages/component/src/Activity/StackedLayout.js @@ -88,8 +88,7 @@ const StackedLayout = ({ activity, avatarInitials, children, language, styleSet, channelData: { messageBack: { displayText: messageBackDisplayText } = {}, state } = {}, from: { role } = {}, text, - textFormat, - type + textFormat } = activity; const activityDisplayText = messageBackDisplayText || text; @@ -122,30 +121,20 @@ const StackedLayout = ({ activity, avatarInitials, children, language, styleSet, )}
- {type === 'typing' ? ( -
- {children({ - activity, - attachment: { contentType: 'typing' } - })} + {!!activityDisplayText && ( +
+ + + {children({ + activity, + attachment: { + content: activityDisplayText, + contentType: textFormatToContentType(textFormat) + } + })} +
- ) : ( - !!activityDisplayText && ( -
- - - {children({ - activity, - attachment: { - content: activityDisplayText, - contentType: textFormatToContentType(textFormat) - } - })} - -
-
- ) )} {/* Because of differences in browser implementations, aria-label=" " is used to make the screen reader not repeat the same text multiple times in Chrome v75 */} {attachments.map((attachment, index) => ( diff --git a/packages/component/src/Attachment/TypingActivity.js b/packages/component/src/Attachment/TypingActivity.js deleted file mode 100644 index 63cdb02b6f..0000000000 --- a/packages/component/src/Attachment/TypingActivity.js +++ /dev/null @@ -1,21 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; - -import { localize } from '../Localization/Localize'; -import connectToWebChat from '../connectToWebChat'; -import TypingAnimation from './Assets/TypingAnimation'; - -const TypingActivity = ({ language, styleSet }) => ( -
- -
-); - -TypingActivity.propTypes = { - language: PropTypes.string.isRequired, - styleSet: PropTypes.shape({ - typingActivity: PropTypes.any.isRequired - }).isRequired -}; - -export default connectToWebChat(({ language, styleSet }) => ({ language, styleSet }))(TypingActivity); diff --git a/packages/component/src/BasicSendBox.js b/packages/component/src/BasicSendBox.js index b4d171c8e0..b8beffc7f2 100644 --- a/packages/component/src/BasicSendBox.js +++ b/packages/component/src/BasicSendBox.js @@ -11,6 +11,7 @@ import MicrophoneButton from './SendBox/MicrophoneButton'; import SendButton from './SendBox/SendButton'; import SuggestedActions from './SendBox/SuggestedActions'; import TextBox from './SendBox/TextBox'; +import TypingIndicator from './SendBox/TypingIndicator'; import UploadButton from './SendBox/UploadButton'; const { @@ -29,6 +30,7 @@ const TEXT_BOX_CSS = css({ flex: 10000 }); const BasicSendBox = ({ className, dictationStarted, styleSet, webSpeechPonyfill }) => (
+
diff --git a/packages/component/src/Middleware/Attachment/createCoreMiddleware.js b/packages/component/src/Middleware/Attachment/createCoreMiddleware.js index ea773f9d25..ef21bf5f00 100644 --- a/packages/component/src/Middleware/Attachment/createCoreMiddleware.js +++ b/packages/component/src/Middleware/Attachment/createCoreMiddleware.js @@ -6,7 +6,6 @@ import DownloadAttachment from '../../Attachment/DownloadAttachment'; import ImageAttachment from '../../Attachment/ImageAttachment'; import UploadAttachment from '../../Attachment/UploadAttachment'; import TextAttachment from '../../Attachment/TextAttachment'; -import TypingActivity from '../../Attachment/TypingActivity'; import VideoAttachment from '../../Attachment/VideoAttachment'; function hasThumbnail({ attachments = [], channelData: { attachmentThumbnails = [] } = {} }, attachment) { @@ -18,10 +17,13 @@ function hasThumbnail({ attachments = [], channelData: { attachmentThumbnails = // TODO: [P4] Rename this file or the whole middleware, it looks either too simple or too comprehensive now export default function createCoreMiddleware() { return () => next => { - const Attachment = ({ activity = {}, attachment, attachment: { contentType, contentUrl } = {} }) => - activity.type === 'typing' ? ( - - ) : activity.from.role === 'user' && !/^text\//u.test(contentType) && !hasThumbnail(activity, attachment) ? ( + const Attachment = ({ + activity = {}, + activity: { from: { role } } = {}, + attachment, + attachment: { contentType, contentUrl } = {} + }) => + role === 'user' && !/^text\//u.test(contentType) && !hasThumbnail(activity, attachment) ? ( ) : /^audio\//u.test(contentType) ? ( diff --git a/packages/component/src/Attachment/Assets/TypingAnimation.js b/packages/component/src/SendBox/Assets/TypingAnimation.js similarity index 100% rename from packages/component/src/Attachment/Assets/TypingAnimation.js rename to packages/component/src/SendBox/Assets/TypingAnimation.js diff --git a/packages/component/src/SendBox/TypingIndicator.js b/packages/component/src/SendBox/TypingIndicator.js new file mode 100644 index 0000000000..cf7de55efe --- /dev/null +++ b/packages/component/src/SendBox/TypingIndicator.js @@ -0,0 +1,55 @@ +import PropTypes from 'prop-types'; +import React, { useEffect, useState } from 'react'; + +import { localize } from '../Localization/Localize'; +import connectToWebChat from '../connectToWebChat'; +import TypingAnimation from './Assets/TypingAnimation'; + +const TypingIndicator = ({ + language, + lastTypingAt, + styleSet: { + options: { typingAnimationDuration }, + typingIndicator + } +}) => { + const [showTyping, setShowTyping] = useState(false); + + useEffect(() => { + let timeout; + const last = Math.max(Object.values(lastTypingAt)); + const typingAnimationTimeRemaining = typingAnimationDuration - Date.now() + last; + + if (last && typingAnimationTimeRemaining > 0) { + setShowTyping(true); + timeout = setTimeout(() => setShowTyping(false), typingAnimationTimeRemaining); + } else { + setShowTyping(false); + } + + return () => clearTimeout(timeout); + }, [lastTypingAt, typingAnimationDuration]); + + return ( + showTyping && ( +
+ +
+ ) + ); +}; + +TypingIndicator.propTypes = { + language: PropTypes.string.isRequired, + lastTypingAt: PropTypes.any.isRequired, + styleSet: PropTypes.shape({ + options: PropTypes.shape({ + typingAnimationDuration: PropTypes.number + }).isRequired, + typingIndicator: PropTypes.any.isRequired + }).isRequired +}; + +export default connectToWebChat(({ lastTypingAt, language, styleSet }) => ({ lastTypingAt, language, styleSet }))( + TypingIndicator +); diff --git a/packages/component/src/Styles/StyleSet/TypingActivity.js b/packages/component/src/Styles/StyleSet/TypingActivity.js deleted file mode 100644 index 6a89afcc2d..0000000000 --- a/packages/component/src/Styles/StyleSet/TypingActivity.js +++ /dev/null @@ -1,8 +0,0 @@ -export default function createTypingActivityStyle({ avatarSize }) { - return { - alignItems: 'center', - // TODO: [P2] We should not set "display" in styleSet, this will allow the user to break the layout for no good reasons. - display: 'flex', - height: avatarSize - }; -} diff --git a/packages/component/src/Styles/StyleSet/TypingIndicator.js b/packages/component/src/Styles/StyleSet/TypingIndicator.js new file mode 100644 index 0000000000..30ec74d630 --- /dev/null +++ b/packages/component/src/Styles/StyleSet/TypingIndicator.js @@ -0,0 +1,6 @@ +export default function createTypingIndicatorStyle({ paddingRegular }) { + return { + paddingBottom: paddingRegular, + paddingLeft: paddingRegular + }; +} diff --git a/packages/component/src/Styles/createStyleSet.js b/packages/component/src/Styles/createStyleSet.js index 2a8460f956..5361d61439 100644 --- a/packages/component/src/Styles/createStyleSet.js +++ b/packages/component/src/Styles/createStyleSet.js @@ -27,8 +27,8 @@ import createSuggestedActionsStyleSet from './StyleSet/SuggestedActionsStyleSet' import createSuggestedActionStyle from './StyleSet/SuggestedAction'; import createTextContentStyle from './StyleSet/TextContent'; import createTimestampStyle from './StyleSet/Timestamp'; -import createTypingActivityStyle from './StyleSet/TypingActivity'; import createTypingAnimationStyle from './StyleSet/TypingAnimation'; +import createTypingIndicatorStyle from './StyleSet/TypingIndicator'; import createUploadAttachmentStyle from './StyleSet/UploadAttachment'; import createUploadButtonStyle from './StyleSet/UploadButton'; import createVideoAttachmentStyle from './StyleSet/VideoAttachment'; @@ -142,8 +142,8 @@ export default function createStyleSet(options) { suggestedActions: createSuggestedActionsStyle(options), textContent: createTextContentStyle(options), timestamp: createTimestampStyle(options), - typingActivity: createTypingActivityStyle(options), typingAnimation: createTypingAnimationStyle(options), + typingIndicator: createTypingIndicatorStyle(options), uploadAttachment: createUploadAttachmentStyle(options), uploadButton: createUploadButtonStyle(options), videoAttachment: createVideoAttachmentStyle(options), diff --git a/packages/component/src/Styles/defaultStyleOptions.js b/packages/component/src/Styles/defaultStyleOptions.js index 787efe7418..0624c58b41 100644 --- a/packages/component/src/Styles/defaultStyleOptions.js +++ b/packages/component/src/Styles/defaultStyleOptions.js @@ -125,6 +125,7 @@ const DEFAULT_OPTIONS = { notificationText: '#5E5E5E', typingAnimationBackgroundImage: null, + typingAnimationDuration: 5000, typingAnimationHeight: 20, typingAnimationWidth: 64, diff --git a/packages/core/src/reducer.ts b/packages/core/src/reducer.ts index d62d2fabb0..1a1904caa3 100644 --- a/packages/core/src/reducer.ts +++ b/packages/core/src/reducer.ts @@ -6,6 +6,7 @@ import connectivityStatus from './reducers/connectivityStatus'; import dictateInterims from './reducers/dictateInterims'; import dictateState from './reducers/dictateState'; import language from './reducers/language'; +import lastTypingAt from './reducers/lastTypingAt'; import readyState from './reducers/readyState'; import referenceGrammarID from './reducers/referenceGrammarID'; import sendBoxValue from './reducers/sendBoxValue'; @@ -21,6 +22,7 @@ export default combineReducers({ dictateInterims, dictateState, language, + lastTypingAt, readyState, referenceGrammarID, sendBoxValue, diff --git a/packages/core/src/reducers/activities.js b/packages/core/src/reducers/activities.js index 7042d69c1c..20e9eb38d9 100644 --- a/packages/core/src/reducers/activities.js +++ b/packages/core/src/reducers/activities.js @@ -21,39 +21,23 @@ function findByClientActivityID(clientActivityID) { } function upsertActivityWithSort(activities, nextActivity) { - const { - channelData: { clientActivityID: nextClientActivityID } = {}, - from: { id: nextFromID, role: nextFromRole } = {}, - type: nextType - } = nextActivity; - - if (nextType === 'typing' && nextFromRole === 'user') { - return activities; - } + const { channelData: { clientActivityID: nextClientActivityID } = {} } = nextActivity; const nextTimestamp = Date.parse(nextActivity.timestamp); const nextActivities = activities.filter( - ({ channelData: { clientActivityID } = {}, from, type }) => - // We will remove all "typing" and "sending messages" activities + ({ channelData: { clientActivityID } = {} }) => + // We will remove all "sending messages" activities // "clientActivityID" is unique and used to track if the message has been sent and echoed back from the server - !( - (type === 'typing' && from.id === nextFromID) || - (nextClientActivityID && clientActivityID === nextClientActivityID) - ) + !(nextClientActivityID && clientActivityID === nextClientActivityID) ); - // Then, find the right (sorted) place to insert the new activity at, based on timestamp, and must be before "typing" + // Then, find the right (sorted) place to insert the new activity at, based on timestamp // Since clockskew might happen, we will ignore timestamp on messages that are sending - // If we are inserting "typing", we will always append it - // TODO: [P4] Move "typing" into Constants.ActivityType - const indexToInsert = - nextActivity.type === 'typing' - ? -1 - : nextActivities.findIndex( - ({ channelData: { state } = {}, timestamp, type }) => - (Date.parse(timestamp) > nextTimestamp && state !== SENDING && state !== SEND_FAILED) || type === 'typing' - ); + const indexToInsert = nextActivities.findIndex( + ({ channelData: { state } = {}, timestamp }) => + Date.parse(timestamp) > nextTimestamp && state !== SENDING && state !== SEND_FAILED + ); // If no right place are found, append it nextActivities.splice(~indexToInsert ? indexToInsert : nextActivities.length, 0, nextActivity); @@ -97,7 +81,8 @@ export default function activities(state = DEFAULT_STATE, { meta, payload, type case INCOMING_ACTIVITY: // UpdateActivity is not supported right now because we ignore duplicated activity ID - if (!~state.findIndex(({ id }) => id === payload.activity.id)) { + // TODO: [P4] Move "typing" into Constants.ActivityType + if (payload.activity.type !== 'typing' && !~state.findIndex(({ id }) => id === payload.activity.id)) { state = upsertActivityWithSort(state, payload.activity); } diff --git a/packages/core/src/reducers/lastTypingAt.js b/packages/core/src/reducers/lastTypingAt.js new file mode 100644 index 0000000000..5d7c4288cf --- /dev/null +++ b/packages/core/src/reducers/lastTypingAt.js @@ -0,0 +1,29 @@ +/*eslint no-case-declarations: "off"*/ +/*eslint no-unused-vars: "off"*/ + +import { INCOMING_ACTIVITY } from '../actions/incomingActivity'; + +const DEFAULT_STATE = {}; + +export default function lastTypingAt(state = DEFAULT_STATE, { payload, type }) { + if (type === INCOMING_ACTIVITY) { + const { + activity: { + from: { id, role }, + type: activityType + } + } = payload; + + if (role === 'bot') { + if (activityType === 'typing') { + state = { ...state, [id]: Date.now() }; + } else if (activityType === 'message') { + const { [id]: last, ...nextState } = state; + + state = nextState; + } + } + } + + return state; +} diff --git a/packages/core/src/sagas.js b/packages/core/src/sagas.js index eebb9da07d..14af433c64 100644 --- a/packages/core/src/sagas.js +++ b/packages/core/src/sagas.js @@ -7,7 +7,6 @@ import detectSlowConnectionSaga from './sagas/detectSlowConnectionSaga'; import incomingActivitySaga from './sagas/incomingActivitySaga'; import markAllAsSpokenOnStopSpeakActivitySaga from './sagas/markAllAsSpokenOnStopSpeakActivitySaga'; import postActivitySaga from './sagas/postActivitySaga'; -import removeIncomingTypingAfterIntervalSaga from './sagas/removeIncomingTypingAfterIntervalSaga'; import sendEventToPostActivitySaga from './sagas/sendEventToPostActivitySaga'; import sendFilesToPostActivitySaga from './sagas/sendFilesToPostActivitySaga'; import sendMessageBackToPostActivitySaga from './sagas/sendMessageBackToPostActivitySaga'; @@ -30,7 +29,6 @@ export default function* sagas() { yield fork(incomingActivitySaga); yield fork(markAllAsSpokenOnStopSpeakActivitySaga); yield fork(postActivitySaga); - yield fork(removeIncomingTypingAfterIntervalSaga); yield fork(sendEventToPostActivitySaga); yield fork(sendFilesToPostActivitySaga); yield fork(sendMessageBackToPostActivitySaga); diff --git a/packages/core/src/sagas/removeIncomingTypingAfterIntervalSaga.js b/packages/core/src/sagas/removeIncomingTypingAfterIntervalSaga.js deleted file mode 100644 index a63a69729a..0000000000 --- a/packages/core/src/sagas/removeIncomingTypingAfterIntervalSaga.js +++ /dev/null @@ -1,29 +0,0 @@ -import { call, put, takeEvery } from 'redux-saga/effects'; - -import { INCOMING_ACTIVITY } from '../actions/incomingActivity'; -import deleteActivity from '../actions/deleteActivity'; - -const REMOVE_TYPING_ACTIVITY_AFTER = 5000; - -function sleep(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); -} - -function* removeActivityAfterInterval({ - payload: { - activity: { id } - } -}) { - // TODO: [P2] We could optimize here. - // Given there is an activity typing activity, when the bot send another typing activity, we will remove the first one. - // That means, we don't actually need to remove it anymore, and could cancel out this call. - yield call(sleep, REMOVE_TYPING_ACTIVITY_AFTER); - yield put(deleteActivity(id)); -} - -export default function*() { - yield takeEvery( - ({ type, payload }) => type === INCOMING_ACTIVITY && payload.activity.type === 'typing', - removeActivityAfterInterval - ); -}