From 89fde1f8ad32bba32a0d4d2a07ad094a4d01cf02 Mon Sep 17 00:00:00 2001 From: Vitali Zaidman Date: Mon, 22 Sep 2025 09:57:32 -0700 Subject: [PATCH 1/4] Align Android with iOS in displaying HMR "refreshing" in color (#53846) Summary: Changelog: [Android][Added] - hot reload banner is now displayed in blue background like on iOS iOS has a colorful "Refreshing..." banner: {F1982086204} While android doesn't respect the color HMR asks it to set up for the banner: {F1982086218} Reviewed By: cortinico Differential Revision: D82726743 --- .../DefaultDevLoadingViewImplementation.kt | 14 ++++++++++++-- .../devsupport/interfaces/DevLoadingViewManager.kt | 2 ++ .../react/modules/devloading/DevLoadingModule.kt | 4 +++- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/DefaultDevLoadingViewImplementation.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/DefaultDevLoadingViewImplementation.kt index 7e06ff82c4ce..330c2e89acde 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/DefaultDevLoadingViewImplementation.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/DefaultDevLoadingViewImplementation.kt @@ -33,10 +33,14 @@ public class DefaultDevLoadingViewImplementation( private var devLoadingPopup: PopupWindow? = null override fun showMessage(message: String) { + showMessage(message, color = null, backgroundColor = null) + } + + override fun showMessage(message: String, color: Double?, backgroundColor: Double?) { if (!isEnabled) { return } - UiThreadUtil.runOnUiThread { showInternal(message) } + UiThreadUtil.runOnUiThread { showInternal(message, color, backgroundColor) } } override fun updateProgress(status: String?, done: Int?, total: Int?) { @@ -59,7 +63,7 @@ public class DefaultDevLoadingViewImplementation( } } - private fun showInternal(message: String) { + private fun showInternal(message: String, color: Double?, backgroundColor: Double?) { if (devLoadingPopup?.isShowing == true) { // already showing return @@ -84,6 +88,12 @@ public class DefaultDevLoadingViewImplementation( currentActivity.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater val view = inflater.inflate(R.layout.dev_loading_view, null) as TextView view.text = message + if (color != null) { + view.setTextColor(color.toInt()) + } + if (backgroundColor != null) { + view.setBackgroundColor(backgroundColor.toInt()) + } val popup = PopupWindow( view, diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/interfaces/DevLoadingViewManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/interfaces/DevLoadingViewManager.kt index 08d4757a4c5b..9366cfb3b300 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/interfaces/DevLoadingViewManager.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/interfaces/DevLoadingViewManager.kt @@ -11,6 +11,8 @@ package com.facebook.react.devsupport.interfaces public interface DevLoadingViewManager { public fun showMessage(message: String) + public fun showMessage(message: String, color: Double?, backgroundColor: Double?) + public fun updateProgress(status: String?, done: Int?, total: Int?) public fun hide() diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/devloading/DevLoadingModule.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/devloading/DevLoadingModule.kt index 7344fcdad8dd..78a3a84146d9 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/devloading/DevLoadingModule.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/modules/devloading/DevLoadingModule.kt @@ -31,7 +31,9 @@ internal class DevLoadingModule(reactContext: ReactApplicationContext) : } override fun showMessage(message: String, color: Double?, backgroundColor: Double?) { - UiThreadUtil.runOnUiThread { devLoadingViewManager?.showMessage(message) } + UiThreadUtil.runOnUiThread { + devLoadingViewManager?.showMessage(message, color, backgroundColor) + } } override fun hide() { From cef4e614cbc1d1a60ef145dda92e326b9c2e93e6 Mon Sep 17 00:00:00 2001 From: Vitali Zaidman Date: Mon, 22 Sep 2025 09:57:32 -0700 Subject: [PATCH 2/4] add disconnection message (#53883) Summary: Changelog: [General][Added] - new banner added to indicate that Fast Refresh has lost connection, and that the app needs to be restarted to reconnect When HMR were getting disconnected, we were not giving any indication for the user. This lead to situations were the connection is re-established, but HMR is still disconnected, which is unexpected for users. Instead, raise a banner hinting the user to reload the app to reconnect. Reviewed By: huntie Differential Revision: D82726853 --- .../Libraries/Utilities/DevLoadingView.js | 20 +++++++++++++------ .../Libraries/Utilities/HMRClient.js | 11 ++++++---- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/packages/react-native/Libraries/Utilities/DevLoadingView.js b/packages/react-native/Libraries/Utilities/DevLoadingView.js index f43bb86b2969..338a5614df35 100644 --- a/packages/react-native/Libraries/Utilities/DevLoadingView.js +++ b/packages/react-native/Libraries/Utilities/DevLoadingView.js @@ -14,29 +14,37 @@ import NativeDevLoadingView from './NativeDevLoadingView'; const COLOR_SCHEME = { dark: { + load: { + backgroundColor: '#fafafa', + textColor: '#242526', + }, refresh: { backgroundColor: '#2584e8', textColor: '#ffffff', }, - load: { - backgroundColor: '#fafafa', - textColor: '#242526', + error: { + backgroundColor: '#1065AF', + textColor: '#ffffff', }, }, default: { + load: { + backgroundColor: '#404040', + textColor: '#ffffff', + }, refresh: { backgroundColor: '#2584e8', textColor: '#ffffff', }, - load: { - backgroundColor: '#404040', + error: { + backgroundColor: '#1065AF', textColor: '#ffffff', }, }, }; export default { - showMessage(message: string, type: 'load' | 'refresh') { + showMessage(message: string, type: 'load' | 'refresh' | 'error') { if (NativeDevLoadingView) { const colorScheme = getColorScheme() === 'dark' ? COLOR_SCHEME.dark : COLOR_SCHEME.default; diff --git a/packages/react-native/Libraries/Utilities/HMRClient.js b/packages/react-native/Libraries/Utilities/HMRClient.js index 6d16b6b973f3..f466f7a7f99a 100644 --- a/packages/react-native/Libraries/Utilities/HMRClient.js +++ b/packages/react-native/Libraries/Utilities/HMRClient.js @@ -232,8 +232,6 @@ Error: ${e.message}`; }); client.on('error', data => { - DevLoadingView.hide(); - if (data.type === 'GraphNotFoundError') { client.close(); setHMRUnavailableReason( @@ -253,8 +251,6 @@ Error: ${e.message}`; }); client.on('close', closeEvent => { - DevLoadingView.hide(); - // https://www.rfc-editor.org/rfc/rfc6455.html#section-7.4.1 // https://www.rfc-editor.org/rfc/rfc6455.html#section-7.1.5 const isNormalOrUnsetCloseReason = @@ -296,10 +292,17 @@ function setHMRUnavailableReason(reason: string) { } hmrUnavailableReason = reason; + const DevLoadingView = require('./DevLoadingView').default; + DevLoadingView.hide(); + // We only want to show a warning if Fast Refresh is on *and* if we ever // previously managed to connect successfully. We don't want to show // the warning to native engineers who use cached bundles without Metro. if (hmrClient.isEnabled() && didConnect) { + DevLoadingView.showMessage( + 'Fast Refresh disconnected. Reload app to reconnect.', + 'error', + ); console.warn(reason); // (Not using the `warning` module to prevent a Buck cycle.) } From 0f5e74aedf8ba80aeb1b72a359e16045f294b734 Mon Sep 17 00:00:00 2001 From: Vitali Zaidman Date: Mon, 22 Sep 2025 09:57:32 -0700 Subject: [PATCH 3/4] remove connection hints from being logged for the user (#53884) Summary: Changelog: [Internal] Now that there's a clear display for when HMR is disconnected, we don't want to show messages regardless devices connection and disconnection to the DevServer because they are too partial. Reviewed By: huntie Differential Revision: D82727105 --- .../src/inspector-proxy/InspectorProxy.js | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/packages/dev-middleware/src/inspector-proxy/InspectorProxy.js b/packages/dev-middleware/src/inspector-proxy/InspectorProxy.js index 23df229f056a..fd82442f2366 100644 --- a/packages/dev-middleware/src/inspector-proxy/InspectorProxy.js +++ b/packages/dev-middleware/src/inspector-proxy/InspectorProxy.js @@ -372,12 +372,6 @@ export default class InspectorProxy implements InspectorProxyQueries { this.#devices.set(deviceId, newDevice); - this.#logger?.info( - "Connection established to app='%s' on device='%s'.", - appName, - deviceName, - ); - debug( "Got new device connection: name='%s', app=%s, device=%s, via=%s", deviceName, @@ -450,7 +444,7 @@ export default class InspectorProxy implements InspectorProxyQueries { ); socket.on('close', (code: number, reason: string) => { - this.#logger?.info( + debug( "Connection closed to device='%s' for app='%s' with code='%s' and reason='%s'.", deviceName, appName, @@ -526,7 +520,7 @@ export default class InspectorProxy implements InspectorProxyQueries { throw new Error(INTERNAL_ERROR_MESSAGES.UNREGISTERED_DEVICE); } - this.#logger?.info( + debug( "Connection established to DevTools for app='%s' on device='%s'.", device.getApp() || 'unknown', device.getName() || 'unknown', @@ -594,7 +588,7 @@ export default class InspectorProxy implements InspectorProxyQueries { }); socket.on('close', (code: number, reason: string) => { - this.#logger?.info( + debug( "Connection closed to DevTools for app='%s' on device='%s' with code='%s' and reason='%s'.", device.getApp() || 'unknown', device.getName() || 'unknown', From 240eaa3c51a1b14510b4f1a47ad3650bbb66ba32 Mon Sep 17 00:00:00 2001 From: Vitali Zaidman Date: Mon, 22 Sep 2025 09:57:32 -0700 Subject: [PATCH 4/4] Dismiss DevLoadingView toast on touch Summary: Changelog: [General][Added] DevServer banners can now be closed on tap. On iOS, don't close them after 15s anymore. Banners, on iOS, but not Android, are currently closed after 15s since {D47478373} where it was introduced to deal with the banner telling users they need to connect to Metro stuck forever. Instead of closing it after 15 seconds, allow the users on both Android and iOS to close it via a touch. Reviewed By: huntie Differential Revision: D82727997 --- .../React/CoreModules/RCTDevLoadingView.mm | 13 +++---------- .../DefaultDevLoadingViewImplementation.kt | 2 +- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/packages/react-native/React/CoreModules/RCTDevLoadingView.mm b/packages/react-native/React/CoreModules/RCTDevLoadingView.mm index 3022f42447c0..3d866e91a40d 100644 --- a/packages/react-native/React/CoreModules/RCTDevLoadingView.mm +++ b/packages/react-native/React/CoreModules/RCTDevLoadingView.mm @@ -85,14 +85,6 @@ - (void)showInitialMessageDelayed:(void (^)())initialMessage dispatch_time(DISPATCH_TIME_NOW, 0.2 * NSEC_PER_SEC), dispatch_get_main_queue(), self->_initialMessageBlock); } -- (void)hideBannerAfter:(CGFloat)delay -{ - // Cancel previous hide call after the delay. - [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(hide) object:nil]; - // Set new hide call after a delay. - [self performSelector:@selector(hide) withObject:nil afterDelay:delay]; -} - - (void)showMessage:(NSString *)message color:(UIColor *)color backgroundColor:(UIColor *)backgroundColor { if (!RCTDevLoadingViewGetEnabled() || _hiding) { @@ -128,6 +120,9 @@ - (void)showMessage:(NSString *)message color:(UIColor *)color backgroundColor:( self->_container = [[UIView alloc] init]; self->_container.backgroundColor = backgroundColor; self->_container.translatesAutoresizingMaskIntoConstraints = NO; + UITapGestureRecognizer *tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(hide)]; + [self->_container addGestureRecognizer:tapGesture]; + self->_container.userInteractionEnabled = YES; self->_label = [[UILabel alloc] init]; self->_label.translatesAutoresizingMaskIntoConstraints = NO; @@ -158,8 +153,6 @@ - (void)showMessage:(NSString *)message color:(UIColor *)color backgroundColor:( [self->_label.centerXAnchor constraintEqualToAnchor:self->_container.centerXAnchor], [self->_label.bottomAnchor constraintEqualToAnchor:self->_container.bottomAnchor constant:-5], ]]; - - [self hideBannerAfter:15.0]; }); } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/DefaultDevLoadingViewImplementation.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/DefaultDevLoadingViewImplementation.kt index 330c2e89acde..ff366e685cf8 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/DefaultDevLoadingViewImplementation.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/devsupport/DefaultDevLoadingViewImplementation.kt @@ -94,13 +94,13 @@ public class DefaultDevLoadingViewImplementation( if (backgroundColor != null) { view.setBackgroundColor(backgroundColor.toInt()) } + view.setOnClickListener { hideInternal() } val popup = PopupWindow( view, ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT, ) - popup.isTouchable = false popup.showAtLocation(currentActivity.window.decorView, Gravity.NO_GRAVITY, 0, topOffset) devLoadingView = view devLoadingPopup = popup