diff --git a/CHANGELOG.md b/CHANGELOG.md index 80d8b8132f..4d5d3ecc06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,12 @@ Legends: ## [Unreleased] +### Added + +- Rich cards: added fill-in of `replyToId` for "IM back", "post back", and "message back" card actions, in PR [#5871](https://github.com/microsoft/BotFramework-WebChat/pull/5871), by [@compulim](https://github.com/compulim) +- `hooks/useSendMessage`: added `replyToId`, in PR [#5871](https://github.com/microsoft/BotFramework-WebChat/pull/5871), by [@compulim](https://github.com/compulim) +- `hooks/useSendMessageBack`: added `replyToId`, in PR [#5871](https://github.com/microsoft/BotFramework-WebChat/pull/5871), by [@compulim](https://github.com/compulim) + ## [4.19.2] - 2026-10-08 ### Added @@ -30,7 +36,7 @@ Legends: - ~~Refer to [this test](./__tests__/html2/adaptiveCard/signInAction.html) for the reference payload~~ - ~~Note: this implementation is based on observation of how Microsoft Teams behave and could deviate from their official implementation~~ - Obsoleted in favor of PR [#5862](https://github.com/microsoft/BotFramework-WebChat/pull/5862) -- Added card action `webchat:callURL` and [Adaptive Card action `Action.OpenUrlDialog`](https://adaptivecards.microsoft.com/?topic=Action.OpenUrlDialog) +- Added card action `webchat:callURL` and [Adaptive Card action `Action.OpenUrlDialog`](https://adaptivecards.microsoft.com/?topic=Action.OpenUrlDialog), in PR [#5862](https://github.com/microsoft/BotFramework-WebChat/pull/5862) and PR [#5863](https://github.com/microsoft/BotFramework-WebChat/pull/5863), by [@compulim](https://github.com/compulim) - The card action is designed to host popup for authentication and authorization (call-and-return pattern), it can optionally send a postback message to the bot - Open in popup, in PR [#5862](https://github.com/microsoft/BotFramework-WebChat/pull/5862), by [@compulim](https://github.com/compulim) - Added `styleOptions.callURLActionPopupWindowHeight/Width` for sizing the popup window diff --git a/__tests__/html2/avatar/layout.default.oneSide.html b/__tests__/html2/avatar/layout.default.oneSide.html index 32f5800652..6e041c26d0 100644 --- a/__tests__/html2/avatar/layout.default.oneSide.html +++ b/__tests__/html2/avatar/layout.default.oneSide.html @@ -49,6 +49,7 @@ await pageObjects.sendMessageViaSendBox('normal'); await pageConditions.numActivitiesShown(2); + await pageConditions.allOutgoingActivitiesSent(); await host.snapshot('local', { skipCheckAccessibility: true }); diff --git a/__tests__/html2/hooks/usePerformCardAction.replyToId.html b/__tests__/html2/hooks/usePerformCardAction.replyToId.html new file mode 100644 index 0000000000..a44733a852 --- /dev/null +++ b/__tests__/html2/hooks/usePerformCardAction.replyToId.html @@ -0,0 +1,71 @@ + + + + + + +
+ + + + diff --git a/__tests__/html2/hooks/useSendPostBack.replyToId.html b/__tests__/html2/hooks/useSendPostBack.replyToId.html new file mode 100644 index 0000000000..af3ed68e47 --- /dev/null +++ b/__tests__/html2/hooks/useSendPostBack.replyToId.html @@ -0,0 +1,84 @@ + + + + + + +
+ + + + diff --git a/__tests__/html2/hooks/useSendPostBack.replyToId.html.snap-1.png b/__tests__/html2/hooks/useSendPostBack.replyToId.html.snap-1.png new file mode 100644 index 0000000000..21152c76d8 Binary files /dev/null and b/__tests__/html2/hooks/useSendPostBack.replyToId.html.snap-1.png differ diff --git a/__tests__/html2/hooks/useSendPostBack.text.html b/__tests__/html2/hooks/useSendPostBack.text.html new file mode 100644 index 0000000000..724f50e2f0 --- /dev/null +++ b/__tests__/html2/hooks/useSendPostBack.text.html @@ -0,0 +1,85 @@ + + + + + + +
+ + + + diff --git a/__tests__/html2/hooks/useSendPostBack.text.html.snap-1.png b/__tests__/html2/hooks/useSendPostBack.text.html.snap-1.png new file mode 100644 index 0000000000..aa4f11d4d9 Binary files /dev/null and b/__tests__/html2/hooks/useSendPostBack.text.html.snap-1.png differ diff --git a/__tests__/html2/replyToId/cardAction/adaptiveCard.html b/__tests__/html2/replyToId/cardAction/adaptiveCard.html new file mode 100644 index 0000000000..8540f05034 --- /dev/null +++ b/__tests__/html2/replyToId/cardAction/adaptiveCard.html @@ -0,0 +1,111 @@ + + + + + + +
+ + + + diff --git a/__tests__/html2/replyToId/cardAction/adaptiveCard.html.snap-1.png b/__tests__/html2/replyToId/cardAction/adaptiveCard.html.snap-1.png new file mode 100644 index 0000000000..002d722631 Binary files /dev/null and b/__tests__/html2/replyToId/cardAction/adaptiveCard.html.snap-1.png differ diff --git a/__tests__/html2/replyToId/cardAction/animationCard.html b/__tests__/html2/replyToId/cardAction/animationCard.html new file mode 100644 index 0000000000..ba3af6ba54 --- /dev/null +++ b/__tests__/html2/replyToId/cardAction/animationCard.html @@ -0,0 +1,107 @@ + + + + + + +
+ + + + diff --git a/__tests__/html2/replyToId/cardAction/animationCard.html.snap-1.png b/__tests__/html2/replyToId/cardAction/animationCard.html.snap-1.png new file mode 100644 index 0000000000..2eccfe34c8 Binary files /dev/null and b/__tests__/html2/replyToId/cardAction/animationCard.html.snap-1.png differ diff --git a/__tests__/html2/replyToId/cardAction/audioCard.html b/__tests__/html2/replyToId/cardAction/audioCard.html new file mode 100644 index 0000000000..073c57ccfe --- /dev/null +++ b/__tests__/html2/replyToId/cardAction/audioCard.html @@ -0,0 +1,102 @@ + + + + + + +
+ + + + diff --git a/__tests__/html2/replyToId/cardAction/audioCard.html.snap-1.png b/__tests__/html2/replyToId/cardAction/audioCard.html.snap-1.png new file mode 100644 index 0000000000..821ed47962 Binary files /dev/null and b/__tests__/html2/replyToId/cardAction/audioCard.html.snap-1.png differ diff --git a/__tests__/html2/replyToId/cardAction/heroCard.html b/__tests__/html2/replyToId/cardAction/heroCard.html new file mode 100644 index 0000000000..01ef1c7f1d --- /dev/null +++ b/__tests__/html2/replyToId/cardAction/heroCard.html @@ -0,0 +1,94 @@ + + + + + + +
+ + + + diff --git a/__tests__/html2/replyToId/cardAction/heroCard.html.snap-1.png b/__tests__/html2/replyToId/cardAction/heroCard.html.snap-1.png new file mode 100644 index 0000000000..664ac91b3a Binary files /dev/null and b/__tests__/html2/replyToId/cardAction/heroCard.html.snap-1.png differ diff --git a/__tests__/html2/replyToId/cardAction/imBack.html b/__tests__/html2/replyToId/cardAction/imBack.html new file mode 100644 index 0000000000..e83da1c8ee --- /dev/null +++ b/__tests__/html2/replyToId/cardAction/imBack.html @@ -0,0 +1,98 @@ + + + + + + +
+ + + + diff --git a/__tests__/html2/replyToId/cardAction/imBack.html.snap-1.png b/__tests__/html2/replyToId/cardAction/imBack.html.snap-1.png new file mode 100644 index 0000000000..aae5e034ca Binary files /dev/null and b/__tests__/html2/replyToId/cardAction/imBack.html.snap-1.png differ diff --git a/__tests__/html2/replyToId/cardAction/messageBack.html b/__tests__/html2/replyToId/cardAction/messageBack.html new file mode 100644 index 0000000000..83f88dac98 --- /dev/null +++ b/__tests__/html2/replyToId/cardAction/messageBack.html @@ -0,0 +1,99 @@ + + + + + + +
+ + + + diff --git a/__tests__/html2/replyToId/cardAction/messageBack.html.snap-1.png b/__tests__/html2/replyToId/cardAction/messageBack.html.snap-1.png new file mode 100644 index 0000000000..23a3493f4c Binary files /dev/null and b/__tests__/html2/replyToId/cardAction/messageBack.html.snap-1.png differ diff --git a/__tests__/html2/replyToId/cardAction/oauthCard.html b/__tests__/html2/replyToId/cardAction/oauthCard.html new file mode 100644 index 0000000000..5f3063743c --- /dev/null +++ b/__tests__/html2/replyToId/cardAction/oauthCard.html @@ -0,0 +1,96 @@ + + + + + + +
+ + + + diff --git a/__tests__/html2/replyToId/cardAction/oauthCard.html.snap-1.png b/__tests__/html2/replyToId/cardAction/oauthCard.html.snap-1.png new file mode 100644 index 0000000000..664ac91b3a Binary files /dev/null and b/__tests__/html2/replyToId/cardAction/oauthCard.html.snap-1.png differ diff --git a/__tests__/html2/replyToId/cardAction/receiptCard.html b/__tests__/html2/replyToId/cardAction/receiptCard.html new file mode 100644 index 0000000000..153df80fa9 --- /dev/null +++ b/__tests__/html2/replyToId/cardAction/receiptCard.html @@ -0,0 +1,108 @@ + + + + + + +
+ + + + diff --git a/__tests__/html2/replyToId/cardAction/receiptCard.html.snap-1.png b/__tests__/html2/replyToId/cardAction/receiptCard.html.snap-1.png new file mode 100644 index 0000000000..41e232ad9a Binary files /dev/null and b/__tests__/html2/replyToId/cardAction/receiptCard.html.snap-1.png differ diff --git a/__tests__/html2/replyToId/cardAction/signInCard.html b/__tests__/html2/replyToId/cardAction/signInCard.html new file mode 100644 index 0000000000..ad1598d699 --- /dev/null +++ b/__tests__/html2/replyToId/cardAction/signInCard.html @@ -0,0 +1,94 @@ + + + + + + +
+ + + + diff --git a/__tests__/html2/replyToId/cardAction/signInCard.html.snap-1.png b/__tests__/html2/replyToId/cardAction/signInCard.html.snap-1.png new file mode 100644 index 0000000000..664ac91b3a Binary files /dev/null and b/__tests__/html2/replyToId/cardAction/signInCard.html.snap-1.png differ diff --git a/__tests__/html2/replyToId/cardAction/thumbnailCard.html b/__tests__/html2/replyToId/cardAction/thumbnailCard.html new file mode 100644 index 0000000000..b3e04b2339 --- /dev/null +++ b/__tests__/html2/replyToId/cardAction/thumbnailCard.html @@ -0,0 +1,100 @@ + + + + + + +
+ + + + diff --git a/__tests__/html2/replyToId/cardAction/thumbnailCard.html.snap-1.png b/__tests__/html2/replyToId/cardAction/thumbnailCard.html.snap-1.png new file mode 100644 index 0000000000..431c9aa19b Binary files /dev/null and b/__tests__/html2/replyToId/cardAction/thumbnailCard.html.snap-1.png differ diff --git a/__tests__/html2/replyToId/cardAction/videoCard.html b/__tests__/html2/replyToId/cardAction/videoCard.html new file mode 100644 index 0000000000..cb858ce115 --- /dev/null +++ b/__tests__/html2/replyToId/cardAction/videoCard.html @@ -0,0 +1,110 @@ + + + + + + +
+ + + + diff --git a/__tests__/html2/replyToId/cardAction/videoCard.html.snap-1.png b/__tests__/html2/replyToId/cardAction/videoCard.html.snap-1.png new file mode 100644 index 0000000000..1e4b26d796 Binary files /dev/null and b/__tests__/html2/replyToId/cardAction/videoCard.html.snap-1.png differ diff --git a/docs/API.md b/docs/API.md index 121486ea04..7712e5209c 100644 --- a/docs/API.md +++ b/docs/API.md @@ -9,18 +9,18 @@ There are several properties that you might pass into your Web Chat React Compon | `adaptiveCardHostConfig` | Pass in a custom Adaptive Cards host config. Be sure to verify your Host Config with the version of Adaptive Cards that is being used. See [Custom Host config](https://github.com/microsoft/BotFramework-WebChat/issues/2034#issuecomment-501818238) for more information. | | `attachmentMiddleware` | A chain of middleware that allows the developer to add their own custom HTML Elements on attachments. The signature is the following: `options => next => card => next(card)`. | | `attachmentRenderer` | The "flattened" version of `attachmentMiddleware`. | -| `cardActionMiddleware` | A chain of middleware that allows the developer to modify card actions, like Adaptive Cards or suggested actions. The middleware signature is the following: `cardActionMiddleware: () => next => ({ cardAction, getSignInUrl }) => next(cardAction)` | +| `cardActionMiddleware` | A chain of middleware that allows the developer to modify card actions, like Adaptive Cards or suggested actions. The middleware signature is the following: `cardActionMiddleware: () => next => request => next(request)`, where `request` contains `{ cardAction, getSignInUrl, replyToId, target }`. | | `createDirectLine` | A factory method for instantiating the Direct Line object. Azure Government users should use `createDirectLine({ domain: 'https://directline.botframework.azure.us/v3/directline', token });` to change the endpoint. The full list of parameters are: `conversationId`, `domain`, `fetch`, `pollingInterval`, `secret`, `streamUrl`, `token`, `watermark` `webSocket`. | | `createStore` | A chain of middleware that allows the developer to modify the store actions. The middleware signature is the following: `createStore: ({}, ({ dispatch }) => next => action => next(cardAction)` | | `directLine` | Specify the DirectLine object with DirectLine token. We strongly recommend using the token API for authentication instead of providing the app with your secret. To learn more about why, see the [authentication documentation](https://docs.microsoft.com/en-us/azure/bot-service/rest-api/bot-framework-rest-direct-line-3-0-authentication?view=azure-bot-service-4.0) or [connecting client app to bot](#how-to-connect-client-app-to-bot) | | `disabled` | Disable the UI (i.e. for presentation mode) of Web Chat. | -| `grammars` | Specify a grammar list for Speech (Cognitive Services Speech Services). | +| `grammars` | Specify a grammar list for Speech (Cognitive Services Speech Services). | | `groupTimeStamp` | Change default settings for timestamp groupings. | | `locale` | Indicate the default language of Web Chat. Four letter codes (such as `en-US`) are strongly recommended. | | `renderMarkdown` | Change the default Markdown renderer object. | | `sendTypingIndicator` | Display a typing signal from the user to the bot to indicate that the user is not idling. | | `store` | Specify a custom store, e.g. for adding programmatic activity to the bot. | -| `styleOptions` | Object that stores customization values for your styling of Web Chat. For the complete list of (frequently updated) default style options, please see the [defaultStyleOptions.ts](https://github.com/microsoft/BotFramework-WebChat/blob/main/packages/api/src/defaultStyleOptions.ts) and [adaptiveCards/defaultStyleOptions.ts](https://github.com/microsoft/BotFramework-WebChat/blob/main/packages/bundle/src/adaptiveCards/defaultStyleOptions.ts) file. | +| `styleOptions` | Object that stores customization values for your styling of Web Chat. For the complete list of (frequently updated) default style options, please see the [defaultStyleOptions.ts](https://github.com/microsoft/BotFramework-WebChat/blob/main/packages/api/src/defaultStyleOptions.ts) and [adaptiveCards/defaultStyleOptions.ts](https://github.com/microsoft/BotFramework-WebChat/blob/main/packages/bundle/src/adaptiveCards/defaultStyleOptions.ts) file. | | `styleSet` | The non-recommended way of overriding styles. | | `userID` | Specify a userID. There are two ways to specify the `userID`: in props, or in the token when generating the token call (`createDirectLine()`). If both methods are used to specify the userID, the token userID property will be used, and a `console.warn` will appear during runtime. If the `userID` is provided via props but is prefixed with `'dl'`, e.g. `'dl_1234'`, the value will be thrown and a new `ID` generated. If `userID` is not specified, it will default to a random user ID. Multiple users sharing the same user ID is not recommended; their user state will be shared, which creates a security risk when authenticating. | | `username` | Specify a username. | diff --git a/docs/HOOKS.md b/docs/HOOKS.md index 0c9a96068c..6cf95462fc 100644 --- a/docs/HOOKS.md +++ b/docs/HOOKS.md @@ -958,16 +958,17 @@ To stop observing scroll positions, pass a falsy value to the `observer` argumen ## `usePerformCardAction` -```js -usePerformCardAction(): ({ - displayText: string, - text: string, - type: string, - value: string -}) => void +```ts +usePerformCardAction(): ( + cardAction: DirectLineCardAction, + event?: { readonly target: EventTarget } | undefined, + init?: { readonly replyToId?: string | undefined } | undefined +) => void ``` +> New in 4.19.2: `init.replyToId` is added. + When called, this function will perform the card action based on its `type`. The card action will be performed by `cardActionMiddleware`. List of supported card action types can be found in this [Direct Line Activity card action schema](https://github.com/microsoft/botframework-sdk/blob/main/specs/botframework-activity/botframework-activity.md#type-1). @@ -1311,11 +1312,13 @@ When called, this function will send a `messageBack` activity to the bot. ```js -useSendPostBack(): (value: any) => void +useSendPostBack(): (value: any, init?: { replyToId?: string | undefined } | undefined) => void ``` -When called, this function will send a `postBack` activity to the bot. +> New in 4.19.2: Added `init.replyToId` argument. + +When called, this function will send a `postBack` activity to the bot with optionally `replyToId`. ## `useSendTimeoutForActivity` diff --git a/packages/api/src/hooks/middleware/createDefaultCardActionMiddleware.ts b/packages/api/src/hooks/middleware/createDefaultCardActionMiddleware.ts index d2b50fde2f..23f8a85e2d 100644 --- a/packages/api/src/hooks/middleware/createDefaultCardActionMiddleware.ts +++ b/packages/api/src/hooks/middleware/createDefaultCardActionMiddleware.ts @@ -19,7 +19,7 @@ export default function createDefaultCardActionMiddleware(): CardActionMiddlewar case 'imBack': if (typeof value === 'string') { // TODO: [P4] Instead of calling dispatch, we should move to dispatchers instead for completeness - dispatch(sendMessage(value, 'imBack')); + dispatch(sendMessage(value, 'imBack', { replyToId })); } else { throw new Error('cannot send "imBack" with a non-string value'); } @@ -27,7 +27,7 @@ export default function createDefaultCardActionMiddleware(): CardActionMiddlewar break; case 'messageBack': - dispatch(sendMessageBack(value, cardAction.text, cardAction.displayText)); + dispatch(sendMessageBack(value, cardAction.text, cardAction.displayText, { replyToId })); break; diff --git a/packages/api/src/hooks/useSendMessage.ts b/packages/api/src/hooks/useSendMessage.ts index 8473026285..a89461301a 100644 --- a/packages/api/src/hooks/useSendMessage.ts +++ b/packages/api/src/hooks/useSendMessage.ts @@ -4,24 +4,36 @@ import { useCallback } from 'react'; import useWebChatAPIContext from './internal/useWebChatAPIContext'; import useTrackEvent from './useTrackEvent'; -export default function useSendMessage(): ( - text?: string, - method?: string, - init?: { attachments?: readonly SendBoxAttachment[] | undefined; channelData?: any } +type UseSendMessageInit = { + readonly attachments?: readonly SendBoxAttachment[] | undefined; + readonly channelData?: any; + readonly replyToId?: string | undefined; +}; + +function useSendMessage(): ( + text?: string | undefined, + method?: string | undefined, + init?: UseSendMessageInit | undefined ) => void { const { sendMessage } = useWebChatAPIContext(); const trackEvent = useTrackEvent(); return useCallback( - (text, method, { attachments, channelData } = {}) => { + (text, method, { attachments, channelData, replyToId } = {}) => { trackEvent('sendMessage', { numAttachments: attachments?.length || 0, - // eslint-disable-next-line no-magic-numbers - sumSizeInKB: Math.round(attachments?.reduce((total, { blob: { size } }) => total + size, 0) / 1024) + sumSizeInKB: Math.round( + attachments?.reduce((total, { blob: { size } }) => total + size, 0) / + // eslint-disable-next-line no-magic-numbers + 1024 + ) }); - sendMessage(text, method, { attachments, channelData }); + sendMessage(text, method, { attachments, channelData, replyToId }); }, [sendMessage, trackEvent] ); } + +export default useSendMessage; +export { type UseSendMessageInit }; diff --git a/packages/bundle/src/adaptiveCards/Attachment/AdaptiveCardAttachment.tsx b/packages/bundle/src/adaptiveCards/Attachment/AdaptiveCardAttachment.tsx index 3a670ebd83..7eb4bf5816 100644 --- a/packages/bundle/src/adaptiveCards/Attachment/AdaptiveCardAttachment.tsx +++ b/packages/bundle/src/adaptiveCards/Attachment/AdaptiveCardAttachment.tsx @@ -27,7 +27,7 @@ function AdaptiveCardAttachment(props: AdaptiveCardAttachmentProps) { replyToId } = validateProps(adaptiveCardAttachmentPropsSchema, props); - return ; + return !!content && ; } export default memo(AdaptiveCardAttachment); diff --git a/packages/bundle/src/adaptiveCards/Attachment/AdaptiveCardRenderer.tsx b/packages/bundle/src/adaptiveCards/Attachment/AdaptiveCardRenderer.tsx index 4888cea5dc..9e46e9a3e8 100644 --- a/packages/bundle/src/adaptiveCards/Attachment/AdaptiveCardRenderer.tsx +++ b/packages/bundle/src/adaptiveCards/Attachment/AdaptiveCardRenderer.tsx @@ -28,7 +28,7 @@ import useActiveElementModEffect from './AdaptiveCardHacks/useActiveElementModEf import useDisabledModEffect from './AdaptiveCardHacks/useDisabledModEffect'; import usePersistValuesModEffect from './AdaptiveCardHacks/usePersistValuesModEffect'; import useRoleModEffect from './AdaptiveCardHacks/useRoleModEffect'; -import { directLineCardActionSchema } from './private/directLineSchema'; +import { directLineHeadlessCardActionSchema } from './private/directLineCardActionSchema'; import renderAdaptiveCard from './private/renderAdaptiveCard'; import styles from './AdaptiveCardRenderer.module.css'; @@ -41,7 +41,7 @@ const adaptiveCardRendererPropsSchema = pipe( adaptiveCard: any(), disabled: optional(boolean()), replyToId: optional(string()), - tapAction: optional(directLineCardActionSchema) + tapAction: optional(directLineHeadlessCardActionSchema) }), readonly() ); diff --git a/packages/bundle/src/adaptiveCards/Attachment/AnimationCardAttachment.js b/packages/bundle/src/adaptiveCards/Attachment/AnimationCardAttachment.js deleted file mode 100644 index 8de9f38d5e..0000000000 --- a/packages/bundle/src/adaptiveCards/Attachment/AnimationCardAttachment.js +++ /dev/null @@ -1,28 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; - -import AnimationCardContent from './AnimationCardContent'; - -const AnimationCardAttachment = ({ attachment: { content }, disabled }) => ( - -); - -AnimationCardAttachment.defaultProps = { - disabled: undefined -}; - -AnimationCardAttachment.propTypes = { - attachment: PropTypes.shape({ - content: PropTypes.shape({ - media: PropTypes.arrayOf( - PropTypes.shape({ - profile: PropTypes.string, - url: PropTypes.string.isRequired - }) - ).isRequired - }).isRequired - }).isRequired, - disabled: PropTypes.bool -}; - -export default AnimationCardAttachment; diff --git a/packages/bundle/src/adaptiveCards/Attachment/AnimationCardAttachment.tsx b/packages/bundle/src/adaptiveCards/Attachment/AnimationCardAttachment.tsx new file mode 100644 index 0000000000..fd48d689ed --- /dev/null +++ b/packages/bundle/src/adaptiveCards/Attachment/AnimationCardAttachment.tsx @@ -0,0 +1,38 @@ +import { validateProps } from '@msinternal/botframework-webchat-react-valibot'; +import React, { memo } from 'react'; +import { boolean, literal, object, optional, pipe, readonly, string, type InferInput } from 'valibot'; + +import AnimationCardContent from './AnimationCardContent'; +import { directLineMediaCardSchema } from './private/directLineSchema'; + +const animationCardAttachmentPropsSchema = pipe( + object({ + attachment: pipe( + object({ + content: optional(directLineMediaCardSchema), + contentType: literal('application/vnd.microsoft.card.animation') + }), + readonly() + ), + disabled: optional(boolean()), + replyToId: optional(string()) + }), + readonly() +); + +type AnimationCardAttachmentProps = InferInput; + +const AnimationCardAttachment = memo((props: AnimationCardAttachmentProps) => { + const { + attachment: { content }, + disabled, + replyToId + } = validateProps(animationCardAttachmentPropsSchema, props); + + return !!content && ; +}); + +AnimationCardAttachment.displayName = 'AnimationCardAttachment'; + +export default AnimationCardAttachment; +export { animationCardAttachmentPropsSchema, type AnimationCardAttachmentProps }; diff --git a/packages/bundle/src/adaptiveCards/Attachment/AnimationCardContent.tsx b/packages/bundle/src/adaptiveCards/Attachment/AnimationCardContent.tsx index 6a738306da..1a149a5a43 100644 --- a/packages/bundle/src/adaptiveCards/Attachment/AnimationCardContent.tsx +++ b/packages/bundle/src/adaptiveCards/Attachment/AnimationCardContent.tsx @@ -1,31 +1,34 @@ /* eslint react/no-array-index-key: "off" */ -import { validateProps } from '@msinternal/botframework-webchat-react-valibot'; +import { validateProps, type InferReactProps } from '@msinternal/botframework-webchat-react-valibot'; import { useStyles } from '@msinternal/botframework-webchat-styles/react'; import { Components } from 'botframework-webchat-component'; import React, { memo } from 'react'; -import { boolean, object, optional, pipe, readonly, string, type InferInput } from 'valibot'; +import { boolean, object, optional, pipe, readonly, string } from 'valibot'; +import styles from './AnimationCardContent.module.css'; import CommonCard from './CommonCard'; import { directLineMediaCardSchema } from './private/directLineSchema'; -import styles from './AnimationCardContent.module.css'; - const { ImageContent, VideoContent } = Components; const animationCardContentPropsSchema = pipe( object({ actionPerformedClassName: optional(string()), content: directLineMediaCardSchema, - disabled: optional(boolean()) + disabled: optional(boolean()), + replyToId: optional(string()) }), readonly() ); -type AnimationCardContentProps = InferInput; +type AnimationCardContentProps = InferReactProps; function AnimationCardContent(props: AnimationCardContentProps) { - const { actionPerformedClassName, content, disabled } = validateProps(animationCardContentPropsSchema, props); + const { actionPerformedClassName, content, disabled, replyToId } = validateProps( + animationCardContentPropsSchema, + props + ); const { media = [] } = content; const classNames = useStyles(styles); @@ -35,14 +38,25 @@ function AnimationCardContent(props: AnimationCardContentProps) {
    {media.map(({ profile = '', url }, index) => (
  • - {/\.gif$/iu.test(url) ? : } + {/\.gif$/iu.test(url) ? ( + + ) : ( + + )}
  • ))}
- + ); } +AnimationCardContent.displayName = 'AnimationCardContent'; + export default memo(AnimationCardContent); export { animationCardContentPropsSchema, type AnimationCardContentProps }; diff --git a/packages/bundle/src/adaptiveCards/Attachment/AudioCardAttachment.js b/packages/bundle/src/adaptiveCards/Attachment/AudioCardAttachment.js deleted file mode 100644 index b0e3d967e7..0000000000 --- a/packages/bundle/src/adaptiveCards/Attachment/AudioCardAttachment.js +++ /dev/null @@ -1,34 +0,0 @@ -/* eslint react/no-array-index-key: "off" */ - -import PropTypes from 'prop-types'; -import React from 'react'; - -import AudioCardContent from './AudioCardContent'; - -const AudioCardAttachment = ({ attachment: { content }, disabled }) => ( - -); - -AudioCardAttachment.defaultProps = { - disabled: undefined -}; - -AudioCardAttachment.propTypes = { - attachment: PropTypes.shape({ - content: PropTypes.shape({ - autostart: PropTypes.bool, - autoloop: PropTypes.bool, - image: PropTypes.shape({ - url: PropTypes.string.isRequired - }), - media: PropTypes.arrayOf( - PropTypes.shape({ - url: PropTypes.string.isRequired - }).isRequired - ).isRequired - }) - }).isRequired, - disabled: PropTypes.bool -}; - -export default AudioCardAttachment; diff --git a/packages/bundle/src/adaptiveCards/Attachment/AudioCardAttachment.tsx b/packages/bundle/src/adaptiveCards/Attachment/AudioCardAttachment.tsx new file mode 100644 index 0000000000..0b23ffc9c3 --- /dev/null +++ b/packages/bundle/src/adaptiveCards/Attachment/AudioCardAttachment.tsx @@ -0,0 +1,40 @@ +/* eslint react/no-array-index-key: "off" */ + +import { validateProps, type InferReactProps } from '@msinternal/botframework-webchat-react-valibot'; +import React, { memo } from 'react'; +import { boolean, literal, object, optional, pipe, readonly, string } from 'valibot'; + +import AudioCardContent from './AudioCardContent'; +import { directLineMediaCardSchema } from './private/directLineSchema'; + +const audioCardAttachmentPropsSchema = pipe( + object({ + attachment: pipe( + object({ + content: optional(directLineMediaCardSchema), + contentType: literal('application/vnd.microsoft.card.audio') + }), + readonly() + ), + disabled: optional(boolean()), + replyToId: optional(string()) + }), + readonly() +); + +type AudioCardAttachmentProps = InferReactProps; + +const AudioCardAttachment = memo((props: AudioCardAttachmentProps) => { + const { + attachment: { content }, + disabled, + replyToId + } = validateProps(audioCardAttachmentPropsSchema, props); + + return !!content && ; +}); + +AudioCardAttachment.displayName = 'AudioCardAttachment'; + +export default AudioCardAttachment; +export { audioCardAttachmentPropsSchema, type AudioCardAttachmentProps }; diff --git a/packages/bundle/src/adaptiveCards/Attachment/AudioCardContent.tsx b/packages/bundle/src/adaptiveCards/Attachment/AudioCardContent.tsx index 5579a820ee..f7d56c68ed 100644 --- a/packages/bundle/src/adaptiveCards/Attachment/AudioCardContent.tsx +++ b/packages/bundle/src/adaptiveCards/Attachment/AudioCardContent.tsx @@ -1,10 +1,10 @@ /* eslint react/no-array-index-key: "off" */ -import { validateProps } from '@msinternal/botframework-webchat-react-valibot'; +import { validateProps, type InferReactProps } from '@msinternal/botframework-webchat-react-valibot'; import { useStyles } from '@msinternal/botframework-webchat-styles/react'; import { Components } from 'botframework-webchat-component'; import React, { memo } from 'react'; -import { boolean, object, optional, pipe, readonly, string, type InferInput } from 'valibot'; +import { boolean, object, optional, pipe, readonly, string } from 'valibot'; import CommonCard from './CommonCard'; import { directLineMediaCardSchema } from './private/directLineSchema'; @@ -17,15 +17,16 @@ const audioCardContentPropsSchema = pipe( object({ actionPerformedClassName: optional(string()), content: directLineMediaCardSchema, - disabled: optional(boolean()) + disabled: optional(boolean()), + replyToId: optional(string()) }), readonly() ); -type AudioCardContentProps = InferInput; +type AudioCardContentProps = InferReactProps; function AudioCardContent(props: AudioCardContentProps) { - const { actionPerformedClassName, content, disabled } = validateProps(audioCardContentPropsSchema, props); + const { actionPerformedClassName, content, disabled, replyToId } = validateProps(audioCardContentPropsSchema, props); const { autostart = false, autoloop = false, image: { url: imageURL = '' } = {}, media = [] } = content; const classNames = useStyles(styles); @@ -39,7 +40,12 @@ function AudioCardContent(props: AudioCardContentProps) { ))} - + ); } diff --git a/packages/bundle/src/adaptiveCards/Attachment/CommonCard.js b/packages/bundle/src/adaptiveCards/Attachment/CommonCard.js deleted file mode 100644 index b4c960ef23..0000000000 --- a/packages/bundle/src/adaptiveCards/Attachment/CommonCard.js +++ /dev/null @@ -1,51 +0,0 @@ -import PropTypes from 'prop-types'; -import React, { useMemo } from 'react'; - -import { hooks } from 'botframework-webchat-component'; - -import AdaptiveCardBuilder from './AdaptiveCardBuilder'; -import AdaptiveCardRenderer from './AdaptiveCardRenderer'; -import useAdaptiveCardsPackage from '../hooks/useAdaptiveCardsPackage'; -import useStyleOptions from '../../hooks/useStyleOptions'; - -const { useDirection } = hooks; - -const CommonCard = ({ actionPerformedClassName, content, disabled }) => { - const [adaptiveCardsPackage] = useAdaptiveCardsPackage(); - const [direction] = useDirection(); - const [styleOptions] = useStyleOptions(); - - const builtCard = useMemo(() => { - if (content) { - const builder = new AdaptiveCardBuilder(adaptiveCardsPackage, styleOptions, direction); - - builder.addCommon(content); - - return builder.card; - } - }, [adaptiveCardsPackage, content, direction, styleOptions]); - - return ( - - ); -}; - -CommonCard.defaultProps = { - actionPerformedClassName: '', - disabled: undefined -}; - -CommonCard.propTypes = { - actionPerformedClassName: PropTypes.string, - content: PropTypes.shape({ - tap: PropTypes.any - }).isRequired, - disabled: PropTypes.bool -}; - -export default CommonCard; diff --git a/packages/bundle/src/adaptiveCards/Attachment/CommonCard.tsx b/packages/bundle/src/adaptiveCards/Attachment/CommonCard.tsx new file mode 100644 index 0000000000..b0c9e6cb98 --- /dev/null +++ b/packages/bundle/src/adaptiveCards/Attachment/CommonCard.tsx @@ -0,0 +1,63 @@ +import { validateProps, type InferReactProps } from '@msinternal/botframework-webchat-react-valibot'; +import { hooks } from 'botframework-webchat-component'; +import React, { memo, useMemo } from 'react'; +import { array, boolean, object, optional, pipe, readonly, string } from 'valibot'; + +import useStyleOptions from '../../hooks/useStyleOptions'; +import useAdaptiveCardsPackage from '../hooks/useAdaptiveCardsPackage'; +import AdaptiveCardBuilder from './AdaptiveCardBuilder'; +import AdaptiveCardRenderer from './AdaptiveCardRenderer'; +import directLineCardActionSchema, { directLineHeadlessCardActionSchema } from './private/directLineCardActionSchema'; + +const { useDirection } = hooks; + +const commonCardPropsSchema = pipe( + object({ + actionPerformedClassName: optional(string()), + content: object({ + buttons: optional(pipe(array(directLineCardActionSchema), readonly())), + subtitle: optional(string()), + text: optional(string()), + title: optional(string()), + tap: optional(directLineHeadlessCardActionSchema) + }), + disabled: optional(boolean()), + replyToId: optional(string()) + }), + readonly() +); + +type CommonCardProps = InferReactProps; + +const CommonCard = memo((props: CommonCardProps) => { + const { actionPerformedClassName, content, disabled, replyToId } = validateProps(commonCardPropsSchema, props); + + const [adaptiveCardsPackage] = useAdaptiveCardsPackage(); + const [direction] = useDirection(); + const [styleOptions] = useStyleOptions(); + + const builtCard = useMemo(() => { + if (content) { + const builder = new AdaptiveCardBuilder(adaptiveCardsPackage, styleOptions, direction); + + builder.addCommon(content); + + return builder.card; + } + }, [adaptiveCardsPackage, content, direction, styleOptions]); + + return ( + + ); +}); + +CommonCard.displayName = 'CommonCard'; + +export default CommonCard; +export { commonCardPropsSchema, type CommonCardProps }; diff --git a/packages/bundle/src/adaptiveCards/Attachment/HeroCardAttachment.js b/packages/bundle/src/adaptiveCards/Attachment/HeroCardAttachment.js deleted file mode 100644 index 4ba7b71feb..0000000000 --- a/packages/bundle/src/adaptiveCards/Attachment/HeroCardAttachment.js +++ /dev/null @@ -1,33 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; - -import HeroCardContent from './HeroCardContent'; - -const HeroCardAttachment = ({ attachment: { content } = {}, disabled }) => - !!content && ; - -HeroCardAttachment.defaultProps = { - disabled: undefined -}; - -HeroCardAttachment.propTypes = { - attachment: PropTypes.shape({ - content: PropTypes.shape({ - buttons: PropTypes.any, - images: PropTypes.arrayOf( - PropTypes.shape({ - alt: PropTypes.string, - tap: PropTypes.any, - url: PropTypes.string.isRequired - }) - ), - subtitle: PropTypes.string, - tap: PropTypes.any, - text: PropTypes.string, - title: PropTypes.string - }).isRequired - }).isRequired, - disabled: PropTypes.bool -}; - -export default HeroCardAttachment; diff --git a/packages/bundle/src/adaptiveCards/Attachment/HeroCardAttachment.tsx b/packages/bundle/src/adaptiveCards/Attachment/HeroCardAttachment.tsx new file mode 100644 index 0000000000..0e584f938a --- /dev/null +++ b/packages/bundle/src/adaptiveCards/Attachment/HeroCardAttachment.tsx @@ -0,0 +1,31 @@ +import { validateProps, type InferReactProps } from '@msinternal/botframework-webchat-react-valibot'; +import React, { memo } from 'react'; +import { boolean, literal, object, optional, pipe, readonly, string } from 'valibot'; + +import HeroCardContent from './HeroCardContent'; +import { directLineBasicCardSchema } from './private/directLineSchema'; + +const heroCardAttachmentPropsSchema = pipe( + object({ + attachment: object({ + content: optional(directLineBasicCardSchema), + contentType: literal('application/vnd.microsoft.card.hero') + }), + disabled: optional(boolean()), + replyToId: optional(string()) + }), + readonly() +); + +type HeroCardAttachmentProps = InferReactProps; + +const HeroCardAttachment = memo((props: HeroCardAttachmentProps) => { + const { attachment: { content } = {}, disabled, replyToId } = validateProps(heroCardAttachmentPropsSchema, props); + + return !!content && ; +}); + +HeroCardAttachment.displayName = 'HeroCardAttachment'; + +export default HeroCardAttachment; +export { heroCardAttachmentPropsSchema, type HeroCardAttachmentProps }; diff --git a/packages/bundle/src/adaptiveCards/Attachment/HeroCardContent.tsx b/packages/bundle/src/adaptiveCards/Attachment/HeroCardContent.tsx index 7db0058223..c353c3d5de 100644 --- a/packages/bundle/src/adaptiveCards/Attachment/HeroCardContent.tsx +++ b/packages/bundle/src/adaptiveCards/Attachment/HeroCardContent.tsx @@ -1,8 +1,8 @@ -import { validateProps } from '@msinternal/botframework-webchat-react-valibot'; +import { validateProps, type InferReactProps } from '@msinternal/botframework-webchat-react-valibot'; import { hooks } from 'botframework-webchat-component'; import { type DirectLineCardAction } from 'botframework-webchat-core'; import React, { memo, useMemo } from 'react'; -import { boolean, object, optional, pipe, readonly, string, type InferInput } from 'valibot'; +import { boolean, object, optional, pipe, readonly, string } from 'valibot'; import useStyleOptions from '../../hooks/useStyleOptions'; import useAdaptiveCardsPackage from '../hooks/useAdaptiveCardsPackage'; @@ -16,15 +16,16 @@ const heroCardContentPropsSchema = pipe( object({ actionPerformedClassName: optional(string()), content: directLineBasicCardSchema, - disabled: optional(boolean()) + disabled: optional(boolean()), + replyToId: optional(string()) }), readonly() ); -type HeroCardContentProps = InferInput; +type HeroCardContentProps = InferReactProps; function HeroCardContent(props: HeroCardContentProps) { - const { actionPerformedClassName, content, disabled } = validateProps(heroCardContentPropsSchema, props); + const { actionPerformedClassName, content, disabled, replyToId } = validateProps(heroCardContentPropsSchema, props); const [adaptiveCardsPackage] = useAdaptiveCardsPackage(); const [styleOptions] = useStyleOptions(); @@ -51,10 +52,13 @@ function HeroCardContent(props: HeroCardContentProps) { actionPerformedClassName={actionPerformedClassName} adaptiveCard={builtCard} disabled={disabled} + replyToId={replyToId} tapAction={content && content.tap} /> ); } +HeroCardContent.displayName = 'HeroCardContent'; + export default memo(HeroCardContent); export { heroCardContentPropsSchema, type HeroCardContentProps }; diff --git a/packages/bundle/src/adaptiveCards/Attachment/OAuthCardAttachment.js b/packages/bundle/src/adaptiveCards/Attachment/OAuthCardAttachment.js deleted file mode 100644 index 189f04bdae..0000000000 --- a/packages/bundle/src/adaptiveCards/Attachment/OAuthCardAttachment.js +++ /dev/null @@ -1,23 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; - -import OAuthCardContent from './OAuthCardContent'; - -const OAuthCardAttachment = ({ attachment: { content } = {}, disabled }) => ( - -); - -OAuthCardAttachment.defaultProps = { - disabled: undefined -}; - -OAuthCardAttachment.propTypes = { - attachment: PropTypes.shape({ - content: PropTypes.shape({ - buttons: PropTypes.array - }).isRequired - }).isRequired, - disabled: PropTypes.bool -}; - -export default OAuthCardAttachment; diff --git a/packages/bundle/src/adaptiveCards/Attachment/OAuthCardAttachment.tsx b/packages/bundle/src/adaptiveCards/Attachment/OAuthCardAttachment.tsx new file mode 100644 index 0000000000..09dd902a86 --- /dev/null +++ b/packages/bundle/src/adaptiveCards/Attachment/OAuthCardAttachment.tsx @@ -0,0 +1,34 @@ +import React, { memo } from 'react'; + +import { validateProps, type InferReactProps } from '@msinternal/botframework-webchat-react-valibot'; +import { boolean, literal, object, optional, pipe, readonly, string } from 'valibot'; +import OAuthCardContent from './OAuthCardContent'; +import { directLineSignInCardSchema } from './private/directLineSchema'; + +const oauthCardAttachmentPropsSchema = pipe( + object({ + attachment: pipe( + object({ + content: optional(directLineSignInCardSchema), + contentType: literal('application/vnd.microsoft.card.oauth') + }), + readonly() + ), + disabled: optional(boolean()), + replyToId: optional(string()) + }), + readonly() +); + +type OAuthCardAttachmentProps = InferReactProps; + +const OAuthCardAttachment = memo((props: OAuthCardAttachmentProps) => { + const { attachment: { content } = {}, disabled, replyToId } = validateProps(oauthCardAttachmentPropsSchema, props); + + return !!content && ; +}); + +OAuthCardAttachment.displayName = 'OAuthCardAttachment'; + +export default OAuthCardAttachment; +export { oauthCardAttachmentPropsSchema, type OAuthCardAttachmentProps }; diff --git a/packages/bundle/src/adaptiveCards/Attachment/OAuthCardContent.tsx b/packages/bundle/src/adaptiveCards/Attachment/OAuthCardContent.tsx index 609143e868..ea8722b3ad 100644 --- a/packages/bundle/src/adaptiveCards/Attachment/OAuthCardContent.tsx +++ b/packages/bundle/src/adaptiveCards/Attachment/OAuthCardContent.tsx @@ -1,7 +1,7 @@ -import { validateProps } from '@msinternal/botframework-webchat-react-valibot'; +import { validateProps, type InferReactProps } from '@msinternal/botframework-webchat-react-valibot'; import { hooks } from 'botframework-webchat-component'; import React, { memo, useMemo } from 'react'; -import { boolean, object, optional, pipe, readonly, string, type InferInput } from 'valibot'; +import { boolean, object, optional, pipe, readonly, string } from 'valibot'; import useStyleOptions from '../../hooks/useStyleOptions'; import useAdaptiveCardsPackage from '../hooks/useAdaptiveCardsPackage'; @@ -15,15 +15,16 @@ const oauthCardContentPropsSchema = pipe( object({ actionPerformedClassName: optional(string()), content: directLineSignInCardSchema, - disabled: optional(boolean()) + disabled: optional(boolean()), + replyToId: optional(string()) }), readonly() ); -type OAuthCardContentProps = InferInput; +type OAuthCardContentProps = InferReactProps; function OAuthCardContent(props: OAuthCardContentProps) { - const { actionPerformedClassName, content, disabled } = validateProps(oauthCardContentPropsSchema, props); + const { actionPerformedClassName, content, disabled, replyToId } = validateProps(oauthCardContentPropsSchema, props); const [adaptiveCardsPackage] = useAdaptiveCardsPackage(); const [direction] = useDirection(); @@ -45,6 +46,7 @@ function OAuthCardContent(props: OAuthCardContentProps) { actionPerformedClassName={actionPerformedClassName} adaptiveCard={builtCard} disabled={disabled} + replyToId={replyToId} /> ); } diff --git a/packages/bundle/src/adaptiveCards/Attachment/ReceiptCardAttachment.js b/packages/bundle/src/adaptiveCards/Attachment/ReceiptCardAttachment.js deleted file mode 100644 index 5f75b9bbaf..0000000000 --- a/packages/bundle/src/adaptiveCards/Attachment/ReceiptCardAttachment.js +++ /dev/null @@ -1,49 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; - -import ReceiptCardContent from './ReceiptCardContent'; - -const ReceiptCardAttachment = ({ attachment: { content }, disabled }) => ( - -); - -ReceiptCardAttachment.defaultProps = { - disabled: undefined -}; - -ReceiptCardAttachment.propTypes = { - attachment: PropTypes.shape({ - content: PropTypes.shape({ - buttons: PropTypes.array, - facts: PropTypes.arrayOf( - PropTypes.shape({ - key: PropTypes.string, - value: PropTypes.string - }) - ), - items: PropTypes.arrayOf( - PropTypes.shape({ - image: PropTypes.shape({ - alt: PropTypes.string.isRequired, - tap: PropTypes.any, - url: PropTypes.string.isRequired - }), - price: PropTypes.string.isRequired, - quantity: PropTypes.string, - subtitle: PropTypes.string, - tap: PropTypes.any, - text: PropTypes.string, - title: PropTypes.string.isRequired - }) - ), - tap: PropTypes.any, - tax: PropTypes.string, - title: PropTypes.string, - total: PropTypes.string, - vat: PropTypes.string - }).isRequired - }).isRequired, - disabled: PropTypes.bool -}; - -export default ReceiptCardAttachment; diff --git a/packages/bundle/src/adaptiveCards/Attachment/ReceiptCardAttachment.tsx b/packages/bundle/src/adaptiveCards/Attachment/ReceiptCardAttachment.tsx new file mode 100644 index 0000000000..f613ead18a --- /dev/null +++ b/packages/bundle/src/adaptiveCards/Attachment/ReceiptCardAttachment.tsx @@ -0,0 +1,38 @@ +import { validateProps, type InferReactProps } from '@msinternal/botframework-webchat-react-valibot'; +import React, { memo } from 'react'; +import { boolean, literal, object, optional, pipe, readonly, string } from 'valibot'; + +import ReceiptCardContent from './ReceiptCardContent'; +import { directLineReceiptCardSchema } from './private/directLineSchema'; + +const receiptCardAttachmentPropsSchema = pipe( + object({ + attachment: pipe( + object({ + content: optional(directLineReceiptCardSchema), + contentType: literal('application/vnd.microsoft.card.receipt') + }), + readonly() + ), + disabled: optional(boolean()), + replyToId: optional(string()) + }), + readonly() +); + +type ReceiptCardAttachmentProps = InferReactProps; + +const ReceiptCardAttachment = memo((props: ReceiptCardAttachmentProps) => { + const { + attachment: { content }, + disabled, + replyToId + } = validateProps(receiptCardAttachmentPropsSchema, props); + + return !!content && ; +}); + +ReceiptCardAttachment.displayName = 'ReceiptCardAttachment'; + +export default ReceiptCardAttachment; +export { receiptCardAttachmentPropsSchema, type ReceiptCardAttachmentProps }; diff --git a/packages/bundle/src/adaptiveCards/Attachment/ReceiptCardContent.tsx b/packages/bundle/src/adaptiveCards/Attachment/ReceiptCardContent.tsx index 938ac39750..52ef7f8d33 100644 --- a/packages/bundle/src/adaptiveCards/Attachment/ReceiptCardContent.tsx +++ b/packages/bundle/src/adaptiveCards/Attachment/ReceiptCardContent.tsx @@ -1,10 +1,10 @@ /* eslint no-magic-numbers: ["error", { "ignore": [0, 1, 10, 15, 25, 50, 75] }] */ -import { validateProps } from '@msinternal/botframework-webchat-react-valibot'; +import { validateProps, type InferReactProps } from '@msinternal/botframework-webchat-react-valibot'; import { hooks } from 'botframework-webchat-component'; import { type DirectLineCardAction } from 'botframework-webchat-core'; import React, { memo, useMemo } from 'react'; -import { boolean, object, optional, pipe, readonly, string, type InferInput } from 'valibot'; +import { boolean, object, optional, pipe, readonly, string } from 'valibot'; import useStyleOptions from '../../hooks/useStyleOptions'; import useAdaptiveCardsPackage from '../hooks/useAdaptiveCardsPackage'; @@ -22,15 +22,19 @@ const receiptCardContentPropsSchema = pipe( object({ actionPerformedClassName: optional(string()), content: directLineReceiptCardSchema, - disabled: optional(boolean()) + disabled: optional(boolean()), + replyToId: optional(string()) }), readonly() ); -type ReceiptCardContentProps = InferInput; +type ReceiptCardContentProps = InferReactProps; function ReceiptCardContent(props: ReceiptCardContentProps) { - const { actionPerformedClassName, content, disabled } = validateProps(receiptCardContentPropsSchema, props); + const { actionPerformedClassName, content, disabled, replyToId } = validateProps( + receiptCardContentPropsSchema, + props + ); const [adaptiveCardsPackage] = useAdaptiveCardsPackage(); const [direction] = useDirection(); @@ -127,6 +131,7 @@ function ReceiptCardContent(props: ReceiptCardContentProps) { actionPerformedClassName={actionPerformedClassName} adaptiveCard={builtCard} disabled={disabled} + replyToId={replyToId} tapAction={content && content.tap} /> ); diff --git a/packages/bundle/src/adaptiveCards/Attachment/SignInCardAttachment.js b/packages/bundle/src/adaptiveCards/Attachment/SignInCardAttachment.js deleted file mode 100644 index 295d5f644e..0000000000 --- a/packages/bundle/src/adaptiveCards/Attachment/SignInCardAttachment.js +++ /dev/null @@ -1,21 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; - -import SignInCardContent from './SignInCardContent'; - -const SignInCardAttachment = ({ attachment: { content }, disabled }) => ( - -); - -SignInCardAttachment.defaultProps = { - disabled: undefined -}; - -SignInCardAttachment.propTypes = { - attachment: PropTypes.shape({ - content: PropTypes.any.isRequired - }).isRequired, - disabled: PropTypes.bool -}; - -export default SignInCardAttachment; diff --git a/packages/bundle/src/adaptiveCards/Attachment/SignInCardAttachment.tsx b/packages/bundle/src/adaptiveCards/Attachment/SignInCardAttachment.tsx new file mode 100644 index 0000000000..8371cc130a --- /dev/null +++ b/packages/bundle/src/adaptiveCards/Attachment/SignInCardAttachment.tsx @@ -0,0 +1,38 @@ +import { validateProps, type InferReactProps } from '@msinternal/botframework-webchat-react-valibot'; +import React, { memo } from 'react'; +import { boolean, literal, object, optional, pipe, readonly, string } from 'valibot'; + +import { directLineSignInCardSchema } from './private/directLineSchema'; +import SignInCardContent from './SignInCardContent'; + +const signInCardAttachmentPropsSchema = pipe( + object({ + attachment: pipe( + object({ + content: optional(directLineSignInCardSchema), + contentType: literal('application/vnd.microsoft.card.signin') + }), + readonly() + ), + disabled: optional(boolean()), + replyToId: optional(string()) + }), + readonly() +); + +type SignInCardAttachmentProps = InferReactProps; + +const SignInCardAttachment = memo((props: SignInCardAttachmentProps) => { + const { + attachment: { content }, + disabled, + replyToId + } = validateProps(signInCardAttachmentPropsSchema, props); + + return !!content && ; +}); + +SignInCardAttachment.displayName = 'SignInCardAttachment'; + +export default SignInCardAttachment; +export { signInCardAttachmentPropsSchema, type SignInCardAttachmentProps }; diff --git a/packages/bundle/src/adaptiveCards/Attachment/SignInCardContent.tsx b/packages/bundle/src/adaptiveCards/Attachment/SignInCardContent.tsx index b9a57bca9c..57887dc9ff 100644 --- a/packages/bundle/src/adaptiveCards/Attachment/SignInCardContent.tsx +++ b/packages/bundle/src/adaptiveCards/Attachment/SignInCardContent.tsx @@ -1,7 +1,7 @@ -import { validateProps } from '@msinternal/botframework-webchat-react-valibot'; +import { validateProps, type InferReactProps } from '@msinternal/botframework-webchat-react-valibot'; import { useStyles } from '@msinternal/botframework-webchat-styles/react'; import React, { memo } from 'react'; -import { boolean, object, optional, pipe, readonly, string, type InferInput } from 'valibot'; +import { boolean, object, optional, pipe, readonly, string } from 'valibot'; import CommonCard from './CommonCard'; import { directLineSignInCardSchema } from './private/directLineSchema'; @@ -12,21 +12,27 @@ const signInCardContentPropsSchema = pipe( object({ actionPerformedClassName: optional(string()), content: directLineSignInCardSchema, - disabled: optional(boolean()) + disabled: optional(boolean()), + replyToId: optional(string()) }), readonly() ); -type SignInCardContentProps = InferInput; +type SignInCardContentProps = InferReactProps; function SignInCardContent(props: SignInCardContentProps) { - const { actionPerformedClassName, content, disabled } = validateProps(signInCardContentPropsSchema, props); + const { actionPerformedClassName, content, disabled, replyToId } = validateProps(signInCardContentPropsSchema, props); const classNames = useStyles(styles); return (
- +
); } diff --git a/packages/bundle/src/adaptiveCards/Attachment/ThumbnailCardAttachment.js b/packages/bundle/src/adaptiveCards/Attachment/ThumbnailCardAttachment.js deleted file mode 100644 index e73a3dcfe9..0000000000 --- a/packages/bundle/src/adaptiveCards/Attachment/ThumbnailCardAttachment.js +++ /dev/null @@ -1,34 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; - -import ThumbnailCardContent from './ThumbnailCardContent'; - -const ThumbnailCardAttachment = ({ attachment: { content }, disabled }) => ( - -); - -ThumbnailCardAttachment.defaultProps = { - disabled: undefined -}; - -ThumbnailCardAttachment.propTypes = { - attachment: PropTypes.shape({ - content: PropTypes.shape({ - buttons: PropTypes.array, - images: PropTypes.arrayOf( - PropTypes.shape({ - alt: PropTypes.string.isRequired, - tap: PropTypes.any, - url: PropTypes.string.isRequired - }) - ), - subtitle: PropTypes.string, - tap: PropTypes.any, - text: PropTypes.string, - title: PropTypes.string - }).isRequired - }).isRequired, - disabled: PropTypes.bool -}; - -export default ThumbnailCardAttachment; diff --git a/packages/bundle/src/adaptiveCards/Attachment/ThumbnailCardAttachment.tsx b/packages/bundle/src/adaptiveCards/Attachment/ThumbnailCardAttachment.tsx new file mode 100644 index 0000000000..e371f378ac --- /dev/null +++ b/packages/bundle/src/adaptiveCards/Attachment/ThumbnailCardAttachment.tsx @@ -0,0 +1,38 @@ +import React, { memo } from 'react'; +import { boolean, literal, object, optional, pipe, readonly, string } from 'valibot'; + +import { validateProps, type InferReactProps } from '@msinternal/botframework-webchat-react-valibot'; +import ThumbnailCardContent from './ThumbnailCardContent'; +import { directLineBasicCardSchema } from './private/directLineSchema'; + +const thumbnailCardAttachmentPropsSchema = pipe( + object({ + attachment: pipe( + object({ + content: optional(directLineBasicCardSchema), + contentType: literal('application/vnd.microsoft.card.thumbnail') + }), + readonly() + ), + disabled: optional(boolean()), + replyToId: optional(string()) + }), + readonly() +); + +type ThumbnailCardAttachmentProps = InferReactProps; + +const ThumbnailCardAttachment = memo((props: ThumbnailCardAttachmentProps) => { + const { + attachment: { content }, + disabled, + replyToId + } = validateProps(thumbnailCardAttachmentPropsSchema, props); + + return !!content && ; +}); + +ThumbnailCardAttachment.displayName = 'ThumbnailCardAttachment'; + +export default ThumbnailCardAttachment; +export { thumbnailCardAttachmentPropsSchema, type ThumbnailCardAttachmentProps }; diff --git a/packages/bundle/src/adaptiveCards/Attachment/ThumbnailCardContent.tsx b/packages/bundle/src/adaptiveCards/Attachment/ThumbnailCardContent.tsx index 72fe9cb8d9..c35905031e 100644 --- a/packages/bundle/src/adaptiveCards/Attachment/ThumbnailCardContent.tsx +++ b/packages/bundle/src/adaptiveCards/Attachment/ThumbnailCardContent.tsx @@ -2,8 +2,9 @@ import { hooks } from 'botframework-webchat-component'; import React, { memo, useMemo } from 'react'; -import { boolean, object, optional, parse, pipe, readonly, string, type InferInput } from 'valibot'; +import { boolean, object, optional, parse, pipe, readonly, string } from 'valibot'; +import type { InferReactProps } from '@msinternal/botframework-webchat-react-valibot'; import useStyleOptions from '../../hooks/useStyleOptions'; import useAdaptiveCardsPackage from '../hooks/useAdaptiveCardsPackage'; import AdaptiveCardBuilder from './AdaptiveCardBuilder'; @@ -16,15 +17,16 @@ const thumbnailCardContentPropsSchema = pipe( object({ actionPerformedClassName: optional(string()), content: directLineBasicCardSchema, - disabled: optional(boolean()) + disabled: optional(boolean()), + replyToId: optional(string()) }), readonly() ); -type ThumbnailCardContentProps = InferInput; +type ThumbnailCardContentProps = InferReactProps; function ThumbnailCardContent(props: ThumbnailCardContentProps) { - const { actionPerformedClassName, content, disabled } = parse(thumbnailCardContentPropsSchema, props); + const { actionPerformedClassName, content, disabled, replyToId } = parse(thumbnailCardContentPropsSchema, props); const [adaptiveCardsPackage] = useAdaptiveCardsPackage(); const [direction] = useDirection(); @@ -63,6 +65,7 @@ function ThumbnailCardContent(props: ThumbnailCardContentProps) { actionPerformedClassName={actionPerformedClassName} adaptiveCard={builtCard} disabled={disabled} + replyToId={replyToId} tapAction={content && content.tap} /> ); diff --git a/packages/bundle/src/adaptiveCards/Attachment/VideoCardAttachment.js b/packages/bundle/src/adaptiveCards/Attachment/VideoCardAttachment.js deleted file mode 100644 index a65e775e2f..0000000000 --- a/packages/bundle/src/adaptiveCards/Attachment/VideoCardAttachment.js +++ /dev/null @@ -1,35 +0,0 @@ -/* eslint react/no-array-index-key: "off" */ - -import PropTypes from 'prop-types'; -import React from 'react'; - -import VideoCardContent from './VideoCardContent'; - -const VideoCardAttachment = ({ attachment: { content }, disabled }) => ( - -); - -VideoCardAttachment.defaultProps = { - disabled: undefined -}; - -VideoCardAttachment.propTypes = { - attachment: PropTypes.shape({ - content: PropTypes.shape({ - autoloop: PropTypes.bool, - autostart: PropTypes.bool, - image: PropTypes.shape({ - url: PropTypes.string.isRequired - }), - media: PropTypes.arrayOf( - PropTypes.shape({ - profile: PropTypes.string.isRequired, - url: PropTypes.string - }) - ) - }) - }).isRequired, - disabled: PropTypes.bool -}; - -export default VideoCardAttachment; diff --git a/packages/bundle/src/adaptiveCards/Attachment/VideoCardAttachment.tsx b/packages/bundle/src/adaptiveCards/Attachment/VideoCardAttachment.tsx new file mode 100644 index 0000000000..b199d3abf4 --- /dev/null +++ b/packages/bundle/src/adaptiveCards/Attachment/VideoCardAttachment.tsx @@ -0,0 +1,40 @@ +/* eslint react/no-array-index-key: "off" */ + +import { validateProps, type InferReactProps } from '@msinternal/botframework-webchat-react-valibot'; +import React, { memo } from 'react'; +import { boolean, literal, object, optional, pipe, readonly, string } from 'valibot'; + +import VideoCardContent from './VideoCardContent'; +import { directLineMediaCardSchema } from './private/directLineSchema'; + +const videoCardAttachmentPropsSchema = pipe( + object({ + attachment: pipe( + object({ + content: optional(directLineMediaCardSchema), + contentType: literal('application/vnd.microsoft.card.video') + }), + readonly() + ), + disabled: optional(boolean()), + replyToId: optional(string()) + }), + readonly() +); + +type VideoCardAttachmentProps = InferReactProps; + +const VideoCardAttachment = memo((props: VideoCardAttachmentProps) => { + const { + attachment: { content }, + disabled, + replyToId + } = validateProps(videoCardAttachmentPropsSchema, props); + + return !!content && ; +}); + +VideoCardAttachment.displayName = 'VideoCardAttachment'; + +export default VideoCardAttachment; +export { videoCardAttachmentPropsSchema, type VideoCardAttachmentProps }; diff --git a/packages/bundle/src/adaptiveCards/Attachment/VideoCardContent.tsx b/packages/bundle/src/adaptiveCards/Attachment/VideoCardContent.tsx index 61b7e772f6..5ee76622a4 100644 --- a/packages/bundle/src/adaptiveCards/Attachment/VideoCardContent.tsx +++ b/packages/bundle/src/adaptiveCards/Attachment/VideoCardContent.tsx @@ -1,9 +1,10 @@ /* eslint react/no-array-index-key: "off" */ +import type { InferReactProps } from '@msinternal/botframework-webchat-react-valibot'; import { useStyles } from '@msinternal/botframework-webchat-styles/react'; import { Components } from 'botframework-webchat-component'; import React, { memo } from 'react'; -import { boolean, object, optional, parse, pipe, readonly, string, type InferInput } from 'valibot'; +import { boolean, object, optional, parse, pipe, readonly, string } from 'valibot'; import CommonCard from './CommonCard'; import { directLineMediaCardSchema } from './private/directLineSchema'; @@ -16,15 +17,16 @@ const videoCardContentPropsSchema = pipe( object({ actionPerformedClassName: optional(string()), content: directLineMediaCardSchema, - disabled: optional(boolean()) + disabled: optional(boolean()), + replyToId: optional(string()) }), readonly() ); -type VideoCardContentProps = InferInput; +type VideoCardContentProps = InferReactProps; function VideoCardContent(props: VideoCardContentProps) { - const { actionPerformedClassName, content, disabled } = parse(videoCardContentPropsSchema, props); + const { actionPerformedClassName, content, disabled, replyToId } = parse(videoCardContentPropsSchema, props); const { autoloop, autostart, image: { url: imageURL } = { url: undefined }, media } = content; const classNames = useStyles(styles); @@ -38,7 +40,12 @@ function VideoCardContent(props: VideoCardContentProps) { ))} - + ); } diff --git a/packages/bundle/src/adaptiveCards/Attachment/private/directLineCardActionSchema.ts b/packages/bundle/src/adaptiveCards/Attachment/private/directLineCardActionSchema.ts new file mode 100644 index 0000000000..2ff947b638 --- /dev/null +++ b/packages/bundle/src/adaptiveCards/Attachment/private/directLineCardActionSchema.ts @@ -0,0 +1,159 @@ +import { + fallback, + intersect, + literal, + object, + optional, + pipe, + readonly, + string, + undefined_, + union, + unknown, + variant, + type InferOutput +} from 'valibot'; + +// TODO: [P1] Consider a plan for migrating to stricter action validation. +// Maybe we could have a `PostBackCardAction` and a `LoosePostBackCardAction`. +// We warn on `PostBackCardAction` and fail on `LoosePostBackCardAction`. +// Also need to think about postback value which only accept complex type. + +// TODO: [P1] Naive complex, we should make complex equivalent to JSON compatible and drop incompatible properties. +const complexSchema = unknown(); + +// TODO: [P0] We should rebuild this schema into a stricter one that follows Direct Line spec. + +const cardActionWithImageAndTitleEssenceSchema = union([ + pipe(object({ image: string(), imageAltText: optional(string()), title: string() }), readonly()), + pipe(object({ image: string(), imageAltText: optional(string()), title: optional(undefined_()) }), readonly()), + pipe(object({ image: optional(undefined_()), imageAltText: optional(undefined_()), title: string() }), readonly()) +]); + +/** + * A `call` action represents a telephone number that may be called. + * + * https://github.com/Microsoft/botframework-sdk/blob/main/specs/botframework-activity/botframework-activity.md#call + */ +const callCardActionEssenceSchema = pipe(object({ type: literal('call'), value: string() }), readonly()); + +/** + * A `downloadFile` action represents a hyperlink to be downloaded. + * + * https://github.com/Microsoft/botframework-sdk/blob/main/specs/botframework-activity/botframework-activity.md#download-file-actions + */ +const downloadFileCardActionEssenceSchema = pipe( + object({ type: literal('downloadFile'), value: string() }), + readonly() +); + +/** + * An `imBack` action represents a text response that is added to the chat feed. + * + * https://github.com/Microsoft/botframework-sdk/blob/main/specs/botframework-activity/botframework-activity.md#im-back + */ +const imBackCardActionEssenceSchema = pipe(object({ type: literal('imBack'), value: string() }), readonly()); + +/** + * A `messageBack` action represents a text response to be sent via the chat system. + * + * https://github.com/Microsoft/botframework-sdk/blob/main/specs/botframework-activity/botframework-activity.md#message-back + */ +const messageBackCardActionEssenceSchema = pipe( + object({ + displayText: optional(string()), + text: optional(string()), + type: literal('messageBack'), + value: optional(fallback(complexSchema, undefined)) + }), + readonly() +); + +/** + * An `openUrl` action represents a hyperlink to be handled by the client. + * + * https://github.com/Microsoft/botframework-sdk/blob/main/specs/botframework-activity/botframework-activity.md#open-url-actions + */ +const openURLCardActionEssenceSchema = pipe(object({ type: literal('openUrl'), value: string() }), readonly()); + +/** + * A `playAudio` action represents audio media that may be played. + * + * https://github.com/Microsoft/botframework-sdk/blob/main/specs/botframework-activity/botframework-activity.md#play-audio + */ +const playAudioCardActionEssenceSchema = pipe(object({ type: literal('playAudio'), value: string() }), readonly()); + +/** + * A `playVideo` action represents video media that may be played. + * + * https://github.com/Microsoft/botframework-sdk/blob/main/specs/botframework-activity/botframework-activity.md#play-video + */ +const playVideoCardActionEssenceSchema = pipe(object({ type: literal('playVideo'), value: string() }), readonly()); + +/** + * A `postBack` action represents a text response that is not added to the chat feed. + * + * https://github.com/Microsoft/botframework-sdk/blob/main/specs/botframework-activity/botframework-activity.md#post-back + */ +const postBackCardActionEssenceSchema = pipe( + object({ type: literal('postBack'), value: optional(fallback(complexSchema, undefined)) }), + readonly() +); + +/** + * A `showImage` action represents an image that may be displayed. + * + * https://github.com/Microsoft/botframework-sdk/blob/main/specs/botframework-activity/botframework-activity.md#show-image-file-actions + */ +const showImageCardActionEssenceSchema = pipe(object({ type: literal('showImage'), value: string() }), readonly()); + +/** + * A `signin` action represents a hyperlink to be handled by the client's signin system. + * + * https://github.com/Microsoft/botframework-sdk/blob/main/specs/botframework-activity/botframework-activity.md#signin + */ +const signInCardActionEssenceSchema = pipe(object({ type: literal('signin'), value: string() }), readonly()); + +/** + * Web Chat-only `webchat:callURL` action represents a hyperlink opened in a popup window. + */ +const webChatCallURLCardActionEssenceSchema = pipe( + object({ type: literal('webchat:callURL'), value: string() }), + readonly() +); + +/** + * > This headless card action is intended for tap action only, which does not have `image`, `imageAlt`, and `title`. + * + * A card action represents a clickable or interactive button for use within cards or as suggested actions. They are used to solicit input from users. Despite their name, card actions are not limited to use solely on cards. + * + * https://github.com/Microsoft/botframework-sdk/blob/main/specs/botframework-activity/botframework-activity.md#card-action + */ +const directLineHeadlessCardActionSchema = variant('type', [ + callCardActionEssenceSchema, + downloadFileCardActionEssenceSchema, + imBackCardActionEssenceSchema, + messageBackCardActionEssenceSchema, + openURLCardActionEssenceSchema, + playAudioCardActionEssenceSchema, + playVideoCardActionEssenceSchema, + postBackCardActionEssenceSchema, + showImageCardActionEssenceSchema, + signInCardActionEssenceSchema, + webChatCallURLCardActionEssenceSchema +]); + +/** + * A card action represents a clickable or interactive button for use within cards or as suggested actions. They are used to solicit input from users. Despite their name, card actions are not limited to use solely on cards. + * + * https://github.com/Microsoft/botframework-sdk/blob/main/specs/botframework-activity/botframework-activity.md#card-action + */ +const directLineCardActionSchema = intersect([ + cardActionWithImageAndTitleEssenceSchema, + directLineHeadlessCardActionSchema +]); + +type DirectLineCardAction = InferOutput; + +export default directLineCardActionSchema; +export { directLineHeadlessCardActionSchema, type DirectLineCardAction }; diff --git a/packages/bundle/src/adaptiveCards/Attachment/private/directLineSchema.ts b/packages/bundle/src/adaptiveCards/Attachment/private/directLineSchema.ts index df373468cb..e6010bc833 100644 --- a/packages/bundle/src/adaptiveCards/Attachment/private/directLineSchema.ts +++ b/packages/bundle/src/adaptiveCards/Attachment/private/directLineSchema.ts @@ -1,28 +1,6 @@ -import { - any, - array, - boolean, - literal, - looseObject, - number, - object, - optional, - pipe, - readonly, - string, - union -} from 'valibot'; +import { any, array, boolean, literal, number, object, optional, pipe, readonly, string, union } from 'valibot'; -// TODO: Should build a better `directLineCardActionSchema`. -const directLineCardActionSchema = pipe( - looseObject({ - image: optional(string()), - title: optional(string()), - type: string(), - value: optional(any()) - }), - readonly() -); +import directLineCardActionSchema, { directLineHeadlessCardActionSchema } from './directLineCardActionSchema'; // https://github.com/microsoft/botframework-sdk/blob/master/specs/botframework-activity/botframework-cards.md#media-cards const directLineMediaCardSchema = pipe( @@ -61,7 +39,7 @@ const directLineBasicCardSchema = pipe( pipe( object({ alt: optional(string()), - tap: optional(directLineCardActionSchema), + tap: optional(directLineHeadlessCardActionSchema), url: optional(string()) }), readonly() @@ -71,7 +49,7 @@ const directLineBasicCardSchema = pipe( ) ), subtitle: optional(string()), - tap: optional(directLineCardActionSchema), + tap: optional(directLineHeadlessCardActionSchema), text: optional(string()), title: optional(string()) }), @@ -105,7 +83,7 @@ const directLineReceiptCardSchema = pipe( pipe( object({ alt: string(), - tap: optional(directLineCardActionSchema), + tap: optional(directLineHeadlessCardActionSchema), url: string() }), readonly() @@ -114,7 +92,7 @@ const directLineReceiptCardSchema = pipe( price: string(), quantity: optional(union([number(), string()])), // TODO: Should be string only. subtitle: optional(string()), - tap: optional(directLineCardActionSchema), + tap: optional(directLineHeadlessCardActionSchema), text: optional(string()), title: string() }), @@ -124,7 +102,7 @@ const directLineReceiptCardSchema = pipe( readonly() ) ), - tap: optional(directLineCardActionSchema), + tap: optional(directLineHeadlessCardActionSchema), tax: optional(string()), title: optional(string()), total: optional(string()), @@ -144,7 +122,6 @@ const directLineSignInCardSchema = pipe( export { directLineBasicCardSchema, - directLineCardActionSchema, directLineMediaCardSchema, directLineReceiptCardSchema, directLineSignInCardSchema diff --git a/packages/bundle/src/adaptiveCards/createAdaptiveCardsAttachmentMiddleware.tsx b/packages/bundle/src/adaptiveCards/createAdaptiveCardsAttachmentMiddleware.tsx index 1e19093859..9e432bbbe3 100644 --- a/packages/bundle/src/adaptiveCards/createAdaptiveCardsAttachmentMiddleware.tsx +++ b/packages/bundle/src/adaptiveCards/createAdaptiveCardsAttachmentMiddleware.tsx @@ -20,23 +20,49 @@ export default function createAdaptiveCardsAttachmentMiddleware(): AttachmentMid const [{ activity, attachment }] = args; return attachment.contentType === 'application/vnd.microsoft.card.hero' ? ( - + ) : attachment.contentType === 'application/vnd.microsoft.card.adaptive' ? ( ) : attachment.contentType === 'application/vnd.microsoft.card.animation' ? ( - + ) : attachment.contentType === 'application/vnd.microsoft.card.audio' ? ( - + ) : attachment.contentType === 'application/vnd.microsoft.card.oauth' ? ( - + ) : attachment.contentType === 'application/vnd.microsoft.card.receipt' ? ( - + ) : attachment.contentType === 'application/vnd.microsoft.card.signin' ? ( - + ) : attachment.contentType === 'application/vnd.microsoft.card.thumbnail' ? ( - + ) : attachment.contentType === 'application/vnd.microsoft.card.video' ? ( - + ) : ( next(...args) ); diff --git a/packages/core/src/actions/sendMessage.ts b/packages/core/src/actions/sendMessage.ts index ec3b632944..b2679d9b61 100644 --- a/packages/core/src/actions/sendMessage.ts +++ b/packages/core/src/actions/sendMessage.ts @@ -1,16 +1,49 @@ -import { type SendBoxAttachment } from '../types/SendBoxAttachment'; +import { any, array, literal, object, optional, parse, pipe, readonly, string, type InferOutput } from 'valibot'; +import { sendBoxAttachmentSchema, type SendBoxAttachment } from '../types/SendBoxAttachment'; const SEND_MESSAGE = 'WEB_CHAT/SEND_MESSAGE'; -export default function sendMessage( +type SendMessageActionFactoryInit = { + readonly attachments?: readonly SendBoxAttachment[] | undefined; + readonly channelData?: any; + readonly replyToId?: string | undefined; +}; + +const sendMessageActionSchema = pipe( + object({ + type: literal(SEND_MESSAGE), + payload: pipe( + object({ + attachments: optional(pipe(array(sendBoxAttachmentSchema), readonly())), + channelData: optional(any()), + replyToId: optional(string()), + method: optional(string()), + text: optional(string()) + }), + readonly() + ) + }), + readonly() +); + +type SendMessageAction = InferOutput; + +function sendMessage( text: string | undefined, method: string | undefined, - { attachments, channelData }: { attachments?: readonly SendBoxAttachment[]; channelData?: any } = {} -) { - return { - type: SEND_MESSAGE, - payload: { attachments, channelData, method, text } - }; + { attachments, channelData, replyToId }: SendMessageActionFactoryInit = {} +): SendMessageAction { + return parse(sendMessageActionSchema, { + payload: { + attachments, + channelData, + method, + replyToId, + text + }, + type: SEND_MESSAGE + }); } -export { SEND_MESSAGE }; +export default sendMessage; +export { SEND_MESSAGE, sendMessageActionSchema, type SendMessageAction, type SendMessageActionFactoryInit }; diff --git a/packages/core/src/actions/sendMessageBack.ts b/packages/core/src/actions/sendMessageBack.ts index 272a5261a4..66ebd729cd 100644 --- a/packages/core/src/actions/sendMessageBack.ts +++ b/packages/core/src/actions/sendMessageBack.ts @@ -1,10 +1,47 @@ +import { any, literal, object, optional, parse, pipe, readonly, string, type InferOutput } from 'valibot'; + const SEND_MESSAGE_BACK = 'WEB_CHAT/SEND_MESSAGE_BACK'; -export default function sendMessageback(value: any, text: string | undefined, displayText: string | undefined) { - return { - type: SEND_MESSAGE_BACK, - payload: { displayText, text, value } - }; +const sendMessageBackActionSchema = pipe( + object({ + payload: pipe( + object({ + displayText: optional(string()), + replyToId: optional(string()), + text: optional(string()), + value: optional(any()) + }), + readonly() + ), + type: literal(SEND_MESSAGE_BACK) + }), + readonly() +); + +type SendMessageBackAction = InferOutput; +type SendMessageBackActionFactoryInit = { readonly replyToId?: string | undefined }; + +function sendMessageBack( + value: any, + text: string | undefined, + displayText: string | undefined, + init?: SendMessageBackActionFactoryInit | undefined +): SendMessageBackAction { + return parse(sendMessageBackActionSchema, { + payload: { + displayText, + replyToId: init?.replyToId, + text, + value + }, + type: SEND_MESSAGE_BACK + }); } -export { SEND_MESSAGE_BACK }; +export default sendMessageBack; +export { + SEND_MESSAGE_BACK, + sendMessageBackActionSchema, + type SendMessageBackAction, + type SendMessageBackActionFactoryInit +}; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 3fb23c8f15..05cc4831d4 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -10,7 +10,6 @@ import type { VoiceHandler } from './actions/registerVoiceHandler'; import registerVoiceHandler from './actions/registerVoiceHandler'; import sendEvent from './actions/sendEvent'; import sendFiles from './actions/sendFiles'; -import sendMessage from './actions/sendMessage'; import sendMessageBack from './actions/sendMessageBack'; import sendPostBack, { type SendPostBackInit } from './actions/sendPostBack'; import setDictateInterims from './actions/setDictateInterims'; @@ -66,8 +65,17 @@ export { type SetSuggestedActionsAction } from './actions/setSuggestedActions'; +export { + SEND_MESSAGE, + default as sendMessage, + sendMessageActionSchema, + type SendMessageAction, + type SendMessageActionFactoryInit +} from './actions/sendMessage'; + +export { sendBoxAttachmentSchema, type SendBoxAttachment } from './types/SendBoxAttachment'; + import type { GlobalScopePonyfill } from './types/GlobalScopePonyfill'; -import type { SendBoxAttachment } from './types/SendBoxAttachment'; import type { WebChatActivity } from './types/WebChatActivity'; import type { DirectLineActivity } from './types/external/DirectLineActivity'; import type { DirectLineAnimationCard } from './types/external/DirectLineAnimationCard'; @@ -112,7 +120,6 @@ export { registerVoiceHandler, sendEvent, sendFiles, - sendMessage, sendMessageBack, sendPostBack, setDictateInterims, @@ -151,7 +158,6 @@ export type { DirectLineVideoCard, GlobalScopePonyfill, Observable, - SendBoxAttachment, SendPostBackInit, VoiceHandler, VoiceState, diff --git a/packages/core/src/sagas/sendMessageBackToPostActivitySaga.js b/packages/core/src/sagas/sendMessageBackToPostActivitySaga.ts similarity index 55% rename from packages/core/src/sagas/sendMessageBackToPostActivitySaga.js rename to packages/core/src/sagas/sendMessageBackToPostActivitySaga.ts index 9fb91be7b5..ccee064dd0 100644 --- a/packages/core/src/sagas/sendMessageBackToPostActivitySaga.js +++ b/packages/core/src/sagas/sendMessageBackToPostActivitySaga.ts @@ -1,21 +1,28 @@ import { put, takeEvery } from 'redux-saga/effects'; import postActivity from '../actions/postActivity'; -import { SEND_MESSAGE_BACK } from '../actions/sendMessageBack'; +import { SEND_MESSAGE_BACK, type SendMessageBackAction } from '../actions/sendMessageBack'; +import type { WebChatActivity } from '../types/WebChatActivity'; import whileConnected from './effects/whileConnected'; // https://github.com/microsoft/botframework-sdk/blob/main/specs/botframework-activity/botframework-activity.md#message-back -function* postActivityWithMessageBack({ payload: { displayText, text, value } }) { +function* postActivityWithMessageBack({ payload: { displayText, replyToId, text, value } }: SendMessageBackAction) { yield put( postActivity({ channelData: { messageBack: { displayText } - }, + // TODO: [P2] We need to have a new type `OutgoingWebChatActivity`. + } satisfies Record as WebChatActivity['channelData'], + replyToId, text, type: 'message', value - }) + } satisfies Omit< + // TODO: [P2] We need to have a new type `OutgoingWebChatActivity`. + WebChatActivity & { text: string | undefined; value: string | undefined }, + 'from' + > as WebChatActivity) ); } diff --git a/packages/core/src/sagas/sendMessageToPostActivitySaga.ts b/packages/core/src/sagas/sendMessageToPostActivitySaga.ts index 9b36e09993..af80814f05 100644 --- a/packages/core/src/sagas/sendMessageToPostActivitySaga.ts +++ b/packages/core/src/sagas/sendMessageToPostActivitySaga.ts @@ -2,10 +2,13 @@ import { put, takeEvery } from 'redux-saga/effects'; import postActivity from '../actions/postActivity'; import sendMessage, { SEND_MESSAGE } from '../actions/sendMessage'; +import type { SendBoxAttachment } from '../types/SendBoxAttachment'; import whileConnected from './effects/whileConnected'; +const EMPTY_ATTACHMENTS: readonly SendBoxAttachment[] = Object.freeze([]); + function* postActivityWithMessage({ - payload: { attachments = [], channelData, method, text } + payload: { attachments = EMPTY_ATTACHMENTS, channelData, method, replyToId, text } }: ReturnType) { yield put( postActivity( @@ -23,6 +26,7 @@ function* postActivityWithMessage({ ...channelData, attachmentSizes: attachments.map(({ blob: { size } }) => size) }, + replyToId: replyToId || undefined, text: text || undefined, textFormat: 'plain', type: 'message' diff --git a/packages/core/src/types/SendBoxAttachment.ts b/packages/core/src/types/SendBoxAttachment.ts index 2016177bb1..9f4d1d5b05 100644 --- a/packages/core/src/types/SendBoxAttachment.ts +++ b/packages/core/src/types/SendBoxAttachment.ts @@ -1,4 +1,37 @@ -export type SendBoxAttachment = Readonly<{ - blob: Blob | File; - thumbnailURL?: URL; -}>; +import { + blob, + file, + instance, + object, + optional, + pipe, + readonly, + string, + transform, + union, + url, + type InferOutput +} from 'valibot'; + +const sendBoxAttachmentSchema = pipe( + object({ + blob: union([blob(), file()]), + thumbnailURL: optional( + union([ + instance(URL), + // `thumbnailURL` should be solely of type URL but never a string. + // But for historic reason, we need to allow string() + url(). + pipe( + string(), + url(), + transform(value => new URL(value)) + ) + ]) + ) + }), + readonly() +); + +type SendBoxAttachment = InferOutput; + +export { sendBoxAttachmentSchema, type SendBoxAttachment }; diff --git a/packages/core/src/types/external/DirectLineCardAction.ts b/packages/core/src/types/external/DirectLineCardAction.ts index 39a6edbdc2..dc6a93255c 100644 --- a/packages/core/src/types/external/DirectLineCardAction.ts +++ b/packages/core/src/types/external/DirectLineCardAction.ts @@ -87,7 +87,7 @@ type PlayVideoCardAction = CardActionWithImageAndTitle & { */ type PostBackCardAction = CardActionWithImageAndTitle & { type: 'postBack'; - value: any; // For legacy reason, postBack support any. + value?: any; // For legacy reason, postBack support any. }; /** diff --git a/packages/react-valibot/src/InferReactProps.ts b/packages/react-valibot/src/InferReactProps.ts new file mode 100644 index 0000000000..6c29cb9724 --- /dev/null +++ b/packages/react-valibot/src/InferReactProps.ts @@ -0,0 +1,35 @@ +import type { ReadonlyDeep } from 'type-fest'; +import { + type BaseIssue, + type BaseMetadata, + type BaseSchema, + type BaseSchemaAsync, + type BaseTransformation, + type BaseTransformationAsync, + type BaseValidation, + type BaseValidationAsync, + type InferInput +} from 'valibot'; + +// Q: Why `InferInput` but not `InferOutput`? +// A: The component is going to take whatever the consumer send to us. +// We might rectify (fill in the blank), but they aren't very strict until things get into our system. + +// Q: Why not just `InferInput`? +// A: By default, `InferInput` don't mark things as read-only. +// When parent component send props, those props could be read-only. +// But the child component (wrongly) prefer read-write because `InferInput` ignore read-only. + +// Copied from `InferInput` and added `ReadonlyDeep`. +type InferReactProps< + TItem$1 extends + | BaseSchema> + | BaseSchemaAsync> + | BaseValidation> + | BaseValidationAsync> + | BaseTransformation> + | BaseTransformationAsync> + | BaseMetadata +> = ReadonlyDeep>; + +export { type InferReactProps }; diff --git a/packages/react-valibot/src/index.ts b/packages/react-valibot/src/index.ts index d1e6fe3bb5..4ead6498d1 100644 --- a/packages/react-valibot/src/index.ts +++ b/packages/react-valibot/src/index.ts @@ -1,2 +1,3 @@ +export { type InferReactProps } from './InferReactProps'; export { default as reactNode } from './reactNode'; export { default as validateProps } from './validateProps'; diff --git a/packages/test/page-object/src/globals/pageConditions/allOutgoingActivitiesSent.js b/packages/test/page-object/src/globals/pageConditions/allOutgoingActivitiesSent.js index c2ede6a38f..4b7571f626 100644 --- a/packages/test/page-object/src/globals/pageConditions/allOutgoingActivitiesSent.js +++ b/packages/test/page-object/src/globals/pageConditions/allOutgoingActivitiesSent.js @@ -12,7 +12,14 @@ export default function allOutgoingActivitiesSent() { const { innerText } = activityStatusElement; - return !innerText.includes('Send failed') && !innerText.includes('Sending'); + return !( + innerText.includes('Send failed') || + innerText.includes('Sending') || + // Send failed. + innerText.includes('فشل في الإرسال') || + // Sending. + innerText.includes('أرسال') + ); }), 15000 );