Skip to content

Commit

Permalink
Moved Typing Indicator to SendBox (#2321)
Browse files Browse the repository at this point in the history
* Moved typing indicator to the send box.

* Added tests for typing indicator

* Updated CHANGELOG.md

* Fixed eslint issues

* Moved TypingIndicator above ConnectivityStatus

* Update CHANGELOG.md

* Update CHANGELOG.md

* Requested Changes

* Fixed offline ui test failure
  • Loading branch information
tdurnford authored and corinagum committed Aug 19, 2019
1 parent 011eae8 commit e4ca942
Show file tree
Hide file tree
Showing 22 changed files with 194 additions and 123 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- `component`: Remove [`react`](https://www.npmjs.com/package/react) and [`react-dom`](https://www.npmjs.com/package/react-dom) from `devDependencies`
- `playground`: Remove [`react`](https://www.npmjs.com/package/react) and [`react-dom`](https://www.npmjs.com/package/react-dom) from `dependencies`
- `samples/*`: Move to production version of Web Chat, and bump to [`[email protected]`](https://www.npmjs.com/package/react) and [`[email protected]`](https://www.npmjs.com/package/react-dom)
- Moved the typing indicator to the send box and removed the typing indicator logic from the sagas, by [@tdurnford](https://github.com/tdurnford), in PR [#2321](https://github.com/microsoft/BotFramework-WebChat/pull/2321)

### Fixed

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
46 changes: 40 additions & 6 deletions __tests__/sendTypingIndicator.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { By } from 'selenium-webdriver';

import { timeouts } from './constants.json';
import { imageSnapshotOptions, timeouts } from './constants.json';
import minNumActivitiesShown from './setup/conditions/minNumActivitiesShown';
import typingActivityReceived from './setup/conditions/typingActivityReceived';
import typingAnimationBackgroundImage from './setup/assets/typingIndicator';
import uiConnected from './setup/conditions/uiConnected';

// selenium-webdriver API doc:
// https://seleniumhq.github.io/selenium/docs/api/javascript/module/selenium-webdriver/index_exports_WebDriver.html
Expand All @@ -20,21 +23,52 @@ test('Send typing indicator', async () => {
await input.sendKeys('ABC');

// Typing indicator takes longer to come back
await driver.wait(minNumActivitiesShown(3), 5000);
await driver.wait(typingActivityReceived(), timeouts.directLine);
});

// TODO: [P3] Take this deprecation code out when releasing on or after January 13 2020
test('Send typing indicator using deprecated props', async () => {
const { driver, pageObjects } = await setupWebDriver({ props: { sendTyping: true } });
const { driver, pageObjects } = await setupWebDriver({
props: { sendTyping: true }
});

await pageObjects.sendMessageViaSendBox('echo-typing', { waitForSend: true });
await driver.wait(uiConnected(), timeouts.directLine);

await driver.wait(minNumActivitiesShown(2), timeouts.directLine);
await pageObjects.sendMessageViaSendBox('echo-typing', { waitForSend: true });

const input = await driver.findElement(By.css('input[type="text"]'));

await input.sendKeys('ABC');

// Typing indicator takes longer to come back
await driver.wait(minNumActivitiesShown(3), 5000);
await driver.wait(typingActivityReceived(), timeouts.directLine);
});

test('typing indicator should display in SendBox', async () => {
const { driver, pageObjects } = await setupWebDriver({ props: { styleOptions: { typingAnimationBackgroundImage } } });

await driver.wait(uiConnected(), timeouts.directLine);

await pageObjects.sendMessageViaSendBox('typing 1', { waitForSend: true });

// Typing indicator takes longer to come back
await driver.wait(typingActivityReceived(), timeouts.directLine);

const base64PNG = await driver.takeScreenshot();

expect(base64PNG).toMatchImageSnapshot(imageSnapshotOptions);
});

test('typing indicator should not display after second activity', async () => {
const { driver, pageObjects } = await setupWebDriver({
props: {
styleOptions: { typingAnimationBackgroundImage, typingAnimationDuration: 10000 }
}
});

await pageObjects.sendMessageViaSendBox('typing', { waitForSend: true });
await driver.wait(minNumActivitiesShown(2), 5000);

const base64PNG = await driver.takeScreenshot();
expect(base64PNG).toMatchImageSnapshot(imageSnapshotOptions);
});
1 change: 1 addition & 0 deletions __tests__/setup/assets/typingIndicator.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 23 additions & 0 deletions __tests__/setup/conditions/typingActivityReceived.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Condition } from 'selenium-webdriver';

export default function typingActivityReceived() {
return new Condition(
`Waiting for typing activity`,
async driver =>
await driver.executeScript(
() =>
~window.WebChatTest.actions
.filter(({ type }) => type === 'DIRECT_LINE/INCOMING_ACTIVITY')
.findIndex(
({
payload: {
activity: {
from: { role },
type
}
}
}) => role === 'bot' && type === 'typing'
)
)
);
}
1 change: 1 addition & 0 deletions packages/bundle/src/index-es5.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import 'core-js/modules/es.array.iterator';
import 'core-js/modules/es.math.sign';
import 'core-js/modules/es.number.is-finite';
import 'core-js/modules/es.object.assign';
import 'core-js/modules/es.object.values';
import 'core-js/modules/es.promise';
import 'core-js/modules/es.promise.finally';
import 'core-js/modules/es.string.starts-with';
Expand Down
37 changes: 13 additions & 24 deletions packages/component/src/Activity/StackedLayout.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,7 @@ const StackedLayout = ({ activity, avatarInitials, children, language, styleSet,
channelData: { messageBack: { displayText: messageBackDisplayText } = {}, state } = {},
from: { role } = {},
text,
textFormat,
type
textFormat
} = activity;

const activityDisplayText = messageBackDisplayText || text;
Expand Down Expand Up @@ -122,30 +121,20 @@ const StackedLayout = ({ activity, avatarInitials, children, language, styleSet,
)}
<Avatar aria-hidden={true} className="avatar" fromUser={fromUser} />
<div className="content">
{type === 'typing' ? (
<div className="webchat__row typing">
{children({
activity,
attachment: { contentType: 'typing' }
})}
{!!activityDisplayText && (
<div className="webchat__row message">
<ScreenReaderText text={ariaLabel} />
<Bubble aria-hidden={true} className="bubble" fromUser={fromUser} nub={true}>
{children({
activity,
attachment: {
content: activityDisplayText,
contentType: textFormatToContentType(textFormat)
}
})}
</Bubble>
<div className="filler" />
</div>
) : (
!!activityDisplayText && (
<div className="webchat__row message">
<ScreenReaderText text={ariaLabel} />
<Bubble aria-hidden={true} className="bubble" fromUser={fromUser} nub={true}>
{children({
activity,
attachment: {
content: activityDisplayText,
contentType: textFormatToContentType(textFormat)
}
})}
</Bubble>
<div className="filler" />
</div>
)
)}
{/* Because of differences in browser implementations, aria-label=" " is used to make the screen reader not repeat the same text multiple times in Chrome v75 */}
{attachments.map((attachment, index) => (
Expand Down
21 changes: 0 additions & 21 deletions packages/component/src/Attachment/TypingActivity.js

This file was deleted.

2 changes: 2 additions & 0 deletions packages/component/src/BasicSendBox.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import MicrophoneButton from './SendBox/MicrophoneButton';
import SendButton from './SendBox/SendButton';
import SuggestedActions from './SendBox/SuggestedActions';
import TextBox from './SendBox/TextBox';
import TypingIndicator from './SendBox/TypingIndicator';
import UploadButton from './SendBox/UploadButton';

const {
Expand All @@ -29,6 +30,7 @@ const TEXT_BOX_CSS = css({ flex: 10000 });

const BasicSendBox = ({ className, dictationStarted, styleSet, webSpeechPonyfill }) => (
<div className={classNames(styleSet.sendBox + '', ROOT_CSS + '', className + '')} role="form">
<TypingIndicator />
<ConnectivityStatus />
<SuggestedActions />
<div className="main">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import DownloadAttachment from '../../Attachment/DownloadAttachment';
import ImageAttachment from '../../Attachment/ImageAttachment';
import UploadAttachment from '../../Attachment/UploadAttachment';
import TextAttachment from '../../Attachment/TextAttachment';
import TypingActivity from '../../Attachment/TypingActivity';
import VideoAttachment from '../../Attachment/VideoAttachment';

function hasThumbnail({ attachments = [], channelData: { attachmentThumbnails = [] } = {} }, attachment) {
Expand All @@ -18,10 +17,13 @@ function hasThumbnail({ attachments = [], channelData: { attachmentThumbnails =
// TODO: [P4] Rename this file or the whole middleware, it looks either too simple or too comprehensive now
export default function createCoreMiddleware() {
return () => next => {
const Attachment = ({ activity = {}, attachment, attachment: { contentType, contentUrl } = {} }) =>
activity.type === 'typing' ? (
<TypingActivity />
) : activity.from.role === 'user' && !/^text\//u.test(contentType) && !hasThumbnail(activity, attachment) ? (
const Attachment = ({
activity = {},
activity: { from: { role } } = {},
attachment,
attachment: { contentType, contentUrl } = {}
}) =>
role === 'user' && !/^text\//u.test(contentType) && !hasThumbnail(activity, attachment) ? (
<UploadAttachment activity={activity} attachment={attachment} />
) : /^audio\//u.test(contentType) ? (
<AudioAttachment activity={activity} attachment={attachment} />
Expand Down
55 changes: 55 additions & 0 deletions packages/component/src/SendBox/TypingIndicator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import PropTypes from 'prop-types';
import React, { useEffect, useState } from 'react';

import { localize } from '../Localization/Localize';
import connectToWebChat from '../connectToWebChat';
import TypingAnimation from './Assets/TypingAnimation';

const TypingIndicator = ({
language,
lastTypingAt,
styleSet: {
options: { typingAnimationDuration },
typingIndicator
}
}) => {
const [showTyping, setShowTyping] = useState(false);

useEffect(() => {
let timeout;
const last = Math.max(Object.values(lastTypingAt));
const typingAnimationTimeRemaining = typingAnimationDuration - Date.now() + last;

if (last && typingAnimationTimeRemaining > 0) {
setShowTyping(true);
timeout = setTimeout(() => setShowTyping(false), typingAnimationTimeRemaining);
} else {
setShowTyping(false);
}

return () => clearTimeout(timeout);
}, [lastTypingAt, typingAnimationDuration]);

return (
showTyping && (
<div className={typingIndicator}>
<TypingAnimation aria-label={localize('TypingIndicator', language)} />
</div>
)
);
};

TypingIndicator.propTypes = {
language: PropTypes.string.isRequired,
lastTypingAt: PropTypes.any.isRequired,
styleSet: PropTypes.shape({
options: PropTypes.shape({
typingAnimationDuration: PropTypes.number
}).isRequired,
typingIndicator: PropTypes.any.isRequired
}).isRequired
};

export default connectToWebChat(({ lastTypingAt, language, styleSet }) => ({ lastTypingAt, language, styleSet }))(
TypingIndicator
);
8 changes: 0 additions & 8 deletions packages/component/src/Styles/StyleSet/TypingActivity.js

This file was deleted.

6 changes: 6 additions & 0 deletions packages/component/src/Styles/StyleSet/TypingIndicator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export default function createTypingIndicatorStyle({ paddingRegular }) {
return {
paddingBottom: paddingRegular,
paddingLeft: paddingRegular
};
}
4 changes: 2 additions & 2 deletions packages/component/src/Styles/createStyleSet.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ import createSuggestedActionsStyleSet from './StyleSet/SuggestedActionsStyleSet'
import createSuggestedActionStyle from './StyleSet/SuggestedAction';
import createTextContentStyle from './StyleSet/TextContent';
import createTimestampStyle from './StyleSet/Timestamp';
import createTypingActivityStyle from './StyleSet/TypingActivity';
import createTypingAnimationStyle from './StyleSet/TypingAnimation';
import createTypingIndicatorStyle from './StyleSet/TypingIndicator';
import createUploadAttachmentStyle from './StyleSet/UploadAttachment';
import createUploadButtonStyle from './StyleSet/UploadButton';
import createVideoAttachmentStyle from './StyleSet/VideoAttachment';
Expand Down Expand Up @@ -142,8 +142,8 @@ export default function createStyleSet(options) {
suggestedActions: createSuggestedActionsStyle(options),
textContent: createTextContentStyle(options),
timestamp: createTimestampStyle(options),
typingActivity: createTypingActivityStyle(options),
typingAnimation: createTypingAnimationStyle(options),
typingIndicator: createTypingIndicatorStyle(options),
uploadAttachment: createUploadAttachmentStyle(options),
uploadButton: createUploadButtonStyle(options),
videoAttachment: createVideoAttachmentStyle(options),
Expand Down
1 change: 1 addition & 0 deletions packages/component/src/Styles/defaultStyleOptions.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ const DEFAULT_OPTIONS = {
notificationText: '#5E5E5E',

typingAnimationBackgroundImage: null,
typingAnimationDuration: 5000,
typingAnimationHeight: 20,
typingAnimationWidth: 64,

Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import connectivityStatus from './reducers/connectivityStatus';
import dictateInterims from './reducers/dictateInterims';
import dictateState from './reducers/dictateState';
import language from './reducers/language';
import lastTypingAt from './reducers/lastTypingAt';
import readyState from './reducers/readyState';
import referenceGrammarID from './reducers/referenceGrammarID';
import sendBoxValue from './reducers/sendBoxValue';
Expand All @@ -21,6 +22,7 @@ export default combineReducers({
dictateInterims,
dictateState,
language,
lastTypingAt,
readyState,
referenceGrammarID,
sendBoxValue,
Expand Down
Loading

0 comments on commit e4ca942

Please sign in to comment.