Skip to content

Commit

Permalink
Customizing typing indicator (#2912)
Browse files Browse the repository at this point in the history
* Add activeTyping

* Fix send typing momentarily not showing after post

* Add sample

* Doc

* Fix build break

* Doc

* Format

* Add entry

* Add GitHub CDN

* Fix tests

* Add tests and doc

* Add tests

* Remove dead code

* Use useLocalizer

* Format

* Apply suggestions from code review

Co-Authored-By: TJ Durnford <[email protected]>

* Remove deprecated code

Co-authored-by: TJ Durnford <[email protected]>
  • Loading branch information
compulim and tdurnford authored Feb 20, 2020
1 parent e75021f commit 2172870
Show file tree
Hide file tree
Showing 31 changed files with 730 additions and 65 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- If the new strings are undesirable, please use the [`overideLocalizedStrings` prop](https://github.com/microsoft/BotFramework-WebChat/tree/master/docs/LOCALIZATION.md#overriding-localization-strings) for customization
- String IDs have been refreshed and now use a standard format
- `useLocalize` and `useLocalizeDate` is deprecated. Please use `useLocalizer` and `useDateFormatter` instead
- Customizable typing indicator: data and hook related to typing indicator are being revamped in PR [#2912](https://github.com/microsoft/BotFramework-WebChat/pull/2912)
- `lastTypingAt` reducer is deprecated, use `typing` instead. The newer reducer contains typing indicator from the user
- `useLastTypingAt()` hook is deprecated, use `useActiveTyping(duration?: number)` instead. For all typing information, pass `Infinity` to `duration` argument

### Added

Expand All @@ -45,6 +48,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- Resolves [#2756](https://github.com/microsoft/BotFramework-WebChat/issues/2756). Improved localizability and add override support for localized strings, by [@compulim](https://github.com/compulim) in PR [#2894](https://github.com/microsoft/BotFramework-WebChat/pull/2894)
- Will be translated into 44 languages, plus 2 community-contributed translations
- For details, please read the [documentation on the localization](https://github.com/microsoft/BotFramework-WebChat/tree/master/docs/LOCALIZATION.md)
- Resolves [#2213](https://github.com/microsoft/BotFramework-WebChat/issues/2213). Added customization for typing activity, by [@compulim](https://github.com/compulim), in PR [#2912](https://github.com/microsoft/BotFramework-WebChat/pull/2912)

### Fixed

Expand Down Expand Up @@ -137,6 +141,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
- Bump samples to Web Chat 4.7.0, by [@compulim](https://github.com/compulim) in PR [#2726](https://github.com/microsoft/BotFramework-WebChat/issues/2726)
- Resolves [#2641](https://github.com/microsoft/BotFramework-WebChat/issues/2641). Reorganize Web Chat samples, by [@corinagum](https://github.com/corinagum), in PR [#2762](https://github.com/microsoft/BotFramework-WebChat/pull/2762)
- Resolves [#2755](https://github.com/microsoft/BotFramework-WebChat/issues/2755), added "how to use notification and customize the toast UI" sample, by [@compulim](https://github.com/compulim), in PR [#2883](https://github.com/microsoft/BotFramework-WebChat/pull/2883)
- Resolves [#2213](https://github.com/microsoft/BotFramework-WebChat/issues/2213). Added [Customize Typing Indicator Demo](https://microsoft.github.io/BotFramework-WebChat/05.custom-components/j.typing-indicator), by [@compulim](https://github.com/compulim), in PR [#2912](https://github.com/microsoft/BotFramework-WebChat/pull/2912)

## [4.7.1] - 2019-12-13

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.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
150 changes: 150 additions & 0 deletions __tests__/hooks/useActiveTyping.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import { Key } from 'selenium-webdriver';

import { timeouts } from '../constants.json';

import minNumActivitiesShown from '../setup/conditions/minNumActivitiesShown';
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 represent bot and user typing respectively', async () => {
const { driver, pageObjects } = await setupWebDriver({
props: { sendTypingIndicator: true },
setup: () =>
Promise.all([
window.WebChatTest.loadScript('https://unpkg.com/[email protected]/client/core.min.js'),
window.WebChatTest.loadScript('https://unpkg.com/[email protected]/lolex.js')
]).then(() => {
window.WebChatTest.clock = lolex.install();
})
});

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

let activeTyping;

await expect(pageObjects.runHook('useActiveTyping')).resolves.toEqual([{}]);

await pageObjects.typeOnSendBox('typing 1');

activeTyping = await pageObjects.runHook('useActiveTyping');

expect(Object.values(activeTyping[0])).toEqual([
{
at: 0,
expireAt: 5000,
name: 'Happy Web Chat user',
role: 'user'
}
]);

await pageObjects.typeOnSendBox(Key.ENTER);
await driver.wait(minNumActivitiesShown(2), timeouts.directLine);

activeTyping = await pageObjects.runHook('useActiveTyping');

expect(Object.values(activeTyping[0])).toEqual([
{
at: 0,
expireAt: 5000,
name: 'bot',
role: 'bot'
}
]);

await pageObjects.typeOnSendBox('.');

activeTyping = await pageObjects.runHook('useActiveTyping');

expect(Object.values(activeTyping[0]).sort(({ role: x }, { role: y }) => x - y)).toEqual([
{
at: 0,
expireAt: 5000,
name: 'bot',
role: 'bot'
},
{
at: 0,
expireAt: 5000,
name: 'Happy Web Chat user',
role: 'user'
}
]);
});

test('getter should filter out inactive typing', async () => {
const { driver, pageObjects } = await setupWebDriver({
props: { sendTypingIndicator: true },
setup: () =>
Promise.all([
window.WebChatTest.loadScript('https://unpkg.com/[email protected]/client/core.min.js'),
window.WebChatTest.loadScript('https://unpkg.com/[email protected]/lolex.js')
]).then(() => {
window.WebChatTest.clock = lolex.install();
})
});

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

let activeTyping;

await expect(pageObjects.runHook('useActiveTyping')).resolves.toEqual([{}]);

await pageObjects.typeOnSendBox('Hello, World!');

activeTyping = await pageObjects.runHook('useActiveTyping');

expect(Object.values(activeTyping[0])).toEqual([
{
at: 0,
expireAt: 5000,
name: 'Happy Web Chat user',
role: 'user'
}
]);

// We need to wait for 6000 ms because:
// 1. t=0: Typed letter "H"
// 2. t=0: Send typing activity
// 3. t=10: Typed letter "a"
// 4. t=10: Scheduled another typing indicator at t=3000
// 5. t=3000: Send typing activity
await driver.executeScript(() => window.WebChatTest.clock.tick(3000));

activeTyping = await pageObjects.runHook('useActiveTyping');

expect(Object.values(activeTyping[0])).toEqual([
{
at: 3000,
expireAt: 8000,
name: 'Happy Web Chat user',
role: 'user'
}
]);

await driver.executeScript(() => window.WebChatTest.clock.tick(8000));

await expect(pageObjects.runHook('useActiveTyping')).resolves.toEqual([{}]);

// Even it is filtered out, when setting a longer expiration, it should come back.
activeTyping = await pageObjects.runHook('useActiveTyping', [10000]);

expect(Object.values(activeTyping[0])).toEqual([
{
at: 3000,
expireAt: 13000,
name: 'Happy Web Chat user',
role: 'user'
}
]);
});

test('setter should be falsy', async () => {
const { pageObjects } = await setupWebDriver();
const [_, setActiveTyping] = await pageObjects.runHook('useActiveTyping');

expect(setActiveTyping).toBeFalsy();
});
30 changes: 30 additions & 0 deletions __tests__/sendTypingIndicator.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ import { By } from 'selenium-webdriver';

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

// selenium-webdriver API doc:
Expand Down Expand Up @@ -54,3 +56,31 @@ test('typing indicator should not display after second activity', async () => {
const base64PNG = await driver.takeScreenshot();
expect(base64PNG).toMatchImageSnapshot(imageSnapshotOptions);
});

test('changing typing indicator duration on-the-fly', async () => {
const { driver, pageObjects } = await setupWebDriver({
props: {
styleOptions: { typingAnimationBackgroundImage, typingAnimationDuration: 1000 }
}
});

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

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

expect(await driver.takeScreenshot()).toMatchImageSnapshot(imageSnapshotOptions);

await driver.wait(negationOf(typingIndicatorShown()), 2000);

expect(await driver.takeScreenshot()).toMatchImageSnapshot(imageSnapshotOptions);

await pageObjects.updateProps({
styleOptions: { typingAnimationBackgroundImage, typingAnimationDuration: 5000 }
});

await driver.wait(typingIndicatorShown(), timeouts.ui);

expect(await driver.takeScreenshot()).toMatchImageSnapshot(imageSnapshotOptions);
});
5 changes: 5 additions & 0 deletions __tests__/setup/conditions/typingIndicatorShown.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { By, until } from 'selenium-webdriver';

export default function typingIndicatorShown() {
return until.elementLocated(By.css('.webchat__typingIndicator'));
}
70 changes: 67 additions & 3 deletions docs/HOOKS.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ setSendBoxValue('Hello, World!');

Following is the list of hooks supported by Web Chat API.

- [`useActiveTyping`](#useactivetyping)
- [`useActivities`](#useactivities)
- [`useAdaptiveCardsHostConfig`](#useadaptivecardshostconfig)
- [`useAdaptiveCardsPackage`](#useadaptivecardspackage)
Expand All @@ -69,6 +70,7 @@ Following is the list of hooks supported by Web Chat API.
- [`useLastTypingAt`](#uselasttypingat)
- [`useLocalize`](#uselocalize) (Deprecated)
- [`useLocalizer`](#useLocalizer)
- [`useLastTypingAt`](#uselasttypingat) (Deprecated)
- [`useMarkActivityAsSpoken`](#usemarkactivityasspoken)
- [`useNotification`](#usenotification)
- [`usePerformCardAction`](#useperformcardaction)
Expand All @@ -79,6 +81,8 @@ Following is the list of hooks supported by Web Chat API.
- [`useRenderActivityStatus`](#userenderactivitystatus)
- [`useRenderAttachment`](#userenderattachment)
- [`useRenderMarkdownAsHTML`](#userendermarkdownashtml)
- [`useRenderToast`](#userendertoast)
- [`useRenderTypingIndicator`](#userendertypingindicator)
- [`useScrollToEnd`](#usescrolltoend)
- [`useSendBoxValue`](#usesendboxvalue)
- [`useSendEvent`](#usesendevent)
Expand All @@ -102,6 +106,27 @@ Following is the list of hooks supported by Web Chat API.
- [`useVoiceSelector`](#usevoiceselector)
- [`useWebSpeechPonyfill`](#usewebspeechponyfill)

## `useActiveTyping`

```js
interface Typing {
at: number;
expireAt: number;
name: string;
role: 'bot' | 'user';
}

useActiveTyping(expireAfter?: number): [{ [id: string]: Typing }]
```

This function will return a list of participants who are actively typing, including the start typing time (`at`) and expiration time (`expireAt`), the name and the role of the participant.

If the participant sends a message after the typing activity, the participant will be explicitly removed from the list. If no messages or typing activities are received, the participant is considered inactive and not listed in the result. To keep the typing indicator active, participants should continuously send the typing activity.

The `expireAfter` argument can override the inactivity timer. If `expireAfter` is `Infinity`, it will return all participants who did not explicitly remove from the list. In other words, it will return participants who sent a typing activity, but did not send a message activity afterward.

> This hook will trigger render of your component if one or more typing information is expired or removed.
## `useActivities`

```js
Expand Down Expand Up @@ -199,7 +224,7 @@ This function will return a function that, when called with a `Date` object, `nu
interface Notification {
alt?: string;
id: string;
level: 'error' | 'info' | 'success' | 'warn';
level: 'error' | 'info' | 'success' | 'warn' | string;
message: string;
}

Expand Down Expand Up @@ -420,7 +445,7 @@ When called, this function will mark the activity as spoken and remove it from t
interface Notification {
alt?: string;
id: string;
level: 'error' | 'info' | 'success' | 'warn';
level: 'error' | 'info' | 'success' | 'warn' | string;
message: string;
}

Expand Down Expand Up @@ -544,6 +569,45 @@ renderMarkdown('Hello, World!') === '<p>Hello, World!</p>\n';

To modify this value, change the value in the style options prop passed to Web Chat.

## `useRenderToast`

```js
interface Notification {
alt?: string;
id: string;
level: 'error' | 'info' | 'success' | 'warn' | string;
message: string;
}

useRenderToast(): ({ notification: Notification }) => React.Element
```

This function is for rendering a toast for the notification toaster. The caller will need to pass `notification` as parameter to the function. This function is a composition of `toastMiddleware`, which is passed as a prop to Web Chat.

## `useRenderTypingIndicator`

```js
interface Typing {
at: number;
expireAt: number;
name: string;
role: 'bot' | 'user';
}

useRenderTypingIndicator():
({
activeTyping: { [id: string]: Typing },
typing: { [id: string]: Typing },
visible: boolean
}) => React.Element
```

This function is for rendering typing indicator for all participants of the conversation. This function is a composition of `typingIndicatorMiddleware`, which is passed as a prop to Web Chat. The caller will pass the following arguments:

- `activeTyping` lists of participants who are actively typing.
- `typing` lists participants who did not explicitly stopped typing. This list is a superset of `activeTyping`.
- `visible` indicates whether typing indicator should be shown in normal case. This is based on participants in `activeTyping` and their `role` (role not equal to `"user"`).

## `useScrollToEnd`

```js
Expand Down Expand Up @@ -631,7 +695,7 @@ To modify this value, change the value in the style options prop passed to Web C
interface Notification {
alt?: string;
id: string;
level: 'error' | 'info' | 'success' | 'warn';
level: 'error' | 'info' | 'success' | 'warn' | string;
message: string;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import useAdaptiveCardsHostConfig from '../hooks/useAdaptiveCardsHostConfig';
import useAdaptiveCardsPackage from '../hooks/useAdaptiveCardsPackage';

const { ErrorBox } = Components;
const { useDisabled, useLocalize, usePerformCardAction, useRenderMarkdownAsHTML, useStyleSet } = hooks;
const { useDisabled, useLocalizer, usePerformCardAction, useRenderMarkdownAsHTML, useStyleSet } = hooks;

function isPlainObject(obj) {
return Object.getPrototypeOf(obj) === Object.prototype;
Expand Down Expand Up @@ -69,7 +69,7 @@ const AdaptiveCardRenderer = ({ adaptiveCard, tapAction }) => {
const [{ HostConfig }] = useAdaptiveCardsPackage();
const [adaptiveCardsHostConfig] = useAdaptiveCardsHostConfig();
const [disabled] = useDisabled();
const errorMessage = useLocalize('Adaptive Card render error');
const localize = useLocalizer();
const performCardAction = usePerformCardAction();
const renderMarkdownAsHTML = useRenderMarkdownAsHTML();

Expand Down Expand Up @@ -206,7 +206,7 @@ const AdaptiveCardRenderer = ({ adaptiveCard, tapAction }) => {
]);

return error ? (
<ErrorBox message={errorMessage}>
<ErrorBox message={localize('ADAPTIVE_CARD_ERROR_BOX_TITLE_RENDER')}>
<pre>{JSON.stringify(error, null, 2)}</pre>
</ErrorBox>
) : (
Expand Down
5 changes: 4 additions & 1 deletion packages/component/src/Assets/TypingAnimation.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ const TypingAnimation = () => {
return (
<React.Fragment>
<ScreenReaderText text={localize('TYPING_INDICATOR_ALT')} />
<div aria-hidden={true} className={classNames(rtlScale + '', typingAnimationStyleSet + '')} />
<div
aria-hidden={true}
className={classNames('webchat__typingIndicator', rtlScale + '', typingAnimationStyleSet + '')}
/>
</React.Fragment>
);
};
Expand Down
Loading

0 comments on commit 2172870

Please sign in to comment.