From 9a5c564bd779cdb57e414114865bc02ac1c75f4b Mon Sep 17 00:00:00 2001 From: William Wong Date: Thu, 1 Nov 2018 03:55:11 -0700 Subject: [PATCH 01/27] Send join event and various fixes --- CHANGELOG.md | 9 +++ packages/component/src/Composer.js | 2 + packages/core/src/actions/connect.js | 2 + packages/core/src/actions/sendEvent.js | 10 ++++ packages/core/src/index.js | 2 + packages/core/src/sagas.js | 2 + packages/core/src/sagas/connectSaga.js | 2 + .../core/src/sagas/effects/whileConnected.js | 4 +- .../src/sagas/sendEventToPostActivitySaga.js | 23 ++++++++ .../src/sagas/sendFilesToPostActivitySaga.js | 8 +-- .../sagas/sendMessageToPostActivitySaga.js | 8 +-- .../sagas/sendPostBackToPostActivitySaga.js | 8 +-- .../src/sagas/stopSpeakActivityOnInputSaga.js | 33 ++++++----- .../05.c.presentation-mode-styling/index.html | 14 +++-- .../index.html | 9 ++- .../11.customization-redux-actions/index.html | 3 +- samples/send-join-event/index.html | 56 +++++++++++++++++++ 17 files changed, 153 insertions(+), 42 deletions(-) create mode 100644 packages/core/src/actions/sendEvent.js create mode 100644 packages/core/src/sagas/sendEventToPostActivitySaga.js create mode 100644 samples/send-join-event/index.html diff --git a/CHANGELOG.md b/CHANGELOG.md index e513478297..f723a50309 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Fix [#1383](https://github.com/Microsoft/BotFramework-WebChat/issues/1383). Added options to hide upload button, by [@compulim](https://github.com/compulim) in PR [#1491](https://github.com/Microsoft/BotFramework-WebChat/pull/1491) - Added support of avatar image, thru `styleOptions.botAvatarImage` and `styleOptions.userAvatarImage`, in PR [#1486](https://github.com/Microsoft/BotFramework-WebChat/pull/1486) - Added ability to style sendbox background and text color, thru `styleOptions.sendBoxBackground` and `styleOptions.sendBoxTextColor`, in PR [#1575](https://github.com/Microsoft/BotFramework-WebChat/pull/1575) +- Core: Added `sendEvent`, in PR [#1286](https://github.com/Microsoft/BotFramework-WebChat/pull/1286) +- Core: Added `CONNECT_FULFILLING` action to workaround `redux-saga` [design decision](https://github.com/redux-saga/redux-saga/issues/1651), in PR [#1286](https://github.com/Microsoft/BotFramework-WebChat/pull/1286) ### Changed - Moved `botAvatarImage` and `userAvatarImage` to `styleOptions.botAvatarImage` and `styleOptions.userAvatarImage` respectively, in PR [#1486](https://github.com/Microsoft/BotFramework-WebChat/pull/1486) @@ -43,6 +45,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - `component`: Fix [#1560](https://github.com/Microsoft/BotFramework-WebChat/issues/1560). Fixed carousel layout did not show date and alignment issues, by [@compulim](https://github.com/compulim) in PR [#1561](https://github.com/Microsoft/BotFramework-WebChat/pull/1561) - `playground`: Fix [#1562](https://github.com/Microsoft/BotFramework-WebChat/issues/1562). Fixed timestamp grouping "Don't group" and added "Don't show timestamp", by [@compulim](https://github.com/compulim) in PR [#1563](https://github.com/Microsoft/BotFramework-WebChat/pull/1563) - `component`: Fix [#1576](https://github.com/Microsoft/BotFramework-WebChat/issues/1576). Rich card without `tap` should be rendered properly, by [@compulim](https://github.com/compulim) in PR [#1577](https://github.com/Microsoft/BotFramework-WebChat/pull/1577) +- `core`: Some sagas missed handling successive actions, in PR [#1286](https://github.com/Microsoft/BotFramework-WebChat/pull/1286) + - `sendFilesToPostActivitySaga` + - `sendMessageToPostActivitySaga` + - `sendPostBackToPostActivitySaga` + - `stopSpeakActivityOnInputSaga` +- `core`: `incomingActivitySaga` may null-ref if the first activity is from user, in PR [#1286](https://github.com/Microsoft/BotFramework-WebChat/pull/1286) ### Removed - `botAvatarImage` and `userAvatarImage` props, as they are moved inside `styleOptions`, in PR [#1486](https://github.com/Microsoft/BotFramework-WebChat/pull/1486) @@ -52,6 +60,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - `component`: [Hide upload button](https://microsoft.github.io/BotFramework-WebChat/05.d.hide-upload-button-styling/), in [#1491](https://github.com/Microsoft/BotFramework-WebChat/pull/1491) - `component`: [Avatar image](https://microsoft.github.io/BotFramework-WebChat/04.b.display-user-bot-images-styling/), in [#1486](https://github.com/Microsoft/BotFramework-WebChat/pull/1486) - `core`: [Incoming activity to JavaScript event](https://microsoft.github.io/BotFramework-WebChat/15.b.incoming-activity-event/), in [#1567](https://github.com/Microsoft/BotFramework-WebChat/pull/1567) +- Backchannel: New send join event sample, in PR [#1286](https://github.com/Microsoft/BotFramework-WebChat/pull/1286) ## [4.2.0] - 2018-12-11 ### Added diff --git a/packages/component/src/Composer.js b/packages/component/src/Composer.js index eec02f387e..6e2ce35249 100644 --- a/packages/component/src/Composer.js +++ b/packages/component/src/Composer.js @@ -15,6 +15,7 @@ import { disconnect, markActivity, postActivity, + sendEvent, sendFiles, sendMessage, sendPostBack, @@ -44,6 +45,7 @@ const EMPTY_ARRAY = []; const DISPATCHERS = { markActivity, postActivity, + sendEvent, sendFiles, sendMessage, sendPostBack, diff --git a/packages/core/src/actions/connect.js b/packages/core/src/actions/connect.js index 45ac4111ac..0c6f680ea2 100644 --- a/packages/core/src/actions/connect.js +++ b/packages/core/src/actions/connect.js @@ -1,6 +1,7 @@ const CONNECT = 'DIRECT_LINE/CONNECT'; const CONNECT_PENDING = `${ CONNECT }_PENDING`; const CONNECT_REJECTED = `${ CONNECT }_REJECTED`; +const CONNECT_FULFILLING = `${ CONNECT }_FULFILLING`; const CONNECT_FULFILLED = `${ CONNECT }_FULFILLED`; export default function ({ directLine, userID }) { @@ -14,5 +15,6 @@ export { CONNECT, CONNECT_PENDING, CONNECT_REJECTED, + CONNECT_FULFILLING, CONNECT_FULFILLED } diff --git a/packages/core/src/actions/sendEvent.js b/packages/core/src/actions/sendEvent.js new file mode 100644 index 0000000000..c9f7b76d5d --- /dev/null +++ b/packages/core/src/actions/sendEvent.js @@ -0,0 +1,10 @@ +const SEND_EVENT = 'WEB_CHAT/SEND_EVENT'; + +export default function sendEvent(name, value) { + return { + type: SEND_EVENT, + payload: { name, value } + }; +} + +export { SEND_EVENT } diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 333dc309ba..bc00f61782 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -3,6 +3,7 @@ import createStore from './createStore'; import disconnect from './actions/disconnect'; import markActivity from './actions/markActivity'; import postActivity from './actions/postActivity'; +import sendEvent from './actions/sendEvent'; import sendFiles from './actions/sendFiles'; import sendMessage from './actions/sendMessage'; import sendPostBack from './actions/sendPostBack'; @@ -31,6 +32,7 @@ export { disconnect, markActivity, postActivity, + sendEvent, sendFiles, sendMessage, sendPostBack, diff --git a/packages/core/src/sagas.js b/packages/core/src/sagas.js index 7f7fedc687..159a4f9c8a 100644 --- a/packages/core/src/sagas.js +++ b/packages/core/src/sagas.js @@ -7,6 +7,7 @@ import incomingActivitySaga from './sagas/incomingActivitySaga'; import incomingTypingSaga from './sagas/incomingTypingSaga'; import markActivityForSpeakSaga from './sagas/markActivityForSpeakSaga'; import postActivitySaga from './sagas/postActivitySaga'; +import sendEventToPostActivitySaga from './sagas/sendEventToPostActivitySaga'; import sendFilesToPostActivitySaga from './sagas/sendFilesToPostActivitySaga'; import sendMessageToPostActivitySaga from './sagas/sendMessageToPostActivitySaga'; import sendPostBackToPostActivitySaga from './sagas/sendPostBackToPostActivitySaga'; @@ -24,6 +25,7 @@ export default function* () { yield fork(incomingTypingSaga); yield fork(markActivityForSpeakSaga); yield fork(postActivitySaga); + yield fork(sendEventToPostActivitySaga); yield fork(sendFilesToPostActivitySaga); yield fork(sendMessageToPostActivitySaga); yield fork(sendPostBackToPostActivitySaga); diff --git a/packages/core/src/sagas/connectSaga.js b/packages/core/src/sagas/connectSaga.js index 9e49ae1449..ec12bc8127 100644 --- a/packages/core/src/sagas/connectSaga.js +++ b/packages/core/src/sagas/connectSaga.js @@ -18,6 +18,7 @@ import { CONNECT, CONNECT_PENDING, CONNECT_REJECTED, + CONNECT_FULFILLING, CONNECT_FULFILLED } from '../actions/connect'; @@ -83,6 +84,7 @@ function* connectSaga(directLine, userID) { try { try { yield callUntil(connectionStatusQueue.shift, [], connectionStatus => connectionStatus === ONLINE); + yield put({ type: CONNECT_FULFILLING, meta, payload: { directLine } }); yield put({ type: CONNECT_FULFILLED, meta, payload: { directLine } }); } catch (err) { yield put({ type: CONNECT_REJECTED, error: true, meta, payload: err }); diff --git a/packages/core/src/sagas/effects/whileConnected.js b/packages/core/src/sagas/effects/whileConnected.js index 95baeb9450..7adb6c73f2 100644 --- a/packages/core/src/sagas/effects/whileConnected.js +++ b/packages/core/src/sagas/effects/whileConnected.js @@ -5,13 +5,13 @@ import { take } from 'redux-saga/effects'; -import { CONNECT_FULFILLED } from '../../actions/connect'; +import { CONNECT_FULFILLING } from '../../actions/connect'; import { DISCONNECT_FULFILLED } from '../../actions/disconnect'; export default function (fn) { return call(function* () { for (;;) { - const { meta: { userID }, payload: { directLine } } = yield take(CONNECT_FULFILLED); + const { meta: { userID }, payload: { directLine } } = yield take(CONNECT_FULFILLING); const task = yield fork(fn, directLine, userID); yield take(DISCONNECT_FULFILLED); diff --git a/packages/core/src/sagas/sendEventToPostActivitySaga.js b/packages/core/src/sagas/sendEventToPostActivitySaga.js new file mode 100644 index 0000000000..780df192fa --- /dev/null +++ b/packages/core/src/sagas/sendEventToPostActivitySaga.js @@ -0,0 +1,23 @@ +import { + put, + takeEvery +} from 'redux-saga/effects'; + +import whileConnected from './effects/whileConnected'; + +import { SEND_EVENT } from '../actions/sendEvent'; +import postActivity from '../actions/postActivity'; + +export default function* () { + yield whileConnected(function* () { + yield takeEvery(SEND_EVENT, function* ({ payload: { name, value } }) { + if (name) { + yield put(postActivity({ + name, + type: 'event', + value + })); + } + }); + }); +} diff --git a/packages/core/src/sagas/sendFilesToPostActivitySaga.js b/packages/core/src/sagas/sendFilesToPostActivitySaga.js index 2612b99fbc..d4f7acff1a 100644 --- a/packages/core/src/sagas/sendFilesToPostActivitySaga.js +++ b/packages/core/src/sagas/sendFilesToPostActivitySaga.js @@ -1,6 +1,6 @@ import { put, - take + takeEvery } from 'redux-saga/effects'; import mime from 'mime'; @@ -15,9 +15,7 @@ const getType = mime.getType.bind(mime); export default function* () { yield whileConnected(function* () { - for (;;) { - const { payload: { files } } = yield take(SEND_FILES); - + yield takeEvery(SEND_FILES, function* ({ payload: { files } }) { if (files.length) { yield put(postActivity({ attachments: [].map.call(files, file => ({ @@ -33,6 +31,6 @@ export default function* () { yield put(stopSpeakingActivity()); } - } + }); }); } diff --git a/packages/core/src/sagas/sendMessageToPostActivitySaga.js b/packages/core/src/sagas/sendMessageToPostActivitySaga.js index 2c63b3ca39..9547cc5d6e 100644 --- a/packages/core/src/sagas/sendMessageToPostActivitySaga.js +++ b/packages/core/src/sagas/sendMessageToPostActivitySaga.js @@ -1,6 +1,6 @@ import { put, - take + takeEvery } from 'redux-saga/effects'; import whileConnected from './effects/whileConnected'; @@ -12,9 +12,7 @@ import stopSpeakingActivity from '../actions/stopSpeakingActivity'; export default function* () { yield whileConnected(function* () { - for (;;) { - const { payload: { text, via } } = yield take(SEND_MESSAGE); - + yield takeEvery(SEND_MESSAGE, function* ({ payload: { text, via } }) { if (text) { yield put(postActivity({ text, @@ -28,6 +26,6 @@ export default function* () { yield put(stopSpeakingActivity()); } } - } + }); }); } diff --git a/packages/core/src/sagas/sendPostBackToPostActivitySaga.js b/packages/core/src/sagas/sendPostBackToPostActivitySaga.js index 4e45e952a1..79bada0d49 100644 --- a/packages/core/src/sagas/sendPostBackToPostActivitySaga.js +++ b/packages/core/src/sagas/sendPostBackToPostActivitySaga.js @@ -1,6 +1,6 @@ import { put, - take + takeEvery } from 'redux-saga/effects'; import whileConnected from './effects/whileConnected'; @@ -10,9 +10,7 @@ import postActivity from '../actions/postActivity'; export default function* () { yield whileConnected(function* () { - for (;;) { - const { payload: { value } } = yield take(SEND_POST_BACK); - + yield takeEvery(SEND_POST_BACK, function* ({ payload: { value } }) { if (value) { yield put(postActivity({ channelData: { @@ -23,6 +21,6 @@ export default function* () { value: typeof value !== 'string' ? value : undefined })); } - } + }); }); } diff --git a/packages/core/src/sagas/stopSpeakActivityOnInputSaga.js b/packages/core/src/sagas/stopSpeakActivityOnInputSaga.js index 511c5a4db4..b83e0a0d80 100644 --- a/packages/core/src/sagas/stopSpeakActivityOnInputSaga.js +++ b/packages/core/src/sagas/stopSpeakActivityOnInputSaga.js @@ -1,7 +1,7 @@ import { put, select, - take + takeEvery } from 'redux-saga/effects'; import whileConnected from './effects/whileConnected'; @@ -14,26 +14,25 @@ import { START_DICTATE } from '../actions/startDictate'; export default function* () { yield whileConnected(function* () { - for (;;) { - yield take( - ({ payload, type }) => type === START_DICTATE - || (type === SET_SEND_BOX && payload.text && payload.via !== 'speech') + yield takeEvery( + ({ payload, type }) => type === START_DICTATE + || (type === SET_SEND_BOX && payload.text && payload.via !== 'speech') - // We want to stop speaking activity when the user click on a card action - // But currently there are no actions generated out of a card action - // So, right now, we are using best-effort by listening to POST_ACTIVITY_PENDING with a "message" event - || (type === POST_ACTIVITY_PENDING && payload.activity.type === 'message') - ); + // We want to stop speaking activity when the user click on a card action + // But currently there are no actions generated out of a card action + // So, right now, we are using best-effort by listening to POST_ACTIVITY_PENDING with a "message" event + || (type === POST_ACTIVITY_PENDING && payload.activity.type === 'message'), + function* () { + yield put(stopSpeakingActivity()); - yield put(stopSpeakingActivity()); + const activities = yield select(({ activities }) => activities); - const activities = yield select(({ activities }) => activities); - - for (let activity of activities) { - if (activity.channelData && activity.channelData.speak) { - yield put(markActivity(activity, 'speak', false)); + for (let activity of activities) { + if (activity.channelData && activity.channelData.speak) { + yield put(markActivity(activity, 'speak', false)); + } } } - } + ); }); } diff --git a/samples/05.c.presentation-mode-styling/index.html b/samples/05.c.presentation-mode-styling/index.html index 81dc342ede..70c94c431e 100644 --- a/samples/05.c.presentation-mode-styling/index.html +++ b/samples/05.c.presentation-mode-styling/index.html @@ -30,8 +30,17 @@ const res = await fetch('https://webchat-mockbot.azurewebsites.net/directline/token', { method: 'POST' }); const { token } = await res.json(); + const store = window.WebChat.createStore({}, ({ dispatch }) => next => action => { + // The following code is for convenient when trying out this sample, not required for production. + if (action.type === 'DIRECT_LINE/CONNECT_FULFILLED') { + dispatch({ type: 'WEB_CHAT/SEND_MESSAGE', payload: { text: 'card inputs' } }); - const store = window.WebChat.createStore(); + // Although suggested actions were sent from the bot, we hide the send box, thus, all suggested actions are also hidden + dispatch({ type: 'WEB_CHAT/SEND_MESSAGE', payload: { text: 'suggested-actions' } }); + } + + return next(action); + }); window.WebChat.renderWebChat({ directLine: window.WebChat.createDirectLine({ token }), @@ -41,9 +50,6 @@ hideSendBox: true } }, document.getElementById('webchat')); - - setTimeout(() => store.dispatch({ type: 'WEB_CHAT/SEND_MESSAGE', payload: { text: 'card inputs' } }), 1000); - setTimeout(() => store.dispatch({ type: 'WEB_CHAT/SEND_MESSAGE', payload: { text: 'suggested-actions' } }), 1500); })().catch(err => console.error(err)); diff --git a/samples/07.customization-timestamp-grouping/index.html b/samples/07.customization-timestamp-grouping/index.html index b4311e7389..8768d14400 100644 --- a/samples/07.customization-timestamp-grouping/index.html +++ b/samples/07.customization-timestamp-grouping/index.html @@ -87,8 +87,14 @@ const res = await fetch('https://webchat-mockbot.azurewebsites.net/directline/token', { method: 'POST' }); const { token } = await res.json(); + const store = window.WebChat.createStore({}, ({ dispatch }) => next => action => { + // The following code is for convenient when trying out this sample, not required for production. + if (action.type === 'DIRECT_LINE/CONNECT_FULFILLED') { + dispatch({ type: 'WEB_CHAT/SEND_MESSAGE', payload: { text: 'timestamp' } }); + } - const store = window.WebChat.createStore(); + return next(action); + }); window.WebChat.renderWebChat({ directLine: window.WebChat.createDirectLine({ token }), @@ -103,7 +109,6 @@ // The following code is for convenient when trying out this sample, not required for production. document.querySelector('#webchat > *').focus(); - setTimeout(() => store.dispatch({ type: 'WEB_CHAT/SEND_MESSAGE', payload: { text: 'timestamp' } }), 1000); })().catch(err => console.error(err)); diff --git a/samples/11.customization-redux-actions/index.html b/samples/11.customization-redux-actions/index.html index 9523f0e31f..8e56685c57 100644 --- a/samples/11.customization-redux-actions/index.html +++ b/samples/11.customization-redux-actions/index.html @@ -35,8 +35,7 @@ const store = window.WebChat.createStore( {}, ({ dispatch }) => next => action => { - // TODO: [P4] Investigate why we need to wait until DIRECT_LINE/CONNECTION_STATUS_UPDATE, instead of DIRECT_LINE/CONNECT_FULFILLED. - if (action.type === 'DIRECT_LINE/CONNECTION_STATUS_UPDATE' && action.payload.connectionStatus === 2) { + if (action.type === 'DIRECT_LINE/CONNECT_FULFILLED') { // After connected, we will send a message by dispatching a Redux action. dispatch({ type: 'WEB_CHAT/SEND_MESSAGE', payload: { text: 'sample:backchannel' } }); } else if (action.type === 'DIRECT_LINE/INCOMING_ACTIVITY') { diff --git a/samples/send-join-event/index.html b/samples/send-join-event/index.html new file mode 100644 index 0000000000..2bd72027fa --- /dev/null +++ b/samples/send-join-event/index.html @@ -0,0 +1,56 @@ + + + + Web Chat: Full-featured bundle + + + + + +
+ + + From 4de7ee1ad12271316eaf4ba884ccb51f0830390d Mon Sep 17 00:00:00 2001 From: William Wong Date: Sun, 4 Nov 2018 01:51:17 -0700 Subject: [PATCH 02/27] Cleanup --- ...clearSuggestedActionsOnPostActivitySaga.js | 19 +- .../src/sagas/markActivityForSpeakSaga.js | 28 +-- packages/core/src/sagas/postActivitySaga.js | 162 +++++++++--------- .../src/sagas/sendEventToPostActivitySaga.js | 10 +- .../src/sagas/sendFilesToPostActivitySaga.js | 12 +- .../sagas/sendPostBackToPostActivitySaga.js | 13 +- .../src/sagas/sendTypingOnSetSendBoxSaga.js | 10 +- .../startDictateAfterSpeakActivitySaga.js | 38 ++-- .../core/src/sagas/stopDictateOnCardAction.js | 27 +-- .../src/sagas/stopSpeakActivityOnInputSaga.js | 1 + packages/core/src/sagas/submitSendBoxSaga.js | 7 +- 11 files changed, 179 insertions(+), 148 deletions(-) diff --git a/packages/core/src/sagas/clearSuggestedActionsOnPostActivitySaga.js b/packages/core/src/sagas/clearSuggestedActionsOnPostActivitySaga.js index e92722e7f9..491eaa4827 100644 --- a/packages/core/src/sagas/clearSuggestedActionsOnPostActivitySaga.js +++ b/packages/core/src/sagas/clearSuggestedActionsOnPostActivitySaga.js @@ -1,17 +1,24 @@ import { put, - take + takeEvery } from 'redux-saga/effects'; -import { POST_ACTIVITY_PENDING } from '../actions/postActivity'; import whileConnected from './effects/whileConnected'; + +import { POST_ACTIVITY_PENDING } from '../actions/postActivity'; import setSuggestedActions from '../actions/setSuggestedActions'; export default function* () { yield whileConnected(function* () { - for (;;) { - yield take(({ payload, type }) => type === POST_ACTIVITY_PENDING && payload.activity.type === 'message'); - yield put(setSuggestedActions()); - } + yield takeEvery( + ({ payload, type }) => + type === POST_ACTIVITY_PENDING + && payload + && payload.activity + && payload.activity.type === 'message', + function* () { + yield put(setSuggestedActions()); + } + ); }); } diff --git a/packages/core/src/sagas/markActivityForSpeakSaga.js b/packages/core/src/sagas/markActivityForSpeakSaga.js index e1c50f81eb..0edc35b96e 100644 --- a/packages/core/src/sagas/markActivityForSpeakSaga.js +++ b/packages/core/src/sagas/markActivityForSpeakSaga.js @@ -2,7 +2,8 @@ import { cancel, fork, put, - take + take, + takeEvery } from 'redux-saga/effects'; import speakableActivity from './definition/speakableActivity'; @@ -18,22 +19,21 @@ export default function* () { for (;;) { yield take(START_SPEAKING_ACTIVITY); - const task = yield fork(markActivityForSpeakSaga, userID); + const task = fork(function* () { + yield takeEvery( + ({ payload, type }) => + type === INCOMING_ACTIVITY + && payload + && payload.activity + && speakableActivity(payload.activity, userID), + function* ({ payload: { activity } }) { + yield put(markActivity(activity, 'speak', true)); + } + ); + }); yield take(STOP_SPEAKING_ACTIVITY); yield cancel(task); } }); } - -function* markActivityForSpeakSaga(userID) { - for (;;) { - const { payload: { activity } } = yield take( - ({ payload: { activity } = {}, type }) => - type === INCOMING_ACTIVITY - && speakableActivity(activity, userID) - ); - - yield put(markActivity(activity, 'speak', true)); - } -} diff --git a/packages/core/src/sagas/postActivitySaga.js b/packages/core/src/sagas/postActivitySaga.js index 75f44aad46..adce38e9ad 100644 --- a/packages/core/src/sagas/postActivitySaga.js +++ b/packages/core/src/sagas/postActivitySaga.js @@ -2,11 +2,11 @@ import { all, call, cancelled, - fork, put, race, select, - take + take, + takeEvery } from 'redux-saga/effects'; import sleep from '../utils/sleep'; @@ -29,89 +29,85 @@ import { INCOMING_ACTIVITY } from '../actions/incomingActivity'; export default function* () { yield whileConnected(function* (directLine, userID) { - for (let numActivitiesPosted = 0;; numActivitiesPosted++) { - const action = yield take(POST_ACTIVITY); - - yield fork(postActivity, directLine, userID, numActivitiesPosted, action); - } - }); -} + let numActivitiesPosted = 0; + + yield takeEvery(POST_ACTIVITY, function* ({ payload: { activity } }) { + const locale = yield select(({ language }) => language); + const { attachments, channelData: { clientActivityID = uniqueID() } = {} } = activity; + + activity = { + ...deleteKey(activity, 'id'), + attachments: attachments && attachments.map(({ contentType, contentUrl, name }) => ({ + contentType, + contentUrl, + name + })), + channelData: { + clientActivityID, + ...deleteKey(activity.channelData, 'state') + }, + channelId: 'webchat', + from: { + id: userID, + role: 'user' + }, + locale, + timestamp: getTimestamp() + }; + + if (!numActivitiesPosted++) { + activity.entities = [...activity.entities || [], { + // TODO: [P4] Currently in v3, we send the capabilities although the client might not actually have them + // We need to understand why we need to send these, and only send capabilities the client have + requiresBotState: true, + supportsListening: true, + supportsTts: true, + type: 'ClientCapabilities' + }]; + } -function* postActivity(directLine, userID, numActivitiesPosted, { payload: { activity } }) { - const locale = yield select(({ language }) => language); - const { attachments, channelData: { clientActivityID = uniqueID() } = {} } = activity; - - activity = { - ...deleteKey(activity, 'id'), - attachments: attachments && attachments.map(({ contentType, contentUrl, name }) => ({ - contentType, - contentUrl, - name - })), - channelData: { - clientActivityID, - ...deleteKey(activity.channelData, 'state') - }, - channelId: 'webchat', - from: { - id: userID, - role: 'user' - }, - locale, - timestamp: getTimestamp() - }; - - if (!numActivitiesPosted) { - activity.entities = [...activity.entities || [], { - // TODO: [P4] Currently in v3, we send the capabilities although the client might not actually have them - // We need to understand why we need to send these, and only send capabilities the client have - requiresBotState: true, - supportsListening: true, - supportsTts: true, - type: 'ClientCapabilities' - }]; - } - - const meta = { clientActivityID }; - - yield put({ type: POST_ACTIVITY_PENDING, payload: { activity }, meta }); - - try { - // Quirks: We might receive INCOMING_ACTIVITY before the postActivity call completed - // So, we setup expectation first, then postActivity afterward - - const echoBackCall = call(function* () { - for (;;) { - const { payload: { activity } } = yield take(INCOMING_ACTIVITY); - const { channelData = {}, id } = activity; - - if (channelData.clientActivityID === clientActivityID && id) { - return activity; + const meta = { clientActivityID }; + + yield put({ type: POST_ACTIVITY_PENDING, payload: { activity }, meta }); + + try { + // Quirks: We might receive INCOMING_ACTIVITY before the postActivity call completed + // So, we setup expectation first, then postActivity afterward + + const echoBackCall = call(function* () { + for (;;) { + const { payload: { activity } } = yield take(INCOMING_ACTIVITY); + const { channelData = {}, id } = activity; + + if (channelData.clientActivityID === clientActivityID && id) { + return activity; + } + } + }); + + // Timeout could be due to either: + // - Post activity call may take too long time to complete + // - Direct Line service only respond on HTTP after bot respond to Direct Line + // - Activity may take too long time to echo back + + const sendTimeout = yield select(({ sendTimeout }) => sendTimeout); + + const { send: { echoBack } } = yield race({ + send: all({ + echoBack: echoBackCall, + postActivity: observeOnce(directLine.postActivity(activity)) + }), + timeout: call(() => sleep(sendTimeout).then(() => Promise.reject(new Error('timeout')))) + }); + + yield put({ type: POST_ACTIVITY_FULFILLED, meta, payload: { activity: echoBack } }); + } catch (err) { + yield put({ type: POST_ACTIVITY_REJECTED, error: true, meta, payload: err }); + } finally { + if (yield cancelled()) { + yield put({ type: POST_ACTIVITY_REJECTED, error: true, meta, payload: new Error('cancelled') }); } } }); - - // Timeout could be due to either: - // - Post activity call may take too long time to complete - // - Direct Line service only respond on HTTP after bot respond to Direct Line - // - Activity may take too long time to echo back - - const sendTimeout = yield select(({ sendTimeout }) => sendTimeout); - - const { send: { echoBack } } = yield race({ - send: all({ - echoBack: echoBackCall, - postActivity: observeOnce(directLine.postActivity(activity)) - }), - timeout: call(() => sleep(sendTimeout).then(() => Promise.reject(new Error('timeout')))) - }); - - yield put({ type: POST_ACTIVITY_FULFILLED, meta, payload: { activity: echoBack } }); - } catch (err) { - yield put({ type: POST_ACTIVITY_REJECTED, error: true, meta, payload: err }); - } finally { - if (yield cancelled()) { - yield put({ type: POST_ACTIVITY_REJECTED, error: true, meta, payload: new Error('cancelled') }); - } - } + }); } diff --git a/packages/core/src/sagas/sendEventToPostActivitySaga.js b/packages/core/src/sagas/sendEventToPostActivitySaga.js index 780df192fa..d94622d1e7 100644 --- a/packages/core/src/sagas/sendEventToPostActivitySaga.js +++ b/packages/core/src/sagas/sendEventToPostActivitySaga.js @@ -10,14 +10,18 @@ import postActivity from '../actions/postActivity'; export default function* () { yield whileConnected(function* () { - yield takeEvery(SEND_EVENT, function* ({ payload: { name, value } }) { - if (name) { + yield takeEvery( + ({ payload, type }) => + type === SEND_EVENT + && payload + && payload.name, + function* ({ payload: { name, value } }) { yield put(postActivity({ name, type: 'event', value })); } - }); + ); }); } diff --git a/packages/core/src/sagas/sendFilesToPostActivitySaga.js b/packages/core/src/sagas/sendFilesToPostActivitySaga.js index d4f7acff1a..0ee0550786 100644 --- a/packages/core/src/sagas/sendFilesToPostActivitySaga.js +++ b/packages/core/src/sagas/sendFilesToPostActivitySaga.js @@ -15,8 +15,13 @@ const getType = mime.getType.bind(mime); export default function* () { yield whileConnected(function* () { - yield takeEvery(SEND_FILES, function* ({ payload: { files } }) { - if (files.length) { + yield takeEvery( + ({ payload, type }) => + type === SEND_FILES + && payload + && payload.files + && payload.files.length, + function* ({ payload: { files } }) { yield put(postActivity({ attachments: [].map.call(files, file => ({ contentType: getType(file.name) || 'application/octet-stream', @@ -29,8 +34,9 @@ export default function* () { type: 'message' })); + // TODO: [P4] Should we put this as an individual saga instead? yield put(stopSpeakingActivity()); } - }); + ); }); } diff --git a/packages/core/src/sagas/sendPostBackToPostActivitySaga.js b/packages/core/src/sagas/sendPostBackToPostActivitySaga.js index 79bada0d49..c4588fad6c 100644 --- a/packages/core/src/sagas/sendPostBackToPostActivitySaga.js +++ b/packages/core/src/sagas/sendPostBackToPostActivitySaga.js @@ -3,15 +3,18 @@ import { takeEvery } from 'redux-saga/effects'; -import whileConnected from './effects/whileConnected'; - import { SEND_POST_BACK } from '../actions/sendPostBack'; import postActivity from '../actions/postActivity'; +import whileConnected from './effects/whileConnected'; export default function* () { yield whileConnected(function* () { - yield takeEvery(SEND_POST_BACK, function* ({ payload: { value } }) { - if (value) { + yield takeEvery( + ({ payload, type }) => + type === SEND_POST_BACK + && payload + && payload.value, + function* ({ payload: { value } }) { yield put(postActivity({ channelData: { postBack: true @@ -21,6 +24,6 @@ export default function* () { value: typeof value !== 'string' ? value : undefined })); } - }); + ); }); } diff --git a/packages/core/src/sagas/sendTypingOnSetSendBoxSaga.js b/packages/core/src/sagas/sendTypingOnSetSendBoxSaga.js index 49f98664e5..dc1ba06a52 100644 --- a/packages/core/src/sagas/sendTypingOnSetSendBoxSaga.js +++ b/packages/core/src/sagas/sendTypingOnSetSendBoxSaga.js @@ -30,8 +30,12 @@ export default function* () { for (;;) { let lastSend = 0; - const task = yield takeLatest(SET_SEND_BOX, function* ({ payload: { text } }) { - if (text) { + const task = yield takeLatest( + ({ payload, type }) => + type === SET_SEND_BOX + && payload + && payload.text, + function* () { const interval = SEND_INTERVAL - Date.now() + lastSend; if (interval > 0) { @@ -42,7 +46,7 @@ export default function* () { lastSend = Date.now(); } - }); + ); yield takeSendTyping(false); yield cancel(task); diff --git a/packages/core/src/sagas/startDictateAfterSpeakActivitySaga.js b/packages/core/src/sagas/startDictateAfterSpeakActivitySaga.js index 0aa39ba5e4..2b4b391e5b 100644 --- a/packages/core/src/sagas/startDictateAfterSpeakActivitySaga.js +++ b/packages/core/src/sagas/startDictateAfterSpeakActivitySaga.js @@ -3,7 +3,8 @@ import { fork, put, select, - take + take, + takeEvery } from 'redux-saga/effects'; import whileConnected from './effects/whileConnected'; @@ -14,25 +15,34 @@ import { STOP_SPEAKING_ACTIVITY } from '../actions/stopSpeakingActivity'; import startDictate from '../actions/startDictate'; export default function* () { - yield whileConnected(function* (_, userID) { + yield whileConnected(function* () { for (;;) { yield take(START_SPEAKING_ACTIVITY); - const task = yield fork(startDictateAfterSpeakActivitySaga, userID); + const task = yield fork(function* () { + yield takeEvery( + ({ payload, type }) => + type === MARK_ACTIVITY + && payload + && payload.name === 'speak' + && payload.value === false, + function* ({ payload: { activityID } }) { + const activities = yield select(({ activities }) => activities); + + if (!activities.some(activity => activity.id !== activityID && activity.channelData && activity.channelData.speak === true)) { + // TODO: [P2] We should also check inputHint + // acceptingInput = do nothing (or enable send box) + // expectingInput = enable dictate + // ignoringInput = do nothing (or disable send box) + // https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-howto-add-input-hints?view=azure-bot-service-4.0&tabs=cs + yield put(startDictate()); + } + } + ); + }); yield take(STOP_SPEAKING_ACTIVITY); yield cancel(task); } }); } - -function* startDictateAfterSpeakActivitySaga() { - for (;;) { - const { payload: { activityID } } = yield take(({ payload, type }) => type === MARK_ACTIVITY && payload.name === 'speak' && payload.value === false); - const activities = yield select(({ activities }) => activities); - - if (!activities.some(activity => activity.id !== activityID && activity.channelData && activity.channelData.speak === true)) { - yield put(startDictate()); - } - } -} diff --git a/packages/core/src/sagas/stopDictateOnCardAction.js b/packages/core/src/sagas/stopDictateOnCardAction.js index 63ba7aab58..b5d1f78328 100644 --- a/packages/core/src/sagas/stopDictateOnCardAction.js +++ b/packages/core/src/sagas/stopDictateOnCardAction.js @@ -1,6 +1,6 @@ import { put, - take + takeEvery } from 'redux-saga/effects'; import whileConnected from './effects/whileConnected'; @@ -10,18 +10,19 @@ import { POST_ACTIVITY_PENDING } from '../actions/postActivity'; export default function* () { yield whileConnected(function* () { - for (;;) { - // TODO: [P2] We should stop speech input when the user click on anything on a card, including open URL which doesn't generate postActivity - // This functionality was not implemented in v3 + // TODO: [P2] We should stop speech input when the user click on anything on a card, including open URL which doesn't generate postActivity + // This functionality was not implemented in v3 - yield take( - // Currently, there are no actions that are related to card input - // For now, we are using POST_ACTIVITY of a "message" activity - // In the future, if we have an action for card input, we should use that instead - ({ payload, type }) => type === POST_ACTIVITY_PENDING && payload.activity.type === 'message' - ); - - yield put(stopDictate()); - } + yield takeEvery( + // Currently, there are no actions that are related to card input + // For now, we are using POST_ACTIVITY of a "message" activity + // In the future, if we have an action for card input, we should use that instead + ({ payload, type }) => + type === POST_ACTIVITY_PENDING + && payload.activity.type === 'message', + function* () { + yield put(stopDictate()); + } + ); }); } diff --git a/packages/core/src/sagas/stopSpeakActivityOnInputSaga.js b/packages/core/src/sagas/stopSpeakActivityOnInputSaga.js index b83e0a0d80..f0ebff3442 100644 --- a/packages/core/src/sagas/stopSpeakActivityOnInputSaga.js +++ b/packages/core/src/sagas/stopSpeakActivityOnInputSaga.js @@ -25,6 +25,7 @@ export default function* () { function* () { yield put(stopSpeakingActivity()); + // TODO: [P4] Should stopSpeakingActivity automatically mark all activities as spoken? const activities = yield select(({ activities }) => activities); for (let activity of activities) { diff --git a/packages/core/src/sagas/submitSendBoxSaga.js b/packages/core/src/sagas/submitSendBoxSaga.js index d57952a229..7ddb136e7e 100644 --- a/packages/core/src/sagas/submitSendBoxSaga.js +++ b/packages/core/src/sagas/submitSendBoxSaga.js @@ -1,7 +1,7 @@ import { put, select, - take + takeEvery } from 'redux-saga/effects'; import whileConnected from './effects/whileConnected'; @@ -12,14 +12,13 @@ import setSendBox from '../actions/setSendBox'; export default function* () { yield whileConnected(function* () { - for (;;) { - const { payload: { via } } = yield take(SUBMIT_SEND_BOX); + yield takeEvery(SUBMIT_SEND_BOX, function* ({ payload: { via } }) { const sendBoxValue = yield select(({ sendBoxValue }) => sendBoxValue); if (sendBoxValue) { yield put(sendMessage(sendBoxValue, via)); yield put(setSendBox('', 'keyboard')); } - } + }); }); } From 7cc35066e66c2a0cae23329fa833578b3c88e080 Mon Sep 17 00:00:00 2001 From: William Wong Date: Fri, 14 Dec 2018 12:17:28 -0800 Subject: [PATCH 03/27] Clean up coding style --- .../core/src/sagas/effects/whileSpeaking.js | 22 +++ .../src/sagas/markActivityForSpeakSaga.js | 37 ++--- packages/core/src/sagas/postActivitySaga.js | 154 +++++++++--------- .../startDictateAfterSpeakActivitySaga.js | 53 +++--- .../core/src/sagas/stopDictateOnCardAction.js | 4 +- 5 files changed, 137 insertions(+), 133 deletions(-) create mode 100644 packages/core/src/sagas/effects/whileSpeaking.js diff --git a/packages/core/src/sagas/effects/whileSpeaking.js b/packages/core/src/sagas/effects/whileSpeaking.js new file mode 100644 index 0000000000..57ebfe246c --- /dev/null +++ b/packages/core/src/sagas/effects/whileSpeaking.js @@ -0,0 +1,22 @@ +import { + call, + cancel, + fork, + take +} from 'redux-saga/effects'; + +import { START_SPEAKING_ACTIVITY } from '../actions/startSpeakingActivity'; +import { STOP_SPEAKING_ACTIVITY } from '../actions/stopSpeakingActivity'; + +export default function (fn) { + return call(function* () { + for (;;) { + yield take(START_SPEAKING_ACTIVITY); + + const task = yield fork(fn); + + yield take(STOP_SPEAKING_ACTIVITY); + yield cancel(task); + } + }); +} diff --git a/packages/core/src/sagas/markActivityForSpeakSaga.js b/packages/core/src/sagas/markActivityForSpeakSaga.js index 0edc35b96e..a0fc972659 100644 --- a/packages/core/src/sagas/markActivityForSpeakSaga.js +++ b/packages/core/src/sagas/markActivityForSpeakSaga.js @@ -1,39 +1,28 @@ import { - cancel, - fork, put, - take, takeEvery } from 'redux-saga/effects'; import speakableActivity from './definition/speakableActivity'; import whileConnected from './effects/whileConnected'; +import whileSpeaking from './effects/whileSpeaking'; import { INCOMING_ACTIVITY } from '../actions/incomingActivity'; -import { START_SPEAKING_ACTIVITY } from '../actions/startSpeakingActivity'; -import { STOP_SPEAKING_ACTIVITY } from '../actions/stopSpeakingActivity'; import markActivity from '../actions/markActivity'; export default function* () { yield whileConnected(function* (_, userID) { - for (;;) { - yield take(START_SPEAKING_ACTIVITY); - - const task = fork(function* () { - yield takeEvery( - ({ payload, type }) => - type === INCOMING_ACTIVITY - && payload - && payload.activity - && speakableActivity(payload.activity, userID), - function* ({ payload: { activity } }) { - yield put(markActivity(activity, 'speak', true)); - } - ); - }); - - yield take(STOP_SPEAKING_ACTIVITY); - yield cancel(task); - } + yield whileSpeaking(function* () { + yield takeEvery( + ({ payload, type }) => + type === INCOMING_ACTIVITY + && payload + && payload.activity + && speakableActivity(payload.activity, userID), + function* ({ payload: { activity } }) { + yield put(markActivity(activity, 'speak', true)); + } + ); + }); }); } diff --git a/packages/core/src/sagas/postActivitySaga.js b/packages/core/src/sagas/postActivitySaga.js index adce38e9ad..a39ac060f7 100644 --- a/packages/core/src/sagas/postActivitySaga.js +++ b/packages/core/src/sagas/postActivitySaga.js @@ -31,83 +31,87 @@ export default function* () { yield whileConnected(function* (directLine, userID) { let numActivitiesPosted = 0; - yield takeEvery(POST_ACTIVITY, function* ({ payload: { activity } }) { - const locale = yield select(({ language }) => language); - const { attachments, channelData: { clientActivityID = uniqueID() } = {} } = activity; - - activity = { - ...deleteKey(activity, 'id'), - attachments: attachments && attachments.map(({ contentType, contentUrl, name }) => ({ - contentType, - contentUrl, - name - })), - channelData: { - clientActivityID, - ...deleteKey(activity.channelData, 'state') - }, - channelId: 'webchat', - from: { - id: userID, - role: 'user' - }, - locale, - timestamp: getTimestamp() - }; - - if (!numActivitiesPosted++) { - activity.entities = [...activity.entities || [], { - // TODO: [P4] Currently in v3, we send the capabilities although the client might not actually have them - // We need to understand why we need to send these, and only send capabilities the client have - requiresBotState: true, - supportsListening: true, - supportsTts: true, - type: 'ClientCapabilities' - }]; - } + yield takeEvery(POST_ACTIVITY, function* (action) { + yield* postActivitySaga(action, directLine, userID, numActivitiesPosted); + }); + }); +} - const meta = { clientActivityID }; - - yield put({ type: POST_ACTIVITY_PENDING, payload: { activity }, meta }); - - try { - // Quirks: We might receive INCOMING_ACTIVITY before the postActivity call completed - // So, we setup expectation first, then postActivity afterward - - const echoBackCall = call(function* () { - for (;;) { - const { payload: { activity } } = yield take(INCOMING_ACTIVITY); - const { channelData = {}, id } = activity; - - if (channelData.clientActivityID === clientActivityID && id) { - return activity; - } - } - }); - - // Timeout could be due to either: - // - Post activity call may take too long time to complete - // - Direct Line service only respond on HTTP after bot respond to Direct Line - // - Activity may take too long time to echo back - - const sendTimeout = yield select(({ sendTimeout }) => sendTimeout); - - const { send: { echoBack } } = yield race({ - send: all({ - echoBack: echoBackCall, - postActivity: observeOnce(directLine.postActivity(activity)) - }), - timeout: call(() => sleep(sendTimeout).then(() => Promise.reject(new Error('timeout')))) - }); - - yield put({ type: POST_ACTIVITY_FULFILLED, meta, payload: { activity: echoBack } }); - } catch (err) { - yield put({ type: POST_ACTIVITY_REJECTED, error: true, meta, payload: err }); - } finally { - if (yield cancelled()) { - yield put({ type: POST_ACTIVITY_REJECTED, error: true, meta, payload: new Error('cancelled') }); +function* postActivitySaga({ payload: { activity } }, directLine, userID, numActivitiesPosted) { + const locale = yield select(({ language }) => language); + const { attachments, channelData: { clientActivityID = uniqueID() } = {} } = activity; + + activity = { + ...deleteKey(activity, 'id'), + attachments: attachments && attachments.map(({ contentType, contentUrl, name }) => ({ + contentType, + contentUrl, + name + })), + channelData: { + clientActivityID, + ...deleteKey(activity.channelData, 'state') + }, + channelId: 'webchat', + from: { + id: userID, + role: 'user' + }, + locale, + timestamp: getTimestamp() + }; + + if (!numActivitiesPosted++) { + activity.entities = [...activity.entities || [], { + // TODO: [P4] Currently in v3, we send the capabilities although the client might not actually have them + // We need to understand why we need to send these, and only send capabilities the client have + requiresBotState: true, + supportsListening: true, + supportsTts: true, + type: 'ClientCapabilities' + }]; + } + + const meta = { clientActivityID }; + + yield put({ type: POST_ACTIVITY_PENDING, payload: { activity }, meta }); + + try { + // Quirks: We might receive INCOMING_ACTIVITY before the postActivity call completed + // So, we setup expectation first, then postActivity afterward + + const echoBackCall = call(function* () { + for (;;) { + const { payload: { activity } } = yield take(INCOMING_ACTIVITY); + const { channelData = {}, id } = activity; + + if (channelData.clientActivityID === clientActivityID && id) { + return activity; } } }); - }); + + // Timeout could be due to either: + // - Post activity call may take too long time to complete + // - Direct Line service only respond on HTTP after bot respond to Direct Line + // - Activity may take too long time to echo back + + const sendTimeout = yield select(({ sendTimeout }) => sendTimeout); + + const { send: { echoBack } } = yield race({ + send: all({ + echoBack: echoBackCall, + postActivity: observeOnce(directLine.postActivity(activity)) + }), + timeout: call(() => sleep(sendTimeout).then(() => Promise.reject(new Error('timeout')))) + }); + + yield put({ type: POST_ACTIVITY_FULFILLED, meta, payload: { activity: echoBack } }); + } catch (err) { + yield put({ type: POST_ACTIVITY_REJECTED, error: true, meta, payload: err }); + } finally { + if (yield cancelled()) { + yield put({ type: POST_ACTIVITY_REJECTED, error: true, meta, payload: new Error('cancelled') }); + } + } } diff --git a/packages/core/src/sagas/startDictateAfterSpeakActivitySaga.js b/packages/core/src/sagas/startDictateAfterSpeakActivitySaga.js index 2b4b391e5b..942534de66 100644 --- a/packages/core/src/sagas/startDictateAfterSpeakActivitySaga.js +++ b/packages/core/src/sagas/startDictateAfterSpeakActivitySaga.js @@ -1,48 +1,39 @@ import { - cancel, - fork, put, select, - take, takeEvery } from 'redux-saga/effects'; import whileConnected from './effects/whileConnected'; +import whileSpeaking from './effects/whileSpeaking'; import { MARK_ACTIVITY } from '../actions/markActivity'; -import { START_SPEAKING_ACTIVITY } from '../actions/startSpeakingActivity'; -import { STOP_SPEAKING_ACTIVITY } from '../actions/stopSpeakingActivity'; import startDictate from '../actions/startDictate'; export default function* () { yield whileConnected(function* () { - for (;;) { - yield take(START_SPEAKING_ACTIVITY); - - const task = yield fork(function* () { - yield takeEvery( - ({ payload, type }) => - type === MARK_ACTIVITY - && payload - && payload.name === 'speak' - && payload.value === false, - function* ({ payload: { activityID } }) { - const activities = yield select(({ activities }) => activities); + yield whileSpeaking(startDictateAfterSpeakActivitySaga); + }); +} - if (!activities.some(activity => activity.id !== activityID && activity.channelData && activity.channelData.speak === true)) { - // TODO: [P2] We should also check inputHint - // acceptingInput = do nothing (or enable send box) - // expectingInput = enable dictate - // ignoringInput = do nothing (or disable send box) - // https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-howto-add-input-hints?view=azure-bot-service-4.0&tabs=cs - yield put(startDictate()); - } - } - ); - }); +function* startDictateAfterSpeakActivitySaga() { + yield takeEvery( + ({ payload, type }) => + type === MARK_ACTIVITY + && payload + && payload.name === 'speak' + && payload.value === false, + function* ({ payload: { activityID } }) { + const activities = yield select(({ activities }) => activities); - yield take(STOP_SPEAKING_ACTIVITY); - yield cancel(task); + if (!activities.some(activity => activity.id !== activityID && activity.channelData && activity.channelData.speak === true)) { + // TODO: [P2] We should also check inputHint + // acceptingInput = do nothing (or enable send box) + // expectingInput = enable dictate + // ignoringInput = do nothing (or disable send box) + // https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-howto-add-input-hints?view=azure-bot-service-4.0&tabs=cs + yield put(startDictate()); + } } - }); + ); } diff --git a/packages/core/src/sagas/stopDictateOnCardAction.js b/packages/core/src/sagas/stopDictateOnCardAction.js index b5d1f78328..fe9dddcb42 100644 --- a/packages/core/src/sagas/stopDictateOnCardAction.js +++ b/packages/core/src/sagas/stopDictateOnCardAction.js @@ -17,9 +17,7 @@ export default function* () { // Currently, there are no actions that are related to card input // For now, we are using POST_ACTIVITY of a "message" activity // In the future, if we have an action for card input, we should use that instead - ({ payload, type }) => - type === POST_ACTIVITY_PENDING - && payload.activity.type === 'message', + ({ payload, type }) => type === POST_ACTIVITY_PENDING && payload.activity.type === 'message', function* () { yield put(stopDictate()); } From ee92ada39449c141562c79951dd86dfecfaa7774 Mon Sep 17 00:00:00 2001 From: William Wong Date: Fri, 14 Dec 2018 12:21:58 -0800 Subject: [PATCH 04/27] Clean up coding style --- .../src/sagas/markActivityForSpeakSaga.js | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/packages/core/src/sagas/markActivityForSpeakSaga.js b/packages/core/src/sagas/markActivityForSpeakSaga.js index a0fc972659..bb65d49078 100644 --- a/packages/core/src/sagas/markActivityForSpeakSaga.js +++ b/packages/core/src/sagas/markActivityForSpeakSaga.js @@ -12,17 +12,19 @@ import markActivity from '../actions/markActivity'; export default function* () { yield whileConnected(function* (_, userID) { - yield whileSpeaking(function* () { - yield takeEvery( - ({ payload, type }) => - type === INCOMING_ACTIVITY - && payload - && payload.activity - && speakableActivity(payload.activity, userID), - function* ({ payload: { activity } }) { - yield put(markActivity(activity, 'speak', true)); - } - ); - }); + yield whileSpeaking(markActivityForSpeakSaga.bind(null, userID)); }); } + +function* markActivityForSpeakSaga(userID) { + yield takeEvery( + ({ payload, type }) => + type === INCOMING_ACTIVITY + && payload + && payload.activity + && speakableActivity(payload.activity, userID), + function* () { + yield put(markActivity(activity, 'speak', true)); + } + ); +} From 21852e726845f648bf9d773d51f95483d61fa3a4 Mon Sep 17 00:00:00 2001 From: William Wong Date: Fri, 14 Dec 2018 12:22:24 -0800 Subject: [PATCH 05/27] Fix numActivitiesPosted --- packages/core/src/sagas/postActivitySaga.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/sagas/postActivitySaga.js b/packages/core/src/sagas/postActivitySaga.js index a39ac060f7..7811e5d692 100644 --- a/packages/core/src/sagas/postActivitySaga.js +++ b/packages/core/src/sagas/postActivitySaga.js @@ -32,7 +32,7 @@ export default function* () { let numActivitiesPosted = 0; yield takeEvery(POST_ACTIVITY, function* (action) { - yield* postActivitySaga(action, directLine, userID, numActivitiesPosted); + yield* postActivitySaga(action, directLine, userID, numActivitiesPosted++); }); }); } From 6e10f1ef616531cf0739d28ff1a42e2e832f3547 Mon Sep 17 00:00:00 2001 From: William Wong Date: Fri, 14 Dec 2018 15:43:11 -0800 Subject: [PATCH 06/27] Clean up --- packages/core/src/sagas/postActivitySaga.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/core/src/sagas/postActivitySaga.js b/packages/core/src/sagas/postActivitySaga.js index 7811e5d692..20c003a993 100644 --- a/packages/core/src/sagas/postActivitySaga.js +++ b/packages/core/src/sagas/postActivitySaga.js @@ -32,12 +32,12 @@ export default function* () { let numActivitiesPosted = 0; yield takeEvery(POST_ACTIVITY, function* (action) { - yield* postActivitySaga(action, directLine, userID, numActivitiesPosted++); + yield* postActivitySaga(directLine, userID, numActivitiesPosted++, action); }); }); } -function* postActivitySaga({ payload: { activity } }, directLine, userID, numActivitiesPosted) { +function* postActivitySaga(directLine, userID, numActivitiesPosted, { payload: { activity } }) { const locale = yield select(({ language }) => language); const { attachments, channelData: { clientActivityID = uniqueID() } = {} } = activity; @@ -61,7 +61,7 @@ function* postActivitySaga({ payload: { activity } }, directLine, userID, numAct timestamp: getTimestamp() }; - if (!numActivitiesPosted++) { + if (!numActivitiesPosted) { activity.entities = [...activity.entities || [], { // TODO: [P4] Currently in v3, we send the capabilities although the client might not actually have them // We need to understand why we need to send these, and only send capabilities the client have From 128309660a760b9e327b11ff7793a6d977044b2a Mon Sep 17 00:00:00 2001 From: William Wong Date: Fri, 14 Dec 2018 15:56:52 -0800 Subject: [PATCH 07/27] Stop speak activity action will mark all as spoken --- packages/core/src/sagas.js | 2 ++ .../markAllAsSpokenOnStopSpeakActivitySaga.js | 20 +++++++++++++++++++ .../src/sagas/stopSpeakActivityOnInputSaga.js | 11 ---------- 3 files changed, 22 insertions(+), 11 deletions(-) create mode 100644 packages/core/src/sagas/markAllAsSpokenOnStopSpeakActivitySaga.js diff --git a/packages/core/src/sagas.js b/packages/core/src/sagas.js index 159a4f9c8a..7bdc8f702a 100644 --- a/packages/core/src/sagas.js +++ b/packages/core/src/sagas.js @@ -6,6 +6,7 @@ import connectSaga from './sagas/connectSaga'; import incomingActivitySaga from './sagas/incomingActivitySaga'; import incomingTypingSaga from './sagas/incomingTypingSaga'; import markActivityForSpeakSaga from './sagas/markActivityForSpeakSaga'; +import markAllAsSpokenOnStopSpeakActivitySaga from './sagas/markAllAsSpokenOnStopSpeakActivitySaga'; import postActivitySaga from './sagas/postActivitySaga'; import sendEventToPostActivitySaga from './sagas/sendEventToPostActivitySaga'; import sendFilesToPostActivitySaga from './sagas/sendFilesToPostActivitySaga'; @@ -24,6 +25,7 @@ export default function* () { yield fork(incomingActivitySaga); yield fork(incomingTypingSaga); yield fork(markActivityForSpeakSaga); + yield fork(markAllAsSpokenOnStopSpeakActivitySaga); yield fork(postActivitySaga); yield fork(sendEventToPostActivitySaga); yield fork(sendFilesToPostActivitySaga); diff --git a/packages/core/src/sagas/markAllAsSpokenOnStopSpeakActivitySaga.js b/packages/core/src/sagas/markAllAsSpokenOnStopSpeakActivitySaga.js new file mode 100644 index 0000000000..b6ee3f64af --- /dev/null +++ b/packages/core/src/sagas/markAllAsSpokenOnStopSpeakActivitySaga.js @@ -0,0 +1,20 @@ +import { + put, + select, + takeEvery +} from 'redux-saga/effects'; + +import markActivity from '../actions/markActivity'; +import { STOP_SPEAKING_ACTIVITY } from '../actions/stopSpeakingActivity'; + +export default function* () { + yield takeEvery(STOP_SPEAKING_ACTIVITY, function* () { + const activities = yield select(({ activities }) => activities); + + for (let activity of activities) { + if (activity.channelData && activity.channelData.speak) { + yield put(markActivity(activity, 'speak', false)); + } + } + }); +} diff --git a/packages/core/src/sagas/stopSpeakActivityOnInputSaga.js b/packages/core/src/sagas/stopSpeakActivityOnInputSaga.js index f0ebff3442..0c817d6a55 100644 --- a/packages/core/src/sagas/stopSpeakActivityOnInputSaga.js +++ b/packages/core/src/sagas/stopSpeakActivityOnInputSaga.js @@ -1,12 +1,10 @@ import { put, - select, takeEvery } from 'redux-saga/effects'; import whileConnected from './effects/whileConnected'; -import markActivity from '../actions/markActivity'; import stopSpeakingActivity from '../actions/stopSpeakingActivity'; import { POST_ACTIVITY_PENDING } from '../actions/postActivity'; import { SET_SEND_BOX } from '../actions/setSendBox'; @@ -24,15 +22,6 @@ export default function* () { || (type === POST_ACTIVITY_PENDING && payload.activity.type === 'message'), function* () { yield put(stopSpeakingActivity()); - - // TODO: [P4] Should stopSpeakingActivity automatically mark all activities as spoken? - const activities = yield select(({ activities }) => activities); - - for (let activity of activities) { - if (activity.channelData && activity.channelData.speak) { - yield put(markActivity(activity, 'speak', false)); - } - } } ); }); From b2826b0777cd782ea7aadd88b5647d0d3e4d13f7 Mon Sep 17 00:00:00 2001 From: William Wong Date: Tue, 18 Dec 2018 20:38:54 -0800 Subject: [PATCH 08/27] Clean up --- .../core/src/sagas/markAllAsSpokenOnStopSpeakActivitySaga.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/src/sagas/markAllAsSpokenOnStopSpeakActivitySaga.js b/packages/core/src/sagas/markAllAsSpokenOnStopSpeakActivitySaga.js index b6ee3f64af..cfa84e4268 100644 --- a/packages/core/src/sagas/markAllAsSpokenOnStopSpeakActivitySaga.js +++ b/packages/core/src/sagas/markAllAsSpokenOnStopSpeakActivitySaga.js @@ -4,12 +4,12 @@ import { takeEvery } from 'redux-saga/effects'; -import markActivity from '../actions/markActivity'; import { STOP_SPEAKING_ACTIVITY } from '../actions/stopSpeakingActivity'; +import markActivity from '../actions/markActivity'; export default function* () { yield takeEvery(STOP_SPEAKING_ACTIVITY, function* () { - const activities = yield select(({ activities }) => activities); + const { activities } = yield select(); for (let activity of activities) { if (activity.channelData && activity.channelData.speak) { From 99261521f6792900231ea7c0cf78f264bd24a10b Mon Sep 17 00:00:00 2001 From: William Wong Date: Fri, 21 Dec 2018 23:55:53 -0800 Subject: [PATCH 09/27] Revisit all sagas related to dictate and speak --- CHANGELOG.md | 10 +++----- packages/component/src/Dictation.js | 4 +-- packages/component/src/SendBox/TextBox.js | 4 +-- packages/core/src/actions/postActivity.js | 3 ++- packages/core/src/actions/setSendBox.js | 4 +-- packages/core/src/actions/submitSendBox.js | 2 +- packages/core/src/reducer.ts | 2 ++ packages/core/src/reducers/activities.js | 7 +++++- packages/core/src/reducers/sendTyping.js | 15 +++++++++++ packages/core/src/sagas.js | 2 ++ .../core/src/sagas/effects/whileSpeaking.js | 4 +-- packages/core/src/sagas/incomingTypingSaga.js | 6 ++--- .../src/sagas/markActivityForSpeakSaga.js | 2 +- packages/core/src/sagas/postActivitySaga.js | 6 ++--- .../src/sagas/sendFilesToPostActivitySaga.js | 4 --- .../sagas/sendMessageToPostActivitySaga.js | 10 +------- .../startDictateAfterSpeakActivitySaga.js | 19 ++++++++------ .../startSpeakActivityOnPostActivitySaga.js | 25 +++++++++++++++++++ .../src/sagas/stopSpeakActivityOnInputSaga.js | 24 ++++++++++++------ packages/core/src/sagas/submitSendBoxSaga.js | 2 +- 20 files changed, 101 insertions(+), 54 deletions(-) create mode 100644 packages/core/src/reducers/sendTyping.js create mode 100644 packages/core/src/sagas/startSpeakActivityOnPostActivitySaga.js diff --git a/CHANGELOG.md b/CHANGELOG.md index f723a50309..1a8f0c1f8e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,12 +45,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - `component`: Fix [#1560](https://github.com/Microsoft/BotFramework-WebChat/issues/1560). Fixed carousel layout did not show date and alignment issues, by [@compulim](https://github.com/compulim) in PR [#1561](https://github.com/Microsoft/BotFramework-WebChat/pull/1561) - `playground`: Fix [#1562](https://github.com/Microsoft/BotFramework-WebChat/issues/1562). Fixed timestamp grouping "Don't group" and added "Don't show timestamp", by [@compulim](https://github.com/compulim) in PR [#1563](https://github.com/Microsoft/BotFramework-WebChat/pull/1563) - `component`: Fix [#1576](https://github.com/Microsoft/BotFramework-WebChat/issues/1576). Rich card without `tap` should be rendered properly, by [@compulim](https://github.com/compulim) in PR [#1577](https://github.com/Microsoft/BotFramework-WebChat/pull/1577) -- `core`: Some sagas missed handling successive actions, in PR [#1286](https://github.com/Microsoft/BotFramework-WebChat/pull/1286) - - `sendFilesToPostActivitySaga` - - `sendMessageToPostActivitySaga` - - `sendPostBackToPostActivitySaga` - - `stopSpeakActivityOnInputSaga` -- `core`: `incomingActivitySaga` may null-ref if the first activity is from user, in PR [#1286](https://github.com/Microsoft/BotFramework-WebChat/pull/1286) +- Core: Some sagas missed handling successive actions, in PR [#1286](https://github.com/Microsoft/BotFramework-WebChat/pull/1286) +- Core: `incomingActivitySaga` may throw null-ref exception if the first activity is from user, in PR [#1286](https://github.com/Microsoft/BotFramework-WebChat/pull/1286) +- Fix [#1328](https://github.com/Microsoft/BotFramework-WebChat/issues/1328). Should not start microphone if input hint is set to `ignoringInput`, in PR [#1286](https://github.com/Microsoft/BotFramework-WebChat/pull/1286) +- Fix outgoing typing indicators are not sent and acknowledged properly, in PR [#1286](https://github.com/Microsoft/BotFramework-WebChat/pull/1286) ### Removed - `botAvatarImage` and `userAvatarImage` props, as they are moved inside `styleOptions`, in PR [#1486](https://github.com/Microsoft/BotFramework-WebChat/pull/1486) diff --git a/packages/component/src/Dictation.js b/packages/component/src/Dictation.js index dff3d18037..f14858a526 100644 --- a/packages/component/src/Dictation.js +++ b/packages/component/src/Dictation.js @@ -29,7 +29,7 @@ class Dictation extends React.Component { props.stopDictate(); if (transcript) { - props.setSendBox(transcript, 'speech'); + props.setSendBox(transcript); props.submitSendBox('speech'); props.startSpeakingActivity(); } @@ -45,7 +45,7 @@ class Dictation extends React.Component { // This is for two purposes: // 1. Set send box will also trigger send typing // 2. If the user cancelled out, the interim result will be in the send box so the user can update it before send - props.setSendBox(interims.join(' '), 'speech'); + props.setSendBox(interims.join(' ')); } handleError(event) { diff --git a/packages/component/src/SendBox/TextBox.js b/packages/component/src/SendBox/TextBox.js index 0733eac360..c57b3c1b0d 100644 --- a/packages/component/src/SendBox/TextBox.js +++ b/packages/component/src/SendBox/TextBox.js @@ -26,7 +26,7 @@ const connectSendTextBox = (...selectors) => connectToWebChat( disabled, language, onChange: ({ target: { value } }) => { - setSendBox(value, 'keyboard'); + setSendBox(value); }, onSubmit: event => { event.preventDefault(); @@ -36,7 +36,7 @@ const connectSendTextBox = (...selectors) => connectToWebChat( if (sendBoxValue) { scrollToEnd(); - submitSendBox('keyboard'); + submitSendBox(); } }, value: sendBoxValue diff --git a/packages/core/src/actions/postActivity.js b/packages/core/src/actions/postActivity.js index 9fb666f3fd..aaa5cc31d8 100644 --- a/packages/core/src/actions/postActivity.js +++ b/packages/core/src/actions/postActivity.js @@ -5,9 +5,10 @@ export const POST_ACTIVITY_FULFILLED = `${ POST_ACTIVITY }_${ FULFILLED }`; export const POST_ACTIVITY_PENDING = `${ POST_ACTIVITY }_${ PENDING }`; export const POST_ACTIVITY_REJECTED = `${ POST_ACTIVITY }_${ REJECTED }`; -export default function (activity) { +export default function (activity, via = 'keyboard') { return { type: POST_ACTIVITY, + meta: { via }, payload: { activity } }; } diff --git a/packages/core/src/actions/setSendBox.js b/packages/core/src/actions/setSendBox.js index 4c34c74557..806e4a58b7 100644 --- a/packages/core/src/actions/setSendBox.js +++ b/packages/core/src/actions/setSendBox.js @@ -1,9 +1,9 @@ const SET_SEND_BOX = 'WEB_CHAT/SET_SEND_BOX'; -export default function (text, via) { +export default function (text) { return { type: SET_SEND_BOX, - payload: { text, via } + payload: { text } }; } diff --git a/packages/core/src/actions/submitSendBox.js b/packages/core/src/actions/submitSendBox.js index a008ea0770..4e323761ae 100644 --- a/packages/core/src/actions/submitSendBox.js +++ b/packages/core/src/actions/submitSendBox.js @@ -1,6 +1,6 @@ const SUBMIT_SEND_BOX = 'WEB_CHAT/SUBMIT_SEND_BOX'; -export default function submitSendBox(via) { +export default function submitSendBox(via = 'keyboard') { return { type: SUBMIT_SEND_BOX, payload: { via } diff --git a/packages/core/src/reducer.ts b/packages/core/src/reducer.ts index ad28c21f58..e4acaf7329 100644 --- a/packages/core/src/reducer.ts +++ b/packages/core/src/reducer.ts @@ -8,6 +8,7 @@ import readyState from './reducers/readyState'; import referenceGrammarID from './reducers/referenceGrammarID'; import sendBoxValue from './reducers/sendBoxValue'; import sendTimeout from './reducers/sendTimeout'; +import sendTyping from './reducers/sendTyping'; import suggestedActions from './reducers/suggestedActions'; export default combineReducers({ @@ -19,5 +20,6 @@ export default combineReducers({ referenceGrammarID, sendBoxValue, sendTimeout, + sendTyping, suggestedActions }) diff --git a/packages/core/src/reducers/activities.js b/packages/core/src/reducers/activities.js index a8c397f819..f743815a1a 100644 --- a/packages/core/src/reducers/activities.js +++ b/packages/core/src/reducers/activities.js @@ -25,9 +25,14 @@ function findByClientActivityID(clientActivityID) { function upsertActivityWithSort(activities, nextActivity) { const { channelData: { clientActivityID: nextClientActivityID } = {}, - from: { id: nextFromID } = {} + from: { id: nextFromID, role: nextFromRole } = {}, + type: nextType } = nextActivity; + if (nextType === 'typing' && nextFromRole === 'user') { + return activities; + } + const nextTimestamp = Date.parse(nextActivity.timestamp); const nextActivities = activities.filter(({ channelData: { clientActivityID } = {}, from, type }) => // We will remove all "typing" and "sending messages" activities diff --git a/packages/core/src/reducers/sendTyping.js b/packages/core/src/reducers/sendTyping.js new file mode 100644 index 0000000000..d7f1dc280c --- /dev/null +++ b/packages/core/src/reducers/sendTyping.js @@ -0,0 +1,15 @@ +import { SET_SEND_TYPING } from '../actions/setSendTyping'; + +const DEFAULT_STATE = false; + +export default function (state = DEFAULT_STATE, { payload, type }) { + switch (type) { + case SET_SEND_TYPING: + state = payload.sendTyping; + break; + + default: break; + } + + return state; +} diff --git a/packages/core/src/sagas.js b/packages/core/src/sagas.js index 7bdc8f702a..5548b672e1 100644 --- a/packages/core/src/sagas.js +++ b/packages/core/src/sagas.js @@ -14,6 +14,7 @@ import sendMessageToPostActivitySaga from './sagas/sendMessageToPostActivitySaga import sendPostBackToPostActivitySaga from './sagas/sendPostBackToPostActivitySaga'; import sendTypingOnSetSendBoxSaga from './sagas/sendTypingOnSetSendBoxSaga'; import startDictateAfterSpeakActivitySaga from './sagas/startDictateAfterSpeakActivitySaga'; +import startSpeakActivityOnPostActivitySaga from './sagas/startSpeakActivityOnPostActivitySaga'; import stopDictateOnCardAction from './sagas/stopDictateOnCardAction'; import stopSpeakActivityOnInputSaga from './sagas/stopSpeakActivityOnInputSaga'; import submitSendBoxSaga from './sagas/submitSendBoxSaga'; @@ -33,6 +34,7 @@ export default function* () { yield fork(sendPostBackToPostActivitySaga); yield fork(sendTypingOnSetSendBoxSaga); yield fork(startDictateAfterSpeakActivitySaga); + yield fork(startSpeakActivityOnPostActivitySaga); yield fork(stopDictateOnCardAction); yield fork(stopSpeakActivityOnInputSaga); yield fork(submitSendBoxSaga); diff --git a/packages/core/src/sagas/effects/whileSpeaking.js b/packages/core/src/sagas/effects/whileSpeaking.js index 57ebfe246c..a9bf568468 100644 --- a/packages/core/src/sagas/effects/whileSpeaking.js +++ b/packages/core/src/sagas/effects/whileSpeaking.js @@ -5,8 +5,8 @@ import { take } from 'redux-saga/effects'; -import { START_SPEAKING_ACTIVITY } from '../actions/startSpeakingActivity'; -import { STOP_SPEAKING_ACTIVITY } from '../actions/stopSpeakingActivity'; +import { START_SPEAKING_ACTIVITY } from '../../actions/startSpeakingActivity'; +import { STOP_SPEAKING_ACTIVITY } from '../../actions/stopSpeakingActivity'; export default function (fn) { return call(function* () { diff --git a/packages/core/src/sagas/incomingTypingSaga.js b/packages/core/src/sagas/incomingTypingSaga.js index 4df8196c62..17496c156a 100644 --- a/packages/core/src/sagas/incomingTypingSaga.js +++ b/packages/core/src/sagas/incomingTypingSaga.js @@ -1,7 +1,7 @@ import { call, put, - takeLatest + takeEvery } from 'redux-saga/effects'; import deleteActivity from '../actions/deleteActivity'; @@ -16,9 +16,7 @@ function isTypingActivity({ type, payload }) { } export default function* () { - yield takeLatest(isTypingActivity, function* ({ payload: { activity } }) { - const id = activity.id; - + yield takeEvery(isTypingActivity, function* ({ payload: { activity: { id } } }) { yield call(sleep, 5000); yield put(deleteActivity(id)); }); diff --git a/packages/core/src/sagas/markActivityForSpeakSaga.js b/packages/core/src/sagas/markActivityForSpeakSaga.js index bb65d49078..550b6eef2a 100644 --- a/packages/core/src/sagas/markActivityForSpeakSaga.js +++ b/packages/core/src/sagas/markActivityForSpeakSaga.js @@ -23,7 +23,7 @@ function* markActivityForSpeakSaga(userID) { && payload && payload.activity && speakableActivity(payload.activity, userID), - function* () { + function* ({ payload: { activity } }) { yield put(markActivity(activity, 'speak', true)); } ); diff --git a/packages/core/src/sagas/postActivitySaga.js b/packages/core/src/sagas/postActivitySaga.js index 20c003a993..02296184c0 100644 --- a/packages/core/src/sagas/postActivitySaga.js +++ b/packages/core/src/sagas/postActivitySaga.js @@ -37,7 +37,7 @@ export default function* () { }); } -function* postActivitySaga(directLine, userID, numActivitiesPosted, { payload: { activity } }) { +function* postActivitySaga(directLine, userID, numActivitiesPosted, { meta: { via }, payload: { activity } }) { const locale = yield select(({ language }) => language); const { attachments, channelData: { clientActivityID = uniqueID() } = {} } = activity; @@ -72,9 +72,9 @@ function* postActivitySaga(directLine, userID, numActivitiesPosted, { payload: { }]; } - const meta = { clientActivityID }; + const meta = { clientActivityID, via }; - yield put({ type: POST_ACTIVITY_PENDING, payload: { activity }, meta }); + yield put({ type: POST_ACTIVITY_PENDING, meta, payload: { activity } }); try { // Quirks: We might receive INCOMING_ACTIVITY before the postActivity call completed diff --git a/packages/core/src/sagas/sendFilesToPostActivitySaga.js b/packages/core/src/sagas/sendFilesToPostActivitySaga.js index 0ee0550786..33d103f535 100644 --- a/packages/core/src/sagas/sendFilesToPostActivitySaga.js +++ b/packages/core/src/sagas/sendFilesToPostActivitySaga.js @@ -9,7 +9,6 @@ import whileConnected from './effects/whileConnected'; import { SEND_FILES } from '../actions/sendFiles'; import postActivity from '../actions/postActivity'; -import stopSpeakingActivity from '../actions/stopSpeakingActivity'; const getType = mime.getType.bind(mime); @@ -33,9 +32,6 @@ export default function* () { }, type: 'message' })); - - // TODO: [P4] Should we put this as an individual saga instead? - yield put(stopSpeakingActivity()); } ); }); diff --git a/packages/core/src/sagas/sendMessageToPostActivitySaga.js b/packages/core/src/sagas/sendMessageToPostActivitySaga.js index 9547cc5d6e..7b53c7088e 100644 --- a/packages/core/src/sagas/sendMessageToPostActivitySaga.js +++ b/packages/core/src/sagas/sendMessageToPostActivitySaga.js @@ -7,8 +7,6 @@ import whileConnected from './effects/whileConnected'; import { SEND_MESSAGE } from '../actions/sendMessage'; import postActivity from '../actions/postActivity'; -import startSpeakingActivity from '../actions/startSpeakingActivity'; -import stopSpeakingActivity from '../actions/stopSpeakingActivity'; export default function* () { yield whileConnected(function* () { @@ -18,13 +16,7 @@ export default function* () { text, textFormat: 'plain', type: 'message' - })); - - if (via === 'speech') { - yield put(startSpeakingActivity()); - } else { - yield put(stopSpeakingActivity()); - } + }, via)); } }); }); diff --git a/packages/core/src/sagas/startDictateAfterSpeakActivitySaga.js b/packages/core/src/sagas/startDictateAfterSpeakActivitySaga.js index 942534de66..e276bf0666 100644 --- a/packages/core/src/sagas/startDictateAfterSpeakActivitySaga.js +++ b/packages/core/src/sagas/startDictateAfterSpeakActivitySaga.js @@ -20,18 +20,21 @@ function* startDictateAfterSpeakActivitySaga() { yield takeEvery( ({ payload, type }) => type === MARK_ACTIVITY - && payload && payload.name === 'speak' && payload.value === false, function* ({ payload: { activityID } }) { - const activities = yield select(({ activities }) => activities); + const { activities } = yield select(); + const activity = activities.find(({ id }) => id === activityID); - if (!activities.some(activity => activity.id !== activityID && activity.channelData && activity.channelData.speak === true)) { - // TODO: [P2] We should also check inputHint - // acceptingInput = do nothing (or enable send box) - // expectingInput = enable dictate - // ignoringInput = do nothing (or disable send box) - // https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-howto-add-input-hints?view=azure-bot-service-4.0&tabs=cs + if ( + activity.inputHint !== 'ignoringInput' + // Checks if there are no more activities that will be synthesis + && !activities.some( + ({ channelData, id }) => id !== activityID && channelData && channelData.speak === true + ) + ) { + // We honor input hint based on this article + // https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-howto-add-input-hints?view=azure-bot-service-4.0&tabs=cs yield put(startDictate()); } } diff --git a/packages/core/src/sagas/startSpeakActivityOnPostActivitySaga.js b/packages/core/src/sagas/startSpeakActivityOnPostActivitySaga.js new file mode 100644 index 0000000000..238022f183 --- /dev/null +++ b/packages/core/src/sagas/startSpeakActivityOnPostActivitySaga.js @@ -0,0 +1,25 @@ +import { + put, + takeEvery +} from 'redux-saga/effects'; + +import whileConnected from './effects/whileConnected'; + +import { POST_ACTIVITY_PENDING } from '../actions/postActivity'; +import startSpeakingActivity from '../actions/startSpeakingActivity'; +import stopSpeakingActivity from '../actions/stopSpeakingActivity'; + +export default function* () { + yield whileConnected(function* () { + yield takeEvery( + ({ meta, payload, type }) => ( + type === POST_ACTIVITY_PENDING + && meta.via === 'speech' + && payload.activity.type === 'message' + ), + function* () { + yield put(startSpeakingActivity()); + } + ); + }); +} diff --git a/packages/core/src/sagas/stopSpeakActivityOnInputSaga.js b/packages/core/src/sagas/stopSpeakActivityOnInputSaga.js index 0c817d6a55..cdef5abe4f 100644 --- a/packages/core/src/sagas/stopSpeakActivityOnInputSaga.js +++ b/packages/core/src/sagas/stopSpeakActivityOnInputSaga.js @@ -5,21 +5,31 @@ import { import whileConnected from './effects/whileConnected'; -import stopSpeakingActivity from '../actions/stopSpeakingActivity'; import { POST_ACTIVITY_PENDING } from '../actions/postActivity'; import { SET_SEND_BOX } from '../actions/setSendBox'; import { START_DICTATE } from '../actions/startDictate'; +import stopSpeakingActivity from '../actions/stopSpeakingActivity'; export default function* () { yield whileConnected(function* () { yield takeEvery( - ({ payload, type }) => type === START_DICTATE - || (type === SET_SEND_BOX && payload.text && payload.via !== 'speech') + ({ meta, payload, type }) => ( + type === START_DICTATE + + || ( + type === SET_SEND_BOX + && payload.text + ) - // We want to stop speaking activity when the user click on a card action - // But currently there are no actions generated out of a card action - // So, right now, we are using best-effort by listening to POST_ACTIVITY_PENDING with a "message" event - || (type === POST_ACTIVITY_PENDING && payload.activity.type === 'message'), + // We want to stop speaking activity when the user click on a card action + // But currently there are no actions generated out of a card action + // So, right now, we are using best-effort by listening to POST_ACTIVITY_PENDING with a "message" event + || ( + type === POST_ACTIVITY_PENDING + && meta.via !== 'speech' + && payload.activity.type === 'message' + ) + ), function* () { yield put(stopSpeakingActivity()); } diff --git a/packages/core/src/sagas/submitSendBoxSaga.js b/packages/core/src/sagas/submitSendBoxSaga.js index 7ddb136e7e..31c877e3bb 100644 --- a/packages/core/src/sagas/submitSendBoxSaga.js +++ b/packages/core/src/sagas/submitSendBoxSaga.js @@ -17,7 +17,7 @@ export default function* () { if (sendBoxValue) { yield put(sendMessage(sendBoxValue, via)); - yield put(setSendBox('', 'keyboard')); + yield put(setSendBox('')); } }); }); From 938a0b64408262e5734208823e23ffbb9690ed67 Mon Sep 17 00:00:00 2001 From: William Wong Date: Sat, 22 Dec 2018 00:51:18 -0800 Subject: [PATCH 10/27] Clean up --- .../core/src/sagas/clearSuggestedActionsOnPostActivitySaga.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/core/src/sagas/clearSuggestedActionsOnPostActivitySaga.js b/packages/core/src/sagas/clearSuggestedActionsOnPostActivitySaga.js index 491eaa4827..48071760e1 100644 --- a/packages/core/src/sagas/clearSuggestedActionsOnPostActivitySaga.js +++ b/packages/core/src/sagas/clearSuggestedActionsOnPostActivitySaga.js @@ -13,8 +13,6 @@ export default function* () { yield takeEvery( ({ payload, type }) => type === POST_ACTIVITY_PENDING - && payload - && payload.activity && payload.activity.type === 'message', function* () { yield put(setSuggestedActions()); From b017121d8cdb794c85ef25db9bfa3764737c6bdb Mon Sep 17 00:00:00 2001 From: William Wong Date: Sat, 22 Dec 2018 04:01:50 -0800 Subject: [PATCH 11/27] Clean up --- .../src/sagas/clearSuggestedActionsOnPostActivitySaga.js | 5 +++-- packages/core/src/sagas/incomingActivitySaga.js | 2 +- packages/core/src/sagas/markActivityForSpeakSaga.js | 7 +++---- packages/core/src/sagas/postActivitySaga.js | 4 ++-- packages/core/src/sagas/sendEventToPostActivitySaga.js | 6 +++--- packages/core/src/sagas/sendFilesToPostActivitySaga.js | 7 +++---- packages/core/src/sagas/sendPostBackToPostActivitySaga.js | 6 +++--- packages/core/src/sagas/sendTypingOnSetSendBoxSaga.js | 8 ++++---- .../core/src/sagas/startDictateAfterSpeakActivitySaga.js | 5 +++-- packages/core/src/sagas/stopSpeakActivityOnInputSaga.js | 1 + packages/core/src/sagas/submitSendBoxSaga.js | 2 +- 11 files changed, 27 insertions(+), 26 deletions(-) diff --git a/packages/core/src/sagas/clearSuggestedActionsOnPostActivitySaga.js b/packages/core/src/sagas/clearSuggestedActionsOnPostActivitySaga.js index 48071760e1..0535dadb4c 100644 --- a/packages/core/src/sagas/clearSuggestedActionsOnPostActivitySaga.js +++ b/packages/core/src/sagas/clearSuggestedActionsOnPostActivitySaga.js @@ -11,9 +11,10 @@ import setSuggestedActions from '../actions/setSuggestedActions'; export default function* () { yield whileConnected(function* () { yield takeEvery( - ({ payload, type }) => + ({ payload, type }) => ( type === POST_ACTIVITY_PENDING - && payload.activity.type === 'message', + && payload.activity.type === 'message' + ), function* () { yield put(setSuggestedActions()); } diff --git a/packages/core/src/sagas/incomingActivitySaga.js b/packages/core/src/sagas/incomingActivitySaga.js index 3e5118416c..a6a29546a8 100644 --- a/packages/core/src/sagas/incomingActivitySaga.js +++ b/packages/core/src/sagas/incomingActivitySaga.js @@ -40,7 +40,7 @@ export default function* () { yield put(incomingActivity(activity)); // Update suggested actions - const activities = yield select(({ activities }) => activities); + const { activities } = yield select(); const lastMessageActivity = last(activities, ({ type }) => type === 'message'); if ( diff --git a/packages/core/src/sagas/markActivityForSpeakSaga.js b/packages/core/src/sagas/markActivityForSpeakSaga.js index 550b6eef2a..e146051184 100644 --- a/packages/core/src/sagas/markActivityForSpeakSaga.js +++ b/packages/core/src/sagas/markActivityForSpeakSaga.js @@ -18,11 +18,10 @@ export default function* () { function* markActivityForSpeakSaga(userID) { yield takeEvery( - ({ payload, type }) => + ({ payload, type }) => ( type === INCOMING_ACTIVITY - && payload - && payload.activity - && speakableActivity(payload.activity, userID), + && speakableActivity(payload.activity, userID) + ), function* ({ payload: { activity } }) { yield put(markActivity(activity, 'speak', true)); } diff --git a/packages/core/src/sagas/postActivitySaga.js b/packages/core/src/sagas/postActivitySaga.js index 02296184c0..188c2ff97f 100644 --- a/packages/core/src/sagas/postActivitySaga.js +++ b/packages/core/src/sagas/postActivitySaga.js @@ -38,7 +38,7 @@ export default function* () { } function* postActivitySaga(directLine, userID, numActivitiesPosted, { meta: { via }, payload: { activity } }) { - const locale = yield select(({ language }) => language); + const { language: locale } = yield select(); const { attachments, channelData: { clientActivityID = uniqueID() } = {} } = activity; activity = { @@ -96,7 +96,7 @@ function* postActivitySaga(directLine, userID, numActivitiesPosted, { meta: { vi // - Direct Line service only respond on HTTP after bot respond to Direct Line // - Activity may take too long time to echo back - const sendTimeout = yield select(({ sendTimeout }) => sendTimeout); + const { sendTimeout } = yield select(); const { send: { echoBack } } = yield race({ send: all({ diff --git a/packages/core/src/sagas/sendEventToPostActivitySaga.js b/packages/core/src/sagas/sendEventToPostActivitySaga.js index d94622d1e7..f0cc52ac8f 100644 --- a/packages/core/src/sagas/sendEventToPostActivitySaga.js +++ b/packages/core/src/sagas/sendEventToPostActivitySaga.js @@ -11,10 +11,10 @@ import postActivity from '../actions/postActivity'; export default function* () { yield whileConnected(function* () { yield takeEvery( - ({ payload, type }) => + ({ payload, type }) => ( type === SEND_EVENT - && payload - && payload.name, + && payload.name + ), function* ({ payload: { name, value } }) { yield put(postActivity({ name, diff --git a/packages/core/src/sagas/sendFilesToPostActivitySaga.js b/packages/core/src/sagas/sendFilesToPostActivitySaga.js index 33d103f535..3a0ad5bbb9 100644 --- a/packages/core/src/sagas/sendFilesToPostActivitySaga.js +++ b/packages/core/src/sagas/sendFilesToPostActivitySaga.js @@ -15,11 +15,10 @@ const getType = mime.getType.bind(mime); export default function* () { yield whileConnected(function* () { yield takeEvery( - ({ payload, type }) => + ({ payload, type }) => ( type === SEND_FILES - && payload - && payload.files - && payload.files.length, + && payload.files.length + ), function* ({ payload: { files } }) { yield put(postActivity({ attachments: [].map.call(files, file => ({ diff --git a/packages/core/src/sagas/sendPostBackToPostActivitySaga.js b/packages/core/src/sagas/sendPostBackToPostActivitySaga.js index c4588fad6c..c4c4ce511e 100644 --- a/packages/core/src/sagas/sendPostBackToPostActivitySaga.js +++ b/packages/core/src/sagas/sendPostBackToPostActivitySaga.js @@ -10,10 +10,10 @@ import whileConnected from './effects/whileConnected'; export default function* () { yield whileConnected(function* () { yield takeEvery( - ({ payload, type }) => + ({ payload, type }) => ( type === SEND_POST_BACK - && payload - && payload.value, + && payload.value + ), function* ({ payload: { value } }) { yield put(postActivity({ channelData: { diff --git a/packages/core/src/sagas/sendTypingOnSetSendBoxSaga.js b/packages/core/src/sagas/sendTypingOnSetSendBoxSaga.js index dc1ba06a52..bd6df187e9 100644 --- a/packages/core/src/sagas/sendTypingOnSetSendBoxSaga.js +++ b/packages/core/src/sagas/sendTypingOnSetSendBoxSaga.js @@ -22,7 +22,7 @@ function takeSendTyping(value) { export default function* () { yield whileConnected(function* () { - const sendTyping = yield select(({ sendTyping }) => sendTyping); + const { sendTyping } = yield select(); if (!sendTyping) { yield takeSendTyping(true); @@ -31,10 +31,10 @@ export default function* () { for (;;) { let lastSend = 0; const task = yield takeLatest( - ({ payload, type }) => + ({ payload, type }) => ( type === SET_SEND_BOX - && payload - && payload.text, + && payload.text + ), function* () { const interval = SEND_INTERVAL - Date.now() + lastSend; diff --git a/packages/core/src/sagas/startDictateAfterSpeakActivitySaga.js b/packages/core/src/sagas/startDictateAfterSpeakActivitySaga.js index e276bf0666..9eedaf853a 100644 --- a/packages/core/src/sagas/startDictateAfterSpeakActivitySaga.js +++ b/packages/core/src/sagas/startDictateAfterSpeakActivitySaga.js @@ -18,10 +18,11 @@ export default function* () { function* startDictateAfterSpeakActivitySaga() { yield takeEvery( - ({ payload, type }) => + ({ payload, type }) => ( type === MARK_ACTIVITY && payload.name === 'speak' - && payload.value === false, + && payload.value === false + ), function* ({ payload: { activityID } }) { const { activities } = yield select(); const activity = activities.find(({ id }) => id === activityID); diff --git a/packages/core/src/sagas/stopSpeakActivityOnInputSaga.js b/packages/core/src/sagas/stopSpeakActivityOnInputSaga.js index cdef5abe4f..6e9fa3cd7a 100644 --- a/packages/core/src/sagas/stopSpeakActivityOnInputSaga.js +++ b/packages/core/src/sagas/stopSpeakActivityOnInputSaga.js @@ -24,6 +24,7 @@ export default function* () { // We want to stop speaking activity when the user click on a card action // But currently there are no actions generated out of a card action // So, right now, we are using best-effort by listening to POST_ACTIVITY_PENDING with a "message" event + // We explicitly filter out speech because we will call startSpeakingActivity() for POST_ACTIVITY_PENDING via speech || ( type === POST_ACTIVITY_PENDING && meta.via !== 'speech' diff --git a/packages/core/src/sagas/submitSendBoxSaga.js b/packages/core/src/sagas/submitSendBoxSaga.js index 31c877e3bb..a3c041af20 100644 --- a/packages/core/src/sagas/submitSendBoxSaga.js +++ b/packages/core/src/sagas/submitSendBoxSaga.js @@ -13,7 +13,7 @@ import setSendBox from '../actions/setSendBox'; export default function* () { yield whileConnected(function* () { yield takeEvery(SUBMIT_SEND_BOX, function* ({ payload: { via } }) { - const sendBoxValue = yield select(({ sendBoxValue }) => sendBoxValue); + const { sendBoxValue } = yield select(); if (sendBoxValue) { yield put(sendMessage(sendBoxValue, via)); From f589c9b2a4f498c2ab672cf94f61f0054da145bc Mon Sep 17 00:00:00 2001 From: William Wong Date: Sat, 22 Dec 2018 04:21:51 -0800 Subject: [PATCH 12/27] Clean up --- packages/core/src/sagas/stopSpeakActivityOnInputSaga.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/sagas/stopSpeakActivityOnInputSaga.js b/packages/core/src/sagas/stopSpeakActivityOnInputSaga.js index 6e9fa3cd7a..c0eade78a7 100644 --- a/packages/core/src/sagas/stopSpeakActivityOnInputSaga.js +++ b/packages/core/src/sagas/stopSpeakActivityOnInputSaga.js @@ -24,7 +24,7 @@ export default function* () { // We want to stop speaking activity when the user click on a card action // But currently there are no actions generated out of a card action // So, right now, we are using best-effort by listening to POST_ACTIVITY_PENDING with a "message" event - // We explicitly filter out speech because we will call startSpeakingActivity() for POST_ACTIVITY_PENDING via speech + // We filter out speech because we will call startSpeakingActivity() for POST_ACTIVITY_PENDING dispatched by speech || ( type === POST_ACTIVITY_PENDING && meta.via !== 'speech' From aa7f70b617e250ceb0a7066bbae476a6996e1aba Mon Sep 17 00:00:00 2001 From: William Wong Date: Sun, 23 Dec 2018 20:55:02 -0800 Subject: [PATCH 13/27] Move send typing to another branch and clean up sample --- CHANGELOG.md | 1 + README.md | 1 + packages/core/src/reducer.ts | 2 -- packages/core/src/reducers/activities.js | 7 +------ packages/core/src/reducers/sendTyping.js | 15 --------------- packages/core/src/sagas/incomingTypingSaga.js | 6 ++++-- packages/core/src/sagas/postActivitySaga.js | 4 ++-- .../core/src/sagas/sendTypingOnSetSendBoxSaga.js | 2 +- .../index.html | 5 ++++- 9 files changed, 14 insertions(+), 29 deletions(-) delete mode 100644 packages/core/src/reducers/sendTyping.js rename samples/{send-join-event => 15.b.backchannel-send-welcome-event}/index.html (88%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a8f0c1f8e..437d175b6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -59,6 +59,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - `component`: [Avatar image](https://microsoft.github.io/BotFramework-WebChat/04.b.display-user-bot-images-styling/), in [#1486](https://github.com/Microsoft/BotFramework-WebChat/pull/1486) - `core`: [Incoming activity to JavaScript event](https://microsoft.github.io/BotFramework-WebChat/15.b.incoming-activity-event/), in [#1567](https://github.com/Microsoft/BotFramework-WebChat/pull/1567) - Backchannel: New send join event sample, in PR [#1286](https://github.com/Microsoft/BotFramework-WebChat/pull/1286) +- Backchannel: [Send welcome event](https://microsoft.github.io/BotFramework-WebChat/15.b.backchannel-send-welcome-event/), in PR [#1286](https://github.com/Microsoft/BotFramework-WebChat/pull/1286) ## [4.2.0] - 2018-12-11 ### Added diff --git a/README.md b/README.md index af523b175d..fe16ed085f 100644 --- a/README.md +++ b/README.md @@ -236,6 +236,7 @@ npm run prepublishOnly | [`15.a.backchannel-piggyback-on-outgoing-activities`](https://github.com/Microsoft/BotFramework-WebChat/tree/master/samples/15.a.backchannel-piggyback-on-outgoing-activities) | Advanced tutorial: Demonstrates how to add custom data to every outgoing activities. | [Demo](https://microsoft.github.io/BotFramework-WebChat/15.a.backchannel-piggyback-on-outgoing-activities) | | [`15.b.incoming-activity-event`](https://github.com/Microsoft/BotFramework-WebChat/tree/master/samples/15.a.incoming-activity-event) | Advanced tutorial: Demonstrates how to forward all incoming activities to a JavaScript event for further processing. | [Demo](https://microsoft.github.io/BotFramework-WebChat/15.a.incoming-activity-event) | | [`15.c.programmatic-post-activity`](https://github.com/Microsoft/BotFramework-WebChat/tree/master/samples/15.c.programmatic-post-activity) | Advanced tutorial: Demonstrates how to send a message programmatically. | [Demo](https://microsoft.github.io/BotFramework-WebChat/15.c.programmatic-post-activity) | +| [`15.b.backchannel-send-welcome-event`](https://github.com/Microsoft/BotFramework-WebChat/tree/master/samples/15.b.backchannel-send-welcome-event) | Advanced tutorial: Demonstrates how to send welcome event with client capabilities such as browser language. | [Demo](https://microsoft.github.io/BotFramework-WebChat/15.b.backchannel-send-welcome-event) | # Contributions diff --git a/packages/core/src/reducer.ts b/packages/core/src/reducer.ts index e4acaf7329..ad28c21f58 100644 --- a/packages/core/src/reducer.ts +++ b/packages/core/src/reducer.ts @@ -8,7 +8,6 @@ import readyState from './reducers/readyState'; import referenceGrammarID from './reducers/referenceGrammarID'; import sendBoxValue from './reducers/sendBoxValue'; import sendTimeout from './reducers/sendTimeout'; -import sendTyping from './reducers/sendTyping'; import suggestedActions from './reducers/suggestedActions'; export default combineReducers({ @@ -20,6 +19,5 @@ export default combineReducers({ referenceGrammarID, sendBoxValue, sendTimeout, - sendTyping, suggestedActions }) diff --git a/packages/core/src/reducers/activities.js b/packages/core/src/reducers/activities.js index f743815a1a..a8c397f819 100644 --- a/packages/core/src/reducers/activities.js +++ b/packages/core/src/reducers/activities.js @@ -25,14 +25,9 @@ function findByClientActivityID(clientActivityID) { function upsertActivityWithSort(activities, nextActivity) { const { channelData: { clientActivityID: nextClientActivityID } = {}, - from: { id: nextFromID, role: nextFromRole } = {}, - type: nextType + from: { id: nextFromID } = {} } = nextActivity; - if (nextType === 'typing' && nextFromRole === 'user') { - return activities; - } - const nextTimestamp = Date.parse(nextActivity.timestamp); const nextActivities = activities.filter(({ channelData: { clientActivityID } = {}, from, type }) => // We will remove all "typing" and "sending messages" activities diff --git a/packages/core/src/reducers/sendTyping.js b/packages/core/src/reducers/sendTyping.js deleted file mode 100644 index d7f1dc280c..0000000000 --- a/packages/core/src/reducers/sendTyping.js +++ /dev/null @@ -1,15 +0,0 @@ -import { SET_SEND_TYPING } from '../actions/setSendTyping'; - -const DEFAULT_STATE = false; - -export default function (state = DEFAULT_STATE, { payload, type }) { - switch (type) { - case SET_SEND_TYPING: - state = payload.sendTyping; - break; - - default: break; - } - - return state; -} diff --git a/packages/core/src/sagas/incomingTypingSaga.js b/packages/core/src/sagas/incomingTypingSaga.js index 17496c156a..4df8196c62 100644 --- a/packages/core/src/sagas/incomingTypingSaga.js +++ b/packages/core/src/sagas/incomingTypingSaga.js @@ -1,7 +1,7 @@ import { call, put, - takeEvery + takeLatest } from 'redux-saga/effects'; import deleteActivity from '../actions/deleteActivity'; @@ -16,7 +16,9 @@ function isTypingActivity({ type, payload }) { } export default function* () { - yield takeEvery(isTypingActivity, function* ({ payload: { activity: { id } } }) { + yield takeLatest(isTypingActivity, function* ({ payload: { activity } }) { + const id = activity.id; + yield call(sleep, 5000); yield put(deleteActivity(id)); }); diff --git a/packages/core/src/sagas/postActivitySaga.js b/packages/core/src/sagas/postActivitySaga.js index 188c2ff97f..02296184c0 100644 --- a/packages/core/src/sagas/postActivitySaga.js +++ b/packages/core/src/sagas/postActivitySaga.js @@ -38,7 +38,7 @@ export default function* () { } function* postActivitySaga(directLine, userID, numActivitiesPosted, { meta: { via }, payload: { activity } }) { - const { language: locale } = yield select(); + const locale = yield select(({ language }) => language); const { attachments, channelData: { clientActivityID = uniqueID() } = {} } = activity; activity = { @@ -96,7 +96,7 @@ function* postActivitySaga(directLine, userID, numActivitiesPosted, { meta: { vi // - Direct Line service only respond on HTTP after bot respond to Direct Line // - Activity may take too long time to echo back - const { sendTimeout } = yield select(); + const sendTimeout = yield select(({ sendTimeout }) => sendTimeout); const { send: { echoBack } } = yield race({ send: all({ diff --git a/packages/core/src/sagas/sendTypingOnSetSendBoxSaga.js b/packages/core/src/sagas/sendTypingOnSetSendBoxSaga.js index bd6df187e9..8ac6b92573 100644 --- a/packages/core/src/sagas/sendTypingOnSetSendBoxSaga.js +++ b/packages/core/src/sagas/sendTypingOnSetSendBoxSaga.js @@ -22,7 +22,7 @@ function takeSendTyping(value) { export default function* () { yield whileConnected(function* () { - const { sendTyping } = yield select(); + const sendTyping = yield select(({ sendTyping }) => sendTyping); if (!sendTyping) { yield takeSendTyping(true); diff --git a/samples/send-join-event/index.html b/samples/15.b.backchannel-send-welcome-event/index.html similarity index 88% rename from samples/send-join-event/index.html rename to samples/15.b.backchannel-send-welcome-event/index.html index 2bd72027fa..785644fef0 100644 --- a/samples/send-join-event/index.html +++ b/samples/15.b.backchannel-send-welcome-event/index.html @@ -1,7 +1,7 @@ - Web Chat: Full-featured bundle + Web Chat: Send welcome event