diff --git a/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.js b/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.js
index ff99cbf38d3f4..37a9f693cf2fb 100755
--- a/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.js
+++ b/src/components/HTMLEngineProvider/BaseHTMLEngineProvider.js
@@ -1,30 +1,15 @@
-/* eslint-disable react/prop-types */
import _ from 'underscore';
import React, {useMemo} from 'react';
-import {TouchableOpacity, Linking} from 'react-native';
import {
TRenderEngineProvider,
RenderHTMLConfigProvider,
defaultHTMLElementModels,
- TNodeChildrenRenderer,
- splitBoxModelStyle,
} from 'react-native-render-html';
import PropTypes from 'prop-types';
-import lodashGet from 'lodash/get';
-import Config from '../../CONFIG';
+import htmlRenderers from './HTMLRenderers';
+import * as HTMLEngineUtils from './htmlEngineUtils';
import styles from '../../styles/styles';
-import * as StyleUtils from '../../styles/StyleUtils';
import fontFamily from '../../styles/fontFamily';
-import AnchorForCommentsOnly from '../AnchorForCommentsOnly';
-import InlineCodeBlock from '../InlineCodeBlock';
-import AttachmentModal from '../AttachmentModal';
-import ThumbnailImage from '../ThumbnailImage';
-import variables from '../../styles/variables';
-import themeColors from '../../styles/themes/default';
-import Text from '../Text';
-import withLocalize from '../withLocalize';
-import Navigation from '../../libs/Navigation/Navigation';
-import CONST from '../../CONST';
const propTypes = {
/** Whether text elements should be selectable */
@@ -37,223 +22,6 @@ const defaultProps = {
children: null,
};
-const MAX_IMG_DIMENSIONS = 512;
-
-const EXTRA_FONTS = [
- fontFamily.GTA,
- fontFamily.GTA_BOLD,
- fontFamily.GTA_ITALIC,
- fontFamily.MONOSPACE,
- fontFamily.MONOSPACE_ITALIC,
- fontFamily.MONOSPACE_BOLD,
- fontFamily.MONOSPACE_BOLD_ITALIC,
- fontFamily.SYSTEM,
-];
-
-/**
- * Compute embedded maximum width from the available screen width. This function
- * is used by the HTML component in the default renderer for img tags to scale
- * down images that would otherwise overflow horizontally.
- *
- * @param {string} tagName - The name of the tag for which max width should be constrained.
- * @param {number} contentWidth - The content width provided to the HTML
- * component.
- * @returns {number} The minimum between contentWidth and MAX_IMG_DIMENSIONS
- */
-function computeEmbeddedMaxWidth(tagName, contentWidth) {
- if (tagName === 'img') {
- return Math.min(MAX_IMG_DIMENSIONS, contentWidth);
- }
- return contentWidth;
-}
-
-/**
- * Check if there is an ancestor node with name 'comment'.
- * Finding node with name 'comment' flags that we are rendering a comment.
- * @param {TNode} tnode
- * @returns {Boolean}
- */
-function isInsideComment(tnode) {
- let currentNode = tnode;
- while (currentNode.parent) {
- if (currentNode.domNode.name === 'comment') {
- return true;
- }
- currentNode = currentNode.parent;
- }
- return false;
-}
-
-function AnchorRenderer(props) {
- const htmlAttribs = props.tnode.attributes;
-
- // An auth token is needed to download Expensify chat attachments
- const isAttachment = Boolean(htmlAttribs['data-expensify-source']);
- const fileName = lodashGet(props.tnode, 'domNode.children[0].data', '');
- const parentStyle = lodashGet(props.tnode, 'parent.styles.nativeTextRet', {});
- const attrHref = htmlAttribs.href || '';
- const internalExpensifyPath = (attrHref.startsWith(CONST.NEW_EXPENSIFY_URL) && attrHref.replace(CONST.NEW_EXPENSIFY_URL, ''))
- || (attrHref.startsWith(CONST.STAGING_NEW_EXPENSIFY_URL) && attrHref.replace(CONST.STAGING_NEW_EXPENSIFY_URL, ''));
-
- // If we are handling a New Expensify link then we will assume this should be opened by the app internally. This ensures that the links are opened internally via react-navigation
- // instead of in a new tab or with a page refresh (which is the default behavior of an anchor tag)
- if (internalExpensifyPath) {
- return (
- Navigation.navigate(internalExpensifyPath)}
- >
-
-
- );
- }
-
- if (!isInsideComment(props.tnode)) {
- // This is not a comment from a chat, the AnchorForCommentsOnly uses a Pressable to create a context menu on right click.
- // We don't have this behaviour in other links in NewDot
- // TODO: We should use TextLink, but I'm leaving it as Text for now because TextLink breaks the alignment in Android.
- return (
- {
- Linking.openURL(attrHref);
- }}
- >
-
-
- );
- }
-
- return (
-
-
-
- );
-}
-
-function CodeRenderer(props) {
- // We split wrapper and inner styles
- // "boxModelStyle" corresponds to border, margin, padding and backgroundColor
- const {boxModelStyle, otherStyle: textStyle} = splitBoxModelStyle(props.style);
-
- // Get the correct fontFamily variant based in the fontStyle and fontWeight
- const font = StyleUtils.getFontFamilyMonospace({
- fontStyle: textStyle.fontStyle,
- fontWeight: textStyle.fontWeight,
- });
-
- const textStyleOverride = {
- fontFamily: font,
-
- // We need to override this properties bellow that was defined in `textStyle`
- // Because by default the `react-native-render-html` add a style in the elements,
- // for example the tag has a fontWeight: "bold" and in the android it break the font
- fontWeight: undefined,
- fontStyle: undefined,
- };
-
- const defaultRendererProps = _.omit(props, ['TDefaultRenderer', 'style']);
-
- return (
-
- );
-}
-
-function EditedRenderer(props) {
- const defaultRendererProps = _.omit(props, ['TDefaultRenderer', 'style', 'tnode']);
- return (
-
- {/* Native devices do not support margin between nested text */}
- {' '}
- {props.translate('reportActionCompose.edited')}
-
- );
-}
-
-function ImgRenderer(props) {
- const htmlAttribs = props.tnode.attributes;
-
- // There are two kinds of images that need to be displayed:
- //
- // - Chat Attachment images
- //
- // Images uploaded by the user via the app or email.
- // These have a full-sized image `htmlAttribs['data-expensify-source']`
- // and a thumbnail `htmlAttribs.src`. Both of these URLs need to have
- // an authToken added to them in order to control who
- // can see the images.
- //
- // - Non-Attachment Images
- //
- // These could be hosted from anywhere (Expensify or another source)
- // and are not protected by any kind of access control e.g. certain
- // Concierge responder attachments are uploaded to S3 without any access
- // control and thus require no authToken to verify access.
- //
- const isAttachment = Boolean(htmlAttribs['data-expensify-source']);
- const originalFileName = htmlAttribs['data-name'];
- let previewSource = htmlAttribs.src;
- let source = isAttachment
- ? htmlAttribs['data-expensify-source']
- : htmlAttribs.src;
-
- // Update the image URL so the images can be accessed depending on the config environment
- previewSource = previewSource.replace(
- Config.EXPENSIFY.URL_EXPENSIFY_COM,
- Config.EXPENSIFY.URL_API_ROOT,
- );
- source = source.replace(
- Config.EXPENSIFY.URL_EXPENSIFY_COM,
- Config.EXPENSIFY.URL_API_ROOT,
- );
-
- return (
-
- {({show}) => (
- show()}
- >
-
-
- )}
-
- );
-}
-
// Declare nonstandard tags and their content model here
const customHTMLElementModels = {
edited: defaultHTMLElementModels.span.extend({
@@ -268,23 +36,6 @@ const customHTMLElementModels = {
}),
};
-// Define the custom renderer components
-const renderers = {
- a: AnchorRenderer,
- code: CodeRenderer,
- img: ImgRenderer,
- edited: withLocalize(EditedRenderer),
-};
-
-const renderersProps = {
- img: {
- initialDimensions: {
- width: MAX_IMG_DIMENSIONS,
- height: MAX_IMG_DIMENSIONS,
- },
- },
-};
-
const defaultViewProps = {style: {alignItems: 'flex-start'}};
// We are using the explicit composite architecture for performance gains.
@@ -303,14 +54,13 @@ const BaseHTMLEngineProvider = (props) => {
tagsStyles={styles.webViewStyles.tagStyles}
enableCSSInlineProcessing={false}
dangerouslyDisableWhitespaceCollapsing
- systemFonts={EXTRA_FONTS}
+ systemFonts={_.values(fontFamily)}
>
{props.children}
diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.js b/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.js
new file mode 100644
index 0000000000000..62362d41b77dd
--- /dev/null
+++ b/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.js
@@ -0,0 +1,77 @@
+import React from 'react';
+import {Linking} from 'react-native';
+import {
+ TNodeChildrenRenderer,
+} from 'react-native-render-html';
+import lodashGet from 'lodash/get';
+import htmlRendererPropTypes from './htmlRendererPropTypes';
+import * as HTMLEngineUtils from '../htmlEngineUtils';
+import Text from '../../Text';
+import CONST from '../../../CONST';
+import styles from '../../../styles/styles';
+import Navigation from '../../../libs/Navigation/Navigation';
+import AnchorForCommentsOnly from '../../AnchorForCommentsOnly';
+
+const AnchorRenderer = (props) => {
+ const htmlAttribs = props.tnode.attributes;
+
+ // An auth token is needed to download Expensify chat attachments
+ const isAttachment = Boolean(htmlAttribs['data-expensify-source']);
+ const fileName = lodashGet(props.tnode, 'domNode.children[0].data', '');
+ const parentStyle = lodashGet(props.tnode, 'parent.styles.nativeTextRet', {});
+ const attrHref = htmlAttribs.href || '';
+ const internalExpensifyPath = (attrHref.startsWith(CONST.NEW_EXPENSIFY_URL) && attrHref.replace(CONST.NEW_EXPENSIFY_URL, ''))
+ || (attrHref.startsWith(CONST.STAGING_NEW_EXPENSIFY_URL) && attrHref.replace(CONST.STAGING_NEW_EXPENSIFY_URL, ''));
+
+ // If we are handling a New Expensify link then we will assume this should be opened by the app internally. This ensures that the links are opened internally via react-navigation
+ // instead of in a new tab or with a page refresh (which is the default behavior of an anchor tag)
+ if (internalExpensifyPath) {
+ return (
+ Navigation.navigate(internalExpensifyPath)}
+ >
+
+
+ );
+ }
+
+ if (!HTMLEngineUtils.isInsideComment(props.tnode)) {
+ // This is not a comment from a chat, the AnchorForCommentsOnly uses a Pressable to create a context menu on right click.
+ // We don't have this behaviour in other links in NewDot
+ // TODO: We should use TextLink, but I'm leaving it as Text for now because TextLink breaks the alignment in Android.
+ return (
+ Linking.openURL(attrHref)}
+ >
+
+
+ );
+ }
+
+ return (
+
+
+
+ );
+};
+
+AnchorRenderer.propTypes = htmlRendererPropTypes;
+AnchorRenderer.displayName = 'AnchorRenderer';
+
+export default AnchorRenderer;
diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/CodeRenderer.js b/src/components/HTMLEngineProvider/HTMLRenderers/CodeRenderer.js
new file mode 100644
index 0000000000000..1a351447d4eca
--- /dev/null
+++ b/src/components/HTMLEngineProvider/HTMLRenderers/CodeRenderer.js
@@ -0,0 +1,45 @@
+import _ from 'underscore';
+import React from 'react';
+import {splitBoxModelStyle} from 'react-native-render-html';
+import htmlRendererPropTypes from './htmlRendererPropTypes';
+import * as StyleUtils from '../../../styles/StyleUtils';
+import InlineCodeBlock from '../../InlineCodeBlock';
+
+const CodeRenderer = (props) => {
+ // We split wrapper and inner styles
+ // "boxModelStyle" corresponds to border, margin, padding and backgroundColor
+ const {boxModelStyle, otherStyle: textStyle} = splitBoxModelStyle(props.style);
+
+ // Get the correct fontFamily variant based in the fontStyle and fontWeight
+ const font = StyleUtils.getFontFamilyMonospace({
+ fontStyle: textStyle.fontStyle,
+ fontWeight: textStyle.fontWeight,
+ });
+
+ const textStyleOverride = {
+ fontFamily: font,
+
+ // We need to override this properties bellow that was defined in `textStyle`
+ // Because by default the `react-native-render-html` add a style in the elements,
+ // for example the tag has a fontWeight: "bold" and in the android it break the font
+ fontWeight: undefined,
+ fontStyle: undefined,
+ };
+
+ const defaultRendererProps = _.omit(props, ['TDefaultRenderer', 'style']);
+
+ return (
+
+ );
+};
+
+CodeRenderer.propTypes = htmlRendererPropTypes;
+CodeRenderer.displayName = 'CodeRenderer';
+
+export default CodeRenderer;
diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/EditedRenderer.js b/src/components/HTMLEngineProvider/HTMLRenderers/EditedRenderer.js
new file mode 100644
index 0000000000000..0b04c3a885beb
--- /dev/null
+++ b/src/components/HTMLEngineProvider/HTMLRenderers/EditedRenderer.js
@@ -0,0 +1,34 @@
+import _ from 'underscore';
+import React from 'react';
+import htmlRendererPropTypes from './htmlRendererPropTypes';
+import withLocalize, {withLocalizePropTypes} from '../../withLocalize';
+import Text from '../../Text';
+import variables from '../../../styles/variables';
+import themeColors from '../../../styles/themes/default';
+import styles from '../../../styles/styles';
+
+const propTypes = {
+ ...htmlRendererPropTypes,
+ ...withLocalizePropTypes,
+};
+
+const EditedRenderer = (props) => {
+ const defaultRendererProps = _.omit(props, ['TDefaultRenderer', 'style', 'tnode']);
+ return (
+
+ {/* Native devices do not support margin between nested text */}
+ {' '}
+ {props.translate('reportActionCompose.edited')}
+
+ );
+};
+
+EditedRenderer.propTypes = propTypes;
+EditedRenderer.displayName = 'EditedRenderer';
+
+export default withLocalize(EditedRenderer);
diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.js b/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.js
new file mode 100644
index 0000000000000..e86bdedc87d1a
--- /dev/null
+++ b/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.js
@@ -0,0 +1,71 @@
+import React from 'react';
+import {TouchableOpacity} from 'react-native';
+import htmlRendererPropTypes from './htmlRendererPropTypes';
+import Config from '../../../CONFIG';
+import AttachmentModal from '../../AttachmentModal';
+import styles from '../../../styles/styles';
+import ThumbnailImage from '../../ThumbnailImage';
+
+const ImageRenderer = (props) => {
+ const htmlAttribs = props.tnode.attributes;
+
+ // There are two kinds of images that need to be displayed:
+ //
+ // - Chat Attachment images
+ //
+ // Images uploaded by the user via the app or email.
+ // These have a full-sized image `htmlAttribs['data-expensify-source']`
+ // and a thumbnail `htmlAttribs.src`. Both of these URLs need to have
+ // an authToken added to them in order to control who
+ // can see the images.
+ //
+ // - Non-Attachment Images
+ //
+ // These could be hosted from anywhere (Expensify or another source)
+ // and are not protected by any kind of access control e.g. certain
+ // Concierge responder attachments are uploaded to S3 without any access
+ // control and thus require no authToken to verify access.
+ //
+ const isAttachment = Boolean(htmlAttribs['data-expensify-source']);
+ const originalFileName = htmlAttribs['data-name'];
+ let previewSource = htmlAttribs.src;
+ let source = isAttachment
+ ? htmlAttribs['data-expensify-source']
+ : htmlAttribs.src;
+
+ // Update the image URL so the images can be accessed depending on the config environment
+ previewSource = previewSource.replace(
+ Config.EXPENSIFY.URL_EXPENSIFY_COM,
+ Config.EXPENSIFY.URL_API_ROOT,
+ );
+ source = source.replace(
+ Config.EXPENSIFY.URL_EXPENSIFY_COM,
+ Config.EXPENSIFY.URL_API_ROOT,
+ );
+
+ return (
+
+ {({show}) => (
+
+
+
+ )}
+
+ );
+};
+
+ImageRenderer.propTypes = htmlRendererPropTypes;
+ImageRenderer.displayName = 'ImageRenderer';
+
+export default ImageRenderer;
diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/htmlRendererPropTypes.js b/src/components/HTMLEngineProvider/HTMLRenderers/htmlRendererPropTypes.js
new file mode 100644
index 0000000000000..f26806482e484
--- /dev/null
+++ b/src/components/HTMLEngineProvider/HTMLRenderers/htmlRendererPropTypes.js
@@ -0,0 +1,8 @@
+import PropTypes from 'prop-types';
+
+export default {
+ tnode: PropTypes.object,
+ TDefaultRenderer: PropTypes.oneOfType([PropTypes.object, PropTypes.func]),
+ key: PropTypes.string,
+ style: PropTypes.object,
+};
diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/index.js b/src/components/HTMLEngineProvider/HTMLRenderers/index.js
new file mode 100644
index 0000000000000..8186dd57841b3
--- /dev/null
+++ b/src/components/HTMLEngineProvider/HTMLRenderers/index.js
@@ -0,0 +1,17 @@
+import AnchorRenderer from './AnchorRenderer';
+import CodeRenderer from './CodeRenderer';
+import EditedRenderer from './EditedRenderer';
+import ImageRenderer from './ImageRenderer';
+
+/**
+ * This collection defines our custom renderers. It is a mapping from HTML tag type to the corresponding component.
+ */
+export default {
+ // Standard HTML tag renderers
+ a: AnchorRenderer,
+ code: CodeRenderer,
+ img: ImageRenderer,
+
+ // Custom tag renderers
+ edited: EditedRenderer,
+};
diff --git a/src/components/HTMLEngineProvider/htmlEngineUtils.js b/src/components/HTMLEngineProvider/htmlEngineUtils.js
new file mode 100644
index 0000000000000..dc678b99d6017
--- /dev/null
+++ b/src/components/HTMLEngineProvider/htmlEngineUtils.js
@@ -0,0 +1,40 @@
+const MAX_IMG_DIMENSIONS = 512;
+
+/**
+ * Compute embedded maximum width from the available screen width. This function
+ * is used by the HTML component in the default renderer for img tags to scale
+ * down images that would otherwise overflow horizontally.
+ *
+ * @param {string} tagName - The name of the tag for which max width should be constrained.
+ * @param {number} contentWidth - The content width provided to the HTML
+ * component.
+ * @returns {number} The minimum between contentWidth and MAX_IMG_DIMENSIONS
+ */
+function computeEmbeddedMaxWidth(tagName, contentWidth) {
+ if (tagName === 'img') {
+ return Math.min(MAX_IMG_DIMENSIONS, contentWidth);
+ }
+ return contentWidth;
+}
+
+/**
+ * Check if there is an ancestor node with name 'comment'.
+ * Finding node with name 'comment' flags that we are rendering a comment.
+ * @param {TNode} tnode
+ * @returns {Boolean}
+ */
+function isInsideComment(tnode) {
+ let currentNode = tnode;
+ while (currentNode.parent) {
+ if (currentNode.domNode.name === 'comment') {
+ return true;
+ }
+ currentNode = currentNode.parent;
+ }
+ return false;
+}
+
+export {
+ computeEmbeddedMaxWidth,
+ isInsideComment,
+};