Skip to content

Commit

Permalink
Make links in LogBox tappable
Browse files Browse the repository at this point in the history
Summary:
changelog:
[General][Add] LogBox now makes URL links tappable.

Logbox messages may contain URLs with more information about the given error. Right now, they are not tappable or copyable. So engineers need to resort to manually retype the link to browser. This diff tries to address that.

Example of StrictMode error that has link in it.

{F798509909}

Reviewed By: yungsters

Differential Revision: D41305784

fbshipit-source-id: 456a9faf34f8b9e443759dd6903ba67d0b9de73c
  • Loading branch information
sammy-SC authored and facebook-github-bot committed Nov 21, 2022
1 parent 850f855 commit d9ade19
Show file tree
Hide file tree
Showing 3 changed files with 247 additions and 70 deletions.
87 changes: 84 additions & 3 deletions Libraries/LogBox/UI/LogBoxMessage.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -22,6 +24,81 @@ type Props = {
...
};

type Range = {
lowerBound: number,
upperBound: number,
};

function getLinkRanges(string: string): $ReadOnlyArray<Range> {
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 <Text style={props.style}>{props.content}</Text>;
}

// URLs were detected. Construct array of Text nodes.

let fragments: Array<React.Node> = [];
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 key={++indexCounter}>{text}</Text>);
}

const link = props.content.substring(
linkRange.lowerBound,
linkRange.upperBound,
);
fragments.push(
<Text
onPress={() => {
Linking.openURL(link);
}}
key={++indexCounter}
style={styles.linkText}>
{link}
</Text>,
);

startIndex = linkRange.upperBound;
}

if (startIndex < props.content.length) {
const text = props.content.substring(startIndex);
fragments.push(
<Text key={++indexCounter} style={props.style}>
{text}
</Text>,
);
}

return <Text style={props.style}>{fragments}</Text>;
}

const cleanContent = (content: string) =>
content.replace(/^(TransformError |Warning: (Warning: )?|Error: )/g, '');

Expand Down Expand Up @@ -49,9 +126,7 @@ function LogBoxMessage(props: Props): React.Node {

if (length < maxLength) {
elements.push(
<Text key={key} style={style}>
{cleanMessage}
</Text>,
<TappableLinks content={cleanMessage} key={key} style={style} />,
);
}

Expand Down Expand Up @@ -87,4 +162,10 @@ function LogBoxMessage(props: Props): React.Node {
return <>{elements}</>;
}

const styles = StyleSheet.create({
linkText: {
textDecorationLine: 'underline',
},
});

export default LogBoxMessage;
69 changes: 56 additions & 13 deletions Libraries/LogBox/UI/__tests__/LogBoxMessage-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const React = require('react');

describe('LogBoxMessage', () => {
it('should render message', () => {
const output = render.shallowRender(
const output = render.create(
<LogBoxMessage
style={{}}
message={{
Expand All @@ -31,7 +31,7 @@ describe('LogBoxMessage', () => {
});

it('should render message truncated to 6 chars', () => {
const output = render.shallowRender(
const output = render.create(
<LogBoxMessage
style={{}}
maxLength={5}
Expand All @@ -47,7 +47,7 @@ describe('LogBoxMessage', () => {

it('should render the whole message when maxLength = message length', () => {
const message = 'Some kind of message';
const output = render.shallowRender(
const output = render.create(
<LogBoxMessage
style={{}}
maxLength={message.length}
Expand All @@ -62,7 +62,7 @@ describe('LogBoxMessage', () => {
});

it('should render message with substitution', () => {
const output = render.shallowRender(
const output = render.create(
<LogBoxMessage
style={{}}
message={{
Expand All @@ -76,7 +76,7 @@ describe('LogBoxMessage', () => {
});

it('should render message with substitution, truncating the first word 3 letters in', () => {
const output = render.shallowRender(
const output = render.create(
<LogBoxMessage
style={{}}
maxLength={3}
Expand All @@ -91,7 +91,7 @@ describe('LogBoxMessage', () => {
});

it('should render message with substitution, truncating the second word 6 letters in', () => {
const output = render.shallowRender(
const output = render.create(
<LogBoxMessage
style={{}}
maxLength={13}
Expand All @@ -106,7 +106,7 @@ describe('LogBoxMessage', () => {
});

it('should render message with substitution, truncating the third word 2 letters in', () => {
const output = render.shallowRender(
const output = render.create(
<LogBoxMessage
style={{}}
maxLength={22}
Expand All @@ -122,7 +122,7 @@ describe('LogBoxMessage', () => {

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(
<LogBoxMessage
style={{}}
maxLength={message.length}
Expand All @@ -137,7 +137,7 @@ describe('LogBoxMessage', () => {
});

it('should render a plaintext message with no substitutions', () => {
const output = render.shallowRender(
const output = render.create(
<LogBoxMessage
plaintext
style={{}}
Expand All @@ -152,7 +152,7 @@ describe('LogBoxMessage', () => {
});

it('should render a plaintext message and clean the content', () => {
const output = render.shallowRender(
const output = render.create(
<LogBoxMessage
plaintext
style={{}}
Expand All @@ -167,7 +167,7 @@ describe('LogBoxMessage', () => {
});

it('Should strip "TransformError " without breaking substitution', () => {
const output = render.shallowRender(
const output = render.create(
<LogBoxMessage
style={{}}
message={{
Expand All @@ -181,7 +181,7 @@ describe('LogBoxMessage', () => {
});

it('Should strip "Warning: " without breaking substitution', () => {
const output = render.shallowRender(
const output = render.create(
<LogBoxMessage
style={{}}
message={{
Expand All @@ -195,7 +195,7 @@ describe('LogBoxMessage', () => {
});

it('Should strip "Warning: Warning: " without breaking substitution', () => {
const output = render.shallowRender(
const output = render.create(
<LogBoxMessage
style={{}}
message={{
Expand All @@ -207,4 +207,47 @@ describe('LogBoxMessage', () => {

expect(output).toMatchSnapshot();
});

it('Should make links tappable', () => {
const output = render.create(
<LogBoxMessage
style={{}}
message={{
content: 'http://reactnative.dev',
substitutions: [],
}}
/>,
);

expect(output).toMatchSnapshot();
});

it('Should handle multiple links', () => {
const output = render.create(
<LogBoxMessage
style={{}}
message={{
content: 'http://reactnative.dev and http://reactjs.org',
substitutions: [],
}}
/>,
);

expect(output).toMatchSnapshot();
});

it('Should handle truncated links', () => {
const output = render.create(
<LogBoxMessage
style={{}}
maxLength={35}
message={{
content: 'http://reactnative.dev and http://reactjs.org',
substitutions: [],
}}
/>,
);

expect(output).toMatchSnapshot();
});
});

0 comments on commit d9ade19

Please sign in to comment.