From 7d4121da024c603c949215f026355cbd22c63960 Mon Sep 17 00:00:00 2001 From: Rick Hanlon Date: Mon, 21 Oct 2019 21:05:47 -0700 Subject: [PATCH] Add LogBox for warnings Summary: # Overview This diff adds the initial LogBox redesign implementing only Warnings for now. The following diffs include the tests for this, as well as a way to enable an experimental flag to opt-in. Changelog: [Internal] ## Changes To init LogBox, we've taken the core of YellowBox and rewritten it entirely. Differences from Yellowbox include: - Data model re-written - More performant - Allows a future listing of logs in order - Allows a future toggle to show ignored logs - Moves category into a property - Groups by the same sequential message (as chrome does) instead of by category - Does not store dupes of the same messages, only a count - UI revamp - Color and design refresh - Does not spam UI with logs - Only shows the most recent log - Dismiss all button is always in one place - Allows navigating through all of the warnings in the list, not just ones in the same category - Collapses message to 5 lines (tap to expand) - Collapses unrelated stack frames (tap to expand) - Moves React stack to it's own section - Formats React Stack like a stack frame - Collapses any React frames over 3 deep (tap to expand) - Adds a "Meta" information (to be expanded on later) - De-emphasizes the source map indicator - Better Engineering - Rewrote almost all components to hooks (will follow up with the rest) - Added more tests for Data files - Added testes for UI components (previously there were none) - Refactored some imperative render code to declarative ## Known Problems - The first major problem is that in the collapsed state (which is meant to model the FBLogger on Comet) does not show the user how many logs are in the console (only the count of the current log). - The way we're doing symbolication and navigation is slow. We will follow up with perf improvements - The React Stack logic is too simple and missed cases - We need to get properly scaled images for the close button ## What's next Next up we'll be: - Move over Moti's improvements to filtering and YellowBox changes since I started this - Adding in Errors, and not using the native redbox when LogBox is available - Adding in a list of all errors and a way to navigate to it - Adding in Logs, so users can see console.log in the app - Make React stack frames clickable - And many more Reviewed By: cpojer Differential Revision: D17965726 fbshipit-source-id: 2f28584ecb7e3ca8d3df034ea1e1a4a50e018c02 --- Libraries/LogBox/Data/LogBoxLog.js | 105 ++++++++ Libraries/LogBox/Data/LogBoxLogData.js | 173 +++++++++++++ Libraries/LogBox/Data/LogBoxLogParser.js | 168 +++++++++++++ Libraries/LogBox/Data/LogBoxSymbolication.js | 76 ++++++ Libraries/LogBox/LogBox.js | 171 +++++++++++++ Libraries/LogBox/UI/LogBoxButton.js | 71 ++++++ Libraries/LogBox/UI/LogBoxContainer.js | 113 +++++++++ Libraries/LogBox/UI/LogBoxImageSource.js | 65 +++++ Libraries/LogBox/UI/LogBoxInspector.js | 151 +++++++++++ Libraries/LogBox/UI/LogBoxInspectorFooter.js | 86 +++++++ Libraries/LogBox/UI/LogBoxInspectorHeader.js | 126 ++++++++++ .../LogBox/UI/LogBoxInspectorMessageHeader.js | 117 +++++++++ Libraries/LogBox/UI/LogBoxInspectorMeta.js | 80 ++++++ .../LogBox/UI/LogBoxInspectorReactFrames.js | 134 ++++++++++ .../UI/LogBoxInspectorSourceMapStatus.js | 154 ++++++++++++ .../LogBox/UI/LogBoxInspectorStackFrame.js | 94 +++++++ .../LogBox/UI/LogBoxInspectorStackFrames.js | 169 +++++++++++++ Libraries/LogBox/UI/LogBoxLogNotification.js | 234 ++++++++++++++++++ Libraries/LogBox/UI/LogBoxMessage.js | 59 +++++ Libraries/LogBox/UI/LogBoxStyle.js | 55 ++++ 20 files changed, 2401 insertions(+) create mode 100644 Libraries/LogBox/Data/LogBoxLog.js create mode 100644 Libraries/LogBox/Data/LogBoxLogData.js create mode 100644 Libraries/LogBox/Data/LogBoxLogParser.js create mode 100644 Libraries/LogBox/Data/LogBoxSymbolication.js create mode 100644 Libraries/LogBox/LogBox.js create mode 100644 Libraries/LogBox/UI/LogBoxButton.js create mode 100644 Libraries/LogBox/UI/LogBoxContainer.js create mode 100644 Libraries/LogBox/UI/LogBoxImageSource.js create mode 100644 Libraries/LogBox/UI/LogBoxInspector.js create mode 100644 Libraries/LogBox/UI/LogBoxInspectorFooter.js create mode 100644 Libraries/LogBox/UI/LogBoxInspectorHeader.js create mode 100644 Libraries/LogBox/UI/LogBoxInspectorMessageHeader.js create mode 100644 Libraries/LogBox/UI/LogBoxInspectorMeta.js create mode 100644 Libraries/LogBox/UI/LogBoxInspectorReactFrames.js create mode 100644 Libraries/LogBox/UI/LogBoxInspectorSourceMapStatus.js create mode 100644 Libraries/LogBox/UI/LogBoxInspectorStackFrame.js create mode 100644 Libraries/LogBox/UI/LogBoxInspectorStackFrames.js create mode 100644 Libraries/LogBox/UI/LogBoxLogNotification.js create mode 100644 Libraries/LogBox/UI/LogBoxMessage.js create mode 100644 Libraries/LogBox/UI/LogBoxStyle.js diff --git a/Libraries/LogBox/Data/LogBoxLog.js b/Libraries/LogBox/Data/LogBoxLog.js new file mode 100644 index 0000000000..3c05cea21b --- /dev/null +++ b/Libraries/LogBox/Data/LogBoxLog.js @@ -0,0 +1,105 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + * @format + */ + +'use strict'; + +import * as LogBoxSymbolication from './LogBoxSymbolication'; + +import type {Category, Message, ComponentStack} from './LogBoxLogParser'; +import type {Stack} from './LogBoxSymbolication'; + +export type SymbolicationRequest = $ReadOnly<{| + abort: () => void, +|}>; + +class LogBoxLog { + message: Message; + category: Category; + componentStack: ComponentStack; + stack: Stack; + count: number; + ignored: boolean; + symbolicated: + | $ReadOnly<{|error: null, stack: null, status: 'NONE'|}> + | $ReadOnly<{|error: null, stack: null, status: 'PENDING'|}> + | $ReadOnly<{|error: null, stack: Stack, status: 'COMPLETE'|}> + | $ReadOnly<{|error: Error, stack: null, status: 'FAILED'|}> = { + error: null, + stack: null, + status: 'NONE', + }; + + constructor( + message: Message, + stack: Stack, + category: string, + componentStack: ComponentStack, + ignored: boolean, + ) { + this.message = message; + this.stack = stack; + this.category = category; + this.componentStack = componentStack; + this.ignored = ignored; + this.count = 1; + } + + incrementCount(): void { + this.count += 1; + } + + getAvailableStack(): Stack { + return this.symbolicated.status === 'COMPLETE' + ? this.symbolicated.stack + : this.stack; + } + + retrySymbolicate(callback: () => void): SymbolicationRequest { + LogBoxSymbolication.deleteStack(this.stack); + return this.symbolicate(callback); + } + + symbolicate(callback: () => void): SymbolicationRequest { + let aborted = false; + + if (this.symbolicated.status !== 'COMPLETE') { + const updateStatus = (error: ?Error, stack: ?Stack): void => { + if (error != null) { + this.symbolicated = {error, stack: null, status: 'FAILED'}; + } else if (stack != null) { + this.symbolicated = {error: null, stack, status: 'COMPLETE'}; + } else { + this.symbolicated = {error: null, stack: null, status: 'PENDING'}; + } + if (!aborted) { + callback(); + } + }; + + updateStatus(null, null); + LogBoxSymbolication.symbolicate(this.stack).then( + stack => { + updateStatus(null, stack); + }, + error => { + updateStatus(error, null); + }, + ); + } + + return { + abort(): void { + aborted = true; + }, + }; + } +} + +export default LogBoxLog; diff --git a/Libraries/LogBox/Data/LogBoxLogData.js b/Libraries/LogBox/Data/LogBoxLogData.js new file mode 100644 index 0000000000..115314271b --- /dev/null +++ b/Libraries/LogBox/Data/LogBoxLogData.js @@ -0,0 +1,173 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + */ + +('use strict'); + +import LogBoxLog from './LogBoxLog'; +import LogBoxLogParser from './LogBoxLogParser'; + +export type LogBoxLogs = Array; +export type LogBoxLogsStore = Set; + +export type Observer = (logs: LogBoxLogs) => void; + +export type IgnorePattern = string | RegExp; + +export type Subscription = $ReadOnly<{| + unsubscribe: () => void, +|}>; + +const observers: Set<{observer: Observer}> = new Set(); +const ignorePatterns: Set = new Set(); +const logs: LogBoxLogsStore = new Set(); + +let _isDisabled = false; +let updateTimeout = null; + +function isMessageIgnored(message: string): boolean { + for (const pattern of ignorePatterns) { + if (pattern instanceof RegExp && pattern.test(message)) { + return true; + } else if (typeof pattern === 'string' && message.includes(pattern)) { + return true; + } + } + return false; +} + +function handleUpdate(): void { + if (updateTimeout == null) { + updateTimeout = setImmediate(() => { + updateTimeout = null; + const logsArray = _isDisabled ? [] : Array.from(logs); + for (const {observer} of observers) { + observer(logsArray); + } + }); + } +} + +export function add({ + args, +}: $ReadOnly<{| + args: $ReadOnlyArray, +|}>): void { + // This is carried over from the old YellowBox, but it is not clear why. + if (typeof args[0] === 'string' && args[0].startsWith('(ADVICE)')) { + return; + } + + const {category, message, stack, componentStack} = LogBoxLogParser({ + args, + }); + + // In most cases, the "last log" will be the "last log not ignored". + // This will result in out of order logs when we display ignored logs, + // but is a reasonable compromise. + const lastLog = Array.from(logs) + .filter(log => !log.ignored) + .pop(); + + // If the next log has the same category as the previous one + // then we want to roll it up into the last log in the list + // by incrementing the count (simar to how Chrome does it). + if (lastLog && lastLog.category === category) { + lastLog.incrementCount(); + } else { + logs.add( + new LogBoxLog( + message, + stack, + category, + componentStack, + isMessageIgnored(message.content), + ), + ); + } + + handleUpdate(); +} + +export function clear(): void { + if (logs.size > 0) { + logs.clear(); + handleUpdate(); + } +} + +export function dismiss(log: LogBoxLog): void { + if (logs.has(log)) { + logs.delete(log); + handleUpdate(); + } +} + +export function addIgnorePatterns( + patterns: $ReadOnlyArray, +): void { + // The same pattern may be added multiple times, but adding a new pattern + // can be expensive so let's find only the ones that are new. + const newPatterns = patterns.filter((pattern: IgnorePattern) => { + if (pattern instanceof RegExp) { + for (const existingPattern of ignorePatterns.entries()) { + if ( + existingPattern instanceof RegExp && + existingPattern.toString() === pattern.toString() + ) { + return false; + } + } + return true; + } + return !ignorePatterns.has(pattern); + }); + + if (newPatterns.length === 0) { + return; + } + for (const pattern of newPatterns) { + ignorePatterns.add(pattern); + + // We need to update all of the ignore flags in the existing logs. + // This allows adding an ignore pattern anywhere in the codebase. + // Without this, if you ignore a pattern after the a log is created, + // then we would always show the log. + for (let log of logs) { + log.ignored = isMessageIgnored(log.message.content); + } + } + handleUpdate(); +} + +export function setDisabled(value: boolean): void { + if (value === _isDisabled) { + return; + } + _isDisabled = value; + handleUpdate(); +} + +export function isDisabled(): boolean { + return _isDisabled; +} + +export function observe(observer: Observer): Subscription { + const subscription = {observer}; + observers.add(subscription); + + const logsToObserve = _isDisabled ? [] : logs; + observer(Array.from(logsToObserve)); + + return { + unsubscribe(): void { + observers.delete(subscription); + }, + }; +} diff --git a/Libraries/LogBox/Data/LogBoxLogParser.js b/Libraries/LogBox/Data/LogBoxLogParser.js new file mode 100644 index 0000000000..adaeaa187d --- /dev/null +++ b/Libraries/LogBox/Data/LogBoxLogParser.js @@ -0,0 +1,168 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + * @format + */ + +'use strict'; + +import UTFSequence from '../../UTFSequence'; +import stringifySafe from '../../Utilities/stringifySafe'; +import parseErrorStack from '../../Core/Devtools/parseErrorStack'; +import type {Stack} from './LogBoxSymbolication'; + +export type Category = string; +export type Message = $ReadOnly<{| + content: string, + substitutions: $ReadOnlyArray< + $ReadOnly<{| + length: number, + offset: number, + |}>, + >, +|}>; + +export type ComponentStack = $ReadOnlyArray< + $ReadOnly<{| + component: string, + location: string, + |}>, +>; + +const SUBSTITUTION = UTFSequence.BOM + '%s'; + +function parseCategory( + args: $ReadOnlyArray, +): $ReadOnly<{| + category: Category, + message: Message, +|}> { + const categoryParts = []; + const contentParts = []; + const substitutionOffsets = []; + + const remaining = [...args]; + if (typeof remaining[0] === 'string') { + const formatString = String(remaining.shift()); + const formatStringParts = formatString.split('%s'); + const substitutionCount = formatStringParts.length - 1; + const substitutions = remaining.splice(0, substitutionCount); + + let categoryString = ''; + let contentString = ''; + + let substitutionIndex = 0; + for (const formatStringPart of formatStringParts) { + categoryString += formatStringPart; + contentString += formatStringPart; + + if (substitutionIndex < substitutionCount) { + if (substitutionIndex < substitutions.length) { + // Don't stringify a string type. + // It adds quotation mark wrappers around the string, + // which causes the LogBox to look odd. + const substitution = + typeof substitutions[substitutionIndex] === 'string' + ? substitutions[substitutionIndex] + : stringifySafe(substitutions[substitutionIndex]); + substitutionOffsets.push({ + length: substitution.length, + offset: contentString.length, + }); + + categoryString += SUBSTITUTION; + contentString += substitution; + } else { + substitutionOffsets.push({ + length: 2, + offset: contentString.length, + }); + + categoryString += '%s'; + contentString += '%s'; + } + + substitutionIndex++; + } + } + + categoryParts.push(categoryString); + contentParts.push(contentString); + } + + const remainingArgs = remaining.map(arg => { + // Don't stringify a string type. + // It adds quotation mark wrappers around the string, + // which causes the LogBox to look odd. + return typeof arg === 'string' ? arg : stringifySafe(arg); + }); + categoryParts.push(...remainingArgs); + contentParts.push(...remainingArgs); + + return { + category: categoryParts.join(' '), + message: { + content: contentParts.join(' '), + substitutions: substitutionOffsets, + }, + }; +} + +function parseMessage({ + args, +}: $ReadOnly<{| + args: $ReadOnlyArray, +|}>): {| + componentStack: ComponentStack, + category: Category, + message: Message, + stack: Stack, +|} { + let mutableArgs: Array = [...args]; + + // This detects a very narrow case of a simple log string, + // with a component stack appended by React DevTools. + // In this case, we extract the component stack, + // because LogBox formats those pleasantly. + // If there are other substitutions or formatting, + // we bail to avoid potentially corrupting the data. + let componentStack = []; + if (mutableArgs.length === 2) { + const first = mutableArgs[0]; + const last = mutableArgs[1]; + if ( + typeof first === 'string' && + typeof last === 'string' && + /^\n {4}in/.exec(last) + ) { + componentStack = last + .split(/\n {4}in /g) + .map(s => { + if (!s) { + return null; + } + let [component, location] = s.split(/ \(at /); + if (!location) { + [component, location] = s.split(/ \(/); + } + return {component, location: location && location.replace(')', '')}; + }) + .filter(Boolean); + + mutableArgs = [first]; + } + } + + return { + ...parseCategory(mutableArgs), + componentStack, + // TODO: Use Error.captureStackTrace on Hermes + stack: parseErrorStack(new Error()), + }; +} + +export default parseMessage; diff --git a/Libraries/LogBox/Data/LogBoxSymbolication.js b/Libraries/LogBox/Data/LogBoxSymbolication.js new file mode 100644 index 0000000000..55d1b95739 --- /dev/null +++ b/Libraries/LogBox/Data/LogBoxSymbolication.js @@ -0,0 +1,76 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + */ + +'use strict'; + +import symbolicateStackTrace from '../../Core/Devtools/symbolicateStackTrace'; + +import type {StackFrame} from '../../Core/NativeExceptionsManager'; + +export type Stack = Array; + +const cache: Map> = new Map(); + +/** + * Sanitize because sometimes, `symbolicateStackTrace` gives us invalid values. + */ +const sanitize = (maybeStack: mixed): Stack => { + if (!Array.isArray(maybeStack)) { + throw new Error('Expected stack to be an array.'); + } + const stack = []; + for (const maybeFrame of maybeStack) { + if (typeof maybeFrame !== 'object' || maybeFrame == null) { + throw new Error('Expected each stack frame to be an object.'); + } + if (typeof maybeFrame.column !== 'number' && maybeFrame.column != null) { + throw new Error('Expected stack frame `column` to be a nullable number.'); + } + if (typeof maybeFrame.file !== 'string') { + throw new Error('Expected stack frame `file` to be a string.'); + } + if (typeof maybeFrame.lineNumber !== 'number') { + throw new Error('Expected stack frame `lineNumber` to be a number.'); + } + if (typeof maybeFrame.methodName !== 'string') { + throw new Error('Expected stack frame `methodName` to be a string.'); + } + + let collapse = false; + if ('collapse' in maybeFrame) { + if (typeof maybeFrame.collapse !== 'boolean') { + throw new Error('Expected stack frame `collapse` to be a boolean.'); + } + collapse = maybeFrame.collapse; + } + stack.push({ + column: maybeFrame.column, + file: maybeFrame.file, + lineNumber: maybeFrame.lineNumber, + methodName: maybeFrame.methodName, + collapse, + }); + } + return stack; +}; + +export function deleteStack(stack: Stack): void { + cache.delete(stack); +} + +export function symbolicate(stack: Stack): Promise { + let promise = cache.get(stack); + if (promise == null) { + promise = symbolicateStackTrace(stack).then(sanitize); + cache.set(stack, promise); + } + + return promise; +} diff --git a/Libraries/LogBox/LogBox.js b/Libraries/LogBox/LogBox.js new file mode 100644 index 0000000000..5bd283414a --- /dev/null +++ b/Libraries/LogBox/LogBox.js @@ -0,0 +1,171 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + * @format + */ + +'use strict'; + +import * as React from 'react'; +import Platform from '../Utilities/Platform'; +import RCTLog from '../Utilities/RCTLog'; +import LogBoxContainer from './UI/LogBoxContainer'; +import * as LogBoxLogData from './Data/LogBoxLogData'; + +import type { + LogBoxLogs, + Subscription, + IgnorePattern, +} from './Data/LogBoxLogData'; + +import LogBoxLog from './Data/LogBoxLog'; + +type Props = $ReadOnly<{||}>; +type State = {| + logs: ?LogBoxLogs, +|}; + +let LogBoxComponent; + +/** + * LogBox displays logs in the app. + */ +if (__DEV__) { + // LogBox needs to insert itself early, + // in order to access the component stacks appended by React DevTools. + const {error, warn} = console; + let errorImpl = error.bind(console); + let warnImpl = warn.bind(console); + + (console: any).error = function(...args) { + errorImpl(...args); + }; + (console: any).warn = function(...args) { + warnImpl(...args); + }; + + LogBoxComponent = class LogBox extends React.Component { + // TODO: deprecated, replace with ignoreLogs + static ignoreWarnings(patterns: $ReadOnlyArray): void { + LogBox.ignoreLogs(patterns); + } + + static ignoreLogs(patterns: $ReadOnlyArray): void { + LogBoxLogData.addIgnorePatterns(patterns); + } + + static install(): void { + errorImpl = function(...args) { + error.call(console, ...args); + // Show LogBox for the `warning` module. + if (typeof args[0] === 'string' && args[0].startsWith('Warning: ')) { + registerLog(...args); + } + }; + + warnImpl = function(...args) { + warn.call(console, ...args); + registerLog(...args); + }; + + if ((console: any).disableLogBox === true) { + LogBoxLogData.setDisabled(true); + } + (Object.defineProperty: any)(console, 'disableLogBox', { + configurable: true, + get: () => LogBoxLogData.isDisabled(), + set: value => LogBoxLogData.setDisabled(value), + }); + + if (Platform.isTesting) { + (console: any).disableLogBox = true; + } + + RCTLog.setWarningHandler((...args) => { + registerLog(...args); + }); + } + + static uninstall(): void { + errorImpl = error; + warnImpl = warn; + delete (console: any).disableLogBox; + } + + _subscription: ?Subscription; + + state = { + logs: null, + }; + + render(): React.Node { + // TODO: Ignore logs that fire when rendering `LogBox` itself. + return this.state.logs == null ? null : ( + + ); + } + + componentDidMount(): void { + this._subscription = LogBoxLogData.observe(logs => { + this.setState({logs}); + }); + } + + componentWillUnmount(): void { + if (this._subscription != null) { + this._subscription.unsubscribe(); + } + } + + _handleDismissAll(): void { + LogBoxLogData.clear(); + } + + _handleDismiss(log: LogBoxLog): void { + LogBoxLogData.dismiss(log); + } + }; + + const registerLog = (...args): void => { + LogBoxLogData.add({args}); + }; +} else { + LogBoxComponent = class extends React.Component { + // TODO: deprecated, replace with ignoreLogs + static ignoreWarnings(patterns: $ReadOnlyArray): void { + // Do nothing. + } + + static ignoreLogs(patterns: $ReadOnlyArray): void { + // Do nothing. + } + + static install(): void { + // Do nothing. + } + + static uninstall(): void { + // Do nothing. + } + + render(): React.Node { + return null; + } + }; +} + +module.exports = (LogBoxComponent: Class> & { + // TODO: deprecated, replace with ignoreLogs + ignoreWarnings($ReadOnlyArray): void, + ignoreLogs($ReadOnlyArray): void, + install(): void, + uninstall(): void, +}); diff --git a/Libraries/LogBox/UI/LogBoxButton.js b/Libraries/LogBox/UI/LogBoxButton.js new file mode 100644 index 0000000000..9fd149638f --- /dev/null +++ b/Libraries/LogBox/UI/LogBoxButton.js @@ -0,0 +1,71 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + */ + +'use strict'; + +import * as React from 'react'; +import StyleSheet from '../../StyleSheet/StyleSheet'; +import TouchableWithoutFeedback from '../../Components/Touchable/TouchableWithoutFeedback'; +import View from '../../Components/View/View'; +import * as LogBoxStyle from './LogBoxStyle'; +import type {EdgeInsetsProp} from '../../StyleSheet/EdgeInsetsPropType'; +import type {ViewStyleProp} from '../../StyleSheet/StyleSheet'; +import type {PressEvent} from '../../Types/CoreEventTypes'; + +type Props = $ReadOnly<{| + backgroundColor: $ReadOnly<{| + default: string, + pressed: string, + |}>, + children?: React.Node, + hitSlop?: ?EdgeInsetsProp, + onPress?: ?(event: PressEvent) => void, + style?: ViewStyleProp, +|}>; + +function LogBoxButton(props: Props): React.Node { + const [pressed, setPressed] = React.useState(false); + + let backgroundColor = props.backgroundColor; + if (!backgroundColor) { + backgroundColor = { + default: LogBoxStyle.getBackgroundColor(0.95), + pressed: LogBoxStyle.getBackgroundColor(0.6), + }; + } + + const content = ( + + {props.children} + + ); + + return props.onPress == null ? ( + content + ) : ( + setPressed(true)} + onPressOut={() => setPressed(false)}> + {content} + + ); +} + +export default LogBoxButton; diff --git a/Libraries/LogBox/UI/LogBoxContainer.js b/Libraries/LogBox/UI/LogBoxContainer.js new file mode 100644 index 0000000000..39f2134b86 --- /dev/null +++ b/Libraries/LogBox/UI/LogBoxContainer.js @@ -0,0 +1,113 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + */ + +'use strict'; + +import * as React from 'react'; +import SafeAreaView from '../../Components/SafeAreaView/SafeAreaView'; +import StyleSheet from '../../StyleSheet/StyleSheet'; +import View from '../../Components/View/View'; +import LogBoxInspector from './LogBoxInspector'; +import LogBoxLog from '../Data/LogBoxLog'; +import LogBoxLogNotification from './LogBoxLogNotification'; +import type {LogBoxLogs} from '../Data/LogBoxLogData'; + +type Props = $ReadOnly<{| + onDismiss: (log: LogBoxLog) => void, + onDismissAll: () => void, + logs: LogBoxLogs, +|}>; + +function LogBoxContainer(props: Props): React.Node { + const [selectedLogIndex, setSelectedLog] = React.useState(null); + + const filteredLogs = props.logs.filter(log => !log.ignored); + + function getVisibleLog() { + // TODO: currently returns the newest log but later will need to return + // the newest log of the highest level. For example, we want to show + // the latest error message even if there are newer warnings. + return filteredLogs[filteredLogs.length - 1]; + } + + function handleInspectorDismissAll() { + props.onDismissAll(); + } + + function handleInspectorDismiss() { + // Here we handle the cases when the log is dismissed and it + // was either the last log, or when the current index + // is now outside the bounds of the log array. + if (selectedLogIndex != null) { + if (props.logs.length - 1 <= 0) { + setSelectedLog(null); + } else if (selectedLogIndex >= props.logs.length - 1) { + setSelectedLog(selectedLogIndex - 1); + } + props.onDismiss(props.logs[selectedLogIndex]); + } + } + + function handleInspectorMinimize() { + setSelectedLog(null); + } + + function handleRowPress(index: number) { + setSelectedLog(filteredLogs.length - 1); + } + + if (selectedLogIndex != null) { + return ( + + + + ); + } + + return filteredLogs.length === 0 ? null : ( + + + { + /* TODO: open log list */ + }} + onPressDismiss={handleInspectorDismissAll} + /> + + + + ); +} + +const styles = StyleSheet.create({ + list: { + bottom: 10, + left: 10, + right: 10, + position: 'absolute', + }, + toast: { + borderRadius: 8, + overflow: 'hidden', + }, + safeArea: { + flex: 1, + }, +}); + +export default LogBoxContainer; diff --git a/Libraries/LogBox/UI/LogBoxImageSource.js b/Libraries/LogBox/UI/LogBoxImageSource.js new file mode 100644 index 0000000000..abf122d234 --- /dev/null +++ b/Libraries/LogBox/UI/LogBoxImageSource.js @@ -0,0 +1,65 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + */ + +'use strict'; + +import PixelRatio from '../../Utilities/PixelRatio'; + +const scale = PixelRatio.get(); + +/** + * We use inline images for LogBox in order to avoid display latency due to + * resource contention with symbolicating stack traces. + * + * The following steps were used to create these: + * + * 1. Download SVG files from: https://feathericons.com + * 2. Rasterize SVG files to PNG files at 16dp, 36dp, and 48dp. + * 3. Convert to Base64: https://www.google.com/search?q=base64+image+encoder + * + * @see https://github.com/feathericons/feather + * @copyright 2013-2017 Cole Bemis + * @license MIT + */ +const LogBoxImageSource = { + alertTriangle: ((scale > 2 + ? 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAB60lEQVRoge2Z3W3DIBSFj9oFPAIjZARGyAiMkBHuJh4hI2QEj5AR3Me+tQ91JALHmD8bKvmTkCr5Auc6/kzUACcnRXzuvL4GoAB8Afjeea9qXADcAfw4475c65orgBl++NeYl5ouUQiHt5tQTRJuwB6b5zLY49QVGn7I0bo+kuv60IQbuHf5CWCIqOkCgX93maia1MkRAUMo+OI+AvUPp7a50EzcUCBF6psJrUkYiZgnZJ7eId8mMeIyhpW5hyLw72LKCXsl86VqwgAKceKapW5e/nZpJnSsuHaTM7muyDq7C63JprJS69YxhNTpSlkpKeLGNHCo0EJChcSNaQA4SGiFtBMXJFSI3YVOPXFB6kMoUl9NaE0Wl4h5KQ0AOwqde+KmNrCL0EKCxJ64qQ0AlYVWSBfXZusgW6Oa0Dni2hiEv0qsoci+yUJrsoikLlKAkP11ygK54taiSOgb/O5b/DMqS+gBZeLWJlnoEX7XwQkBDPIktlEkz7hWrEmxZG4M5L9GXYTk0qxwcopKxa3VABN6cosM/C5LxTUof4ReMKHf1nRlaSnuGsGM7kfU4w8RF5Bz4aNlokLe/HQ/ngl9/Qih4L9k3h4hA1+S3odxu3Q77Hl4r1Hg75n6D01M2Difbp02Mi3ZTk5OLH4BUyEtOlDYuK0AAAAASUVORK5CYII=' + : scale > 1 + ? 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAABVklEQVRYheWX4U3DMBBGH4gBMoJHyAgeoSNkAxjBG5QNOkJHCGzQDcoGZQP4gY3Oqe1cEscS4pNOqs9Jvqvv6ZrCf9fDhnutD4A3H810Br4mcW5l7hLmIdze5mZi+OJD5syeBYzC6CjyR5Ef9zI/CJMb0Im9zufC/qG2eQdchcGQuGYQ+9dJgZvl0B2xbJGrZW6IIevFXp9YVwcyB540syJfFcgSeJb0cVcDcg68XAFQCUhH+ShLBcBGIA158LQFqIB8zBRwEp9fgctcxQld/L2pZxZVAk/KkucjaDGQmoknrz35KEE2sABIRxm8tVIBaZgHb61UQOYmXk7aFgQVJ6QWPCnLAriYAVILnpTxD7yh/9EZiIEE4m+y29uMkGy1nQ6i9wYFRB5PwKdYP/v1msmnUe89gn695bG0iqjdXeMiRu9599csvGKZ0jlu0Ac/7d2rxX9Q37HW6QfX/ZguAAAAAElFTkSuQmCC' + : 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAvUlEQVQ4jbWT4Q3CIBCFP40DdANxg24gIzhKuwEjuIFxAkcwTtARGicoG+iPXlMCB8UfvoQc4e7ePV4A/ogWuMlqc0W7AsEo0QMNcPplugMmwMia5KwKWkNIuIkHq3wLXGQ/Sq4IC3wkLpOfmZyKeEpIEKsDYB8VN0Afkfpg30uNiycbdKcNqXEOxdBEWoEAoqta8uZ0iqqkxwGDUrSFAXAHZpOWd/+ubD5Kz335Cx1wZna4Bh54AddauVl8ARfCLO9Xq7xGAAAAAElFTkSuQmCC'): string), + check: ((scale > 2 + ? 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAAqElEQVRoge3YTQ7CIBRF4bPUu/8JS6gTSaqilh95vuR+CaO2cGgYNAUzMzOzFgHlPhRaMkDAcRoltKaTeIxPtQHxGn+Q5AgJx8cQjo8hHB9DOP76Yiu/RcTmN18WLiQCjs3zBkYXVGOeLWd+xcIr5pgyEzDz7FIjISPP/FRPUM+9W4nvYVfuCSXeB3669ldEOzRFfCUSx1cicXwlEsdXIvEPKDMzM7PMbtugw3XTpNA2AAAAAElFTkSuQmCC' + : scale > 1 + ? 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAf0lEQVRYhe2UvQ2AIBQGL3EBR3AESkv3bxxFN8DmWUgwvkI+En1X0cBd+IMg+DuDyDMCs413kfMiX4EMbD3l8oCaPIU85B4mYLEF5XJscrYFPRGvb/sZ4IlocubJGdH0wj1FSG77XYT0qdUi5O+8jOjyyZQRUnkZ0UUeBMF3OQC/0VsyGlxligAAAABJRU5ErkJggg==' + : 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAASElEQVQ4jWNgGJHAgIGBIYESze8ZGBjWU6L5PAMDgwBNNCdAFZJt83qoQmRDSHK2AFQhzBCy/IxsCNkBJsDAwLAfiknWPBIBAETPFeuA4fr6AAAAAElFTkSuQmCC'): string), + chevronLeft: ((scale > 2 + ? 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAAgElEQVRoge3YMQ6DQBAEwRYvnf8nPAECbAnkyATsrt0lXUyPdAE6kCRJ/yXA+jopLbkhwHY6a2nNl8I1ftSA8Bm/MeQKBeNrBONrBONrBONrhMHxcPwOlMUvT32oszD8CoEj+giO6CE4oofgiB7Cj44Y86zyFoYPgOFPi5Ik6WwHji+QVIOyhqgAAAAASUVORK5CYII=' + : scale > 1 + ? 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACQAAAAkCAYAAADhAJiYAAAAaUlEQVRYhe3WsQ2AMAwAwRcs5LEYg3HpYANoQKKgcEEUI/6adM5LbgySfmZsNDeACdiApdEfaQGswH6+Xd1jugc9xYQxxhjz9RhaxwxvDuul3MrAqDyjsozKKnWgXUqdsJcAZgqsTFJ5B7gjUNw0n0HHAAAAAElFTkSuQmCC' + : 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAARklEQVQ4jWNgGPKAmUh1AQwMDBIMDAwPyLEkgYGB4T/UELI1J9BdcwCxmpnIMZ1YkECsK+hmCNZoZCHCgAUMDAwfoHg4AgDJuQ/bcLyV+QAAAABJRU5ErkJggg=='): string), + chevronRight: ((scale > 2 + ? 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAAeElEQVRoge3YMQ6AIBQE0Ykn3fs3HEEbC6MdFp+v8xJaspNQAZIkqbcA4zwpXTJpAPvlpHTNhHtAu4jwDDCiQjBiDcGINQQj1hCMWEN4Boy3l25vL/iL0PgJBcfXCI6vERxfIzi+Rmg8Hj7wrdL+Yys0/1qUJEmzDvSAkFQ8EOdJAAAAAElFTkSuQmCC' + : scale > 1 + ? 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACQAAAAkCAYAAADhAJiYAAAAZElEQVRYhe3WsQmAQAxA0Y8ulLEcw3HtdANtBNvzCJjD/5pUgQ9pApJ+Zu7YCWABDmDLzemzA+c94+MW4AkqExUY1caoVka1GibqlSm7qJJSJzPGGGMylYqBgi9sACtFYiQN7wKC6VDcJ7tlpQAAAABJRU5ErkJggg==' + : 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAANUlEQVQ4jWNgGLbAgYGBIYASAwIYGBj+MzAwJFBiSMLQMISJEpMptp2mmimORgcGChPSEAIAHGENPH8gqdYAAAAASUVORK5CYII='): string), + // TOOO: properly scale + close: ((scale > 2 + ? 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAAXNSR0IArs4c6QAAAPJJREFUSA3FllEOAiEMRIm3888/7x+vodEOWsPWLk4rIElDgOk8YBdCKaUcJS6vOEk9usBT/cGqjbvUiKvEWWJUgRc81R/gDXAk1MLgXYFYcjuLEVAPBsb7k+0J0B8ttBct7Mwg7BFOaODp3ExiJqeZ6/NosD/SzzAlM0aMRv2oumfYG6PM90Se8U3ECL1BRpzdDd+DToMpGVC7KkDRhzGqHCjVH0RLt9SDTftpPBguBPT3xlIfgTFkNBQ8YhTRuvCMQSanwtOJkh3ODSc4+0N70EIHYru+ei1/ROHpNuMi9lb68S7VQ223Kdu20Apc+tR/AAbAEIZxSf3+AAAAAElFTkSuQmCC' + : scale > 1 + ? 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAAXNSR0IArs4c6QAAAPJJREFUSA3FllEOAiEMRIm3888/7x+vodEOWsPWLk4rIElDgOk8YBdCKaUcJS6vOEk9usBT/cGqjbvUiKvEWWJUgRc81R/gDXAk1MLgXYFYcjuLEVAPBsb7k+0J0B8ttBct7Mwg7BFOaODp3ExiJqeZ6/NosD/SzzAlM0aMRv2oumfYG6PM90Se8U3ECL1BRpzdDd+DToMpGVC7KkDRhzGqHCjVH0RLt9SDTftpPBguBPT3xlIfgTFkNBQ8YhTRuvCMQSanwtOJkh3ODSc4+0N70EIHYru+ei1/ROHpNuMi9lb68S7VQ223Kdu20Apc+tR/AAbAEIZxSf3+AAAAAElFTkSuQmCC' + : 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAYAAAByDd+UAAAAAXNSR0IArs4c6QAAAPJJREFUSA3FllEOAiEMRIm3888/7x+vodEOWsPWLk4rIElDgOk8YBdCKaUcJS6vOEk9usBT/cGqjbvUiKvEWWJUgRc81R/gDXAk1MLgXYFYcjuLEVAPBsb7k+0J0B8ttBct7Mwg7BFOaODp3ExiJqeZ6/NosD/SzzAlM0aMRv2oumfYG6PM90Se8U3ECL1BRpzdDd+DToMpGVC7KkDRhzGqHCjVH0RLt9SDTftpPBguBPT3xlIfgTFkNBQ8YhTRuvCMQSanwtOJkh3ODSc4+0N70EIHYru+ei1/ROHpNuMi9lb68S7VQ223Kdu20Apc+tR/AAbAEIZxSf3+AAAAAElFTkSuQmCC'): string), + loader: ((scale > 2 + ? 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAABXElEQVRoge2a3W3DMAyEr+0CHkGjaISOcKN4k6zQETpCR+gGzgbpQ10kcamIpKQ6avQBBPxg3pHwL2UDg/8LASxrcNdKnCwATmssrUyeWgnju/DmXs8tRP+Sh2kgAJga1rFlWj2rcMD5YqQh77QJLbzIORjyRIJQCJW5ngYo5AVlrsgkCGqbsDbAhFfxqZsSZibP0oDXQ43HQPsg82i7sBoR+VcJq2YxKcPo0IoJLRZXmYGC6ezQmQUdVqhPBVH/CNBTSMkLVlzjA8Bbocb7GoPBoADi+umZilYzbrG/JrnljOvy734iu4To/BQaDB6Rl4LciPPF9Lmjhgvi+s7w6tCIGw3WKS0P8fvWNjt0ZkGHFeq7CQXTbkZKGg2JOxrqPUZ3s6ziNdju38IjS/dLi0EQpDLX2gDQYHEX6Hx5/YcA+6H0NgAYPnCMj3x7Mxq4wTGx3Q1E578aDDR8AX0mOGD6BEN/AAAAAElFTkSuQmCC' + : scale > 1 + ? 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAABN0lEQVRYhe2WzU3EMBCFP34KyJEjJaQDXAIlJJ24BSow2wEdhHSwJSwd7JHbcmC0mOxMnDiWDIInWbHkN29exo4n8IvRAEFGU8OAA04yulyR60Jm7msbyIZloAMGwBfI4UWrWxM08LW/weC4iOMNTog4g0awKjBG827GxBwC3996NHizAifsSrTRmlsZm23CT9adktyXSq6ZUPdxgiXnZzW8CLcLuC3lvqA/gCt5NtjlPQL7TP0Wu1HtRRu4PO3T4TKTz2kG+AG9IN6CR/Su9iojBw69egfghWgL/pGCp+JFVPUqTjWjlsuqeAo1o6rt2C8QcNiV0UxoHPMieojmz0CfMKyhl1hN84xbI3gnz5Ftp7kH3iT5LsFdDUf6pzSJ6r2glIFDbuDNhqRH4I7Pvv4EvG/QqocP2Jh/xzzX/zUAAAAASUVORK5CYII=' + : 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAsklEQVQ4jaWTYRHCMAyFP7gJmIQ6oChgEpBQKXMwC3MADpAAEiphDuBHC4QuDRu8u9ylyWtem7Rgw2X7GT1wsghb4beAVzhtsfYyJgs44AoEQzBkjrMId1HkKPwyZ6oMSnxYsnk1NqT7yMo34Fzhd9meGJvs7Hh3NhqCLXDI/rT0lKsR+KOJgc9RdaRRarkZvELogYsi8HqxjUhGYE+aQg1jzketwFTZXHbbEpjB8eU7PwAbLiJz46707gAAAABJRU5ErkJggg=='): string), +}; + +export default LogBoxImageSource; diff --git a/Libraries/LogBox/UI/LogBoxInspector.js b/Libraries/LogBox/UI/LogBoxInspector.js new file mode 100644 index 0000000000..6ab5e33db8 --- /dev/null +++ b/Libraries/LogBox/UI/LogBoxInspector.js @@ -0,0 +1,151 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + */ + +'use strict'; + +import Platform from '../../Utilities/Platform'; +import * as React from 'react'; +import ScrollView from '../../Components/ScrollView/ScrollView'; +import StyleSheet from '../../StyleSheet/StyleSheet'; +import View from '../../Components/View/View'; +import LogBoxInspectorFooter from './LogBoxInspectorFooter'; +import LogBoxInspectorMessageHeader from './LogBoxInspectorMessageHeader'; +import LogBoxInspectorReactFrames from './LogBoxInspectorReactFrames'; +import LogBoxInspectorStackFrames from './LogBoxInspectorStackFrames'; +import LogBoxInspectorMeta from './LogBoxInspectorMeta'; +import LogBoxInspectorHeader from './LogBoxInspectorHeader'; +import * as LogBoxStyle from './LogBoxStyle'; + +import type LogBoxLog from '../Data/LogBoxLog'; +import type {SymbolicationRequest} from '../Data/LogBoxLog'; + +type Props = $ReadOnly<{| + onDismiss: () => void, + onChangeSelectedIndex: (index: number) => void, + onMinimize: () => void, + logs: $ReadOnlyArray, + selectedIndex: number, +|}>; + +class LogBoxInspector extends React.Component { + _symbolication: ?SymbolicationRequest; + + _handleDismiss = () => { + this.props.onDismiss(); + }; + + render(): React.Node { + const {logs, selectedIndex} = this.props; + + const log = logs[selectedIndex]; + if (log == null) { + return null; + } + + return ( + + + + + + ); + } + + componentDidMount(): void { + this._handleSymbolication(); + } + + componentDidUpdate(prevProps: Props): void { + if ( + prevProps.logs[prevProps.selectedIndex] !== + this.props.logs[this.props.selectedIndex] + ) { + this._handleSymbolication(); + } + } + + _handleRetrySymbolication = () => { + this.forceUpdate(() => { + const log = this.props.logs[this.props.selectedIndex]; + this._symbolication = log.retrySymbolicate(() => { + this.forceUpdate(); + }); + }); + }; + + _handleSymbolication(): void { + const log = this.props.logs[this.props.selectedIndex]; + if (log.symbolicated.status !== 'COMPLETE') { + this._symbolication = log.symbolicate(() => { + this.forceUpdate(); + }); + } + } + + _handleSelectIndex = (selectedIndex: number): void => { + this.props.onChangeSelectedIndex(selectedIndex); + }; +} + +function LogBoxInspectorBody(props) { + const [collapsed, setCollapsed] = React.useState(true); + if (collapsed) { + return ( + <> + setCollapsed(!collapsed)} + message={props.log.message} + /> + + + + + + + ); + } + return ( + + setCollapsed(!collapsed)} + message={props.log.message} + /> + + + + + ); +} + +const styles = StyleSheet.create({ + root: { + backgroundColor: LogBoxStyle.getTextColor(1), + elevation: Platform.OS === 'android' ? Number.MAX_SAFE_INTEGER : undefined, + height: '100%', + }, + scrollBody: { + backgroundColor: LogBoxStyle.getBackgroundColor(0.9), + flex: 1, + }, +}); + +export default LogBoxInspector; diff --git a/Libraries/LogBox/UI/LogBoxInspectorFooter.js b/Libraries/LogBox/UI/LogBoxInspectorFooter.js new file mode 100644 index 0000000000..013e3a3d64 --- /dev/null +++ b/Libraries/LogBox/UI/LogBoxInspectorFooter.js @@ -0,0 +1,86 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + */ + +'use strict'; + +import * as React from 'react'; +import SafeAreaView from '../../Components/SafeAreaView/SafeAreaView'; +import StyleSheet from '../../StyleSheet/StyleSheet'; +import Text from '../../Text/Text'; +import View from '../../Components/View/View'; +import LogBoxButton from './LogBoxButton'; +import * as LogBoxStyle from './LogBoxStyle'; + +type Props = $ReadOnly<{| + onDismiss: () => void, + onMinimize: () => void, +|}>; + +function LogBoxInspectorFooter(props: Props): React.Node { + return ( + + + + + ); +} + +type ButtonProps = $ReadOnly<{| + onPress: () => void, + text: string, +|}>; + +function FooterButton(props: ButtonProps): React.Node { + return ( + + + {props.text} + + + + ); +} + +const buttonStyles = StyleSheet.create({ + button: { + flex: 1, + }, + content: { + alignItems: 'center', + height: 48, + justifyContent: 'center', + }, + label: { + color: LogBoxStyle.getTextColor(1), + fontSize: 14, + includeFontPadding: false, + lineHeight: 18, + }, +}); + +const styles = StyleSheet.create({ + root: { + backgroundColor: LogBoxStyle.getBackgroundColor(1), + shadowColor: '#000', + shadowOffset: {width: 0, height: -2}, + shadowRadius: 2, + shadowOpacity: 0.5, + elevation: 1, + flexDirection: 'row', + }, +}); + +export default LogBoxInspectorFooter; diff --git a/Libraries/LogBox/UI/LogBoxInspectorHeader.js b/Libraries/LogBox/UI/LogBoxInspectorHeader.js new file mode 100644 index 0000000000..ab22e0c716 --- /dev/null +++ b/Libraries/LogBox/UI/LogBoxInspectorHeader.js @@ -0,0 +1,126 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + */ + +'use strict'; + +import Image from '../../Image/Image'; +import Platform from '../../Utilities/Platform'; +import * as React from 'react'; +import SafeAreaView from '../../Components/SafeAreaView/SafeAreaView'; +import StyleSheet from '../../StyleSheet/StyleSheet'; +import Text from '../../Text/Text'; +import View from '../../Components/View/View'; +import LogBoxImageSource from './LogBoxImageSource'; +import LogBoxButton from './LogBoxButton'; +import * as LogBoxStyle from './LogBoxStyle'; + +type Props = $ReadOnly<{| + onSelectIndex: (selectedIndex: number) => void, + selectedIndex: number, + total: number, +|}>; + +function LogBoxInspectorHeader(props: Props): React.Node { + const prevIndex = props.selectedIndex - 1; + const nextIndex = props.selectedIndex + 1; + + const titleText = + props.total === 1 + ? 'Log' + : `Log ${props.selectedIndex + 1} of ${props.total}`; + + return ( + + + props.onSelectIndex(prevIndex)} + /> + + {titleText} + + = props.total} + image={LogBoxImageSource.chevronRight} + onPress={() => props.onSelectIndex(nextIndex)} + /> + + + ); +} + +function LogBoxInspectorHeaderButton( + props: $ReadOnly<{| + disabled: boolean, + image: string, + onPress?: ?() => void, + |}>, +): React.Node { + return ( + + {props.disabled ? null : ( + + )} + + ); +} + +const headerStyles = StyleSheet.create({ + button: { + alignItems: 'center', + aspectRatio: 1, + justifyContent: 'center', + marginTop: 3, + marginRight: 6, + marginLeft: 6, + marginBottom: -8, + borderRadius: 3, + }, + buttonImage: { + tintColor: LogBoxStyle.getTextColor(1), + }, +}); + +const styles = StyleSheet.create({ + root: { + backgroundColor: LogBoxStyle.getWarningColor(1), + }, + header: { + flexDirection: 'row', + height: Platform.select({ + android: 48, + ios: 44, + }), + }, + title: { + alignItems: 'center', + flex: 1, + justifyContent: 'center', + }, + titleText: { + color: LogBoxStyle.getTextColor(1), + fontSize: 16, + fontWeight: '600', + includeFontPadding: false, + lineHeight: 20, + }, +}); + +export default LogBoxInspectorHeader; diff --git a/Libraries/LogBox/UI/LogBoxInspectorMessageHeader.js b/Libraries/LogBox/UI/LogBoxInspectorMessageHeader.js new file mode 100644 index 0000000000..8f7b826355 --- /dev/null +++ b/Libraries/LogBox/UI/LogBoxInspectorMessageHeader.js @@ -0,0 +1,117 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + */ + +'use strict'; + +import * as React from 'react'; +import StyleSheet from '../../StyleSheet/StyleSheet'; +import Text from '../../Text/Text'; +import View from '../../Components/View/View'; +import LogBoxButton from './LogBoxButton'; +import * as LogBoxStyle from './LogBoxStyle'; +import LogBoxMessage from './LogBoxMessage'; + +import type {Message} from '../Data/LogBoxLogParser'; + +type Props = $ReadOnly<{| + collapsed: boolean, + message: Message, + onPress: () => void, +|}>; + +function LogBoxInspectorMessageHeader(props: Props): React.Node { + function renderShowMore() { + if (props.message.content.length < 140) { + return null; + } + return ( + props.onPress()}> + + {props.collapsed ? 'see more' : 'collapse'} + + + ); + } + + return ( + + + Warning + {renderShowMore()} + + + + + + ); +} + +const messageStyles = StyleSheet.create({ + body: { + backgroundColor: LogBoxStyle.getBackgroundColor(1), + shadowColor: '#000', + shadowOffset: {width: 0, height: 2}, + shadowRadius: 2, + shadowOpacity: 0.5, + elevation: 2, + flex: 0, + }, + bodyText: { + color: LogBoxStyle.getTextColor(1), + fontSize: 14, + includeFontPadding: false, + lineHeight: 20, + fontWeight: '500', + paddingHorizontal: 12, + paddingBottom: 10, + }, + heading: { + alignItems: 'center', + flexDirection: 'row', + paddingHorizontal: 12, + marginTop: 10, + marginBottom: 5, + }, + headingText: { + color: LogBoxStyle.getWarningColor(1), + flex: 1, + fontSize: 20, + fontWeight: '600', + includeFontPadding: false, + lineHeight: 28, + }, + messageText: { + color: LogBoxStyle.getTextColor(0.6), + }, + collapse: { + color: LogBoxStyle.getTextColor(0.7), + fontSize: 12, + fontWeight: '300', + lineHeight: 12, + }, + button: { + paddingVertical: 5, + paddingHorizontal: 10, + borderRadius: 3, + }, +}); + +export default LogBoxInspectorMessageHeader; diff --git a/Libraries/LogBox/UI/LogBoxInspectorMeta.js b/Libraries/LogBox/UI/LogBoxInspectorMeta.js new file mode 100644 index 0000000000..000a9e9024 --- /dev/null +++ b/Libraries/LogBox/UI/LogBoxInspectorMeta.js @@ -0,0 +1,80 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + */ + +'use strict'; + +import * as React from 'react'; +import StyleSheet from '../../StyleSheet/StyleSheet'; +import Text from '../../Text/Text'; +import View from '../../Components/View/View'; +import * as LogBoxStyle from './LogBoxStyle'; + +type Props = $ReadOnly<{||}>; + +function LogBoxInspectorMeta(props: Props): React.Node { + return ( + + + Meta + + + + Engine + + + {/* TODO: Determine engine correctly */} + + {global.HermesInternal ? 'Hermes' : 'Unknown'} + + + + + ); +} + +const metaStyles = StyleSheet.create({ + section: { + marginTop: 15, + }, + heading: { + alignItems: 'center', + flexDirection: 'row', + paddingHorizontal: 12, + marginBottom: 10, + }, + headingText: { + color: LogBoxStyle.getTextColor(1), + flex: 1, + fontSize: 20, + fontWeight: '600', + includeFontPadding: false, + lineHeight: 20, + }, + body: { + paddingLeft: 25, + paddingRight: 25, + paddingBottom: 100, + flexDirection: 'row', + justifyContent: 'space-between', + }, + bodyItem: { + flex: 0, + }, + bodyText: { + color: LogBoxStyle.getTextColor(1), + fontSize: 14, + includeFontPadding: false, + lineHeight: 20, + flex: 0, + flexGrow: 0, + }, +}); + +export default LogBoxInspectorMeta; diff --git a/Libraries/LogBox/UI/LogBoxInspectorReactFrames.js b/Libraries/LogBox/UI/LogBoxInspectorReactFrames.js new file mode 100644 index 0000000000..f857ed1d2a --- /dev/null +++ b/Libraries/LogBox/UI/LogBoxInspectorReactFrames.js @@ -0,0 +1,134 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + */ + +'use strict'; + +import * as React from 'react'; +import StyleSheet from '../../StyleSheet/StyleSheet'; +import Text from '../../Text/Text'; +import View from '../../Components/View/View'; +import LogBoxButton from './LogBoxButton'; +import * as LogBoxStyle from './LogBoxStyle'; + +import type LogBoxLog from '../Data/LogBoxLog'; + +type Props = $ReadOnly<{| + log: LogBoxLog, +|}>; + +function LogBoxInspectorReactFrames(props: Props): React.Node { + const [collapsed, setCollapsed] = React.useState(true); + if (props.log.componentStack == null || props.log.componentStack.length < 1) { + return null; + } + + function getStackList() { + if (collapsed) { + return props.log.componentStack.slice(0, 3); + } else { + return props.log.componentStack; + } + } + + function getCollapseMessage() { + const count = props.log.componentStack.length - 3; + if (collapsed) { + return `See ${count} more components`; + } else { + return `Collapse ${count} components`; + } + } + + return ( + + + React + + + {getStackList().map((frame, index) => ( + + {frame.component} + {frame.location} + + ))} + setCollapsed(!collapsed)}> + {getCollapseMessage()} + + + + ); +} + +const componentStyles = StyleSheet.create({ + section: { + marginTop: 15, + }, + heading: { + alignItems: 'center', + flexDirection: 'row', + paddingHorizontal: 12, + marginBottom: 10, + }, + headingText: { + color: LogBoxStyle.getTextColor(1), + flex: 1, + fontSize: 20, + fontWeight: '600', + includeFontPadding: false, + lineHeight: 20, + }, + body: { + paddingBottom: 10, + }, + bodyText: { + color: LogBoxStyle.getTextColor(1), + fontSize: 14, + includeFontPadding: false, + lineHeight: 18, + fontWeight: '500', + }, + collapse: { + color: LogBoxStyle.getTextColor(0.7), + fontSize: 12, + fontWeight: '300', + lineHeight: 20, + marginLeft: 25, + marginTop: 0, + paddingVertical: 5, + }, + frame: { + paddingHorizontal: 25, + paddingVertical: 4, + }, + frameName: { + color: LogBoxStyle.getTextColor(1), + fontSize: 14, + includeFontPadding: false, + lineHeight: 18, + }, + frameLocation: { + color: LogBoxStyle.getTextColor(0.7), + fontSize: 12, + fontWeight: '300', + includeFontPadding: false, + lineHeight: 16, + paddingLeft: 10, + }, +}); + +export default LogBoxInspectorReactFrames; diff --git a/Libraries/LogBox/UI/LogBoxInspectorSourceMapStatus.js b/Libraries/LogBox/UI/LogBoxInspectorSourceMapStatus.js new file mode 100644 index 0000000000..db2fd9adea --- /dev/null +++ b/Libraries/LogBox/UI/LogBoxInspectorSourceMapStatus.js @@ -0,0 +1,154 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + */ + +'use strict'; + +import Animated from '../../Animated/src/Animated'; +import Easing from '../../Animated/src/Easing'; +import * as React from 'react'; +import StyleSheet from '../../StyleSheet/StyleSheet'; +import Text from '../../Text/Text'; +import LogBoxImageSource from './LogBoxImageSource'; +import LogBoxButton from './LogBoxButton'; +import * as LogBoxStyle from './LogBoxStyle'; + +import type {CompositeAnimation} from '../../Animated/src/AnimatedImplementation'; +import type AnimatedInterpolation from '../../Animated/src/nodes/AnimatedInterpolation'; +import type {PressEvent} from '../../Types/CoreEventTypes'; + +type Props = $ReadOnly<{| + onPress?: ?(event: PressEvent) => void, + status: 'COMPLETE' | 'FAILED' | 'NONE' | 'PENDING', +|}>; + +type State = {| + animation: ?CompositeAnimation, + rotate: ?AnimatedInterpolation, +|}; + +class LogBoxInspectorSourceMapStatus extends React.Component { + state: State = { + animation: null, + rotate: null, + }; + + render(): React.Node { + let image; + let color; + switch (this.props.status) { + case 'COMPLETE': + image = LogBoxImageSource.check; + color = LogBoxStyle.getTextColor(0.4); + break; + case 'FAILED': + image = LogBoxImageSource.alertTriangle; + color = LogBoxStyle.getErrorColor(1); + break; + case 'PENDING': + image = LogBoxImageSource.loader; + color = LogBoxStyle.getWarningColor(1); + break; + } + + return image == null ? null : ( + + + Source Map + + ); + } + + componentDidMount(): void { + this._updateAnimation(); + } + + componentDidUpdate(): void { + this._updateAnimation(); + } + + componentWillUnmount(): void { + if (this.state.animation != null) { + this.state.animation.stop(); + } + } + + _updateAnimation(): void { + if (this.props.status === 'PENDING') { + if (this.state.animation == null) { + const animated = new Animated.Value(0); + const animation = Animated.loop( + Animated.timing(animated, { + duration: 2000, + easing: Easing.linear, + toValue: 1, + useNativeDriver: true, + }), + ); + this.setState( + { + animation, + rotate: animated.interpolate({ + inputRange: [0, 1], + outputRange: ['0deg', '360deg'], + }), + }, + () => { + animation.start(); + }, + ); + } + } else { + if (this.state.animation != null) { + this.state.animation.stop(); + this.setState({ + animation: null, + rotate: null, + }); + } + } + } +} + +const styles = StyleSheet.create({ + root: { + alignItems: 'center', + borderRadius: 12, + flexDirection: 'row', + height: 24, + paddingHorizontal: 8, + }, + image: { + marginEnd: 4, + tintColor: LogBoxStyle.getTextColor(0.4), + }, + text: { + fontSize: 12, + includeFontPadding: false, + lineHeight: 16, + }, +}); + +export default LogBoxInspectorSourceMapStatus; diff --git a/Libraries/LogBox/UI/LogBoxInspectorStackFrame.js b/Libraries/LogBox/UI/LogBoxInspectorStackFrame.js new file mode 100644 index 0000000000..d9fec6335e --- /dev/null +++ b/Libraries/LogBox/UI/LogBoxInspectorStackFrame.js @@ -0,0 +1,94 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + */ + +'use strict'; + +import * as React from 'react'; +import StyleSheet from '../../StyleSheet/StyleSheet'; +import Text from '../../Text/Text'; +import LogBoxButton from './LogBoxButton'; +import * as LogBoxStyle from './LogBoxStyle'; + +import type {PressEvent} from '../../Types/CoreEventTypes'; +import type {StackFrame} from '../../Core/NativeExceptionsManager'; + +type Props = $ReadOnly<{| + frame: StackFrame, + onPress?: ?(event: PressEvent) => void, +|}>; + +function LogBoxInspectorStackFrame(props: Props): React.Node { + const {frame, onPress} = props; + + return ( + + {frame.methodName} + + {formatFrameLocation(frame)} + + + ); +} + +function formatFrameLocation(frame: StackFrame): string { + const {file, lineNumber, column} = frame; + if (file == null) { + return ''; + } + const queryIndex = file.indexOf('?'); + const query = queryIndex < 0 ? '' : file.substr(queryIndex); + + const path = queryIndex < 0 ? file : file.substr(0, queryIndex); + let location = path.substr(path.lastIndexOf('/') + 1) + query; + + if (lineNumber == null) { + return location; + } + + location = location + ':' + lineNumber; + + if (column == null) { + return location; + } + + return location + ':' + column; +} + +const styles = StyleSheet.create({ + frame: { + paddingHorizontal: 25, + paddingVertical: 4, + }, + frameName: { + color: LogBoxStyle.getTextColor(1), + fontSize: 14, + includeFontPadding: false, + lineHeight: 18, + }, + frameLocation: { + color: LogBoxStyle.getTextColor(0.7), + fontSize: 12, + fontWeight: '300', + includeFontPadding: false, + lineHeight: 16, + paddingLeft: 10, + }, +}); + +export default LogBoxInspectorStackFrame; diff --git a/Libraries/LogBox/UI/LogBoxInspectorStackFrames.js b/Libraries/LogBox/UI/LogBoxInspectorStackFrames.js new file mode 100644 index 0000000000..16fbe3329d --- /dev/null +++ b/Libraries/LogBox/UI/LogBoxInspectorStackFrames.js @@ -0,0 +1,169 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + */ + +'use strict'; + +import * as React from 'react'; +import StyleSheet from '../../StyleSheet/StyleSheet'; +import Text from '../../Text/Text'; +import View from '../../Components/View/View'; +import LogBoxButton from './LogBoxButton'; +import LogBoxInspectorSourceMapStatus from './LogBoxInspectorSourceMapStatus'; +import LogBoxInspectorStackFrame from './LogBoxInspectorStackFrame'; +import * as LogBoxStyle from './LogBoxStyle'; +import openFileInEditor from '../../Core/Devtools/openFileInEditor'; + +import type LogBoxLog from '../Data/LogBoxLog'; + +type Props = $ReadOnly<{| + log: LogBoxLog, + onRetry: () => void, +|}>; + +function LogBoxInspectorStackFrames(props: Props): React.Node { + const [collapsed, setCollapsed] = React.useState(true); + + function getStackList() { + if (collapsed === true) { + return props.log.getAvailableStack().filter(({collapse}) => !collapse); + } else { + return props.log.getAvailableStack(); + } + } + + function getCollapseMessage() { + const stackFrames = props.log.getAvailableStack(); + const collapsedCount = stackFrames.reduce((count, {collapse}) => { + if (collapse !== true) { + return count + 1; + } + + return count; + }, 0); + + if (collapsed) { + return `See ${collapsedCount} more frames`; + } else { + return `Collapse ${collapsedCount} frames`; + } + } + return ( + + + + + setCollapsed(!collapsed)} + message={getCollapseMessage()} + /> + + + ); +} + +function StackFrameHeader(props) { + return ( + + Stack + + + ); +} + +function StackFrameList(props) { + return ( + <> + {props.list.map((frame, index) => { + const {file, lineNumber} = frame; + return ( + { + if ( + props.status === 'COMPLETE' && + file != null && + lineNumber != null + ) { + openFileInEditor(file, lineNumber); + } + }} + /> + ); + })} + + ); +} + +function StackFrameFooter(props) { + return ( + + + {props.message} + + + ); +} + +const stackStyles = StyleSheet.create({ + section: { + marginTop: 15, + }, + heading: { + alignItems: 'center', + flexDirection: 'row', + paddingHorizontal: 12, + marginBottom: 10, + }, + headingText: { + color: LogBoxStyle.getTextColor(1), + flex: 1, + fontSize: 20, + fontWeight: '600', + includeFontPadding: false, + lineHeight: 20, + }, + body: { + paddingBottom: 10, + }, + bodyText: { + color: LogBoxStyle.getTextColor(1), + fontSize: 14, + includeFontPadding: false, + lineHeight: 18, + fontWeight: '500', + paddingHorizontal: 27, + }, + collapse: { + color: LogBoxStyle.getTextColor(0.7), + fontSize: 12, + fontWeight: '300', + lineHeight: 20, + marginLeft: 25, + marginTop: 0, + paddingVertical: 5, + }, +}); + +export default LogBoxInspectorStackFrames; diff --git a/Libraries/LogBox/UI/LogBoxLogNotification.js b/Libraries/LogBox/UI/LogBoxLogNotification.js new file mode 100644 index 0000000000..3ef3b9f1de --- /dev/null +++ b/Libraries/LogBox/UI/LogBoxLogNotification.js @@ -0,0 +1,234 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + */ + +'use strict'; + +import * as React from 'react'; +import Image from '../../Image/Image'; +import LogBoxImageSource from './LogBoxImageSource'; +import StyleSheet from '../../StyleSheet/StyleSheet'; +import Text from '../../Text/Text'; +import View from '../../Components/View/View'; +import LogBoxButton from './LogBoxButton'; +import * as LogBoxStyle from './LogBoxStyle'; +import LogBoxLog from '../Data/LogBoxLog'; +import LogBoxMessage from './LogBoxMessage'; + +type Props = $ReadOnly<{| + log: LogBoxLog, + onPressOpen: (index: number) => void, + onPressList: () => void, + onPressDismiss: () => void, +|}>; + +class LogBoxLogNotification extends React.Component { + static GUTTER: number = StyleSheet.hairlineWidth; + static HEIGHT: number = 48; + + shouldComponentUpdate(nextProps: Props): boolean { + const prevProps = this.props; + return ( + prevProps.onPressOpen !== nextProps.onPressOpen || + prevProps.onPressList !== nextProps.onPressList || + prevProps.onPressDismiss !== nextProps.onPressDismiss || + prevProps.log !== nextProps.log + ); + } + + _handlePressOpen = () => { + this.props.onPressOpen(0); + }; + + _handlePressList = () => { + this.props.onPressList(); + }; + + _handlePressDismiss = () => { + this.props.onPressDismiss(); + }; + + render(): React.Node { + const {log} = this.props; + + return ( + + + + + + + + + + ); + } +} + +function CountBadge(props) { + return ( + + + + {props.count <= 1 ? '!' : props.count} + + + + ); +} + +function Message(props) { + return ( + + + {props.message && ( + + )} + + + ); +} + +function DismissButton(props) { + return ( + + + + + + ); +} + +const countStyles = StyleSheet.create({ + warn: { + backgroundColor: LogBoxStyle.getWarningColor(1), + }, + error: { + backgroundColor: LogBoxStyle.getErrorColor(1), + }, + log: { + backgroundColor: LogBoxStyle.getLogColor(1), + }, + outside: { + padding: 2, + borderRadius: 25, + backgroundColor: '#fff', + marginRight: 8, + }, + inside: { + minWidth: 18, + paddingLeft: 4, + paddingRight: 4, + borderRadius: 25, + fontWeight: '600', + }, + text: { + color: LogBoxStyle.getTextColor(1), + fontSize: 14, + includeFontPadding: false, + lineHeight: 18, + textAlign: 'center', + fontWeight: '600', + }, +}); + +const messageStyles = StyleSheet.create({ + container: { + alignSelf: 'stretch', + flexGrow: 1, + flexShrink: 1, + flexBasis: 'auto', + borderLeftColor: LogBoxStyle.getTextColor(0.2), + borderLeftWidth: 1, + paddingLeft: 8, + }, + text: { + color: LogBoxStyle.getTextColor(1), + flex: 1, + fontSize: 14, + includeFontPadding: false, + lineHeight: 22, + }, + substitutionText: { + color: LogBoxStyle.getTextColor(0.6), + }, +}); + +const dismissStyles = StyleSheet.create({ + container: { + alignSelf: 'center', + flexDirection: 'row', + flexGrow: 0, + flexShrink: 0, + flexBasis: 'auto', + marginLeft: 5, + }, + press: { + height: 20, + width: 20, + borderRadius: 25, + alignSelf: 'flex-end', + alignItems: 'center', + justifyContent: 'center', + }, + image: { + tintColor: LogBoxStyle.getBackgroundColor(1), + }, +}); + +const toastStyles = StyleSheet.create({ + container: { + height: LogBoxLogNotification.HEIGHT, + position: 'relative', + width: '100%', + justifyContent: 'center', + marginTop: LogBoxLogNotification.GUTTER, + backgroundColor: LogBoxStyle.getTextColor(1), + }, + press: { + height: LogBoxLogNotification.HEIGHT, + position: 'relative', + width: '100%', + justifyContent: 'center', + marginTop: LogBoxLogNotification.GUTTER, + paddingHorizontal: 12, + }, + content: { + alignItems: 'flex-start', + flexDirection: 'row', + borderRadius: 8, + flexGrow: 0, + flexShrink: 0, + flexBasis: 'auto', + }, +}); + +export default LogBoxLogNotification; diff --git a/Libraries/LogBox/UI/LogBoxMessage.js b/Libraries/LogBox/UI/LogBoxMessage.js new file mode 100644 index 0000000000..72a331cf86 --- /dev/null +++ b/Libraries/LogBox/UI/LogBoxMessage.js @@ -0,0 +1,59 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + */ + +'use strict'; + +import * as React from 'react'; +import Text from '../../Text/Text'; + +import type {TextStyleProp} from '../../StyleSheet/StyleSheet'; +import type {Message} from '../Data/LogBoxLogParser'; + +type Props = { + message: Message, + style: TextStyleProp, +}; + +function LogBoxMessage(props: Props): React.Node { + const {content, substitutions}: Message = props.message; + const substitutionStyle: TextStyleProp = props.style; + const elements = []; + + const lastOffset = substitutions.reduce((prevOffset, substitution, index) => { + const key = String(index); + + if (substitution.offset > prevOffset) { + const prevPart = content + .substr(prevOffset, substitution.offset - prevOffset) + .replace('Warning: ', ''); + elements.push({prevPart}); + } + + const substititionPart = content + .substr(substitution.offset, substitution.length) + .replace('Warning: ', ''); + elements.push( + + {substititionPart} + , + ); + + return substitution.offset + substitution.length; + }, 0); + + if (lastOffset < content.length) { + const lastPart = content.substr(lastOffset).replace('Warning: ', ''); + elements.push({lastPart}); + } + + return <>{elements}; +} + +export default LogBoxMessage; diff --git a/Libraries/LogBox/UI/LogBoxStyle.js b/Libraries/LogBox/UI/LogBoxStyle.js new file mode 100644 index 0000000000..6c17e876ee --- /dev/null +++ b/Libraries/LogBox/UI/LogBoxStyle.js @@ -0,0 +1,55 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict + * @format + */ + +'use strict'; + +export function getBackgroundColor(opacity?: number): string { + return `rgba(51, 51, 51, ${opacity == null ? 1 : opacity})`; +} + +export function getBackgroundLightColor(opacity?: number): string { + return `rgba(69, 69, 69, ${opacity == null ? 1 : opacity})`; +} + +export function getBackgroundDarkColor(opacity?: number): string { + return `rgba(34, 34, 34, ${opacity == null ? 1 : opacity})`; +} + +export function getWarningColor(opacity?: number): string { + return `rgba(250, 186, 48, ${opacity == null ? 1 : opacity})`; +} + +export function getWarningDarkColor(opacity?: number): string { + return `rgba(224, 167, 8, ${opacity == null ? 1 : opacity})`; +} + +export function getErrorColor(opacity?: number): string { + return `rgba(243, 83, 105, ${opacity == null ? 1 : opacity})`; +} + +export function getLogColor(opacity?: number): string { + return `rgba(119, 119, 119, ${opacity == null ? 1 : opacity})`; +} + +export function getWarningHighlightColor(opacity?: number): string { + return `rgba(252, 176, 29, ${opacity == null ? 1 : opacity})`; +} + +export function getDividerColor(opacity?: number): string { + return `rgba(255, 255, 255, ${opacity == null ? 1 : opacity})`; +} + +export function getHighlightColor(opacity?: number): string { + return `rgba(252, 176, 29, ${opacity == null ? 1 : opacity})`; +} + +export function getTextColor(opacity?: number): string { + return `rgba(255, 255, 255, ${opacity == null ? 1 : opacity})`; +}