,
+ // despite the fact there are no "tabIndex" attributes set on the filler.
+ // We need to artificially send the focus back to the transcript.
+ const handleFocusFiller = useCallback(() => focusByActivityKey(undefined), [focusByActivityKey]);
+
+ // When focus into the transcript using TAB/SHIFT-TAB, scroll the focused activity into view.
+ useObserveFocusVisible(
+ rootElementRef,
+ useCallback(() => focusByActivityKey(undefined), [focusByActivityKey])
+ );
+
+ const hasAnyChild = !!numRenderingActivities;
+
+ return (
+
for details.
+ aria-activedescendant={android ? undefined : activeDescendantId}
+ aria-label={transcriptAriaLabel}
+ className={classNames('webchat__basic-transcript', rootClassName, (className || '') + '')}
+ dir={direction}
+ onFocus={handleFocus}
+ onKeyDown={handleTranscriptKeyDown}
+ onKeyDownCapture={handleTranscriptKeyDownCapture}
+ ref={callbackRef}
+ // "aria-activedescendant" will only works with a number of roles and it must be explicitly set.
+ // https://www.w3.org/TR/wai-aria/#aria-activedescendant
+ role="group"
+ // For up/down arrow key navigation across activities, this component must be included in the tab sequence.
+ // Otherwise, "aria-activedescendant" will not be narrated when the user press up/down arrow keys.
+ // https://www.w3.org/TR/wai-aria-practices-1.1/#kbd_focus_activedescendant
+ tabIndex={0}
+ >
+
+ {hasAnyChild && }
+
+ {hasAnyChild && }
+
+ {hasAnyChild && (
+
+
+
+
+ )}
+
+ );
+ }
+ )
);
InternalTranscript.displayName = 'InternalTranscript';
diff --git a/packages/component/src/Composer.tsx b/packages/component/src/Composer.tsx
index 5b651bf1ec..e8fb5b4419 100644
--- a/packages/component/src/Composer.tsx
+++ b/packages/component/src/Composer.tsx
@@ -1,25 +1,34 @@
/* eslint-disable react/require-default-props */
+import { singleToArray } from '@msinternal/botframework-webchat-base/utils';
+import { useMemoIterable } from '@msinternal/botframework-webchat-react-hooks';
import {
Composer as APIComposer,
+ AttachmentForScreenReaderMiddleware,
extractSendBoxMiddleware,
extractSendBoxToolbarMiddleware,
hooks,
WebSpeechPonyfillFactory,
+ type ActivityStatusMiddleware,
type ComposerProps as APIComposerProps,
+ type AttachmentMiddleware,
+ type AvatarMiddleware,
+ type CardActionMiddleware,
+ type ScrollToEndButtonMiddleware,
type SendBoxMiddleware,
- type SendBoxToolbarMiddleware
+ type SendBoxToolbarMiddleware,
+ type ToastMiddleware,
+ type TypingIndicatorMiddleware
} from 'botframework-webchat-api';
import { DecoratorComposer, type DecoratorMiddleware } from 'botframework-webchat-api/decorator';
-import { type Polymiddleware } from 'botframework-webchat-api/middleware';
-import { singleToArray } from 'botframework-webchat-core';
+import { type LegacyActivityMiddleware, type Polymiddleware } from 'botframework-webchat-api/middleware';
+import { StoreDebugAPIRegistry, type StoreDebugAPI } from 'botframework-webchat-core/internal';
import classNames from 'classnames';
import MarkdownIt from 'markdown-it';
import PropTypes from 'prop-types';
import React, { memo, useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from 'react';
import { Composer as SayComposer } from 'react-say';
-import { StoreDebugAPIRegistry, type StoreDebugAPI } from 'botframework-webchat-core/internal';
import createDefaultAttachmentMiddleware from './Attachment/createMiddleware';
import BuiltInDecorator from './BuiltInDecorator';
import Dictation from './Dictation';
@@ -32,10 +41,10 @@ import UITracker from './hooks/internal/UITracker';
import WebChatUIContext from './hooks/internal/WebChatUIContext';
import { FocusSendBoxScope } from './hooks/sendBoxFocus';
import { ScrollRelativeTranscriptScope } from './hooks/transcriptScrollRelative';
-import createDefaultActivityMiddleware from './Middleware/Activity/createCoreMiddleware';
+import defaultActivityPolymiddleware from './Middleware/Activity/defaultActivityPolymiddleware';
import createDefaultActivityStatusMiddleware from './Middleware/ActivityStatus/createCoreMiddleware';
import createDefaultAttachmentForScreenReaderMiddleware from './Middleware/AttachmentForScreenReader/createCoreMiddleware';
-import createDefaultAvatarMiddleware from './Middleware/Avatar/createCoreMiddleware';
+import createDefaultAvatarMiddleware from './Middleware/Avatar/createDefaultAvatarPolymiddleware';
import createDefaultCardActionMiddleware from './Middleware/CardAction/createCoreMiddleware';
import createDefaultScrollToEndButtonMiddleware from './Middleware/ScrollToEndButton/createScrollToEndButtonMiddleware';
import createDefaultToastMiddleware from './Middleware/Toast/createCoreMiddleware';
@@ -367,83 +376,102 @@ const Composer = ({
const { nonce, onTelemetry } = composerProps;
const theme = useTheme();
- const patchedActivityMiddleware = useMemo(
- () => [...singleToArray(activityMiddleware), ...theme.activityMiddleware, ...createDefaultActivityMiddleware()],
+ const patchedActivityMiddleware = useMemoIterable
(
+ () => Object.freeze([...singleToArray(activityMiddleware ?? []), ...theme.activityMiddleware]),
[activityMiddleware, theme.activityMiddleware]
);
- const patchedActivityStatusMiddleware = useMemo(
- () => [
- ...singleToArray(activityStatusMiddleware),
- ...theme.activityStatusMiddleware,
- ...createDefaultActivityStatusMiddleware()
- ],
+ const patchedActivityStatusMiddleware = useMemoIterable(
+ () =>
+ Object.freeze([
+ ...singleToArray(activityStatusMiddleware ?? []),
+ ...theme.activityStatusMiddleware,
+ ...createDefaultActivityStatusMiddleware()
+ ]),
[activityStatusMiddleware, theme.activityStatusMiddleware]
);
- const patchedAttachmentForScreenReaderMiddleware = useMemo(
- () => [
- ...singleToArray(attachmentForScreenReaderMiddleware),
- ...theme.attachmentForScreenReaderMiddleware,
- ...createDefaultAttachmentForScreenReaderMiddleware()
- ],
+ const patchedAttachmentForScreenReaderMiddleware = useMemoIterable(
+ () =>
+ Object.freeze([
+ ...singleToArray(attachmentForScreenReaderMiddleware ?? []),
+ ...theme.attachmentForScreenReaderMiddleware,
+ ...createDefaultAttachmentForScreenReaderMiddleware()
+ ]),
[attachmentForScreenReaderMiddleware, theme.attachmentForScreenReaderMiddleware]
);
- const patchedAttachmentMiddleware = useMemo(
- () => [
- ...singleToArray(attachmentMiddleware),
- ...theme.attachmentMiddleware,
- ...createDefaultAttachmentMiddleware()
- ],
+ const patchedAttachmentMiddleware = useMemoIterable(
+ () =>
+ Object.freeze([
+ ...singleToArray(attachmentMiddleware ?? []),
+ ...theme.attachmentMiddleware,
+ ...createDefaultAttachmentMiddleware()
+ ]),
[attachmentMiddleware, theme.attachmentMiddleware]
);
- const patchedAvatarMiddleware = useMemo(
- () => [...singleToArray(avatarMiddleware), ...theme.avatarMiddleware, ...createDefaultAvatarMiddleware()],
+ const patchedAvatarMiddleware = useMemoIterable(
+ () => Object.freeze([...singleToArray(avatarMiddleware ?? []), ...theme.avatarMiddleware]),
[avatarMiddleware, theme.avatarMiddleware]
);
- const patchedCardActionMiddleware = useMemo(
- () => [
- ...singleToArray(cardActionMiddleware),
- ...theme.cardActionMiddleware,
- ...createDefaultCardActionMiddleware()
- ],
+ const patchedCardActionMiddleware = useMemoIterable(
+ () =>
+ Object.freeze([
+ ...singleToArray(cardActionMiddleware ?? []),
+ ...theme.cardActionMiddleware,
+ ...createDefaultCardActionMiddleware()
+ ]),
[cardActionMiddleware, theme.cardActionMiddleware]
);
- const patchedPolymiddleware = useMemo(
- () => Object.freeze([...(polymiddleware || []), ...theme.polymiddleware]),
- [polymiddleware, theme.polymiddleware]
+ const defaultAvatarPolymiddleware = useMemo(() => createDefaultAvatarMiddleware(styleOptions), [styleOptions]);
+
+ const patchedPolymiddleware = useMemoIterable(
+ () =>
+ Object.freeze([
+ ...(polymiddleware || []),
+ ...theme.polymiddleware,
+ // Polymiddleware has lower priority than legacy middleware.
+ // Later, we should move default middleware to a "default theme."
+ defaultActivityPolymiddleware,
+ defaultAvatarPolymiddleware
+ ]),
+ [defaultAvatarPolymiddleware, polymiddleware, theme.polymiddleware]
);
- const patchedToastMiddleware = useMemo(
- () => [...singleToArray(toastMiddleware), ...theme.toastMiddleware, ...createDefaultToastMiddleware()],
+ const patchedToastMiddleware = useMemoIterable(
+ () =>
+ Object.freeze([
+ ...singleToArray(toastMiddleware ?? []),
+ ...theme.toastMiddleware,
+ ...createDefaultToastMiddleware()
+ ]),
[toastMiddleware, theme.toastMiddleware]
);
- const patchedTypingIndicatorMiddleware = useMemo(
- () => [
- ...singleToArray(typingIndicatorMiddleware),
- ...theme.typingIndicatorMiddleware,
- ...createDefaultTypingIndicatorMiddleware()
- ],
+ const patchedTypingIndicatorMiddleware = useMemoIterable(
+ () =>
+ Object.freeze([
+ ...singleToArray(typingIndicatorMiddleware ?? []),
+ ...theme.typingIndicatorMiddleware,
+ ...createDefaultTypingIndicatorMiddleware()
+ ]),
[typingIndicatorMiddleware, theme.typingIndicatorMiddleware]
);
- const defaultScrollToEndButtonMiddleware = useMemo(() => createDefaultScrollToEndButtonMiddleware(), []);
-
- const patchedScrollToEndButtonMiddleware = useMemo(
- () => [
- ...singleToArray(scrollToEndButtonMiddleware),
- ...theme.scrollToEndButtonMiddleware,
- ...defaultScrollToEndButtonMiddleware
- ],
- [defaultScrollToEndButtonMiddleware, scrollToEndButtonMiddleware, theme.scrollToEndButtonMiddleware]
+ const patchedScrollToEndButtonMiddleware = useMemoIterable(
+ () =>
+ Object.freeze([
+ ...singleToArray(scrollToEndButtonMiddleware ?? []),
+ ...theme.scrollToEndButtonMiddleware,
+ ...createDefaultScrollToEndButtonMiddleware()
+ ]),
+ [scrollToEndButtonMiddleware, theme.scrollToEndButtonMiddleware]
);
- const sendBoxMiddleware = useMemo(
+ const sendBoxMiddleware = useMemoIterable(
() =>
Object.freeze([
...extractSendBoxMiddleware(sendBoxMiddlewareFromProps),
@@ -453,7 +481,7 @@ const Composer = ({
[sendBoxMiddlewareFromProps, theme.sendBoxMiddleware]
);
- const sendBoxToolbarMiddleware = useMemo(
+ const sendBoxToolbarMiddleware = useMemoIterable(
() =>
Object.freeze([
...extractSendBoxToolbarMiddleware(sendBoxToolbarMiddlewareFromProps),
diff --git a/packages/component/src/Middleware/Activity/createCoreMiddleware.tsx b/packages/component/src/Middleware/Activity/defaultActivityPolymiddleware.tsx
similarity index 84%
rename from packages/component/src/Middleware/Activity/createCoreMiddleware.tsx
rename to packages/component/src/Middleware/Activity/defaultActivityPolymiddleware.tsx
index e6b6434cd1..b76c939fd0 100644
--- a/packages/component/src/Middleware/Activity/createCoreMiddleware.tsx
+++ b/packages/component/src/Middleware/Activity/defaultActivityPolymiddleware.tsx
@@ -1,5 +1,6 @@
/* eslint complexity: ["error", 21] */
import { ActivityMiddleware } from 'botframework-webchat-api';
+import { createActivityPolymiddlewareFromLegacy, type Polymiddleware } from 'botframework-webchat-api/middleware';
import {
getActivityLivestreamingMetadata,
getOrgSchemaMessage,
@@ -35,8 +36,11 @@ function shouldFilterActivity(activity, messageThing) {
return false;
}
-export default function createCoreMiddleware(): ActivityMiddleware[] {
- return [
+/**
+ * @deprecated Use `defaultActivityPolymiddleware` instead. The `createCoreActivityMiddleware` will be removed on or after 2028-03-18.
+ */
+function createCoreActivityMiddleware(): readonly ActivityMiddleware[] {
+ return Object.freeze([
() =>
next =>
(...args) => {
@@ -84,5 +88,15 @@ export default function createCoreMiddleware(): ActivityMiddleware[] {
return next(...args);
}
- ];
+ ]);
}
+
+const defaultActivityPolymiddleware: Polymiddleware = createActivityPolymiddlewareFromLegacy(
+ ...createCoreActivityMiddleware()
+);
+
+export default defaultActivityPolymiddleware;
+export {
+ // Exporting `createCoreActivityMiddleware()` for backward compatibility.
+ createCoreActivityMiddleware
+};
diff --git a/packages/component/src/Middleware/Avatar/createCoreMiddleware.tsx b/packages/component/src/Middleware/Avatar/DefaultAvatar.tsx
similarity index 58%
rename from packages/component/src/Middleware/Avatar/createCoreMiddleware.tsx
rename to packages/component/src/Middleware/Avatar/DefaultAvatar.tsx
index 1b153d9933..9a84697b0b 100644
--- a/packages/component/src/Middleware/Avatar/createCoreMiddleware.tsx
+++ b/packages/component/src/Middleware/Avatar/DefaultAvatar.tsx
@@ -1,8 +1,7 @@
-import { AvatarMiddleware } from 'botframework-webchat-api';
import { validateProps } from '@msinternal/botframework-webchat-react-valibot';
import classNames from 'classnames';
import React, { memo } from 'react';
-import { boolean, object, optional, pipe, readonly, string, type InferInput } from 'valibot';
+import { boolean, never, object, optional, pipe, readonly, string, type InferInput } from 'valibot';
import ImageAvatar from '../../Avatar/ImageAvatar';
import InitialsAvatar from '../../Avatar/InitialsAvatar';
@@ -22,7 +21,8 @@ const ROOT_STYLE = {
const defaultAvatarPropsSchema = pipe(
object({
- 'aria-hidden': optional(boolean()),
+ 'aria-hidden': optional(boolean(), true),
+ children: optional(never()),
className: optional(string()),
fromUser: boolean()
}),
@@ -32,7 +32,7 @@ const defaultAvatarPropsSchema = pipe(
type DefaultAvatarProps = InferInput;
function DefaultAvatar(props: DefaultAvatarProps) {
- const { 'aria-hidden': ariaHidden = true, className, fromUser } = validateProps(defaultAvatarPropsSchema, props);
+ const { 'aria-hidden': ariaHidden, className, fromUser } = validateProps(defaultAvatarPropsSchema, props, 'strict');
const [{ avatar: avatarStyleSet }] = useStyleSet();
const rootClassName = useStyleToEmotionObject()(ROOT_STYLE) + '';
@@ -54,22 +54,8 @@ function DefaultAvatar(props: DefaultAvatarProps) {
);
}
-export default function createCoreAvatarMiddleware(): AvatarMiddleware[] {
- return [
- () =>
- () =>
- ({ fromUser, styleOptions }) => {
- const { botAvatarImage, botAvatarInitials, userAvatarImage, userAvatarInitials } = styleOptions;
+DefaultAvatar.displayName = 'DefaultAvatar';
- if (fromUser ? userAvatarImage || userAvatarInitials : botAvatarImage || botAvatarInitials) {
- return () => ;
- }
+export default memo(DefaultAvatar);
- return false;
- }
- ];
-}
-
-const MemoizedDefaultAvatar = memo(DefaultAvatar);
-
-export { MemoizedDefaultAvatar as DefaultAvatar, defaultAvatarPropsSchema, type DefaultAvatarProps };
+export { defaultAvatarPropsSchema, type DefaultAvatarProps };
diff --git a/packages/component/src/Middleware/Avatar/createDefaultAvatarPolymiddleware.tsx b/packages/component/src/Middleware/Avatar/createDefaultAvatarPolymiddleware.tsx
new file mode 100644
index 0000000000..c1d7a42329
--- /dev/null
+++ b/packages/component/src/Middleware/Avatar/createDefaultAvatarPolymiddleware.tsx
@@ -0,0 +1,22 @@
+import type { StyleOptions } from 'botframework-webchat-api';
+import {
+ avatarComponent,
+ createAvatarPolymiddleware,
+ // For type portability.
+ type __INTERNAL_DO_NOT_USE__avatarPolymiddlewareRequestStyleOptionsSymbol
+} from 'botframework-webchat-api/middleware';
+import DefaultAvatar from './DefaultAvatar';
+
+function createDefaultAvatarMiddleware(styleOptions: StyleOptions | undefined) {
+ const { botAvatarImage, botAvatarInitials, userAvatarImage, userAvatarInitials } = styleOptions ?? {};
+
+ return createAvatarPolymiddleware(_next => ({ activity }) => {
+ const fromUser = activity.from?.role === 'user';
+
+ return (fromUser ? userAvatarImage || userAvatarInitials : botAvatarImage || botAvatarInitials)
+ ? avatarComponent(DefaultAvatar, Object.freeze({ fromUser }))
+ : undefined;
+ });
+}
+
+export default createDefaultAvatarMiddleware;
diff --git a/packages/component/src/Transcript/hooks/useRenderActivityProps.ts b/packages/component/src/Transcript/hooks/useRenderActivityProps.ts
index 2d4041f03a..4467295396 100644
--- a/packages/component/src/Transcript/hooks/useRenderActivityProps.ts
+++ b/packages/component/src/Transcript/hooks/useRenderActivityProps.ts
@@ -1,4 +1,6 @@
import { hooks } from 'botframework-webchat-api';
+import { __INTERNAL_DO_NOT_USE__avatarPolymiddlewareRequestStyleOptionsSymbol } from 'botframework-webchat-api/internal';
+import { useBuildRenderAvatarCallback } from 'botframework-webchat-api/middleware';
import { type WebChatActivity } from 'botframework-webchat-core';
import { useMemo, type ReactNode } from 'react';
@@ -8,7 +10,7 @@ import useFirstActivityInStatusGroup from '../../Middleware/ActivityGrouping/ui/
import useLastActivityInStatusGroup from '../../Middleware/ActivityGrouping/ui/StatusGrouping/useLastActivity';
import isZeroOrPositive from '../../Utils/isZeroOrPositive';
-const { useCreateActivityStatusRenderer, useCreateAvatarRenderer, useStyleOptions } = hooks;
+const { useCreateActivityStatusRenderer, useStyleOptions } = hooks;
type RenderActivityProps = {
hideTimestamp: boolean;
@@ -18,13 +20,15 @@ type RenderActivityProps = {
};
const useRenderActivityProps = (activity: WebChatActivity): RenderActivityProps => {
- const [{ bubbleFromUserNubOffset, bubbleNubOffset, groupTimestamp, showAvatarInGroup }] = useStyleOptions();
+ const [styleOptions] = useStyleOptions();
const [firstActivityInSenderGroup] = useFirstActivityInSenderGroup();
const [firstActivityInStatusGroup] = useFirstActivityInStatusGroup();
const [lastActivityInSenderGroup] = useLastActivityInSenderGroup();
const [lastActivityInStatusGroup] = useLastActivityInStatusGroup();
const createActivityStatusRenderer = useCreateActivityStatusRenderer();
- const renderAvatar = useCreateAvatarRenderer();
+ const buildRenderAvatar = useBuildRenderAvatarCallback();
+
+ const { bubbleFromUserNubOffset, bubbleNubOffset, groupTimestamp, showAvatarInGroup } = styleOptions;
const hideAllTimestamps = groupTimestamp === false;
const isFirstInSenderGroup =
@@ -36,10 +40,13 @@ const useRenderActivityProps = (activity: WebChatActivity): RenderActivityProps
const isLastInStatusGroup =
lastActivityInStatusGroup === activity || typeof lastActivityInStatusGroup === 'undefined';
- const renderAvatarForSenderGroup = useMemo(
- () => !!renderAvatar && renderAvatar({ activity }),
- [activity, renderAvatar]
- );
+ const renderAvatarForSenderGroup = useMemo Exclude)>(() => {
+ // Pass styleOptions through the runtime object (not typed in public request) for internal use
+ // by the core middleware and legacy bridge handlers.
+ const renderer = buildRenderAvatar(Object.freeze({ activity }));
+
+ return renderer ? (): ReactNode => renderer({}) : false;
+ }, [activity, buildRenderAvatar]);
const isTopSideBotNub = isZeroOrPositive(bubbleNubOffset);
const isTopSideUserNub = isZeroOrPositive(bubbleFromUserNubOffset);
diff --git a/packages/component/src/Utils/singleToArray.ts b/packages/component/src/Utils/singleToArray.ts
deleted file mode 100644
index 2e59925b9b..0000000000
--- a/packages/component/src/Utils/singleToArray.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-export default function singleToArray(singleOrArray: T | T[]): T[] {
- return singleOrArray ? (Array.isArray(singleOrArray) ? singleOrArray : [singleOrArray]) : [];
-}
diff --git a/packages/component/src/boot/internal.ts b/packages/component/src/boot/internal.ts
index 9064f4abf0..1dc92dd9f6 100644
--- a/packages/component/src/boot/internal.ts
+++ b/packages/component/src/boot/internal.ts
@@ -10,3 +10,6 @@ export { default as ScreenReaderText } from '../ScreenReaderText';
export { default as createIconComponent } from '../Utils/createIconComponent';
export { default as parseDocumentFragmentFromString } from '../Utils/parseDocumentFragmentFromString';
export { default as serializeDocumentFragmentIntoString } from '../Utils/serializeDocumentFragmentIntoString';
+
+// For type portability
+export { type __INTERNAL_DO_NOT_USE__avatarPolymiddlewareRequestStyleOptionsSymbol } from 'botframework-webchat-api/internal';
diff --git a/packages/component/src/index.ts b/packages/component/src/index.ts
index 8d4269df49..e8fd434caa 100644
--- a/packages/component/src/index.ts
+++ b/packages/component/src/index.ts
@@ -3,7 +3,7 @@ export { type WebChatActivity } from 'botframework-webchat-core';
export { default as createCoreAttachmentMiddleware } from './Attachment/createMiddleware';
export { default as Context } from './hooks/internal/WebChatUIContext';
-export { default as createCoreActivityMiddleware } from './Middleware/Activity/createCoreMiddleware';
+export { createCoreActivityMiddleware } from './Middleware/Activity/defaultActivityPolymiddleware';
export { default as createCoreActivityStatusMiddleware } from './Middleware/ActivityStatus/createCoreMiddleware';
export {
type HTMLContentTransformEnhancer,
diff --git a/packages/core-debug-api/src/RestrictedDebugAPI.ts b/packages/core-debug-api/src/RestrictedDebugAPI.ts
index 79472fe7b1..8ef030a7ad 100644
--- a/packages/core-debug-api/src/RestrictedDebugAPI.ts
+++ b/packages/core-debug-api/src/RestrictedDebugAPI.ts
@@ -3,7 +3,7 @@ import DebugAPI from './private/DebugAPI';
import type { BaseContext, BreakpointObject, RestrictedDebugAPIType } from './types';
// 🔒 This function must be left empty.
-// eslint-disable-next-line @typescript-eslint/no-empty-function, @typescript-eslint/no-unused-vars
+// eslint-disable-next-line @typescript-eslint/no-empty-function
const BREAKPOINT_FUNCTION = (__DEBUG_CONTEXT__: T) => {};
type AsGetters = {
diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts
index c29a003704..fadb704911 100644
--- a/packages/core/src/index.ts
+++ b/packages/core/src/index.ts
@@ -6,6 +6,7 @@ import markActivity from './actions/markActivity';
import muteVoiceRecording from './actions/muteVoiceRecording';
import postActivity from './actions/postActivity';
import postVoiceActivity from './actions/postVoiceActivity';
+import type { VoiceHandler } from './actions/registerVoiceHandler';
import registerVoiceHandler from './actions/registerVoiceHandler';
import sendEvent from './actions/sendEvent';
import sendFiles from './actions/sendFiles';
@@ -20,6 +21,7 @@ import setSendBox from './actions/setSendBox';
import setSendBoxAttachments from './actions/setSendBoxAttachments';
import setSendTimeout from './actions/setSendTimeout';
import setSendTypingIndicator from './actions/setSendTypingIndicator';
+import type { VoiceState } from './actions/setVoiceState';
import setVoiceState from './actions/setVoiceState';
import startDictate from './actions/startDictate';
import startSpeakingActivity from './actions/startSpeakingActivity';
@@ -36,7 +38,6 @@ import createStore, {
withDevTools as createStoreWithDevTools,
withOptions as createStoreWithOptions
} from './createStore';
-import OneOrMany from './types/OneOrMany';
import { parseAction } from './types/external/OrgSchema/Action';
import { parseClaim } from './types/external/OrgSchema/Claim';
import { parseCreativeWork } from './types/external/OrgSchema/CreativeWork';
@@ -47,13 +48,10 @@ import { parseVoteAction } from './types/external/OrgSchema/VoteAction';
import getActivityLivestreamingMetadata from './utils/getActivityLivestreamingMetadata';
import getOrgSchemaMessage from './utils/getOrgSchemaMessage';
import onErrorResumeNext from './utils/onErrorResumeNext';
-import singleToArray from './utils/singleToArray';
-import isVoiceActivity from './utils/voiceActivity/isVoiceActivity';
-import isVoiceTranscriptActivity from './utils/voiceActivity/isVoiceTranscriptActivity';
import getVoiceActivityRole from './utils/voiceActivity/getVoiceActivityRole';
import getVoiceActivityText from './utils/voiceActivity/getVoiceActivityText';
-import type { VoiceState } from './actions/setVoiceState';
-import type { VoiceHandler } from './actions/registerVoiceHandler';
+import isVoiceActivity from './utils/voiceActivity/isVoiceActivity';
+import isVoiceTranscriptActivity from './utils/voiceActivity/isVoiceTranscriptActivity';
export {
isForbiddenPropertyName,
@@ -100,6 +98,9 @@ import type { Project as OrgSchemaProject } from './types/external/OrgSchema/Pro
import type { Thing as OrgSchemaThing } from './types/external/OrgSchema/Thing';
import type { UserReview as OrgSchemaUserReview } from './types/external/OrgSchema/UserReview';
+/** @deprecated */
+export { singleToArray, type OneOrMany } from '@msinternal/botframework-webchat-base/utils';
+
const Constants = { ActivityClientState, DictateState };
export {
@@ -144,7 +145,6 @@ export {
setSendTimeout,
setSendTypingIndicator,
setVoiceState,
- singleToArray,
startDictate,
startSpeakingActivity,
startVoiceRecording,
@@ -172,7 +172,6 @@ export type {
DirectLineVideoCard,
GlobalScopePonyfill,
Observable,
- OneOrMany,
OrgSchemaAction,
OrgSchemaClaim,
OrgSchemaCreativeWork,
diff --git a/packages/core/src/types/OneOrMany.ts b/packages/core/src/types/OneOrMany.ts
deleted file mode 100644
index 905b1b6cee..0000000000
--- a/packages/core/src/types/OneOrMany.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-type OneOrMany = T | T[];
-
-export default OneOrMany;
diff --git a/packages/core/src/utils/singleToArray.ts b/packages/core/src/utils/singleToArray.ts
deleted file mode 100644
index 2e59925b9b..0000000000
--- a/packages/core/src/utils/singleToArray.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-export default function singleToArray(singleOrArray: T | T[]): T[] {
- return singleOrArray ? (Array.isArray(singleOrArray) ? singleOrArray : [singleOrArray]) : [];
-}
diff --git a/packages/react-hooks/package.json b/packages/react-hooks/package.json
index 057700add4..a8ca025a54 100644
--- a/packages/react-hooks/package.json
+++ b/packages/react-hooks/package.json
@@ -49,10 +49,15 @@
"precommit:eslint": "../../node_modules/.bin/eslint --report-unused-disable-directives --max-warnings 0",
"precommit:typecheck": "tsc --project ./src --emitDeclarationOnly false --esModuleInterop true --noEmit --pretty false",
"preversion": "../../scripts/npm/preversion.sh",
- "start": "../../scripts/npm/notify-build.sh \"src\""
+ "start": "../../scripts/npm/notify-build.sh \"src\" \"../base/package.json\""
+ },
+ "localDependencies": {
+ "@msinternal/botframework-webchat-base": "development"
},
- "localDependencies": {},
"peerDependencies": {
"react": ">= 16.8.6"
+ },
+ "devDependencies": {
+ "@msinternal/botframework-webchat-base": "0.0.0-0"
}
}
diff --git a/packages/react-hooks/src/index.ts b/packages/react-hooks/src/index.ts
index 117ee2543c..8dc71742e5 100644
--- a/packages/react-hooks/src/index.ts
+++ b/packages/react-hooks/src/index.ts
@@ -1 +1,3 @@
+export { default as useDebugDeps } from './useDebugDeps';
+export { default as useMemoIterable } from './useMemoIterable';
export { default as useMemoWithPrevious } from './useMemoWithPrevious';
diff --git a/packages/react-hooks/src/useDebugDeps.ts b/packages/react-hooks/src/useDebugDeps.ts
new file mode 100644
index 0000000000..5793794349
--- /dev/null
+++ b/packages/react-hooks/src/useDebugDeps.ts
@@ -0,0 +1,26 @@
+/* eslint no-console: "off" */
+
+import { useRef } from 'react';
+
+export default function useDebugDeps(depsObject: Record, name: string): void {
+ const depsMap = Object.freeze(new Map(Object.entries(depsObject)));
+ const prevDepsMapRef = useRef | undefined>();
+
+ const { current: prevDepsMap } = prevDepsMapRef;
+
+ // Ignores initial rendering.
+ if (prevDepsMap) {
+ const keys = new Set([...depsMap.keys(), ...prevDepsMap.keys()]);
+ const keysChanged = Array.from(keys).filter(key => !Object.is(depsMap.get(key), prevDepsMap.get(key)));
+
+ if (keysChanged.length) {
+ console.groupCollapsed(`Changes found in ${name}`);
+
+ keysChanged.forEach(key => console.log(key, { from: prevDepsMap.get(key), to: depsMap.get(key) }));
+
+ console.groupEnd();
+ }
+ }
+
+ prevDepsMapRef.current = depsMap;
+}
diff --git a/packages/react-hooks/src/useMemoIterable.ts b/packages/react-hooks/src/useMemoIterable.ts
new file mode 100644
index 0000000000..87d5fb73c7
--- /dev/null
+++ b/packages/react-hooks/src/useMemoIterable.ts
@@ -0,0 +1,11 @@
+import { iterateEquals } from '@msinternal/botframework-webchat-base/utils';
+import { type DependencyList } from 'react';
+import useMemoWithPrevious from './useMemoWithPrevious';
+
+export default function useMemoIterable>(factory: () => T, deps: DependencyList) {
+ return useMemoWithPrevious(prevValue => {
+ const value = factory();
+
+ return typeof prevValue !== 'undefined' && iterateEquals(value, prevValue) ? prevValue : value;
+ }, deps);
+}
diff --git a/packages/test/page-object/src/globals/testHelpers/activityGrouping/ActivityGroupingSurface.js b/packages/test/page-object/src/globals/testHelpers/activityGrouping/ActivityGroupingSurface.js
index 97e0782380..b6093d354b 100644
--- a/packages/test/page-object/src/globals/testHelpers/activityGrouping/ActivityGroupingSurface.js
+++ b/packages/test/page-object/src/globals/testHelpers/activityGrouping/ActivityGroupingSurface.js
@@ -1,7 +1,7 @@
import PropTypes from 'prop-types';
-import ActivityGroupingContext from './ActivityGroupingContext';
import createDirectLineWithTranscript from '../createDirectLineWithTranscript';
+import ActivityGroupingContext from './ActivityGroupingContext';
// Use React from window (UMD) instead of import.
const { React: { useEffect, useMemo, useState } = {} } = window;
@@ -23,22 +23,6 @@ const URL_QUERY_MAPPING = {
wd: 'hide'
};
-function createCustomActivityMiddleware(attachmentLayout) {
- return () =>
- next =>
- (arg0, ...args) =>
- next(
- {
- ...arg0,
- activity: {
- ...arg0.activity,
- ...(attachmentLayout && arg0.activity.from.role === 'bot' ? { attachmentLayout } : {})
- }
- },
- ...args
- );
-}
-
function generateURL(state) {
const params = {};
@@ -143,7 +127,15 @@ const ActivityGroupingSurface = ({ children }) => {
let directLine;
(async function () {
- directLine = await createDirectLineWithTranscript(transcriptName);
+ directLine = await createDirectLineWithTranscript(transcriptName, {
+ patchActivity: activity => {
+ if ((attachmentLayout === 'carousel' || attachmentLayout === 'stacked') && activity.from?.role === 'bot') {
+ return Object.freeze({ ...activity, attachmentLayout });
+ }
+
+ return activity;
+ }
+ });
aborted || setDirectLine(directLine);
})();
@@ -152,15 +144,7 @@ const ActivityGroupingSurface = ({ children }) => {
aborted = true;
directLine && directLine.end();
};
- }, [setDirectLine, transcriptName]);
-
- const activityMiddleware = useMemo(
- () =>
- attachmentLayout === 'carousel' || attachmentLayout === 'stacked'
- ? createCustomActivityMiddleware(attachmentLayout)
- : undefined,
- [attachmentLayout]
- );
+ }, [attachmentLayout, setDirectLine, transcriptName]);
const styleOptions = useMemo(
() => ({
@@ -223,7 +207,6 @@ const ActivityGroupingSurface = ({ children }) => {
const context = useMemo(
() => ({
...contextState,
- activityMiddleware,
directLine,
setAttachmentLayout,
setBotAvatarInitials,
@@ -242,7 +225,6 @@ const ActivityGroupingSurface = ({ children }) => {
url
}),
[
- activityMiddleware,
contextState,
directLine,
setAttachmentLayout,
diff --git a/packages/test/page-object/src/globals/testHelpers/createDirectLineWithTranscript.js b/packages/test/page-object/src/globals/testHelpers/createDirectLineWithTranscript.js
index c1e1fe0b72..a8218f28ce 100644
--- a/packages/test/page-object/src/globals/testHelpers/createDirectLineWithTranscript.js
+++ b/packages/test/page-object/src/globals/testHelpers/createDirectLineWithTranscript.js
@@ -36,10 +36,13 @@ function createUpdateRelativeTimestamp(now, { Date }) {
export default function createDirectLineWithTranscript(
activitiesOrFilename,
- { overridePostActivity, ponyfill: { Date } = { Date: window.Date } } = {}
+ { overridePostActivity, patchActivity: patchActivityFromOptions, ponyfill: { Date } = { Date: window.Date } } = {}
) {
const now = Date.now();
- const patchActivity = createUpdateRelativeTimestamp(now, { Date });
+ const patchActivity = activity =>
+ createUpdateRelativeTimestamp(now, { Date })(
+ patchActivityFromOptions ? patchActivityFromOptions(activity) : activity
+ );
const connectionStatusDeferredObservable = createDeferredObservable(() => {
connectionStatusDeferredObservable.next(0);
});
diff --git a/packages/test/page-object/src/globals/testHelpers/createRunHookActivityMiddleware.js b/packages/test/page-object/src/globals/testHelpers/createRunHookActivityMiddleware.js
index 3ecac51882..eda97b3ad2 100644
--- a/packages/test/page-object/src/globals/testHelpers/createRunHookActivityMiddleware.js
+++ b/packages/test/page-object/src/globals/testHelpers/createRunHookActivityMiddleware.js
@@ -1,5 +1,7 @@
const RunHook = ({ fn, resolve }) => {
- resolve(fn(window.WebChat.hooks));
+ const numCalledRef = window.React.useRef(0);
+
+ resolve(fn(window.WebChat.hooks, numCalledRef.current++));
return false;
};