diff --git a/.eslintrc.yml b/.eslintrc.yml
index 0ac026ba64..eb12c3c9d9 100644
--- a/.eslintrc.yml
+++ b/.eslintrc.yml
@@ -55,7 +55,7 @@ overrides:
no-unused-vars: off
'@typescript-eslint/no-unused-vars':
- error
- - argsIgnorePattern: ^_$
+ - argsIgnorePattern: ^_
varsIgnorePattern: ^_
no-empty-function: off
diff --git a/.github/agents/polymiddleware-promoter.agent.md b/.github/agents/polymiddleware-promoter.agent.md
new file mode 100644
index 0000000000..4a868b0ceb
--- /dev/null
+++ b/.github/agents/polymiddleware-promoter.agent.md
@@ -0,0 +1,45 @@
+---
+name: Polymiddleware promoter
+description: Upgrade middleware to polymiddleware
+argument-hint: The existing middleware to upgrade
+# tools: ['vscode', 'execute', 'read', 'agent', 'edit', 'search', 'web', 'todo'] # specify the tools this agent can use. If not set, all enabled tools are allowed.
+---
+
+
+
+You are a developer upgrading middleware to polymiddleware.
+
+Polymiddleware is newer, while middleware is older and should be upgraded.
+
+Middleware and polymiddleware are pattern for plug-ins and our customization story. There are 2 sides: writing the middleware and using the middleware. Web Chat write and use the middleware. 3P developers write middleware and pass it to Web Chat.
+
+Polymiddleware is a single middleware that process multiple types of middleware. Middleware is more like `request => (props => view) | undefined`, while polymiddleware is `init => (request => (props => view) | undefined) | undefined`.
+
+The middleware philosophy can be found at https://npmjs.com/package/react-chain-of-responsibility.
+
+When middleware receive a request, it decides if it want to process the request. If yes, it will return a React component. If no, it will pass it to the next middleware.
+
+Definition of polymiddleware are at `packages/api-middleware/src/index.ts`.
+
+Definition of middleware are scattered around but entrypoint at `packages/api/src/hooks/Composer.tsx`.
+
+- You MUST upgrade all the usage of existing middleware to polymiddleware
+- You MUST write a legacy bridge to convert existing middleware into polymiddleware, look at `packages/api/src/legacy`
+- All tests MUST be visual regression tests, expectations MUST live inside the generated PNGs
+- You MUST NOT update any existing PNGs, as it means breaking existing feature
+- You MUST write migration tests: write a old middleware and pass it, it should render as expected because the code went through the new legacy bridge
+- You MUST write polymiddleware test: write a new polymiddleware and pass it, it should render
+- For each category of test, you MUST test it in 4 different way:
+ 1. Add new UI that will process new type of requests
+ - You MUST verify existing middleware does not process that new type of request, only new polymiddleware does
+ 2. Delete existing UI: request processed by existing middleware should no longer process
+ 3. Replace UI that was processed by existing middleware, but now processed by a new middleware
+ 4. Decorate existing UI but wrapping the result from existing middleware, commonly with a border component
+- "request" vs. "props"
+ - Code processing the request MUST NOT call hooks
+ - Code processing the request decide to render a React component or not
+ - Code processing the props MUST render, minimally, `` or `null`, they are processed by React
+ - Request SHOULD contains information about "should render or not"
+ - Props SHOULD contains information about "how to render"
+- You MUST NOT remove the existing middleware from ``, however, print a deprecation warn-once, then bridge it to the polymiddleware
+- You SHOULD NOT export the ``, `XXXProviderProps`, and `extractXXXEnhancer`
diff --git a/.github/workflows/pull-request-validation.yml b/.github/workflows/pull-request-validation.yml
index 5e14e26873..370f51e8ee 100644
--- a/.github/workflows/pull-request-validation.yml
+++ b/.github/workflows/pull-request-validation.yml
@@ -101,7 +101,7 @@ jobs:
strategy:
matrix:
os:
- - macos-latest
+ - macos-26
- ubuntu-latest
- windows-latest
runs-on: ${{ matrix.os }}
diff --git a/AGENTS.md b/AGENTS.md
index cf4fafc8de..b469f44c1c 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -23,6 +23,7 @@
- Prefer uppercase for acronyms instead of Pascal case, e.g. `getURL()` over `getUrl()`
- The only exception is `id`, e.g. `getId()` over `getID()`
- Use fewer shorthands, only allow `min`, `max`, `num`
+- Prefer ternary operator over one-liner `if` statement
### Design
@@ -35,6 +36,7 @@
### Typing
- TypeScript is best-effort checking, use `valibot` for strict type checking
+- Use TypeScript CLI instead of `tsc`
- Use `valibot` for runtime type checker, never use `zod`
- Assume all externally exported functions will receive unsafe/invalid input, always check with `valibot`
- Avoid `any`
@@ -100,9 +102,15 @@ export { MyComponentPropsSchema, type MyComponentProps };
- Use `@testduet/given-when-then` package instead of xUnit style `describe`/`before`/`test`/`after`
- Prefer integration/end-to-end testing than unit testing
- Use as realistic setup as possible, such as using `msw` than mocking calls
+- Use `emulateIncomingActivity` and `emulateOutgoingActivity` to emulate conversation
## PR instructions
- Run new test and all of them must be green
- Run `npm run precommit` to make sure it pass all linting process
- Add changelog entry to `CHANGELOG.md`, follow our existing format
+
+## Code review
+
+- Code should use as much immutable (via `Object.freeze()`) as possible, DO NOT trust `readonly`
+- All inputs SHOULD be validated
diff --git a/__tests__/html2/middleware/avatar/legacyAvatarMiddleware/addNew.html b/__tests__/html2/middleware/avatar/legacyAvatarMiddleware/addNew.html
new file mode 100644
index 0000000000..a0d557c4da
--- /dev/null
+++ b/__tests__/html2/middleware/avatar/legacyAvatarMiddleware/addNew.html
@@ -0,0 +1,77 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/__tests__/html2/middleware/avatar/legacyAvatarMiddleware/addNew.html.snap-1.png b/__tests__/html2/middleware/avatar/legacyAvatarMiddleware/addNew.html.snap-1.png
new file mode 100644
index 0000000000..c716774bee
Binary files /dev/null and b/__tests__/html2/middleware/avatar/legacyAvatarMiddleware/addNew.html.snap-1.png differ
diff --git a/__tests__/html2/middleware/avatar/legacyAvatarMiddleware/decorate.html b/__tests__/html2/middleware/avatar/legacyAvatarMiddleware/decorate.html
new file mode 100644
index 0000000000..39506c6300
--- /dev/null
+++ b/__tests__/html2/middleware/avatar/legacyAvatarMiddleware/decorate.html
@@ -0,0 +1,80 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/__tests__/html2/middleware/avatar/legacyAvatarMiddleware/decorate.html.snap-1.png b/__tests__/html2/middleware/avatar/legacyAvatarMiddleware/decorate.html.snap-1.png
new file mode 100644
index 0000000000..d12ac4461a
Binary files /dev/null and b/__tests__/html2/middleware/avatar/legacyAvatarMiddleware/decorate.html.snap-1.png differ
diff --git a/__tests__/html2/middleware/avatar/legacyAvatarMiddleware/delete.html b/__tests__/html2/middleware/avatar/legacyAvatarMiddleware/delete.html
new file mode 100644
index 0000000000..18229e4737
--- /dev/null
+++ b/__tests__/html2/middleware/avatar/legacyAvatarMiddleware/delete.html
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/__tests__/html2/middleware/avatar/legacyAvatarMiddleware/delete.html.snap-1.png b/__tests__/html2/middleware/avatar/legacyAvatarMiddleware/delete.html.snap-1.png
new file mode 100644
index 0000000000..b947953c52
Binary files /dev/null and b/__tests__/html2/middleware/avatar/legacyAvatarMiddleware/delete.html.snap-1.png differ
diff --git a/__tests__/html2/middleware/avatar/legacyAvatarMiddleware/replace.html b/__tests__/html2/middleware/avatar/legacyAvatarMiddleware/replace.html
new file mode 100644
index 0000000000..e71ab23198
--- /dev/null
+++ b/__tests__/html2/middleware/avatar/legacyAvatarMiddleware/replace.html
@@ -0,0 +1,78 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/__tests__/html2/middleware/avatar/legacyAvatarMiddleware/replace.html.snap-1.png b/__tests__/html2/middleware/avatar/legacyAvatarMiddleware/replace.html.snap-1.png
new file mode 100644
index 0000000000..03b6aabe9b
Binary files /dev/null and b/__tests__/html2/middleware/avatar/legacyAvatarMiddleware/replace.html.snap-1.png differ
diff --git a/__tests__/html2/middleware/avatar/polymiddleware/addNew.html b/__tests__/html2/middleware/avatar/polymiddleware/addNew.html
new file mode 100644
index 0000000000..431e4be5bb
--- /dev/null
+++ b/__tests__/html2/middleware/avatar/polymiddleware/addNew.html
@@ -0,0 +1,80 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/__tests__/html2/middleware/avatar/polymiddleware/addNew.html.snap-1.png b/__tests__/html2/middleware/avatar/polymiddleware/addNew.html.snap-1.png
new file mode 100644
index 0000000000..b371ae9009
Binary files /dev/null and b/__tests__/html2/middleware/avatar/polymiddleware/addNew.html.snap-1.png differ
diff --git a/__tests__/html2/middleware/avatar/polymiddleware/decorate.html b/__tests__/html2/middleware/avatar/polymiddleware/decorate.html
new file mode 100644
index 0000000000..0049b0fbc4
--- /dev/null
+++ b/__tests__/html2/middleware/avatar/polymiddleware/decorate.html
@@ -0,0 +1,77 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/__tests__/html2/middleware/avatar/polymiddleware/decorate.html.snap-1.png b/__tests__/html2/middleware/avatar/polymiddleware/decorate.html.snap-1.png
new file mode 100644
index 0000000000..c3ac963ce1
Binary files /dev/null and b/__tests__/html2/middleware/avatar/polymiddleware/decorate.html.snap-1.png differ
diff --git a/__tests__/html2/middleware/avatar/polymiddleware/delete.html b/__tests__/html2/middleware/avatar/polymiddleware/delete.html
new file mode 100644
index 0000000000..7dcb2c7b55
--- /dev/null
+++ b/__tests__/html2/middleware/avatar/polymiddleware/delete.html
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/__tests__/html2/middleware/avatar/polymiddleware/delete.html.snap-1.png b/__tests__/html2/middleware/avatar/polymiddleware/delete.html.snap-1.png
new file mode 100644
index 0000000000..b947953c52
Binary files /dev/null and b/__tests__/html2/middleware/avatar/polymiddleware/delete.html.snap-1.png differ
diff --git a/__tests__/html2/middleware/avatar/polymiddleware/replace.html b/__tests__/html2/middleware/avatar/polymiddleware/replace.html
new file mode 100644
index 0000000000..c937ffdb99
--- /dev/null
+++ b/__tests__/html2/middleware/avatar/polymiddleware/replace.html
@@ -0,0 +1,77 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/__tests__/html2/middleware/avatar/polymiddleware/replace.html.snap-1.png b/__tests__/html2/middleware/avatar/polymiddleware/replace.html.snap-1.png
new file mode 100644
index 0000000000..2f8f30ac89
Binary files /dev/null and b/__tests__/html2/middleware/avatar/polymiddleware/replace.html.snap-1.png differ
diff --git a/packages/api-middleware/src/PolymiddlewareComposer.tsx b/packages/api-middleware/src/PolymiddlewareComposer.tsx
index 4780e3fc86..db6c3e6d2e 100644
--- a/packages/api-middleware/src/PolymiddlewareComposer.tsx
+++ b/packages/api-middleware/src/PolymiddlewareComposer.tsx
@@ -15,6 +15,7 @@ import {
} from 'valibot';
import { ActivityPolymiddlewareProvider, extractActivityEnhancer } from './activityPolymiddleware';
+import { AvatarPolymiddlewareProvider, extractAvatarEnhancer } from './avatarPolymiddleware';
import { ErrorBoxPolymiddlewareProvider, extractErrorBoxEnhancer } from './errorBoxPolymiddleware';
import { Polymiddleware } from './types/Polymiddleware';
@@ -49,6 +50,21 @@ function PolymiddlewareComposer(props: PolymiddlewareComposerProps) {
const activityPolymiddleware = useMemo(() => activityEnhancers.map(enhancer => () => enhancer), [activityEnhancers]);
+ const avatarEnhancers = useMemoWithPrevious>(
+ (prevAvatarEnhancers = []) => {
+ const avatarEnhancers = extractAvatarEnhancer(polymiddleware);
+
+ // Checks for array equality, return previous version if nothing has changed.
+ return prevAvatarEnhancers.length === avatarEnhancers.length &&
+ avatarEnhancers.every((middleware, index) => Object.is(middleware, prevAvatarEnhancers.at(index)))
+ ? prevAvatarEnhancers
+ : avatarEnhancers;
+ },
+ [polymiddleware]
+ );
+
+ const avatarPolymiddleware = useMemo(() => avatarEnhancers.map(enhancer => () => enhancer), [avatarEnhancers]);
+
const errorBoxEnhancers = useMemoWithPrevious>(
(prevErrorBoxEnhancers = []) => {
const errorBoxEnhancers = extractErrorBoxEnhancer(polymiddleware);
@@ -75,7 +91,9 @@ function PolymiddlewareComposer(props: PolymiddlewareComposerProps) {
return (
- {children}
+
+ {children}
+
);
}
diff --git a/packages/api-middleware/src/avatarPolymiddleware.tsx b/packages/api-middleware/src/avatarPolymiddleware.tsx
new file mode 100644
index 0000000000..d389a98366
--- /dev/null
+++ b/packages/api-middleware/src/avatarPolymiddleware.tsx
@@ -0,0 +1,92 @@
+import { validateProps } from '@msinternal/botframework-webchat-react-valibot';
+import { type WebChatActivity } from 'botframework-webchat-core';
+import React, { memo, useMemo } from 'react';
+import { any, boolean, custom, object, pipe, readonly, safeParse, type InferInput } from 'valibot';
+
+import templatePolymiddleware, {
+ type InferHandler,
+ type InferHandlerResult,
+ type InferMiddleware,
+ type InferProps,
+ type InferProviderProps,
+ type InferRenderer,
+ type InferRequest
+} from './private/templatePolymiddleware';
+
+// This is for bridging legacy AvatarMiddleware, will be removed when AvatarMiddleware is removed.
+// Customization developers should request access to styleOptions themselves someway, says, `polymiddleware={[createCustomAvatarPolymiddleware(styleOptions)]}`.
+const __INTERNAL_DO_NOT_USE__avatarPolymiddlewareRequestStyleOptionsSymbol = Symbol();
+
+const {
+ createMiddleware: createAvatarPolymiddleware,
+ extractEnhancer: extractAvatarEnhancer,
+ Provider: AvatarPolymiddlewareProvider,
+ Proxy,
+ reactComponent: avatarComponent,
+ useBuildRenderCallback: useBuildRenderAvatarCallback
+} = templatePolymiddleware<
+ {
+ readonly [__INTERNAL_DO_NOT_USE__avatarPolymiddlewareRequestStyleOptionsSymbol]: any;
+ readonly activity: WebChatActivity;
+ readonly fromUser: boolean;
+ },
+ { readonly children?: never }
+>('avatar');
+
+type AvatarPolymiddleware = InferMiddleware;
+type AvatarPolymiddlewareHandler = InferHandler;
+type AvatarPolymiddlewareHandlerResult = InferHandlerResult;
+type AvatarPolymiddlewareProps = InferProps;
+type AvatarPolymiddlewareRenderer = InferRenderer;
+type AvatarPolymiddlewareRequest = InferRequest;
+type AvatarPolymiddlewareProviderProps = InferProviderProps;
+
+const avatarPolymiddlewareProxyPropsSchema = pipe(
+ object({
+ [__INTERNAL_DO_NOT_USE__avatarPolymiddlewareRequestStyleOptionsSymbol]: any(),
+ activity: custom>(value => safeParse(object({}), value).success),
+ fromUser: boolean()
+ }),
+ readonly()
+);
+
+type AvatarPolymiddlewareProxyProps = Readonly>;
+
+// A friendlier version than the organic .
+const AvatarPolymiddlewareProxy = memo(function AvatarPolymiddlewareProxy(props: AvatarPolymiddlewareProxyProps) {
+ const {
+ [__INTERNAL_DO_NOT_USE__avatarPolymiddlewareRequestStyleOptionsSymbol]: styleOptions,
+ activity,
+ fromUser
+ } = validateProps(avatarPolymiddlewareProxyPropsSchema, props);
+
+ const request = useMemo(
+ () =>
+ Object.freeze({
+ [__INTERNAL_DO_NOT_USE__avatarPolymiddlewareRequestStyleOptionsSymbol]: styleOptions,
+ activity,
+ fromUser
+ }),
+ [activity, fromUser, styleOptions]
+ );
+
+ return ;
+});
+
+export {
+ __INTERNAL_DO_NOT_USE__avatarPolymiddlewareRequestStyleOptionsSymbol,
+ avatarComponent,
+ AvatarPolymiddlewareProvider,
+ AvatarPolymiddlewareProxy,
+ createAvatarPolymiddleware,
+ extractAvatarEnhancer,
+ useBuildRenderAvatarCallback,
+ type AvatarPolymiddleware,
+ type AvatarPolymiddlewareHandler,
+ type AvatarPolymiddlewareHandlerResult,
+ type AvatarPolymiddlewareProps,
+ type AvatarPolymiddlewareProviderProps,
+ type AvatarPolymiddlewareProxyProps,
+ type AvatarPolymiddlewareRenderer,
+ type AvatarPolymiddlewareRequest
+};
diff --git a/packages/api-middleware/src/index.ts b/packages/api-middleware/src/index.ts
index 9e0a8aeb8e..80a766e350 100644
--- a/packages/api-middleware/src/index.ts
+++ b/packages/api-middleware/src/index.ts
@@ -12,6 +12,21 @@ export {
type ActivityPolymiddlewareRequest
} from './activityPolymiddleware';
+export {
+ __INTERNAL_DO_NOT_USE__avatarPolymiddlewareRequestStyleOptionsSymbol,
+ avatarComponent,
+ AvatarPolymiddlewareProxy,
+ createAvatarPolymiddleware,
+ useBuildRenderAvatarCallback,
+ type AvatarPolymiddleware,
+ type AvatarPolymiddlewareHandler,
+ type AvatarPolymiddlewareHandlerResult,
+ type AvatarPolymiddlewareProps,
+ type AvatarPolymiddlewareProxyProps,
+ type AvatarPolymiddlewareRenderer,
+ type AvatarPolymiddlewareRequest
+} from './avatarPolymiddleware';
+
export {
createErrorBoxPolymiddleware,
errorBoxComponent,
@@ -27,5 +42,6 @@ export {
} from './errorBoxPolymiddleware';
// TODO: [P0] Add tests for nesting `polymiddleware`.
+export { __INTERNAL_DO_NOT_USE__legacyAvatarMiddlewareOriginalRequestSymbol } from './legacy/avatarMiddleware';
export { default as PolymiddlewareComposer } from './PolymiddlewareComposer';
export { type Polymiddleware } from './types/Polymiddleware';
diff --git a/packages/api-middleware/src/legacy.ts b/packages/api-middleware/src/legacy.ts
index c9405239c4..db6fbf25b5 100644
--- a/packages/api-middleware/src/legacy.ts
+++ b/packages/api-middleware/src/legacy.ts
@@ -6,3 +6,5 @@ export {
} from './legacy/activityMiddleware';
export { type LegacyAttachmentMiddleware, type LegacyRenderAttachment } from './legacy/attachmentMiddleware';
+
+export { type LegacyAvatarMiddleware, type LegacyAvatarRenderer } from './legacy/avatarMiddleware';
diff --git a/packages/api-middleware/src/legacy/avatarMiddleware.ts b/packages/api-middleware/src/legacy/avatarMiddleware.ts
new file mode 100644
index 0000000000..6899783972
--- /dev/null
+++ b/packages/api-middleware/src/legacy/avatarMiddleware.ts
@@ -0,0 +1,29 @@
+// TODO: This is moved from /api, need to revisit/rewrite everything in this file.
+import { type WebChatActivity } from 'botframework-webchat-core';
+import { type ReactNode } from 'react';
+import type { AvatarPolymiddlewareRequest } from '../avatarPolymiddleware';
+
+// Polymiddleware requires immutable request object.
+// When bridging between legacy and polymiddlware, this symbol helps keeping the original object.
+const __INTERNAL_DO_NOT_USE__legacyAvatarMiddlewareOriginalRequestSymbol = Symbol();
+
+type LegacyAvatarComponentFactoryArguments = {
+ readonly [__INTERNAL_DO_NOT_USE__legacyAvatarMiddlewareOriginalRequestSymbol]: AvatarPolymiddlewareRequest;
+ readonly activity: WebChatActivity;
+ readonly fromUser: boolean;
+ readonly styleOptions: Readonly>;
+};
+
+type LegacyAvatarRenderer = false | (() => Exclude);
+
+type LegacyAvatarEnhancer = (
+ next: (args: LegacyAvatarComponentFactoryArguments) => LegacyAvatarRenderer
+) => (args: LegacyAvatarComponentFactoryArguments) => LegacyAvatarRenderer;
+
+type LegacyAvatarMiddleware = () => LegacyAvatarEnhancer;
+
+export {
+ __INTERNAL_DO_NOT_USE__legacyAvatarMiddlewareOriginalRequestSymbol,
+ type LegacyAvatarMiddleware,
+ type LegacyAvatarRenderer
+};
diff --git a/packages/api-middleware/src/private/templatePolymiddleware.tsx b/packages/api-middleware/src/private/templatePolymiddleware.tsx
index 9b9cd80541..db84af06b7 100644
--- a/packages/api-middleware/src/private/templatePolymiddleware.tsx
+++ b/packages/api-middleware/src/private/templatePolymiddleware.tsx
@@ -19,6 +19,8 @@ const isArrayOfFunction = (middleware: unknown): middleware is InferOutput = next => request => next(request);
+const DEBUG_ENHANCER_SYMBOL = Symbol('OriginalEnhancer');
+const DEBUG_NAME_SYMBOL = Symbol('MiddlewareName');
const EMPTY_ARRAY = Object.freeze([]);
// Following @types/react to use {} for props.
@@ -54,7 +56,21 @@ function templatePolymiddleware(name: string) {
// We enforce middleware to be created using factory function.
Object.defineProperty(taggedEnhancer, middlewareFactoryTag, { enumerable: false });
- return init => (init === name ? taggedEnhancer : BYPASS_ENHANCER);
+ const middleware: TemplatedMiddleware = init => (init === name ? taggedEnhancer : BYPASS_ENHANCER);
+
+ Object.defineProperty(middleware, DEBUG_ENHANCER_SYMBOL, {
+ configurable: false,
+ value: enhancer,
+ writable: false
+ });
+
+ Object.defineProperty(middleware, DEBUG_NAME_SYMBOL, {
+ configurable: false,
+ value: name,
+ writable: false
+ });
+
+ return middleware;
};
const warnInvalidExtraction = warnOnce(`Middleware passed for extraction of "${name}" must be an array of function`);
diff --git a/packages/api/src/boot/internal.ts b/packages/api/src/boot/internal.ts
index 1deec63da8..add882acb8 100644
--- a/packages/api/src/boot/internal.ts
+++ b/packages/api/src/boot/internal.ts
@@ -1,3 +1,4 @@
+export { __INTERNAL_DO_NOT_USE__avatarPolymiddlewareRequestStyleOptionsSymbol } from '@msinternal/botframework-webchat-api-middleware';
export { default as LowPriorityDecoratorComposer } from '../decorator/internal/LowPriorityDecoratorComposer';
export { default as usePostVoiceActivity } from '../hooks/internal/usePostVoiceActivity';
export { default as useSetDictateState } from '../hooks/internal/useSetDictateState';
diff --git a/packages/api/src/boot/middleware.ts b/packages/api/src/boot/middleware.ts
index 8d42eb1069..a38e239327 100644
--- a/packages/api/src/boot/middleware.ts
+++ b/packages/api/src/boot/middleware.ts
@@ -16,6 +16,20 @@ export {
type ActivityPolymiddlewareRequest
} from '@msinternal/botframework-webchat-api-middleware';
+export {
+ avatarComponent,
+ AvatarPolymiddlewareProxy,
+ createAvatarPolymiddleware,
+ useBuildRenderAvatarCallback,
+ type AvatarPolymiddleware,
+ type AvatarPolymiddlewareHandler,
+ type AvatarPolymiddlewareHandlerResult,
+ type AvatarPolymiddlewareProps,
+ type AvatarPolymiddlewareProxyProps,
+ type AvatarPolymiddlewareRenderer,
+ type AvatarPolymiddlewareRequest
+} from '@msinternal/botframework-webchat-api-middleware';
+
export {
createErrorBoxPolymiddleware,
errorBoxComponent,
@@ -31,3 +45,5 @@ export {
} from '@msinternal/botframework-webchat-api-middleware';
export { default as createActivityPolymiddlewareFromLegacy } from '../legacy/createActivityPolymiddlewareFromLegacy';
+
+export { default as createAvatarPolymiddlewareFromLegacy } from '../legacy/createAvatarPolymiddlewareFromLegacy';
diff --git a/packages/api/src/hooks/Composer.tsx b/packages/api/src/hooks/Composer.tsx
index 3b1794d86b..815ebc42f8 100644
--- a/packages/api/src/hooks/Composer.tsx
+++ b/packages/api/src/hooks/Composer.tsx
@@ -50,6 +50,7 @@ import errorBoxTelemetryPolymiddleware from '../errorBox/errorBoxTelemetryPolymi
import PrecompiledGlobalize from '../external/PrecompiledGlobalize';
import usePonyfill from '../hooks/usePonyfill';
import createActivityPolymiddlewareFromLegacy from '../legacy/createActivityPolymiddlewareFromLegacy';
+import createAvatarPolymiddlewareFromLegacy from '../legacy/createAvatarPolymiddlewareFromLegacy';
import getAllLocalizedStrings from '../localization/getAllLocalizedStrings';
import { SendBoxMiddlewareProvider, type SendBoxMiddleware } from '../middleware/SendBoxMiddleware';
import {
@@ -84,10 +85,10 @@ import isObject from '../utils/isObject';
import mapMap from '../utils/mapMap';
import normalizeLanguage from '../utils/normalizeLanguage';
import Tracker from './internal/Tracker';
-import useVoiceHandlers from './internal/useVoiceHandlers';
import WebChatAPIContext, { type WebChatAPIContextType } from './internal/WebChatAPIContext';
import WebChatReduxContext, { useDispatch } from './internal/WebChatReduxContext';
import defaultSelectVoice from './internal/defaultSelectVoice';
+import useVoiceHandlers from './internal/useVoiceHandlers';
import activityFallbackPolymiddleware from './middleware/activityFallbackPolymiddleware';
import applyMiddleware, {
forLegacyRenderer as applyMiddlewareForLegacyRenderer,
@@ -213,12 +214,15 @@ function mergeStringsOverrides(localizedStrings, language, overrideLocalizedStri
type ComposerCoreProps = Readonly<{
/**
- * @deprecated The `activityMiddleware` prop is being deprecated, please use `polymiddleware` instead. This prop will be removed on or after 2027-08-21.
+ * @deprecated Use `polymiddleware` instead. The `activityMiddleware` prop is being deprecated, please use `polymiddleware` instead. This prop will be removed on or after 2027-08-21.
*/
activityMiddleware?: OneOrMany;
activityStatusMiddleware?: OneOrMany;
attachmentForScreenReaderMiddleware?: OneOrMany;
attachmentMiddleware?: OneOrMany;
+ /**
+ * @deprecated Use `polymiddleware` instead. The `avatarMiddleware` prop is being deprecated, please use `polymiddleware` instead. This prop will be removed on or after 2028-03-16.
+ */
avatarMiddleware?: OneOrMany;
cardActionMiddleware?: OneOrMany;
children?: ReactNode | ((context: ContextOf>) => ReactNode);
@@ -467,14 +471,11 @@ const ComposerCore = ({
[attachmentMiddleware]
);
- const patchedAvatarRenderer = useMemo(
+ const polymiddlewareForLegacyAvatarMiddleware = useMemo(
() =>
- applyMiddlewareForRenderer(
- 'avatar',
- { strict: false },
- ...singleToArray(avatarMiddleware),
- () => () => () => false
- )({}),
+ avatarMiddleware
+ ? Object.freeze([createAvatarPolymiddlewareFromLegacy(...singleToArray(avatarMiddleware))])
+ : EMPTY_ARRAY,
[avatarMiddleware]
);
@@ -530,11 +531,32 @@ const ComposerCore = ({
// Error box telemetry polymiddleware is special and has a much higher priority.
// This guarantees telemetry is always emitted for exception and no other polymiddleware can override this behavior.
errorBoxTelemetryPolymiddleware,
- ...(polymiddlewareFromProps || []),
+
+ // # Why render legacy middleware before polymiddleware?
+ //
+ // - Legacy middleware should have high priority than defaults
+ // - Default middleware will be upgraded to polymiddleware, however, they should have lower priority
+ // - Default middleware are implemented in the `component` package, and passed via `polymiddleware` props
+ // - They are UI, cannot be implemented in `api` package
+ //
+ // We have a few way out, either one of the followings:
+ //
+ // - Add a new `lowPriorityPolymiddleware` props for default polymiddleware, so we can put them after legacy
+ // - We don't want any special treatments or any prioritization system
+ // - We put the upgrade logics inside both `api` and `component` package
+ // - `component` will upgrade legacy to polymiddleware and prioritize properly
+ // - Spaghetti code and it is difficult to test the logic in `api` package
+ // - We always render legacy middleware before polymiddleware
+ // - Default middleware are polymiddleware, has lower priority than legacy
+ //
+ // The simplest and logical move is #3: render legacy middleware before polymiddleware.
+
...polymiddlewareForLegacyActivityMiddleware,
+ ...polymiddlewareForLegacyAvatarMiddleware,
+ ...(polymiddlewareFromProps || []),
activityFallbackPolymiddleware
]),
- [polymiddlewareForLegacyActivityMiddleware, polymiddlewareFromProps]
+ [polymiddlewareForLegacyActivityMiddleware, polymiddlewareForLegacyAvatarMiddleware, polymiddlewareFromProps]
);
/**
@@ -555,7 +577,6 @@ const ComposerCore = ({
activityStatusRenderer: patchedActivityStatusRenderer,
attachmentForScreenReaderRenderer: patchedAttachmentForScreenReaderRenderer,
attachmentRenderer: patchedAttachmentRenderer,
- avatarRenderer: patchedAvatarRenderer,
dir: patchedDir,
directLine,
downscaleImageToDataURL,
@@ -589,7 +610,6 @@ const ComposerCore = ({
patchedActivityStatusRenderer,
patchedAttachmentForScreenReaderRenderer,
patchedAttachmentRenderer,
- patchedAvatarRenderer,
patchedDir,
patchedGrammars,
patchedLocalizedStrings,
diff --git a/packages/api/src/hooks/internal/WebChatAPIContext.ts b/packages/api/src/hooks/internal/WebChatAPIContext.ts
index a0bc434b51..f567c3dc7f 100644
--- a/packages/api/src/hooks/internal/WebChatAPIContext.ts
+++ b/packages/api/src/hooks/internal/WebChatAPIContext.ts
@@ -11,7 +11,6 @@ import { createContext } from 'react';
import { RenderActivityStatus } from '../../types/ActivityStatusMiddleware';
import { AttachmentForScreenReaderComponentFactory } from '../../types/AttachmentForScreenReaderMiddleware';
-import { AvatarComponentFactory } from '../../types/AvatarMiddleware';
import { PerformCardAction } from '../../types/CardActionMiddleware';
import { GroupActivities } from '../../types/GroupActivitiesMiddleware';
import LocalizedStrings from '../../types/LocalizedStrings';
@@ -25,7 +24,6 @@ export type WebChatAPIContextType = {
activityStatusRenderer: RenderActivityStatus;
attachmentForScreenReaderRenderer?: AttachmentForScreenReaderComponentFactory;
attachmentRenderer?: LegacyRenderAttachment;
- avatarRenderer: AvatarComponentFactory;
clearSuggestedActions?: () => void;
dir?: string;
directLine?: DirectLineJSBotConnection;
diff --git a/packages/api/src/hooks/useCreateAvatarRenderer.ts b/packages/api/src/hooks/useCreateAvatarRenderer.ts
index 086e985e3a..61a47f69f5 100644
--- a/packages/api/src/hooks/useCreateAvatarRenderer.ts
+++ b/packages/api/src/hooks/useCreateAvatarRenderer.ts
@@ -1,40 +1,37 @@
-import { useMemo } from 'react';
-import useStyleOptions from './useStyleOptions';
-import useWebChatAPIContext from './internal/useWebChatAPIContext';
-
-import type { AvatarComponentFactory } from '../types/AvatarMiddleware';
-import type { ReactNode } from 'react';
+import {
+ __INTERNAL_DO_NOT_USE__avatarPolymiddlewareRequestStyleOptionsSymbol,
+ useBuildRenderAvatarCallback
+} from '@msinternal/botframework-webchat-api-middleware';
import type { WebChatActivity } from 'botframework-webchat-core';
+import { useMemo, type ReactNode } from 'react';
+import useStyleOptions from './useStyleOptions';
+/**
+ * @deprecated Use `` or `useBuildRenderAvatarCallback` instead. This hook will be removed on or after 2028-03-16.
+ */
export default function useCreateAvatarRenderer(): ({
activity
}: {
activity: WebChatActivity;
}) => false | (() => Exclude) {
const [styleOptions] = useStyleOptions();
- const { avatarRenderer }: { avatarRenderer: AvatarComponentFactory } = useWebChatAPIContext();
+ const buildRenderAvatar = useBuildRenderAvatarCallback();
return useMemo(
() =>
({ activity }) => {
const { from: { role } = {} }: { from?: { role?: string } } = activity;
- const result = avatarRenderer({
- activity,
- fromUser: role === 'user',
- 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. Please refer to HOOKS.md for details.'
- );
-
- return () => result;
- }
+ const renderer = buildRenderAvatar(
+ Object.freeze({
+ activity,
+ fromUser: role === 'user',
+ [__INTERNAL_DO_NOT_USE__avatarPolymiddlewareRequestStyleOptionsSymbol]: styleOptions
+ })
+ );
- return result;
+ return renderer ? (): ReactNode => renderer({}) : false;
},
- [avatarRenderer, styleOptions]
+ [buildRenderAvatar, styleOptions]
);
}
diff --git a/packages/api/src/legacy/createActivityPolymiddlewareFromLegacy.tsx b/packages/api/src/legacy/createActivityPolymiddlewareFromLegacy.tsx
index 8135ee1040..a1f8c1e076 100644
--- a/packages/api/src/legacy/createActivityPolymiddlewareFromLegacy.tsx
+++ b/packages/api/src/legacy/createActivityPolymiddlewareFromLegacy.tsx
@@ -27,6 +27,7 @@ import {
import LegacyActivityBridge from './LegacyActivityBridge';
+const DEBUG_ORIGINAL_LEGACY_MIDDLEWARE_SYMBOL = Symbol('OriginalLegacyMiddleware');
const webChatActivitySchema = custom(value => safeParse(object({}), value).success);
type LegacyRenderFunction = (
@@ -87,7 +88,7 @@ function createActivityPolymiddlewareFromLegacy(
): ActivityPolymiddleware {
const legacyEnhancer = composeEnhancer(...middleware.map(middleware => middleware()));
- return createActivityPolymiddleware(next => {
+ const polymiddleware = createActivityPolymiddleware(next => {
const legacyHandler = legacyEnhancer(request => {
const handler = next(request);
@@ -102,6 +103,14 @@ function createActivityPolymiddlewareFromLegacy(
: undefined;
};
});
+
+ Object.defineProperty(polymiddleware, DEBUG_ORIGINAL_LEGACY_MIDDLEWARE_SYMBOL, {
+ configurable: false,
+ value: Object.freeze([...middleware]),
+ writable: false
+ });
+
+ return polymiddleware;
}
export default createActivityPolymiddlewareFromLegacy;
diff --git a/packages/api/src/legacy/createAvatarPolymiddlewareFromLegacy.tsx b/packages/api/src/legacy/createAvatarPolymiddlewareFromLegacy.tsx
new file mode 100644
index 0000000000..a547decaff
--- /dev/null
+++ b/packages/api/src/legacy/createAvatarPolymiddlewareFromLegacy.tsx
@@ -0,0 +1,103 @@
+import {
+ __INTERNAL_DO_NOT_USE__avatarPolymiddlewareRequestStyleOptionsSymbol,
+ __INTERNAL_DO_NOT_USE__legacyAvatarMiddlewareOriginalRequestSymbol,
+ avatarComponent,
+ createAvatarPolymiddleware,
+ type AvatarPolymiddleware
+} from '@msinternal/botframework-webchat-api-middleware';
+import { type LegacyAvatarMiddleware } from '@msinternal/botframework-webchat-api-middleware/legacy';
+import { composeEnhancer } from 'handler-chain';
+import React, { Fragment, memo, type ReactNode } from 'react';
+import { custom, function_, never, object, optional, pipe, readonly, safeParse, type InferInput } from 'valibot';
+
+type LegacyAvatarRenderFunction = () => Exclude;
+
+const legacyAvatarBridgeComponentPropsSchema = pipe(
+ object({
+ children: optional(never()),
+ renderFn: custom(value => safeParse(function_(), value).success)
+ }),
+ readonly()
+);
+
+type LegacyAvatarBridgeComponentProps = Readonly<
+ InferInput & { children?: never }
+>;
+
+/**
+ * Bridge component for the legacy avatar middleware.
+ * Renders the result of the legacy render function.
+ */
+function LegacyAvatarBridge(props: LegacyAvatarBridgeComponentProps) {
+ const { renderFn } = props;
+
+ return {renderFn()};
+}
+
+const MemoizedLegacyAvatarBridge = memo(LegacyAvatarBridge);
+
+/**
+ * Polyfill legacy avatar middleware into a polymiddleware.
+ *
+ * @deprecated Use `polymiddleware` instead. Legacy avatar middleware is being deprecated and will be removed on or after 2027-08-16.
+ * @param middleware An array of legacy avatar middleware.
+ * @returns A polymiddleware composed by legacy avatar middleware.
+ */
+function createAvatarPolymiddlewareFromLegacy(...middlewares: readonly LegacyAvatarMiddleware[]): AvatarPolymiddleware {
+ const legacyEnhancer = composeEnhancer(...middlewares.map(middleware => middleware()));
+
+ return createAvatarPolymiddleware(next => {
+ const legacyHandler = legacyEnhancer(
+ ({ [__INTERNAL_DO_NOT_USE__legacyAvatarMiddlewareOriginalRequestSymbol]: originalRequest }) => {
+ if (!originalRequest) {
+ // TODO: Add a test
+ throw new Error('botframework-webchat: `avatarMiddleware` must not modify the request object');
+ }
+
+ // Pass styleOptions through the polymiddleware chain via the internal runtime extension
+ // so downstream handlers (e.g. core middleware) can still read it.
+ const handler = next(originalRequest);
+
+ return !!handler && ((): Exclude => handler.render({}));
+ }
+ );
+
+ return request => {
+ const {
+ [__INTERNAL_DO_NOT_USE__avatarPolymiddlewareRequestStyleOptionsSymbol]: styleOptions,
+ activity,
+ fromUser
+ } = request;
+
+ const legacyResult = legacyHandler(
+ Object.freeze({
+ activity,
+ fromUser,
+ styleOptions,
+ [__INTERNAL_DO_NOT_USE__legacyAvatarMiddlewareOriginalRequestSymbol]: request
+ })
+ );
+
+ if (!legacyResult) {
+ return;
+ }
+
+ let props: LegacyAvatarBridgeComponentProps;
+
+ if (typeof legacyResult !== 'function') {
+ console.warn(
+ 'botframework-webchat: avatarMiddleware should return a function to render the avatar, or return false if avatar should be hidden. Please refer to HOOKS.md for details.'
+ );
+
+ props = Object.freeze({ renderFn: () => legacyResult });
+ } else {
+ props = Object.freeze({ renderFn: legacyResult });
+ }
+
+ return avatarComponent(MemoizedLegacyAvatarBridge, props);
+ };
+ });
+}
+
+export default createAvatarPolymiddlewareFromLegacy;
+export { legacyAvatarBridgeComponentPropsSchema, type LegacyAvatarBridgeComponentProps };
diff --git a/packages/api/src/types/AvatarMiddleware.ts b/packages/api/src/types/AvatarMiddleware.ts
index 351b11f9fa..50b3c3dd40 100644
--- a/packages/api/src/types/AvatarMiddleware.ts
+++ b/packages/api/src/types/AvatarMiddleware.ts
@@ -1,10 +1,15 @@
import { type WebChatActivity } from 'botframework-webchat-core';
+import type {
+ __INTERNAL_DO_NOT_USE__legacyAvatarMiddlewareOriginalRequestSymbol,
+ AvatarPolymiddlewareRequest
+} from '@msinternal/botframework-webchat-api-middleware';
import { StrictStyleOptions } from '../StyleOptions';
import ComponentMiddleware, { ComponentFactory } from './ComponentMiddleware';
type AvatarComponentFactoryArguments = [
{
+ [__INTERNAL_DO_NOT_USE__legacyAvatarMiddlewareOriginalRequestSymbol]: AvatarPolymiddlewareRequest;
activity: WebChatActivity;
fromUser: boolean;
styleOptions: StrictStyleOptions;
diff --git a/packages/bundle/src/boot/actual/middleware.ts b/packages/bundle/src/boot/actual/middleware.ts
index 9641816786..39662aec03 100644
--- a/packages/bundle/src/boot/actual/middleware.ts
+++ b/packages/bundle/src/boot/actual/middleware.ts
@@ -13,6 +13,8 @@ export {
type Polymiddleware
} from 'botframework-webchat-api/middleware';
+export { createActivityPolymiddlewareFromLegacy } from 'botframework-webchat-api/middleware';
+
export {
createErrorBoxPolymiddleware,
errorBoxComponent,
@@ -27,4 +29,18 @@ export {
type ErrorBoxPolymiddlewareRequest
} from 'botframework-webchat-api/middleware';
-export { createActivityPolymiddlewareFromLegacy } from 'botframework-webchat-api/middleware';
+export {
+ avatarComponent,
+ AvatarPolymiddlewareProxy,
+ createAvatarPolymiddleware,
+ useBuildRenderAvatarCallback,
+ type AvatarPolymiddleware,
+ type AvatarPolymiddlewareHandler,
+ type AvatarPolymiddlewareHandlerResult,
+ type AvatarPolymiddlewareProps,
+ type AvatarPolymiddlewareProxyProps,
+ type AvatarPolymiddlewareRenderer,
+ type AvatarPolymiddlewareRequest
+} from 'botframework-webchat-api/middleware';
+
+export { createAvatarPolymiddlewareFromLegacy } from 'botframework-webchat-api/middleware';
diff --git a/packages/component/src/Activity/Avatar.tsx b/packages/component/src/Activity/Avatar.tsx
index 5bd9995f44..ba405241bb 100644
--- a/packages/component/src/Activity/Avatar.tsx
+++ b/packages/component/src/Activity/Avatar.tsx
@@ -2,7 +2,7 @@ import { validateProps } from '@msinternal/botframework-webchat-react-valibot';
import React, { memo } from 'react';
import { boolean, object, optional, pipe, readonly, string, type InferInput } from 'valibot';
-import { DefaultAvatar } from '../Middleware/Avatar/createCoreMiddleware';
+import DefaultAvatar from '../Middleware/Avatar/DefaultAvatar';
const avatarPropsSchema = pipe(
object({
diff --git a/packages/component/src/Composer.tsx b/packages/component/src/Composer.tsx
index 5b651bf1ec..485ed99c1f 100644
--- a/packages/component/src/Composer.tsx
+++ b/packages/component/src/Composer.tsx
@@ -11,7 +11,7 @@ import {
type SendBoxToolbarMiddleware
} from 'botframework-webchat-api';
import { DecoratorComposer, type DecoratorMiddleware } from 'botframework-webchat-api/decorator';
-import { type Polymiddleware } from 'botframework-webchat-api/middleware';
+import { createActivityPolymiddlewareFromLegacy, type Polymiddleware } from 'botframework-webchat-api/middleware';
import { singleToArray } from 'botframework-webchat-core';
import classNames from 'classnames';
import MarkdownIt from 'markdown-it';
@@ -368,7 +368,7 @@ const Composer = ({
const theme = useTheme();
const patchedActivityMiddleware = useMemo(
- () => [...singleToArray(activityMiddleware), ...theme.activityMiddleware, ...createDefaultActivityMiddleware()],
+ () => [...singleToArray(activityMiddleware), ...theme.activityMiddleware],
[activityMiddleware, theme.activityMiddleware]
);
@@ -400,7 +400,7 @@ const Composer = ({
);
const patchedAvatarMiddleware = useMemo(
- () => [...singleToArray(avatarMiddleware), ...theme.avatarMiddleware, ...createDefaultAvatarMiddleware()],
+ () => [...singleToArray(avatarMiddleware), ...theme.avatarMiddleware],
[avatarMiddleware, theme.avatarMiddleware]
);
@@ -414,7 +414,15 @@ const Composer = ({
);
const patchedPolymiddleware = useMemo(
- () => Object.freeze([...(polymiddleware || []), ...theme.polymiddleware]),
+ () =>
+ Object.freeze([
+ ...(polymiddleware || []),
+ ...theme.polymiddleware,
+ // Polymiddleware has lower priority than legacy middleware.
+ // Later, we should move default middleware to a "default theme."
+ createActivityPolymiddlewareFromLegacy(...createDefaultActivityMiddleware()),
+ ...createDefaultAvatarMiddleware()
+ ]),
[polymiddleware, theme.polymiddleware]
);
diff --git a/packages/component/src/Middleware/Avatar/DefaultAvatar.tsx b/packages/component/src/Middleware/Avatar/DefaultAvatar.tsx
new file mode 100644
index 0000000000..9a84697b0b
--- /dev/null
+++ b/packages/component/src/Middleware/Avatar/DefaultAvatar.tsx
@@ -0,0 +1,61 @@
+import { validateProps } from '@msinternal/botframework-webchat-react-valibot';
+import classNames from 'classnames';
+import React, { memo } from 'react';
+import { boolean, never, object, optional, pipe, readonly, string, type InferInput } from 'valibot';
+
+import ImageAvatar from '../../Avatar/ImageAvatar';
+import InitialsAvatar from '../../Avatar/InitialsAvatar';
+import { useStyleToEmotionObject } from '../../hooks/internal/styleToEmotionObject';
+import useStyleSet from '../../hooks/useStyleSet';
+
+const ROOT_STYLE = {
+ overflow: ['hidden', 'clip'],
+ position: 'relative',
+
+ '> *': {
+ left: 0,
+ position: 'absolute',
+ top: 0
+ }
+};
+
+const defaultAvatarPropsSchema = pipe(
+ object({
+ 'aria-hidden': optional(boolean(), true),
+ children: optional(never()),
+ className: optional(string()),
+ fromUser: boolean()
+ }),
+ readonly()
+);
+
+type DefaultAvatarProps = InferInput;
+
+function DefaultAvatar(props: DefaultAvatarProps) {
+ const { 'aria-hidden': ariaHidden, className, fromUser } = validateProps(defaultAvatarPropsSchema, props, 'strict');
+
+ const [{ avatar: avatarStyleSet }] = useStyleSet();
+ const rootClassName = useStyleToEmotionObject()(ROOT_STYLE) + '';
+
+ return (
+
+
+
+
+ );
+}
+
+DefaultAvatar.displayName = 'DefaultAvatar';
+
+export default memo(DefaultAvatar);
+
+export { defaultAvatarPropsSchema, type DefaultAvatarProps };
diff --git a/packages/component/src/Middleware/Avatar/createCoreMiddleware.tsx b/packages/component/src/Middleware/Avatar/createCoreMiddleware.tsx
index 1b153d9933..dcd580fade 100644
--- a/packages/component/src/Middleware/Avatar/createCoreMiddleware.tsx
+++ b/packages/component/src/Middleware/Avatar/createCoreMiddleware.tsx
@@ -1,75 +1,27 @@
-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 ImageAvatar from '../../Avatar/ImageAvatar';
-import InitialsAvatar from '../../Avatar/InitialsAvatar';
-import { useStyleToEmotionObject } from '../../hooks/internal/styleToEmotionObject';
-import useStyleSet from '../../hooks/useStyleSet';
-
-const ROOT_STYLE = {
- overflow: ['hidden', 'clip'],
- position: 'relative',
-
- '> *': {
- left: 0,
- position: 'absolute',
- top: 0
- }
-};
-
-const defaultAvatarPropsSchema = pipe(
- object({
- 'aria-hidden': optional(boolean()),
- className: optional(string()),
- fromUser: boolean()
- }),
- readonly()
-);
-
-type DefaultAvatarProps = InferInput;
-
-function DefaultAvatar(props: DefaultAvatarProps) {
- const { 'aria-hidden': ariaHidden = true, className, fromUser } = validateProps(defaultAvatarPropsSchema, props);
-
- const [{ avatar: avatarStyleSet }] = useStyleSet();
- const rootClassName = useStyleToEmotionObject()(ROOT_STYLE) + '';
-
- return (
-
-
-
-
- );
+import { __INTERNAL_DO_NOT_USE__avatarPolymiddlewareRequestStyleOptionsSymbol } from 'botframework-webchat-api/internal';
+import {
+ avatarComponent,
+ createAvatarPolymiddleware,
+ type AvatarPolymiddleware
+} from 'botframework-webchat-api/middleware';
+import DefaultAvatar from './DefaultAvatar';
+
+export default function createDefaultAvatarMiddleware(): readonly AvatarPolymiddleware[] {
+ return Object.freeze([
+ createAvatarPolymiddleware(
+ _next =>
+ ({
+ fromUser,
+ [__INTERNAL_DO_NOT_USE__avatarPolymiddlewareRequestStyleOptionsSymbol]: {
+ botAvatarImage,
+ botAvatarInitials,
+ userAvatarImage,
+ userAvatarInitials
+ }
+ }) =>
+ (fromUser ? userAvatarImage || userAvatarInitials : botAvatarImage || botAvatarInitials)
+ ? avatarComponent(DefaultAvatar, Object.freeze({ fromUser }))
+ : undefined
+ )
+ ]);
}
-
-export default function createCoreAvatarMiddleware(): AvatarMiddleware[] {
- return [
- () =>
- () =>
- ({ fromUser, styleOptions }) => {
- const { botAvatarImage, botAvatarInitials, userAvatarImage, userAvatarInitials } = styleOptions;
-
- if (fromUser ? userAvatarImage || userAvatarInitials : botAvatarImage || botAvatarInitials) {
- return () => ;
- }
-
- return false;
- }
- ];
-}
-
-const MemoizedDefaultAvatar = memo(DefaultAvatar);
-
-export { MemoizedDefaultAvatar as DefaultAvatar, defaultAvatarPropsSchema, type DefaultAvatarProps };
diff --git a/packages/component/src/Transcript/hooks/useRenderActivityProps.ts b/packages/component/src/Transcript/hooks/useRenderActivityProps.ts
index 2d4041f03a..62070d0ee3 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,20 @@ const useRenderActivityProps = (activity: WebChatActivity): RenderActivityProps
const isLastInStatusGroup =
lastActivityInStatusGroup === activity || typeof lastActivityInStatusGroup === 'undefined';
- const renderAvatarForSenderGroup = useMemo(
- () => !!renderAvatar && renderAvatar({ activity }),
- [activity, renderAvatar]
- );
+ const renderAvatarForSenderGroup = useMemo Exclude)>(() => {
+ const fromUser = activity.from?.role === 'user';
+ // 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({
+ [__INTERNAL_DO_NOT_USE__avatarPolymiddlewareRequestStyleOptionsSymbol]: styleOptions,
+ activity,
+ fromUser
+ })
+ );
+
+ return renderer ? (): ReactNode => renderer({}) : false;
+ }, [activity, buildRenderAvatar, styleOptions]);
const isTopSideBotNub = isZeroOrPositive(bubbleNubOffset);
const isTopSideUserNub = isZeroOrPositive(bubbleFromUserNubOffset);
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/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);
});