diff --git a/Libraries/LogBox/UI/LogBoxMessage.js b/Libraries/LogBox/UI/LogBoxMessage.js index 02ecf3d37e6fe6..772c6680103557 100644 --- a/Libraries/LogBox/UI/LogBoxMessage.js +++ b/Libraries/LogBox/UI/LogBoxMessage.js @@ -11,6 +11,8 @@ import type {TextStyleProp} from '../../StyleSheet/StyleSheet'; import type {Message} from '../Data/parseLogBoxLog'; +import Linking from '../../Linking/Linking'; +import StyleSheet from '../../StyleSheet/StyleSheet'; import Text from '../../Text/Text'; import * as React from 'react'; @@ -22,6 +24,81 @@ type Props = { ... }; +type Range = { + lowerBound: number, + upperBound: number, +}; + +function getLinkRanges(string: string): $ReadOnlyArray { + const regex = /https?:\/\/[^\s$.?#].[^\s]*/gi; + const matches = []; + + let regexResult: RegExp$matchResult | null; + while ((regexResult = regex.exec(string)) !== null) { + if (regexResult != null) { + matches.push({ + lowerBound: regexResult.index, + upperBound: regex.lastIndex, + }); + } + } + + return matches; +} + +function TappableLinks(props: { + content: string, + style: void | TextStyleProp, +}): React.Node { + const matches = getLinkRanges(props.content); + + if (matches.length === 0) { + // No URLs detected. Just return the content. + return {props.content}; + } + + // URLs were detected. Construct array of Text nodes. + + let fragments: Array = []; + let indexCounter = 0; + let startIndex = 0; + + for (const linkRange of matches) { + if (startIndex < linkRange.lowerBound) { + const text = props.content.substring(startIndex, linkRange.lowerBound); + fragments.push({text}); + } + + const link = props.content.substring( + linkRange.lowerBound, + linkRange.upperBound, + ); + fragments.push( + { + Linking.openURL(link); + }} + key={++indexCounter} + style={styles.linkText}> + {link} + , + ); + + startIndex = linkRange.upperBound; + } + + if (startIndex < props.content.length) { + const text = props.content.substring(startIndex); + fragments.push( + + {text} + , + ); + } + + return {fragments}; +} + const cleanContent = (content: string) => content.replace(/^(TransformError |Warning: (Warning: )?|Error: )/g, ''); @@ -49,9 +126,7 @@ function LogBoxMessage(props: Props): React.Node { if (length < maxLength) { elements.push( - - {cleanMessage} - , + , ); } @@ -87,4 +162,10 @@ function LogBoxMessage(props: Props): React.Node { return <>{elements}; } +const styles = StyleSheet.create({ + linkText: { + textDecorationLine: 'underline', + }, +}); + export default LogBoxMessage; diff --git a/Libraries/LogBox/UI/__tests__/LogBoxMessage-test.js b/Libraries/LogBox/UI/__tests__/LogBoxMessage-test.js index fba7dc725d9073..889593faa1afb5 100644 --- a/Libraries/LogBox/UI/__tests__/LogBoxMessage-test.js +++ b/Libraries/LogBox/UI/__tests__/LogBoxMessage-test.js @@ -17,7 +17,7 @@ const React = require('react'); describe('LogBoxMessage', () => { it('should render message', () => { - const output = render.shallowRender( + const output = render.create( { }); it('should render message truncated to 6 chars', () => { - const output = render.shallowRender( + const output = render.create( { it('should render the whole message when maxLength = message length', () => { const message = 'Some kind of message'; - const output = render.shallowRender( + const output = render.create( { }); it('should render message with substitution', () => { - const output = render.shallowRender( + const output = render.create( { }); it('should render message with substitution, truncating the first word 3 letters in', () => { - const output = render.shallowRender( + const output = render.create( { }); it('should render message with substitution, truncating the second word 6 letters in', () => { - const output = render.shallowRender( + const output = render.create( { }); it('should render message with substitution, truncating the third word 2 letters in', () => { - const output = render.shallowRender( + const output = render.create( { it('should render the whole message with substitutions when maxLength = message length', () => { const message = 'normal substitution normal'; - const output = render.shallowRender( + const output = render.create( { }); it('should render a plaintext message with no substitutions', () => { - const output = render.shallowRender( + const output = render.create( { }); it('should render a plaintext message and clean the content', () => { - const output = render.shallowRender( + const output = render.create( { }); it('Should strip "TransformError " without breaking substitution', () => { - const output = render.shallowRender( + const output = render.create( { }); it('Should strip "Warning: " without breaking substitution', () => { - const output = render.shallowRender( + const output = render.create( { }); it('Should strip "Warning: Warning: " without breaking substitution', () => { - const output = render.shallowRender( + const output = render.create( { expect(output).toMatchSnapshot(); }); + + it('Should make links tappable', () => { + const output = render.create( + , + ); + + expect(output).toMatchSnapshot(); + }); + + it('Should handle multiple links', () => { + const output = render.create( + , + ); + + expect(output).toMatchSnapshot(); + }); + + it('Should handle truncated links', () => { + const output = render.create( + , + ); + + expect(output).toMatchSnapshot(); + }); }); diff --git a/Libraries/LogBox/UI/__tests__/__snapshots__/LogBoxMessage-test.js.snap b/Libraries/LogBox/UI/__tests__/__snapshots__/LogBoxMessage-test.js.snap index 9f9260a0429072..ab09f2b290ab21 100644 --- a/Libraries/LogBox/UI/__tests__/__snapshots__/LogBoxMessage-test.js.snap +++ b/Libraries/LogBox/UI/__tests__/__snapshots__/LogBoxMessage-test.js.snap @@ -1,51 +1,112 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`LogBoxMessage Should handle multiple links 1`] = ` + + + http://reactnative.dev + + + and + + + http://reactjs.org + + +`; + +exports[`LogBoxMessage Should handle truncated links 1`] = ` + + + http://reactnative.dev + + + and http://r + + +`; + +exports[`LogBoxMessage Should make links tappable 1`] = ` + + + http://reactnative.dev + + +`; + exports[`LogBoxMessage Should strip "TransformError " without breaking substitution 1`] = ` - +Array [ normal - + , substitution - + , normal - - + , +] `; exports[`LogBoxMessage Should strip "Warning: " without breaking substitution 1`] = ` - +Array [ normal - + , substitution - + , normal - - + , +] `; exports[`LogBoxMessage Should strip "Warning: Warning: " without breaking substitution 1`] = ` - +Array [ normal - + , substitution - + , normal - - + , +] `; exports[`LogBoxMessage should render a plaintext message and clean the content 1`] = ` @@ -61,94 +122,86 @@ exports[`LogBoxMessage should render a plaintext message with no substitutions 1 `; exports[`LogBoxMessage should render message 1`] = ` - - - Some kind of message - - + + Some kind of message + `; exports[`LogBoxMessage should render message truncated to 6 chars 1`] = ` - - - Some - - + + Some + `; exports[`LogBoxMessage should render message with substitution 1`] = ` - +Array [ normal - + , substitution - + , normal - - + , +] `; exports[`LogBoxMessage should render message with substitution, truncating the first word 3 letters in 1`] = ` - - - nor - - + + nor + `; exports[`LogBoxMessage should render message with substitution, truncating the second word 6 letters in 1`] = ` - +Array [ normal - + , substi - - + , +] `; exports[`LogBoxMessage should render message with substitution, truncating the third word 2 letters in 1`] = ` - +Array [ normal - + , substitution - + , no - - + , +] `; exports[`LogBoxMessage should render the whole message when maxLength = message length 1`] = ` - - - Some kind of message - - + + Some kind of message + `; exports[`LogBoxMessage should render the whole message with substitutions when maxLength = message length 1`] = ` - +Array [ normal - + , substitution - + , normal - - + , +] `;