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})`; +}