diff --git a/CHANGELOG.md b/CHANGELOG.md
index e513478297..c3ae904cf4 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,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)
+- `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)
+- `component`: 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)
+- `component`: 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)
@@ -52,6 +58,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)
+- `core`: [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..67dbb35a84 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.d.backchannel-send-welcome-event`](https://github.com/Microsoft/BotFramework-WebChat/tree/master/samples/15.d.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.d.backchannel-send-welcome-event) |
# Contributions
diff --git a/packages/component/src/BasicTranscript.js b/packages/component/src/BasicTranscript.js
index e8f7645263..e38d88719d 100644
--- a/packages/component/src/BasicTranscript.js
+++ b/packages/component/src/BasicTranscript.js
@@ -110,7 +110,10 @@ const BasicTranscript = ({
role="listitem"
>
{ activityRenderer({ activity, showTimestamp })(({ attachment }) => attachmentRenderer({ activity, attachment })) }
- { activity.channelData && activity.channelData.speak && }
+ {
+ // TODO: [P2] We should use core/definitions/speakingActivity for this predicate instead
+ activity.channelData && activity.channelData.speak &&
+ }
);
})
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/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/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/postActivity.js b/packages/core/src/actions/postActivity.js
index 9fb666f3fd..89375e4d9b 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, method = 'keyboard') {
return {
type: POST_ACTIVITY,
+ meta: { method },
payload: { activity }
};
}
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/actions/sendMessage.js b/packages/core/src/actions/sendMessage.js
index cb937ec1ec..a96f51740d 100644
--- a/packages/core/src/actions/sendMessage.js
+++ b/packages/core/src/actions/sendMessage.js
@@ -1,9 +1,9 @@
const SEND_MESSAGE = 'WEB_CHAT/SEND_MESSAGE';
-export default function sendMessage(text, via) {
+export default function sendMessage(text, method) {
return {
type: SEND_MESSAGE,
- payload: { text, via }
+ payload: { method, text }
};
}
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..aaec313686 100644
--- a/packages/core/src/actions/submitSendBox.js
+++ b/packages/core/src/actions/submitSendBox.js
@@ -1,9 +1,9 @@
const SUBMIT_SEND_BOX = 'WEB_CHAT/SUBMIT_SEND_BOX';
-export default function submitSendBox(via) {
+export default function submitSendBox(method = 'keyboard') {
return {
type: SUBMIT_SEND_BOX,
- payload: { via }
+ payload: { method }
};
}
diff --git a/packages/core/src/definitions/activityFromBot.js b/packages/core/src/definitions/activityFromBot.js
new file mode 100644
index 0000000000..8570b01d2d
--- /dev/null
+++ b/packages/core/src/definitions/activityFromBot.js
@@ -0,0 +1 @@
+export default activity => activity && activity.from.role === 'bot'
diff --git a/packages/core/src/sagas/definition/speakableActivity.js b/packages/core/src/definitions/speakableActivity.js
similarity index 52%
rename from packages/core/src/sagas/definition/speakableActivity.js
rename to packages/core/src/definitions/speakableActivity.js
index 502fbcbb42..6a3605ca37 100644
--- a/packages/core/src/sagas/definition/speakableActivity.js
+++ b/packages/core/src/definitions/speakableActivity.js
@@ -1,8 +1,7 @@
-export default function (activity, userID) {
+export default function (activity) {
return (
activity
&& activity.from
- && activity.from.id !== userID
&& activity.type === 'message'
);
}
diff --git a/packages/core/src/definitions/speakingActivity.js b/packages/core/src/definitions/speakingActivity.js
new file mode 100644
index 0000000000..ac9e1e203a
--- /dev/null
+++ b/packages/core/src/definitions/speakingActivity.js
@@ -0,0 +1,3 @@
+export default function (activity) {
+ return activity.channelData && activity.channelData.speak;
+}
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..bc064e8b52 100644
--- a/packages/core/src/sagas.js
+++ b/packages/core/src/sagas.js
@@ -4,16 +4,19 @@ import clearSuggestedActionsOnPostActivitySaga from './sagas/clearSuggestedActio
import connectionStatusUpdateSaga from './sagas/connectionStatusUpdateSaga';
import connectSaga from './sagas/connectSaga';
import incomingActivitySaga from './sagas/incomingActivitySaga';
-import incomingTypingSaga from './sagas/incomingTypingSaga';
-import markActivityForSpeakSaga from './sagas/markActivityForSpeakSaga';
+import markActivityForSpeakOnIncomingActivityFromOthersSaga from './sagas/markActivityForSpeakOnIncomingActivityFromOthersSaga';
+import markAllAsSpokenOnStopSpeakActivitySaga from './sagas/markAllAsSpokenOnStopSpeakActivitySaga';
import postActivitySaga from './sagas/postActivitySaga';
+import removeIncomingTypingAfterIntervalSaga from './sagas/removeIncomingTypingAfterIntervalSaga';
+import sendEventToPostActivitySaga from './sagas/sendEventToPostActivitySaga';
import sendFilesToPostActivitySaga from './sagas/sendFilesToPostActivitySaga';
import sendMessageToPostActivitySaga from './sagas/sendMessageToPostActivitySaga';
import sendPostBackToPostActivitySaga from './sagas/sendPostBackToPostActivitySaga';
import sendTypingOnSetSendBoxSaga from './sagas/sendTypingOnSetSendBoxSaga';
import startDictateAfterSpeakActivitySaga from './sagas/startDictateAfterSpeakActivitySaga';
-import stopDictateOnCardAction from './sagas/stopDictateOnCardAction';
-import stopSpeakActivityOnInputSaga from './sagas/stopSpeakActivityOnInputSaga';
+import startSpeakActivityOnPostActivitySaga from './sagas/startSpeakActivityOnPostActivitySaga';
+import stopDictateOnCardActionSaga from './sagas/stopDictateOnCardActionSaga';
+import stopSpeakingActivityOnInputSaga from './sagas/stopSpeakingActivityOnInputSaga';
import submitSendBoxSaga from './sagas/submitSendBoxSaga';
export default function* () {
@@ -21,15 +24,18 @@ export default function* () {
yield fork(connectionStatusUpdateSaga);
yield fork(connectSaga);
yield fork(incomingActivitySaga);
- yield fork(incomingTypingSaga);
- yield fork(markActivityForSpeakSaga);
+ yield fork(markActivityForSpeakOnIncomingActivityFromOthersSaga);
+ yield fork(markAllAsSpokenOnStopSpeakActivitySaga);
yield fork(postActivitySaga);
+ yield fork(removeIncomingTypingAfterIntervalSaga);
+ yield fork(sendEventToPostActivitySaga);
yield fork(sendFilesToPostActivitySaga);
yield fork(sendMessageToPostActivitySaga);
yield fork(sendPostBackToPostActivitySaga);
yield fork(sendTypingOnSetSendBoxSaga);
yield fork(startDictateAfterSpeakActivitySaga);
- yield fork(stopDictateOnCardAction);
- yield fork(stopSpeakActivityOnInputSaga);
+ yield fork(startSpeakActivityOnPostActivitySaga);
+ yield fork(stopDictateOnCardActionSaga);
+ yield fork(stopSpeakingActivityOnInputSaga);
yield fork(submitSendBoxSaga);
}
diff --git a/packages/core/src/sagas/clearSuggestedActionsOnPostActivitySaga.js b/packages/core/src/sagas/clearSuggestedActionsOnPostActivitySaga.js
index e92722e7f9..ccd8a57e8a 100644
--- a/packages/core/src/sagas/clearSuggestedActionsOnPostActivitySaga.js
+++ b/packages/core/src/sagas/clearSuggestedActionsOnPostActivitySaga.js
@@ -1,17 +1,27 @@
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 whileConnected(clearSuggestedActionsOnPostActivity);
+}
+
+function* clearSuggestedActionsOnPostActivity() {
+ yield takeEvery(
+ ({ payload, type }) => (
+ type === POST_ACTIVITY_PENDING
+ && payload.activity.type === 'message'
+ ),
+ clearSuggestedActions
+ );
+}
+
+function* clearSuggestedActions() {
+ yield put(setSuggestedActions());
}
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/connectionStatusUpdateSaga.js b/packages/core/src/sagas/connectionStatusUpdateSaga.js
index f53c3bd716..927ac0b2cc 100644
--- a/packages/core/src/sagas/connectionStatusUpdateSaga.js
+++ b/packages/core/src/sagas/connectionStatusUpdateSaga.js
@@ -9,10 +9,12 @@ import connectionStatusUpdate from '../actions/connectionStatusUpdate';
import setReferenceGrammarID from '../actions/setReferenceGrammarID';
export default function* () {
- yield whileConnected(function* (directLine) {
- yield observeEach(directLine.connectionStatus$, function* (connectionStatus) {
- yield put(connectionStatusUpdate(connectionStatus));
- yield put(setReferenceGrammarID(directLine.referenceGrammarId));
- });
+ yield whileConnected(observeConnectionStatus);
+}
+
+function* observeConnectionStatus(directLine) {
+ yield observeEach(directLine.connectionStatus$, function* (connectionStatus) {
+ yield put(connectionStatusUpdate(connectionStatus));
+ yield put(setReferenceGrammarID(directLine.referenceGrammarId));
});
}
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/effects/whileSpeakIncomingActivity.js b/packages/core/src/sagas/effects/whileSpeakIncomingActivity.js
new file mode 100644
index 0000000000..a9bf568468
--- /dev/null
+++ b/packages/core/src/sagas/effects/whileSpeakIncomingActivity.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/incomingActivitySaga.js b/packages/core/src/sagas/incomingActivitySaga.js
index 3e5118416c..6a4fd759a6 100644
--- a/packages/core/src/sagas/incomingActivitySaga.js
+++ b/packages/core/src/sagas/incomingActivitySaga.js
@@ -3,55 +3,57 @@ import {
select
} from 'redux-saga/effects';
-import observeEach from './effects/observeEach';
-import whileConnected from './effects/whileConnected';
+import updateIn from 'simple-update-in';
import incomingActivity from '../actions/incomingActivity';
import setSuggestedActions from '../actions/setSuggestedActions';
-function last(array, predicate) {
- for (let i = array.length - 1; i >= 0; i--) {
- const item = array[i];
+import activityFromBot from '../definitions/activityFromBot';
- if (predicate.call(array, item)) {
- return item;
- }
- }
-}
+import observeEach from './effects/observeEach';
+import whileConnected from './effects/whileConnected';
+
+import { ofType as activitiesOfType } from '../selectors/activities';
export default function* () {
- yield whileConnected(function* (directLine, userID) {
- yield observeEach(directLine.activity$, function* (activity) {
- activity = { ...activity };
-
- // Patch activity.from.role to make sure its either "bot", "user", or "channel"
- if (!activity.from) {
- activity.from = { role: 'channel' };
- } else if (!activity.from.role) {
- if (activity.from.id === userID) {
- activity.from.role = 'user';
- } else if (activity.from.id) {
- activity.from.role = 'bot';
- } else {
- activity.from.role = 'channel';
- }
- }
-
- yield put(incomingActivity(activity));
-
- // Update suggested actions
- const activities = yield select(({ activities }) => activities);
- const lastMessageActivity = last(activities, ({ type }) => type === 'message');
-
- if (
- lastMessageActivity
- && lastMessageActivity.from
- && lastMessageActivity.from.role === 'bot'
- ) {
- const { suggestedActions: { actions } = {} } = lastMessageActivity;
-
- yield put(setSuggestedActions(actions));
- }
- });
+ yield whileConnected(observeActivity);
+}
+
+function* observeActivity(directLine, userID) {
+ yield observeEach(directLine.activity$, function* (activity) {
+ activity = patchActivityWithFromRole(activity, userID);
+
+ yield put(incomingActivity(activity));
+
+ // Update suggested actions
+ const messageActivities = yield select(activitiesOfType('message'));
+ const lastMessageActivity = messageActivities[messageActivities.length - 1];
+
+ if (activityFromBot(lastMessageActivity)) {
+ const { suggestedActions: { actions } = {} } = lastMessageActivity;
+
+ yield put(setSuggestedActions(actions));
+ }
});
}
+
+function patchActivityWithFromRole(activity, userID) {
+ // Some activities, such as "ConversationUpdate", does not have "from" defined.
+ // And although "role" is defined in Direct Line spec, it was not sent over the wire.
+ // We normalize the activity here to simplify null-check and logic later.
+
+ // Patch activity.from.role to make sure its either "bot", "user", or "channel"
+ if (!activity.from) {
+ activity = updateIn(activity, ['from', 'role'], () => 'channel');
+ } else if (!activity.from.role) {
+ if (activity.from.id === userID) {
+ activity = updateIn(activity, ['from', 'role'], () => 'user');
+ } else if (activity.from.id) {
+ activity = updateIn(activity, ['from', 'role'], () => 'bot');
+ } else {
+ activity = updateIn(activity, ['from', 'role'], () => 'channel');
+ }
+ }
+
+ return activity;
+}
diff --git a/packages/core/src/sagas/markActivityForSpeakOnIncomingActivityFromOthersSaga.js b/packages/core/src/sagas/markActivityForSpeakOnIncomingActivityFromOthersSaga.js
new file mode 100644
index 0000000000..50c2f0ff1e
--- /dev/null
+++ b/packages/core/src/sagas/markActivityForSpeakOnIncomingActivityFromOthersSaga.js
@@ -0,0 +1,33 @@
+import {
+ put,
+ takeEvery
+} from 'redux-saga/effects';
+
+import speakableActivity from '../definitions/speakableActivity';
+
+import whileConnected from './effects/whileConnected';
+import whileSpeakIncomingActivity from './effects/whileSpeakIncomingActivity';
+
+import { INCOMING_ACTIVITY } from '../actions/incomingActivity';
+import markActivity from '../actions/markActivity';
+
+export default function* () {
+ yield whileConnected(function* (_, userID) {
+ yield whileSpeakIncomingActivity(markActivityForSpeakOnIncomingActivityFromOthers.bind(null, userID));
+ });
+}
+
+function* markActivityForSpeakOnIncomingActivityFromOthers(userID) {
+ yield takeEvery(
+ ({ payload, type }) => (
+ type === INCOMING_ACTIVITY
+ && speakableActivity(payload.activity)
+ && payload.activity.from.id !== userID
+ ),
+ markActivityForSpeak
+ );
+}
+
+function* markActivityForSpeak({ payload: { activity } }) {
+ yield put(markActivity(activity, 'speak', true));
+}
diff --git a/packages/core/src/sagas/markActivityForSpeakSaga.js b/packages/core/src/sagas/markActivityForSpeakSaga.js
deleted file mode 100644
index e1c50f81eb..0000000000
--- a/packages/core/src/sagas/markActivityForSpeakSaga.js
+++ /dev/null
@@ -1,39 +0,0 @@
-import {
- cancel,
- fork,
- put,
- take
-} from 'redux-saga/effects';
-
-import speakableActivity from './definition/speakableActivity';
-import whileConnected from './effects/whileConnected';
-
-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 = yield fork(markActivityForSpeakSaga, userID);
-
- 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/markAllAsSpokenOnStopSpeakActivitySaga.js b/packages/core/src/sagas/markAllAsSpokenOnStopSpeakActivitySaga.js
new file mode 100644
index 0000000000..84f9617830
--- /dev/null
+++ b/packages/core/src/sagas/markAllAsSpokenOnStopSpeakActivitySaga.js
@@ -0,0 +1,24 @@
+import {
+ put,
+ select,
+ takeEvery
+} from 'redux-saga/effects';
+
+import { STOP_SPEAKING_ACTIVITY } from '../actions/stopSpeakingActivity';
+import markActivity from '../actions/markActivity';
+
+import speakingActivity from '../definitions/speakingActivity';
+
+import { of as activitiesOf } from '../selectors/activities';
+
+export default function* () {
+ yield takeEvery(STOP_SPEAKING_ACTIVITY, markAllAsSpoken);
+}
+
+function* markAllAsSpoken() {
+ const speakingActivities = yield select(activitiesOf(speakingActivity));
+
+ for (let activity of speakingActivities) {
+ yield put(markActivity(activity, 'speak', false));
+ }
+}
diff --git a/packages/core/src/sagas/postActivitySaga.js b/packages/core/src/sagas/postActivitySaga.js
index 75f44aad46..8218c93a3c 100644
--- a/packages/core/src/sagas/postActivitySaga.js
+++ b/packages/core/src/sagas/postActivitySaga.js
@@ -2,20 +2,22 @@ import {
all,
call,
cancelled,
- fork,
put,
race,
select,
- take
+ take,
+ takeEvery
} from 'redux-saga/effects';
-import sleep from '../utils/sleep';
-
import observeOnce from './effects/observeOnce';
import whileConnected from './effects/whileConnected';
+import languageSelector from '../selectors/language';
+import sendTimeoutSelector from '../selectors/sendTimeout';
+
import deleteKey from '../utils/deleteKey';
import getTimestamp from '../utils/getTimestamp';
+import sleep from '../utils/sleep';
import uniqueID from '../utils/uniqueID';
import {
@@ -29,16 +31,16 @@ 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);
+ let numActivitiesPosted = 0;
- yield fork(postActivity, directLine, userID, numActivitiesPosted, action);
- }
+ yield takeEvery(POST_ACTIVITY, function* (action) {
+ yield* postActivity(directLine, userID, numActivitiesPosted++, action);
+ });
});
}
-function* postActivity(directLine, userID, numActivitiesPosted, { payload: { activity } }) {
- const locale = yield select(({ language }) => language);
+function* postActivity(directLine, userID, numActivitiesPosted, { meta: { method }, payload: { activity } }) {
+ const locale = yield select(languageSelector);
const { attachments, channelData: { clientActivityID = uniqueID() } = {} } = activity;
activity = {
@@ -72,9 +74,9 @@ function* postActivity(directLine, userID, numActivitiesPosted, { payload: { act
}];
}
- const meta = { clientActivityID };
+ const meta = { clientActivityID, method };
- 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
@@ -96,7 +98,7 @@ function* postActivity(directLine, userID, numActivitiesPosted, { payload: { act
// - 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(sendTimeoutSelector);
const { send: { echoBack } } = yield race({
send: all({
diff --git a/packages/core/src/sagas/incomingTypingSaga.js b/packages/core/src/sagas/removeIncomingTypingAfterIntervalSaga.js
similarity index 50%
rename from packages/core/src/sagas/incomingTypingSaga.js
rename to packages/core/src/sagas/removeIncomingTypingAfterIntervalSaga.js
index 4df8196c62..953c615ab5 100644
--- a/packages/core/src/sagas/incomingTypingSaga.js
+++ b/packages/core/src/sagas/removeIncomingTypingAfterIntervalSaga.js
@@ -11,15 +11,17 @@ function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
-function isTypingActivity({ type, payload }) {
- return type === INCOMING_ACTIVITY && payload.activity.type === 'typing';
-}
-
export default function* () {
- yield takeLatest(isTypingActivity, function* ({ payload: { activity } }) {
- const id = activity.id;
+ yield takeLatest(
+ ({ type, payload }) => (
+ type === INCOMING_ACTIVITY
+ && payload.activity.type === 'typing'
+ ),
+ removeActivityAfterInterval
+ );
+}
- yield call(sleep, 5000);
- yield put(deleteActivity(id));
- });
+function* removeActivityAfterInterval({ payload: { activity: { id } } }) {
+ yield call(sleep, 5000);
+ yield put(deleteActivity(id));
}
diff --git a/packages/core/src/sagas/sendEventToPostActivitySaga.js b/packages/core/src/sagas/sendEventToPostActivitySaga.js
new file mode 100644
index 0000000000..442f7522c7
--- /dev/null
+++ b/packages/core/src/sagas/sendEventToPostActivitySaga.js
@@ -0,0 +1,31 @@
+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(sendEventToPostActivity);
+}
+
+function* sendEventToPostActivity() {
+ yield takeEvery(
+ ({ payload, type }) => (
+ type === SEND_EVENT
+ && payload.name
+ ),
+ postActivityWithEvent
+ );
+}
+
+function* postActivityWithEvent({ 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 2612b99fbc..e16414eafa 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';
@@ -9,30 +9,33 @@ 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);
export default function* () {
- yield whileConnected(function* () {
- for (;;) {
- const { payload: { files } } = yield take(SEND_FILES);
+ yield whileConnected(sendFilesToPostActivity);
+}
- if (files.length) {
- yield put(postActivity({
- attachments: [].map.call(files, file => ({
- contentType: getType(file.name) || 'application/octet-stream',
- contentUrl: file.url,
- name: file.name
- })),
- channelData: {
- attachmentSizes: [].map.call(files, file => file.size)
- },
- type: 'message'
- }));
+function* sendFilesToPostActivity() {
+ yield takeEvery(
+ ({ payload, type }) => (
+ type === SEND_FILES
+ && payload.files.length
+ ),
+ postActivityWithFiles
+ );
+}
- yield put(stopSpeakingActivity());
- }
- }
- });
+function* postActivityWithFiles({ payload: { files } }) {
+ yield put(postActivity({
+ attachments: [].map.call(files, ({ name, url }) => ({
+ contentType: getType(name) || 'application/octet-stream',
+ contentUrl: url,
+ name: name
+ })),
+ channelData: {
+ attachmentSizes: [].map.call(files, ({ size }) => size)
+ },
+ type: 'message'
+ }));
}
diff --git a/packages/core/src/sagas/sendMessageToPostActivitySaga.js b/packages/core/src/sagas/sendMessageToPostActivitySaga.js
index 2c63b3ca39..1f6f244ce7 100644
--- a/packages/core/src/sagas/sendMessageToPostActivitySaga.js
+++ b/packages/core/src/sagas/sendMessageToPostActivitySaga.js
@@ -1,33 +1,28 @@
import {
put,
- take
+ takeEvery
} from 'redux-saga/effects';
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* () {
- for (;;) {
- const { payload: { text, via } } = yield take(SEND_MESSAGE);
+ yield whileConnected(sendMessageToPostActivity);
+}
- if (text) {
- yield put(postActivity({
- text,
- textFormat: 'plain',
- type: 'message'
- }));
+function* sendMessageToPostActivity() {
+ yield takeEvery(({ payload, type }) => (
+ type === SEND_MESSAGE
+ && payload.text
+ ), postActivityWithMessage);
+}
- if (via === 'speech') {
- yield put(startSpeakingActivity());
- } else {
- yield put(stopSpeakingActivity());
- }
- }
- }
- });
+function* postActivityWithMessage({ payload: { method, text } }) {
+ yield put(postActivity({
+ text,
+ textFormat: 'plain',
+ type: 'message'
+ }, method));
}
diff --git a/packages/core/src/sagas/sendPostBackToPostActivitySaga.js b/packages/core/src/sagas/sendPostBackToPostActivitySaga.js
index 4e45e952a1..958997407b 100644
--- a/packages/core/src/sagas/sendPostBackToPostActivitySaga.js
+++ b/packages/core/src/sagas/sendPostBackToPostActivitySaga.js
@@ -1,28 +1,33 @@
import {
put,
- take
+ 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* () {
- for (;;) {
- const { payload: { value } } = yield take(SEND_POST_BACK);
+ yield whileConnected(sendPostBackToPostActivity);
+}
+
+function* sendPostBackToPostActivity() {
+ yield takeEvery(
+ ({ payload, type }) => (
+ type === SEND_POST_BACK
+ && payload.value
+ ),
+ postActivityWithPostBack
+ );
+}
- if (value) {
- yield put(postActivity({
- channelData: {
- postBack: true
- },
- text: typeof value === 'string' ? value : undefined,
- type: 'message',
- value: typeof value !== 'string' ? value : undefined
- }));
- }
- }
- });
+function* postActivityWithPostBack({ payload: { value } }) {
+ yield put(postActivity({
+ channelData: {
+ postBack: true
+ },
+ text: typeof value === 'string' ? value : undefined,
+ type: 'message',
+ value: typeof value !== 'string' ? value : undefined
+ }));
}
diff --git a/packages/core/src/sagas/sendTypingOnSetSendBoxSaga.js b/packages/core/src/sagas/sendTypingOnSetSendBoxSaga.js
index 49f98664e5..e22015a74b 100644
--- a/packages/core/src/sagas/sendTypingOnSetSendBoxSaga.js
+++ b/packages/core/src/sagas/sendTypingOnSetSendBoxSaga.js
@@ -7,11 +7,14 @@ import {
takeLatest
} from 'redux-saga/effects';
-import whileConnected from './effects/whileConnected';
-
import { SET_SEND_BOX } from '../actions/setSendBox';
import { SET_SEND_TYPING } from '../actions/setSendTyping';
import postActivity from '../actions/postActivity';
+
+import whileConnected from './effects/whileConnected';
+
+import sendTypingSelector from '../selectors/sendTyping';
+
import sleep from '../utils/sleep';
const SEND_INTERVAL = 3000;
@@ -21,32 +24,38 @@ function takeSendTyping(value) {
}
export default function* () {
- yield whileConnected(function* () {
- const sendTyping = yield select(({ sendTyping }) => sendTyping);
-
- if (!sendTyping) {
- yield takeSendTyping(true);
- }
-
- for (;;) {
- let lastSend = 0;
- const task = yield takeLatest(SET_SEND_BOX, function* ({ payload: { text } }) {
- if (text) {
- const interval = SEND_INTERVAL - Date.now() + lastSend;
+ yield whileConnected(sendTypingOnSetSendBox);
+}
- if (interval > 0) {
- yield call(sleep, interval);
- }
+function* sendTypingOnSetSendBox() {
+ const sendTyping = yield select(sendTypingSelector);
+
+ if (!sendTyping) {
+ yield takeSendTyping(true);
+ }
+
+ for (;;) {
+ let lastSend = 0;
+ const task = yield takeLatest(
+ ({ payload, type }) => (
+ type === SET_SEND_BOX
+ && payload.text
+ ),
+ function* () {
+ const interval = SEND_INTERVAL - Date.now() + lastSend;
+
+ if (interval > 0) {
+ yield call(sleep, interval);
+ }
- yield put(postActivity({ type: 'typing' }));
+ yield put(postActivity({ type: 'typing' }));
- lastSend = Date.now();
- }
- });
+ lastSend = Date.now();
+ }
+ );
- yield takeSendTyping(false);
- yield cancel(task);
- yield takeSendTyping(true);
- }
- });
+ yield takeSendTyping(false);
+ yield cancel(task);
+ yield takeSendTyping(true);
+ }
}
diff --git a/packages/core/src/sagas/startDictateAfterSpeakActivitySaga.js b/packages/core/src/sagas/startDictateAfterSpeakActivitySaga.js
index 0aa39ba5e4..2800c66b7c 100644
--- a/packages/core/src/sagas/startDictateAfterSpeakActivitySaga.js
+++ b/packages/core/src/sagas/startDictateAfterSpeakActivitySaga.js
@@ -1,38 +1,50 @@
import {
- cancel,
- fork,
put,
select,
- take
+ takeEvery
} from 'redux-saga/effects';
-import whileConnected from './effects/whileConnected';
-
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* (_, userID) {
- for (;;) {
- yield take(START_SPEAKING_ACTIVITY);
+import speakingActivity from '../definitions/speakingActivity';
+
+import whileConnected from './effects/whileConnected';
+import whileSpeakIncomingActivity from './effects/whileSpeakIncomingActivity';
- const task = yield fork(startDictateAfterSpeakActivitySaga, userID);
+import { ofID as activitiesOfID } from '../selectors/activities';
- yield take(STOP_SPEAKING_ACTIVITY);
- yield cancel(task);
- }
+export default function* () {
+ yield whileConnected(function* () {
+ yield whileSpeakIncomingActivity(startDictateAfterSpeakActivity);
});
}
-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);
+function* startDictateAfterSpeakActivity() {
+ yield takeEvery(
+ ({ payload, type }) => (
+ type === MARK_ACTIVITY
+ && payload.name === 'speak'
+ && payload.value === false
+ ),
+ startDictateAfterAllActivitiesSpoken
+ );
+}
+
+function* startDictateAfterAllActivitiesSpoken({ payload: { activityID } }) {
+ const activities = yield select(activitiesOfID(activityID));
+ const [spokenActivity] = activities;
- if (!activities.some(activity => activity.id !== activityID && activity.channelData && activity.channelData.speak === true)) {
- yield put(startDictate());
- }
+ if (
+ spokenActivity
+ && spokenActivity.inputHint !== 'ignoringInput'
+ // Checks if there are no more activities that will be synthesis
+ && !activities.some(
+ activity => activity.id !== activityID && speakingActivity(activity)
+ )
+ ) {
+ // 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..075e1e9aef
--- /dev/null
+++ b/packages/core/src/sagas/startSpeakActivityOnPostActivitySaga.js
@@ -0,0 +1,26 @@
+import {
+ put,
+ takeEvery
+} from 'redux-saga/effects';
+
+import whileConnected from './effects/whileConnected';
+
+import { POST_ACTIVITY_PENDING } from '../actions/postActivity';
+import startSpeakingActivity from '../actions/startSpeakingActivity';
+
+export default function* () {
+ yield whileConnected(startSpeakActivityOnPostActivity);
+}
+
+function* startSpeakActivityOnPostActivity() {
+ yield takeEvery(
+ ({ meta, payload, type }) => (
+ type === POST_ACTIVITY_PENDING
+ && meta.method === 'speech'
+ && payload.activity.type === 'message'
+ ),
+ function* () {
+ yield put(startSpeakingActivity());
+ }
+ );
+}
diff --git a/packages/core/src/sagas/stopDictateOnCardAction.js b/packages/core/src/sagas/stopDictateOnCardAction.js
deleted file mode 100644
index 63ba7aab58..0000000000
--- a/packages/core/src/sagas/stopDictateOnCardAction.js
+++ /dev/null
@@ -1,27 +0,0 @@
-import {
- put,
- take
-} from 'redux-saga/effects';
-
-import whileConnected from './effects/whileConnected';
-
-import stopDictate from '../actions/stopDictate';
-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
-
- 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());
- }
- });
-}
diff --git a/packages/core/src/sagas/stopDictateOnCardActionSaga.js b/packages/core/src/sagas/stopDictateOnCardActionSaga.js
new file mode 100644
index 0000000000..8e8e90abe6
--- /dev/null
+++ b/packages/core/src/sagas/stopDictateOnCardActionSaga.js
@@ -0,0 +1,28 @@
+import {
+ put,
+ takeEvery
+} from 'redux-saga/effects';
+
+import whileConnected from './effects/whileConnected';
+
+import { POST_ACTIVITY_PENDING } from '../actions/postActivity';
+import stopDictate from '../actions/stopDictate';
+
+export default function* () {
+ yield whileConnected(stopDictateOnCardAction);
+}
+
+function* stopDictateOnCardAction() {
+ // 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 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
deleted file mode 100644
index 511c5a4db4..0000000000
--- a/packages/core/src/sagas/stopSpeakActivityOnInputSaga.js
+++ /dev/null
@@ -1,39 +0,0 @@
-import {
- put,
- select,
- take
-} 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';
-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')
-
- // 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')
- );
-
- yield put(stopSpeakingActivity());
-
- 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/stopSpeakingActivityOnInputSaga.js b/packages/core/src/sagas/stopSpeakingActivityOnInputSaga.js
new file mode 100644
index 0000000000..13c747a974
--- /dev/null
+++ b/packages/core/src/sagas/stopSpeakingActivityOnInputSaga.js
@@ -0,0 +1,41 @@
+import {
+ put,
+ takeEvery
+} from 'redux-saga/effects';
+
+import whileConnected from './effects/whileConnected';
+
+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(stopSpeakingActivityOnInput);
+}
+
+function* stopSpeakingActivityOnInput() {
+ yield takeEvery(
+ ({ 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
+ // We filter out speech because we will call startSpeakingActivity() for POST_ACTIVITY_PENDING dispatched by speech
+ || (
+ type === POST_ACTIVITY_PENDING
+ && meta.method !== '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 d57952a229..cf08e5df4f 100644
--- a/packages/core/src/sagas/submitSendBoxSaga.js
+++ b/packages/core/src/sagas/submitSendBoxSaga.js
@@ -1,25 +1,28 @@
import {
put,
select,
- take
+ takeEvery
} from 'redux-saga/effects';
-import whileConnected from './effects/whileConnected';
-
import { SUBMIT_SEND_BOX } from '../actions/submitSendBox';
import sendMessage from '../actions/sendMessage';
import setSendBox from '../actions/setSendBox';
+import whileConnected from './effects/whileConnected';
+
+import sendBoxValueSelector from '../selectors/sendBoxValue';
+
export default function* () {
- yield whileConnected(function* () {
- for (;;) {
- const { payload: { via } } = yield take(SUBMIT_SEND_BOX);
- const sendBoxValue = yield select(({ sendBoxValue }) => sendBoxValue);
+ yield whileConnected(submitSendBox);
+}
+
+function* submitSendBox() {
+ yield takeEvery(SUBMIT_SEND_BOX, function* ({ payload: { method } }) {
+ const sendBoxValue = yield select(sendBoxValueSelector);
- if (sendBoxValue) {
- yield put(sendMessage(sendBoxValue, via));
- yield put(setSendBox('', 'keyboard'));
- }
+ if (sendBoxValue) {
+ yield put(sendMessage(sendBoxValue, method));
+ yield put(setSendBox(''));
}
});
}
diff --git a/packages/core/src/selectors/activities.js b/packages/core/src/selectors/activities.js
new file mode 100644
index 0000000000..033507d02b
--- /dev/null
+++ b/packages/core/src/selectors/activities.js
@@ -0,0 +1,8 @@
+const activities = ({ activities }) => activities
+
+const of = predicate => state => activities(state).filter(predicate)
+const ofID = targetID => of(({ id }) => id === targetID);
+const ofType = targetType => of(({ type }) => type === targetType);
+
+export default activities
+export { of, ofID, ofType }
diff --git a/packages/core/src/selectors/language.js b/packages/core/src/selectors/language.js
new file mode 100644
index 0000000000..a0805cacab
--- /dev/null
+++ b/packages/core/src/selectors/language.js
@@ -0,0 +1 @@
+export default ({ language }) => language
diff --git a/packages/core/src/selectors/sendBoxValue.js b/packages/core/src/selectors/sendBoxValue.js
new file mode 100644
index 0000000000..1856b80846
--- /dev/null
+++ b/packages/core/src/selectors/sendBoxValue.js
@@ -0,0 +1 @@
+export default ({ sendBoxValue }) => sendBoxValue
diff --git a/packages/core/src/selectors/sendTimeout.js b/packages/core/src/selectors/sendTimeout.js
new file mode 100644
index 0000000000..12b4383623
--- /dev/null
+++ b/packages/core/src/selectors/sendTimeout.js
@@ -0,0 +1 @@
+export default ({ sendTimeout }) => sendTimeout
diff --git a/packages/core/src/selectors/sendTyping.js b/packages/core/src/selectors/sendTyping.js
new file mode 100644
index 0000000000..c90c9813cb
--- /dev/null
+++ b/packages/core/src/selectors/sendTyping.js
@@ -0,0 +1 @@
+export default ({ sendTyping }) => sendTyping
diff --git a/packages/playground/src/index.js b/packages/playground/src/index.js
index b7e7a62dea..4756f8148f 100644
--- a/packages/playground/src/index.js
+++ b/packages/playground/src/index.js
@@ -28,7 +28,14 @@ window.addEventListener('keydown', event => {
});
store = createStore(
- onErrorResumeNext(() => JSON.parse(window.sessionStorage.getItem(REDUX_STORE_KEY)))
+ onErrorResumeNext(() => JSON.parse(window.sessionStorage.getItem(REDUX_STORE_KEY))),
+ ({ dispatch }) => next => action => {
+ if (action.type === 'DIRECT_LINE/CONNECT_FULFILLED') {
+ dispatch({ type: 'WEB_CHAT/SEND_EVENT', payload: { name: 'webchat/join', value: { language: window.navigator.language } } });
+ }
+
+ return next(action);
+ }
);
store.subscribe(() => {
diff --git a/samples/05.c.presentation-mode-styling/index.html b/samples/05.c.presentation-mode-styling/index.html
index 81dc342ede..4c050797eb 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 convenience only. It 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..db77c6e263 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 convenience only. It 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 }),
@@ -101,9 +107,8 @@
store
}, document.getElementById('webchat'));
- // The following code is for convenient when trying out this sample, not required for production.
+ // The following code is for convenience only. It 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/15.d.backchannel-send-welcome-event/index.html b/samples/15.d.backchannel-send-welcome-event/index.html
new file mode 100644
index 0000000000..785644fef0
--- /dev/null
+++ b/samples/15.d.backchannel-send-welcome-event/index.html
@@ -0,0 +1,59 @@
+
+
+