Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ Notes: web developers are advised to use [`~` (tilde range)](https://github.com/
- Resolved [#2661](https://github.com/microsoft/BotFramework-WebChat/issues/2661) and [#5352](https://github.com/microsoft/BotFramework-WebChat/issues/5352). Added speech recognition continuous mode with barge-in support, in PR [#5426](https://github.com/microsoft/BotFramework-WebChat/pull/5426), by [@RushikeshGavali](https://github.com/RushikeshGavali) and [@compulim](https://github.com/compulim)
- Set `styleOptions.speechRecognitionContinuous` to `true` with a Web Speech API provider with continuous mode support
- Added support of [contentless activity in livestream](https://github.com/microsoft/BotFramework-WebChat/blob/main/docs/LIVESTREAMING.md#scenario-3-interim-activities-with-no-content), in PR [#5430](https://github.com/microsoft/BotFramework-WebChat/pull/5430), by [@compulim](https://github.com/compulim)
- Added sliding dots typing indicator in Fluent theme, in PR [#5447](https://github.com/microsoft/BotFramework-WebChat/pull/5447), by [@compulim](https://github.com/compulim)
- Added sliding dots typing indicator in Fluent theme, in PR [#5447](https://github.com/microsoft/BotFramework-WebChat/pull/5447) and PR [#5448](https://github.com/microsoft/BotFramework-WebChat/pull/5448), by [@compulim](https://github.com/compulim)

### Changed

Expand Down
37 changes: 37 additions & 0 deletions packages/fluent-theme/src/components/assets/AssetComposer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { type ContextOf } from 'botframework-webchat-api';
import React, { memo, useEffect, useMemo, type ReactNode } from 'react';

import { type AssetName } from './AssetName';
import Context from './private/Context';

type ContextType = ContextOf<typeof Context>;

type AssetComposerProps = Readonly<{
children?: ReactNode | undefined;
}>;

const SLIDING_DOTS_SVG_STRING =
'<svg xmlns="http://www.w3.org/2000/svg" width="400" height="20" viewBox="0 0 400 20"><defs><linearGradient id="a" x1="0" x2="100%" y1="0" y2="0" gradientUnits="userSpaceOnUse"><stop offset="0%"><animate attributeName="stop-color" dur="2s" keyTimes="0;0.2;0.33;0.5;0.66;0.8;1" repeatCount="indefinite" values="#ad5ae1;#ad5ae1;#0E94E1;#0E94E1;#669fc2;#669fc2;#ad5ae1"/></stop><stop offset="50%"><animate attributeName="stop-color" dur="2s" keyTimes="0;0.2;0.33;0.5;0.66;0.8;1" repeatCount="indefinite" values="#e9618d;#e9618d;#57AB82;#57AB82;#6377e0;#6377e0;#e9618d"/></stop><stop offset="100%"><animate attributeName="stop-color" dur="2s" keyTimes="0;0.2;0.33;0.5;0.66;0.8;1" repeatCount="indefinite" values="#fd9e5f;#fd9e5f;#C6C225;#C6C225;#9b80ec;#9b80ec;#fd9e5f"/></stop></linearGradient></defs><g fill="url(#a)"><rect height="20" rx="10"><animate attributeName="x" dur="2s" keyTimes="0;0.5;0.66;1" repeatCount="indefinite" values="26;26;0;0"/><animate attributeName="width" dur="2s" keyTimes="0;0.2;0.33;0.5;0.66;1" repeatCount="indefinite" values="20;20;30;30;20;20"/><animate attributeName="opacity" dur="2s" keyTimes="0;0.5;0.66;1" repeatCount="indefinite" values="1;1;0;0"/></rect><rect height="20" rx="10"><animate attributeName="x" dur="2s" keyTimes="0;0.2;0.33;0.5;0.66;0.8;1" repeatCount="indefinite" values="62;62;72;72;26;26;0"/><animate attributeName="width" dur="2s" keyTimes="0;0.2;0.33;0.5;0.66;0.8;1" repeatCount="indefinite" values="104;104;20;20;70;70;20"/><animate attributeName="opacity" dur="2s" keyTimes="0;0.8;1" repeatCount="indefinite" values="1;1;0"/></rect><rect height="20" rx="10"><animate attributeName="x" dur="2s" keyTimes="0;0.2;0.33;0.5;0.66;0.8;1" repeatCount="indefinite" values="182;182;108;108;112;112;26"/><animate attributeName="width" dur="2s" keyTimes="0;0.2;0.33;0.5;0.66;1" repeatCount="indefinite" values="20;20;60;60;20;20"/></rect><rect height="20" rx="10"><animate attributeName="x" dur="2s" keyTimes="0;0.2;0.33;0.5;0.66;0.8;1" repeatCount="indefinite" values="218;218;184;184;148;148;62"/><animate attributeName="width" dur="2s" keyTimes="0;0.2;0.33;0.5;0.66;0.8;1" repeatCount="indefinite" values="60;60;80;80;40;40;104"/></rect><rect height="20" rx="10"><animate attributeName="x" dur="2s" keyTimes="0;0.2;0.33;0.5;0.66;0.8;1" repeatCount="indefinite" values="294;294;280;280;204;204;182"/><animate attributeName="width" dur="2s" keyTimes="0;0.2;0.33;0.5;0.66;0.8;1" repeatCount="indefinite" values="40;40;20;20;80;80;20"/></rect><rect height="20" rx="10"><animate attributeName="x" dur="2s" keyTimes="0;0.2;0.33;0.5;0.66;0.8;1" repeatCount="indefinite" values="350;350;316;316;300;300;218"/><animate attributeName="width" dur="2s" keyTimes="0;0.2;0.33;0.5;0.66;0.8;1" repeatCount="indefinite" values="20;20;60;60;20;20;60"/></rect><rect height="20" rx="10"><animate attributeName="x" dur="2s" keyTimes="0;0.2;0.33;0.5;0.66;0.8;1" repeatCount="indefinite" values="386;386;392;392;336;336;294"/><animate attributeName="width" dur="2s" keyTimes="0;0.5;0.66;1" repeatCount="indefinite" values="20;20;40;40"/><animate attributeName="opacity" dur="2s" keyTimes="0;0.5;0.66;1" repeatCount="indefinite" values="0;0;1;1"/></rect><rect width="20" height="20" rx="10"><animate attributeName="x" dur="2s" keyTimes="0;0.2;0.33;0.5;0.66;0.8;1" repeatCount="indefinite" values="422;422;428;428;392;392;350"/><animate attributeName="opacity" dur="2s" keyTimes="0;0.8;1" repeatCount="indefinite" values="0;0;1"/></rect></g></svg>';

const AssetComposer = memo(({ children }: AssetComposerProps) => {
const slidingDotsURL = useMemo(
() => URL.createObjectURL(new Blob([SLIDING_DOTS_SVG_STRING], { type: 'image/svg+xml' })),
[]
);

useEffect(() => () => URL.revokeObjectURL(slidingDotsURL), [slidingDotsURL]);

const context = useMemo<ContextType>(
() =>
Object.freeze({
urlStateMap: new Map<AssetName, readonly [URL]>([['sliding dots', Object.freeze([new URL(slidingDotsURL)])]])
}),
[slidingDotsURL]
);

return <Context.Provider value={context}>{children}</Context.Provider>;
});

AssetComposer.displayName = 'AssetComposer';

export default AssetComposer;
1 change: 1 addition & 0 deletions packages/fluent-theme/src/components/assets/AssetName.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type AssetName = 'sliding dots';
9 changes: 4 additions & 5 deletions packages/fluent-theme/src/components/assets/SlidingDots.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,15 @@ import { hooks } from 'botframework-webchat-component';
import React, { memo, useCallback, useEffect, useRef } from 'react';
import { useRefFrom } from 'use-ref-from';

import useAssetURL from './private/useAssetURL';

const { useLocalizer, useShouldReduceMotion } = hooks;

type SlidingDotsProps = Readonly<{ className: string }>;

const SLIDING_DOTS_SVG_STRING =
'<svg xmlns="http://www.w3.org/2000/svg" width="400" height="20" viewBox="0 0 400 20"><defs><linearGradient id="a" x1="0" x2="100%" y1="0" y2="0" gradientUnits="userSpaceOnUse"><stop offset="0%"><animate attributeName="stop-color" dur="2s" keyTimes="0;0.2;0.33;0.5;0.66;0.8;1" repeatCount="indefinite" values="#ad5ae1;#ad5ae1;#0E94E1;#0E94E1;#669fc2;#669fc2;#ad5ae1"/></stop><stop offset="50%"><animate attributeName="stop-color" dur="2s" keyTimes="0;0.2;0.33;0.5;0.66;0.8;1" repeatCount="indefinite" values="#e9618d;#e9618d;#57AB82;#57AB82;#6377e0;#6377e0;#e9618d"/></stop><stop offset="100%"><animate attributeName="stop-color" dur="2s" keyTimes="0;0.2;0.33;0.5;0.66;0.8;1" repeatCount="indefinite" values="#fd9e5f;#fd9e5f;#C6C225;#C6C225;#9b80ec;#9b80ec;#fd9e5f"/></stop></linearGradient></defs><g fill="url(#a)"><rect height="20" rx="10"><animate attributeName="x" dur="2s" keyTimes="0;0.5;0.66;1" repeatCount="indefinite" values="26;26;0;0"/><animate attributeName="width" dur="2s" keyTimes="0;0.2;0.33;0.5;0.66;1" repeatCount="indefinite" values="20;20;30;30;20;20"/><animate attributeName="opacity" dur="2s" keyTimes="0;0.5;0.66;1" repeatCount="indefinite" values="1;1;0;0"/></rect><rect height="20" rx="10"><animate attributeName="x" dur="2s" keyTimes="0;0.2;0.33;0.5;0.66;0.8;1" repeatCount="indefinite" values="62;62;72;72;26;26;0"/><animate attributeName="width" dur="2s" keyTimes="0;0.2;0.33;0.5;0.66;0.8;1" repeatCount="indefinite" values="104;104;20;20;70;70;20"/><animate attributeName="opacity" dur="2s" keyTimes="0;0.8;1" repeatCount="indefinite" values="1;1;0"/></rect><rect height="20" rx="10"><animate attributeName="x" dur="2s" keyTimes="0;0.2;0.33;0.5;0.66;0.8;1" repeatCount="indefinite" values="182;182;108;108;112;112;26"/><animate attributeName="width" dur="2s" keyTimes="0;0.2;0.33;0.5;0.66;1" repeatCount="indefinite" values="20;20;60;60;20;20"/></rect><rect height="20" rx="10"><animate attributeName="x" dur="2s" keyTimes="0;0.2;0.33;0.5;0.66;0.8;1" repeatCount="indefinite" values="218;218;184;184;148;148;62"/><animate attributeName="width" dur="2s" keyTimes="0;0.2;0.33;0.5;0.66;0.8;1" repeatCount="indefinite" values="60;60;80;80;40;40;104"/></rect><rect height="20" rx="10"><animate attributeName="x" dur="2s" keyTimes="0;0.2;0.33;0.5;0.66;0.8;1" repeatCount="indefinite" values="294;294;280;280;204;204;182"/><animate attributeName="width" dur="2s" keyTimes="0;0.2;0.33;0.5;0.66;0.8;1" repeatCount="indefinite" values="40;40;20;20;80;80;20"/></rect><rect height="20" rx="10"><animate attributeName="x" dur="2s" keyTimes="0;0.2;0.33;0.5;0.66;0.8;1" repeatCount="indefinite" values="350;350;316;316;300;300;218"/><animate attributeName="width" dur="2s" keyTimes="0;0.2;0.33;0.5;0.66;0.8;1" repeatCount="indefinite" values="20;20;60;60;20;20;60"/></rect><rect height="20" rx="10"><animate attributeName="x" dur="2s" keyTimes="0;0.2;0.33;0.5;0.66;0.8;1" repeatCount="indefinite" values="386;386;392;392;336;336;294"/><animate attributeName="width" dur="2s" keyTimes="0;0.5;0.66;1" repeatCount="indefinite" values="20;20;40;40"/><animate attributeName="opacity" dur="2s" keyTimes="0;0.5;0.66;1" repeatCount="indefinite" values="0;0;1;1"/></rect><rect width="20" height="20" rx="10"><animate attributeName="x" dur="2s" keyTimes="0;0.2;0.33;0.5;0.66;0.8;1" repeatCount="indefinite" values="422;422;428;428;392;392;350"/><animate attributeName="opacity" dur="2s" keyTimes="0;0.8;1" repeatCount="indefinite" values="0;0;1"/></rect></g></svg>';
const SLIDING_DOTS_SVG_URL = URL.createObjectURL(new Blob([SLIDING_DOTS_SVG_STRING], { type: 'image/svg+xml' }));

const SlidingDots = ({ className }: SlidingDotsProps) => {
const [shouldReduceMotion] = useShouldReduceMotion();
const [url] = useAssetURL('sliding dots');
const localize = useLocalizer();
const objectElementRef = useRef<HTMLObjectElement>(null);

Expand Down Expand Up @@ -49,7 +48,7 @@ const SlidingDots = ({ className }: SlidingDotsProps) => {
<object
aria-label={altText}
className={className}
data={SLIDING_DOTS_SVG_URL}
data={url.href}
onLoad={pauseOrUnpauseAnimations}
ref={objectElementRef}
type="image/svg+xml"
Expand Down
24 changes: 24 additions & 0 deletions packages/fluent-theme/src/components/assets/private/Context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { createContext } from 'react';

import type { AssetName } from '../AssetName';

type ContextType = Readonly<{
urlStateMap: ReadonlyMap<AssetName, readonly [URL]>;
}>;

type ContextAsGetter<T extends Record<string, unknown>> =
T extends Record<infer K, infer V> ? Record<K, { get(): V }> : never;

const defaultContextValue: ContextAsGetter<ContextType> = {
urlStateMap: {
get() {
throw new Error('urlMap cannot be used outside of <AssetComposerContext>.');
}
}
};

const Context = createContext<ContextType>(Object.create({}, defaultContextValue));

Context.displayName = 'AssetComposerContext';

export default Context;
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { type AssetName } from '../AssetName';
import useContext from './useContext';

export default function useAssetURL(assetName: AssetName): readonly [URL] {
const urlState = useContext().urlStateMap.get(assetName);

if (!urlState) {
throw new Error(`botframework-webchat-fluent-theme internal: Asset "${assetName}" was not found.`);
}

return urlState;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { type ContextOf } from 'botframework-webchat-api';
import { useContext as useReactContext } from 'react';

import Context from './Context';

export default function useContext(): ContextOf<typeof Context> {
return useReactContext(Context);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,14 @@ import { useStyles } from 'botframework-webchat-styles/react';
import cx from 'classnames';
import React, { memo } from 'react';

import { useVariantClassName } from '../../styles';
import SlidingDots from '../assets/SlidingDots';
import styles from './SlidingDotsTypingIndicator.module.css';

function SlidingDotsTypingIndicator() {
const classNames = useStyles(styles);
const variantClassName = useVariantClassName(classNames);

return (
<div
className={cx(classNames['sliding-dots-typing-indicator'], variantClassName)}
data-testid={testIds.typingIndicator}
>
<div className={classNames['sliding-dots-typing-indicator']} data-testid={testIds.typingIndicator}>
<SlidingDots className={cx(classNames['sliding-dots-typing-indicator__image'])} />
</div>
);
Expand Down
9 changes: 6 additions & 3 deletions packages/fluent-theme/src/private/FluentThemeProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import React, { memo, type ReactNode } from 'react';

import { ActivityDecorator } from '../components/activity';
import ActivityLoader from '../components/activity/ActivityLoader';
import AssetComposer from '../components/assets/AssetComposer';
import { isLinerMessageActivity, LinerMessageActivity } from '../components/linerActivity';
import { isPreChatMessageActivity, PreChatMessageActivity } from '../components/preChatActivity';
import { PrimarySendBox } from '../components/sendBox';
Expand Down Expand Up @@ -74,9 +75,11 @@ const FluentThemeProvider = ({ children, variant = 'fluent' }: Props) => (
styles={styles}
typingIndicatorMiddleware={typingIndicatorMiddleware}
>
<WebChatDecorator>
<DecoratorComposer middleware={decoratorMiddleware}>{children}</DecoratorComposer>
</WebChatDecorator>
<AssetComposer>
<WebChatDecorator>
<DecoratorComposer middleware={decoratorMiddleware}>{children}</DecoratorComposer>
</WebChatDecorator>
</AssetComposer>
</ThemeProvider>
</TelephoneKeypadProvider>
</WebChatTheme>
Expand Down
Loading