Skip to content
Merged
Original file line number Diff line number Diff line change
@@ -1,88 +1,90 @@
import PropTypes from 'prop-types';
import React, {useMemo} from 'react';
import {defaultHTMLElementModels, RenderHTMLConfigProvider, TRenderEngineProvider} from 'react-native-render-html';
import _ from 'underscore';
import type {TextProps} from 'react-native';
import {HTMLContentModel, HTMLElementModel, RenderHTMLConfigProvider, TRenderEngineProvider} from 'react-native-render-html';
import useThemeStyles from '@hooks/useThemeStyles';
import convertToLTR from '@libs/convertToLTR';
import FontUtils from '@styles/utils/FontUtils';
import type ChildrenProps from '@src/types/utils/ChildrenProps';
import * as HTMLEngineUtils from './htmlEngineUtils';
import htmlRenderers from './HTMLRenderers';

const propTypes = {
type BaseHTMLEngineProviderProps = ChildrenProps & {
/** Whether text elements should be selectable */
textSelectable: PropTypes.bool,
textSelectable?: boolean;

/** Handle line breaks according to the HTML standard (default on web) */
enableExperimentalBRCollapsing: PropTypes.bool,

children: PropTypes.node,
};

const defaultProps = {
textSelectable: false,
children: null,
enableExperimentalBRCollapsing: false,
enableExperimentalBRCollapsing?: boolean;
};

// We are using the explicit composite architecture for performance gains.
// Configuration for RenderHTML is handled in a top-level component providing
// context to RenderHTMLSource components. See https://git.io/JRcZb
// Beware that each prop should be referentialy stable between renders to avoid
// costly invalidations and commits.
function BaseHTMLEngineProvider(props) {
function BaseHTMLEngineProvider({textSelectable = false, children, enableExperimentalBRCollapsing = false}: BaseHTMLEngineProviderProps) {
const styles = useThemeStyles();

// Declare nonstandard tags and their content model here
/* eslint-disable @typescript-eslint/naming-convention */
Comment thread
ruben-rebelo marked this conversation as resolved.
Outdated
const customHTMLElementModels = useMemo(
() => ({
edited: defaultHTMLElementModels.span.extend({
edited: HTMLElementModel.fromCustomModel({
Comment thread
ruben-rebelo marked this conversation as resolved.
Outdated
tagName: 'edited',
contentModel: HTMLContentModel.textual,
}),
'alert-text': defaultHTMLElementModels.div.extend({
'alert-text': HTMLElementModel.fromCustomModel({
tagName: 'alert-text',
mixedUAStyles: {...styles.formError, ...styles.mb0},
contentModel: HTMLContentModel.block,
}),
'muted-text': defaultHTMLElementModels.div.extend({
'muted-text': HTMLElementModel.fromCustomModel({
tagName: 'muted-text',
mixedUAStyles: {...styles.colorMuted, ...styles.mb0},
contentModel: HTMLContentModel.block,
}),
comment: defaultHTMLElementModels.div.extend({
comment: HTMLElementModel.fromCustomModel({
tagName: 'comment',
mixedUAStyles: {whiteSpace: 'pre'},
contentModel: HTMLContentModel.block,
}),
'email-comment': defaultHTMLElementModels.div.extend({
'email-comment': HTMLElementModel.fromCustomModel({
tagName: 'email-comment',
mixedUAStyles: {whiteSpace: 'normal'},
contentModel: HTMLContentModel.block,
}),
strong: defaultHTMLElementModels.span.extend({
strong: HTMLElementModel.fromCustomModel({
tagName: 'strong',
mixedUAStyles: {whiteSpace: 'pre'},
contentModel: HTMLContentModel.textual,
}),
'mention-user': defaultHTMLElementModels.span.extend({tagName: 'mention-user'}),
'mention-here': defaultHTMLElementModels.span.extend({tagName: 'mention-here'}),
'next-step': defaultHTMLElementModels.span.extend({
'mention-user': HTMLElementModel.fromCustomModel({tagName: 'mention-user', contentModel: HTMLContentModel.textual}),
'mention-here': HTMLElementModel.fromCustomModel({tagName: 'mention-here', contentModel: HTMLContentModel.textual}),
'next-step': HTMLElementModel.fromCustomModel({
tagName: 'next-step',
mixedUAStyles: {...styles.textLabelSupporting, ...styles.lh16},
contentModel: HTMLContentModel.textual,
}),
'next-step-email': defaultHTMLElementModels.span.extend({tagName: 'next-step-email'}),
video: defaultHTMLElementModels.div.extend({
'next-step-email': HTMLElementModel.fromCustomModel({tagName: 'next-step-email', contentModel: HTMLContentModel.textual}),
video: HTMLElementModel.fromCustomModel({
tagName: 'video',
mixedUAStyles: {whiteSpace: 'pre'},
contentModel: HTMLContentModel.block,
}),
}),
[styles.colorMuted, styles.formError, styles.mb0, styles.textLabelSupporting, styles.lh16],
);
/* eslint-enable @typescript-eslint/naming-convention */

// We need to memoize this prop to make it referentially stable.
const defaultTextProps = useMemo(() => ({selectable: props.textSelectable, allowFontScaling: false, textBreakStrategy: 'simple'}), [props.textSelectable]);
const defaultTextProps: TextProps = useMemo(() => ({selectable: textSelectable, allowFontScaling: false, textBreakStrategy: 'simple'}), [textSelectable]);
const defaultViewProps = {style: [styles.alignItemsStart, styles.userSelectText]};
return (
<TRenderEngineProvider
customHTMLElementModels={customHTMLElementModels}
baseStyle={styles.webViewStyles.baseFontStyle}
tagsStyles={styles.webViewStyles.tagStyles}
enableCSSInlineProcessing={false}
systemFonts={_.values(FontUtils.fontFamily.single)}
systemFonts={Object.values(FontUtils.fontFamily.single)}
domVisitors={{
// eslint-disable-next-line no-param-reassign
onText: (text) => (text.data = convertToLTR(text.data)),
Expand All @@ -91,18 +93,17 @@ function BaseHTMLEngineProvider(props) {
<RenderHTMLConfigProvider
defaultTextProps={defaultTextProps}
defaultViewProps={defaultViewProps}
// @ts-expect-error TODO: Remove this once HTMLRenderers (https://github.com/Expensify/App/issues/25154) is migrated to TypeScript.
renderers={htmlRenderers}
computeEmbeddedMaxWidth={HTMLEngineUtils.computeEmbeddedMaxWidth}
enableExperimentalBRCollapsing={props.enableExperimentalBRCollapsing}
enableExperimentalBRCollapsing={enableExperimentalBRCollapsing}
>
{props.children}
{children}
</RenderHTMLConfigProvider>
</TRenderEngineProvider>
);
}

BaseHTMLEngineProvider.displayName = 'BaseHTMLEngineProvider';
BaseHTMLEngineProvider.propTypes = propTypes;
BaseHTMLEngineProvider.defaultProps = defaultProps;

export default BaseHTMLEngineProvider;
15 changes: 0 additions & 15 deletions src/components/HTMLEngineProvider/htmlEnginePropTypes.js

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import lodashGet from 'lodash/get';
import type {TNode} from 'react-native-render-html';

type Predicate = (node: TNode) => boolean;

const MAX_IMG_DIMENSIONS = 512;

Expand All @@ -7,12 +9,12 @@ const MAX_IMG_DIMENSIONS = 512;
* 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
* @param contentWidth - The content width provided to the HTML
* component.
* @returns {number} The minimum between contentWidth and MAX_IMG_DIMENSIONS
* @param tagName - The name of the tag for which max width should be constrained.
* @returns The minimum between contentWidth and MAX_IMG_DIMENSIONS
*/
function computeEmbeddedMaxWidth(tagName, contentWidth) {
function computeEmbeddedMaxWidth(contentWidth: number, tagName: string): number {
Comment on lines -15 to +17

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is interesting were we doing something wrong the whole time? Isn't this param swapping a breaking change?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is something that was actually wrong the whole time, if you take a look at function computeEmbeddedMaxWidth from react-native-render-html the parameters order is the one that we have provided now.
I couldn't find any version where this parameters were swapped by the package maintainer.

if (tagName === 'img') {
return Math.min(MAX_IMG_DIMENSIONS, contentWidth);
}
Expand All @@ -22,21 +24,15 @@ function computeEmbeddedMaxWidth(tagName, contentWidth) {
/**
* Check if tagName is equal to any of our custom tags wrapping chat comments.
*
* @param {string} tagName
* @returns {Boolean}
*/
function isCommentTag(tagName) {
function isCommentTag(tagName: string): boolean {
return tagName === 'email-comment' || tagName === 'comment';
}

/**
* Check if there is an ancestor node for which the predicate returns true.
*
* @param {TNode} tnode
* @param {Function} predicate
* @returns {Boolean}
*/
function isChildOfNode(tnode, predicate) {
function isChildOfNode(tnode: TNode, predicate: Predicate): boolean {
let currentNode = tnode.parent;
while (currentNode) {
if (predicate(currentNode)) {
Expand All @@ -50,21 +46,17 @@ function isChildOfNode(tnode, predicate) {
/**
* 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 isChildOfComment(tnode) {
return isChildOfNode(tnode, (node) => isCommentTag(lodashGet(node, 'domNode.name', '')));
function isChildOfComment(tnode: TNode): boolean {
return isChildOfNode(tnode, (node) => node.domNode?.name !== undefined && isCommentTag(node.domNode.name));
}

/**
* Check if there is an ancestor node with the name 'h1'.
* Finding a node with the name 'h1' flags that we are rendering inside an h1 element.
* @param {TNode} tnode
* @returns {Boolean}
*/
function isChildOfH1(tnode) {
return isChildOfNode(tnode, (node) => lodashGet(node, 'domNode.name', '').toLowerCase() === 'h1');
function isChildOfH1(tnode: TNode): boolean {
return isChildOfNode(tnode, (node) => node.domNode?.name !== undefined && node.domNode.name.toLowerCase() === 'h1');
}

export {computeEmbeddedMaxWidth, isChildOfComment, isCommentTag, isChildOfH1};
22 changes: 0 additions & 22 deletions src/components/HTMLEngineProvider/index.js

This file was deleted.

20 changes: 0 additions & 20 deletions src/components/HTMLEngineProvider/index.native.js

This file was deleted.

11 changes: 11 additions & 0 deletions src/components/HTMLEngineProvider/index.native.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import React from 'react';
import type ChildrenProps from '@src/types/utils/ChildrenProps';
import BaseHTMLEngineProvider from './BaseHTMLEngineProvider';

function HTMLEngineProvider({children}: ChildrenProps) {
return <BaseHTMLEngineProvider enableExperimentalBRCollapsing>{children}</BaseHTMLEngineProvider>;
}

HTMLEngineProvider.displayName = 'HTMLEngineProvider';

export default HTMLEngineProvider;
15 changes: 15 additions & 0 deletions src/components/HTMLEngineProvider/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import React from 'react';
import useWindowDimensions from '@hooks/useWindowDimensions';
import * as DeviceCapabilities from '@libs/DeviceCapabilities';
import type ChildrenProps from '@src/types/utils/ChildrenProps';
import BaseHTMLEngineProvider from './BaseHTMLEngineProvider';

function HTMLEngineProvider({children}: ChildrenProps) {
const {isSmallScreenWidth} = useWindowDimensions();

return <BaseHTMLEngineProvider textSelectable={!DeviceCapabilities.canUseTouchScreen() || !isSmallScreenWidth}>{children}</BaseHTMLEngineProvider>;
}

HTMLEngineProvider.displayName = 'HTMLEngineProvider';

export default HTMLEngineProvider;