diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6b232faa7d..ea94a232cb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -47,12 +47,16 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- PR [#2547](https://github.com/microsoft/BotFramework-WebChat/pull/2547): `useEmitTypingIndicator`, `usePeformCardAction`, `usePostActivity`, `useSendEvent`, `useSendFiles`, `useSendMessage`, `useSendMessageBack`, `useSendPostBack`
- PR [#2548](https://github.com/microsoft/BotFramework-WebChat/pull/2548): `useDisabled`
- PR [#2549](https://github.com/microsoft/BotFramework-WebChat/pull/2549): `useSuggestedActions`
+ - PR [#2550](https://github.com/microsoft/BotFramework-WebChat/pull/2550): `useConnectivityStatus`, `useGroupTimestamp`, `useTimeoutForSend`, `useUserID`, `useUsername`
- PR [#2551](https://github.com/microsoft/BotFramework-WebChat/pull/2551): `useLastTypingAt`, `useSendTypingIndicator`, `useTypingIndicator`
- PR [#2552](https://github.com/microsoft/BotFramework-WebChat/pull/2552): `useFocusSendBox`, `useScrollToEnd`, `useSendBoxValue`, `useSubmitSendBox`, `useTextBoxSubmit`, `useTextBoxValue`
- Bring your own Adaptive Cards package by specifying `adaptiveCardsPackage` prop, by [@compulim](https://github.com/compulim) in PR [#2543](https://github.com/microsoft/BotFramework-WebChat/pull/2543)
- Fixes [#2597](https://github.com/microsoft/BotFramework-WebChat/issues/2597). Modify `watch` script to `start` and add `tableflip` script for throwing `node_modules`, by [@corinagum](https://github.com/corinagum) in PR [#2598](https://github.com/microsoft/BotFramework-WebChat/pull/2598)
- `component`: Fixes [#2331](https://github.com/microsoft/BotFramework-WebChat/issues/2331). Updated timer to use React Hooks, by [@spyip](https://github.com/spyip) in PR [#2546](https://github.com/microsoft/BotFramework-WebChat/pull/2546)
-- Resolves [#2620](https://github.com/Microsoft/BotFramework-WebChat/issues/2620), update Chinese localization files, by [@spyip](https://github.com/spyip) in PR [#2631](https://github.com/microsoft/BotFramework-WebChat/pull/2631)
+- Resolves [#2597](https://github.com/microsoft/BotFramework-WebChat/issues/2597). Modify `watch` script to `start` and add `tableflip` script for throwing `node_modules`, by [@corinagum](https://github.com/corinagum) in PR [#2598](https://github.com/microsoft/BotFramework-WebChat/pull/2598)
+- Adds `suggestedActionLayout` to `defaultStyleOptions`, by [@spyip](https://github.com/spyip), in PR [#2596](https://github.com/microsoft/BotFramework-WebChat/pull/2596)
+- Resolves [#2331](https://github.com/microsoft/BotFramework-WebChat/issues/2331). Updated timer to use React Hooks, by [@spyip](https://github.com/spyip) in PR [#2546](https://github.com/microsoft/BotFramework-WebChat/pull/2546)
+- Resolves [#2620](https://github.com/microsoft/BotFramework-WebChat/issues/2620), update Chinese localization files, by [@spyip](https://github.com/spyip) in PR [#2631](https://github.com/microsoft/BotFramework-WebChat/pull/2631)
### Changed
diff --git a/__tests__/__image_snapshots__/chrome-docker/suggested-actions-js-suggested-actions-command-should-show-correctly-formatted-buttons-when-suggested-actions-are-displayed-as-stacked-1-snap.png b/__tests__/__image_snapshots__/chrome-docker/suggested-actions-js-suggested-actions-command-should-show-correctly-formatted-buttons-when-suggested-actions-are-displayed-as-stacked-1-snap.png
new file mode 100644
index 0000000000..a8f7ed476f
Binary files /dev/null and b/__tests__/__image_snapshots__/chrome-docker/suggested-actions-js-suggested-actions-command-should-show-correctly-formatted-buttons-when-suggested-actions-are-displayed-as-stacked-1-snap.png differ
diff --git a/__tests__/__image_snapshots__/chrome-docker/suggested-actions-js-suggested-actions-command-should-show-suggested-actions-with-larger-images-as-stacked-1-snap.png b/__tests__/__image_snapshots__/chrome-docker/suggested-actions-js-suggested-actions-command-should-show-suggested-actions-with-larger-images-as-stacked-1-snap.png
new file mode 100644
index 0000000000..fd91da858b
Binary files /dev/null and b/__tests__/__image_snapshots__/chrome-docker/suggested-actions-js-suggested-actions-command-should-show-suggested-actions-with-larger-images-as-stacked-1-snap.png differ
diff --git a/__tests__/hooks/useConnectivityStatus.js b/__tests__/hooks/useConnectivityStatus.js
new file mode 100644
index 0000000000..11ec3f4c9e
--- /dev/null
+++ b/__tests__/hooks/useConnectivityStatus.js
@@ -0,0 +1,24 @@
+import { timeouts } from '../constants.json';
+import uiConnected from '../setup/conditions/uiConnected';
+
+// selenium-webdriver API doc:
+// https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/index_exports_WebDriver.html
+
+jest.setTimeout(timeouts.test);
+
+test('getter should return online', async () => {
+ const { driver, pageObjects } = await setupWebDriver();
+
+ await driver.wait(uiConnected(), timeouts.directLine);
+
+ const [connectivityStatus] = await pageObjects.runHook('useConnectivityStatus');
+
+ expect(connectivityStatus).toMatchInlineSnapshot(`"connected"`);
+});
+
+test('setter should be falsy', async () => {
+ const { pageObjects } = await setupWebDriver();
+ const [_, setConnectivityStatus] = await pageObjects.runHook('useConnectivityStatus');
+
+ expect(setConnectivityStatus).toBeFalsy();
+});
diff --git a/__tests__/hooks/useGroupTimestamp.js b/__tests__/hooks/useGroupTimestamp.js
new file mode 100644
index 0000000000..823e238d4c
--- /dev/null
+++ b/__tests__/hooks/useGroupTimestamp.js
@@ -0,0 +1,43 @@
+import { timeouts } from '../constants.json';
+
+// selenium-webdriver API doc:
+// https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/index_exports_WebDriver.html
+
+jest.setTimeout(timeouts.test);
+
+test('getter should return group timestamp set in props', async () => {
+ const { pageObjects } = await setupWebDriver({
+ props: {
+ groupTimestamp: 1000
+ }
+ });
+
+ const [groupTimestamp] = await pageObjects.runHook('useGroupTimestamp');
+
+ expect(groupTimestamp).toMatchInlineSnapshot(`1000`);
+});
+
+test('getter should return default group timestamp if not set in props', async () => {
+ const { pageObjects } = await setupWebDriver();
+
+ const [groupTimestamp] = await pageObjects.runHook('useGroupTimestamp');
+
+ expect(groupTimestamp).toMatchInlineSnapshot(`true`);
+});
+
+test('getter should return false if group timestamp is disabled', async () => {
+ const { pageObjects } = await setupWebDriver({
+ props: { groupTimestamp: false }
+ });
+
+ const [groupTimestamp] = await pageObjects.runHook('useGroupTimestamp');
+
+ expect(groupTimestamp).toMatchInlineSnapshot(`false`);
+});
+
+test('setter should be falsy', async () => {
+ const { pageObjects } = await setupWebDriver();
+ const [_, setGroupTimestamp] = await pageObjects.runHook('useGroupTimestamp');
+
+ expect(setGroupTimestamp).toBeFalsy();
+});
diff --git a/__tests__/hooks/useTimeoutForSend.js b/__tests__/hooks/useTimeoutForSend.js
new file mode 100644
index 0000000000..25b7a22d4c
--- /dev/null
+++ b/__tests__/hooks/useTimeoutForSend.js
@@ -0,0 +1,36 @@
+import { timeouts } from '../constants.json';
+
+// selenium-webdriver API doc:
+// https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/index_exports_WebDriver.html
+
+jest.setTimeout(timeouts.test);
+
+test('getter should return timeout for sending activity', async () => {
+ const { pageObjects } = await setupWebDriver({
+ props: {
+ sendTimeout: 1000
+ }
+ });
+
+ const [timeoutForSend] = await pageObjects.runHook('useTimeoutForSend');
+
+ expect(timeoutForSend).toMatchInlineSnapshot(`1000`);
+});
+
+test('getter should return default timeout for sending activity if not set in props', async () => {
+ const { pageObjects } = await setupWebDriver();
+
+ const [timeoutForSend] = await pageObjects.runHook('useTimeoutForSend');
+
+ expect(timeoutForSend).toMatchInlineSnapshot(`20000`);
+});
+
+test('setter should set the timeout for sending activity', async () => {
+ const { pageObjects } = await setupWebDriver();
+
+ await pageObjects.runHook('useTimeoutForSend', [], result => result[1](1000));
+
+ const [timeoutForSend] = await pageObjects.runHook('useTimeoutForSend');
+
+ expect(timeoutForSend).toMatchInlineSnapshot(`1000`);
+});
diff --git a/__tests__/hooks/useUserID.js b/__tests__/hooks/useUserID.js
new file mode 100644
index 0000000000..ec0e521036
--- /dev/null
+++ b/__tests__/hooks/useUserID.js
@@ -0,0 +1,33 @@
+import { timeouts } from '../constants.json';
+
+// selenium-webdriver API doc:
+// https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/index_exports_WebDriver.html
+
+jest.setTimeout(timeouts.test);
+
+test('getter should return user ID set in props', async () => {
+ const { pageObjects } = await setupWebDriver({
+ props: {
+ userID: 'u-12345'
+ }
+ });
+
+ const [userID] = await pageObjects.runHook('useUserID');
+
+ expect(userID).toMatchInlineSnapshot(`"u-12345"`);
+});
+
+test('getter should return empty string if not set in props', async () => {
+ const { pageObjects } = await setupWebDriver();
+
+ const [userID] = await pageObjects.runHook('useUserID');
+
+ expect(userID).toMatchInlineSnapshot(`""`);
+});
+
+test('setter should be falsy', async () => {
+ const { pageObjects } = await setupWebDriver();
+ const [_, setUserID] = await pageObjects.runHook('useUserID');
+
+ expect(setUserID).toBeFalsy();
+});
diff --git a/__tests__/hooks/useUsername.js b/__tests__/hooks/useUsername.js
new file mode 100644
index 0000000000..0cde8b289e
--- /dev/null
+++ b/__tests__/hooks/useUsername.js
@@ -0,0 +1,33 @@
+import { timeouts } from '../constants.json';
+
+// selenium-webdriver API doc:
+// https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/index_exports_WebDriver.html
+
+jest.setTimeout(timeouts.test);
+
+test('getter should return username set in props', async () => {
+ const { pageObjects } = await setupWebDriver({
+ props: {
+ username: 'u-12345'
+ }
+ });
+
+ const [username] = await pageObjects.runHook('useUsername');
+
+ expect(username).toMatchInlineSnapshot(`"u-12345"`);
+});
+
+test('getter should return undefined if not set in props', async () => {
+ const { pageObjects } = await setupWebDriver();
+
+ const [username] = await pageObjects.runHook('useUsername');
+
+ expect(username).toMatchInlineSnapshot(`"Happy Web Chat user"`);
+});
+
+test('setter should be falsy', async () => {
+ const { pageObjects } = await setupWebDriver();
+ const [_, setUsername] = await pageObjects.runHook('useUsername');
+
+ expect(setUsername).toBeFalsy();
+});
diff --git a/__tests__/setup/conditions/suggestedActionsShown.js b/__tests__/setup/conditions/suggestedActionsShown.js
index 7255e12d23..c3090ab740 100644
--- a/__tests__/setup/conditions/suggestedActionsShown.js
+++ b/__tests__/setup/conditions/suggestedActionsShown.js
@@ -1,5 +1,5 @@
import { By, until } from 'selenium-webdriver';
export default function suggestedActionsShown() {
- return until.elementLocated(By.css('[role="form"] ul'));
+ return until.elementLocated(By.css('[role="form"] > :not(.main) button'));
}
diff --git a/__tests__/suggestedActions.js b/__tests__/suggestedActions.js
index 14b1fd5a9e..3b67b9fb89 100644
--- a/__tests__/suggestedActions.js
+++ b/__tests__/suggestedActions.js
@@ -28,6 +28,22 @@ describe('suggested-actions command', () => {
expect(base64PNG).toMatchImageSnapshot(imageSnapshotOptions);
});
+ test('should show correctly formatted buttons when suggested actions are displayed as stacked', async () => {
+ const { driver, pageObjects } = await setupWebDriver({
+ props: { styleOptions: { suggestedActionLayout: 'stacked' } }
+ });
+
+ await driver.wait(uiConnected(), timeouts.directLine);
+ await pageObjects.sendMessageViaSendBox('suggested-actions', { waitForSend: true });
+
+ await driver.wait(suggestedActionsShown(), timeouts.directLine);
+ await driver.wait(allImagesLoaded(), 2000);
+
+ const base64PNG = await driver.takeScreenshot();
+
+ expect(base64PNG).toMatchImageSnapshot(imageSnapshotOptions);
+ });
+
test('should show response from bot and no text from user on imback', async () => {
const { driver, pageObjects } = await setupWebDriver();
@@ -174,4 +190,23 @@ describe('suggested-actions command', () => {
expect(base64PNG).toMatchImageSnapshot(imageSnapshotOptions);
});
+
+ test('should show suggested actions with larger images as stacked', async () => {
+ const styleOptions = {
+ suggestedActionHeight: 80,
+ suggestedActionImageHeight: 60,
+ suggestedActionLayout: 'stacked'
+ };
+ const { driver, pageObjects } = await setupWebDriver({ props: { styleOptions } });
+
+ await driver.wait(uiConnected(), timeouts.directLine);
+ await pageObjects.sendMessageViaSendBox('emptycard', { waitForSend: true });
+
+ await driver.wait(suggestedActionsShown(), timeouts.directLine);
+ await driver.wait(allImagesLoaded(), 2000);
+
+ const base64PNG = await driver.takeScreenshot();
+
+ expect(base64PNG).toMatchImageSnapshot(imageSnapshotOptions);
+ });
});
diff --git a/packages/component/src/BasicTranscript.js b/packages/component/src/BasicTranscript.js
index b2416204bb..8d6ed31acd 100644
--- a/packages/component/src/BasicTranscript.js
+++ b/packages/component/src/BasicTranscript.js
@@ -9,6 +9,7 @@ import connectToWebChat from './connectToWebChat';
import ScrollToEndButton from './Activity/ScrollToEndButton';
import SpeakActivity from './Activity/Speak';
import useActivities from './hooks/useActivities';
+import useGroupTimestamp from './hooks/useGroupTimestamp';
import useStyleOptions from './hooks/useStyleOptions';
import useStyleSet from './hooks/useStyleSet';
@@ -59,10 +60,11 @@ function sameTimestampGroup(activityX, activityY, groupTimestamp) {
return false;
}
-const BasicTranscript = ({ activityRenderer, attachmentRenderer, className, groupTimestamp, webSpeechPonyfill }) => {
- const [activities] = useActivities();
+const BasicTranscript = ({ activityRenderer, attachmentRenderer, className, webSpeechPonyfill }) => {
const [{ activities: activitiesStyleSet, activity: activityStyleSet }] = useStyleSet();
const [{ hideScrollToEndButton }] = useStyleOptions();
+ const [activities] = useActivities();
+ const [groupTimestamp] = useGroupTimestamp();
const { speechSynthesis, SpeechSynthesisUtterance } = webSpeechPonyfill || {};
@@ -131,7 +133,6 @@ const BasicTranscript = ({ activityRenderer, attachmentRenderer, className, grou
BasicTranscript.defaultProps = {
className: '',
- groupTimestamp: true,
webSpeechPonyfill: undefined
};
@@ -139,16 +140,14 @@ BasicTranscript.propTypes = {
activityRenderer: PropTypes.func.isRequired,
attachmentRenderer: PropTypes.func.isRequired,
className: PropTypes.string,
- groupTimestamp: PropTypes.oneOfType([PropTypes.bool.isRequired, PropTypes.number.isRequired]),
webSpeechPonyfill: PropTypes.shape({
speechSynthesis: PropTypes.any,
SpeechSynthesisUtterance: PropTypes.any
})
};
-export default connectToWebChat(({ activityRenderer, attachmentRenderer, groupTimestamp, webSpeechPonyfill }) => ({
+export default connectToWebChat(({ activityRenderer, attachmentRenderer, webSpeechPonyfill }) => ({
activityRenderer,
attachmentRenderer,
- groupTimestamp,
webSpeechPonyfill
}))(BasicTranscript);
diff --git a/packages/component/src/SendBox/ConnectivityStatus.js b/packages/component/src/SendBox/ConnectivityStatus.js
index 1b95cddc15..9af3d6c882 100644
--- a/packages/component/src/SendBox/ConnectivityStatus.js
+++ b/packages/component/src/SendBox/ConnectivityStatus.js
@@ -5,6 +5,7 @@ import connectToWebChat from '../connectToWebChat';
import ErrorNotificationIcon from '../Attachment/Assets/ErrorNotificationIcon';
import ScreenReaderText from '../ScreenReaderText';
import SpinnerAnimation from '../Attachment/Assets/SpinnerAnimation';
+import useConnectivityStatus from '../hooks/useConnectivityStatus';
import useLocalize from '../hooks/useLocalize';
import useStyleSet from '../hooks/useStyleSet';
import WarningNotificationIcon from '../Attachment/Assets/WarningNotificationIcon';
@@ -51,7 +52,8 @@ DebouncedConnectivityStatus.propTypes = {
const connectConnectivityStatus = (...selectors) =>
connectToWebChat(({ connectivityStatus, language }) => ({ connectivityStatus, language }), ...selectors);
-const ConnectivityStatus = ({ connectivityStatus }) => {
+const ConnectivityStatus = () => {
+ const [connectivityStatus] = useConnectivityStatus();
const [
{
connectivityNotification: connectivityNotificationStyleSet,
@@ -175,8 +177,6 @@ const ConnectivityStatus = ({ connectivityStatus }) => {
);
};
-ConnectivityStatus.propTypes = {
- connectivityStatus: PropTypes.string.isRequired
-};
+export default ConnectivityStatus;
-export default connectConnectivityStatus()(ConnectivityStatus);
+export { connectConnectivityStatus };
diff --git a/packages/component/src/SendBox/SuggestedAction.js b/packages/component/src/SendBox/SuggestedAction.js
index 4e0d42f700..259b705d54 100644
--- a/packages/component/src/SendBox/SuggestedAction.js
+++ b/packages/component/src/SendBox/SuggestedAction.js
@@ -11,11 +11,13 @@ import useStyleSet from '../hooks/useStyleSet';
import useSuggestedActions from '../hooks/useSuggestedActions';
const SUGGESTED_ACTION_CSS = css({
- display: 'inline-block',
+ display: 'flex',
+ flexDirection: 'column',
whiteSpace: 'initial',
'& > button': {
- display: 'flex'
+ display: 'flex',
+ overflow: 'hidden'
}
});
diff --git a/packages/component/src/SendBox/SuggestedActions.js b/packages/component/src/SendBox/SuggestedActions.js
index 5a2f82c5c4..3138fcc5b2 100644
--- a/packages/component/src/SendBox/SuggestedActions.js
+++ b/packages/component/src/SendBox/SuggestedActions.js
@@ -1,5 +1,6 @@
/* eslint react/no-array-index-key: "off" */
+import { css } from 'glamor';
import BasicFilm from 'react-film';
import classNames from 'classnames';
import PropTypes from 'prop-types';
@@ -12,6 +13,11 @@ import useLocalize from '../hooks/useLocalize';
import useStyleOptions from '../hooks/useStyleOptions';
import useStyleSet from '../hooks/useStyleSet';
+const SUGGESTED_ACTION_STACKED_CSS = css({
+ display: 'flex',
+ flexDirection: 'column'
+});
+
function suggestedActionText({ displayText, title, type, value }) {
if (type === 'messageBack') {
return title || displayText;
@@ -35,37 +41,52 @@ const connectSuggestedActions = (...selectors) =>
const SuggestedActions = ({ className, suggestedActions = [] }) => {
const [{ suggestedActions: suggestedActionsStyleSet }] = useStyleSet();
- const [{ suggestedActionsStyleSet: suggestedActionsStyleSetForReactFilm }] = useStyleOptions();
+ const [{ suggestedActionLayout, suggestedActionsStyleSet: suggestedActionsStyleSetForReactFilm }] = useStyleOptions();
const suggestedActionsContentText = useLocalize('SuggestedActionsContent');
const suggestedActionsEmptyText = useLocalize('SuggestedActionsEmpty');
const suggestedActionsContainerText =
useLocalize('SuggestedActionsContainer') +
(suggestedActions.length ? suggestedActionsContentText : suggestedActionsEmptyText);
+ if (!suggestedActions.length) {
+ return false;
+ }
+
+ const children = suggestedActions.map(({ displayText, image, text, title, type, value }, index) => (
+