-
Notifications
You must be signed in to change notification settings - Fork 2.9k
/
BaseHTMLEngineProvider.js
executable file
·258 lines (233 loc) · 8.74 KB
/
BaseHTMLEngineProvider.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
/* eslint-disable react/prop-types */
import _ from 'underscore';
import React, {useMemo} from 'react';
import {TouchableOpacity} from 'react-native';
import {
TRenderEngineProvider,
RenderHTMLConfigProvider,
defaultHTMLElementModels,
TNodeChildrenRenderer,
splitBoxModelStyle,
} from 'react-native-render-html';
import PropTypes from 'prop-types';
import Config from '../../CONFIG';
import styles, {webViewStyles, getFontFamilyMonospace} from '../../styles/styles';
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';
const propTypes = {
/** Whether text elements should be selectable */
textSelectable: PropTypes.bool,
children: PropTypes.node,
};
const defaultProps = {
textSelectable: false,
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;
}
function AnchorRenderer({tnode, key, style}) {
const htmlAttribs = tnode.attributes;
// An auth token is needed to download Expensify chat attachments
const isAttachment = Boolean(htmlAttribs['data-expensify-source']);
return (
<AnchorForCommentsOnly
href={htmlAttribs.href}
isAuthTokenRequired={isAttachment}
// Unless otherwise specified open all links in
// a new window. On Desktop this means that we will
// skip the default Save As... download prompt
// and defer to whatever browser the user has.
// eslint-disable-next-line react/jsx-props-no-multi-spaces
target={htmlAttribs.target || '_blank'}
rel={htmlAttribs.rel || 'noopener noreferrer'}
style={style}
key={key}
>
<TNodeChildrenRenderer tnode={tnode} />
</AnchorForCommentsOnly>
);
}
function CodeRenderer({
key, style, TDefaultRenderer, ...defaultRendererProps
}) {
// We split wrapper and inner styles
// "boxModelStyle" corresponds to border, margin, padding and backgroundColor
const {boxModelStyle, otherStyle: textStyle} = splitBoxModelStyle(style);
// Get the correct fontFamily variant based in the fontStyle and fontWeight
const font = 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 <strong> tag has a fontWeight: "bold" and in the android it break the font
fontWeight: undefined,
fontStyle: undefined,
};
return (
<InlineCodeBlock
TDefaultRenderer={TDefaultRenderer}
boxModelStyle={boxModelStyle}
textStyle={{...textStyle, ...textStyleOverride}}
defaultRendererProps={defaultRendererProps}
key={key}
/>
);
}
function EditedRenderer(props) {
const defaultRendererProps = _.omit(props, ['TDefaultRenderer', 'style', 'tnode']);
return (
<Text
// eslint-disable-next-line react/jsx-props-no-spreading
{...defaultRendererProps}
fontSize={variables.fontSizeSmall}
color={themeColors.textSupporting}
>
{/* Native devices do not support margin between nested text */}
<Text style={styles.w1}>{' '}</Text>
(edited)
</Text>
);
}
function ImgRenderer({tnode}) {
const htmlAttribs = 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']);
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 (
<AttachmentModal
title="Attachment"
sourceURL={source}
isAuthTokenRequired={isAttachment}
>
{({show}) => (
<TouchableOpacity
style={styles.noOutline}
onPress={() => show()}
>
<ThumbnailImage
previewSourceURL={previewSource}
style={webViewStyles.tagStyles.img}
isAuthTokenRequired={isAttachment}
/>
</TouchableOpacity>
)}
</AttachmentModal>
);
}
// Declare nonstandard tags and their content model here
const customHTMLElementModels = {
edited: defaultHTMLElementModels.span.extend({
tagName: 'edited',
}),
};
// Define the custom renderer components
const renderers = {
a: AnchorRenderer,
code: CodeRenderer,
img: ImgRenderer,
edited: 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.
// 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.
const BaseHTMLEngineProvider = ({children, textSelectable}) => {
// We need to memoize this prop to make it referentially stable.
const defaultTextProps = useMemo(() => ({selectable: textSelectable}), [textSelectable]);
return (
<TRenderEngineProvider
customHTMLElementModels={customHTMLElementModels}
baseStyle={webViewStyles.baseFontStyle}
tagsStyles={webViewStyles.tagStyles}
enableCSSInlineProcessing={false}
dangerouslyDisableWhitespaceCollapsing={false}
systemFonts={EXTRA_FONTS}
>
<RenderHTMLConfigProvider
defaultTextProps={defaultTextProps}
defaultViewProps={defaultViewProps}
renderers={renderers}
renderersProps={renderersProps}
computeEmbeddedMaxWidth={computeEmbeddedMaxWidth}
>
{children}
</RenderHTMLConfigProvider>
</TRenderEngineProvider>
);
};
BaseHTMLEngineProvider.displayName = 'BaseHTMLEngineProvider';
BaseHTMLEngineProvider.propTypes = propTypes;
BaseHTMLEngineProvider.defaultProps = defaultProps;
export default BaseHTMLEngineProvider;