UNPKG

communication-react-19

Version:

React library for building modern communication user experiences utilizing Azure Communication Services (React 19 compatible fork)

213 lines 10.8 kB
// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. import React from 'react'; import { _formatString } from "../../../../acs-ui-common/src"; import parse, { Element as DOMElement } from 'html-react-parser'; import { attributesToProps } from 'html-react-parser'; import Linkify from 'react-linkify'; import { Link } from '@fluentui/react'; /* @conditional-compile-remove(data-loss-prevention) */ import { FontIcon, Stack } from '@fluentui/react'; import LiveMessage from '../Announcer/LiveMessage'; /* @conditional-compile-remove(mention) */ import { defaultOnMentionRender } from './MentionRenderer'; import DOMPurify from 'dompurify'; /* @conditional-compile-remove(data-loss-prevention) */ import { dataLossIconStyle } from '../styles/MessageThread.styles'; import { messageTextContentStyles } from '../styles/MessageThread.styles'; /** @private */ export const ChatMessageContent = (props) => { switch (props.message.contentType) { case 'text': return MessageContentAsText(props); case 'html': return MessageContentAsRichTextHTML(props); case 'richtext/html': return MessageContentAsRichTextHTML(props); default: console.warn('unknown message content type'); return React.createElement(React.Fragment, null); } }; const MessageContentWithLiveAria = (props) => { return (React.createElement("div", { "data-ui-status": props.message.status, role: "text", "aria-label": props.ariaLabel, className: props.className }, React.createElement(LiveMessage, { message: props.liveMessage, ariaLive: "polite" }), props.content)); }; const MessageContentAsRichTextHTML = (props) => { return (React.createElement(MessageContentWithLiveAria, { message: props.message, liveMessage: generateLiveMessage(props), ariaLabel: messageContentAriaText(props), content: processHtmlToReact(props) })); }; const MessageContentAsText = (props) => { return (React.createElement(MessageContentWithLiveAria, { message: props.message, liveMessage: generateLiveMessage(props), ariaLabel: messageContentAriaText(props), className: messageTextContentStyles, content: React.createElement(Linkify, { componentDecorator: (decoratedHref, decoratedText, key) => { return (React.createElement(Link, { target: "_blank", href: decoratedHref, key: key }, decoratedText)); } }, props.message.content) })); }; /* @conditional-compile-remove(data-loss-prevention) */ /** * @private */ export const BlockedMessageContent = (props) => { var _a; const Icon = React.createElement(FontIcon, { className: dataLossIconStyle, iconName: 'DataLossPreventionProhibited' }); const blockedMessage = props.message.warningText === undefined ? props.strings.blockedWarningText : props.message.warningText; const blockedMessageLink = props.message.link; const blockedMessageLinkText = blockedMessageLink ? (_a = props.message.linkText) !== null && _a !== void 0 ? _a : props.strings.blockedWarningLinkText : ''; const liveAuthor = props.message.mine || props.message.senderDisplayName === undefined ? '' : props.message.senderDisplayName; const liveBlockedWarningText = `${liveAuthor} ${blockedMessage} ${blockedMessageLinkText}`; return (React.createElement(MessageContentWithLiveAria, { message: props.message, liveMessage: liveBlockedWarningText, ariaLabel: liveBlockedWarningText, content: React.createElement(Stack, { horizontal: true, wrap: true }, Icon, blockedMessage && React.createElement("p", null, blockedMessage), blockedMessageLink && (React.createElement(Link, { target: '_blank', href: blockedMessageLink }, blockedMessageLinkText))) })); }; const extractContentForAllyMessage = (props) => { var _a; if (props.message.content || props.message.attachments) { // Replace all <img> tags with 'image' for aria. const parsedContent = DOMPurify.sanitize((_a = props.message.content) !== null && _a !== void 0 ? _a : '', { ALLOWED_TAGS: ['img'], RETURN_DOM_FRAGMENT: true }); parsedContent.childNodes.forEach((child) => { if (child.nodeName.toLowerCase() !== 'img') { return; } const imageTextNode = document.createElement('div'); imageTextNode.innerHTML = 'image '; parsedContent.replaceChild(imageTextNode, child); }); // Inject message attachment count for aria. // this is only applying to file attachments not for inline images. if (props.message.attachments && props.message.attachments.length > 0) { const attachmentCardDescription = attachmentCardGroupDescription(props); const attachmentTextNode = document.createElement('div'); attachmentTextNode.innerHTML = `${attachmentCardDescription}`; parsedContent.appendChild(attachmentTextNode); } // Strip all html tags from the content for aria. let message = DOMPurify.sanitize(parsedContent, { ALLOWED_TAGS: [] }); // decode HTML entities so that screen reader can read the content properly. message = decodeEntities(message); return message; } return ''; }; const generateLiveMessage = (props) => { const messageContent = extractContentForAllyMessage(props); if (props.message.editedOn) { const liveAuthor = _formatString(props.strings.editedMessageLiveAuthorIntro, { author: `${props.message.senderDisplayName}` }); return `${props.message.mine ? props.strings.editedMessageLocalUserLiveAuthorIntro : liveAuthor} ${messageContent}`; } else { const liveAuthor = _formatString(props.strings.liveAuthorIntro, { author: `${props.message.senderDisplayName}` }); return `${props.message.mine ? '' : liveAuthor} ${messageContent} `; } }; const messageContentAriaText = (props) => { var _a, _b; const message = extractContentForAllyMessage(props); return props.message.mine ? _formatString(props.strings.messageContentMineAriaText, { status: (_a = props.message.status) !== null && _a !== void 0 ? _a : '', message: message }) : _formatString(props.strings.messageContentAriaText, { status: (_b = props.message.status) !== null && _b !== void 0 ? _b : '', author: `${props.message.senderDisplayName}`, message: message }); }; const attachmentCardGroupDescription = (props) => { const attachments = props.message.attachments; return getAttachmentCountLiveMessage(attachments !== null && attachments !== void 0 ? attachments : [], props.strings.attachmentCardGroupMessage); }; /** * @private */ export const getAttachmentCountLiveMessage = (attachments, attachmentCardGroupMessage) => { if (attachments.length === 0) { return ''; } return _formatString(attachmentCardGroupMessage, { attachmentCount: `${attachments.length}` }); }; const defaultOnRenderInlineImage = (inlineImage) => { return (React.createElement("img", Object.assign({ key: inlineImage.imageAttributes.id, tabIndex: 0, "data-ui-id": inlineImage.imageAttributes.id }, inlineImage.imageAttributes))); }; const processHtmlToReact = (props) => { var _a; const options = { transform(reactNode, domNode) { var _a, _b, _c; if (domNode instanceof DOMElement && domNode.attribs) { // Transform custom rendering of mentions /* @conditional-compile-remove(mention) */ if (domNode.name === 'msft-mention' && domNode.attribs.id) { const { id } = domNode.attribs; const mention = { id: id, displayText: (_a = domNode.children[0].nodeValue) !== null && _a !== void 0 ? _a : '' }; if ((_b = props.mentionDisplayOptions) === null || _b === void 0 ? void 0 : _b.onRenderMention) { return props.mentionDisplayOptions.onRenderMention(mention, defaultOnMentionRender); } return defaultOnMentionRender(mention); } // Transform inline images if (domNode.name && domNode.name === 'img' && domNode.attribs && domNode.attribs.id) { if (domNode.attribs.name) { domNode.attribs['aria-label'] = domNode.attribs.name; } const imgProps = attributesToProps(domNode.attribs); const inlineImageProps = { messageId: props.message.messageId, imageAttributes: imgProps }; return ((_c = props.inlineImageOptions) === null || _c === void 0 ? void 0 : _c.onRenderInlineImage) ? props.inlineImageOptions.onRenderInlineImage(inlineImageProps, defaultOnRenderInlineImage) : defaultOnRenderInlineImage(inlineImageProps); } // Transform links to open in new tab if (domNode.name === 'a' && React.isValidElement(reactNode)) { return React.cloneElement(reactNode, { target: '_blank', rel: 'noreferrer noopener' }); } } // Pass through the original node return reactNode; } }; return React.createElement(React.Fragment, null, parse((_a = props.message.content) !== null && _a !== void 0 ? _a : '', options)); }; const decodeEntities = (encodedString) => { // This regular expression matches HTML entities. const translate_re = /&(nbsp|amp|quot|lt|gt);/g; // This object maps HTML entities to their respective characters. const translate = { nbsp: ' ', amp: '&', quot: '"', lt: '<', gt: '>' }; return (encodedString // Find all matches of HTML entities defined in translate_re and // replace them with the corresponding character from the translate object. .replace(translate_re, function (match, entity) { var _a; return (_a = translate[entity]) !== null && _a !== void 0 ? _a : match; }) // Find numeric entities (e.g., &#65;) // and replace them with the equivalent character using the String.fromCharCode method, // which converts Unicode values into characters. .replace(/&#(\d+);/gi, function (match, numStr) { const num = parseInt(numStr, 10); return String.fromCharCode(num); })); }; //# sourceMappingURL=ChatMessageContent.js.map