diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 10350b305f..d4578e2a92 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -14,7 +14,7 @@ provided by the bot. You will only need to do this once across all repos using o > > Forking Web Chat to make your own customizations means you will lose access to our latest updates. Maintaining forks also introduces chores that are substantially more complicated than a version bump. -To build Web Chat, you will need to make sure both your Node.js and NPM is latest version (either LTS or current). +To build Web Chat, you will need to make sure both your Node.js and NPM is latest version (either LTS or current, must be `>= 12`). ## Preparing the build diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000000..24d6f25469 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,2 @@ +/**/nodes_modules/ +/samples/**/build/static/ diff --git a/.vscode/launch.json b/.vscode/launch.json index 28b0da37f1..400a8e4524 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -5,11 +5,30 @@ "version": "0.2.0", "configurations": [ { - "type": "chrome", + "type": "edge", "request": "launch", - "name": "Launch Chrome", - "url": "http://localhost:3000", - "webRoot": "${workspaceFolder}" + "name": "Launch Edge", + "url": "http://localhost:5000/samples/", + "webRoot": "${workspaceFolder}", + "sourceMapPathOverrides": { + "webpack-internal:///../*": "${webRoot}/packages/*", + "webpack://botframework-webchat/bundle:///*": "${webRoot}/packages/bundle/*", + "webpack://botframework-webchat/component:///*": "${webRoot}/packages/component/*", + "webpack://botframework-webchat/core:///*": "${webRoot}/packages/core/*" + } + }, + { + "type": "vscode-edge-devtools.debug", + "request": "launch", + "name": "Launch Microsoft Edge and open the Elements tool", + "url": "http://localhost:5000/samples/", + "webRoot": "${workspaceFolder}", + "sourceMapPathOverrides": { + "webpack-internal:///../*": "${webRoot}/packages/*", + "webpack://botframework-webchat/bundle:///*": "${webRoot}/packages/bundle/*", + "webpack://botframework-webchat/component:///*": "${webRoot}/packages/component/*", + "webpack://botframework-webchat/core:///*": "${webRoot}/packages/core/*" + } } ] } diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a270ff151..f8b476ad36 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,6 +53,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - 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) - Resolves [#2754](https://github.com/microsoft/BotFramework-WebChat/issues/2754). Added [telemetry system](https://github.com/microsoft/BotFramework-WebChat/tree/master/docs/TELEMETRY.md), by [@compulim](https://github.com/compulim), in PR [#2922](https://github.com/microsoft/BotFramework-WebChat/pull/2922) +- Resolves [#2857](https://github.com/microsoft/BotFramework-WebChat/issues/2857). Added the ability to customize the avatar on a per activity basis, by [@compulim](https://github.com/compulim), in PR [#2943](https://github.com/microsoft/BotFramework-WebChat/pull/2943) ### Fixed @@ -88,6 +89,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Fixes [#2946](https://github.com/microsoft/BotFramework-WebChat/issues/2946). Updated JSON filenames for localization strings, by [@compulim](https://github.com/compulim) in PR [#2949](https://github.com/microsoft/BotFramework-WebChat/pull/2949) - Fixes [#2560](https://github.com/microsoft/BotFramework-WebChat/issues/2560). Bumped to [`react-dictate-button@1.2.2`](https://npmjs.com/package/react-dictate-button) to workaround [a bug from Angular/zone.js](https://github.com/angular/angular/issues/31750), by [@compulim](https://github.com/compulim) in PR [#2960](https://github.com/microsoft/BotFramework-WebChat/issues/2960) - Fixes [#2923](https://github.com/microsoft/BotFramework-WebChat/issues/2923). Added `download` attribute to file attachment (``), by [@compulim](https://github.com/compulim) in PR [#2963](https://github.com/microsoft/BotFramework-WebChat/pull/2963) +- Fixes [#2904](https://github.com/microsoft/BotFramework-WebChat/issues/2904). Fixed border radius when rendering bubble nub in RTL, by [@compulim](https://github.com/compulim) in PR [#2943](https://github.com/microsoft/BotFramework-WebChat/pull/2943) ### Changed @@ -152,6 +154,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - 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) - Resolves [#2754](https://github.com/microsoft/BotFramework-WebChat/issues/2754). Added [telemetry collection using Azure Application Insights](https://microsoft.github.io/BotFramework-WebChat/04.api/k.telemetry-application-insights) and [telemetry collection using Google Analytics](https://microsoft.github.io/BotFramework-WebChat/04.api/l.telemetry-google-analytics), by [@compulim](https://github.com/compulim), in PR [#2922](https://github.com/microsoft/BotFramework-WebChat/pull/2922) +- Resolves [#2857](https://github.com/microsoft/BotFramework-WebChat/issues/2857). Added [Customize Avatar Demo](https://microsoft.github.io/BotFramework-WebChat/05.custom-components/k.per-message-avatar), by [@compulim](https://github.com/compulim), in PR [#2943](https://github.com/microsoft/BotFramework-WebChat/pull/2943) ## [4.7.1] - 2019-12-13 diff --git a/__tests__/__image_snapshots__/chrome-docker/customizable-avatar-js-customizable-avatar-in-rtl-with-default-avatar-1-snap.png b/__tests__/__image_snapshots__/chrome-docker/customizable-avatar-js-customizable-avatar-in-rtl-with-default-avatar-1-snap.png new file mode 100644 index 0000000000..aeda82a3f5 Binary files /dev/null and b/__tests__/__image_snapshots__/chrome-docker/customizable-avatar-js-customizable-avatar-in-rtl-with-default-avatar-1-snap.png differ diff --git a/__tests__/__image_snapshots__/chrome-docker/customizable-avatar-js-customizable-avatar-in-rtl-with-default-avatar-2-snap.png b/__tests__/__image_snapshots__/chrome-docker/customizable-avatar-js-customizable-avatar-in-rtl-with-default-avatar-2-snap.png new file mode 100644 index 0000000000..5ce7a1cb76 Binary files /dev/null and b/__tests__/__image_snapshots__/chrome-docker/customizable-avatar-js-customizable-avatar-in-rtl-with-default-avatar-2-snap.png differ diff --git a/__tests__/__image_snapshots__/chrome-docker/customizable-avatar-js-customizable-avatar-in-rtl-with-default-avatar-bubble-nub-and-round-bubble-1-snap.png b/__tests__/__image_snapshots__/chrome-docker/customizable-avatar-js-customizable-avatar-in-rtl-with-default-avatar-bubble-nub-and-round-bubble-1-snap.png new file mode 100644 index 0000000000..82c600efda Binary files /dev/null and b/__tests__/__image_snapshots__/chrome-docker/customizable-avatar-js-customizable-avatar-in-rtl-with-default-avatar-bubble-nub-and-round-bubble-1-snap.png differ diff --git a/__tests__/__image_snapshots__/chrome-docker/customizable-avatar-js-customizable-avatar-in-rtl-with-default-avatar-bubble-nub-and-round-bubble-2-snap.png b/__tests__/__image_snapshots__/chrome-docker/customizable-avatar-js-customizable-avatar-in-rtl-with-default-avatar-bubble-nub-and-round-bubble-2-snap.png new file mode 100644 index 0000000000..035533e579 Binary files /dev/null and b/__tests__/__image_snapshots__/chrome-docker/customizable-avatar-js-customizable-avatar-in-rtl-with-default-avatar-bubble-nub-and-round-bubble-2-snap.png differ diff --git a/__tests__/__image_snapshots__/chrome-docker/customizable-avatar-js-customizable-avatar-in-rtl-with-default-avatar-bubble-nub-and-round-bubble-only-on-one-side-1-snap.png b/__tests__/__image_snapshots__/chrome-docker/customizable-avatar-js-customizable-avatar-in-rtl-with-default-avatar-bubble-nub-and-round-bubble-only-on-one-side-1-snap.png new file mode 100644 index 0000000000..e6bb058b9e Binary files /dev/null and b/__tests__/__image_snapshots__/chrome-docker/customizable-avatar-js-customizable-avatar-in-rtl-with-default-avatar-bubble-nub-and-round-bubble-only-on-one-side-1-snap.png differ diff --git a/__tests__/__image_snapshots__/chrome-docker/customizable-avatar-js-customizable-avatar-in-rtl-with-default-avatar-bubble-nub-and-round-bubble-only-on-one-side-2-snap.png b/__tests__/__image_snapshots__/chrome-docker/customizable-avatar-js-customizable-avatar-in-rtl-with-default-avatar-bubble-nub-and-round-bubble-only-on-one-side-2-snap.png new file mode 100644 index 0000000000..7e2792d1e6 Binary files /dev/null and b/__tests__/__image_snapshots__/chrome-docker/customizable-avatar-js-customizable-avatar-in-rtl-with-default-avatar-bubble-nub-and-round-bubble-only-on-one-side-2-snap.png differ diff --git a/__tests__/__image_snapshots__/chrome-docker/customizable-avatar-js-customizable-avatar-in-rtl-with-default-avatar-only-on-one-side-1-snap.png b/__tests__/__image_snapshots__/chrome-docker/customizable-avatar-js-customizable-avatar-in-rtl-with-default-avatar-only-on-one-side-1-snap.png new file mode 100644 index 0000000000..e3c32b3fa6 Binary files /dev/null and b/__tests__/__image_snapshots__/chrome-docker/customizable-avatar-js-customizable-avatar-in-rtl-with-default-avatar-only-on-one-side-1-snap.png differ diff --git a/__tests__/__image_snapshots__/chrome-docker/customizable-avatar-js-customizable-avatar-in-rtl-with-default-avatar-only-on-one-side-2-snap.png b/__tests__/__image_snapshots__/chrome-docker/customizable-avatar-js-customizable-avatar-in-rtl-with-default-avatar-only-on-one-side-2-snap.png new file mode 100644 index 0000000000..316085eb0f Binary files /dev/null and b/__tests__/__image_snapshots__/chrome-docker/customizable-avatar-js-customizable-avatar-in-rtl-with-default-avatar-only-on-one-side-2-snap.png differ diff --git a/__tests__/__image_snapshots__/chrome-docker/customizable-avatar-js-customizable-avatar-with-default-avatar-1-snap.png b/__tests__/__image_snapshots__/chrome-docker/customizable-avatar-js-customizable-avatar-with-default-avatar-1-snap.png new file mode 100644 index 0000000000..7067cc42ad Binary files /dev/null and b/__tests__/__image_snapshots__/chrome-docker/customizable-avatar-js-customizable-avatar-with-default-avatar-1-snap.png differ diff --git a/__tests__/__image_snapshots__/chrome-docker/customizable-avatar-js-customizable-avatar-with-default-avatar-2-snap.png b/__tests__/__image_snapshots__/chrome-docker/customizable-avatar-js-customizable-avatar-with-default-avatar-2-snap.png new file mode 100644 index 0000000000..5b539b07b7 Binary files /dev/null and b/__tests__/__image_snapshots__/chrome-docker/customizable-avatar-js-customizable-avatar-with-default-avatar-2-snap.png differ diff --git a/__tests__/__image_snapshots__/chrome-docker/customizable-avatar-js-customizable-avatar-with-default-avatar-bubble-nub-and-round-bubble-1-snap.png b/__tests__/__image_snapshots__/chrome-docker/customizable-avatar-js-customizable-avatar-with-default-avatar-bubble-nub-and-round-bubble-1-snap.png new file mode 100644 index 0000000000..6fb5260995 Binary files /dev/null and b/__tests__/__image_snapshots__/chrome-docker/customizable-avatar-js-customizable-avatar-with-default-avatar-bubble-nub-and-round-bubble-1-snap.png differ diff --git a/__tests__/__image_snapshots__/chrome-docker/customizable-avatar-js-customizable-avatar-with-default-avatar-bubble-nub-and-round-bubble-2-snap.png b/__tests__/__image_snapshots__/chrome-docker/customizable-avatar-js-customizable-avatar-with-default-avatar-bubble-nub-and-round-bubble-2-snap.png new file mode 100644 index 0000000000..e2f693e0be Binary files /dev/null and b/__tests__/__image_snapshots__/chrome-docker/customizable-avatar-js-customizable-avatar-with-default-avatar-bubble-nub-and-round-bubble-2-snap.png differ diff --git a/__tests__/__image_snapshots__/chrome-docker/customizable-avatar-js-customizable-avatar-with-default-avatar-bubble-nub-and-round-bubble-only-on-one-side-1-snap.png b/__tests__/__image_snapshots__/chrome-docker/customizable-avatar-js-customizable-avatar-with-default-avatar-bubble-nub-and-round-bubble-only-on-one-side-1-snap.png new file mode 100644 index 0000000000..a62f796a31 Binary files /dev/null and b/__tests__/__image_snapshots__/chrome-docker/customizable-avatar-js-customizable-avatar-with-default-avatar-bubble-nub-and-round-bubble-only-on-one-side-1-snap.png differ diff --git a/__tests__/__image_snapshots__/chrome-docker/customizable-avatar-js-customizable-avatar-with-default-avatar-bubble-nub-and-round-bubble-only-on-one-side-2-snap.png b/__tests__/__image_snapshots__/chrome-docker/customizable-avatar-js-customizable-avatar-with-default-avatar-bubble-nub-and-round-bubble-only-on-one-side-2-snap.png new file mode 100644 index 0000000000..b04a96db45 Binary files /dev/null and b/__tests__/__image_snapshots__/chrome-docker/customizable-avatar-js-customizable-avatar-with-default-avatar-bubble-nub-and-round-bubble-only-on-one-side-2-snap.png differ diff --git a/__tests__/__image_snapshots__/chrome-docker/customizable-avatar-js-customizable-avatar-with-default-avatar-only-on-one-side-1-snap.png b/__tests__/__image_snapshots__/chrome-docker/customizable-avatar-js-customizable-avatar-with-default-avatar-only-on-one-side-1-snap.png new file mode 100644 index 0000000000..2785912e18 Binary files /dev/null and b/__tests__/__image_snapshots__/chrome-docker/customizable-avatar-js-customizable-avatar-with-default-avatar-only-on-one-side-1-snap.png differ diff --git a/__tests__/__image_snapshots__/chrome-docker/customizable-avatar-js-customizable-avatar-with-default-avatar-only-on-one-side-2-snap.png b/__tests__/__image_snapshots__/chrome-docker/customizable-avatar-js-customizable-avatar-with-default-avatar-only-on-one-side-2-snap.png new file mode 100644 index 0000000000..93ec3ade6a Binary files /dev/null and b/__tests__/__image_snapshots__/chrome-docker/customizable-avatar-js-customizable-avatar-with-default-avatar-only-on-one-side-2-snap.png differ diff --git a/__tests__/__image_snapshots__/chrome-docker/customizable-avatar-js-customize-size-and-roundness-of-avatar-1-snap.png b/__tests__/__image_snapshots__/chrome-docker/customizable-avatar-js-customize-size-and-roundness-of-avatar-1-snap.png new file mode 100644 index 0000000000..79c313ee44 Binary files /dev/null and b/__tests__/__image_snapshots__/chrome-docker/customizable-avatar-js-customize-size-and-roundness-of-avatar-1-snap.png differ diff --git a/__tests__/__image_snapshots__/chrome-docker/timestamp-js-timestamp-should-update-time-1-snap.png b/__tests__/__image_snapshots__/chrome-docker/timestamp-js-timestamp-should-update-time-1-snap.png index 1c04efe241..728038ea91 100644 Binary files a/__tests__/__image_snapshots__/chrome-docker/timestamp-js-timestamp-should-update-time-1-snap.png and b/__tests__/__image_snapshots__/chrome-docker/timestamp-js-timestamp-should-update-time-1-snap.png differ diff --git a/__tests__/customizableAvatar.js b/__tests__/customizableAvatar.js new file mode 100644 index 0000000000..e99fbe9f36 --- /dev/null +++ b/__tests__/customizableAvatar.js @@ -0,0 +1,347 @@ +import { imageSnapshotOptions, 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); + +describe('customizable avatar', () => { + const createDefaultProps = () => ({ + avatarMiddleware: () => next => args => { + const { activity } = args; + const { text = '' } = activity; + + if (~text.indexOf('override avatar')) { + return () => + React.createElement( + 'div', + { + style: { + alignItems: 'center', + backgroundColor: 'Red', + borderRadius: 4, + color: 'White', + display: 'flex', + fontFamily: "'Calibri', 'Helvetica Neue', 'Arial', 'sans-serif'", + height: 128, + justifyContent: 'center', + width: 64 + } + }, + React.createElement('div', {}, activity.from.role) + ); + } else if (~text.indexOf('no avatar')) { + return false; + } + + return next(args); + }, + styleOptions: { + botAvatarBackgroundColor: '#77F', + botAvatarInitials: 'WC', + userAvatarBackgroundColor: '#F77', + userAvatarInitials: 'WW' + } + }); + + const createFullCustomizedProps = args => { + const props = createDefaultProps(args); + + return { + ...props, + styleOptions: { + ...props.styleOptions, + bubbleBorderColor: 'Black', + bubbleBorderRadius: 10, + bubbleFromUserBorderColor: 'Black', + bubbleFromUserBorderRadius: 10, + bubbleFromUserNubOffset: 5, + bubbleFromUserNubSize: 10, + bubbleNubOffset: 5, + bubbleNubSize: 10 + } + }; + }; + + test('with default avatar', async () => { + const props = createDefaultProps(); + const { driver, pageObjects } = await setupWebDriver({ + height: 768, + props, + // TODO: [P1] #2954 Currently, offline MockBot has bugs that randomize the activity order. + useProductionBot: true + }); + + await driver.wait(uiConnected(), timeouts.directLine); + await pageObjects.sendMessageViaSendBox('normal'); + await driver.wait(minNumActivitiesShown(2), timeouts.directLine); + await pageObjects.sendMessageViaSendBox('override avatar'); + await driver.wait(minNumActivitiesShown(4), timeouts.directLine); + await pageObjects.sendMessageViaSendBox('no avatar'); + await driver.wait(minNumActivitiesShown(6), timeouts.directLine); + + await expect(driver.takeScreenshot()).resolves.toMatchImageSnapshot(imageSnapshotOptions); + + await pageObjects.updateProps({ + ...props, + styleOptions: { + ...props.styleOptions, + botAvatarInitials: undefined, + userAvatarInitials: undefined + } + }); + + await expect(driver.takeScreenshot()).resolves.toMatchImageSnapshot(imageSnapshotOptions); + }); + + test('with default avatar, bubble nub, and round bubble', async () => { + const props = createFullCustomizedProps(); + const { driver, pageObjects } = await setupWebDriver({ + height: 768, + props, + // TODO: [P1] #2954 Currently, offline MockBot has bugs that randomize the activity order. + useProductionBot: true + }); + + await driver.wait(uiConnected(), timeouts.directLine); + await pageObjects.sendMessageViaSendBox('normal'); + await driver.wait(minNumActivitiesShown(2), timeouts.directLine); + await pageObjects.sendMessageViaSendBox('override avatar'); + await driver.wait(minNumActivitiesShown(4), timeouts.directLine); + await pageObjects.sendMessageViaSendBox('no avatar'); + await driver.wait(minNumActivitiesShown(6), timeouts.directLine); + + await expect(driver.takeScreenshot()).resolves.toMatchImageSnapshot(imageSnapshotOptions); + + await pageObjects.updateProps({ + ...props, + styleOptions: { + ...props.styleOptions, + botAvatarInitials: undefined, + userAvatarInitials: undefined + } + }); + + await expect(driver.takeScreenshot()).resolves.toMatchImageSnapshot(imageSnapshotOptions); + }); + + test('with default avatar only on one side', async () => { + let props = createDefaultProps(); + + props = { ...props, styleOptions: { ...props.styleOptions, userAvatarInitials: undefined } }; + + const { driver, pageObjects } = await setupWebDriver({ + props, + // TODO: [P1] #2954 Currently, offline MockBot has bugs that randomize the activity order. + useProductionBot: true + }); + + await driver.wait(uiConnected(), timeouts.directLine); + await pageObjects.sendMessageViaSendBox('normal'); + await driver.wait(minNumActivitiesShown(2), timeouts.directLine); + + await expect(driver.takeScreenshot()).resolves.toMatchImageSnapshot(imageSnapshotOptions); + + props = createDefaultProps(); + props = { ...props, styleOptions: { ...props.styleOptions, botAvatarInitials: undefined } }; + + await pageObjects.updateProps({ + ...props, + styleOptions: { + ...props.styleOptions, + botAvatarInitials: undefined + } + }); + + await expect(driver.takeScreenshot()).resolves.toMatchImageSnapshot(imageSnapshotOptions); + }); + + test('with default avatar, bubble nub, and round bubble only on one side', async () => { + let props = createFullCustomizedProps(); + + props = { ...props, styleOptions: { ...props.styleOptions, userAvatarInitials: undefined } }; + + const { driver, pageObjects } = await setupWebDriver({ + props, + // TODO: [P1] #2954 Currently, offline MockBot has bugs that randomize the activity order. + useProductionBot: true + }); + + await driver.wait(uiConnected(), timeouts.directLine); + await pageObjects.sendMessageViaSendBox('normal'); + await driver.wait(minNumActivitiesShown(2), timeouts.directLine); + + await expect(driver.takeScreenshot()).resolves.toMatchImageSnapshot(imageSnapshotOptions); + + props = createFullCustomizedProps(); + props = { ...props, styleOptions: { ...props.styleOptions, botAvatarInitials: undefined } }; + + await pageObjects.updateProps({ + ...props, + styleOptions: { + ...props.styleOptions, + botAvatarInitials: undefined + } + }); + + await expect(driver.takeScreenshot()).resolves.toMatchImageSnapshot(imageSnapshotOptions); + }); + + describe('in RTL', () => { + test('with default avatar', async () => { + const props = { + ...createDefaultProps(), + locale: 'ar-EG' + }; + + const { driver, pageObjects } = await setupWebDriver({ + height: 768, + props, + // TODO: [P1] #2954 Currently, offline MockBot has bugs that randomize the activity order. + useProductionBot: true + }); + + await driver.wait(uiConnected(), timeouts.directLine); + await pageObjects.sendMessageViaSendBox('normal'); + await driver.wait(minNumActivitiesShown(2), timeouts.directLine); + await pageObjects.sendMessageViaSendBox('override avatar'); + await driver.wait(minNumActivitiesShown(4), timeouts.directLine); + await pageObjects.sendMessageViaSendBox('no avatar'); + await driver.wait(minNumActivitiesShown(6), timeouts.directLine); + + await expect(driver.takeScreenshot()).resolves.toMatchImageSnapshot(imageSnapshotOptions); + + await pageObjects.updateProps({ + ...props, + styleOptions: { + ...props.styleOptions, + botAvatarInitials: undefined, + userAvatarInitials: undefined + } + }); + + await expect(driver.takeScreenshot()).resolves.toMatchImageSnapshot(imageSnapshotOptions); + }); + + test('with default avatar, bubble nub, and round bubble', async () => { + const props = { + ...createFullCustomizedProps(), + locale: 'ar-EG' + }; + + const { driver, pageObjects } = await setupWebDriver({ + height: 768, + props, + // TODO: [P1] #2954 Currently, offline MockBot has bugs that randomize the activity order. + useProductionBot: true + }); + + await driver.wait(uiConnected(), timeouts.directLine); + await pageObjects.sendMessageViaSendBox('normal'); + await driver.wait(minNumActivitiesShown(2), timeouts.directLine); + await pageObjects.sendMessageViaSendBox('override avatar'); + await driver.wait(minNumActivitiesShown(4), timeouts.directLine); + await pageObjects.sendMessageViaSendBox('no avatar'); + await driver.wait(minNumActivitiesShown(6), timeouts.directLine); + + await expect(driver.takeScreenshot()).resolves.toMatchImageSnapshot(imageSnapshotOptions); + + await pageObjects.updateProps({ + ...props, + styleOptions: { + ...props.styleOptions, + botAvatarInitials: undefined, + userAvatarInitials: undefined + } + }); + + await expect(driver.takeScreenshot()).resolves.toMatchImageSnapshot(imageSnapshotOptions); + }); + + test('with default avatar only on one side', async () => { + let props = createDefaultProps(); + + props = { ...props, locale: 'ar-EG', styleOptions: { ...props.styleOptions, userAvatarInitials: undefined } }; + + const { driver, pageObjects } = await setupWebDriver({ + props, + // TODO: [P1] #2954 Currently, offline MockBot has bugs that randomize the activity order. + useProductionBot: true + }); + + await driver.wait(uiConnected(), timeouts.directLine); + await pageObjects.sendMessageViaSendBox('normal'); + await driver.wait(minNumActivitiesShown(2), timeouts.directLine); + + await expect(driver.takeScreenshot()).resolves.toMatchImageSnapshot(imageSnapshotOptions); + + props = createDefaultProps(); + props = { ...props, styleOptions: { ...props.styleOptions, botAvatarInitials: undefined } }; + + await pageObjects.updateProps({ + ...props, + styleOptions: { + ...props.styleOptions, + botAvatarInitials: undefined + } + }); + + await expect(driver.takeScreenshot()).resolves.toMatchImageSnapshot(imageSnapshotOptions); + }); + + test('with default avatar, bubble nub, and round bubble only on one side', async () => { + let props = createFullCustomizedProps(); + + props = { ...props, locale: 'ar-EG', styleOptions: { ...props.styleOptions, userAvatarInitials: undefined } }; + + const { driver, pageObjects } = await setupWebDriver({ + props, + // TODO: [P1] #2954 Currently, offline MockBot has bugs that randomize the activity order. + useProductionBot: true + }); + + await driver.wait(uiConnected(), timeouts.directLine); + await pageObjects.sendMessageViaSendBox('normal'); + await driver.wait(minNumActivitiesShown(2), timeouts.directLine); + + await expect(driver.takeScreenshot()).resolves.toMatchImageSnapshot(imageSnapshotOptions); + + props = createFullCustomizedProps(); + props = { ...props, styleOptions: { ...props.styleOptions, botAvatarInitials: undefined } }; + + await pageObjects.updateProps({ + ...props, + styleOptions: { + ...props.styleOptions, + botAvatarInitials: undefined + } + }); + + await expect(driver.takeScreenshot()).resolves.toMatchImageSnapshot(imageSnapshotOptions); + }); + }); +}); + +test('customize size and roundness of avatar', async () => { + const { driver, pageObjects } = await setupWebDriver({ + props: { + styleOptions: { + avatarBorderRadius: '20%', + avatarSize: 64, + botAvatarInitials: 'WC', + userAvatarInitials: 'WW' + } + }, + // TODO: [P1] #2954 Currently, offline MockBot has bugs that randomize the activity order. + useProductionBot: true + }); + + await driver.wait(uiConnected(), timeouts.directLine); + await pageObjects.sendMessageViaSendBox('normal'); + await driver.wait(minNumActivitiesShown(2), timeouts.directLine); + + await expect(driver.takeScreenshot()).resolves.toMatchImageSnapshot(imageSnapshotOptions); +}); diff --git a/packages/component/src/Activity/Avatar.js b/packages/component/src/Activity/Avatar.js index 0b7f7dc23e..50e797e602 100644 --- a/packages/component/src/Activity/Avatar.js +++ b/packages/component/src/Activity/Avatar.js @@ -1,50 +1,14 @@ -import classNames from 'classnames'; import PropTypes from 'prop-types'; import React from 'react'; -import connectToWebChat from '../connectToWebChat'; -import CroppedImage from '../Utils/CroppedImage'; -import useAvatarForBot from '../hooks/useAvatarForBot'; -import useAvatarForUser from '../hooks/useAvatarForUser'; -import useStyleSet from '../hooks/useStyleSet'; - -const connectAvatar = (...selectors) => - connectToWebChat( - ( - { - styleSet: { - options: { botAvatarImage, botAvatarInitials, userAvatarImage, userAvatarInitials } - } - }, - { fromUser } - ) => ({ - avatarImage: fromUser ? userAvatarImage : botAvatarImage, - avatarInitials: fromUser ? userAvatarInitials : botAvatarInitials - }), - ...selectors - ); - -// TODO: [P2] Consider memoizing "style={ backgroundImage }" in our upstreamers -// We have 2 different upstreamers and +import { DefaultAvatar } from '../Middleware/Avatar/createCoreMiddleware'; const Avatar = ({ 'aria-hidden': ariaHidden, className, fromUser }) => { - const [botAvatar] = useAvatarForBot(); - const [userAvatar] = useAvatarForUser(); - const [{ avatar: avatarStyleSet }] = useStyleSet(); - - const { image, initials } = fromUser ? userAvatar : botAvatar; - - return ( - !!(image || initials) && ( -
- {initials} - {!!image && } -
- ) + console.warn( + 'botframework-webchat: component is deprecated and will be removed on or after 2022-02-25. Please use `useRenderAvatar` hook instead.' ); + + return ; }; Avatar.defaultProps = { @@ -60,5 +24,3 @@ Avatar.propTypes = { }; export default Avatar; - -export { connectAvatar }; diff --git a/packages/component/src/Activity/CarouselFilmStrip.js b/packages/component/src/Activity/CarouselFilmStrip.js index e2cc483d67..6451424ebc 100644 --- a/packages/component/src/Activity/CarouselFilmStrip.js +++ b/packages/component/src/Activity/CarouselFilmStrip.js @@ -7,7 +7,6 @@ import PropTypes from 'prop-types'; import React from 'react'; import remarkStripMarkdown from '../Utils/remarkStripMarkdown'; -import Avatar from './Avatar'; import Bubble from './Bubble'; import connectToWebChat from '../connectToWebChat'; import ScreenReaderText from '../ScreenReaderText'; @@ -17,6 +16,7 @@ import useAvatarForUser from '../hooks/useAvatarForUser'; import useDirection from '../hooks/useDirection'; import useLocalizer from '../hooks/useLocalizer'; import useRenderActivityStatus from '../hooks/useRenderActivityStatus'; +import useRenderAvatar from '../hooks/useRenderAvatar'; import useStyleOptions from '../hooks/useStyleOptions'; import useStyleSet from '../hooks/useStyleSet'; @@ -32,7 +32,7 @@ const ROOT_CSS = css({ display: 'none' }, - '& > .avatar': { + '& > .webchat__carouselFilmStrip__avatar': { flexShrink: 0 }, @@ -98,6 +98,7 @@ const WebChatCarouselFilmStrip = ({ const [direction] = useDirection(); const localize = useLocalizer(); const renderActivityStatus = useRenderActivityStatus({ activity, nextVisibleActivity }); + const renderAvatar = useRenderAvatar({ activity }); const { attachments = [], @@ -128,7 +129,7 @@ const WebChatCarouselFilmStrip = ({ )} ref={scrollableRef} > - + {renderAvatar &&
{renderAvatar()}
}
{!!activityDisplayText && (
diff --git a/packages/component/src/Activity/StackedLayout.js b/packages/component/src/Activity/StackedLayout.js index cbf33cb106..74f393b289 100644 --- a/packages/component/src/Activity/StackedLayout.js +++ b/packages/component/src/Activity/StackedLayout.js @@ -7,7 +7,6 @@ import PropTypes from 'prop-types'; import React from 'react'; import remarkStripMarkdown from '../Utils/remarkStripMarkdown'; -import Avatar from './Avatar'; import Bubble from './Bubble'; import connectToWebChat from '../connectToWebChat'; import ScreenReaderText from '../ScreenReaderText'; @@ -18,17 +17,18 @@ import useDateFormatter from '../hooks/useDateFormatter'; import useDirection from '../hooks/useDirection'; import useLocalizer from '../hooks/useLocalizer'; import useRenderActivityStatus from '../hooks/useRenderActivityStatus'; +import useRenderAvatar from '../hooks/useRenderAvatar'; import useStyleOptions from '../hooks/useStyleOptions'; import useStyleSet from '../hooks/useStyleSet'; const ROOT_CSS = css({ display: 'flex', - '& > .avatar': { + '& > .webchat__stackedLayout__avatar': { flexShrink: 0 }, - '& > .content': { + '& > .webchat__stackedLayout__content': { flexGrow: 1, overflow: 'hidden', @@ -51,10 +51,10 @@ const ROOT_CSS = css({ flexShrink: 0 }, - '&.from-user': { + '&.webchat__stackedLayout--fromUser': { flexDirection: 'row-reverse', - '& > .content > .webchat__row': { + '& > .webchat__stackedLayout__content > .webchat__row': { flexDirection: 'row-reverse' } } @@ -84,12 +84,13 @@ const connectStackedLayout = (...selectors) => const StackedLayout = ({ activity, children, nextVisibleActivity }) => { const [{ initials: botInitials }] = useAvatarForBot(); const [{ initials: userInitials }] = useAvatarForUser(); - const [{ botAvatarInitials, bubbleNubSize, bubbleFromUserNubSize, userAvatarInitials }] = useStyleOptions(); + const [{ bubbleNubSize, bubbleFromUserNubSize }] = useStyleOptions(); const [{ stackedLayout: stackedLayoutStyleSet }] = useStyleSet(); const [direction] = useDirection(); const formatDate = useDateFormatter(); const localize = useLocalizer(); const renderActivityStatus = useRenderActivityStatus({ activity, nextVisibleActivity }); + const renderAvatar = useRenderAvatar({ activity }); const { attachments = [], @@ -119,22 +120,22 @@ const StackedLayout = ({ activity, children, nextVisibleActivity }) => { className={classNames( ROOT_CSS + '', stackedLayoutStyleSet + '', - direction === 'rtl' ? 'webchat__stacked--rtl' : '', + direction === 'rtl' ? 'webchat__stackedLayout--rtl' : '', { - 'from-user': fromUser, + 'webchat__stackedLayout--fromUser': fromUser, webchat__stacked_extra_left_indent: - (direction !== 'rtl' && fromUser && !botAvatarInitials && bubbleNubSize) || - (direction === 'rtl' && !fromUser && !userAvatarInitials && bubbleFromUserNubSize), + (direction !== 'rtl' && fromUser && !renderAvatar && bubbleNubSize) || + (direction === 'rtl' && !fromUser && !renderAvatar && bubbleFromUserNubSize), webchat__stacked_extra_right_indent: - (direction !== 'rtl' && !fromUser && !userAvatarInitials && bubbleFromUserNubSize) || - (direction === 'rtl' && fromUser && !botAvatarInitials && bubbleNubSize), - webchat__stacked_indented_content: initials && !indented + (direction !== 'rtl' && !fromUser && !renderAvatar && bubbleFromUserNubSize) || + (direction === 'rtl' && fromUser && !renderAvatar && bubbleNubSize), + webchat__stacked_indented_content: renderAvatar && !indented, + 'webchat__stackedLayout--hasAvatar': renderAvatar && !!(fromUser ? bubbleFromUserNubSize : bubbleNubSize) } )} > - {!initials && !!(fromUser ? bubbleFromUserNubSize : bubbleNubSize) &&
} - -
+ {renderAvatar &&
{renderAvatar()}
} +
{!!activityDisplayText && (
diff --git a/packages/component/src/Avatar/ImageAvatar.js b/packages/component/src/Avatar/ImageAvatar.js new file mode 100644 index 0000000000..9a9780534f --- /dev/null +++ b/packages/component/src/Avatar/ImageAvatar.js @@ -0,0 +1,43 @@ +import { css } from 'glamor'; +import classNames from 'classnames'; +import PropTypes from 'prop-types'; +import React from 'react'; + +import CroppedImage from '../Utils/CroppedImage'; +import useAvatarForBot from '../hooks/useAvatarForBot'; +import useAvatarForUser from '../hooks/useAvatarForUser'; +import useStyleSet from '../hooks/useStyleSet'; + +const ROOT_CSS = css({ + '& .webchat__imageAvatar__image': { + width: '100%' + } +}); + +const ImageAvatar = ({ fromUser }) => { + const [{ image: avatarImageForBot }] = useAvatarForBot(); + const [{ image: avatarImageForUser }] = useAvatarForUser(); + const [{ imageAvatar: imageAvatarStyleSet }] = useStyleSet(); + + return ( +
+ +
+ ); +}; + +ImageAvatar.defaultProps = { + fromUser: false +}; + +ImageAvatar.propTypes = { + fromUser: PropTypes.bool +}; + +export default ImageAvatar; diff --git a/packages/component/src/Avatar/InitialsAvatar.js b/packages/component/src/Avatar/InitialsAvatar.js new file mode 100644 index 0000000000..4bb1a59a75 --- /dev/null +++ b/packages/component/src/Avatar/InitialsAvatar.js @@ -0,0 +1,46 @@ +import { css } from 'glamor'; +import classNames from 'classnames'; +import PropTypes from 'prop-types'; +import React from 'react'; + +import useAvatarForBot from '../hooks/useAvatarForBot'; +import useAvatarForUser from '../hooks/useAvatarForUser'; +import useStyleSet from '../hooks/useStyleSet'; + +const ROOT_CSS = css({ + alignItems: 'center', + display: 'flex', + + '& .webchat__initialsAvatar__initials': { + justifyContent: 'center' + } +}); + +const InitialsAvatar = ({ fromUser }) => { + const [{ initials: avatarInitialsForBot }] = useAvatarForBot(); + const [{ initials: avatarInitialsForUser }] = useAvatarForUser(); + const [{ initialsAvatar: initialsAvatarStyleSet }] = useStyleSet(); + + return ( +
+
{fromUser ? avatarInitialsForUser : avatarInitialsForBot}
+
+ ); +}; + +InitialsAvatar.defaultProps = { + fromUser: false +}; + +InitialsAvatar.propTypes = { + fromUser: PropTypes.bool +}; + +export default InitialsAvatar; diff --git a/packages/component/src/BasicWebChat.js b/packages/component/src/BasicWebChat.js index d1d2ba4f41..832c2dc5db 100644 --- a/packages/component/src/BasicWebChat.js +++ b/packages/component/src/BasicWebChat.js @@ -15,6 +15,7 @@ import concatMiddleware from './Middleware/concatMiddleware'; import createCoreActivityMiddleware from './Middleware/Activity/createCoreMiddleware'; import createCoreActivityStatusMiddleware from './Middleware/ActivityStatus/createCoreMiddleware'; import createCoreAttachmentMiddleware from './Middleware/Attachment/createCoreMiddleware'; +import createCoreAvatarMiddleware from './Middleware/Avatar/createCoreMiddleware'; import createCoreToastMiddleware from './Middleware/Toast/createCoreMiddleware'; import createCoreTypingIndicatorMiddleware from './Middleware/TypingIndicator/createCoreMiddleware'; import ErrorBox from './ErrorBox'; @@ -113,6 +114,19 @@ function createAttachmentRenderer(additionalMiddleware) { }; } +// TODO: [P2] #2859 We should move these into +function createAvatarRenderer(additionalMiddleware) { + const avatarMiddleware = concatMiddleware(additionalMiddleware, createCoreAvatarMiddleware())({}); + + return (...args) => { + try { + return avatarMiddleware(() => false)(...args); + } catch (err) { + console.error('Failed to render avatar', err); + } + }; +} + // TODO: [P2] #2859 We should move these into function createToastRenderer(additionalMiddleware) { const toastMiddleware = concatMiddleware(additionalMiddleware, createCoreToastMiddleware())({}); @@ -166,6 +180,7 @@ const BasicWebChat = ({ activityMiddleware, activityStatusMiddleware, attachmentMiddleware, + avatarMiddleware, className, toastMiddleware, typingIndicatorMiddleware, @@ -177,6 +192,7 @@ const BasicWebChat = ({ activityStatusMiddleware ]); const attachmentRenderer = useMemo(() => createAttachmentRenderer(attachmentMiddleware), [attachmentMiddleware]); + const avatarRenderer = useMemo(() => createAvatarRenderer(avatarMiddleware), [avatarMiddleware]); const toastRenderer = useMemo(() => createToastRenderer(toastMiddleware), [toastMiddleware]); const typingIndicatorRenderer = useMemo(() => createTypingIndicatorRenderer(typingIndicatorMiddleware), [ typingIndicatorMiddleware @@ -187,6 +203,7 @@ const BasicWebChat = ({ activityRenderer={activityRenderer} activityStatusRenderer={activityStatusRenderer} attachmentRenderer={attachmentRenderer} + avatarRenderer={avatarRenderer} sendBoxRef={sendBoxRef} toastRenderer={toastRenderer} typingIndicatorRenderer={typingIndicatorRenderer} @@ -214,6 +231,7 @@ BasicWebChat.defaultProps = { ...Composer.defaultProps, activityMiddleware: undefined, attachmentMiddleware: undefined, + avatarMiddleware: undefined, className: '' }; @@ -221,5 +239,6 @@ BasicWebChat.propTypes = { ...Composer.propTypes, activityMiddleware: PropTypes.func, attachmentMiddleware: PropTypes.func, + avatarMiddleware: PropTypes.func, className: PropTypes.string }; diff --git a/packages/component/src/Composer.js b/packages/component/src/Composer.js index 4b68cdd649..a932adee9c 100644 --- a/packages/component/src/Composer.js +++ b/packages/component/src/Composer.js @@ -158,6 +158,7 @@ const Composer = ({ activityRenderer, activityStatusRenderer, attachmentRenderer, + avatarRenderer, cardActionMiddleware, children, dir, @@ -351,6 +352,7 @@ const Composer = ({ activityRenderer, activityStatusRenderer, attachmentRenderer, + avatarRenderer, dictateAbortable, dir: patchedDir, directLine, @@ -382,6 +384,7 @@ const Composer = ({ activityRenderer, activityStatusRenderer, attachmentRenderer, + avatarRenderer, cardActionContext, dictateAbortable, directLine, @@ -473,6 +476,7 @@ Composer.defaultProps = { activityRenderer: undefined, activityStatusRenderer: undefined, attachmentRenderer: undefined, + avatarRenderer: undefined, cardActionMiddleware: undefined, children: undefined, dir: 'auto', @@ -501,6 +505,7 @@ Composer.propTypes = { activityRenderer: PropTypes.func, activityStatusRenderer: PropTypes.func, attachmentRenderer: PropTypes.func, + avatarRenderer: PropTypes.func, cardActionMiddleware: PropTypes.func, children: PropTypes.any, dir: PropTypes.oneOf(['auto', 'ltr', 'rtl']), diff --git a/packages/component/src/Middleware/Avatar/createCoreMiddleware.js b/packages/component/src/Middleware/Avatar/createCoreMiddleware.js new file mode 100644 index 0000000000..c0de630563 --- /dev/null +++ b/packages/component/src/Middleware/Avatar/createCoreMiddleware.js @@ -0,0 +1,66 @@ +import { css } from 'glamor'; +import classNames from 'classnames'; +import PropTypes from 'prop-types'; +import React from 'react'; + +import concatMiddleware from '../concatMiddleware'; +import ImageAvatar from '../../Avatar/ImageAvatar'; +import InitialsAvatar from '../../Avatar/InitialsAvatar'; +import useStyleSet from '../../hooks/useStyleSet'; + +const ROOT_CSS = css({ + overflow: 'hidden', + position: 'relative', + + '> *': { + left: 0, + position: 'absolute', + top: 0 + } +}); + +const DefaultAvatar = ({ 'aria-hidden': ariaHidden, className, fromUser }) => { + const [{ avatar: avatarStyleSet }] = useStyleSet(); + + return ( +
+ + +
+ ); +}; + +DefaultAvatar.defaultProps = { + 'aria-hidden': true, + className: '' +}; + +DefaultAvatar.propTypes = { + 'aria-hidden': PropTypes.bool, + className: PropTypes.string, + fromUser: PropTypes.bool.isRequired +}; + +export default function createCoreAvatarMiddleware() { + return concatMiddleware(() => () => ({ fromUser, styleOptions }) => { + const { botAvatarImage, botAvatarInitials, userAvatarImage, userAvatarInitials } = styleOptions; + + if (fromUser ? userAvatarImage || userAvatarInitials : botAvatarImage || botAvatarInitials) { + // eslint-disable-next-line react/display-name + return () => ; + } + + return false; + }); +} + +export { DefaultAvatar }; diff --git a/packages/component/src/Styles/StyleSet/Avatar.js b/packages/component/src/Styles/StyleSet/Avatar.js index c52d8a0e9f..14a0a35a5e 100644 --- a/packages/component/src/Styles/StyleSet/Avatar.js +++ b/packages/component/src/Styles/StyleSet/Avatar.js @@ -1,35 +1,9 @@ -export default function createAvatarStyle({ - accent, - avatarSize, - botAvatarBackgroundColor, - primaryFont, - userAvatarBackgroundColor -}) { +export default function createAvatarStyle({ avatarBorderRadius, avatarSize }) { return { - alignItems: 'center', - borderRadius: '50%', - color: 'White', - // TODO: [P2] We should not set "display" in styleSet, this will allow the user to break the layout for no good reasons. - display: 'flex', - fontFamily: primaryFont, - height: avatarSize, - justifyContent: 'center', - overflow: 'hidden', - position: 'relative', - width: avatarSize, - - '&.from-user': { - backgroundColor: userAvatarBackgroundColor || accent - }, - - '&:not(.from-user)': { - backgroundColor: botAvatarBackgroundColor || accent - }, - - '& > .image': { - left: 0, - position: 'absolute', - top: 0 + '&.webchat__defaultAvatar': { + borderRadius: avatarBorderRadius, + height: avatarSize, + width: avatarSize } }; } diff --git a/packages/component/src/Styles/StyleSet/Bubble.js b/packages/component/src/Styles/StyleSet/Bubble.js index 69cf12cb0b..f8a2f8098f 100644 --- a/packages/component/src/Styles/StyleSet/Bubble.js +++ b/packages/component/src/Styles/StyleSet/Bubble.js @@ -1,6 +1,6 @@ /* eslint no-magic-numbers: ["error", { "ignore": [0, 1, 2] }] */ -function isPositive(value) { +function isZeroOrPositive(value) { return 1 / value >= 0; } @@ -25,8 +25,8 @@ export default function createBubbleStyle({ messageActivityWordBreak, paddingRegular }) { - const botNubUpSideDown = !isPositive(bubbleNubOffset); - const userNubUpSideDown = !isPositive(bubbleFromUserNubOffset); + const botNubUpSideDown = !isZeroOrPositive(bubbleNubOffset); + const userNubUpSideDown = !isZeroOrPositive(bubbleFromUserNubOffset); const botNubCornerRadius = Math.min(bubbleBorderRadius, Math.abs(bubbleNubOffset)); const userNubCornerRadius = Math.min(bubbleFromUserBorderRadius, Math.abs(bubbleFromUserNubOffset)); @@ -52,15 +52,17 @@ export default function createBubbleStyle({ minHeight: bubbleMinHeight - bubbleBorderWidth * 2 }, - '&.webchat__bubble_has_nub > .webchat__bubble__content': { - // Hide border radius if there is a nub on the top/bottom left corner - ...(bubbleNubSize && botNubUpSideDown ? { borderBottomLeftRadius: botNubCornerRadius } : {}), - ...(bubbleNubSize && !botNubUpSideDown ? { borderTopLeftRadius: botNubCornerRadius } : {}) - }, - '&:not(.webchat__bubble--rtl)': { '&.webchat__bubble_has_nub': { - '& > .webchat__bubble__content': bubbleNubSize ? { marginLeft: paddingRegular } : {} + '& > .webchat__bubble__content': bubbleNubSize + ? { + marginLeft: paddingRegular, + + // Hide border radius if there is a nub on the top/bottom left corner + ...(botNubUpSideDown ? { borderBottomLeftRadius: botNubCornerRadius } : {}), + ...(!botNubUpSideDown ? { borderTopLeftRadius: botNubCornerRadius } : {}) + } + : {} }, '& > .webchat__bubble__nub': { @@ -70,7 +72,15 @@ export default function createBubbleStyle({ '&.webchat__bubble--rtl': { '&.webchat__bubble_has_nub': { - '& > .webchat__bubble__content': bubbleNubSize ? { marginRight: paddingRegular } : {} + '& > .webchat__bubble__content': bubbleNubSize + ? { + marginRight: paddingRegular, + + // Hide border radius if there is a nub on the top/bottom right corner + ...(botNubUpSideDown ? { borderBottomRightRadius: botNubCornerRadius } : {}), + ...(!botNubUpSideDown ? { borderTopRightRadius: botNubCornerRadius } : {}) + } + : {} }, '& > .webchat__bubble__nub': { @@ -80,9 +90,9 @@ export default function createBubbleStyle({ }, '& > .webchat__bubble__nub': { - bottom: isPositive(bubbleNubOffset) ? undefined : -bubbleNubOffset, + bottom: isZeroOrPositive(bubbleNubOffset) ? undefined : -bubbleNubOffset, height: bubbleNubSize, - top: isPositive(bubbleNubOffset) ? bubbleNubOffset : undefined, + top: isZeroOrPositive(bubbleNubOffset) ? bubbleNubOffset : undefined, width: bubbleNubSize, '& > g > path': { @@ -104,15 +114,16 @@ export default function createBubbleStyle({ minHeight: bubbleMinHeight - bubbleFromUserBorderWidth * 2 }, - '&.webchat__bubble_has_nub > .webchat__bubble__content': { - // Hide border radius if there is a nub on the top/bottom right corner - ...(bubbleFromUserNubSize && userNubUpSideDown ? { borderBottomRightRadius: userNubCornerRadius } : {}), - ...(bubbleFromUserNubSize && !userNubUpSideDown ? { borderTopRightRadius: userNubCornerRadius } : {}) - }, - '&:not(.webchat__bubble--rtl)': { '&.webchat__bubble_has_nub': { - '& > .webchat__bubble__content': bubbleFromUserNubSize ? { marginRight: paddingRegular } : {} + '& > .webchat__bubble__content': bubbleFromUserNubSize + ? { + marginRight: paddingRegular, + // Hide border radius if there is a nub on the top/bottom right corner + ...(userNubUpSideDown ? { borderBottomRightRadius: userNubCornerRadius } : {}), + ...(!userNubUpSideDown ? { borderTopRightRadius: userNubCornerRadius } : {}) + } + : {} }, '& > .webchat__bubble__nub': { @@ -122,7 +133,14 @@ export default function createBubbleStyle({ '&.webchat__bubble--rtl': { '&.webchat__bubble_has_nub': { - '& > .webchat__bubble__content': bubbleFromUserNubSize ? { marginLeft: paddingRegular } : {} + '& > .webchat__bubble__content': bubbleFromUserNubSize + ? { + marginLeft: paddingRegular, + // Hide border radius if there is a nub on the top/bottom left corner + ...(userNubUpSideDown ? { borderBottomLeftRadius: userNubCornerRadius } : {}), + ...(!userNubUpSideDown ? { borderTopLeftRadius: userNubCornerRadius } : {}) + } + : {} }, '& > .webchat__bubble__nub': { @@ -133,8 +151,8 @@ export default function createBubbleStyle({ '& > .webchat__bubble__nub': { height: bubbleFromUserNubSize, - bottom: isPositive(bubbleFromUserNubOffset) ? undefined : -bubbleFromUserNubOffset, - top: isPositive(bubbleFromUserNubOffset) ? bubbleFromUserNubOffset : undefined, + bottom: isZeroOrPositive(bubbleFromUserNubOffset) ? undefined : -bubbleFromUserNubOffset, + top: isZeroOrPositive(bubbleFromUserNubOffset) ? bubbleFromUserNubOffset : undefined, width: bubbleFromUserNubSize, '& > g > path': { diff --git a/packages/component/src/Styles/StyleSet/ImageAvatar.js b/packages/component/src/Styles/StyleSet/ImageAvatar.js new file mode 100644 index 0000000000..abceae3ab9 --- /dev/null +++ b/packages/component/src/Styles/StyleSet/ImageAvatar.js @@ -0,0 +1,7 @@ +export default function createImageAvatarStyle({ avatarSize }) { + return { + height: avatarSize, + overflow: 'hidden', + width: avatarSize + }; +} diff --git a/packages/component/src/Styles/StyleSet/InitialsAvatar.js b/packages/component/src/Styles/StyleSet/InitialsAvatar.js new file mode 100644 index 0000000000..fd9581004c --- /dev/null +++ b/packages/component/src/Styles/StyleSet/InitialsAvatar.js @@ -0,0 +1,27 @@ +export default function createInitialsAvatarStyle({ + accent, + avatarSize, + botAvatarBackgroundColor, + primaryFont, + userAvatarBackgroundColor +}) { + return { + '&.webchat__initialsAvatar': { + alignItems: 'center', + color: 'White', + fontFamily: primaryFont, + height: avatarSize, + justifyContent: 'center', + overflow: 'hidden', + width: avatarSize, + + '&.webchat__initialsAvatar--fromUser': { + backgroundColor: userAvatarBackgroundColor || accent + }, + + '&:not(.webchat__initialsAvatar--fromUser)': { + backgroundColor: botAvatarBackgroundColor || accent + } + } + }; +} diff --git a/packages/component/src/Styles/StyleSet/StackedLayout.js b/packages/component/src/Styles/StyleSet/StackedLayout.js index e127c5f23d..b5148ec382 100644 --- a/packages/component/src/Styles/StyleSet/StackedLayout.js +++ b/packages/component/src/Styles/StyleSet/StackedLayout.js @@ -17,51 +17,51 @@ export default function createStackedLayoutStyle({ bubbleMaxWidth, bubbleMinWidt '&:not(.webchat__stacked_extra_right_indent)': { marginRight: paddingRegular }, - '&:not(.webchat__stacked--rtl)': { - '&:not(.from-user)': { - '&.webchat__stacked_indented_content > .avatar': { + '&:not(.webchat__stackedLayout--rtl)': { + '&:not(.webchat__stackedLayout--fromUser)': { + '&.webchat__stacked_indented_content > .webchat__stackedLayout__avatar': { marginRight: paddingRegular }, - '& > .content > .webchat__stacked_item_indented': { + '& > .webchat__stackedLayout__content > .webchat__stacked_item_indented': { marginLeft: paddingRegular } }, - '&.from-user': { - '&.webchat__stacked_indented_content > .avatar': { + '&.webchat__stackedLayout--fromUser': { + '&.webchat__stacked_indented_content > .webchat__stackedLayout__avatar': { marginLeft: paddingRegular }, - '& > .content > .webchat__stacked_item_indented': { + '& > .webchat__stackedLayout__content > .webchat__stacked_item_indented': { marginRight: paddingRegular } } }, - '&.webchat__stacked--rtl': { - '&:not(.from-user)': { - '&.webchat__stacked_indented_content > .avatar': { + '&.webchat__stackedLayout--rtl': { + '&:not(.webchat__stackedLayout--fromUser)': { + '&.webchat__stacked_indented_content > .webchat__stackedLayout__avatar': { marginLeft: paddingRegular }, - '& > .content > .webchat__stacked_item_indented': { + '& > .webchat__stackedLayout__content > .webchat__stacked_item_indented': { marginRight: paddingRegular } }, - '&.from-user': { - '&.webchat__stacked_indented_content > .avatar': { + '&.webchat__stackedLayout--fromUser': { + '&.webchat__stacked_indented_content > .webchat__stackedLayout__avatar': { marginRight: paddingRegular }, - '& > .content > .webchat__stacked_item_indented': { + '& > .webchat__stackedLayout__content > .webchat__stacked_item_indented': { marginLeft: paddingRegular } } }, - '& > .content': { + '& > .webchat__stackedLayout__content': { '& > .webchat__row': { '& > .bubble, & > .timestamp': { maxWidth: bubbleMaxWidth diff --git a/packages/component/src/Styles/createStyleSet.js b/packages/component/src/Styles/createStyleSet.js index dc99f61152..9bee033fe0 100644 --- a/packages/component/src/Styles/createStyleSet.js +++ b/packages/component/src/Styles/createStyleSet.js @@ -12,6 +12,8 @@ import createDictationInterimsStyle from './StyleSet/DictationInterims'; import createErrorBoxStyle from './StyleSet/ErrorBox'; import createErrorNotificationStyle from './StyleSet/ErrorNotification'; import createFileContentStyle from './StyleSet/FileContent'; +import createImageAvatarStyle from './StyleSet/ImageAvatar'; +import createInitialsAvatarStyle from './StyleSet/InitialsAvatar'; import createMicrophoneButtonStyle from './StyleSet/MicrophoneButton'; import createRootStyle from './StyleSet/Root'; import createScrollToEndButtonStyle from './StyleSet/ScrollToEndButton'; @@ -183,6 +185,8 @@ export default function createStyleSet(options) { errorBox: createErrorBoxStyle(options), errorNotification: createErrorNotificationStyle(options), fileContent: createFileContentStyle(options), + imageAvatar: createImageAvatarStyle(options), + initialsAvatar: createInitialsAvatarStyle(options), microphoneButton: createMicrophoneButtonStyle(options), options: { ...options, diff --git a/packages/component/src/Styles/defaultStyleOptions.js b/packages/component/src/Styles/defaultStyleOptions.js index de0f6407e8..c752270c09 100644 --- a/packages/component/src/Styles/defaultStyleOptions.js +++ b/packages/component/src/Styles/defaultStyleOptions.js @@ -26,6 +26,7 @@ const DEFAULT_OPTIONS = { primaryFont: fontFamily(['Calibri', 'Helvetica Neue', 'Arial', 'sans-serif']), // Avatar + avatarBorderRadius: '50%', avatarSize: 40, botAvatarBackgroundColor: undefined, // defaults to accent color botAvatarImage: '', diff --git a/packages/component/src/Utils/addTargetBlankToHyperlinksMarkdown.spec.js b/packages/component/src/Utils/addTargetBlankToHyperlinksMarkdown.spec.js index 3c7fcab57b..2076803152 100644 --- a/packages/component/src/Utils/addTargetBlankToHyperlinksMarkdown.spec.js +++ b/packages/component/src/Utils/addTargetBlankToHyperlinksMarkdown.spec.js @@ -1,7 +1,12 @@ +// TODO: [P4] Object.fromEntries is not on Node.js 11.* +// If all devs are on Node.js >= 12.0, we can remove "core-js" +import fromEntries from 'core-js/features/object/from-entries'; import MarkdownIt from 'markdown-it'; import addTargetBlankToHyperlinksMarkdown from './addTargetBlankToHyperlinksMarkdown'; +Object.fromEntries = fromEntries; + test('add to external links', () => { const markdownIt = new MarkdownIt(); const markdown = 'Hello, [Microsoft](https://microsoft.com/)!'; diff --git a/packages/component/src/Utils/updateMarkdownAttrs.spec.js b/packages/component/src/Utils/updateMarkdownAttrs.spec.js index b00a70eb20..18c414de72 100644 --- a/packages/component/src/Utils/updateMarkdownAttrs.spec.js +++ b/packages/component/src/Utils/updateMarkdownAttrs.spec.js @@ -1,9 +1,11 @@ // TODO: [P4] Object.fromEntries is not on Node.js 11.* // If all devs are on Node.js >= 12.0, we can remove "core-js" -import 'core-js/features/object/from-entries'; +import fromEntries from 'core-js/features/object/from-entries'; import updateMarkdownAttrs from './updateMarkdownAttrs'; +Object.fromEntries = fromEntries; + test('add "rel" and "target" attributes', () => { const token = { attrs: [['href', 'https://example.org/']] diff --git a/packages/component/src/hooks/index.js b/packages/component/src/hooks/index.js index 34eded60ef..18050f3e9f 100644 --- a/packages/component/src/hooks/index.js +++ b/packages/component/src/hooks/index.js @@ -28,6 +28,7 @@ import useRelativeTimeFormatter from './useRelativeTimeFormatter'; import useRenderActivity from './useRenderActivity'; import useRenderActivityStatus from './useRenderActivityStatus'; import useRenderAttachment from './useRenderAttachment'; +import useRenderAvatar from './useRenderAvatar'; import useRenderMarkdownAsHTML from './useRenderMarkdownAsHTML'; import useRenderToast from './useRenderToast'; import useRenderTypingIndicator from './useRenderTypingIndicator'; @@ -96,6 +97,7 @@ export { useRenderActivity, useRenderActivityStatus, useRenderAttachment, + useRenderAvatar, useRenderMarkdownAsHTML, useRenderToast, useRenderTypingIndicator, diff --git a/packages/component/src/hooks/useRenderAvatar.js b/packages/component/src/hooks/useRenderAvatar.js new file mode 100644 index 0000000000..b9b5c09063 --- /dev/null +++ b/packages/component/src/hooks/useRenderAvatar.js @@ -0,0 +1,27 @@ +import { useMemo } from 'react'; + +import useStyleOptions from './useStyleOptions'; +import useWebChatUIContext from './internal/useWebChatUIContext'; + +export default function useRenderAvatar({ activity }) { + const [styleOptions] = useStyleOptions(); + const { avatarRenderer } = useWebChatUIContext(); + + return useMemo(() => { + const { from: { role } = {} } = activity; + + const fromUser = role === 'user'; + + const result = avatarRenderer({ activity, fromUser, styleOptions }); + + if (result !== false && typeof result !== 'function') { + console.warn( + 'botframework-webchat: avatarMiddleware should return a function to render the avatar, or return false if avatar should be hidden.' + ); + + return () => result; + } + + return result; + }, [activity, avatarRenderer, styleOptions]); +} diff --git a/samples/05.custom-components/k.per-message-avatar/README.md b/samples/05.custom-components/k.per-message-avatar/README.md new file mode 100644 index 0000000000..fbd8e9da4b --- /dev/null +++ b/samples/05.custom-components/k.per-message-avatar/README.md @@ -0,0 +1,375 @@ +# Sample - Customize per-message avatar + +This sample shows how to customize avatar on a per-message basis. + +# Test out the hosted sample + +- [Try out MockBot](https://microsoft.github.io/BotFramework-WebChat/05.custom-components/k.per-message-avatar) +- [Try out a comprehensive live demo](https://microsoft.github.io/BotFramework-WebChat/05.custom-components/k.per-message-avatar/comprehensive.html) + +# How to run + +- Fork this repository +- Navigate to `/Your-Local-WebChat/samples/05.custom-components/a.per-message-avatar` in command line +- Run `npx serve` +- Browse to [http://localhost:5000/](http://localhost:5000/) + +# Things to try out + +- Watch the messages appear from Mock Bot. + +# Code + +> Jump to [completed code](#completed-code) to see the end-result `index.html`. + +## Overview + +> This sample is based on the [01.getting-started/e.host-with-react](https://github.com/microsoft/BotFramework-Webchat/tree/master/samples/01.getting-started/e.host-with-react). + +This sample is separated into 2 phases: + +- Adding online status +- Rendering a custom avatar + +### Creating an online status component + +First, create a React component, `` that will decorate its children with an online status icon. + +The component supports two online status: `"online"` and `"busy"`. + +```diff + const res = await fetch('https://webchat-mockbot.azurewebsites.net/directline/token', { method: 'POST' }); + const { token } = await res.json(); + const { ReactWebChat } = window.WebChat; + ++ const AvatarWithOnlineStatus = ({ children, onlineStatus }) => { ++ return ( ++
++ {children} ++ {onlineStatus && ( ++
++ )} ++
++ ); ++ }; + + window.ReactDOM.render( + , + document.getElementById('webchat') + ); +``` + +Then, add the stylesheet for the online status icon: + +```css +.app__avatarWithOnlineStatus { + display: flex; + position: relative; +} + +.app__avatarWithOnlineStatus .app__avatarWithOnlineStatus__status { + background-color: White; + border-radius: 50%; + border: solid 2px White; + bottom: -2px; + height: 10px; + position: absolute; + right: -2px; + transition: background-color 200ms; + width: 10px; +} + +.app__avatarWithOnlineStatus .app__avatarWithOnlineStatus__status.app__avatarWithOnlineStatus__status--online { + background-color: #090; +} + +.app__avatarWithOnlineStatus .app__avatarWithOnlineStatus__status.app__avatarWithOnlineStatus__status--busy { + background-color: Red; +} +``` + +### Create an avatar middleware + +Then, creates an avatar middleware that will render `` on top of the original avatar. + +`avatarMiddleware` should only return a function or `false`. If the avatar is hidden, it should return `false`. Otherwise, it should return a function, which when called, will render the avatar. + +In the following sample, `renderAvatar` is the original avatar of type `false | () => React.Element`. If the avatar should not be shown, `renderAvatar` will be `false` and the middleware return it. Otherwise, `renderAvatar` will be: `() => React.Element`. The middleware will return `` with the result of calling `renderAvatar()`. + +We will also add `botAvatarInitials` and `userAvatarInitials` as `styleOptions`. + +```diff ++ const avatarMiddleware = () => next => ({ fromUser, ...otherArgs }) => { ++ const renderAvatar = next({ fromUser, ...otherArgs }); ++ ++ return ( ++ renderAvatar && ++ (() => ( ++ ++ {renderAvatar()} ++ ++ )) ++ ); ++ }; + + window.ReactDOM.render( + , + document.getElementById('webchat') + ); +``` + +### (Optional) Making the component aware of right-to-left language + +Make the component RTL aware by using the [`useDirection`](https://github.com/microsoft/BotFramework-WebChat/tree/master/docs/HOOKS.md#usedirection) hook. + +```diff + const res = await fetch('https://webchat-mockbot.azurewebsites.net/directline/token', { method: 'POST' }); + const { token } = await res.json(); +- const { ReactWebChat } = window.WebChat; ++ const { ++ ReactWebChat, ++ hooks: { useDirection } ++ } = window.WebChat; + + const AvatarWithOnlineStatus = ({ children, onlineStatus }) => { ++ const [direction] = useDirection(); + + return ( +-
++
+ {children} + {onlineStatus && ( +
+ )} +
+ ); + }; + + window.ReactDOM.render( + , + document.getElementById('webchat') + ); +``` + +Also, update the stylesheet with RTL awareness. In RTL mode, the online status icon will be shown on the bottom-left hand corner. + +```diff + .app__avatarWithOnlineStatus .app__avatarWithOnlineStatus__status { + background-color: White; + border-radius: 50%; + border: solid 2px White; + bottom: -2px; + height: 10px; + position: absolute; +- right: -2px; + transition: background-color 200ms; + width: 10px; + } + ++ .app__avatarWithOnlineStatus:not(.app__avatarWithOnlineStatus--rtl) .app__avatarWithOnlineStatus__status { ++ right: -2px; ++ } ++ ++ .app__avatarWithOnlineStatus.app__avatarWithOnlineStatus--rtl .app__avatarWithOnlineStatus__status { ++ left: -2px; ++ } + + .app__avatarWithOnlineStatus .app__avatarWithOnlineStatus__status.app__avatarWithOnlineStatus__status--online { + background-color: #090; + } + + .app__avatarWithOnlineStatus .app__avatarWithOnlineStatus__status.app__avatarWithOnlineStatus__status--busy { + background-color: Red; + } +``` + +### Use a portrait avatar + +The second part of this sample will show how to customize the avatar using a rectangular image. + +First, add a `` component, which houses the rectangular avatar image. This component will show different avatar images for bot and user. + +```js +const PortraitAvatar = ({ fromUser }) => { + return ; +}; +``` + +Along with its stylesheet: + +```css +.app__portraitAvatar { + border-radius: 4px; +} +``` + +Lastly, modify the middleware to use the new ``: + +```diff + const avatarMiddleware = () => next => ({ fromUser, ...otherArgs }) => { +- const renderAvatar = next({ fromUser, ...otherArgs }); +- +- return ( +- renderAvatar && +- (() => ( +- +- {renderAvatar()} +- +- )) ++ return () => ( ++ ++ ++ + ); + }; +``` + +## Completed code + +Here is the finished `index.html`: + +```diff + + + + Web Chat: Customizable avatar + + + + + + + + + +
+ + + +``` + +# Further reading + +[Comprehensive live demo for customizable avatar](https://microsoft.github.io/BotFramework-WebChat/05.custom-components/k.per-message-avatar/comprehensive.html) + +[Demonstrates how to display initials for both Web Chat participants](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/02.branding-styling-and-customization/c.display-sender-initials) + +[Demonstrates how to display images and initials for both Web Chat participants](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/02.branding-styling-and-customization/d.display-sender-images) + +## Full list of Web Chat hosted samples + +View the list of [available Web Chat samples](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples) diff --git a/samples/05.custom-components/k.per-message-avatar/bot.jpg b/samples/05.custom-components/k.per-message-avatar/bot.jpg new file mode 100644 index 0000000000..76c164eefb Binary files /dev/null and b/samples/05.custom-components/k.per-message-avatar/bot.jpg differ diff --git a/samples/05.custom-components/k.per-message-avatar/comprehensive.html b/samples/05.custom-components/k.per-message-avatar/comprehensive.html new file mode 100644 index 0000000000..afa2c70af5 --- /dev/null +++ b/samples/05.custom-components/k.per-message-avatar/comprehensive.html @@ -0,0 +1,425 @@ + + + + Web Chat: Customizable avatar + + + + + + + + + + + + + + + +
+ + + diff --git a/samples/05.custom-components/k.per-message-avatar/index.html b/samples/05.custom-components/k.per-message-avatar/index.html new file mode 100644 index 0000000000..3f0d6cb06c --- /dev/null +++ b/samples/05.custom-components/k.per-message-avatar/index.html @@ -0,0 +1,139 @@ + + + + Web Chat: Customizable avatar + + + + + + + + + + + + + + + + +
+ + + diff --git a/samples/05.custom-components/k.per-message-avatar/user.jpg b/samples/05.custom-components/k.per-message-avatar/user.jpg new file mode 100644 index 0000000000..de73dcdaef Binary files /dev/null and b/samples/05.custom-components/k.per-message-avatar/user.jpg differ diff --git a/samples/README.md b/samples/README.md index 59a19e09f6..fbe5032bab 100644 --- a/samples/README.md +++ b/samples/README.md @@ -6,67 +6,68 @@ Here you can find all hosted samples of [Web Chat](https://github.com/microsoft/ # Samples list -|                Sample Name                     | Description | Link | -| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | -| **Migration** | | | -| [`00.migration/a.v3-to-v4`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/00.migration/a.v3-to-v4) | Demonstrates how to migrate from your Web Chat v3 bot to v4. | [Migration Demo](https://microsoft.github.io/BotFramework-WebChat/00.migration/a.v3-to-v4) | -| **Getting started** | | | -| [`01.getting-started/a.full-bundle`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/01.getting-started/a.full-bundle) | Introduces Web Chat embed from a CDN, and demonstrates a simple, full-featured Web Chat. This includes Adaptive Cards, Cognitive Services, and Markdown-It dependencies. | [Full Bundle Demo](https://microsoft.github.io/BotFramework-WebChat/01.getting-started/a.full-bundle) | -| [`01.getting-started/b.minimal-bundle`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/01.getting-started/b.minimal-bundle) | Introduces the minimized CDN with only basic dependencies. This does NOT include Adaptive Cards, Cognitive Services dependencies, or Markdown-It dependencies. | [Minimal Bundle Demo](https://microsoft.github.io/BotFramework-WebChat/01.getting-started/b.minimal-bundle) | -| [`01.getting-started/c.es5-bundle`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/01.getting-started/c.es5-bundle) | Introduces full-featured Web Chat embed with backwards compatibility for ES5 browsers using Web Chat's ES5 ponyfill. | [ES5 Bundle Demo](https://microsoft.github.io/BotFramework-WebChat/01.getting-started/c.es5-bundle) | -| [`01.getting-started/d.es5-direct-line-speech`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/01.getting-started/d.es5-direct-line-speech) | Demonstrates how to use Direct Line Speech with ES5 bundle. | [ES5 Direct Line Speech Demo](https://microsoft.github.io/BotFramework-WebChat/01.getting-started/d.es5-direct-line-speech) | -| [`01.getting-started/e.host-with-react`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/01.getting-started/e.host-with-react) | Demonstrates how to create a React component that hosts the full-featured Web Chat. | [Host with React Demo](https://microsoft.github.io/BotFramework-WebChat/01.getting-started/e.host-with-react) | -| [`01.getting-started/f.host-with-angular`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/01.getting-started/f.host-with-angular) | Demonstrates how to create an Angular component that hosts the full-featured Web Chat. | [Host with Angular Demo](https://stackblitz.com/github/omarsourour/ng-webchat-example) | -| [`01.getting-started/g.hybrid-react-npm`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/01.getting-started/g.hybrid-react-npm) | Demonstrates how to use different versions of React on a hosting app via NPM packages | [Hybrid React Demo](https://microsoft.github.io/BotFramework-WebChat/01.getting-started/g.hybrid-react-npm) | -| [`01.getting-started/h.minimal-markdown`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/01.getting-started/h.minimal-markdown) | Demonstrates how to add the CDN for Markdown-It dependency on top of the minimal bundle. | [Minimal with Markdown Demo](https://microsoft.github.io/BotFramework-WebChat/01.getting-started/h.minimal-markdown) | -| **Branding, styling, and customization** | | | -| [`02.branding-styling-and-customization/a.branding-web-chat`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/02.branding-styling-and-customization/a.branding-web-chat) | Introduces the ability to style Web Chat to match your brand. This method of custom styling will not break upon Web Chat updates. | [Branding Web Chat Demo](https://microsoft.github.io/BotFramework-WebChat/02.branding-styling-and-customization/a.branding-web-chat) | -| [`02.branding-styling-and-customization/b.idiosyncratic-manual-styles`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/02.branding-styling-and-customization/b.idiosyncratic-manual-styles) | Demonstrates how to make manual style changes, and is a more complicated and time-consuming way to customize styling of Web Chat. Manual styles may be broken upon Web Chat updates. | [Idiosyncratic Styling Demo](https://microsoft.github.io/BotFramework-WebChat/02.branding-styling-and-customization/b.idiosyncratic-manual-styles) | -| [`02.branding-styling-and-customization/c.display-sender-initials`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/02.branding-styling-and-customization/c.display-sender-initials) | Demonstrates how to display initials for both Web Chat participants. | [Bot initials Demo](https://microsoft.github.io/BotFramework-WebChat/02.branding-styling-and-customization/c.display-sender-initials/) | -| [`02.branding-styling-and-customization/d.display-sender-images`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/02.branding-styling-and-customization/d.display-sender-images) | Demonstrates how to display images and initials for both Web Chat participants. | [User images Demo](https://microsoft.github.io/BotFramework-WebChat/02.branding-styling-and-customization/d.display-sender-images) | -| [`02.branding-styling-and-customization/e.presentation-mode`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/02.branding-styling-and-customization/e.presentation-mode) | Demonstrates how to set up Presentation Mode, which displays chat history but does not show the send box, and disables the interactivity of Adaptive Cards. | [Presentation Mode Demo](https://microsoft.github.io/BotFramework-WebChat/02.branding-styling-and-customization/e.presentation-mode) | -| [`02.branding-styling-and-customization/f.hide-upload-button`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/02.branding-styling-and-customization/f.hide-upload-button) | Demonstrates how to hide file upload button via styling. | [Hide Upload Button Demo](https://microsoft.github.io/BotFramework-WebChat/02.branding-styling-and-customization/f.hide-upload-button) | -| [`02.branding-styling-and-customization/g.change-locale`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/02.branding-styling-and-customization/g.change-locale) | Demonstrates how to change locale when an activity is received from the bot. | [Change Locale Demo](https://microsoft.github.io/BotFramework-WebChat/02.branding-styling-and-customization/g.change-locale) | -| [`02.branding-styling-and-customization/h.send-timeout`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/02.branding-styling-and-customization/h.send-timeout) | Demonstrates how to change timeout for outgoing messages. | [Send Timeout Demo](https://microsoft.github.io/BotFramework-WebChat/02.branding-styling-and-customization/h.send-timeout) | -| [`02.branding-styling-and-customization/i.change-locale-and-direction`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/02.branding-styling-and-customization/i.change-locale-and-direction) | Demonstrates how to change locale and direction of the UI (RTL). | [Change Direction Demo](https://microsoft.github.io/BotFramework-WebChat/02.branding-styling-and-customization/i.change-locale-and-direction) | -| **Speech** | | | -| [`03.speech/a.direct-line-speech`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/03.speech/a.direct-line-speech) | Demonstrates how to use Direct Line Speech channel in Web Chat. | [Direct Line Speech Demo](https://microsoft.github.io/BotFramework-WebChat/03.speech/a.direct-line-speech) | -| [`03.speech/b.cognitive-speech-services-js`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/03.speech/b.cognitive-speech-services-js) | Introduces speech-to-text and text-to-speech ability using Cognitive Services Speech Services API. | [Speech Services with JS Demo](https://microsoft.github.io/BotFramework-WebChat/03.speech/b.cognitive-speech-services-js) | -| [`03.speech/c.cognitive-speech-services-with-lexical-result`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/03.speech/c.cognitive-speech-services-with-lexical-result) | Demonstrates how to use lexical result from Cognitive Services Speech Services API. | [Lexical Result Demo](https://microsoft.github.io/BotFramework-WebChat/03.speech/c.cognitive-speech-services-with-lexical-result) | -| [`03.speech/d.cognitive-speech-services-speech-recognition-only`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/03.speech/d.cognitive-speech-services-speech-recognition-only) | Implement Cognitive Speech Services with only Speech Recognition. | [Cognitive Speech: Speech Recognition](https://microsoft.github.io/BotFramework-WebChat/03.speech/d.cognitive-speech-services-speech-recognition-only) | -| [`03.speech/e.select-voice`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/03.speech/e.select-voice) | Demonstrates how to select speech synthesis voice based on activity. | [Select Voice Demo](https://microsoft.github.io/BotFramework-WebChat/03.speech/e.select-voice) | -| [`03.speech/f.web-browser-speech`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/03.speech/f.web-browser-speech) | Demonstrates how to implement text-to-speech using Web Chat's browser-based Web Speech API. (link to W3C standard in the sample) | [Web Speech API Demo](https://microsoft.github.io/BotFramework-WebChat/03.speech/f.web-browser-speech) | -| [`03.speech/g.hybrid-speech`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/03.speech/g.hybrid-speech) | Demonstrates how to use both browser-based Web Speech API for speech-to-text, and Cognitive Services Speech Services API for text-to-speech. | [Hybrid Speech Demo](https://microsoft.github.io/BotFramework-WebChat/03.speech/g.hybrid-speech) | -| **API** | | | -| [`04.api/a.welcome-event`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/04.api/a.welcome-event) | Advanced tutorial: Demonstrates how to send welcome event with client capabilities such as browser language. | [Welcome Event Demo](https://microsoft.github.io/BotFramework-WebChat/04.api/a.welcome-event) | -| [`04.api/b.piggyback-on-outgoing-activities`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/04.api/b.piggyback-on-outgoing-activities) | Advanced tutorial: Demonstrates how to add custom data to every outgoing activities. | [Backchannel Piggybacking Demo](https://microsoft.github.io/BotFramework-WebChat/04.api/b.piggyback-on-outgoing-activities) | -| [`04.api/c.incoming-activity-event`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/04.api/c.incoming-activity-event) | Advanced tutorial: Demonstrates how to forward all incoming activities to a JavaScript event for further processing. | [Incoming Activity Demo](https://microsoft.github.io/BotFramework-WebChat/04.api/c.incoming-activity-event) | -| [`04.api/d.post-activity-event`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/04.api/d.post-activity-event) | Advanced tutorial: Demonstrates how to send a message programmatically. | [Programmatic Posting Demo](https://microsoft.github.io/BotFramework-WebChat/04.api/d.post-activity-event) | -| [`04.api/e.piping-to-redux`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/04.api/e.piping-to-redux) | Advanced tutorial: Demonstrates how to pipe bot activities to your own Redux store and use your bot to control your page through bot activities and Redux. | [Piping to Redux Demo](https://microsoft.github.io/BotFramework-WebChat/04.api/e.piping-to-redux) | -| [`04.api/f.selectable-activity`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/04.api/f.selectable-activity) | Advanced tutorial: Demonstrates how to add custom click behavior to each activity. | [Selectable Activity Demo](https://microsoft.github.io/BotFramework-WebChat/04.api/f.selectable-activity) | -| [`04.api/g.chat-send-history`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/04.api/g.chat-send-history) | Advanced tutorial: Demonstrates the ability to save user input and allow the user to step back through previous sent messages. | [Chat Send History Demo](https://microsoft.github.io/BotFramework-WebChat/04.api/g.chat-send-history) | -| [`04.api/h.clear-after-idle`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/04.api/h.clear-after-idle) | Advanced tutorial: Demonstrates how to customize the open URL behavior. | [Clear After Idle Demo](https://microsoft.github.io/BotFramework-WebChat/04.api/h.clear-after-idle) | -| [`04.api/i.open-url`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/04.api/i.open-url) | Advanced tutorial: Demonstrates how to customize the open URL behavior. | [Customize Open URL Demo](https://microsoft.github.io/BotFramework-WebChat/04.api/i.open-url) | -| [`04.api/j.redux-actions`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/04.api/j.redux-actions) | Advanced tutorial: Demonstrates how to incorporate redux middleware into your Web Chat app by sending redux actions through the bot. This example demonstrates manual styling based on activities between bot and user. | [Redux Actions Demo](https://microsoft.github.io/BotFramework-WebChat/04.api/j.redux-actions) | -| [`04.api/k.telemetry-application-insights`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/04.api/k.telemetry-application-insights) | Advanced tutorial: Demonstrates how to collect telemetry measurement using Azure Application Insights. | [Telemetry using Application Insights Demo](https://microsoft.github.io/BotFramework-WebChat/04.api/k.telemetry-application-insights) | -| [`04.api/l.telemetry-google-analytics`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/04.api/k.telemetry-google-analytics) | Advanced tutorial: Demonstrates how to collect telemetry measurement using Google Analytics. | [Telemetry using Google Analytics Demo](https://microsoft.github.io/BotFramework-WebChat/04.api/l.telemetry-google-analytics) | -| **Custom components** | | | -| [`05.custom-components/a.timestamp-grouping`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/05.custom-components/a.timestamp-grouping) | Demonstrates how to customize timestamps by showing or hiding timestamps and changing the grouping of messages by time. | [Timestamp Grouping Demo](https://microsoft.github.io/BotFramework-WebChat/05.custom-components/a.timestamp-grouping) | -| [`05.custom-components/b.send-typing-indicator`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/05.custom-components/b.send-typing-indicator) | Demonstrates how to send typing activity when the user start typing on the send box. | [User Typing Indicator Demo](https://microsoft.github.io/BotFramework-WebChat/05.custom-components/b.send-typing-indicator) | -| [`05.custom-components/c.user-highlighting`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/05.custom-components/c.user-highlighting) | Demonstrates how to customize the styling of activities based whether the message is from the user or the bot. | [User Highlighting Demo](https://microsoft.github.io/BotFramework-WebChat/05.custom-components/c.user-highlighting) | -| [`05.custom-components/d.reaction-buttons`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/05.custom-components/d.reaction-buttons/) | Introduces the ability to create custom components for Web Chat that are unique to your bot's needs. This tutorial demonstrates the ability to add reaction emoji such as :thumbsup: and :thumbsdown: to conversational activities. | [Reaction Buttons Demo](https://microsoft.github.io/BotFramework-WebChat/05.custom-components/d.reaction-buttons) | -| [`05.custom-components/e.card-components`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/05.custom-components/e.card-components) | Demonstrates how to create custom activity card attachments, in this case GitHub repository cards. | [Card Components Demo](https://microsoft.github.io/BotFramework-WebChat/05.custom-components/e.card-components) | -| [`05.custom-components/f.password-input`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/05.custom-components/f.password-input) | Demonstrates how to create custom activity for password input. | [Password Input Demo](https://microsoft.github.io/BotFramework-WebChat/05.custom-components/f.password-input) | -| [`05.custom-components/g.activity-status`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/05.custom-components/g.activity-status/) | Demonstrates how to customize the activity status by including sender's name. | [Customize Activity Status Demo](https://microsoft.github.io/BotFramework-WebChat/05.custom-components/g.activity-status) | -| [`05.custom-components/i.notification`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/05.custom-components/i.notification/) | Demonstrates how to use notification and customize the toast UI. | [Notification Demo](https://microsoft.github.io/BotFramework-WebChat/05.custom-components/i.notification) | -| [`05.custom-components/j.typing-indicator`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/05.custom-components/j.typing-indicator/) | Demonstrates how to customize the typing indicator. | [Customize Typing Indicator Demo](https://microsoft.github.io/BotFramework-WebChat/05.custom-components/j.typing-indicator) | -| **Recomposing UI** | | | -| [`06.recomposing-ui/a.minimizable-web-chat`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/06.recomposing-ui/a.minimizable-web-chat) | Advanced tutorial: Demonstrates how to add the Web Chat interface to your website as a minimizable show/hide chat box. | [Minimizable Web Chat Demo](https://microsoft.github.io/BotFramework-WebChat/06.recomposing-ui/a.minimizable-web-chat) | -| [`06.recomposing-ui/b.speech-ui`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/06.recomposing-ui/b.speech-ui) | Advanced tutorial: Demonstrates how to fully customize key components of your bot, in this case speech, which entirely replaces the text-based transcript UI and instead shows a simple speech button with the bot's response. | [Speech UI Demo](https://microsoft.github.io/BotFramework-WebChat/06.recomposing-ui/b.speech-ui) | -| [`06.recomposing-ui/c.smart-display`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/06.recomposing-ui/c.smart-display) | Demonstrates how to compose Web Chat UI into a Smart Display | [Smart Display Demo](https://microsoft.github.io/BotFramework-WebChat/06.recomposing-ui/c.smart-display) | -| [`06.recomposing-ui/d.plain-ui`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/06.recomposing-ui/d.plain-ui) | Advanced tutorial: Demonstrates how to customize the Web Chat UI by building from ground up instead of needing to rewrite entire Web Chat components. | [Plain UI Demo](https://microsoft.github.io/BotFramework-WebChat/06.recomposing-ui/d.plain-ui) | -| **Advanced Web Chat apps** | | | -| [`07.advanced-web-chat-apps/a.upload-to-azure-storage`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/07.advanced-web-chat-apps/a.upload-to-azure-storage) | Demonstrates how to use upload attachments directly to Azure Storage | [Upload to Azure Storage Demo](https://webchat-sample-upload-to-azure.azurewebsites.net/) | -| [`07.advanced-web-chat-apps/b.sso-for-enterprise`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/07.advanced-web-chat-apps/b.sso-for-enterprise) | Demonstrates how to use single sign-on for enterprise single-page applications using OAuth | [Single Sign-On for Enterprise Single-Page Applications Demo](https://webchat-sample-sso.azurewebsites.net/) | -| [`07.advanced-web-chat-apps/c.sso-for-intranet`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/07.advanced-web-chat-apps/c.sso-for-intranet) | Demonstrates how to use single sign-on for Intranet apps using Azure Active Directory | [Single Sign-On for Intranet Apps Demo](https://webchat-sample-sso-intranet.azurewebsites.net/) | -| [`07.advanced-web-chat-apps/d.sso-for-teams`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/07.advanced-web-chat-apps/d.sso-for-teams) | Demonstrates how to use single sign-on for Microsoft Teams apps using Azure Active Directory | [Single Sign-On for Microsoft Teams Apps Demo](https://webchat-sample-sso-teams.azurewebsites.net/) | +|                Sample Name                     | Description | Link | +| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Migration** | | | +| [`00.migration/a.v3-to-v4`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/00.migration/a.v3-to-v4) | Demonstrates how to migrate from your Web Chat v3 bot to v4. | [Migration Demo](https://microsoft.github.io/BotFramework-WebChat/00.migration/a.v3-to-v4) | +| **Getting started** | | | +| [`01.getting-started/a.full-bundle`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/01.getting-started/a.full-bundle) | Introduces Web Chat embed from a CDN, and demonstrates a simple, full-featured Web Chat. This includes Adaptive Cards, Cognitive Services, and Markdown-It dependencies. | [Full Bundle Demo](https://microsoft.github.io/BotFramework-WebChat/01.getting-started/a.full-bundle) | +| [`01.getting-started/b.minimal-bundle`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/01.getting-started/b.minimal-bundle) | Introduces the minimized CDN with only basic dependencies. This does NOT include Adaptive Cards, Cognitive Services dependencies, or Markdown-It dependencies. | [Minimal Bundle Demo](https://microsoft.github.io/BotFramework-WebChat/01.getting-started/b.minimal-bundle) | +| [`01.getting-started/c.es5-bundle`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/01.getting-started/c.es5-bundle) | Introduces full-featured Web Chat embed with backwards compatibility for ES5 browsers using Web Chat's ES5 ponyfill. | [ES5 Bundle Demo](https://microsoft.github.io/BotFramework-WebChat/01.getting-started/c.es5-bundle) | +| [`01.getting-started/d.es5-direct-line-speech`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/01.getting-started/d.es5-direct-line-speech) | Demonstrates how to use Direct Line Speech with ES5 bundle. | [ES5 Direct Line Speech Demo](https://microsoft.github.io/BotFramework-WebChat/01.getting-started/d.es5-direct-line-speech) | +| [`01.getting-started/e.host-with-react`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/01.getting-started/e.host-with-react) | Demonstrates how to create a React component that hosts the full-featured Web Chat. | [Host with React Demo](https://microsoft.github.io/BotFramework-WebChat/01.getting-started/e.host-with-react) | +| [`01.getting-started/f.host-with-angular`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/01.getting-started/f.host-with-angular) | Demonstrates how to create an Angular component that hosts the full-featured Web Chat. | [Host with Angular Demo](https://stackblitz.com/github/omarsourour/ng-webchat-example) | +| [`01.getting-started/g.hybrid-react-npm`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/01.getting-started/g.hybrid-react-npm) | Demonstrates how to use different versions of React on a hosting app via NPM packages | [Hybrid React Demo](https://microsoft.github.io/BotFramework-WebChat/01.getting-started/g.hybrid-react-npm) | +| [`01.getting-started/h.minimal-markdown`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/01.getting-started/h.minimal-markdown) | Demonstrates how to add the CDN for Markdown-It dependency on top of the minimal bundle. | [Minimal with Markdown Demo](https://microsoft.github.io/BotFramework-WebChat/01.getting-started/h.minimal-markdown) | +| **Branding, styling, and customization** | | | +| [`02.branding-styling-and-customization/a.branding-web-chat`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/02.branding-styling-and-customization/a.branding-web-chat) | Introduces the ability to style Web Chat to match your brand. This method of custom styling will not break upon Web Chat updates. | [Branding Web Chat Demo](https://microsoft.github.io/BotFramework-WebChat/02.branding-styling-and-customization/a.branding-web-chat) | +| [`02.branding-styling-and-customization/b.idiosyncratic-manual-styles`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/02.branding-styling-and-customization/b.idiosyncratic-manual-styles) | Demonstrates how to make manual style changes, and is a more complicated and time-consuming way to customize styling of Web Chat. Manual styles may be broken upon Web Chat updates. | [Idiosyncratic Styling Demo](https://microsoft.github.io/BotFramework-WebChat/02.branding-styling-and-customization/b.idiosyncratic-manual-styles) | +| [`02.branding-styling-and-customization/c.display-sender-initials`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/02.branding-styling-and-customization/c.display-sender-initials) | Demonstrates how to display initials for both Web Chat participants. | [Bot initials Demo](https://microsoft.github.io/BotFramework-WebChat/02.branding-styling-and-customization/c.display-sender-initials/) | +| [`02.branding-styling-and-customization/d.display-sender-images`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/02.branding-styling-and-customization/d.display-sender-images) | Demonstrates how to display images and initials for both Web Chat participants. | [User images Demo](https://microsoft.github.io/BotFramework-WebChat/02.branding-styling-and-customization/d.display-sender-images) | +| [`02.branding-styling-and-customization/e.presentation-mode`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/02.branding-styling-and-customization/e.presentation-mode) | Demonstrates how to set up Presentation Mode, which displays chat history but does not show the send box, and disables the interactivity of Adaptive Cards. | [Presentation Mode Demo](https://microsoft.github.io/BotFramework-WebChat/02.branding-styling-and-customization/e.presentation-mode) | +| [`02.branding-styling-and-customization/f.hide-upload-button`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/02.branding-styling-and-customization/f.hide-upload-button) | Demonstrates how to hide file upload button via styling. | [Hide Upload Button Demo](https://microsoft.github.io/BotFramework-WebChat/02.branding-styling-and-customization/f.hide-upload-button) | +| [`02.branding-styling-and-customization/g.change-locale`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/02.branding-styling-and-customization/g.change-locale) | Demonstrates how to change locale when an activity is received from the bot. | [Change Locale Demo](https://microsoft.github.io/BotFramework-WebChat/02.branding-styling-and-customization/g.change-locale) | +| [`02.branding-styling-and-customization/h.send-timeout`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/02.branding-styling-and-customization/h.send-timeout) | Demonstrates how to change timeout for outgoing messages. | [Send Timeout Demo](https://microsoft.github.io/BotFramework-WebChat/02.branding-styling-and-customization/h.send-timeout) | +| [`02.branding-styling-and-customization/i.change-locale-and-direction`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/02.branding-styling-and-customization/i.change-locale-and-direction) | Demonstrates how to change locale and direction of the UI (RTL). | [Change Direction Demo](https://microsoft.github.io/BotFramework-WebChat/02.branding-styling-and-customization/i.change-locale-and-direction) | +| **Speech** | | | +| [`03.speech/a.direct-line-speech`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/03.speech/a.direct-line-speech) | Demonstrates how to use Direct Line Speech channel in Web Chat. | [Direct Line Speech Demo](https://microsoft.github.io/BotFramework-WebChat/03.speech/a.direct-line-speech) | +| [`03.speech/b.cognitive-speech-services-js`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/03.speech/b.cognitive-speech-services-js) | Introduces speech-to-text and text-to-speech ability using Cognitive Services Speech Services API. | [Speech Services with JS Demo](https://microsoft.github.io/BotFramework-WebChat/03.speech/b.cognitive-speech-services-js) | +| [`03.speech/c.cognitive-speech-services-with-lexical-result`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/03.speech/c.cognitive-speech-services-with-lexical-result) | Demonstrates how to use lexical result from Cognitive Services Speech Services API. | [Lexical Result Demo](https://microsoft.github.io/BotFramework-WebChat/03.speech/c.cognitive-speech-services-with-lexical-result) | +| [`03.speech/d.cognitive-speech-services-speech-recognition-only`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/03.speech/d.cognitive-speech-services-speech-recognition-only) | Implement Cognitive Speech Services with only Speech Recognition. | [Cognitive Speech: Speech Recognition](https://microsoft.github.io/BotFramework-WebChat/03.speech/d.cognitive-speech-services-speech-recognition-only) | +| [`03.speech/e.select-voice`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/03.speech/e.select-voice) | Demonstrates how to select speech synthesis voice based on activity. | [Select Voice Demo](https://microsoft.github.io/BotFramework-WebChat/03.speech/e.select-voice) | +| [`03.speech/f.web-browser-speech`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/03.speech/f.web-browser-speech) | Demonstrates how to implement text-to-speech using Web Chat's browser-based Web Speech API. (link to W3C standard in the sample) | [Web Speech API Demo](https://microsoft.github.io/BotFramework-WebChat/03.speech/f.web-browser-speech) | +| [`03.speech/g.hybrid-speech`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/03.speech/g.hybrid-speech) | Demonstrates how to use both browser-based Web Speech API for speech-to-text, and Cognitive Services Speech Services API for text-to-speech. | [Hybrid Speech Demo](https://microsoft.github.io/BotFramework-WebChat/03.speech/g.hybrid-speech) | +| **API** | | | +| [`04.api/a.welcome-event`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/04.api/a.welcome-event) | Advanced tutorial: Demonstrates how to send welcome event with client capabilities such as browser language. | [Welcome Event Demo](https://microsoft.github.io/BotFramework-WebChat/04.api/a.welcome-event) | +| [`04.api/b.piggyback-on-outgoing-activities`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/04.api/b.piggyback-on-outgoing-activities) | Advanced tutorial: Demonstrates how to add custom data to every outgoing activities. | [Backchannel Piggybacking Demo](https://microsoft.github.io/BotFramework-WebChat/04.api/b.piggyback-on-outgoing-activities) | +| [`04.api/c.incoming-activity-event`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/04.api/c.incoming-activity-event) | Advanced tutorial: Demonstrates how to forward all incoming activities to a JavaScript event for further processing. | [Incoming Activity Demo](https://microsoft.github.io/BotFramework-WebChat/04.api/c.incoming-activity-event) | +| [`04.api/d.post-activity-event`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/04.api/d.post-activity-event) | Advanced tutorial: Demonstrates how to send a message programmatically. | [Programmatic Posting Demo](https://microsoft.github.io/BotFramework-WebChat/04.api/d.post-activity-event) | +| [`04.api/e.piping-to-redux`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/04.api/e.piping-to-redux) | Advanced tutorial: Demonstrates how to pipe bot activities to your own Redux store and use your bot to control your page through bot activities and Redux. | [Piping to Redux Demo](https://microsoft.github.io/BotFramework-WebChat/04.api/e.piping-to-redux) | +| [`04.api/f.selectable-activity`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/04.api/f.selectable-activity) | Advanced tutorial: Demonstrates how to add custom click behavior to each activity. | [Selectable Activity Demo](https://microsoft.github.io/BotFramework-WebChat/04.api/f.selectable-activity) | +| [`04.api/g.chat-send-history`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/04.api/g.chat-send-history) | Advanced tutorial: Demonstrates the ability to save user input and allow the user to step back through previous sent messages. | [Chat Send History Demo](https://microsoft.github.io/BotFramework-WebChat/04.api/g.chat-send-history) | +| [`04.api/h.clear-after-idle`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/04.api/h.clear-after-idle) | Advanced tutorial: Demonstrates how to customize the open URL behavior. | [Clear After Idle Demo](https://microsoft.github.io/BotFramework-WebChat/04.api/h.clear-after-idle) | +| [`04.api/i.open-url`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/04.api/i.open-url) | Advanced tutorial: Demonstrates how to customize the open URL behavior. | [Customize Open URL Demo](https://microsoft.github.io/BotFramework-WebChat/04.api/i.open-url) | +| [`04.api/j.redux-actions`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/04.api/j.redux-actions) | Advanced tutorial: Demonstrates how to incorporate redux middleware into your Web Chat app by sending redux actions through the bot. This example demonstrates manual styling based on activities between bot and user. | [Redux Actions Demo](https://microsoft.github.io/BotFramework-WebChat/04.api/j.redux-actions) | +| [`04.api/k.telemetry-application-insights`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/04.api/k.telemetry-application-insights) | Advanced tutorial: Demonstrates how to collect telemetry measurement using Azure Application Insights. | [Telemetry using Application Insights Demo](https://microsoft.github.io/BotFramework-WebChat/04.api/k.telemetry-application-insights) | +| [`04.api/l.telemetry-google-analytics`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/04.api/k.telemetry-google-analytics) | Advanced tutorial: Demonstrates how to collect telemetry measurement using Google Analytics. | [Telemetry using Google Analytics Demo](https://microsoft.github.io/BotFramework-WebChat/04.api/l.telemetry-google-analytics) | +| **Custom components** | | | +| [`05.custom-components/a.timestamp-grouping`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/05.custom-components/a.timestamp-grouping) | Demonstrates how to customize timestamps by showing or hiding timestamps and changing the grouping of messages by time. | [Timestamp Grouping Demo](https://microsoft.github.io/BotFramework-WebChat/05.custom-components/a.timestamp-grouping) | +| [`05.custom-components/b.send-typing-indicator`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/05.custom-components/b.send-typing-indicator) | Demonstrates how to send typing activity when the user start typing on the send box. | [User Typing Indicator Demo](https://microsoft.github.io/BotFramework-WebChat/05.custom-components/b.send-typing-indicator) | +| [`05.custom-components/c.user-highlighting`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/05.custom-components/c.user-highlighting) | Demonstrates how to customize the styling of activities based whether the message is from the user or the bot. | [User Highlighting Demo](https://microsoft.github.io/BotFramework-WebChat/05.custom-components/c.user-highlighting) | +| [`05.custom-components/d.reaction-buttons`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/05.custom-components/d.reaction-buttons/) | Introduces the ability to create custom components for Web Chat that are unique to your bot's needs. This tutorial demonstrates the ability to add reaction emoji such as :thumbsup: and :thumbsdown: to conversational activities. | [Reaction Buttons Demo](https://microsoft.github.io/BotFramework-WebChat/05.custom-components/d.reaction-buttons) | +| [`05.custom-components/e.card-components`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/05.custom-components/e.card-components) | Demonstrates how to create custom activity card attachments, in this case GitHub repository cards. | [Card Components Demo](https://microsoft.github.io/BotFramework-WebChat/05.custom-components/e.card-components) | +| [`05.custom-components/f.password-input`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/05.custom-components/f.password-input) | Demonstrates how to create custom activity for password input. | [Password Input Demo](https://microsoft.github.io/BotFramework-WebChat/05.custom-components/f.password-input) | +| [`05.custom-components/g.activity-status`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/05.custom-components/g.activity-status/) | Demonstrates how to customize the activity status by including sender's name. | [Customize Activity Status Demo](https://microsoft.github.io/BotFramework-WebChat/05.custom-components/g.activity-status) | +| [`05.custom-components/i.notification`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/05.custom-components/i.notification/) | Demonstrates how to use notification and customize the toast UI. | [Notification Demo](https://microsoft.github.io/BotFramework-WebChat/05.custom-components/i.notification) | +| [`05.custom-components/j.typing-indicator`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/05.custom-components/j.typing-indicator/) | Demonstrates how to customize the typing indicator. | [Customize Typing Indicator Demo](https://microsoft.github.io/BotFramework-WebChat/05.custom-components/j.typing-indicator) | +| [`05.custom-components/k.per-message-avatar`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/05.custom-components/k.per-message-avatar/) | Demonstrates how to customize the avatar on a per-message basis. | [Customize Avatar Demo](https://microsoft.github.io/BotFramework-WebChat/05.custom-components/k.per-message-avatar) [(Comprehensive)](https://microsoft.github.io/BotFramework-WebChat/05.custom-components/k.per-message-avatar/comprehensive.html) | +| **Recomposing UI** | | | +| [`06.recomposing-ui/a.minimizable-web-chat`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/06.recomposing-ui/a.minimizable-web-chat) | Advanced tutorial: Demonstrates how to add the Web Chat interface to your website as a minimizable show/hide chat box. | [Minimizable Web Chat Demo](https://microsoft.github.io/BotFramework-WebChat/06.recomposing-ui/a.minimizable-web-chat) | +| [`06.recomposing-ui/b.speech-ui`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/06.recomposing-ui/b.speech-ui) | Advanced tutorial: Demonstrates how to fully customize key components of your bot, in this case speech, which entirely replaces the text-based transcript UI and instead shows a simple speech button with the bot's response. | [Speech UI Demo](https://microsoft.github.io/BotFramework-WebChat/06.recomposing-ui/b.speech-ui) | +| [`06.recomposing-ui/c.smart-display`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/06.recomposing-ui/c.smart-display) | Demonstrates how to compose Web Chat UI into a Smart Display | [Smart Display Demo](https://microsoft.github.io/BotFramework-WebChat/06.recomposing-ui/c.smart-display) | +| [`06.recomposing-ui/d.plain-ui`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/06.recomposing-ui/d.plain-ui) | Advanced tutorial: Demonstrates how to customize the Web Chat UI by building from ground up instead of needing to rewrite entire Web Chat components. | [Plain UI Demo](https://microsoft.github.io/BotFramework-WebChat/06.recomposing-ui/d.plain-ui) | +| **Advanced Web Chat apps** | | | +| [`07.advanced-web-chat-apps/a.upload-to-azure-storage`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/07.advanced-web-chat-apps/a.upload-to-azure-storage) | Demonstrates how to use upload attachments directly to Azure Storage | [Upload to Azure Storage Demo](https://webchat-sample-upload-to-azure.azurewebsites.net/) | +| [`07.advanced-web-chat-apps/b.sso-for-enterprise`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/07.advanced-web-chat-apps/b.sso-for-enterprise) | Demonstrates how to use single sign-on for enterprise single-page applications using OAuth | [Single Sign-On for Enterprise Single-Page Applications Demo](https://webchat-sample-sso.azurewebsites.net/) | +| [`07.advanced-web-chat-apps/c.sso-for-intranet`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/07.advanced-web-chat-apps/c.sso-for-intranet) | Demonstrates how to use single sign-on for Intranet apps using Azure Active Directory | [Single Sign-On for Intranet Apps Demo](https://webchat-sample-sso-intranet.azurewebsites.net/) | +| [`07.advanced-web-chat-apps/d.sso-for-teams`](https://github.com/microsoft/BotFramework-WebChat/tree/master/samples/07.advanced-web-chat-apps/d.sso-for-teams) | Demonstrates how to use single sign-on for Microsoft Teams apps using Azure Active Directory | [Single Sign-On for Microsoft Teams Apps Demo](https://webchat-sample-sso-teams.azurewebsites.net/) |