diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a78ec2bd5..4b9cb64f94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,18 @@ Notes: web developers are advised to use [`~` (tilde range)](https://github.com/ - `styleOptions.bubbleImageHeight` is being deprecated in favor of `styleOptions.bubbleImageMaxHeight` and `styleOptions.bubbleImageMinHeight`. The option will be removed on or after 2026-07-05 +### Added + +- (Experimental) Added pre-chat message with starter prompts, in PR [#5255](https://github.com/microsoft/BotFramework-WebChat/issues/5255), by [@compulim](https://github.com/compulim) + +### Changed + +- Updated `useSuggestedActions` to return the activity the suggested actions originated from, in PR [#5255](https://github.com/microsoft/BotFramework-WebChat/issues/5255), by [@compulim](https://github.com/compulim) + +### Fixed + +- Fixed [#5256](https://github.com/microsoft/BotFramework-WebChat/issues/5256). `styleOptions.maxMessageLength` should support any JavaScript number value including `Infinity`, by [@compulim](https://github.com/compulim), in PR [#5255](https://github.com/microsoft/BotFramework-WebChat/issues/pull/5255) + ## [4.18.0] - 2024-07-10 ### Added diff --git a/__tests__/__image_snapshots__/html/max-message-length-infinity-js-fluent-theme-applied-handles-max-message-length-of-infinity-1-snap.png b/__tests__/__image_snapshots__/html/max-message-length-infinity-js-fluent-theme-applied-handles-max-message-length-of-infinity-1-snap.png new file mode 100644 index 0000000000..a1d58ee284 Binary files /dev/null and b/__tests__/__image_snapshots__/html/max-message-length-infinity-js-fluent-theme-applied-handles-max-message-length-of-infinity-1-snap.png differ diff --git a/__tests__/__image_snapshots__/html/pre-chat-message-activity-js-fluent-theme-applied-should-display-pre-chat-message-1-snap.png b/__tests__/__image_snapshots__/html/pre-chat-message-activity-js-fluent-theme-applied-should-display-pre-chat-message-1-snap.png new file mode 100644 index 0000000000..17d4729d57 Binary files /dev/null and b/__tests__/__image_snapshots__/html/pre-chat-message-activity-js-fluent-theme-applied-should-display-pre-chat-message-1-snap.png differ diff --git a/__tests__/__image_snapshots__/html/pre-chat-message-activity-wide-js-fluent-theme-applied-should-display-pre-chat-message-in-wide-format-1-snap.png b/__tests__/__image_snapshots__/html/pre-chat-message-activity-wide-js-fluent-theme-applied-should-display-pre-chat-message-in-wide-format-1-snap.png new file mode 100644 index 0000000000..98507c0480 Binary files /dev/null and b/__tests__/__image_snapshots__/html/pre-chat-message-activity-wide-js-fluent-theme-applied-should-display-pre-chat-message-in-wide-format-1-snap.png differ diff --git a/__tests__/html/fluentTheme/maxMessageLength.infinity.html b/__tests__/html/fluentTheme/maxMessageLength.infinity.html new file mode 100644 index 0000000000..7c5679dc6a --- /dev/null +++ b/__tests__/html/fluentTheme/maxMessageLength.infinity.html @@ -0,0 +1,46 @@ + + + + + + + + + + + + + +
+ + + diff --git a/__tests__/html/fluentTheme/maxMessageLength.infinity.js b/__tests__/html/fluentTheme/maxMessageLength.infinity.js new file mode 100644 index 0000000000..fb15dfd18a --- /dev/null +++ b/__tests__/html/fluentTheme/maxMessageLength.infinity.js @@ -0,0 +1,5 @@ +/** @jest-environment ./packages/test/harness/src/host/jest/WebDriverEnvironment.js */ + +describe('Fluent theme applied', () => { + test('handles max message length of Infinity', () => runHTML('fluentTheme/maxMessageLength.infinity')); +}); diff --git a/__tests__/html/fluentTheme/preChatMessageActivity.html b/__tests__/html/fluentTheme/preChatMessageActivity.html new file mode 100644 index 0000000000..62a285c59d --- /dev/null +++ b/__tests__/html/fluentTheme/preChatMessageActivity.html @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + +
+ + + diff --git a/__tests__/html/fluentTheme/preChatMessageActivity.js b/__tests__/html/fluentTheme/preChatMessageActivity.js new file mode 100644 index 0000000000..df16d5cd33 --- /dev/null +++ b/__tests__/html/fluentTheme/preChatMessageActivity.js @@ -0,0 +1,5 @@ +/** @jest-environment ./packages/test/harness/src/host/jest/WebDriverEnvironment.js */ + +describe('Fluent theme applied', () => { + test('should display pre-chat message', () => runHTML('fluentTheme/preChatMessageActivity')); +}); diff --git a/__tests__/html/fluentTheme/preChatMessageActivity.wide.html b/__tests__/html/fluentTheme/preChatMessageActivity.wide.html new file mode 100644 index 0000000000..98bc868e19 --- /dev/null +++ b/__tests__/html/fluentTheme/preChatMessageActivity.wide.html @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + +
+ + + diff --git a/__tests__/html/fluentTheme/preChatMessageActivity.wide.js b/__tests__/html/fluentTheme/preChatMessageActivity.wide.js new file mode 100644 index 0000000000..dd0f21e7d9 --- /dev/null +++ b/__tests__/html/fluentTheme/preChatMessageActivity.wide.js @@ -0,0 +1,5 @@ +/** @jest-environment ./packages/test/harness/src/host/jest/WebDriverEnvironment.js */ + +describe('Fluent theme applied', () => { + test('should display pre-chat message in wide format', () => runHTML('fluentTheme/preChatMessageActivity.wide')); +}); diff --git a/docs/HOOKS.md b/docs/HOOKS.md index 884bc104de..2740de0f9c 100644 --- a/docs/HOOKS.md +++ b/docs/HOOKS.md @@ -1355,16 +1355,19 @@ This function will send the text in the send box to the bot and clear the send b ## `useSuggestedActions` +> New in 4.18.1: Will return the activity which the suggested actions are originated from. + ```js -useSuggestedActions(): [CardAction[], (CardAction[]) => void] +useSuggestedActions(): [CardAction[], (CardAction[]) => void, { activity: WebChatActivity }] ``` -This hook will return an array and a setter function. +This hook will return an array, a setter function, and a property bag. 1. array: a list of suggested actions that should be shown to the user 1. function: a setter function to clear suggested actions. The setter function can only be used to clear suggested actions, and it will accept empty array or falsy value only. +1. `activity`: the activity which the suggested actions are originated from The suggested actions are computed from the last message activity sent from the bot. If the user posts an activity, the suggested actions will be cleared. diff --git a/packages/api/src/hooks/useSuggestedActions.ts b/packages/api/src/hooks/useSuggestedActions.ts index 880adeb185..9402baca17 100644 --- a/packages/api/src/hooks/useSuggestedActions.ts +++ b/packages/api/src/hooks/useSuggestedActions.ts @@ -1,11 +1,23 @@ +import type { DirectLineCardAction, WebChatActivity } from 'botframework-webchat-core'; import { useCallback } from 'react'; -import type { DirectLineCardAction } from 'botframework-webchat-core'; import { useSelector } from './internal/WebChatReduxContext'; import useWebChatAPIContext from './internal/useWebChatAPIContext'; -export default function useSuggestedActions(): [DirectLineCardAction[], (suggestedActions: never[]) => void] { - const value = useSelector(({ suggestedActions }) => suggestedActions); +export default function useSuggestedActions(): [ + DirectLineCardAction[], + (suggestedActions: never[]) => void, + extras: { activity: WebChatActivity } +] { + const [value, activity] = useSelector( + ({ + suggestedActions, + suggestedActionsOriginActivity: { activity } + }: { + suggestedActions: readonly DirectLineCardAction[]; + suggestedActionsOriginActivity: { activity: undefined | WebChatActivity }; + }) => [suggestedActions, activity] + ); const { clearSuggestedActions } = useWebChatAPIContext(); return [ @@ -19,6 +31,7 @@ export default function useSuggestedActions(): [DirectLineCardAction[], (suggest clearSuggestedActions(); }, [clearSuggestedActions] - ) + ), + { activity } ]; } diff --git a/packages/core/src/actions/clearSuggestedActions.js b/packages/core/src/actions/clearSuggestedActions.ts similarity index 94% rename from packages/core/src/actions/clearSuggestedActions.js rename to packages/core/src/actions/clearSuggestedActions.ts index c37dd7a291..47782d11e7 100644 --- a/packages/core/src/actions/clearSuggestedActions.js +++ b/packages/core/src/actions/clearSuggestedActions.ts @@ -1,4 +1,4 @@ -const CLEAR_SUGGESTED_ACTIONS = 'WEB_CHAT/CLEAR_SUGGESTED_ACTIONS'; +const CLEAR_SUGGESTED_ACTIONS = 'WEB_CHAT/CLEAR_SUGGESTED_ACTIONS' as const; export default function clearSuggestedActions() { return { diff --git a/packages/core/src/actions/setSuggestedActions.js b/packages/core/src/actions/setSuggestedActions.js deleted file mode 100644 index a1b854d63c..0000000000 --- a/packages/core/src/actions/setSuggestedActions.js +++ /dev/null @@ -1,12 +0,0 @@ -const EMPTY_ARRAY = []; - -const SET_SUGGESTED_ACTIONS = 'WEB_CHAT/SET_SUGGESTED_ACTIONS'; - -export default function setSuggestedActions(suggestedActions = EMPTY_ARRAY) { - return { - type: SET_SUGGESTED_ACTIONS, - payload: { suggestedActions } - }; -} - -export { SET_SUGGESTED_ACTIONS }; diff --git a/packages/core/src/actions/setSuggestedActions.ts b/packages/core/src/actions/setSuggestedActions.ts new file mode 100644 index 0000000000..e9e55ff194 --- /dev/null +++ b/packages/core/src/actions/setSuggestedActions.ts @@ -0,0 +1,18 @@ +import type { DirectLineCardAction } from '../types/external/DirectLineCardAction'; +import type { WebChatActivity } from '../types/WebChatActivity'; + +const EMPTY_ARRAY: readonly DirectLineCardAction[] = Object.freeze([]); + +const SET_SUGGESTED_ACTIONS = 'WEB_CHAT/SET_SUGGESTED_ACTIONS' as const; + +export default function setSuggestedActions( + suggestedActions: readonly DirectLineCardAction[] = EMPTY_ARRAY, + originActivity: undefined | WebChatActivity = undefined +) { + return { + type: SET_SUGGESTED_ACTIONS, + payload: { originActivity, suggestedActions } + }; +} + +export { SET_SUGGESTED_ACTIONS }; diff --git a/packages/core/src/createReducer.ts b/packages/core/src/createReducer.ts index bca68d263d..3e9c9894b1 100644 --- a/packages/core/src/createReducer.ts +++ b/packages/core/src/createReducer.ts @@ -16,6 +16,7 @@ import sendTimeout from './reducers/sendTimeout'; import sendTypingIndicator from './reducers/sendTypingIndicator'; import shouldSpeakIncomingActivity from './reducers/shouldSpeakIncomingActivity'; import suggestedActions from './reducers/suggestedActions'; +import suggestedActionsOriginActivity from './reducers/suggestedActionsOriginActivity'; import type { GlobalScopePonyfill } from './types/GlobalScopePonyfill'; @@ -36,6 +37,7 @@ export default function createReducer(ponyfill: GlobalScopePonyfill) { sendTypingIndicator, shouldSpeakIncomingActivity, suggestedActions, + suggestedActionsOriginActivity, typing: createTypingReducer(ponyfill) }); } diff --git a/packages/core/src/reducers/suggestedActions.js b/packages/core/src/reducers/suggestedActions.js index 706b308684..93bae8d9c4 100644 --- a/packages/core/src/reducers/suggestedActions.js +++ b/packages/core/src/reducers/suggestedActions.js @@ -1,7 +1,7 @@ import { CLEAR_SUGGESTED_ACTIONS } from '../actions/clearSuggestedActions'; import { SET_SUGGESTED_ACTIONS } from '../actions/setSuggestedActions'; -const DEFAULT_STATE = []; +const DEFAULT_STATE = Object.freeze([]); export default function suggestedActions(state = DEFAULT_STATE, { payload = {}, type }) { switch (type) { diff --git a/packages/core/src/reducers/suggestedActionsOriginActivity.ts b/packages/core/src/reducers/suggestedActionsOriginActivity.ts new file mode 100644 index 0000000000..8b2d06cd10 --- /dev/null +++ b/packages/core/src/reducers/suggestedActionsOriginActivity.ts @@ -0,0 +1,33 @@ +import type clearSuggestedActions from '../actions/clearSuggestedActions'; +import { CLEAR_SUGGESTED_ACTIONS } from '../actions/clearSuggestedActions'; +import type setSuggestedActions from '../actions/setSuggestedActions'; +import { SET_SUGGESTED_ACTIONS } from '../actions/setSuggestedActions'; +import type { WebChatActivity } from '../types/WebChatActivity'; + +type ClearSuggestedActions = ReturnType; +type SetSuggestedActions = ReturnType; +type State = Readonly<{ activity: undefined | WebChatActivity }>; + +const DEFAULT_STATE: State = Object.freeze({ activity: undefined }); + +export default function suggestedActionsOriginActivity( + state = DEFAULT_STATE, + action: ClearSuggestedActions | SetSuggestedActions +): State { + switch (action.type) { + case SET_SUGGESTED_ACTIONS: + state = { activity: action.payload.originActivity }; + + break; + + case CLEAR_SUGGESTED_ACTIONS: + state = DEFAULT_STATE; + + break; + + default: + break; + } + + return state; +} diff --git a/packages/core/src/sagas/queueIncomingActivitySaga.ts b/packages/core/src/sagas/queueIncomingActivitySaga.ts index d4038fc435..88717edc3e 100644 --- a/packages/core/src/sagas/queueIncomingActivitySaga.ts +++ b/packages/core/src/sagas/queueIncomingActivitySaga.ts @@ -1,10 +1,10 @@ import { call, cancelled, fork, put, race, select, take } from 'redux-saga/effects'; -import { QUEUE_INCOMING_ACTIVITY } from '../actions/queueIncomingActivity'; -import activitiesSelector, { ofType as activitiesOfType } from '../selectors/activities'; -import activityFromBot from '../definitions/activityFromBot'; import incomingActivity, { INCOMING_ACTIVITY } from '../actions/incomingActivity'; +import { QUEUE_INCOMING_ACTIVITY } from '../actions/queueIncomingActivity'; import setSuggestedActions from '../actions/setSuggestedActions'; +import activityFromBot from '../definitions/activityFromBot'; +import activitiesSelector, { ofType as activitiesOfType } from '../selectors/activities'; import sleep from '../utils/sleep'; import whileConnected from './effects/whileConnected'; @@ -94,7 +94,13 @@ function* queueIncomingActivity({ userID }: { userID: string }, ponyfill: Global // If suggested actions is not destined to anyone, or is destined to the user, show it. // In other words, if suggested actions is destined to someone else, don't show it. - yield put(setSuggestedActions(to?.length && !to.includes(userID) ? null : actions)); + const suggestedActions = to?.length && !to.includes(userID) ? null : actions; + + if (suggestedActions) { + yield put(setSuggestedActions(suggestedActions, lastMessageActivity)); + } else { + yield put(setSuggestedActions()); + } } } ); diff --git a/packages/fluent-theme/package-lock.json b/packages/fluent-theme/package-lock.json index 230212504a..c7c8ceef3a 100644 --- a/packages/fluent-theme/package-lock.json +++ b/packages/fluent-theme/package-lock.json @@ -12,7 +12,8 @@ "classnames": "2.5.1", "inject-meta-tag": "0.0.1", "math-random": "2.0.1", - "use-ref-from": "0.1.0" + "use-ref-from": "0.1.0", + "valibot": "^0.37.0" }, "devDependencies": { "@tsconfig/strictest": "^2.0.5", @@ -2026,6 +2027,19 @@ "react": ">=16.8.0" } }, + "node_modules/valibot": { + "version": "0.37.0", + "resolved": "https://registry.npmjs.org/valibot/-/valibot-0.37.0.tgz", + "integrity": "sha512-FQz52I8RXgFgOHym3XHYSREbNtkgSjF9prvMFH1nBsRyfL6SfCzoT1GuSDTlbsuPubM7/6Kbw0ZMQb8A+V+VsQ==", + "peerDependencies": { + "typescript": ">=5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/webidl-conversions": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", diff --git a/packages/fluent-theme/package.json b/packages/fluent-theme/package.json index a8e24727a6..d0c58b8272 100644 --- a/packages/fluent-theme/package.json +++ b/packages/fluent-theme/package.json @@ -75,7 +75,8 @@ "classnames": "2.5.1", "inject-meta-tag": "0.0.1", "math-random": "2.0.1", - "use-ref-from": "0.1.0" + "use-ref-from": "0.1.0", + "valibot": "^0.37.0" }, "peerDependencies": { "react": ">= 16.8.6" diff --git a/packages/fluent-theme/src/components/preChatActivity/PreChatMessageActivity.module.css b/packages/fluent-theme/src/components/preChatActivity/PreChatMessageActivity.module.css new file mode 100644 index 0000000000..528617e6a5 --- /dev/null +++ b/packages/fluent-theme/src/components/preChatActivity/PreChatMessageActivity.module.css @@ -0,0 +1,34 @@ +:global(.webchat-fluent) .pre-chat-message-activity { + display: grid; + grid-template-areas: 'body' 'toolbar'; + grid-template-rows: auto auto; + gap: var(--webchat-spacingHorizontalXXXL); + padding: var(--webchat-spacingHorizontalXXXL); +} + +:global(.webchat-fluent) .pre-chat-message-activity__body { + font-family: var(--webchat-fontFamilyBase); + font-size: var(--webchat-fontSizeBase300); + font-weight: var(--webchat-fontWeightRegular); + grid-area: body; + line-height: var(--webchat-lineHeightBase300); + text-align: center; +} + +:global(.webchat-fluent) .pre-chat-message-activity__body h2 { + color: var(--webchat-colorNeutralForeground1); + font-family: inherit; + font-weight: var(--webchat-fontWeightSemibold); + font-size: var(--webchat-fontSizeHero700); + line-height: var(--webchat-lineHeightHero700); + margin: var(--webchat-spacingVerticalL) 0 0; +} + +:global(.webchat-fluent) .pre-chat-message-activity__body img { + border-radius: 4px; + height: 64px; +} + +:global(.webchat-fluent) .pre-chat-message-activity__toolbar { + grid-area: toolbar; +} diff --git a/packages/fluent-theme/src/components/preChatActivity/PreChatMessageActivity.tsx b/packages/fluent-theme/src/components/preChatActivity/PreChatMessageActivity.tsx new file mode 100644 index 0000000000..de0f0a9456 --- /dev/null +++ b/packages/fluent-theme/src/components/preChatActivity/PreChatMessageActivity.tsx @@ -0,0 +1,35 @@ +import { hooks } from 'botframework-webchat-component'; +import { type WebChatActivity } from 'botframework-webchat-core'; +import React, { memo, useMemo } from 'react'; +import { useStyles } from '../../styles/index.js'; +import styles from './PreChatMessageActivity.module.css'; +import StarterPromptsToolbar from './StarterPromptsToolbar.js'; + +type Props = Readonly<{ activity: WebChatActivity & { type: 'message' } }>; + +const { useRenderMarkdownAsHTML } = hooks; + +const PreChatMessageActivity = ({ activity }: Props) => { + const classNames = useStyles(styles); + const renderMarkdownAsHTML = useRenderMarkdownAsHTML(); + + const html = useMemo( + () => (renderMarkdownAsHTML ? { __html: renderMarkdownAsHTML(activity.text || '') } : { __html: '' }), + [activity.text, renderMarkdownAsHTML] + ); + + return ( +
+ {/* eslint-disable-next-line react/no-danger */} +
+ +
+ ); +}; + +PreChatMessageActivity.displayName = 'PreChatMessageActivity'; + +export default memo(PreChatMessageActivity); diff --git a/packages/fluent-theme/src/components/preChatActivity/StarterPromptsCardAction.module.css b/packages/fluent-theme/src/components/preChatActivity/StarterPromptsCardAction.module.css new file mode 100644 index 0000000000..867d038646 --- /dev/null +++ b/packages/fluent-theme/src/components/preChatActivity/StarterPromptsCardAction.module.css @@ -0,0 +1,58 @@ +:global(.webchat-fluent) .pre-chat-message-activity__card-action-box { + appearance: none; + background-color: var(--webchat-colorNeutralBackground1); + border: 0; + border-radius: 16px; + box-shadow: var(--webchat-shadow2); + color: var(--webchat-colorNeutralForeground1); + cursor: pointer; + display: grid; + gap: 8px; + grid-template-areas: 'image title' 'image subtitle'; + grid-template-columns: 20px 1fr; + grid-template-rows: auto 1fr; + overflow: hidden; + padding: 16px 20px; + text-align: left; + user-select: none; +} + +:global(.webchat-fluent) .pre-chat-message-activity__card-action-box:disabled { + background-color: var(--webchat-colorNeutralBackground1Disabled); +} + +:global(.webchat-fluent) .pre-chat-message-activity__card-action-box:hover { + background-color: var(--webchat-colorNeutralGrey94); +} + +:global(.webchat-fluent) .pre-chat-message-activity__card-action-box:active { + background-color: var(--webchat-colorNeutralBackground1Pressed); +} + +:global(.webchat-fluent) .pre-chat-message-activity__card-action-box:focus-visible { + outline: solid 2px var(--webchat-colorStrokeFocus2); + outline-offset: -2px; +} + +:global(.webchat-fluent) .pre-chat-message-activity__card-action-image { + grid-area: image; + height: 20px; + width: 20px; +} + +:global(.webchat-fluent) .pre-chat-message-activity__card-action-subtitle { + font-family: var(--webchat-fontFamilyBase); + font-size: 14px; + font-weight: var(--webchat-fontWeightRegular); + grid-area: subtitle; + line-height: 20px; + pointer-events: none; /* Links in subtitle are not clickable. */ +} + +:global(.webchat-fluent) .pre-chat-message-activity__card-action-title { + font-family: var(--webchat-fontFamilyBase); + font-size: 14px; + font-weight: var(--webchat-fontWeightSemibold); + grid-area: title; + line-height: 20px; +} diff --git a/packages/fluent-theme/src/components/preChatActivity/StarterPromptsCardAction.tsx b/packages/fluent-theme/src/components/preChatActivity/StarterPromptsCardAction.tsx new file mode 100644 index 0000000000..11cc78d840 --- /dev/null +++ b/packages/fluent-theme/src/components/preChatActivity/StarterPromptsCardAction.tsx @@ -0,0 +1,62 @@ +import { hooks } from 'botframework-webchat-component'; +import { type DirectLineCardAction } from 'botframework-webchat-core'; +import cx from 'classnames'; +import React, { memo, useCallback, useMemo } from 'react'; +import { useRefFrom } from 'use-ref-from'; +import { useStyles } from '../../styles/index.js'; +import MonochromeImageMasker from './private/MonochromeImageMasker.js'; +import styles from './StarterPromptsCardAction.module.css'; + +const { useFocus, useRenderMarkdownAsHTML, useSendBoxValue } = hooks; + +type Props = Readonly<{ + className?: string | undefined; + messageBackAction: DirectLineCardAction & { type: 'messageBack' }; +}>; + +const StarterPromptAction = ({ className, messageBackAction }: Props) => { + const [_, setSendBoxValue] = useSendBoxValue(); + const classNames = useStyles(styles); + const focus = useFocus(); + const inputTextRef = useRefFrom(messageBackAction.displayText || messageBackAction.text || ''); + const renderMarkdownAsHTML = useRenderMarkdownAsHTML('message activity'); + const subtitleHTML = useMemo( + () => (renderMarkdownAsHTML ? { __html: renderMarkdownAsHTML(messageBackAction.text || '') } : { __html: '' }), + [messageBackAction.text, renderMarkdownAsHTML] + ); + + const handleClick = useCallback(() => { + setSendBoxValue(inputTextRef.current); + + // Focus on the send box after the value is "pasted." + focus('sendBox'); + }, [focus, inputTextRef, setSendBoxValue]); + + return ( + + ); +}; + +StarterPromptAction.displayName = 'StarterPromptAction'; + +export default memo(StarterPromptAction); diff --git a/packages/fluent-theme/src/components/preChatActivity/StarterPromptsToolbar.module.css b/packages/fluent-theme/src/components/preChatActivity/StarterPromptsToolbar.module.css new file mode 100644 index 0000000000..95b0d762b2 --- /dev/null +++ b/packages/fluent-theme/src/components/preChatActivity/StarterPromptsToolbar.module.css @@ -0,0 +1,18 @@ +:global(.webchat-fluent) .pre-chat-message-activity__card-action-toolbar { + container-name: webchat-container; + container-type: inline-size; +} + +:global(.webchat-fluent) .pre-chat-message-activity__card-action-toolbar-grid { + display: grid; + gap: var(--webchat-spacingHorizontalM); + grid-template-columns: 1fr 1fr 1fr; + padding: 0; +} + +/* TODO: What is the good width to show as 3 columns? Web Chat, by default, has a bubble max width of 480px. */ +@container webchat-container (width <= 480px) { + :global(.webchat-fluent) .pre-chat-message-activity__card-action-toolbar-grid { + grid-template-columns: 1fr; + } +} diff --git a/packages/fluent-theme/src/components/preChatActivity/StarterPromptsToolbar.tsx b/packages/fluent-theme/src/components/preChatActivity/StarterPromptsToolbar.tsx new file mode 100644 index 0000000000..9f04d0c1d8 --- /dev/null +++ b/packages/fluent-theme/src/components/preChatActivity/StarterPromptsToolbar.tsx @@ -0,0 +1,35 @@ +import { type DirectLineCardAction } from 'botframework-webchat-core'; +import cx from 'classnames'; +import React, { memo } from 'react'; +import { useStyles } from '../../styles/index.js'; +import StarterPromptsCardAction from './StarterPromptsCardAction.js'; +import styles from './StarterPromptsToolbar.module.css'; + +type Props = Readonly<{ + cardActions: readonly DirectLineCardAction[]; + className?: string | undefined; +}>; + +const StarterPrompts = ({ cardActions, className }: Props) => { + const classNames = useStyles(styles); + + return ( + // TODO: Accessibility-wise, this should be role="toolbar" with keyboard navigation. +
+
+ {cardActions + .filter( + (card: DirectLineCardAction): card is DirectLineCardAction & { type: 'messageBack' } => + card.type === 'messageBack' + ) + .map(cardAction => ( + + ))} +
+
+ ); +}; + +StarterPrompts.displayName = 'StarterPrompts'; + +export default memo(StarterPrompts); diff --git a/packages/fluent-theme/src/components/preChatActivity/index.tsx b/packages/fluent-theme/src/components/preChatActivity/index.tsx new file mode 100644 index 0000000000..445fee35b6 --- /dev/null +++ b/packages/fluent-theme/src/components/preChatActivity/index.tsx @@ -0,0 +1,2 @@ +export { default as isPreChatMessageActivity } from './isPreChatMessageActivity.js'; +export { default as PreChatMessageActivity } from './PreChatMessageActivity.js'; diff --git a/packages/fluent-theme/src/components/preChatActivity/isPreChatMessageActivity.ts b/packages/fluent-theme/src/components/preChatActivity/isPreChatMessageActivity.ts new file mode 100644 index 0000000000..344e85a60c --- /dev/null +++ b/packages/fluent-theme/src/components/preChatActivity/isPreChatMessageActivity.ts @@ -0,0 +1,26 @@ +import type { WebChatActivity } from 'botframework-webchat-core'; +import { array, literal, object, safeParse, string, type InferOutput } from 'valibot'; + +const messageEntity = object({ + '@context': literal('https://schema.org'), + '@id': literal(''), // Must be empty string. + '@type': literal('Message'), + keywords: array(string()), + type: literal('https://schema.org/Message') +}); + +type MessageEntity = InferOutput; + +export default function isPreChatMessageActivity( + activity: undefined | WebChatActivity +): activity is WebChatActivity & { type: 'message' } { + if (activity?.type !== 'message') { + return false; + } + + const message = activity.entities?.find( + (entity): entity is MessageEntity => safeParse(messageEntity, entity).success + ); + + return !!message?.keywords.includes('PreChatMessage'); +} diff --git a/packages/fluent-theme/src/components/preChatActivity/private/MonochromeImageMasker.module.css b/packages/fluent-theme/src/components/preChatActivity/private/MonochromeImageMasker.module.css new file mode 100644 index 0000000000..6c0b1c3ce9 --- /dev/null +++ b/packages/fluent-theme/src/components/preChatActivity/private/MonochromeImageMasker.module.css @@ -0,0 +1,5 @@ +:global(.webchat-fluent) .pre-chat-message-activity__monochrome-image-masker { + background-color: var(--webchat-colorNeutralForeground4); + mask-image: var(--mask-image); + --webkit-mask-image: var(--mask-image); +} diff --git a/packages/fluent-theme/src/components/preChatActivity/private/MonochromeImageMasker.tsx b/packages/fluent-theme/src/components/preChatActivity/private/MonochromeImageMasker.tsx new file mode 100644 index 0000000000..fa679f0772 --- /dev/null +++ b/packages/fluent-theme/src/components/preChatActivity/private/MonochromeImageMasker.tsx @@ -0,0 +1,19 @@ +import cx from 'classnames'; +import React, { memo, useMemo, type CSSProperties } from 'react'; +import { useStyles } from '../../../styles/index.js'; +import styles from './MonochromeImageMasker.module.css'; + +type Props = Readonly<{ className?: string | undefined; src: string }>; + +const MonochromeImageMasker = ({ className, src }: Props) => { + const classNames = useStyles(styles); + const style = useMemo(() => ({ '--mask-image': `url(${src})` }) as CSSProperties, [src]); + + return ( +
+ ); +}; + +MonochromeImageMasker.displayName = 'MonochromeImageMasker'; + +export default memo(MonochromeImageMasker); diff --git a/packages/fluent-theme/src/components/sendBox/SendBox.tsx b/packages/fluent-theme/src/components/sendBox/SendBox.tsx index e93065c40b..770afa17e3 100644 --- a/packages/fluent-theme/src/components/sendBox/SendBox.tsx +++ b/packages/fluent-theme/src/components/sendBox/SendBox.tsx @@ -183,7 +183,7 @@ function SendBox( />
- {!telephoneKeypadShown && maxMessageLength && ( + {!telephoneKeypadShown && maxMessageLength && isFinite(maxMessageLength) && (
{ focus('sendBox'); }, [focus]); - const children = suggestedActions.map((cardAction, index) => { - const { displayText, image, imageAltText, text, type, value } = cardAction as { - displayText?: string; - image?: string; - imageAltText?: string; - text?: string; - type: - | 'call' - | 'downloadFile' - | 'imBack' - | 'messageBack' - | 'openUrl' - | 'playAudio' - | 'playVideo' - | 'postBack' - | 'showImage' - | 'signin'; - value?: { [key: string]: any } | string; - }; + const children = isPreChatMessageActivity(activity) + ? [] // Do not show suggested actions for pre-chat message, suggested actions has already shown inlined. + : suggestedActions.map((cardAction, index) => { + const { displayText, image, imageAltText, text, type, value } = cardAction as { + displayText?: string; + image?: string; + imageAltText?: string; + text?: string; + type: + | 'call' + | 'downloadFile' + | 'imBack' + | 'messageBack' + | 'openUrl' + | 'playAudio' + | 'playVideo' + | 'postBack' + | 'showImage' + | 'signin'; + value?: { [key: string]: any } | string; + }; - if (!suggestedActions?.length) { - return null; - } + if (!suggestedActions?.length) { + return null; + } - return ( - - ); - }); + return ( + + ); + }); return ( diff --git a/packages/fluent-theme/src/components/theme/Theme.module.css b/packages/fluent-theme/src/components/theme/Theme.module.css index a9852058b5..a7f8b004da 100644 --- a/packages/fluent-theme/src/components/theme/Theme.module.css +++ b/packages/fluent-theme/src/components/theme/Theme.module.css @@ -1,4 +1,3 @@ - :global(.webchat-fluent).theme { display: contents; @@ -16,10 +15,15 @@ --webchat-colorNeutralBackground4: var(--colorNeutralBackground4, #f0f0f0); --webchat-colorNeutralBackground5: var(--colorNeutralBackground5, #ebebeb); + --webchat-colorNeutralBackground1Disabled: var(--colorNeutralBackground1Disabled, #f0f0f0); + --webchat-colorNeutralBackground1Pressed: var(--colorNeutralBackground1Pressed, #e0e0e0); + + --webchat-colorNeutralGrey94: var(--colorNeutralGrey94, #f0f0f0); + --webchat-colorNeutralStroke1: var(--colorNeutralStroke1, #d1d1d1); --webchat-colorNeutralStroke2: var(--colorNeutralStroke2, #e0e0e0); --webchat-colorNeutralStroke1Selected: var(--colorNeutralStroke1Selected, #bdbdbd); - + --webchat-colorStrokeFocus2: var(--colorStrokeFocus2, #000000); --webchat-colorBrandStroke2: var(--colorBrandStroke2, #9edcf7); @@ -30,7 +34,7 @@ --webchat-colorBrandForegroundLink: var(--colorBrandForegroundLink, #01678c); --webchat-colorBrandForegroundLinkHover: var(--colorBrandForegroundLinkHover, #015a7a); --webchat-colorBrandForegroundLinkPressed: var(--colorBrandForegroundLinkPressed, #014259); - --webchat-colorBrandForegroundLinkSelected: var(--colorBrandForegroundLinkSelected, #01678c); + --webchat-colorBrandForegroundLinkSelected: var(--colorBrandForegroundLinkSelected, #01678c); --webchat-colorBrandBackground2Hover: var(--colorBrandBackground2Hover, #bee7fa); --webchat-colorBrandBackground2Pressed: var(--colorBrandBackground2Pressed, #7fd2f5); @@ -55,14 +59,25 @@ /* https://github.com/microsoft/fluentui/blob/master/packages/tokens/src/global/spacings.ts */ --webchat-spacingHorizontalMNudge: var(--spacingHorizontalMNudge, 10px); + --webchat-spacingHorizontalM: var(--spacingHorizontalM, 12px); + --webchat-spacingHorizontalXXXL: var(--spacingHorizontalXXXL, 32px); + --webchat-spacingVerticalL: var(--spacingVerticalL, 16px); /* https://github.com/microsoft/fluentui/blob/master/packages/tokens/src/global/fonts.ts */ --webchat-fontFamilyBase: var(--fontFamilyBase, 'Segoe UI', 'Segoe UI Web (West European)', -apple-system, BlinkMacSystemFont, Roboto, 'Helvetica Neue', sans-serif); --webchat-fontFamilyNumeric: var(--fontFamilyNumeric, Bahnschrift, 'Segoe UI', 'Segoe UI Web (West European)', -apple-system, BlinkMacSystemFont, Roboto, 'Helvetica Neue', sans-serif); /* https://github.com/microsoft/fluentui/blob/master/packages/tokens/src/global/fonts.ts */ + --webchat-fontWeightRegular: var(--fontWeightRegular, 400); --webchat-fontWeightSemibold: var(--fontWeightSemibold, 600); - + + /* https://github.com/microsoft/fluentui/blob/master/packages/tokens/src/global/fonts.ts */ + --webchat-fontSizeBase300: var(--fontSizeBase300, 14px); + --webchat-fontSizeHero700: var(--fontSizeHero700, 28px); + + --webchat-lineHeightBase300: var(--lineHeightBase300, 20px); + --webchat-lineHeightHero700: var(--lineHeightHero700, 36px); + --webchat-strokeWidthThicker: var(--strokeWidthThicker, 3px); --webchat-durationUltraFast: var(--durationUltraFast, 0); @@ -70,6 +85,9 @@ --webchat-curveAccelerateMid: var(--curveAccelerateMid, cubic-bezier(1,0,1,1)); --webchat-curveDecelerateMid: var(--curveDecelerateMid, cubic-bezier(0,0,0,1)); + + /* https://github.com/microsoft/fluentui/blob/master/packages/tokens/src/utils/shadows.ts */ + --webchat-shadow2: 0 0 2px rgba(0, 0, 0, 12%), 0 1px 2px rgba(0, 0, 0, 14%); } @media (prefers-reduced-motion) { @@ -77,4 +95,4 @@ --webchat-durationUltraFast: 0.01ms; --webchat-durationNormal: 0.01ms; } -} \ No newline at end of file +} diff --git a/packages/fluent-theme/src/components/theme/Theme.tsx b/packages/fluent-theme/src/components/theme/Theme.tsx index 6b9d9a1b34..8465bf0182 100644 --- a/packages/fluent-theme/src/components/theme/Theme.tsx +++ b/packages/fluent-theme/src/components/theme/Theme.tsx @@ -5,7 +5,7 @@ import { useStyles } from '../../styles'; export const rootClassName = 'webchat-fluent'; -export default function WebchatTheme(props: Readonly<{ readonly children: ReactNode | undefined }>) { +export default function Theme(props: Readonly<{ readonly children: ReactNode | undefined }>) { const classNames = useStyles(styles); return
{props.children}
; } diff --git a/packages/fluent-theme/src/private/FluentThemeProvider.tsx b/packages/fluent-theme/src/private/FluentThemeProvider.tsx index bbd4bfb5af..77614405e2 100644 --- a/packages/fluent-theme/src/private/FluentThemeProvider.tsx +++ b/packages/fluent-theme/src/private/FluentThemeProvider.tsx @@ -1,20 +1,37 @@ import { Components } from 'botframework-webchat-component'; import React, { memo, type ReactNode } from 'react'; +import type { ActivityMiddleware } from 'botframework-webchat-api'; +import { isPreChatMessageActivity, PreChatMessageActivity } from '../components/preChatActivity'; +import { SendBox } from '../components/sendBox'; import { TelephoneKeypadProvider } from '../components/telephoneKeypad'; import { WebChatTheme } from '../components/theme'; -import { SendBox } from '../components/sendBox'; const { ThemeProvider } = Components; type Props = Readonly<{ children?: ReactNode | undefined }>; +const activityMiddleware: ActivityMiddleware[] = [ + () => + next => + (...args) => { + const activity = args[0]?.activity; + + if (activity && isPreChatMessageActivity(activity)) { + return () => ; + } + + return next(...args); + } +]; const sendBoxMiddleware = [() => () => () => SendBox]; const FluentThemeProvider = ({ children }: Props) => ( - {children} + + {children} + );