diff --git a/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.js b/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.js new file mode 100644 index 0000000000000..dec809e1266be --- /dev/null +++ b/src/components/AnchorForAttachmentsOnly/BaseAnchorForAttachmentsOnly.js @@ -0,0 +1,56 @@ +import React from 'react'; +import {Pressable} from 'react-native'; +import * as anchorForAttachmentsOnlyPropTypes from './anchorForAttachmentsOnlyPropTypes'; +import AttachmentView from '../AttachmentView'; +import fileDownload from '../../libs/fileDownload'; +import addEncryptedAuthTokenToURL from '../../libs/addEncryptedAuthTokenToURL'; + +class BaseAnchorForAttachmentsOnly extends React.Component { + constructor(props) { + super(props); + + this.state = { + isDownloading: false, + }; + this.processDownload = this.processDownload.bind(this); + } + + /** + * Initiate file downloading and update downloading flags + * + * @param {String} href + * @param {String} fileName + */ + processDownload(href, fileName) { + this.setState({isDownloading: true}); + fileDownload(href, fileName).then(() => this.setState({isDownloading: false})); + } + + render() { + const source = addEncryptedAuthTokenToURL(this.props.source); + + return ( + { + if (this.state.isDownloading) { + return; + } + this.processDownload(source, this.props.displayName); + }} + > + + + ); + } +} + +BaseAnchorForAttachmentsOnly.propTypes = anchorForAttachmentsOnlyPropTypes.propTypes; +BaseAnchorForAttachmentsOnly.defaultProps = anchorForAttachmentsOnlyPropTypes.defaultProps; + +export default BaseAnchorForAttachmentsOnly; diff --git a/src/components/AnchorForAttachmentsOnly/anchorForAttachmentsOnlyPropTypes.js b/src/components/AnchorForAttachmentsOnly/anchorForAttachmentsOnlyPropTypes.js new file mode 100644 index 0000000000000..a17f0a27ce4d6 --- /dev/null +++ b/src/components/AnchorForAttachmentsOnly/anchorForAttachmentsOnlyPropTypes.js @@ -0,0 +1,21 @@ +import PropTypes from 'prop-types'; +import stylePropTypes from '../../styles/stylePropTypes'; + +const propTypes = { + /** The URL of the attachment */ + source: PropTypes.string, + + /** Filename for attachments. */ + displayName: PropTypes.string, + + /** Any additional styles to apply */ + style: stylePropTypes, +}; + +const defaultProps = { + source: '', + style: {}, + displayName: '', +}; + +export {propTypes, defaultProps}; diff --git a/src/components/AnchorForAttachmentsOnly/index.js b/src/components/AnchorForAttachmentsOnly/index.js new file mode 100644 index 0000000000000..a71d65e969cd7 --- /dev/null +++ b/src/components/AnchorForAttachmentsOnly/index.js @@ -0,0 +1,12 @@ +import React from 'react'; +import * as anchorForAttachmentsOnlyPropTypes from './anchorForAttachmentsOnlyPropTypes'; +import BaseAnchorForAttachmentsOnly from './BaseAnchorForAttachmentsOnly'; + +// eslint-disable-next-line react/jsx-props-no-spreading +const AnchorForAttachmentsOnly = props => ; + +AnchorForAttachmentsOnly.propTypes = anchorForAttachmentsOnlyPropTypes.propTypes; +AnchorForAttachmentsOnly.defaultProps = anchorForAttachmentsOnlyPropTypes.defaultProps; +AnchorForAttachmentsOnly.displayName = 'AnchorForAttachmentsOnly'; + +export default AnchorForAttachmentsOnly; diff --git a/src/components/AnchorForAttachmentsOnly/index.native.js b/src/components/AnchorForAttachmentsOnly/index.native.js new file mode 100644 index 0000000000000..0a98ee0bb4ec8 --- /dev/null +++ b/src/components/AnchorForAttachmentsOnly/index.native.js @@ -0,0 +1,13 @@ +import React from 'react'; +import * as anchorForAttachmentsOnlyPropTypes from './anchorForAttachmentsOnlyPropTypes'; +import BaseAnchorForAttachmentsOnly from './BaseAnchorForAttachmentsOnly'; +import styles from '../../styles/styles'; + +// eslint-disable-next-line react/jsx-props-no-spreading +const AnchorForAttachmentsOnly = props => ; + +AnchorForAttachmentsOnly.propTypes = anchorForAttachmentsOnlyPropTypes.propTypes; +AnchorForAttachmentsOnly.defaultProps = anchorForAttachmentsOnlyPropTypes.defaultProps; +AnchorForAttachmentsOnly.displayName = 'AnchorForAttachmentsOnly'; + +export default AnchorForAttachmentsOnly; diff --git a/src/components/AnchorForCommentsOnly.js b/src/components/AnchorForCommentsOnly.js new file mode 100644 index 0000000000000..c93ecb416b644 --- /dev/null +++ b/src/components/AnchorForCommentsOnly.js @@ -0,0 +1,107 @@ +import _ from 'underscore'; +import React from 'react'; +import {StyleSheet} from 'react-native'; +import lodashGet from 'lodash/get'; +import Str from 'expensify-common/lib/str'; +import PropTypes from 'prop-types'; +import Text from './Text'; +import PressableWithSecondaryInteraction from './PressableWithSecondaryInteraction'; +import * as ReportActionContextMenu from '../pages/home/report/ContextMenu/ReportActionContextMenu'; +import * as ContextMenuActions from '../pages/home/report/ContextMenu/ContextMenuActions'; +import Tooltip from './Tooltip'; +import canUseTouchScreen from '../libs/canUseTouchscreen'; +import styles from '../styles/styles'; +import withWindowDimensions, {windowDimensionsPropTypes} from './withWindowDimensions'; + +const propTypes = { + /** The URL to open */ + href: PropTypes.string, + + /** What headers to send to the linked page (usually noopener and noreferrer) + This is unused in native, but is here for parity with web */ + rel: PropTypes.string, + + /** Used to determine where to open a link ("_blank" is passed for a new tab) + This is unused in native, but is here for parity with web */ + target: PropTypes.string, + + /** Any children to display */ + children: PropTypes.node, + + /** Anchor text of URLs or emails. */ + displayName: PropTypes.string, + + /** Any additional styles to apply */ + // eslint-disable-next-line react/forbid-prop-types + style: PropTypes.any, + + /** Press handler for the link, when not passed, default href is used to create a link like behaviour */ + onPress: PropTypes.func, + + ...windowDimensionsPropTypes, +}; + +const defaultProps = { + href: '', + rel: '', + target: '', + children: null, + style: {}, + displayName: '', + onPress: undefined, +}; + +/* + * This is a default anchor component for regular links. + */ +const BaseAnchorForCommentsOnly = (props) => { + let linkRef; + const rest = _.omit(props, _.keys(propTypes)); + const linkProps = {}; + if (_.isFunction(props.onPress)) { + linkProps.onPress = props.onPress; + } else { + linkProps.href = props.href; + } + const defaultTextStyle = canUseTouchScreen() || props.isSmallScreenWidth ? {} : styles.userSelectText; + + return ( + { + ReportActionContextMenu.showContextMenu( + Str.isValidEmail(props.displayName) ? ContextMenuActions.CONTEXT_MENU_TYPES.EMAIL : ContextMenuActions.CONTEXT_MENU_TYPES.LINK, + event, + props.href, + lodashGet(linkRef, 'current'), + ); + } + } + > + + linkRef = el} + style={StyleSheet.flatten([props.style, defaultTextStyle])} + accessibilityRole="link" + hrefAttrs={{ + rel: props.rel, + target: props.target, + }} + // eslint-disable-next-line react/jsx-props-no-spreading + {...linkProps} + // eslint-disable-next-line react/jsx-props-no-spreading + {...rest} + > + {props.children} + + + + ); +}; + +BaseAnchorForCommentsOnly.propTypes = propTypes; +BaseAnchorForCommentsOnly.defaultProps = defaultProps; +BaseAnchorForCommentsOnly.displayName = 'BaseAnchorForCommentsOnly'; + +export default withWindowDimensions(BaseAnchorForCommentsOnly); diff --git a/src/components/AnchorForCommentsOnly/BaseAnchorForCommentsOnly/index.js b/src/components/AnchorForCommentsOnly/BaseAnchorForCommentsOnly/index.js deleted file mode 100644 index dab9d2ea718f3..0000000000000 --- a/src/components/AnchorForCommentsOnly/BaseAnchorForCommentsOnly/index.js +++ /dev/null @@ -1,104 +0,0 @@ -import _ from 'underscore'; -import React from 'react'; -import {Pressable, StyleSheet} from 'react-native'; -import lodashGet from 'lodash/get'; -import Str from 'expensify-common/lib/str'; -import Text from '../../Text'; -import {propTypes, defaultProps} from '../anchorForCommentsOnlyPropTypes'; -import PressableWithSecondaryInteraction from '../../PressableWithSecondaryInteraction'; -import * as ReportActionContextMenu from '../../../pages/home/report/ContextMenu/ReportActionContextMenu'; -import * as ContextMenuActions from '../../../pages/home/report/ContextMenu/ContextMenuActions'; -import AttachmentView from '../../AttachmentView'; -import fileDownload from '../../../libs/fileDownload'; -import Tooltip from '../../Tooltip'; -import canUseTouchScreen from '../../../libs/canUseTouchscreen'; -import styles from '../../../styles/styles'; - -/* - * This is a default anchor component for regular links. - */ -class BaseAnchorForCommentsOnly extends React.Component { - constructor(props) { - super(props); - - this.state = { - isDownloading: false, - }; - this.processDownload = this.processDownload.bind(this); - } - - /** - * Initiate file downloading and update downloading flags - * - * @param {String} href - * @param {String} fileName - */ - processDownload(href, fileName) { - this.setState({isDownloading: true}); - fileDownload(href, fileName).then(() => this.setState({isDownloading: false})); - } - - render() { - let linkRef; - const rest = _.omit(this.props, _.keys(propTypes)); - const defaultTextStyle = canUseTouchScreen() || this.props.isSmallScreenWidth ? {} : styles.userSelectText; - - return ( - this.props.isAttachment - ? ( - { - if (this.state.isDownloading) { - return; - } - this.processDownload(this.props.href, this.props.displayName); - }} - > - - - ) - : ( - { - ReportActionContextMenu.showContextMenu( - Str.isValidEmail(this.props.displayName) ? ContextMenuActions.CONTEXT_MENU_TYPES.EMAIL : ContextMenuActions.CONTEXT_MENU_TYPES.LINK, - event, - this.props.href, - lodashGet(linkRef, 'current'), - ); - } - } - > - - linkRef = el} - style={StyleSheet.flatten([this.props.style, defaultTextStyle])} - accessibilityRole="link" - href={this.props.href} - hrefAttrs={{ - rel: this.props.rel, - target: this.props.target, - }} - // eslint-disable-next-line react/jsx-props-no-spreading - {...rest} - > - {this.props.children} - - - - ) - ); - } -} - -BaseAnchorForCommentsOnly.propTypes = propTypes; -BaseAnchorForCommentsOnly.defaultProps = defaultProps; -BaseAnchorForCommentsOnly.displayName = 'BaseAnchorForCommentsOnly'; - -export default BaseAnchorForCommentsOnly; diff --git a/src/components/AnchorForCommentsOnly/BaseAnchorForCommentsOnly/index.native.js b/src/components/AnchorForCommentsOnly/BaseAnchorForCommentsOnly/index.native.js deleted file mode 100644 index 19e1b28774c2a..0000000000000 --- a/src/components/AnchorForCommentsOnly/BaseAnchorForCommentsOnly/index.native.js +++ /dev/null @@ -1,95 +0,0 @@ -import _ from 'underscore'; -import React from 'react'; -import lodashGet from 'lodash/get'; -import {Linking, StyleSheet, Pressable} from 'react-native'; -import Str from 'expensify-common/lib/str'; -import {propTypes, defaultProps} from '../anchorForCommentsOnlyPropTypes'; -import fileDownload from '../../../libs/fileDownload'; -import Text from '../../Text'; -import PressableWithSecondaryInteraction from '../../PressableWithSecondaryInteraction'; -import * as ReportActionContextMenu from '../../../pages/home/report/ContextMenu/ReportActionContextMenu'; -import * as ContextMenuActions from '../../../pages/home/report/ContextMenu/ContextMenuActions'; -import AttachmentView from '../../AttachmentView'; -import styles from '../../../styles/styles'; - -/* - * This is a default anchor component for regular links. - */ -class BaseAnchorForCommentsOnly extends React.Component { - constructor(props) { - super(props); - - this.state = { - isDownloading: false, - }; - this.processDownload = this.processDownload.bind(this); - } - - /** - * Initiate file downloading and update downloading flags - * - * @param {String} href - * @param {String} fileName - */ - processDownload(href, fileName) { - this.setState({isDownloading: true}); - fileDownload(href, fileName).then(() => this.setState({isDownloading: false})); - } - - render() { - let linkRef; - const rest = _.omit(this.props, _.keys(propTypes)); - return ( - this.props.isAttachment - ? ( - { - if (this.state.isDownloading) { - return; - } - this.processDownload(this.props.href, this.props.displayName); - }} - > - - - ) - : ( - { - ReportActionContextMenu.showContextMenu( - Str.isValidEmail(this.props.displayName) ? ContextMenuActions.CONTEXT_MENU_TYPES.EMAIL : ContextMenuActions.CONTEXT_MENU_TYPES.LINK, - event, - this.props.href, - lodashGet(linkRef, 'current'), - ); - } - } - onPress={() => Linking.openURL(this.props.href)} - > - linkRef = el} - style={StyleSheet.flatten(this.props.style)} - // eslint-disable-next-line react/jsx-props-no-spreading - {...rest} - > - {this.props.children} - - - ) - ); - } -} - -BaseAnchorForCommentsOnly.propTypes = propTypes; -BaseAnchorForCommentsOnly.defaultProps = defaultProps; -BaseAnchorForCommentsOnly.displayName = 'BaseAnchorForCommentsOnly'; - -export default BaseAnchorForCommentsOnly; diff --git a/src/components/AnchorForCommentsOnly/anchorForCommentsOnlyPropTypes.js b/src/components/AnchorForCommentsOnly/anchorForCommentsOnlyPropTypes.js deleted file mode 100644 index fd8b2148a4af3..0000000000000 --- a/src/components/AnchorForCommentsOnly/anchorForCommentsOnlyPropTypes.js +++ /dev/null @@ -1,43 +0,0 @@ -import PropTypes from 'prop-types'; - -/** - * Text based component that is passed a URL to open onPress - */ -const propTypes = { - /** The URL to open */ - href: PropTypes.string, - - /** What headers to send to the linked page (usually noopener and noreferrer) - This is unused in native, but is here for parity with web */ - rel: PropTypes.string, - - /** Used to determine where to open a link ("_blank" is passed for a new tab) - This is unused in native, but is here for parity with web */ - target: PropTypes.string, - - /** Flag to differentiate attachments and hyperlink. Base on flag link will be treated as a file download or a regular hyperlink */ - isAttachment: PropTypes.bool, - - /** Any children to display */ - children: PropTypes.node, - - /** Filename in case of attachments, anchor text in case of URLs or emails. */ - displayName: PropTypes.string, - - /** Any additional styles to apply */ - // eslint-disable-next-line react/forbid-prop-types - style: PropTypes.any, - -}; - -const defaultProps = { - href: '', - rel: '', - target: '', - isAttachment: false, - children: null, - style: {}, - displayName: '', -}; - -export {propTypes, defaultProps}; diff --git a/src/components/AnchorForCommentsOnly/index.js b/src/components/AnchorForCommentsOnly/index.js deleted file mode 100644 index 55dc0b3154efb..0000000000000 --- a/src/components/AnchorForCommentsOnly/index.js +++ /dev/null @@ -1,40 +0,0 @@ -import _ from 'underscore'; -import React from 'react'; -import PropTypes from 'prop-types'; -import * as anchorForCommentsOnlyPropTypes from './anchorForCommentsOnlyPropTypes'; -import BaseAnchorForCommentsOnly from './BaseAnchorForCommentsOnly'; -import addEncryptedAuthTokenToURL from '../../libs/addEncryptedAuthTokenToURL'; - -const propTypes = { - /** Do we need an auth token to view this link or download the remote resource? */ - isAuthTokenRequired: PropTypes.bool, - - // eslint-disable-next-line react/forbid-foreign-prop-types - ...anchorForCommentsOnlyPropTypes.propTypes, -}; - -const defaultProps = { - isAuthTokenRequired: false, - ...anchorForCommentsOnlyPropTypes.defaultProps, -}; - -/* - * This component acts as a switch between AnchorWithAuthToken and default BaseAnchorForCommentsOnly. - * It is an optimization so that we can attach an auth token to a URL when one is required, - * without using Onyx.connect on links that don't need an authToken. - */ -const AnchorForCommentsOnly = (props) => { - const propsToPass = _.omit(props, 'isAuthTokenRequired'); - if (props.isAuthTokenRequired) { - propsToPass.href = addEncryptedAuthTokenToURL(props.href); - propsToPass.isAttachment = true; - } - // eslint-disable-next-line react/jsx-props-no-spreading - return ; -}; - -AnchorForCommentsOnly.propTypes = propTypes; -AnchorForCommentsOnly.defaultProps = defaultProps; -AnchorForCommentsOnly.displayName = 'AnchorForCommentsOnly'; - -export default AnchorForCommentsOnly; diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.js b/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.js index e93ba851d9964..9d395a8779b99 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.js +++ b/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.js @@ -13,6 +13,7 @@ import CONST from '../../../CONST'; import styles from '../../../styles/styles'; import Navigation from '../../../libs/Navigation/Navigation'; import AnchorForCommentsOnly from '../../AnchorForCommentsOnly'; +import AnchorForAttachmentsOnly from '../../AnchorForAttachmentsOnly'; const AnchorRenderer = (props) => { const htmlAttribs = props.tnode.attributes; @@ -28,31 +29,22 @@ const AnchorRenderer = (props) => { && !attrHref.startsWith(CONFIG.EXPENSIFY.CONCIERGE_URL) && attrHref.replace(CONFIG.EXPENSIFY.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 (internalNewExpensifyPath) { - return ( - Navigation.navigate(internalNewExpensifyPath)} - > - - - ); - } + const navigateToLink = () => { + // 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 (internalNewExpensifyPath) { + Navigation.navigate(internalNewExpensifyPath); + return; + } - // If we are handling an old dot Expensify link (excluding Concierge) we need to open it with openOldDotLink() so we can navigate to it with the user already logged in. - // As attachments also use expensify.com we don't want it working the same as links. - if (internalExpensifyPath && !isAttachment) { - return ( - Link.openOldDotLink(internalExpensifyPath)} - > - - - ); - } + // If we are handling an old dot Expensify link we need to open it with openOldDotLink() so we can navigate to it with the user already logged in. + // As attachments also use expensify.com we don't want it working the same as links. + if (internalExpensifyPath && !isAttachment) { + Link.openOldDotLink(internalExpensifyPath); + return; + } + Linking.openURL(attrHref); + }; 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. @@ -61,17 +53,25 @@ const AnchorRenderer = (props) => { return ( Linking.openURL(attrHref)} + onPress={navigateToLink} > ); } + if (isAttachment) { + return ( + + ); + } + return ( { style={{...props.style, ...parentStyle}} key={props.key} displayName={displayName} + + // Only pass the press handler for internal links, for public links fallback to default link handling + onPress={internalNewExpensifyPath || internalExpensifyPath ? navigateToLink : undefined} > diff --git a/src/stories/Banner.stories.js b/src/stories/Banner.stories.js index 247489d497112..180d130029dd9 100644 --- a/src/stories/Banner.stories.js +++ b/src/stories/Banner.stories.js @@ -27,8 +27,15 @@ HTMLBanner.args = { shouldRenderHTML: true, }; +const BannerWithLink = Template.bind({}); +BannerWithLink.args = { + text: 'This is a informational banner containing internal Link and public link', + shouldRenderHTML: true, +}; + export default story; export { InfoBanner, HTMLBanner, + BannerWithLink, };