botframework-webchat-component
Version:
React component of botframework-webchat
370 lines (328 loc) • 13.5 kB
JavaScript
import { Composer as APIComposer, hooks } from 'botframework-webchat-api';
import { Composer as SayComposer } from 'react-say';
import createEmotion from 'create-emotion';
import createStyleSet from './Styles/createStyleSet';
import MarkdownIt from 'markdown-it';
import PropTypes from 'prop-types';
import React, { useCallback, useMemo, useRef, useState } from 'react';
import {
speechSynthesis as bypassSpeechSynthesis,
SpeechSynthesisUtterance as BypassSpeechSynthesisUtterance
} from './hooks/internal/BypassSpeechSynthesisPonyfill';
import addTargetBlankToHyperlinksMarkdown from './Utils/addTargetBlankToHyperlinksMarkdown';
import createCSSKey from './Utils/createCSSKey';
import createDefaultActivityMiddleware from './Middleware/Activity/createCoreMiddleware';
import createDefaultActivityStatusMiddleware from './Middleware/ActivityStatus/createCoreMiddleware';
import createDefaultAttachmentForScreenReaderMiddleware from './Middleware/AttachmentForScreenReader/createCoreMiddleware';
import createDefaultAttachmentMiddleware from './Middleware/Attachment/createCoreMiddleware';
import createDefaultAvatarMiddleware from './Middleware/Avatar/createCoreMiddleware';
import createDefaultCardActionMiddleware from './Middleware/CardAction/createCoreMiddleware';
import createDefaultToastMiddleware from './Middleware/Toast/createCoreMiddleware';
import createDefaultTypingIndicatorMiddleware from './Middleware/TypingIndicator/createCoreMiddleware';
import Dictation from './Dictation';
import downscaleImageToDataURL from './Utils/downscaleImageToDataURL';
import ErrorBox from './ErrorBox';
import mapMap from './Utils/mapMap';
import singleToArray from './Utils/singleToArray';
import UITracker from './hooks/internal/UITracker';
import WebChatUIContext from './hooks/internal/WebChatUIContext';
const { useReferenceGrammarID, useStyleOptions } = hooks;
// eslint-disable-next-line no-undef
const node_env = process.env.node_env || process.env.NODE_ENV;
const emotionPool = {};
function styleSetToEmotionObjects(styleToEmotionObject, styleSet) {
return mapMap(styleSet, (style, key) => (key === 'options' ? style : styleToEmotionObject(style)));
}
const ComposerCore = ({
children,
extraStyleSet,
nonce,
renderMarkdown,
styleSet,
suggestedActionsAccessKey,
webSpeechPonyfillFactory
}) => {
const [dictateAbortable, setDictateAbortable] = useState();
const [referenceGrammarID] = useReferenceGrammarID();
const [styleOptions] = useStyleOptions();
const focusSendBoxCallbacksRef = useRef([]);
const focusTranscriptCallbacksRef = useRef([]);
const internalMarkdownIt = useMemo(() => new MarkdownIt(), []);
const scrollToCallbacksRef = useRef([]);
const scrollToEndCallbacksRef = useRef([]);
// Instead of having a `scrollUpCallbacksRef` and `scrollDownCallbacksRef`, they are combined into a single `scrollRelativeCallbacksRef`.
// The first argument tells whether it should go "up" or "down".
const scrollRelativeCallbacksRef = useRef([]);
const dictationOnError = useCallback(err => {
console.error(err);
}, []);
const internalRenderMarkdownInline = useMemo(
() => markdown => {
const tree = internalMarkdownIt.parseInline(markdown);
// We should add rel="noopener noreferrer" and target="_blank"
const patchedTree = addTargetBlankToHyperlinksMarkdown(tree);
return internalMarkdownIt.renderer.render(patchedTree);
},
[internalMarkdownIt]
);
const styleToEmotionObject = useMemo(() => {
// Emotion doesn't hash with nonce. We need to provide the pooling mechanism.
// 1. If 2 instances use different nonce, they should result in different hash;
// 2. If 2 instances are being mounted, pooling will make sure we render only 1 set of <style> tags, instead of 2.
const emotion =
emotionPool[nonce] || (emotionPool[nonce] = createEmotion({ key: `webchat--css-${createCSSKey()}`, nonce }));
return style => emotion.css(style);
}, [nonce]);
const patchedStyleSet = useMemo(
() =>
styleSetToEmotionObjects(styleToEmotionObject, {
...(styleSet || createStyleSet(styleOptions)),
...extraStyleSet
}),
[extraStyleSet, styleOptions, styleSet, styleToEmotionObject]
);
const webSpeechPonyfill = useMemo(() => {
const ponyfill = webSpeechPonyfillFactory && webSpeechPonyfillFactory({ referenceGrammarID });
const { speechSynthesis, SpeechSynthesisUtterance } = ponyfill || {};
return {
...ponyfill,
speechSynthesis: speechSynthesis || bypassSpeechSynthesis,
SpeechSynthesisUtterance: SpeechSynthesisUtterance || BypassSpeechSynthesisUtterance
};
}, [referenceGrammarID, webSpeechPonyfillFactory]);
const scrollPositionObserversRef = useRef([]);
const [numScrollPositionObservers, setNumScrollPositionObservers] = useState(0);
const dispatchScrollPosition = useCallback(
event => scrollPositionObserversRef.current.forEach(observer => observer(event)),
[scrollPositionObserversRef]
);
const observeScrollPosition = useCallback(
observer => {
scrollPositionObserversRef.current = [...scrollPositionObserversRef.current, observer];
setNumScrollPositionObservers(scrollPositionObserversRef.current.length);
return () => {
scrollPositionObserversRef.current = scrollPositionObserversRef.current.filter(target => target !== observer);
setNumScrollPositionObservers(scrollPositionObserversRef.current.length);
};
},
[scrollPositionObserversRef, setNumScrollPositionObservers]
);
const transcriptFocusObserversRef = useRef([]);
const [numTranscriptFocusObservers, setNumTranscriptFocusObservers] = useState(0);
const dispatchTranscriptFocus = useCallback(
event => transcriptFocusObserversRef.current.forEach(observer => observer(event)),
[transcriptFocusObserversRef]
);
const observeTranscriptFocus = useCallback(
observer => {
transcriptFocusObserversRef.current = [...transcriptFocusObserversRef.current, observer];
setNumTranscriptFocusObservers(transcriptFocusObserversRef.current.length);
return () => {
transcriptFocusObserversRef.current = transcriptFocusObserversRef.current.filter(target => target !== observer);
setNumTranscriptFocusObservers(transcriptFocusObserversRef.current.length);
};
},
[transcriptFocusObserversRef, setNumTranscriptFocusObservers]
);
const context = useMemo(
() => ({
dictateAbortable,
dispatchScrollPosition,
dispatchTranscriptFocus,
focusSendBoxCallbacksRef,
focusTranscriptCallbacksRef,
internalMarkdownItState: [internalMarkdownIt],
internalRenderMarkdownInline,
nonce,
numScrollPositionObservers,
numTranscriptFocusObservers,
observeScrollPosition,
observeTranscriptFocus,
renderMarkdown,
scrollRelativeCallbacksRef,
scrollToCallbacksRef,
scrollToEndCallbacksRef,
setDictateAbortable,
styleSet: patchedStyleSet,
styleToEmotionObject,
suggestedActionsAccessKey,
webSpeechPonyfill
}),
[
dictateAbortable,
dispatchScrollPosition,
dispatchTranscriptFocus,
focusSendBoxCallbacksRef,
focusTranscriptCallbacksRef,
internalMarkdownIt,
internalRenderMarkdownInline,
nonce,
numScrollPositionObservers,
numTranscriptFocusObservers,
observeScrollPosition,
observeTranscriptFocus,
patchedStyleSet,
renderMarkdown,
scrollRelativeCallbacksRef,
scrollToCallbacksRef,
scrollToEndCallbacksRef,
setDictateAbortable,
styleToEmotionObject,
suggestedActionsAccessKey,
webSpeechPonyfill
]
);
return (
<SayComposer ponyfill={webSpeechPonyfill}>
<WebChatUIContext.Provider value={context}>
{children}
<Dictation onError={dictationOnError} />
</WebChatUIContext.Provider>
</SayComposer>
);
};
ComposerCore.defaultProps = {
extraStyleSet: undefined,
nonce: undefined,
renderMarkdown: undefined,
styleSet: undefined,
suggestedActionsAccessKey: 'A a Å å',
webSpeechPonyfillFactory: undefined
};
ComposerCore.propTypes = {
extraStyleSet: PropTypes.any,
nonce: PropTypes.string,
renderMarkdown: PropTypes.func,
styleSet: PropTypes.any,
suggestedActionsAccessKey: PropTypes.oneOfType([PropTypes.oneOf([false]), PropTypes.string]),
webSpeechPonyfillFactory: PropTypes.func
};
const Composer = ({
activityMiddleware,
activityStatusMiddleware,
attachmentForScreenReaderMiddleware,
attachmentMiddleware,
avatarMiddleware,
cardActionMiddleware,
children,
extraStyleSet,
renderMarkdown,
styleSet,
suggestedActionsAccessKey,
toastMiddleware,
typingIndicatorMiddleware,
webSpeechPonyfillFactory,
...composerProps
}) => {
const { nonce, onTelemetry } = composerProps;
const patchedActivityMiddleware = useMemo(
() => [...singleToArray(activityMiddleware), ...createDefaultActivityMiddleware()],
[activityMiddleware]
);
const patchedActivityStatusMiddleware = useMemo(
() => [...singleToArray(activityStatusMiddleware), ...createDefaultActivityStatusMiddleware()],
[activityStatusMiddleware]
);
const patchedAttachmentForScreenReaderMiddleware = useMemo(
() => [
...singleToArray(attachmentForScreenReaderMiddleware),
...createDefaultAttachmentForScreenReaderMiddleware()
],
[attachmentForScreenReaderMiddleware]
);
const patchedAttachmentMiddleware = useMemo(
() => [...singleToArray(attachmentMiddleware), ...createDefaultAttachmentMiddleware()],
[attachmentMiddleware]
);
const patchedAvatarMiddleware = useMemo(
() => [...singleToArray(avatarMiddleware), ...createDefaultAvatarMiddleware()],
[avatarMiddleware]
);
const patchedCardActionMiddleware = useMemo(
() => [...singleToArray(cardActionMiddleware), ...createDefaultCardActionMiddleware()],
[cardActionMiddleware]
);
const patchedToastMiddleware = useMemo(() => [...singleToArray(toastMiddleware), ...createDefaultToastMiddleware()], [
toastMiddleware
]);
const patchedTypingIndicatorMiddleware = useMemo(
() => [...singleToArray(typingIndicatorMiddleware), ...createDefaultTypingIndicatorMiddleware()],
[typingIndicatorMiddleware]
);
return (
<React.Fragment>
<APIComposer
activityMiddleware={patchedActivityMiddleware}
activityStatusMiddleware={patchedActivityStatusMiddleware}
attachmentForScreenReaderMiddleware={patchedAttachmentForScreenReaderMiddleware}
attachmentMiddleware={patchedAttachmentMiddleware}
avatarMiddleware={patchedAvatarMiddleware}
cardActionMiddleware={patchedCardActionMiddleware}
downscaleImageToDataURL={downscaleImageToDataURL}
// Under dev server of create-react-app, "NODE_ENV" will be set to "development".
internalErrorBoxClass={node_env === 'development' ? ErrorBox : undefined}
nonce={nonce}
toastMiddleware={patchedToastMiddleware}
typingIndicatorMiddleware={patchedTypingIndicatorMiddleware}
{...composerProps}
>
<ComposerCore
extraStyleSet={extraStyleSet}
nonce={nonce}
renderMarkdown={renderMarkdown}
styleSet={styleSet}
suggestedActionsAccessKey={suggestedActionsAccessKey}
webSpeechPonyfillFactory={webSpeechPonyfillFactory}
>
{children}
{onTelemetry && <UITracker />}
</ComposerCore>
</APIComposer>
</React.Fragment>
);
};
Composer.defaultProps = {
...APIComposer.defaultProps,
...ComposerCore.defaultProps,
activityMiddleware: undefined,
activityRenderer: undefined,
activityStatusMiddleware: undefined,
activityStatusRenderer: undefined,
attachmentForScreenReaderMiddleware: undefined,
attachmentMiddleware: undefined,
attachmentRenderer: undefined,
avatarMiddleware: undefined,
avatarRenderer: undefined,
cardActionMiddleware: undefined,
children: undefined,
nonce: undefined,
renderMarkdown: undefined,
toastMiddleware: undefined,
toastRenderer: undefined,
typingIndicatorMiddleware: undefined,
typingIndicatorRenderer: undefined,
webSpeechPonyfillFactory: undefined
};
Composer.propTypes = {
...APIComposer.propTypes,
...ComposerCore.propTypes,
activityMiddleware: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.func), PropTypes.func]),
activityRenderer: PropTypes.func,
activityStatusMiddleware: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.func), PropTypes.func]),
activityStatusRenderer: PropTypes.func,
attachmentForScreenReaderMiddleware: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.func), PropTypes.func]),
attachmentMiddleware: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.func), PropTypes.func]),
attachmentRenderer: PropTypes.func,
avatarMiddleware: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.func), PropTypes.func]),
avatarRenderer: PropTypes.func,
cardActionMiddleware: PropTypes.func,
children: PropTypes.any,
nonce: PropTypes.string,
renderMarkdown: PropTypes.func,
toastMiddleware: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.func), PropTypes.func]),
toastRenderer: PropTypes.func,
typingIndicatorMiddleware: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.func), PropTypes.func]),
typingIndicatorRenderer: PropTypes.func,
webSpeechPonyfillFactory: PropTypes.func
};
export default Composer;