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
JavaScript
// 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., A)
// 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